edward

An extensible POSIX-compatible implementation of the ed(1) text editor

git clone https://git.8pit.net/edward.git

  1;;>| Text Editor Object
  2;;>
  3;;> Object for storing, inspecting, and modifying the editor state.
  4
  5;;> Create a new text editor object on a given (potentially) empty
  6;;> `filename` string. If non-empty, an `edit-proc` needs to be provided
  7;;> which implements the `E` command to initially read the file.
  8;;> Furthermore, a `prompt` string must be provided and it must be
  9;;> indicated whether the editor should start in `silent?` mode.
 10
 11(define (make-text-editor edit-proc filename prompt silent?)
 12  (let* ((h (make-repl prompt))
 13         (b (make-buffer))
 14         (e (%make-text-editor filename h b 0 0 #f '() #f "" '() '() #f #f silent? #f)))
 15    (unless (empty-string? filename)
 16      ;; XXX: Don't print `?` if file doesn't exist.
 17      (edit-proc e filename))
 18    e))
 19
 20;;> Record type encapsulating the text editor state.
 21
 22(define-record-type Text-Editor
 23  (%make-text-editor filename input buffer line last-line error marks state re
 24                     lcmd replace modified? last-modified? silent? help?)
 25  ;;> Predicate which returns true if the given object was created using [make-text-editor](#make-text-editor).
 26  text-editor?
 27  ;; Name of the file currently being edited.
 28  (filename
 29    ;;> Returns the name of the file that is currently being edited.
 30    text-editor-filename
 31
 32    ;;> Change the file that is currently being edited.
 33    text-editor-filename-set!)
 34  ;; Input repl for this text editor.
 35  (input text-editor-repl)
 36  ;; List of strings representing all lines in the file.
 37  (buffer text-editor-buffer text-editor-buffer-set!)
 38  ;; Current line in the buffer.
 39  (line
 40    ;;> Returns the current line in the internal editor buffer.
 41    text-editor-line
 42    text-editor-line-set!)
 43  ;; Previous line in the buffer.
 44  (last-line text-editor-last-line text-editor-last-line-set!)
 45  ;; Last error message encountered (for h and H command).
 46  (error
 47    ;;> Returns a string representing the last encountered error message.
 48    text-editor-error
 49    text-editor-error-set!)
 50  ;; Assoc lists of marks for this editor.
 51  ;; XXX: Since data is never deleted from an assoc list this leaks memory.
 52  (marks text-editor-marks text-editor-marks-set!)
 53  ;; Symbol with previous cmd name executed by the editor or #f if none.
 54  (state
 55    ;;> Returns the symbol of the command previously executed by
 56    ;;> the editor, on `#f` if no previous command was executed.
 57    text-editor-prevcmd
 58    text-editor-prevcmd-set!)
 59  ;; String representing last encountered RE.
 60  (re text-editor-re text-editor-re-set!)
 61  ;; Last command executed by the shell escape editor command or '() if none.
 62  (lcmd
 63    text-editor-last-cmd
 64    ;;> Update the last shell command executed via the shell escape editor command.
 65    text-editor-last-cmd-set!)
 66  ;; Last used replacement for the substitute command or '() if none.
 67  (replace text-editor-last-replace text-editor-last-replace-set!)
 68  ;; Whether the editor has been modified since the last write.
 69  (modified?
 70    ;;> Predicate which returns true if the current file has been
 71    ;;> modified since the last write to a file (i.e. has unwritten data).
 72    text-editor-modified?
 73
 74    ;;> Modify the modified state of the current file.
 75    ;;> Set this to `#t` if the file has been modified.
 76    text-editor-modified-set!)
 77  ;; Whether the editor has been modified before the last command was executed.
 78  (last-modified? text-editor-last-modified?  text-editor-last-modified-set!)
 79  ;; Whether the editor is in silent mode (ed -s option).
 80  (silent? text-editor-silent?)
 81  ;; Whether help mode is activated.
 82  (help?
 83    ;;> Predicate which returns true if help mode is activated (`H` command).
 84    text-editor-help?
 85
 86    ;;> Enable help mode by passing a truth value to this procedure.
 87    text-editor-help-set!))
 88
 89(define (handle-error editor line msg)
 90  (let* ((in (text-editor-repl editor))
 91         (prefix (if (terminal-port? (current-input-port))
 92                   ""
 93                   (string-append
 94                     "line " (number->string line) ": "))))
 95    (text-editor-prevcmd-set! editor #f)
 96    (editor-error
 97      editor
 98      (string-append prefix msg))))
 99
100(define (handle-sighup editor)
101  ;; Returns `#t` if writes to file succeeded and `#f` otherwise.
102  (define (write-file filename lines)
103    (guard
104      (eobj
105        ((file-error? eobj) #f))
106      ;; TODO: If file exists behavior is unspecified
107      (call-with-output-file filename
108        (lambda (port)
109          (lines->port lines port)))
110      #t))
111
112  (when (text-editor-modified? editor)
113    (let* ((buf (text-editor-buffer editor))
114           (lines (buffer->list buf))
115           (success? (write-file "ed.hup" lines)))
116      (unless success?
117        (write-file (path-join (user-home) "ed.hup") lines))))
118  (exit))
119
120;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
121
122;;>| Editor Interface
123;;>
124;;> High-level text editor interfaces.
125
126;;> Start the read-eval-print loop (REPL) of the editor. Within the
127;;> REPL, command parsing is performed using the given `cmd-parser`.
128
129(define (editor-start editor cmd-parser)
130  (define (execute-command line cmd addr)
131    (guard
132      (eobj
133        ((editor-error? eobj)
134         (handle-error editor line (editor-error-msg eobj))))
135      (if (cmd-reversible? cmd)
136        (editor-with-undo editor
137          (lambda ()
138            (editor-exec editor addr cmd)))
139        (editor-exec editor addr cmd))
140      (text-editor-prevcmd-set! editor (cmd-symbol cmd))))
141
142  (signal-mask! signal/quit)
143  (set-signal-handler!
144    signal/hup
145    (lambda (signum)
146      (handle-sighup editor)))
147
148  (repl-run
149    (text-editor-repl editor)
150    cmd-parser
151    ;; Success continuation.
152    (lambda (line res)
153      (let ((cmd  (cdr res))
154            (addr (car res)))
155        (execute-command line cmd addr)))
156    ;; Failure continuation.
157    (lambda (line reason)
158      (handle-error editor line reason))
159    ;; Interrupt continuation.
160    (lambda ()
161      (newline)
162      (editor-error editor "Interrupt"))))
163
164;;> Run an interactive command within the text editor.
165;;> The command is parsed using the provided `cmd-parser`.
166
167(define (editor-interactive editor cmd-parser)
168  (let ((repl (text-editor-repl editor)))
169    (repl-interactive repl
170      cmd-parser
171      (lambda (line reason)
172        (editor-raise "parsing of interactive command failed")))))
173
174;;> Toggle visibility of the REPL prompt.
175
176(define (editor-toggle-prompt! editor)
177  (let* ((repl (text-editor-repl editor))
178         (prompt? (repl-prompt? repl)))
179    (repl-set-prompt! repl (not prompt?))))
180
181;;> Returns the last executed shell command or raises an error if none.
182
183(define (editor-shell-cmd editor)
184  (let ((lcmd (text-editor-last-cmd editor)))
185    (if (null? lcmd)
186      (editor-raise "no previous command")
187      lcmd)))
188
189;;> Build a new [regex][posix-regex regex] object and handle regex syntax
190;;> errors as editor errors. If the provided pattern is empty, the last
191;;> used pattern is re-used, if there is no last-used pattern an editor
192;;> error is raised.
193;;>
194;;> [posix-regex regex]: https://wiki.call-cc.org/eggref/5/posix-regex#make-regex
195
196(define (editor-make-regex editor pattern)
197  (define (editor-pattern editor pattern)
198    (if (empty-string? pattern)
199      (let ((last-re (text-editor-re editor)))
200        (if (empty-string? last-re)
201          (editor-raise "no previous pattern")
202          last-re))
203      (begin
204        (text-editor-re-set! editor pattern)
205        pattern)))
206
207  (let* ((pattern (editor-pattern editor pattern))
208         (regex (call-with-current-continuation
209                  (lambda (k)
210                    (with-exception-handler
211                      (lambda (eobj)
212                        (k (error-object-message eobj)))
213                      (lambda ()
214                        (k (make-regex pattern))))))))
215    (if (regex? regex)
216      regex
217      (editor-raise regex))))
218
219;;> Access a replacement string in the editor context. If
220;;> the provided replacement string `subst` is `'previous-replace`
221;;> then the previously used replacement string is returned or an
222;;> editor error is raised if there is no previous replacement string.
223;;> Otherwise, (if `subst` is a string) then the previous replacement
224;;> is updated and `subst` is returned.
225
226(define (editor-restr editor subst)
227  (if (equal? subst 'previous-replace)
228    (let ((last-subst (text-editor-last-replace editor)))
229      (if (null? last-subst)
230        (editor-raise "no previous replacement")
231        last-subst))
232    (begin
233      (text-editor-last-replace-set! editor subst)
234      subst)))
235
236;;> Return the currently configured filename, if no default is given it
237;;> is an error if no filename is configured for the given editor.
238
239(define editor-filename
240  (case-lambda
241    ((editor) (%editor-filename editor))
242    ((editor default)
243     (if (empty-string? default)
244       (%editor-filename editor)
245       default))))
246
247(define (%editor-filename editor)
248  (let ((fn (text-editor-filename editor)))
249    (if (empty-string? fn)
250      (editor-raise "no file name specified")
251      fn)))
252
253;;> Print `objs`, but only if the editor is not in silent mode.
254
255(define (editor-verbose editor . objs)
256  (unless (text-editor-silent? editor)
257    (apply println objs)))
258
259;;> Print `?` optionally followed by `msg`, if the editor is in help mode.
260;;> If standard input does not refer to a terminal device, the editor
261;;> terminates with a non-zero exit status.
262
263(define (editor-error editor msg)
264  (text-editor-error-set! editor msg)
265  (println "?")
266  (when (text-editor-help? editor)
267    (println msg))
268
269  ;; See "Consequences of Errors" section in POSIX.1-2008.
270  (unless (terminal-port? (current-input-port))
271    (exit #f)))
272
273;;> Raise an R7RS editor error exception with the given `msg`. This
274;;> error is caught by an error handler and causes the `msg` to be
275;;> printed using the [editor-error](#editor-error) procedure.
276
277(define (editor-raise msg)
278  (raise (make-editor-error msg)))
279
280;; Editor-Error is a custom object raised to indicate a non-fatal
281;; error condition handled according to the ed(1) POSIX specification.
282(define-record-type Editor-Error
283  (make-editor-error msg)
284  editor-error?
285  (msg editor-error-msg))
286
287;;> Reset all file-specific state of the editor.
288
289(define (editor-reset! editor)
290  (text-editor-line-set! editor 0)
291  (text-editor-last-line-set! editor 0)
292  (text-editor-buffer-set! editor (make-buffer))
293  (text-editor-marks-set! editor '()))
294
295;;> Create an editor mark named `mark` which refers to the given `line`.
296
297(define (editor-mark-line editor line mark)
298  (let ((lines (editor-get-lines editor (cons line line))))
299    (if (null? lines)
300      (editor-raise "invalid address")
301      (text-editor-marks-set! editor
302        (alist-cons mark (car lines) (text-editor-marks editor))))))
303
304(define (editor-get-mark editor mark)
305  (let ((pair (assv mark (text-editor-marks editor))))
306    (if pair
307      (let ((lnum (editor-get-lnum editor (cdr pair))))
308        (if lnum
309          lnum
310          ;; XXX: Delete mark if it is found to be invalid (e.g. line deleted)?
311          (editor-raise (string-append "invalid mark: " (string mark)))))
312      (editor-raise (string-append "unknown mark: " (string mark))))))
313
314;;> Move editor cursor to specified line. Line 1 is the first line,
315;;> specifying 0 as a line moves the cursor *before* the first line.
316
317(define (editor-goto! editor line)
318  (text-editor-line-set! editor line))
319
320;; Intermediate range values can be invalid, e.g. "7,5,". This
321;; parameter can be set to disable sanity checks on range values.
322(define allow-invalid-ranges
323  (make-parameter #f))
324
325;;> Find current line number for a given line in the editor buffer. False
326;;> is returned if the line does not exist in the editor buffer.
327;;
328;; XXX: This implementation assumes that eq? performs pointer comparision,
329;; which is the case with CHICKEN but technically this is undefinied behaviour.
330
331(define (editor-get-lnum editor line)
332  (let ((buffer (text-editor-buffer editor)))
333    (find
334      (lambda (lnum)
335        (eq? (buffer-ref buffer (dec lnum)) line))
336      (iota (editor-lines editor) 1))))
337
338;;> Return the content of the editor text buffer as a list of lines
339;;> for the specified line pair `lines`. The start address of the
340;;> pair is inclusive while the end address is exclusive.
341
342(define (editor-get-lines editor lines)
343  (if (buffer-empty? (text-editor-buffer editor))
344    '()
345    (let ((sline (car lines))
346          (eline (cdr lines)))
347      (buffer->list
348        (text-editor-buffer editor)
349        (max (dec sline) 0)
350        eline))))
351
352;;> Predicate which returns true if the given `line` is within
353;;> the range specified by `lines`.
354
355(define (editor-in-range? editor lines line)
356  (let ((sline (car lines))
357        (eline (cdr lines)))
358    (and (>= line sline) (< line eline))))
359
360;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
361
362;;>| Editor Operations
363;;>
364;;> Procedure which modify the editor text buffer. Provided operations
365;;> are wrappers around operations of the internal text editor
366;;> [line buffer][edward buffer] and additionally take care of updating
367;;> the editor state (e.g. the [modified state][text-editor-modified?]).
368;;>
369;;> [text-editor-modified?]: #text-editor-modified?
370;;> [edward buffer]: edward.buffer.html
371
372;; Execute the given thunk and make all buffer operations and editor
373;; state modifications performed in thunk undoable via editor-undo!.
374
375(define (editor-with-undo editor thunk)
376  (let ((m? (text-editor-modified? editor))
377        (ll (text-editor-line editor)))
378    (buffer-with-undo (text-editor-buffer editor) thunk)
379
380    ;; buffer-with-undo succeeded → commit previous editor state.
381    (text-editor-last-modified-set! editor m?)
382    (text-editor-last-line-set! editor ll)))
383
384;;> Undo the last operation on the buffer.
385
386(define (editor-undo! editor)
387  (unless (buffer-has-undo? (text-editor-buffer editor))
388    (editor-raise "nothing to undo"))
389
390  (text-editor-modified-set!
391    editor
392    (text-editor-last-modified? editor))
393
394  (let ((cur-line (text-editor-line editor)))
395    (text-editor-line-set!
396      editor
397      (text-editor-last-line editor))
398    (text-editor-last-line-set! editor cur-line))
399  (buffer-undo! (text-editor-buffer editor)))
400
401;;> Returns amount of lines in the buffer.
402
403(define (editor-lines editor)
404  (buffer-length (text-editor-buffer editor)))
405
406;;> Returns list of line numbers for given lines.
407
408(define (editor-line-numbers lines)
409  (let ((sline (car lines))
410        (eline (cdr lines)))
411    (iota (inc (- eline sline)) sline)))
412
413;;> Append the text at the current address.
414;;> Returns line number of last inserted line.
415
416(define (editor-append! editor line text)
417  (unless (null? text)
418    (text-editor-modified-set! editor #t))
419
420  (let ((buf (text-editor-buffer editor)))
421    (buffer-append! buf line text)
422    (+ line (length text))))
423
424;;> Replace text of given lines with given data.
425;;> Returns line number of last inserted line.
426
427(define (editor-replace! editor lines data)
428  (text-editor-modified-set! editor #t)
429  (let* ((sline  (car lines))
430         (eline  (cdr lines))
431         (buffer (text-editor-buffer editor)))
432    (buffer-replace! buffer sline eline data)
433    (+ (max 0 (dec sline)) (length data))))
434
435;;> Join given lines to single line. Return value is undefined.
436
437(define (editor-join! editor lines)
438  (text-editor-modified-set! editor #t)
439  (let ((sline  (car lines))
440        (eline  (cdr lines))
441        (buffer (text-editor-buffer editor)))
442    (buffer-join! buffer sline eline)))
443
444;;> Remove given lines. Return value is undefined.
445
446(define (editor-remove! editor lines)
447  (text-editor-modified-set! editor #t)
448  (let ((sline  (car lines))
449        (eline  (cdr lines))
450        (buffer (text-editor-buffer editor)))
451    (buffer-remove! buffer sline eline)))
452
453;;> Move given `lines` to given destination `dest-line`.
454;;> Returns the address of the last inserted line.
455
456(define (editor-move! editor lines dest-line)
457  (text-editor-modified-set! editor #t)
458  (let ((sline  (car lines))
459        (eline  (cdr lines))
460        (buffer (text-editor-buffer editor)))
461    (buffer-move! buffer sline eline dest-line)
462    (min
463      (editor-lines editor)
464      (let ((diff (- eline sline)))
465        (+ dest-line
466           ;; If we moved multiple lines, we need to increment
467           ;; the destination lines by the amount of lines moved.
468           (if (zero? diff) diff (inc diff)))))))
469
470;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
471
472;;>| Address Translation
473;;>
474;;> Procedures for performing address translation. That is, procedures
475;;> which convert an [edward ed addr][edward ed addr] to a line number
476;;> (or a pair of line numbers) based on the current editor state. For
477;;> example, the ed address `.` would be converted to the current line
478;;> number (as tracked in the [editor object][section text-editor]).
479;;> The resulting address can then be passed to an
480;;> [editor operation][section operations].
481;;>
482;;> [edward ed addr]: edward.ed.addr.html
483;;> [section text-editor]: #section-text-editor-object
484;;> [section operations]: #section-editor-operations
485
486;;> Convert a single address (i.e. as created via [make-addr][make-addr])
487;;> to a single line number. This is a procedure which must be passed the
488;;> text editor object and an edward ed address as procedure arguments.
489;;>
490;;> [make-addr]: edward.ed.addr.html#make-addr
491
492(define addr->line
493  (match-lambda*
494    ((e ('(current-line) off))
495     (%addr->line e off (text-editor-line e)))
496    ((e ('(last-line) off))
497     (%addr->line e off (editor-lines e)))
498    ((e (('nth-line . line) off))
499     (%addr->line e off line))
500    ((e (('marked-line . mark) off))
501     (%addr->line e off (editor-get-mark e mark)))
502    ((e (('regex-forward . bre) off))
503     (%addr->line e off (match-line 'forward e bre)))
504    ((e (('regex-backward . bre) off))
505     (%addr->line e off (match-line 'backward e bre)))
506    ((e (('relative . rel) off))
507     (%addr->line e off (+ (text-editor-line e) rel)))))
508
509(define (%addr->line editor off line)
510  (let* ((total-off (apply + off))
511         (nline (+ total-off line)))
512    (if (or
513          (> 0 nline)
514          (> nline (editor-lines editor)))
515      (editor-raise (string-append "invalid final address value: "
516                                   (number->string nline)))
517      nline)))
518
519(define (match-line direction editor bre)
520  (define (next-index idx)
521    (modulo
522      (case direction
523        ((forward) (inc idx))
524        ((backward) (dec idx)))
525      (editor-lines editor)))
526
527  (when (zero? (editor-lines editor))
528    (editor-raise "no match"))
529  (let* ((buffer (text-editor-buffer editor))
530         (regex (editor-make-regex editor bre))
531         (start-idx (max (dec (text-editor-line editor)) 0)))
532    (let lp ((idx (next-index start-idx)))
533      (if (regex-match? regex (buffer-ref buffer idx))
534        (inc idx) ;; convert index back to line number
535        (if (eqv? idx start-idx)
536          (editor-raise "no match")
537          (lp (next-index idx)))))))
538
539;;> Convert a `range` address (i.e. as created via [make-range][make-range])
540;;> to a line pair. This procedure does not modify the current editor
541;;> addresses, even for address range like `5;6`.
542;;>
543;;> [make-range]: edward.ed.addr.html#make-range
544
545(define (range->lpair editor range)
546  (let* ((cur (make-addr '(current-line)))
547         (old (addr->line editor cur))
548         (ret (range->lpair! editor range)))
549    (editor-goto! editor old) ;; undo range->lpair! side-effect
550    ret))
551
552(define (range->lpair! editor range)
553  (define (%range->lpair! editor start end)
554    (let ((sline (addr->line editor start))
555          (eline (addr->line editor end)))
556      (if (and (not (allow-invalid-ranges))
557               (> sline eline))
558        (editor-raise "invalid range specification")
559        (cons sline eline))))
560
561  ;; In the case of a <semicolon> separator, the current line ('.') shall
562  ;; be set to the first address, and only then will the second address be
563  ;; calculated. This feature can be used to determine the starting line
564  ;; for forwards and backwards searches.
565  (match range
566    ((fst #\; snd)
567      (editor-goto! editor (addr->line editor fst)) ;; side-effect
568      (%range->lpair! editor (make-addr '(current-line)) snd))
569    ((fst #\, snd)
570     (%range->lpair! editor fst snd))))
571
572;; This procedure expands a list of addresses into a single pair of
573;; concrete line numbers. As such, this procedure is responsible for
574;; both applying the omission rules and discarding addresses.
575;;
576;; For example the address list for "7,5," is evaluted as follows:
577;;
578;;  7,5, [discard] -> 5, [expand] -> 5,5
579;;
580(define (%addrlst->lpair editor lst)
581  (range->lpair!
582    editor
583    (expand-addr
584      (fold (lambda (cur stk)
585              (if (and (address-separator? cur)
586                       (any address-separator? stk))
587                ;; Intermediate range values can be invalid (e.g. "7,5,").
588                (let ((lpair (parameterize ((allow-invalid-ranges #t))
589                               (range->lpair! editor (expand-addr stk)))))
590                  (list
591                    (make-addr (cons 'nth-line (cdr lpair))) ;; discard car
592                    cur))
593                (append stk (list cur))))
594            '() lst))))
595
596;;> This procedure takes an addrlist, as returned by [parse-addrs][parse-addrs]
597;;> and an editor object as an argument and returns a concrete line pair
598;;> for this address. This line pair can then be passed to defined
599;;> editor commands.
600;;>
601;;> [parse-addrs]: edward.ed.addr.html#parse-addrs
602
603(define (addrlst->lpair editor lst)
604  (let* ((cur (make-addr '(current-line)))
605         (old (addr->line editor cur))
606         (ret (%addrlst->lpair editor lst)))
607    (editor-goto! editor old) ;; undo range->lpair! side-effect
608    ret))
609
610;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
611
612;;>| Command Execution
613;;>
614;;> Based on defined [editor operations][section operations], it is
615;;> possible to define custom editor commands which combine multiple
616;;> operations. These commands can then be executed by the users via
617;;> the REPL spawned by [editor-start][editor-start].
618;;>
619;;> **Warning:** The procedures documented here provide a low-level
620;;> interface for defining custom editor commands. However, it is highly
621;;> discouraged to use this interface directly. Instead, editor commands
622;;> should be defined through the macros provided by [edward ed
623;;> cmd][edward ed cmd].
624;;>
625;;> [editor-start]: #editor-start
626;;> [section operations]: #section-editor-operations
627;;> [edward ed cmd]: edward.ed.cmd.html
628
629;;> Record type representing editor commands as defined in POSIX.1-2008.
630
631(define-record-type Editor-Command
632  ;;> Create a new editor command. The command is identified by a unique
633  ;;> `symbol` and procedure `proc` which receives an editor object and
634  ;;> the given `args` as procedure arguments.
635  (make-cmd symbol default-addr proc args)
636  ;;> Predicate which returns true if the given `obj` was created using
637  ;;> the [make-cmd](#make-cmd) procedure.
638  editor-cmd?
639  (default-addr cmd-default-addr)
640  (symbol cmd-symbol)
641  (proc cmd-proc)
642  (args
643    ;;> Retrieve additional arguments defined for this command.
644    ;;> The returned list value does not include the editor object.
645    cmd-args))
646
647;; Returns true if the given command is a command which is reversible
648;; according to the definition of the undo command in POSIX. For the
649;; undo command itself, #f is returned.
650
651(define (cmd-reversible? cmd)
652  (member (cmd-symbol cmd)
653          '(append change delete global insert join move
654            read substitute copy global-unmatched interactive
655            interactive-unmatched)))
656
657;;> Execute an editor command `cmd` using the given `editor` state
658;;> on the addresses given by `addrlst`. The given addresses are
659;;> translated to line addresses internally. If a command should
660;;> be executed on a line address directly, use the
661;;> [editor-xexec](#editor-xexec) procedure instead.
662
663(define (editor-exec editor addrlst cmd)
664  ;; XXX: Special handling for write command with empty buffer.
665  ;; Without this, it would be impossible to use write with
666  ;; an empty buffer since the default address is invalid then.
667  ;;
668  ;; TODO: Find a better way to deal with this edge case.
669  (if (and (eqv? (cmd-symbol cmd) 'write)
670           (not addrlst)
671           (buffer-empty? (text-editor-buffer editor)))
672      (editor-xexec editor '(1 . 1) cmd)
673      (let* ((default-addr (cmd-default-addr cmd))
674             ;; Convert addrlst to line pair (if any given) or
675             ;; use default address and convert that (if any).
676             (line-pair (if addrlst
677                          (addrlst->lpair editor addrlst)
678                          (and (not (null? default-addr))
679                               (range->lpair editor (addr->range default-addr)))))
680             ;; Convert given address (if any) to a single line
681             ;; or a line pair (depending on default address).
682             (line-addr (if (or (not line-pair) (range? default-addr))
683                          line-pair
684                          (cdr line-pair))))
685          (editor-xexec editor line-addr cmd))))
686
687;;> Execute given `cmd` using given `editor` state on the address
688;;> `addr`. The address can either be a single line address, a
689;;> line pair, or an empty list depending on the default address
690;;> specified for `cmd`. If the command doesn't specify a default
691;;> address (i.e. doesn't expect an address argument) then it is
692;;> an error to pass anything other than the empty list as an
693;;> `addr` value to this procedure.
694
695(define (editor-xexec editor addr cmd)
696  (let ((default-addr (cmd-default-addr cmd)))
697    (when (and (null? default-addr) addr)
698      (editor-raise "unexpected address"))
699    (when (and (range? default-addr) (zero? (car addr)))
700      (editor-raise "ranges cannot start at address zero"))
701
702    (apply (cmd-proc cmd)
703           editor
704           (if (null? (cmd-default-addr cmd)) ;; doesn't expect address
705             (cmd-args cmd)
706             (append (list addr) (cmd-args cmd))))))
707
708;;> Execute a list of commands using given editor state.
709
710(define (editor-exec-cmdlist editor cmd-pairs)
711  (for-each (lambda (cmd-pair)
712              (editor-exec editor (car cmd-pair) (cdr cmd-pair)))
713            cmd-pairs))