commoned/ce.cl

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))))