Working better, but even the basic sedit functionality isn't right.

This commit is contained in:
simon 2017-06-29 15:29:48 +01:00
parent 04619ed02b
commit 3b90c105d4
3 changed files with 170 additions and 106 deletions

View file

@ -4,16 +4,26 @@ An in-core editor for Clojure data structures and, particularly, functions.
## Usage
Preliminary, incomplete, alpha quality code. This implements a structure editor in a terminal,
not a display editor in the tradition of InterLisp's DEdit. I do intend to follow up with a
Preliminary, incomplete, alpha quality code. This implements a structure editor in a terminal,
not a display editor in the tradition of InterLisp's DEdit. I do intend to follow up with a
display editor, but this is exploratory proof-of-concept code.
Note that to work with this, you need to start with
lein trampoline repl
rather than just
lein repl
I've read explanations of why this is, but I don't claim to fully understand them. Treat it as magic,
but trust me on this.
To edit an arbitrary s-expression:
(sedit sexpr)
This pretty much works now; it returns an edited copy of the s-expression. Vectors are not handled
intelligently (but could be).
This pretty much works now; it returns an edited copy of the s-expression. Maps are not yet handled.
To edit a function definition
@ -28,85 +38,85 @@ NOTE: This is broken, see working notes below; but it is showing promise.
Currently, Clojure metadata on a function symbol as follows:
{
:arglists ([sexpr]),
:ns #<Namespace fedit.core>,
:name sedit, :column 1,
:doc "Edit an S-Expression, and return a modified version of it",
:line 74,
:arglists ([sexpr]),
:ns #<Namespace fedit.core>,
:name sedit, :column 1,
:doc "Edit an S-Expression, and return a modified version of it",
:line 74,
:file "fedit/core.clj"
}
In order to be able to recover the source of a function which has not yet been committed to the file
system, it would be necessary to store the source s-expression on the metadata. You cannot add new
system, it would be necessary to store the source s-expression on the metadata. You cannot add new
metadata to an existing symbol (? check this), but, again, as the package reloader effectively does
so, there must be a way, although it may be dark and mysterious. Obviously if we're smashing and
rebinding the function's compiled form we're doing something dark and mysterious anyway.
### Generating/persisting packages
Editing a function which is in an existing package has problems associated with it. We cannot easily
save it back to its original file, as that will throw out the line numbering of every other definition
Editing a function which is in an existing package has problems associated with it. We cannot easily
save it back to its original file, as that will throw out the line numbering of every other definition
in the file. Also, critically, files contain textual comments which are not read by the reader, and
consequently would be smashed by overwriting the old definition with the new definition.
Consequently I'm thinking that a revised package manager for Clojure-with-in-core-editing should
generate new packages with names of the form packagename_serial; that when files are edited in core,
the serial number should be incremented to above the highest existing serial number for that package,
and the new package (with the new serial number) should depend on the next-older version of the
package (obviously, recursively). The function (use 'packagename) should be rewritten so if passed
a package name without a version number part, it would seek the highest numbered version of the
and the new package (with the new serial number) should depend on the next-older version of the
package (obviously, recursively). The function (use 'packagename) should be rewritten so if passed
a package name without a version number part, it would seek the highest numbered version of the
specified package available on the path.
At the end of a Clojure session (or, actually, at any stage within a session) the user could issue
At the end of a Clojure session (or, actually, at any stage within a session) the user could issue
a directive
(persist-edits)
Until this directive had been called, none of the in-core edits which had been made in the session
would be saved. When the directive was made, the persister would go through all functions/symbols
which had been edited during the session, and if they had package metadata would immediately save
them; if they had no package metadata would prompt for it.
Until this directive had been called, none of the in-core edits which had been made in the session
would be saved. When the directive was made, the persister would go through all functions/symbols
which had been edited during the session, and if they had package metadata would immediately save
them; if they had no package metadata would prompt for it.
## Working notes
### 20130919 13:20
The function 'source-fn' in package 'clojure.repl' returns, as a string, the source of the
function (or macro) whose name is passed to it as argument. It does this by checking the metadata
associated with the function object using the 'meta' function. This metadata (if present) is a map
The function 'source-fn' in package 'clojure.repl' returns, as a string, the source of the
function (or macro) whose name is passed to it as argument. It does this by checking the metadata
associated with the function object using the 'meta' function. This metadata (if present) is a map
containing the keys ':file' and ':line'. I'm guessing, therefore, that this metadata is set up while
reading the source file.
The function 'read-string' can be used to parse a string into an S-expression. I'm taking it
The function 'read-string' can be used to parse a string into an S-expression. I'm taking it
as read that the string returned by source-fn will always be a single well-formed S-expression.
As a first pass, I'll write a function sedit which takes an s-expression as argument, pretty prints
As a first pass, I'll write a function sedit which takes an s-expression as argument, pretty prints
it to the screen, and awaits a key stroke from the user. The following keys will be recognised:
* A: ['CAR'] call sedit recursively on the CAR of the current s-expression;
return a cons of the result of this with the cdr of the current s-expression. Obviously, only
* A: ['CAR'] call sedit recursively on the CAR of the current s-expression;
return a cons of the result of this with the cdr of the current s-expression. Obviously, only
available if the current s-expression is a list with at least one element.
* D: ['CDR'] call sedit recursively on the CDR of the current s-expression;
return a cons of the CAR of the current s-expression with the result of this. Obviously, only
available if the current s-expression is a list.
* S: ['Substitute'] read a new s-expression from the user and return it in place of the
* S: ['Substitute'] read a new s-expression from the user and return it in place of the
current s-exression
* X: ['Cut'] return nil.
### 20130920 10:37
Now sort-of working. One change can be made to an s-expression, and it can be made anywhere in the
s-expression. For some reason having made one change you can't then navigate further into the
Now sort-of working. One change can be made to an s-expression, and it can be made anywhere in the
s-expression. For some reason having made one change you can't then navigate further into the
s-expression to make another change; I suspect this is a lazy-evaluation problem, but I haven't yet
fixed it.
Also, the 'clear screen' functionality is *extremely* crude, and you have to type a carriage return
Also, the 'clear screen' functionality is *extremely* crude, and you have to type a carriage return
after every command character, which slows down the user interaction badly. For a proof-of-concept
demonstrator that isn't critical, but if anyone is actually going to use this thing it needs to be
fixed.
I've written a wrapper round sedit called fedit, which grabs the source of a function from its
metadata and passes it to sedit, attempting to redefine the function from the result; this fails for
I've written a wrapper round sedit called fedit, which grabs the source of a function from its
metadata and passes it to sedit, attempting to redefine the function from the result; this fails for
the basic clojure 'all data is immutable' reason. But, when you invoke (use 'namespace :reload), it
is able to redefine all the functions, so I must be able to work out how this is done.

View file

@ -3,5 +3,6 @@
:url "http://example.com/FIXME"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.5.1"]]
:dependencies [[org.clojure/clojure "1.8.0"]
[jline "2.11"]]
:clean-targets ["classes" "bin"])

View file

@ -1,5 +1,8 @@
(ns fedit.core
(:use clojure.repl))
(:require [clojure.repl :refer :all]
[clojure.pprint :refer [pprint]])
(:import [jline.console ConsoleReader]))
(defn clear-terminal
"Clear the terminal screen - should be possible to do this by printing a \f, but
@ -7,87 +10,137 @@
[]
(dotimes [_ 25] (println)))
(defn print-indent
"indent this many spaces and then print this s-expression"
[x spaces]
(dotimes [_ spaces] (print " "))
(println x)
x)
(defn recursively-frob-strings
"Walk this s-expression, replacing strings with quoted strings.
TODO: does not fix strings in vectors"
[sexpr]
(cond
(nil? sexpr) nil
(symbol? sexpr) sexpr
(empty? sexpr) ()
(list? sexpr)(cons (recursively-frob-strings (first sexpr))(recursively-frob-strings (rest sexpr)))
(string? sexpr)(str "\"" sexpr "\"")
true sexpr))
(defn rereadable-print-str
"print-str doesn't produce a re-readable output, because it does not surround
embedded strings with quotation marks. This attempts to fix this problem."
[sexpr]
(let [fixed (recursively-frob-strings sexpr)]
(print-str fixed)))
(defn pretty-print
"Print this s-expression neatly indented.
TODO: Does not yet handle vectors intelligently"
([sexpr] (pretty-print sexpr 0))
([sexpr indent]
(cond
(string? sexpr)
(let [printform (str "\"" sexpr "\"")](print-indent printform indent))
(list? sexpr)
(let [asstring (rereadable-print-str sexpr)]
;; print-str isn't right here because it does not substitute in quotation marks around strings
;; need to write a new function of my own.
(cond
(< (+ indent (count asstring)) 80) (print-indent asstring indent)
true (do
(let [firstline (str "(" (rereadable-print-str (first sexpr)))]
(print-indent firstline indent))
(doall (map (fn [x] (pretty-print x (+ indent 2))) (rest sexpr)))
(print-indent ")" indent))))
true (print-indent sexpr indent))
sexpr))
(defn read-char
"Ultimately this will read a single character, probably requiring some Java hackery; but for now
just read"
[]
(read))
"Read from standard input a single character which is one of these targets return it."
[targets]
(let [cr (ConsoleReader.)
keyint (.readCharacter cr)
key (char keyint)]
(if
(some #(= % key) targets)
key
(recur targets))))
(defn prompt-and-read
(def symbol-menu
{\r "Return"
\s "Substitute"
\x "eXcise"})
(def sequence-menu
{\a "cAr"
\d "cDr"
\r "Return"
\s "Substitute"
\x "eXcise"})
(defn prompt-and-read
"Show a prompt, and read a form from the input
TODO: the read should be on the same line as the prompt - again, possibly some hackery needed."
[prompt]
;; print, on its own, does not flush the buffer.
(println prompt)
(read))
(print (str prompt " "))
(flush)
(read-string
(read-line)))
(defn print-menu
"Print this menu."
[menu]
(println
(apply
str
(cons
"Enter one character: "
(map
#(str " \t" % ": " (menu %))
(keys menu))))))
(defn- prepare-screen
"Prepare the screen for editing this s-expression"
[sexpr menu]
(clear-terminal)
(pprint sexpr)
(print-menu menu))
(defn- sedit-list
"Edit something believed to be a list."
[l]
(if
(list? l)
(do
(prepare-screen l sequence-menu)
(let [key (read-char (keys sequence-menu))]
(case key
\x nil
\s (sedit (prompt-and-read "==?"))
\a (sedit (let [[car & cdr] l] (cons (sedit car) cdr)))
\d (sedit (let [[car & cdr] l] (cons car (sedit cdr))))
\r l
(sedit l))))
l))
(defn- sedit-vector
"Edit something believed to be a vector. Different from
sedit-list, since vectors are recomposed differently."
[v]
(if
(vector? v)
(do
(prepare-screen v sequence-menu)
(let [key (read-char (keys sequence-menu))]
(case key
\x nil
\s (sedit (prompt-and-read "==?"))
\a (sedit (let [[car & cdr] v] (apply vector (cons (sedit car) cdr))))
\d (sedit (let [[car & cdr] v] (apply vector (cons car (sedit cdr)))))
\r v
(sedit v))))
v))
(defn- sedit-token
"Edit something which from our point of view is a single token
(which for now includes strings)."
[token]
(prepare-screen token symbol-menu)
(let [key (read-char (keys symbol-menu))]
(case key
\x nil
\s (sedit (prompt-and-read "==?"))
\r token
(sedit token))))
(defn cons?
"Return true if this sexpr is either a cons or a list.
Bizarrely, in Clojure, a cons cell is not a list."
[sexpr]
(or
(list? sexpr)
(instance? clojure.lang.Cons sexpr)))
(defn sedit
"Edit an S-Expression, and return a modified version of it"
"Edit an S-Expression, and return a modified version of it"
[sexpr]
(clear-terminal)
(pretty-print sexpr)
(cond (list? sexpr) (println "Enter one character: a:CAR; d:CDR; s:Substitute; x:Cut; r:Return")
true (println "Enter one character: s:Substitute; x:Cut; r:Return"))
(let [key (read-char)]
(cond
(= key 'x) nil
(= key 's) (prompt-and-read "==?")
(and (= key 'a)(list? sexpr)(> (count sexpr) 0))
(let [car (sedit (first sexpr)) cdr (rest sexpr)](sedit (cons car cdr)))
(and (= key 'd)(list? sexpr))
(let [car (first sexpr) cdr (sedit (rest sexpr))](sedit (cons car cdr)))
(= key 'r) sexpr
true (sedit sexpr))))
(cond
(nil? sexpr) (sedit-token sexpr)
(cons? sexpr) (sedit-list sexpr)
(vector? sexpr) (sedit-vector sexpr)
(or
(symbol? sexpr)
(number? sexpr)
(string? sexpr)) (sedit-token sexpr)
true (println (str "Unexpected: " (type sexpr)))))
(defn fedit
"Edit a named function or macro, and recompile the result.