Skip to content

Commit 8f09804

Browse files
Save cursor state when changing history in repl (#2218)
* Move cursor to a safe spot before replacing the buffer underneath * Extract internal helper for next and previous history * Keep the correct offset when moving through history * Don't lose track of history When you tried to move past the end of the history list we lost track of where we were and lost the current edited line. * Clean up function * Add tests for listener next/previous history search Ran format on whole, some parts seems to have a different formatting. * Change name of internal function to match name of commands * Refactor to use cond instead
1 parent f89297f commit 8f09804

2 files changed

Lines changed: 121 additions & 58 deletions

File tree

src/ext/listener-mode.lisp

Lines changed: 34 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@
116116
(when (input-start-point buffer)
117117
(delete-point (input-start-point buffer)))
118118
(set-input-start-point buffer
119-
(copy-point point :right-inserting))))
119+
(copy-point point :right-inserting))))
120120

121121
(defun write-prompt (point)
122122
(let ((buffer (point-buffer point)))
@@ -183,52 +183,48 @@
183183
(when win
184184
(replace-textarea buffer str))))
185185

186+
(defun %search-history-startswith-input (direction)
187+
"Internal helper for history next and previous history navigation."
188+
(let* ((buffer (current-buffer))
189+
(point (buffer-point buffer))
190+
(start (input-start-point buffer))
191+
(prefix (points-to-string (input-start-point buffer) point))
192+
(prefix-len (length prefix))
193+
(step-fn (case direction
194+
(:previous #'lem/common/history:previous-history)
195+
(:next #'lem/common/history:next-history))))
196+
(backup-edit-string (current-buffer))
197+
(loop :for steps :from 0
198+
:do (multiple-value-bind (str found)
199+
(funcall step-fn (current-listener-history))
200+
(cond
201+
((and found (eql 0 (search prefix str :test #'string=)))
202+
(replace-textarea buffer str)
203+
(move-point point start)
204+
(character-offset point prefix-len)
205+
(return))
206+
((not found)
207+
(ecase direction
208+
(:previous
209+
(dotimes (i steps)
210+
(lem/common/history:next-history (current-listener-history))))
211+
(:next
212+
(restore-edit-string buffer)
213+
(move-point point start)
214+
(character-offset point prefix-len)))
215+
(return)))))))
216+
186217
(define-command listener-previous-startswith-input () ()
187218
"Find the previous prompt starting with the current input.
188219
189220
See also `listener-previous-input`."
190-
(block nil
191-
(let* ((buffer (current-buffer))
192-
(point (buffer-point buffer))
193-
(charpos (point-charpos point))
194-
(prefix (points-to-string (input-start-point buffer) point)))
195-
(backup-edit-string (current-buffer))
196-
(flet ((commit (str)
197-
(replace-textarea buffer str)
198-
(setf (point-charpos point) charpos)
199-
(return)))
200-
(loop
201-
(multiple-value-bind (str win)
202-
(lem/common/history:previous-history (current-listener-history))
203-
(if win
204-
(when (eql 0 (search prefix str :test #'string=))
205-
(commit str))
206-
(return))))))))
221+
(%search-history-startswith-input :previous))
207222

208223
(define-command listener-next-startswith-input () ()
209224
"Find the next prompt starting with the current input.
210225
211226
See also `listener-next-input`."
212-
(block nil
213-
(let* ((buffer (current-buffer))
214-
(point (buffer-point buffer))
215-
(charpos (point-charpos point))
216-
(prefix (points-to-string (input-start-point buffer) point)))
217-
(backup-edit-string (current-buffer))
218-
(flet ((commit (str)
219-
(replace-textarea buffer str)
220-
(setf (point-charpos point) charpos)
221-
(return))
222-
(rollback ()
223-
(restore-edit-string buffer)
224-
(return)))
225-
(loop
226-
(multiple-value-bind (str win)
227-
(lem/common/history:next-history (current-listener-history))
228-
(if win
229-
(when (eql 0 (search prefix str :test #'string=))
230-
(commit str))
231-
(rollback))))))))
227+
(%search-history-startswith-input :next))
232228

233229
(define-command listener-previous-input () ()
234230
"Get and insert the previous REPL input."

tests/listener-mode.lisp

Lines changed: 87 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
:listener-execute-function
1313
:start-listener-mode
1414
:refresh-prompt
15-
:clamp-cursor-to-input-area))
15+
:clamp-cursor-to-input-area
16+
:listener-previous-startswith-input
17+
:listener-next-startswith-input))
1618
(in-package :lem-tests/listener-mode)
1719

1820
(defun setup-listener-buffer ()
@@ -35,28 +37,93 @@
3537
(with-current-buffers ()
3638
(let ((buffer (setup-listener-buffer)))
3739
(testing "cursor at input-start-point is not moved"
38-
(move-point (current-point) (input-start-point buffer))
39-
(lem/listener-mode:clamp-cursor-to-input-area)
40-
(ok (point= (current-point) (input-start-point buffer))))
40+
(move-point (current-point) (input-start-point buffer))
41+
(lem/listener-mode:clamp-cursor-to-input-area)
42+
(ok (point= (current-point) (input-start-point buffer))))
4143

4244
(testing "cursor before input-start-point on same line is clamped"
43-
(line-start (current-point))
44-
(ok (point< (current-point) (input-start-point buffer)))
45-
(lem/listener-mode:clamp-cursor-to-input-area)
46-
(ok (point= (current-point) (input-start-point buffer))))
45+
(line-start (current-point))
46+
(ok (point< (current-point) (input-start-point buffer)))
47+
(lem/listener-mode:clamp-cursor-to-input-area)
48+
(ok (point= (current-point) (input-start-point buffer))))
4749

4850
(testing "cursor after input-start-point is not moved"
49-
(buffer-end (current-point))
50-
(insert-string (current-point) "hello")
51-
(let ((pos (copy-point (current-point) :temporary)))
52-
(lem/listener-mode:clamp-cursor-to-input-area)
53-
(ok (point= (current-point) pos))))
51+
(buffer-end (current-point))
52+
(insert-string (current-point) "hello")
53+
(let ((pos (copy-point (current-point) :temporary)))
54+
(lem/listener-mode:clamp-cursor-to-input-area)
55+
(ok (point= (current-point) pos))))
5456

5557
(testing "cursor on previous line is not clamped"
56-
;; Add a newline in the input area so cursor can go to a previous line
57-
(move-point (current-point) (input-start-point buffer))
58-
(character-offset (current-point) -1)
59-
;; cursor is now on a line before input-start-point's line
60-
(unless (same-line-p (current-point) (input-start-point buffer))
61-
(lem/listener-mode:clamp-cursor-to-input-area)
62-
(ok (not (point= (current-point) (input-start-point buffer))))))))))
58+
;; Add a newline in the input area so cursor can go to a previous line
59+
(move-point (current-point) (input-start-point buffer))
60+
(character-offset (current-point) -1)
61+
;; cursor is now on a line before input-start-point's line
62+
(unless (same-line-p (current-point) (input-start-point buffer))
63+
(lem/listener-mode:clamp-cursor-to-input-area)
64+
(ok (not (point= (current-point) (input-start-point buffer))))))))))
65+
66+
;; make-history does not need pathname?
67+
68+
(deftest listener-history-search-state-and-geometry
69+
(with-fake-interface ()
70+
(with-current-buffers ()
71+
(let* ((buffer (setup-listener-buffer))
72+
(test-history (lem/common/history:make-history)))
73+
74+
(setf (buffer-value buffer 'lem/listener-mode::%listener-history)
75+
test-history)
76+
77+
(lem/common/history:add-history test-history "hello")
78+
(lem/common/history:add-history test-history "world")
79+
(lem/common/history:add-history test-history "hector")
80+
81+
(testing "Successful previous match perfectly preserves prefix cursor offset"
82+
;; Type our unsubmitted prefix "'he"
83+
(move-point (current-point) (input-start-point buffer))
84+
(insert-string (current-point) "he")
85+
86+
;; Trigger the search
87+
(listener-previous-startswith-input)
88+
89+
;; Assert 1: The buffer text was completely replaced with the newest match
90+
(let ((full-input (points-to-string (input-start-point buffer)
91+
(buffer-end-point buffer))))
92+
(ok (string= "hector" full-input)))
93+
94+
;; Assert 2: The cursor is resting exactly at the end of the "he" prefix.
95+
;; This proves our linear offset math is working and preventing the timer crash!
96+
(let ((cursor-prefix (points-to-string (input-start-point buffer)
97+
(current-point))))
98+
(ok (string= "he" cursor-prefix))))
99+
100+
(testing "State drift is prevented on failed searches and rollback succeeds"
101+
;; We are currently viewing "hector".
102+
;; Search previous again to hit the oldest match ("hello").
103+
(listener-previous-startswith-input)
104+
(ok (string= "hello" (points-to-string (input-start-point buffer)
105+
(buffer-end-point buffer))))
106+
107+
;; Now we force the failure state. Searching previous again will find nothing.
108+
;; Because of our fix, the internal loop should rewind its phantom steps.
109+
(listener-previous-startswith-input)
110+
(ok (string= "hello" (points-to-string (input-start-point buffer)
111+
(buffer-end-point buffer))))
112+
113+
;; If the rewind worked, stepping 'Next' exactly twice should traverse
114+
;; perfectly back through "hector" and hit the Rollback state.
115+
(listener-next-startswith-input)
116+
(ok (string= "hector" (points-to-string (input-start-point buffer)
117+
(buffer-end-point buffer))))
118+
119+
;; The final step Next: Rollback triggers!
120+
(listener-next-startswith-input)
121+
122+
;; Assert 3: The unsubmitted text was flawlessly restored
123+
(ok (string= "he" (points-to-string (input-start-point buffer)
124+
(buffer-end-point buffer))))
125+
126+
;; Assert 4: The rollback explicitly clamped the cursor safely
127+
(let ((cursor-prefix (points-to-string (input-start-point buffer)
128+
(current-point))))
129+
(ok (string= "he" cursor-prefix))))))))

0 commit comments

Comments
 (0)