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 ## Usage
Preliminary, incomplete, alpha quality code. This implements a structure editor in a terminal, 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 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. 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: To edit an arbitrary s-expression:
(sedit sexpr) (sedit sexpr)
This pretty much works now; it returns an edited copy of the s-expression. Vectors are not handled This pretty much works now; it returns an edited copy of the s-expression. Maps are not yet handled.
intelligently (but could be).
To edit a function definition 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: Currently, Clojure metadata on a function symbol as follows:
{ {
:arglists ([sexpr]), :arglists ([sexpr]),
:ns #<Namespace fedit.core>, :ns #<Namespace fedit.core>,
:name sedit, :column 1, :name sedit, :column 1,
:doc "Edit an S-Expression, and return a modified version of it", :doc "Edit an S-Expression, and return a modified version of it",
:line 74, :line 74,
:file "fedit/core.clj" :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 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 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 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. rebinding the function's compiled form we're doing something dark and mysterious anyway.
### Generating/persisting packages ### Generating/persisting packages
Editing a function which is in an existing package has problems associated with it. We cannot easily 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 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 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 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 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, 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, 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 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 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 a package name without a version number part, it would seek the highest numbered version of the
specified package available on the path. 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 a directive
(persist-edits) (persist-edits)
Until this directive had been called, none of the in-core edits which had been made in the session 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 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 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. them; if they had no package metadata would prompt for it.
## Working notes ## Working notes
### 20130919 13:20 ### 20130919 13:20
The function 'source-fn' in package 'clojure.repl' returns, as a string, the source of the 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 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 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 containing the keys ':file' and ':line'. I'm guessing, therefore, that this metadata is set up while
reading the source file. 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 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: 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; * 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 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. 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; * 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 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. 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 current s-exression
* X: ['Cut'] return nil. * X: ['Cut'] return nil.
### 20130920 10:37 ### 20130920 10:37
Now sort-of working. One change can be made to an s-expression, and it can be made anywhere in 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. 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 s-expression to make another change; I suspect this is a lazy-evaluation problem, but I haven't yet
fixed it. 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 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 demonstrator that isn't critical, but if anyone is actually going to use this thing it needs to be
fixed. fixed.
I've written a wrapper round sedit called fedit, which grabs the source of a function from its 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 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 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. 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" :url "http://example.com/FIXME"
:license {:name "Eclipse Public License" :license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"} :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"]) :clean-targets ["classes" "bin"])

View file

@ -1,5 +1,8 @@
(ns fedit.core (ns fedit.core
(:use clojure.repl)) (:require [clojure.repl :refer :all]
[clojure.pprint :refer [pprint]])
(:import [jline.console ConsoleReader]))
(defn clear-terminal (defn clear-terminal
"Clear the terminal screen - should be possible to do this by printing a \f, but "Clear the terminal screen - should be possible to do this by printing a \f, but
@ -7,87 +10,137 @@
[] []
(dotimes [_ 25] (println))) (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 (defn read-char
"Ultimately this will read a single character, probably requiring some Java hackery; but for now "Read from standard input a single character which is one of these targets return it."
just read" [targets]
[] (let [cr (ConsoleReader.)
(read)) 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 "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." TODO: the read should be on the same line as the prompt - again, possibly some hackery needed."
[prompt] [prompt]
;; print, on its own, does not flush the buffer. ;; print, on its own, does not flush the buffer.
(println prompt) (print (str prompt " "))
(read)) (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 (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] [sexpr]
(clear-terminal) (cond
(pretty-print sexpr) (nil? sexpr) (sedit-token sexpr)
(cond (list? sexpr) (println "Enter one character: a:CAR; d:CDR; s:Substitute; x:Cut; r:Return") (cons? sexpr) (sedit-list sexpr)
true (println "Enter one character: s:Substitute; x:Cut; r:Return")) (vector? sexpr) (sedit-vector sexpr)
(let [key (read-char)] (or
(cond (symbol? sexpr)
(= key 'x) nil (number? sexpr)
(= key 's) (prompt-and-read "==?") (string? sexpr)) (sedit-token sexpr)
(and (= key 'a)(list? sexpr)(> (count sexpr) 0)) true (println (str "Unexpected: " (type sexpr)))))
(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))))
(defn fedit (defn fedit
"Edit a named function or macro, and recompile the result. "Edit a named function or macro, and recompile the result.