commoned/ce.cl

319 lines
9.1 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-backwards)
(#\a . ce-command-add)
(#\A . ce-command-add-before)
(#\c . ce-command-line-replace)
(#\d . ce-command-delete)
(#\e . ce-command-open)
(#\h . ce-command-help)
(#\i . ce-command-insert)
(#\I . ce-command-insert-beg)
(#\m . ce-command-move)
(#\p . ce-command-print)
(#\s . ce-command-reg-replace)
(#\t . ce-command-copy)
(#\w . ce-command-write)
(#\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 err nil)
; 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 ce-mod (num div)
"modulus but handle zero"
(if (= 0 div) 0 (mod num div)))
(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"
(if lines
(progn
(ce-push-line index (car lines))
(ce-push-lines (1+ index) (cdr lines)))))
(defun ce-delete (in out)
"delete a range of lines"
(setq buffer (nconc
(subseq buffer 0 in)
(nthcdr (1+ out) buffer))))
; 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 "?~%")))
(progn (read-line) (setq newpoint 0) (format t "?~%")))))))
(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 "?~%"))))
(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 "?~%")
(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 "?~%") 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 (string= 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 ((match (read-line)) (len (list-length buffer)))
(let ((off (if (string= "" match) -1 0)))
(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 ((match (read-line)) (len (list-length buffer)))
(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-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 "?~%")))
(format t "?~%")))
(defun ce-command-open (&optional c)
"open a file for editing"
(declare (ignore c))
(ce-reset-input)
(let ((name (read-line)))
(if (string= "" name)
(format t "?~%")
(ce-open 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 "~%"))
(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 "?~%"))))
; 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))))
(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))))