Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions eca-chat.el
Original file line number Diff line number Diff line change
Expand Up @@ -2763,6 +2763,16 @@ matches the region we considered."
(setq pos next))))
`(jit-lock-bounds ,beg . ,end))

(defun eca-chat--font-lock-ensure (beg end)
"Fontify BEG to END without shifting visible chat windows.
Hidden markdown markup can change display geometry after streaming.
Preserve window starts and force a cheap redisplay update so fenced
block layout settles before later point motion."
(prog1
(eca-chat--with-preserved-scroll
(font-lock-ensure beg end))
(force-window-update (current-buffer))))

(defun eca-chat--schedule-fontify ()
"Schedule a deferred `font-lock-ensure' for the current chat buffer.
Cancels any previously scheduled timer. Does nothing when
Expand All @@ -2785,7 +2795,7 @@ the new turn instead of the full chat history."
(when (buffer-live-p buf)
(with-current-buffer buf
(setq eca-chat--fontify-timer nil)
(font-lock-ensure
(eca-chat--font-lock-ensure
(or eca-chat--last-user-message-pos (point-min))
(point-max))))))))))

Expand Down Expand Up @@ -3910,8 +3920,9 @@ Must be called with `eca-chat--with-current-buffer' or equivalent."
;; (re)fontification at end-of-stream; full-buffer
;; fontify is O(buffer size) and was the dominant
;; cost on long chats.
(font-lock-ensure (or eca-chat--last-user-message-pos (point-min))
(point-max))
(eca-chat--font-lock-ensure
(or eca-chat--last-user-message-pos (point-min))
(point-max))
(eca-chat--refresh-copy-scopes)
(eca-chat--set-chat-loading session nil)
(eca-chat--refresh-progress chat-buffer)
Expand All @@ -3932,8 +3943,9 @@ Must be called with `eca-chat--with-current-buffer' or equivalent."
;; to the current turn so cost does not grow with
;; chat history; previous turns were already
;; fontified at their own end-of-stream.
(font-lock-ensure (or eca-chat--last-user-message-pos (point-min))
(point-max))
(eca-chat--font-lock-ensure
(or eca-chat--last-user-message-pos (point-min))
(point-max))
(eca-chat--refresh-copy-scopes)
;; Table align/beautify default to scanning from
;; `eca-chat--last-user-message-pos' when called with
Expand Down
54 changes: 54 additions & 0 deletions test/eca-chat-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,35 @@ does not treat the first line as metadata. Returns FN's value."
(expect (current-kill 0 t)
:to-equal "Previous assistant response")))))

(describe "eca-chat--font-lock-ensure"
(it "fontifies and requests a redisplay update"
(with-temp-buffer
(spy-on 'font-lock-ensure :and-return-value 'fontified)
(spy-on 'force-window-update)
(expect (eca-chat--font-lock-ensure 1 1) :to-equal 'fontified)
(expect 'font-lock-ensure :to-have-been-called-with 1 1)
(expect 'force-window-update
:to-have-been-called-with (current-buffer)))))

(describe "eca-chat--schedule-fontify"
(it "uses stable fontification from the current turn"
(with-temp-buffer
(insert "abc")
(setq-local eca-chat--last-user-message-pos 2)
(setq-local eca-chat-fontify-debounce-interval 0.15)
(let (scheduled)
(cl-letf (((symbol-function 'run-with-idle-timer)
(lambda (_secs _repeat fn &rest args)
(setq scheduled (lambda () (apply fn args)))
'test-timer)))
(spy-on 'eca-chat--font-lock-ensure)
(eca-chat--schedule-fontify)
(expect scheduled :to-be-truthy)
(funcall scheduled)
(expect 'eca-chat--font-lock-ensure
:to-have-been-called-with 2 (point-max))
(expect eca-chat--fontify-timer :to-be nil))))))

(describe "eca-chat--render-content"
(describe "progress finished"
(it "clears progress text and spinner when chat-loading is nil"
Expand Down Expand Up @@ -512,6 +541,31 @@ does not treat the first line as metadata. Returns FN's value."
:to-have-been-called))
(when (timerp eca-chat--spinner-timer)
(cancel-timer eca-chat--spinner-timer))
(kill-buffer buf))))

(it "uses stable fontification for normal completion"
(let ((buf (eca-chat-test--make-prompt-buffer ""))
(session (make-eca--session)))
(unwind-protect
(with-current-buffer buf
(setq-local eca-chat--progress-text "thinking...")
(setq-local eca-chat--chat-loading t)
(setq-local eca-chat--last-user-message-pos (point-min))
(spy-on 'eca-chat--font-lock-ensure)
(spy-on 'eca-chat--align-tables)
(spy-on 'eca-chat--beautify-tables)
(spy-on 'eca-chat--refresh-progress)
(spy-on 'eca-chat--set-chat-loading)
(spy-on 'eca-chat--send-steered-prompt)
(spy-on 'eca-chat--send-queued-prompt)
(eca-chat--render-content
session buf "system"
(list :type "progress" :state "finished")
nil)
(expect 'eca-chat--font-lock-ensure
:to-have-been-called-with (point-min) (point-max))
(expect 'eca-chat--align-tables :to-have-been-called)
(expect 'eca-chat--beautify-tables :to-have-been-called))
(kill-buffer buf))))))

(describe "eca-chat--transient-segment-loading"
Expand Down
Loading