483 lines
14 KiB
Common Lisp
483 lines
14 KiB
Common Lisp
; -*- lisp -*-
|
|
|
|
(require :asdf)
|
|
; pregexp seems to not have a provide, so we cannot require
|
|
; instead try loading it if not already bundled in via asdf
|
|
(when (not (boundp '*pregexp-version*))
|
|
(load "pregexp/pregexp.lisp"))
|
|
|
|
(defvar ce-commands-alist '((#\q . quit)
|
|
(#\Newline . ce-command-enter)
|
|
(#\: . ce-command-eval)
|
|
(#\; . ce-command-eval-region)
|
|
(#\{ . ce-command-expand-before)
|
|
(#\} . ce-command-expand)
|
|
(#\= . ce-command-get-point)
|
|
(#\, . ce-command-swap-point)
|
|
(#\/ . ce-command-search)
|
|
(#\? . ce-command-search-before)
|
|
(#\a . ce-command-add)
|
|
(#\A . ce-command-add-before)
|
|
(#\c . ce-command-line-replace)
|
|
(#\d . ce-command-delete)
|
|
(#\e . ce-command-open)
|
|
(#\f . ce-command-fmt)
|
|
(#\g . ce-command-reg-apply)
|
|
(#\h . ce-command-help)
|
|
(#\i . ce-command-insert)
|
|
(#\I . ce-command-insert-beg)
|
|
(#\j . ce-command-join)
|
|
(#\m . ce-command-copy)
|
|
(#\n . ce-command-num-print)
|
|
(#\p . ce-command-print)
|
|
(#\s . ce-command-reg-replace)
|
|
(#\t . ce-command-copy)
|
|
(#\w . ce-command-write)
|
|
(#\x . ce-command-chop)
|
|
(#\X . ce-command-chop-beg)
|
|
(#\0 . ce-command-number)
|
|
(#\1 . ce-command-number)
|
|
(#\2 . ce-command-number)
|
|
(#\3 . ce-command-number)
|
|
(#\4 . ce-command-number)
|
|
(#\5 . ce-command-number)
|
|
(#\6 . ce-command-number)
|
|
(#\7 . ce-command-number)
|
|
(#\8 . ce-command-number)
|
|
(#\9 . ce-command-number)
|
|
))
|
|
|
|
(defvar buffer nil)
|
|
(defvar filename nil)
|
|
(defvar query nil)
|
|
(defvar ins nil)
|
|
(defvar sfl nil)
|
|
(defvar err nil)
|
|
(defvar errf "?~%")
|
|
|
|
; newpoint values:
|
|
; 0 - reusing previous point
|
|
; 1 - outpoint set, inpoint is outpoint
|
|
; 2 - inpoint set, outpoint is eof
|
|
; 3 - both set
|
|
(defvar newpoint 0)
|
|
(defvar inpoint 0)
|
|
(defvar outpoint -1)
|
|
(defvar inline-inpoint 0)
|
|
(defvar inline-outpoint -1)
|
|
|
|
(defun concat (&rest args)
|
|
"concatenate strings together but with format"
|
|
(format nil "~{~a~}" args))
|
|
|
|
(defun not-num-new-p (c)
|
|
"return nil if numeric or newline"
|
|
(not (or (digit-char-p c) (char= #\Newline c))))
|
|
|
|
(defun string-or (a b)
|
|
"or but first being empty is equivalent to nil"
|
|
(or (if (string= "" a) nil a) b))
|
|
|
|
(defun ce-mod (num div)
|
|
"modulus but handle zero"
|
|
(if (= 0 div) 0 (mod num div)))
|
|
|
|
(defun ce-apply-region (fun)
|
|
"apply fun to each line of region"
|
|
(let ((mlen (list-length buffer)))
|
|
(let ((in (ce-mod inpoint mlen)) (out (1+ (ce-mod outpoint mlen))))
|
|
(let ((new (mapcar fun (subseq buffer in out))))
|
|
(ce-replace-lines in out new)))))
|
|
|
|
(defun ce-push-line (index line)
|
|
"push a line into the buffer at index"
|
|
(if (= 0 index) ; index 0 is special as buffer is a singly linked list
|
|
(setq buffer (cons line buffer))
|
|
(push line (cdr (nthcdr (1- index) buffer)))))
|
|
|
|
(defun ce-push-lines (index lines)
|
|
"push lines into the buffer at index"
|
|
(setq buffer (nconc
|
|
(subseq buffer 0 index)
|
|
(nconc lines (nthcdr index buffer)))))
|
|
|
|
(defun ce-replace-lines (in out lines)
|
|
"replace a range of lines"
|
|
(setq buffer (nconc
|
|
(subseq buffer 0 in)
|
|
(nconc lines (nthcdr out buffer)))))
|
|
|
|
(defun ce-delete (in out)
|
|
"delete a range of lines"
|
|
(setq buffer (nconc
|
|
(subseq buffer 0 in)
|
|
(nthcdr (1+ out) buffer))))
|
|
|
|
(defun ce-tokens (str tok &optional (len (length str)) (sta 0) (cur 0)
|
|
(bs nil) (out nil) (rep (pregexp (pregexp-quote
|
|
(format nil "\\~c" tok)))) (ins (format nil "~c" tok)))
|
|
"tokenize of str at character tok. respects backslashes for escaping."
|
|
(if (< cur len)
|
|
(let ((c (char str cur)))
|
|
(if bs
|
|
; last char was a backslash, ignore this one
|
|
(ce-tokens str tok len sta (1+ cur) nil out rep ins)
|
|
(if (char= tok c)
|
|
(ce-tokens str tok len (1+ cur) (1+ cur) nil (cons
|
|
(pregexp-replace* rep (subseq str sta cur) ins) out) rep ins)
|
|
(ce-tokens str tok len sta (1+ cur) (char= #\\ c) out rep ins))))
|
|
(reverse (if (= sta cur)
|
|
out ; there was a trailing delimiter, ignore it
|
|
(cons (pregexp-replace* rep (subseq str sta cur) ins) out)))))
|
|
|
|
; TODO: possibly flatten the region here instead of nearly
|
|
; every command needing ce-mod to get proper numbers?
|
|
; commands that change number of lines would change
|
|
; the region anyways
|
|
(defun ce-reset-input ()
|
|
"fix point to allow inputting new numbers,
|
|
should be called at the beginning of most commands"
|
|
(if (= 1 newpoint)
|
|
(setq inpoint outpoint))
|
|
(setq newpoint 0))
|
|
|
|
(defun ce-repl ()
|
|
"parse commands from stdin"
|
|
(loop
|
|
(let ((input (read-char)))
|
|
(let ((cmd (cdr (assoc input ce-commands-alist))))
|
|
(if cmd
|
|
(handler-case (funcall cmd input)
|
|
(error (e) (setq err e) (format t errf)))
|
|
(progn (read-line) (setq newpoint 0) (format t errf)))))))
|
|
|
|
(defun ce-main ()
|
|
"initalize commoned from bin"
|
|
(let ((args (cdr (ext:command-args))))
|
|
(case (list-length args)
|
|
(0 ())
|
|
(1 (ce-open (car args)))
|
|
(otherwise (format t errf))))
|
|
(ce-repl)
|
|
(ext:quit 0))
|
|
|
|
(defun ce-command-enter (&optional c)
|
|
"process newlines if not eaten by another command"
|
|
(declare (ignore c))
|
|
(if (= 0 newpoint)
|
|
(if (>= (1+ outpoint) (list-length buffer))
|
|
(progn
|
|
(format t errf)
|
|
(return-from ce-command-enter))
|
|
(progn
|
|
(setq outpoint (1+ outpoint))
|
|
(setq inpoint outpoint)))
|
|
(ce-reset-input))
|
|
(let ((out (ce-mod outpoint (list-length buffer))))
|
|
(format t "~a~%" (car (nthcdr out buffer)))))
|
|
|
|
(defun ce-command-eval (&optional c)
|
|
"evaluate a lisp expression"
|
|
(declare (ignore c))
|
|
(ce-reset-input)
|
|
(format t "~a~%" (eval (read))))
|
|
|
|
(defun ce-command-eval-region (&optional c)
|
|
"evaluate first expression in region"
|
|
(declare (ignore c))
|
|
(ce-reset-input)
|
|
(read-line)
|
|
(let ((mlen (list-length buffer)))
|
|
(let ((in (ce-mod inpoint mlen)) (out (1+ (ce-mod outpoint mlen))))
|
|
(format t "~a~%" (eval (read-from-string
|
|
(format nil "~{~a~%~}" (subseq buffer in out))))))))
|
|
|
|
(defun ce-walk-match (dir match n stop &optional (offset 0))
|
|
"increment n in dir direction until line matches match
|
|
or n reaches stop"
|
|
(if (or (= n stop) (= (+ n offset) stop))
|
|
(progn (format t errf) stop)
|
|
(let ((nn (+ n dir)))
|
|
; using nth like this is a bit silly and inefficent
|
|
; when walking forwards, but we can then reuse the
|
|
; function when walking backwards
|
|
(if (pregexp-match-positions match (nth (+ nn offset) buffer))
|
|
nn
|
|
(ce-walk-match dir match nn stop offset)))))
|
|
|
|
(defun ce-command-expand-before (&optional c)
|
|
"decrement inpoint until line matches argument"
|
|
(declare (ignore c))
|
|
(ce-reset-input)
|
|
(let ((inp (read-line)) (len (list-length buffer)))
|
|
; TODO: checking the same condition twice is silly,
|
|
; find a better way
|
|
(let ((off (if (string= "" inp) -1 0))
|
|
(match (pregexp (if (string= "" inp) "^[[:space:]]*$" inp))))
|
|
(setq
|
|
inpoint
|
|
(ce-walk-match -1 match (ce-mod inpoint len) 0 off)))))
|
|
|
|
(defun ce-command-expand (&optional c)
|
|
"increment outpoint until line matches argument"
|
|
(declare (ignore c))
|
|
(ce-reset-input)
|
|
(let ((inp (read-line)) (len (list-length buffer)))
|
|
(let ((match (pregexp (if (string= "" inp) "^[[:space:]]*$" inp))))
|
|
(setq
|
|
outpoint
|
|
(ce-walk-match 1 match (ce-mod outpoint len) (1- len))))))
|
|
|
|
(defun ce-command-get-point (&optional c)
|
|
"print the point"
|
|
(declare (ignore c))
|
|
(ce-reset-input)
|
|
(read-line)
|
|
(let ((len (list-length buffer)))
|
|
(if (not (= inpoint outpoint))
|
|
(format t "~a," (ce-mod inpoint len)))
|
|
(format t "~a ~a~%" (ce-mod outpoint len) filename)))
|
|
|
|
(defun ce-command-swap-point (&optional c)
|
|
"set the inpoint to recent outpoint or beginning, outpoint to eof
|
|
|
|
for example:
|
|
3,5 selects lines 3 through 5 (inclusive)
|
|
,5 selects from the beginning of the document through line 5
|
|
4, selects from line 4 to the end of the document"
|
|
(declare (ignore c))
|
|
(if (= 0 newpoint)
|
|
(setq inpoint 0)
|
|
(setq inpoint outpoint))
|
|
(setq outpoint -1)
|
|
(setq newpoint 2))
|
|
|
|
(defun ce-command-search (&optional c)
|
|
"set point at next line to match argument"
|
|
(declare (ignore c))
|
|
(ce-reset-input)
|
|
(let ((inp (read-line)) (len (list-length buffer)))
|
|
(let ((match
|
|
(pregexp (if (string= "" inp) query (setq query inp)))))
|
|
(setq
|
|
outpoint
|
|
(ce-walk-match 1 match (ce-mod inpoint len) (1- len)))))
|
|
(setq inpoint outpoint)
|
|
(format t "~a~%" (car (nthcdr outpoint buffer))))
|
|
|
|
(defun ce-command-search-before (&optional c)
|
|
"set point at previous line to match argument"
|
|
(declare (ignore c))
|
|
(ce-reset-input)
|
|
(let ((inp (read-line)) (len (list-length buffer)))
|
|
(let ((match
|
|
(pregexp (if (string= "" inp) query (setq query inp)))))
|
|
(setq
|
|
inpoint
|
|
(ce-walk-match -1 match (ce-mod outpoint len) 0))))
|
|
(setq outpoint inpoint)
|
|
(format t "~a~%" (car (nthcdr outpoint buffer))))
|
|
|
|
(defun ce-add-til-dot (index lines)
|
|
"read input until dot, add to buffer"
|
|
(let ((line (read-line)))
|
|
(if (string= "." line)
|
|
(progn
|
|
(setq inpoint index)
|
|
(setq outpoint (+ index (1- (list-length lines))))
|
|
(ce-push-lines index (reverse lines)))
|
|
(ce-add-til-dot index (cons line lines)))))
|
|
|
|
(defun ce-common-add (index)
|
|
"common parts of ce-command-add and ce-command-add-before"
|
|
(let ((line (read-line)))
|
|
(if (string= "" line)
|
|
(ce-add-til-dot index nil)
|
|
(progn
|
|
(setq inpoint index)
|
|
(setq outpoint index)
|
|
(ce-push-line index line)))))
|
|
|
|
(defun ce-command-add (&optional c)
|
|
"add lines after point"
|
|
(declare (ignore c))
|
|
(ce-reset-input)
|
|
(if buffer
|
|
(ce-common-add (1+ (ce-mod outpoint (list-length buffer))))
|
|
(ce-common-add 0)))
|
|
|
|
(defun ce-command-add-before (&optional c)
|
|
"add lines before point"
|
|
(declare (ignore c))
|
|
(ce-reset-input)
|
|
(ce-common-add (ce-mod inpoint (list-length buffer))))
|
|
|
|
(defun ce-command-delete (&optional c)
|
|
"delete the region"
|
|
(declare (ignore c))
|
|
(ce-reset-input)
|
|
(read-line)
|
|
(let ((mlen (list-length buffer)))
|
|
(ce-delete (ce-mod inpoint mlen) (ce-mod outpoint mlen))
|
|
(setq outpoint inpoint)))
|
|
|
|
(defun ce-command-line-replace (&optional c)
|
|
"replace the region"
|
|
(declare (ignore c))
|
|
(ce-reset-input)
|
|
(let ((mlen (list-length buffer)))
|
|
(let ((in (ce-mod inpoint mlen)) (out (ce-mod outpoint mlen)))
|
|
(ce-delete in out)
|
|
(ce-common-add in))))
|
|
|
|
; TODO: needs error handling
|
|
(defun ce-open (name)
|
|
"function to open a file for editing"
|
|
(setq filename name)
|
|
(if (uiop:file-exists-p name)
|
|
(handler-case (setq buffer (uiop:read-file-lines filename))
|
|
(error (e) (setq err e) (format t errf)))
|
|
(format t errf)))
|
|
|
|
(defun ce-command-open (&optional c)
|
|
"open a file for editing"
|
|
(declare (ignore c))
|
|
(ce-reset-input)
|
|
(let ((name (read-line)))
|
|
(ce-open (if (string= "" name)
|
|
filename name))))
|
|
|
|
(defun ce-command-help (&optional c)
|
|
"get help for commoned commands"
|
|
(declare (ignore c))
|
|
(ce-reset-input)
|
|
(let ((key (read-char)))
|
|
(if (char= #\Newline key)
|
|
(format t "Welcome to commoned. try h<letter> to get help for a
|
|
specific command. the recognized commands are as follows:
|
|
~{~a~^ ~}" (remove-if-not 'not-num-new-p (mapcar 'car ce-commands-alist)))
|
|
(progn (read-line) (help (cdr (assoc key ce-commands-alist))))))
|
|
(format t "~%"))
|
|
|
|
(defmacro ce-build-insert (a b)
|
|
"helper for insert and insert-beg"
|
|
`(progn
|
|
(ce-reset-input)
|
|
(let ((inp (read-line)))
|
|
(ce-apply-region (lambda (x) (concat ,a ,b))))))
|
|
|
|
(defun ce-command-insert (&optional c)
|
|
"add to the end of each line in region"
|
|
(declare (ignore c))
|
|
(ce-build-insert x inp))
|
|
|
|
(defun ce-command-insert-beg (&optional c)
|
|
"add to the start of each line in region"
|
|
(declare (ignore c))
|
|
(ce-build-insert inp x))
|
|
|
|
(defun ce-command-num-print (&optional c)
|
|
"print a region with line numbers"
|
|
(declare (ignore c))
|
|
(ce-reset-input)
|
|
(read-line)
|
|
(let ((mlen (list-length buffer)))
|
|
(let ((in (ce-mod inpoint mlen)) (out (1+ (ce-mod outpoint mlen))))
|
|
(loop for lin in (nthcdr in buffer) and i from in repeat (- out in) do
|
|
(format t "~3d ~a~%" i lin)))))
|
|
|
|
(defun ce-command-print (&optional c)
|
|
"print a region"
|
|
(declare (ignore c))
|
|
(ce-reset-input)
|
|
(read-line)
|
|
(let ((mlen (list-length buffer)))
|
|
(if (not (= 0 mlen))
|
|
(let ((in (ce-mod inpoint mlen)) (out (1+ (ce-mod outpoint mlen))))
|
|
(format t "~{~a~%~}" (subseq buffer in out)))
|
|
(format t errf))))
|
|
|
|
(defun ce-command-reg-replace (&optional c)
|
|
"do a sed-like replacement"
|
|
(declare (ignore c))
|
|
(ce-reset-input)
|
|
(let ((sep (read-char)))
|
|
(if (char= #\Newline sep)
|
|
(when (or (not query) (not ins) (not sfl))
|
|
(format t errf)
|
|
(return-from ce-command-reg-replace))
|
|
(let ((pat (ce-tokens (read-line) sep)))
|
|
(setq query (car pat))
|
|
(setq ins (or (nth 1 pat) ""))
|
|
(setq sfl (or (nth 2 pat) "")))))
|
|
(let ((mlen (list-length buffer)))
|
|
(let ((in (ce-mod inpoint mlen)) (out (1+ (ce-mod outpoint mlen))))
|
|
(if (find #\g sfl)
|
|
(loop for lin in (nthcdr in buffer) and i from in repeat (- out in) do
|
|
(let ((res (pregexp-replace* query lin ins)))
|
|
(ce-delete i i)
|
|
(ce-push-line i res)))
|
|
(loop for lin in (nthcdr in buffer) and i from in repeat (- out in) do
|
|
(let ((res (pregexp-replace query lin ins)))
|
|
(when (not (string= lin res))
|
|
(ce-delete i i)
|
|
(ce-push-line i res)
|
|
(return))))))))
|
|
|
|
(defun ce-command-copy (c)
|
|
"copy the region to argument line"
|
|
(ce-reset-input)
|
|
(let ((mlen (list-length buffer)))
|
|
(let ((inp (1+ (ce-mod (parse-integer (read-line)) mlen)))
|
|
(in (ce-mod inpoint mlen))
|
|
(out (1+ (ce-mod outpoint mlen))))
|
|
(let ((copy (subseq buffer in out))
|
|
(diff (1- (- out in))))
|
|
(let ((ninp (if (char= c #\m)
|
|
(progn
|
|
(ce-delete in (1- out))
|
|
(if (< in inp) (1- (- inp diff)) inp))
|
|
inp)))
|
|
(ce-push-lines ninp copy)
|
|
(setq inpoint ninp)
|
|
(setq outpoint (+ ninp diff)))))))
|
|
|
|
; TODO: needs error handling
|
|
(defun ce-command-write (&optional c)
|
|
"write a file to disk"
|
|
(declare (ignore c))
|
|
(ce-reset-input)
|
|
(let ((name (read-line)))
|
|
(with-open-file (out (if (string= "" name) filename name)
|
|
:direction :output
|
|
:if-exists :overwrite
|
|
:if-does-not-exist :create)
|
|
(format out "~{~a~%~}" buffer))))
|
|
|
|
(defmacro ce-build-chop (fin)
|
|
"helper for chop and chop-beg"
|
|
`(progn
|
|
(ce-reset-input)
|
|
(let ((inp (parse-integer (string-or (read-line) "1"))))
|
|
(ce-apply-region (lambda (x)
|
|
(let ((llen (length x)))
|
|
(if (< llen inp)
|
|
x ,fin)))))))
|
|
|
|
(defun ce-command-chop (&optional c)
|
|
"chop off argument characters from end of each line in region"
|
|
(declare (ignore c))
|
|
(ce-build-chop (subseq x 0 (- llen inp))))
|
|
|
|
(defun ce-command-chop-beg (&optional c)
|
|
"chop off argument characters from start of each line in region"
|
|
(declare (ignore c))
|
|
(ce-build-chop (subseq x inp)))
|
|
|
|
(defun ce-command-number (c)
|
|
"input a number"
|
|
(if (or (= 0 newpoint) (= 2 newpoint))
|
|
(progn (setq newpoint (1+ newpoint)) (setq outpoint 0)))
|
|
(setq outpoint (+ (* 10 outpoint) (digit-char-p c))))
|
|
|