c64-livecoding/wip-duuqnd/editor/editor.lisp
2025-07-14 17:29:45 +02:00

723 lines
24 KiB
Common Lisp
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

;;;; A cleanup pass, so often required.
;;;; A cleanup pass, so rarely afforded.
;;;; A cleanup pass, so very desired.
;;;; A cleanup pass, this poem received.
;;;; But not the code, that garbage is FIRST DRAFT quality. Wrap this sucker
;;;; in a big ol' HANDLER-BIND because it *will* throw errors in real use.
(in-package #:editor)
(require '#:sb-posix)
(defvar *eio*)
(defclass hybrid-stream (fundamental-stream)
((%input-stream :accessor input-stream :initarg :input-stream)
(%output-stream :accessor output-stream :initarg :output-stream)))
(defmethod close ((stream hybrid-stream) &key abort)
(when (slot-boundp stream '%input-stream)
(close (input-stream stream) :abort abort))
(when (slot-boundp stream '%output-stream)
(close (output-stream stream) :abort abort)))
(defmethod input-descriptor ((stream hybrid-stream))
(sb-posix:file-descriptor (input-stream stream)))
(defmethod output-descriptor ((stream hybrid-stream))
(sb-posix:file-descriptor (output-stream stream)))
(defmethod stream-write-byte ((stream hybrid-stream) integer)
(write-byte integer (output-stream stream)))
(defmethod stream-write-char ((stream hybrid-stream) character)
(if (char= character #\Newline)
(terpri stream)
(let ((byte (char-code character)))
(when (not (typep character 'base-char))
(setf byte #.(char-code #\?)))
(stream-write-byte stream byte))))
(defmethod stream-read-byte ((stream hybrid-stream))
(read-byte (input-stream stream)))
(defmethod stream-read-char ((stream hybrid-stream))
(the base-char (code-char (stream-read-byte stream))))
(defmethod stream-read-line ((stream hybrid-stream))
(warn "READ-LINE: Cancel is not yet implemented")
(loop :with buffer := (make-array 80 :element-type 'base-char
:adjustable t
:fill-pointer 0)
:for char := (read-char stream)
:until (or (char= char #\Return) (char= char #\Newline))
:do (vector-push char buffer)
:finally (return (copy-seq buffer))))
(defmethod stream-write-string ((stream hybrid-stream) string &optional start end)
(loop :for index :from start :below end
:do (write-char (char string index) stream)))
(defmethod stream-line-column ((stream hybrid-stream))
nil)
(defmethod stream-terpri ((stream hybrid-stream))
(write-byte (char-code #\Return) stream)
(write-byte (char-code #\Newline) stream))
(defmethod stream-fresh-line ((stream hybrid-stream))
(terpri stream)
t)
(defmethod stream-force-output ((stream hybrid-stream))
(force-output (output-stream stream)))
(defmethod stream-finish-output ((stream hybrid-stream))
(finish-output (output-stream stream)))
(defun open-terminal (path)
(destructuring-bind (input-stream output-stream)
(loop :repeat 2
:collect (open path
:direction :io
:element-type '(unsigned-byte 8)
:if-exists :overwrite))
(handler-case
(make-instance 'hybrid-stream :input-stream input-stream
:output-stream output-stream)
(error ()
(close input-stream)
(close output-stream)))))
(defun close-terminal ()
(close (input-stream *eio*))
(close (output-stream *eio*)))
(defun set-up-terminal ()
(let ((raw (make-instance 'sb-posix:termios))
(fd (input-descriptor *eio*)))
(flet
((bit-number (bit-set)
(1- (the (integer 1) (integer-length bit-set)))))
(sb-posix:tcgetattr fd raw)
(loop :for bit :in (list sb-posix:ixon sb-posix:brkint sb-posix:istrip)
:do (setf (ldb (byte 1 (bit-number bit))
(sb-posix:termios-iflag raw))
0))
(loop :for bit :in (list sb-posix:echo sb-posix:icanon sb-posix:isig
sb-posix:iexten)
:do (setf (ldb (byte 1 (bit-number bit))
(sb-posix:termios-lflag raw))
0))
(setf (ldb (byte 1 (bit-number sb-posix:opost))
(sb-posix:termios-oflag raw))
0)
(sb-posix:tcsetattr fd sb-posix:tcsaflush raw))))
(defun move-cursor (row col)
(write-byte #x1B *eio*)
(write-char #\[ *eio*)
(format *eio* "~D;~DH" row (1+ col))
(force-output *eio*))
(defun clear-screen ()
(move-cursor 1 0)
(write-byte #x1B *eio*)
(write-char #\[ *eio*)
(write-char #\J *eio*)
(force-output *eio*))
(defun clear-line ()
(write-char #\Return *eio*)
(write-byte #x1B *eio*)
(write-char #\[ *eio*)
(write-char #\K *eio*)
(force-output *eio*))
(defun scroll-screen-down (&optional (amount 1))
(write-byte #x1B *eio*)
(write-char #\[ *eio*)
(format *eio* "~D" amount)
(write-char #\T *eio*))
(defun scroll-screen-up (&optional (amount 1))
(write-byte #x1B *eio*)
(write-char #\[ *eio*)
(format *eio* "~D" amount)
(write-char #\S *eio*))
(defparameter +screen-width+ 80)
(defclass editor ()
((%buffers :accessor buffers)
(%current-buffer :accessor current-buffer)
(%stream :accessor editor-stream)))
(defparameter *terminal-path* "/dev/pts/0")
(defun %make-editor (stream)
(let ((editor (make-instance 'editor)))
(setf (editor-stream editor)
stream
(buffers editor)
(list (make-instance 'buffer))
(current-buffer editor)
(first (buffers editor)))
(set-up-terminal)
editor))
(defun make-editor ()
(let ((stream (open-terminal *terminal-path*)))
(handler-bind ((error (lambda (c)
(close stream)
(error c))))
(setf *eio* stream)
(setf *editor* (%make-editor stream)))))
(defvar *editor*)
(defmacro with-editor-accessors (editor (&key buffers current-buffer
current-line current-column
stream)
&body body)
`(with-accessors ,(loop :for (accessor name)
:in `((buffers ,buffers)
(current-buffer ,current-buffer)
(editor-stream ,stream))
:unless (null name)
:collect (list name accessor))
,editor
(with-accessors ,(loop :for (accessor name)
:in `((cursor-line ,current-line)
(cursor-column ,current-column))
:unless (null name)
:collect (list name accessor))
(cursor (current-buffer ,editor))
,@body)))
(defclass cursor ()
((%buffer :accessor buffer :initarg :buffer)
(%line :accessor cursor-line :initarg :line)
(%column :accessor cursor-column :initarg :column :initform 0)))
(defmethod (setf cursor-line) :after (new-value (object cursor))
(setf (cursor-column object) (cursor-column object)))
(defmethod (setf cursor-column) :around (new-value (object cursor))
(when (> new-value (line-length (cursor-line object)))
(setf new-value (line-length (cursor-line object))))
(call-next-method new-value object))
;;; Shortcuts
(defmethod cursor ((obj editor))
(cursor (current-buffer obj)))
(defmethod cursor-line ((obj editor))
(cursor-line (cursor obj)))
(defmethod cursor-column ((obj editor))
(cursor-column (cursor obj)))
(defmethod cursor-go-to ((cursor cursor) line column)
(check-type line integer)
(setf (cursor-line cursor)
(loop :for line-obj := (first-line (buffer cursor))
:then (next-line line-obj)
:when (null line-obj)
:do (error "No such line number ~D" line)
:when (eql line (line-number line-obj))
:return line-obj))
(setf (cursor-column cursor) column))
(defclass line ()
((%line-number :accessor line-number :initarg :number :initform 1)
(%content :accessor line-content
:initform (make-array 80 :element-type 'base-char
:initial-element #\Null
:fill-pointer 0))
(%next-line :accessor next-line :initarg :next-line
:initform nil)
(%prev-line :accessor prev-line :initarg :prev-line
:initform nil)))
(defmethod ensure-correct-line-number ((line line))
(cond ((null (prev-line line))
(setf (line-number line) 1))
(t
(setf (line-number line)
(1+ (line-number (prev-line line)))))))
(defmethod set-line-text ((line line) text)
(setf (fill-pointer (line-content line))
(length text))
(replace (line-content line) text))
(defmethod line-has-room-p ((line line))
(< (fill-pointer (line-content line)) (1- +screen-width+)))
(defmethod line-length ((line line))
(length (line-content line)))
(defmethod insert-character-into-line ((line line) column character)
(unless (line-has-room-p line)
(error "No room on line."))
(cond ((= column (fill-pointer (line-content line)))
(vector-push character (line-content line)))
(t
(let ((to-shove (subseq (line-content line)
column)))
(incf (fill-pointer (line-content line)))
(replace (line-content line) to-shove
:start1 (1+ column))
(setf (char (line-content line) column) character)))))
(defmethod delete-character-in-line ((line line) column)
(unless (< column (fill-pointer (line-content line)))
(error "Can't delete past the FILL-POINTER."))
(let ((to-shove (subseq (line-content line) (1+ column))))
(replace (line-content line) to-shove :start1 column)
(decf (fill-pointer (line-content line)))))
(defmethod new-line-at ((line line) column)
(let ((new (make-instance 'line :prev-line line
:next-line (next-line line))))
(unless (null (next-line line))
(setf (prev-line (next-line line)) new))
(setf (next-line line) new)
(set-line-text new (subseq (line-content line) column))
(setf (fill-pointer (line-content line)) column)
new))
(defmethod delete-line ((line line))
(when (and (null (next-line line))
(null (prev-line line)))
(error "Tried to delete the only line, ~A" line))
(setf (cursor-line (cursor *editor*))
(if (null (prev-line line))
(next-line line)
(prev-line line)))
(unless (null (next-line line))
(setf (prev-line (next-line line)) (prev-line line)))
(unless (null (prev-line line))
(setf (next-line (prev-line line)) (next-line line)))
(unless (null (next-line line))
(ensure-correct-line-number (next-line line))))
(defmethod merge-lines ((left-line line) (right-line line))
(cond ((>= (+ (line-length left-line) (line-length right-line))
+screen-width+)
(feep))
(t
(let ((start (line-length left-line)))
(setf (fill-pointer (line-content left-line))
(+ (line-length left-line) (line-length right-line)))
(replace (line-content left-line) (line-content right-line)
:start1 start)
(delete-line right-line)
(redisplay-view-from-line (current-view *editor*) left-line)
(setf (cursor-line (cursor *editor*)) left-line
(cursor-column (cursor *editor*)) start)
(update-buffer-cursor (current-buffer *editor*))))))
(defmethod redisplay-line ((line line))
;; TODO: save the cursor position
(with-editor-accessors *editor* (:current-line current
:current-buffer buffer)
(move-cursor (view-line-number (buffer-view buffer) line) 0)
(write-char #\Return *eio*)
(write-byte #x1B *eio*)
(write-char #\[ *eio*)
(write-char #\K *eio*)
(write-string (line-content line) *eio*)
(update-buffer-cursor buffer))
(force-output *eio*))
(defun save-cursor ())
(defun restore-cursor ())
(defclass buffer ()
((%first-line :accessor first-line)
(%cursor :accessor cursor)
(%view :accessor buffer-view)))
(defmethod initialize-instance :after ((instance buffer) &rest initargs
&key &allow-other-keys)
(declare (ignore initargs))
(setf (first-line instance) (make-instance 'line)
(cursor instance) (make-instance 'cursor
:line (first-line instance)
:buffer instance)
(buffer-view instance) (make-instance 'buffer-view :buffer instance)
(top-line (buffer-view instance)) (first-line instance)))
(defmethod buffer-string ((buffer buffer))
"Returns a string equivalent to BUFFER's contents."
(with-output-to-string (output)
(loop :for line := (first-line buffer) :then (next-line line)
:until (null line)
:do (write-string (line-content line) output)
(terpri output))))
(defmethod update-buffer-cursor ((buffer buffer))
"Makes sure the screen cursor is the same as BUFFER's logical cursor.
Additionally ensures correct line numbers on the way, as a bonus."
(loop :for line := (first-line buffer) :then (next-line line)
:until (null line)
:do (ensure-correct-line-number line))
(let ((line-number (view-line-number (buffer-view buffer)
(cursor-line (cursor buffer)))))
;; the ugly (of many, one)
(when (null line-number) ; line is off-screen
(if (and (not (null (bottom-line (buffer-view buffer))))
(eql (next-line (bottom-line (buffer-view buffer)))
(cursor-line (cursor buffer))))
(setf (top-line (buffer-view buffer))
(next-line (top-line (buffer-view buffer)))
(bottom-line (buffer-view buffer)) nil)
(setf (top-line (buffer-view buffer)) (cursor-line (cursor buffer))
(bottom-line (buffer-view buffer)) nil))
(redisplay-view (buffer-view buffer))
(setf line-number (view-line-number (buffer-view buffer)
(cursor-line (cursor buffer)))))
(move-cursor line-number (cursor-column (cursor buffer)))))
(defun status-line-string ()
(format nil "Line ~D, column ~D.~80T"
(line-number (cursor-line *editor*))
(cursor-column *editor*)))
(defun redisplay-status-line ()
(move-cursor 24 0)
(write-byte #x1B *eio*)
(write-char #\[ *eio*)
(write-char #\7 *eio*)
(write-char #\m *eio*)
(write-string (status-line-string) *eio*)
(write-byte #x1B *eio*)
(write-char #\[ *eio*)
(write-char #\m *eio*)
(update-buffer-cursor (current-buffer *editor*)))
(defclass buffer-view ()
((%buffer :accessor buffer :initarg :buffer)
(%top-line :accessor top-line :initform nil)
(%bottom-line :accessor bottom-line :initform nil)
(%height :accessor height :initform 20)))
(defmacro do-view-lines ((line local-line-number) view &body body)
`(loop :repeat (height ,view)
:for ,local-line-number :from 1
:for ,line := (top-line ,view) :then (next-line ,line)
:until (null ,line)
:do (ensure-correct-line-number ,line)
:do (progn
,@body)))
(defmacro do-whole-view ((line local-line-number) view &body body)
`(loop :repeat (height ,view)
:for ,local-line-number :from 1
:for ,line := (top-line ,view) :then (if (null ,line)
nil
(next-line ,line))
:when (typep ,line 'line)
:do (ensure-correct-line-number ,line)
:do (progn
,@body)))
(defmethod view-line-number ((view buffer-view) (line line))
(loop :for number :from 1
:repeat (height view)
:for view-line := (top-line view) :then (next-line view-line)
:until (null view-line)
:when (eql line view-line)
:return number))
(defmethod redisplay-view ((view buffer-view))
(clear-screen)
(do-view-lines (line number) view
(redisplay-line line)
(setf (bottom-line view) line))
(update-buffer-cursor (buffer view)))
(defmethod update-view-bottom ((view buffer-view))
(do-view-lines (line number) view
(setf (bottom-line view) line)))
(defmethod redisplay-view-line ((view buffer-view) (line line))
(redisplay-line line))
(defmethod redisplay-view-from-line ((view buffer-view) (from-line line))
(let ((startedp nil))
(do-whole-view (current-line number) view
(when (eql from-line current-line)
(setf startedp t))
(when startedp
(if (typep current-line 'line)
(redisplay-view-line view current-line)
(progn
(move-cursor number 0)
(clear-line)))))
(update-buffer-cursor (buffer view))))
(defmethod scroll-view-down ((view buffer-view))
(scroll-screen-up 1) ; view goes down, screen contents go up
(update-view-bottom view)
(setf (top-line view) (next-line (top-line view))
(bottom-line view) (next-line (bottom-line view)))
(unless (null (bottom-line view))
(redisplay-view-from-line view (bottom-line view)))
(redisplay-status-line))
(defmethod scroll-view-up ((view buffer-view))
(scroll-screen-down 1) ; view goes up, screen contents go down
(update-view-bottom view)
(setf (top-line view)
(prev-line (top-line view))
(bottom-line view)
(prev-line (bottom-line view)))
(redisplay-line (top-line view))
(redisplay-status-line))
(defmethod current-view ((obj editor))
(buffer-view (current-buffer obj)))
(defun feep ()
(write-char #\Bel *eio*)
(force-output *eio*))
(defun insert-char (char)
(with-editor-accessors *editor* (:current-column column
:current-line line)
(if (line-has-room-p line)
(progn
(insert-character-into-line line column char)
(incf column))
(feep))
(redisplay-line line)))
(defun com-forward-delete ()
;; TODO: Check for end of line
(with-editor-accessors *editor* (:current-column column
:current-line line)
(delete-character-in-line line column)))
(defun com-backward-delete ()
(with-editor-accessors *editor* (:current-column column
:current-line line)
(cond ((and (zerop column) (null (prev-line line)))
(feep))
((zerop column)
(merge-lines (prev-line line) line))
(t
(delete-character-in-line line (1- column))
(decf column)
(redisplay-line line)))))
(defun com-newline ()
(with-editor-accessors *editor* (:current-column column
:current-line line)
(ensure-correct-line-number (new-line-at line column))
(redisplay-view-from-line (current-view *editor*) line)
(com-next-line)
(com-beginning-of-line)
(redisplay-line line)))
(defun com-beginning-of-line ()
(with-editor-accessors *editor* (:current-column column
:current-buffer buffer)
(setf column 0)
(update-buffer-cursor buffer)))
(defun com-end-of-line ()
(with-editor-accessors *editor* (:current-column column
:current-line line
:current-buffer buffer)
(setf column (line-length line))
(update-buffer-cursor buffer)))
(defun com-previous-line ()
(with-editor-accessors *editor* (:current-line line
:current-buffer buffer)
(cond ((null (prev-line line))
(feep))
(t
(setf line (prev-line line))
(update-buffer-cursor buffer)))))
(defun com-next-line ()
(with-editor-accessors *editor* (:current-line line
:current-buffer buffer)
(cond ((null (next-line line))
(feep))
(t
(setf line (next-line line))
(update-buffer-cursor buffer)))))
(defun com-forward-char ()
(with-editor-accessors *editor* (:current-column column
:current-line line
:current-buffer buffer)
(cond ((> (1+ column)
(line-length line))
(if (null (next-line line))
(feep)
(progn
(com-beginning-of-line)
(com-next-line))))
(t
(incf column)
(update-buffer-cursor buffer)))))
(defun com-backward-char ()
(with-editor-accessors *editor* (:current-column column
:current-line line
:current-buffer buffer)
(decf column)
(when (< column 0)
(if (null (prev-line line))
(progn
(incf column)
(feep))
(progn
(com-previous-line)
(com-end-of-line))))
(update-buffer-cursor buffer)))
(defun com-new-buffer ()
(setf (current-buffer *editor*)
(make-instance 'buffer))
(push (current-buffer *editor*) (buffers *editor*))
(redisplay-view (buffer-view (current-buffer *editor*))))
(defun com-refresh-screen ()
(redisplay-view (buffer-view (current-buffer *editor*))))
(defun compile-fail-prompt (text line col)
(clear-screen)
(move-cursor 1 0)
(format *eio* "~A~%~%RET: Go there.~%Anything else: Ignore." text)
(force-output *eio*)
(let ((key (get-input-sequence)))
(if (equal '(:C . #\j) key)
(cursor-go-to (cursor *editor*) line col)
(format t "~A~%" key)))
(clear-screen)
(redisplay-view (current-view *editor*)))
(defun com-compile-buffer ()
(let ((bytes
(handler-case
(usc:compile-string-to-bytes
(buffer-string (current-buffer *editor*)))
(usc:usc-error (c)
(let ((source (cdr (usc:source c))))
(compile-fail-prompt c (car source) (cdr source)))))))))
(defun ctrl (key)
(when (numberp key)
(setf key (code-char (+ (1- (char-code #\A)) key))))
(when (characterp key)
(setf key (char-downcase key)))
(cons :c key))
(defparameter *csi-sequences*
'(((#\1 #\7 #\~) :f6)
((#\1 #\8 #\~) :f7)
((#\1 #\9 #\~) :f8)
((#\2 #\0 #\~) :f9)
((#\2 #\1 #\~) :f10)
((#\A) :up)
((#\B) :down)
((#\C) :right)
((#\D) :left)))
(defun csi-handler ()
(loop :for buffer := '() :then (append buffer (list (read-char *eio*)))
:for found := (assoc buffer *csi-sequences* :test #'equalp)
:when (or found (not (zerop (some
(lambda (c)
(search buffer (car c)))
*csi-sequences*))))
:return (cadr found)))
(defun esc (key)
(when (numberp key)
(setf key (code-char key)))
(when (char= key #\[)
(let ((next (csi-handler)))
(setf key next)))
(if (keywordp key)
key
(cons :m key)))
(defparameter *key-command-mappings*
'(((:c . #\a) com-beginning-of-line)
((:c . #\e) com-end-of-line)
(:f6 com-compile-buffer)
(:up com-previous-line)
(:down com-next-line)
(:right com-forward-char)
(:left com-backward-char)
(#\Del com-backward-delete)
((:c . #\d) com-forward-delete)
((:c . #\j) com-newline)
((:c . #\n) com-new-buffer)
((:c . #\l) com-refresh-screen)))
(defun key-command-dispatch (key)
(when (numberp key)
(setf key (code-char key)))
(when (characterp key)
(setf key (char-downcase key)))
(let ((mapping (assoc key *key-command-mappings* :test #'equalp)))
(if (null mapping)
(format *eio* "Key ~A is unbound.~%" key)
(funcall (cadr mapping)))
(force-output *eio*)))
(defun get-input-sequence ()
(let ((first-byte (read-byte *eio*)))
(cond ((< #x1F first-byte #x7F)
(code-char first-byte))
((<= 1 first-byte 26)
(ctrl first-byte))
((= first-byte #x1B)
(esc (read-byte *eio*)))
(t first-byte))))
(defun editor-take-command ()
(let ((input (get-input-sequence)))
(if (characterp input)
(insert-char input)
(key-command-dispatch input)))
(redisplay-status-line)
(force-output *eio*))
(defun editor-loop ()
(loop (editor-take-command)))