diff --git a/source/editor.lisp b/source/editor.lisp index eaf9f66..d8c74e1 100644 --- a/source/editor.lisp +++ b/source/editor.lisp @@ -254,6 +254,20 @@ :leave-history-p t)) nil) +(defun line-editor--kill-word-right (editor) + "Delete separators and the word immediately after EDITOR's cursor." + (let* ((text (line-editor-text editor)) + (cursor (line-editor-cursor editor)) + (end (line-editor--word-end editor))) + (line-editor--set-state + editor + (concatenate 'string + (subseq text 0 cursor) + (subseq text end)) + cursor + :leave-history-p t)) + nil) + (defun line-editor--history-entry-matches-p (editor query entry) "True when ENTRY is eligible for EDITOR's fixed history QUERY." (let ((function (line-editor-history-match-function editor))) @@ -528,6 +542,9 @@ return values. NIL and :IGNORED are no-op commands returning :IGNORED." (:kill-word (line-editor--kill-word editor) (line-editor--continue-action)) + (:kill-word-right + (line-editor--kill-word-right editor) + (line-editor--continue-action)) (:complete (values :complete nil)) (:complete-previous @@ -570,6 +587,7 @@ return values. NIL and :IGNORED are no-op commands returning :IGNORED." :toggle-word-delimiter-mode :home :end :backspace :delete :history-previous :history-next :kill-to-end :kill-line :kill-word + :kill-word-right :complete :complete-previous :up :down :submit :interrupt :end-of-input :stream-end :escape :clear-screen :ignore :ignored))))) diff --git a/source/input.lisp b/source/input.lisp index c4ae600..a993f04 100644 --- a/source/input.lisp +++ b/source/input.lisp @@ -117,6 +117,40 @@ be balanced with DISABLE-KEYBOARD-ENHANCEMENT while the same terminal is owned." :escape) ((string= body "127u") :backspace) + ((string= body "97;5u") + :home) + ((string= body "98;5u") + :left) + ((string= body "101;5u") + :end) + ((string= body "102;5u") + :right) + ((string= body "104;5u") + :backspace) + ((string= body "105;5u") + :complete) + ((member body '("106;5u" "109;5u") :test #'string=) + :submit) + ((string= body "107;5u") + :kill-to-end) + ((string= body "108;5u") + :clear-screen) + ((string= body "110;5u") + :history-next) + ((string= body "112;5u") + :history-previous) + ((string= body "117;5u") + :kill-line) + ((string= body "119;5u") + :kill-word) + ((member body '("98;3u" "27;3;98~") :test #'string=) + :word-left) + ((member body '("100;3u" "27;3;100~") :test #'string=) + :kill-word-right) + ((member body '("102;3u" "27;3;102~") :test #'string=) + :word-right) + ((member body '("127;3u" "27;3;8~" "27;3;127~") :test #'string=) + :kill-word) ((member body '("100;5u" "27;5;100~") :test #'string=) :end-of-input) ((member body '("8;5u" "127;5u" @@ -159,6 +193,10 @@ be balanced with DISABLE-KEYBOARD-ENHANCEMENT while the same terminal is owned." (#\H :home) (#\F :end) (t :ignore))) + ((#\b #\B) :word-left) + ((#\d #\D) :kill-word-right) + ((#\f #\F) :word-right) + ((#\backspace #\rubout) :kill-word) ((#\newline #\return) :insert-newline) (t :escape)))) @@ -168,8 +206,10 @@ be balanced with DISABLE-KEYBOARD-ENHANCEMENT while the same terminal is owned." Printable input becomes (:INSERT text). Control and escape sequences become editing keywords. Bracketed paste becomes one (:PASTE text) event, with terminal controls sanitized before the text reaches an editor. CSI-u reports for Enter, -Tab, Shift-Tab, Escape, Backspace, and Ctrl-D map to their raw semantic events. -Modified Enter becomes :INSERT-NEWLINE when distinguishable from Enter. +Tab, Shift-Tab, Escape, Backspace, and supported Ctrl editing keys map to their +semantic events. Legacy escape-prefix and CSI-u reports for Alt-B and Alt-F +move by words, Alt-D deletes the following word, and Alt-Backspace deletes the +preceding word. Modified Enter becomes :INSERT-NEWLINE when distinguishable. Ctrl-Backspace and Ctrl-W become :KILL-WORD. Ctrl-Left and Ctrl-Right become :WORD-LEFT and :WORD-RIGHT. When a line editor enables word-delimiter mode, these events also recognize that editor's configured word delimiters. Arrow Up diff --git a/source/keymap.lisp b/source/keymap.lisp index b335e01..914c846 100644 --- a/source/keymap.lisp +++ b/source/keymap.lisp @@ -123,6 +123,7 @@ command values remain opaque and are retained." (:kill-to-end . :kill-to-end) (:kill-line . :kill-line) (:kill-word . :kill-word) + (:kill-word-right . :kill-word-right) (:complete . :complete) (:complete-previous . :complete-previous) (:up . :up) diff --git a/tests/editor.lisp b/tests/editor.lisp index cf3a6b1..fe52ea9 100644 --- a/tests/editor.lisp +++ b/tests/editor.lisp @@ -178,6 +178,12 @@ (editor-tests--assert (and (string= (line-editor-text editor) "") (zerop (line-editor-cursor editor))) "Kill-line must clear text and cursor.")) + (let ((editor (make-line-editor :text "alpha beta gamma" :cursor 5))) + (editor-tests--event editor :kill-word-right :continue) + (editor-tests--assert + (and (string= (line-editor-text editor) "alpha gamma") + (= (line-editor-cursor editor) 5)) + "Kill-word-right must remove separators and the next word.")) nil) (defun editor-tests--test-history () diff --git a/tests/input.lisp b/tests/input.lisp index afdef2c..2614001 100644 --- a/tests/input.lisp +++ b/tests/input.lisp @@ -85,6 +85,47 @@ (third case) (input-test--event (input-test--escape-sequence (second case))))) + (dolist (case '(("CSI-u control-A event" "[97;5u" :home) + ("CSI-u control-B event" "[98;5u" :left) + ("CSI-u control-E event" "[101;5u" :end) + ("CSI-u control-F event" "[102;5u" :right) + ("CSI-u control-H event" "[104;5u" :backspace) + ("CSI-u control-I event" "[105;5u" :complete) + ("CSI-u control-J event" "[106;5u" :submit) + ("CSI-u control-K event" "[107;5u" :kill-to-end) + ("CSI-u control-L event" "[108;5u" :clear-screen) + ("CSI-u control-M event" "[109;5u" :submit) + ("CSI-u control-N event" "[110;5u" :history-next) + ("CSI-u control-P event" "[112;5u" :history-previous) + ("CSI-u control-U event" "[117;5u" :kill-line) + ("CSI-u control-W event" "[119;5u" :kill-word))) + (check-equal (first case) + (third case) + (input-test--event + (input-test--escape-sequence (second case))))) + (dolist (case '(("CSI-u alt-B event" "[98;3u" :word-left) + ("CSI-u alt-D event" "[100;3u" :kill-word-right) + ("CSI-u alt-F event" "[102;3u" :word-right) + ("CSI-u alt-backspace event" "[127;3u" :kill-word) + ("modify-other-keys alt-B event" "[27;3;98~" :word-left) + ("modify-other-keys alt-D event" "[27;3;100~" :kill-word-right) + ("modify-other-keys alt-F event" "[27;3;102~" :word-right) + ("modify-other-keys alt-backspace event" + "[27;3;127~" :kill-word))) + (check-equal (first case) + (third case) + (input-test--event + (input-test--escape-sequence (second case))))) + (dolist (case `(("legacy alt-B event" "b" :word-left) + ("legacy alt-D event" "d" :kill-word-right) + ("legacy alt-F event" "f" :word-right) + ("legacy alt-backspace event" + ,(string (code-char 127)) + :kill-word))) + (check-equal (first case) + (third case) + (input-test--event + (input-test--escape-sequence (second case))))) (dolist (code '(10 13)) (loop for modifier from 2 to 8 do (check-equal