Skip to content

Commit 8cbf851

Browse files
committed
Re-enable undo with safer support
Fixes #64
1 parent 6a04d08 commit 8cbf851

File tree

1 file changed

+71
-67
lines changed

1 file changed

+71
-67
lines changed

eca-chat.el

Lines changed: 71 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@
3434
Can be `'left', `'right', `'top', or `'bottom'. This setting will only
3535
be used when `eca-chat-use-side-window' is non-nil."
3636
:type '(choice (const :tag "Left" left)
37-
(const :tag "Right" right)
38-
(const :tag "Top" top)
39-
(const :tag "Bottom" bottom))
37+
(const :tag "Right" right)
38+
(const :tag "Top" top)
39+
(const :tag "Bottom" bottom))
4040
:group 'eca)
4141

4242
(defcustom eca-chat-window-width 0.40
@@ -164,14 +164,14 @@ Must be a valid model supported by server, check `eca-chat-select-model`."
164164
(defcustom eca-chat-diff-tool 'smerge
165165
"Select the method for displaying file-change diffs in ECA chat."
166166
:type '(choice (const :tag "Side-by-side Ediff" ediff)
167-
(const :tag "Merge-style Smerge" smerge))
167+
(const :tag "Merge-style Smerge" smerge))
168168
:group 'eca)
169169

170170
(defcustom eca-chat-tool-call-prepare-throttle 'smart
171171
"Throttle strategy for handling `toolCallPrepare` events.
172172
Possible values: `all` or `smart` (default)."
173173
:type '(choice (const :tag "Process all updates" all)
174-
(const :tag "Smart throttle" smart))
174+
(const :tag "Smart throttle" smart))
175175
:group 'eca)
176176

177177
(defcustom eca-chat-tool-call-prepare-update-interval 5
@@ -441,6 +441,11 @@ Must be a positive integer."
441441
(eca-dissoc 'empty)))
442442
empty-chat-buffer)))
443443

444+
(defun eca-chat--insert (&rest contents)
445+
"Insert CONTENTS reseting undo-list to avoid buffer inconsistencies."
446+
(apply #'insert contents)
447+
(setq-local buffer-undo-list nil))
448+
444449
(defmacro eca-chat--allow-write (&rest body)
445450
"Execute BODY allowing write to buffer."
446451
`(let ((inhibit-read-only t))
@@ -569,15 +574,15 @@ Must be a positive integer."
569574
"Insert the prompt and context string adding overlay metadatas."
570575
(let ((prompt-area-ov (make-overlay (line-beginning-position) (1+ (line-beginning-position)) (current-buffer))))
571576
(overlay-put prompt-area-ov 'eca-chat-prompt-area t))
572-
(insert eca-chat-prompt-separator)
577+
(eca-chat--insert eca-chat-prompt-separator)
573578
(let ((progress-area-ov (make-overlay (line-beginning-position) (line-end-position) (current-buffer) nil t)))
574579
(overlay-put progress-area-ov 'eca-chat-progress-area t)
575-
(insert "\n")
580+
(eca-chat--insert "\n")
576581
(move-overlay progress-area-ov (overlay-start progress-area-ov) (1- (overlay-end progress-area-ov))))
577582
(let ((context-area-ov (make-overlay (line-beginning-position) (line-end-position) (current-buffer) nil t)))
578583
(overlay-put context-area-ov 'eca-chat-context-area t)
579-
(insert (propertize eca-chat-context-prefix 'font-lock-face 'eca-chat-context-unlinked-face))
580-
(insert "\n")
584+
(eca-chat--insert (propertize eca-chat-context-prefix 'font-lock-face 'eca-chat-context-unlinked-face))
585+
(eca-chat--insert "\n")
581586
(move-overlay context-area-ov (overlay-start context-area-ov) (1- (overlay-end context-area-ov))))
582587
(let ((prompt-field-ov (make-overlay (line-beginning-position) (1+ (line-beginning-position)) (current-buffer))))
583588
(overlay-put prompt-field-ov 'eca-chat-prompt-field t)
@@ -587,7 +592,7 @@ Must be a positive integer."
587592
"Clear the chat for SESSION."
588593
(erase-buffer)
589594
(remove-overlays (point-min) (point-max))
590-
(insert "\n")
595+
(eca-chat--insert "\n")
591596
(eca-chat--insert-prompt-string)
592597
(eca-chat--refresh-context))
593598

@@ -615,7 +620,7 @@ Otherwise to a not loading state."
615620
(overlay-put prompt-field-ov 'before-string (propertize eca-chat-prompt-prefix-loading 'font-lock-face 'default))
616621
(save-excursion
617622
(goto-char (overlay-start prompt-field-ov))
618-
(insert stop-text)))
623+
(eca-chat--insert stop-text)))
619624
(progn
620625
(overlay-put prompt-field-ov 'before-string (propertize eca-chat-prompt-prefix 'font-lock-face 'eca-chat-prompt-prefix-face))
621626
(save-excursion
@@ -626,7 +631,7 @@ Otherwise to a not loading state."
626631
"Set the chat prompt to be TEXT."
627632
(-some-> (eca-chat--prompt-field-start-point) (goto-char))
628633
(delete-region (point) (point-max))
629-
(insert text))
634+
(eca-chat--insert text))
630635

631636
(defun eca-chat--cycle-history (n)
632637
"Cycle history by N."
@@ -650,7 +655,7 @@ Otherwise to a not loading state."
650655
"Insert a newline character at point."
651656
(interactive)
652657
(when (>= (point) (eca-chat--prompt-field-start-point))
653-
(insert "\n")))
658+
(eca-chat--insert "\n")))
654659

655660
(defun eca-chat--prompt-field-ov ()
656661
"Return the overlay for the prompt field."
@@ -983,7 +988,7 @@ If `eca-chat-focus-on-open' is non-nil, the window is selected."
983988
(when eca-chat--last-user-message-pos
984989
(save-excursion
985990
(goto-char eca-chat--last-user-message-pos)
986-
(insert content))))
991+
(eca-chat--insert content))))
987992

988993
(defun eca-chat--add-text-content (text &optional overlay-key overlay-value)
989994
"Add TEXT to the chat current position.
@@ -997,7 +1002,7 @@ Add a overlay before with OVERLAY-KEY = OVERLAY-VALUE if passed."
9971002
(overlay-put ov overlay-key overlay-value)
9981003
(when (eq overlay-key 'eca-chat--user-message-id)
9991004
(overlay-put ov 'eca-chat--timestamp (float-time)))))
1000-
(insert text)
1005+
(eca-chat--insert text)
10011006
(point))))
10021007

10031008
(defun eca-chat--expandable-content-at-point ()
@@ -1035,21 +1040,21 @@ Applies LABEL-FACE to label and CONTENT-FACE to content."
10351040
(let* ((context-start (eca-chat--prompt-area-start-point))
10361041
(start-point (1- context-start)))
10371042
(goto-char start-point)
1038-
(unless (bolp) (insert "\n"))
1043+
(unless (bolp) (eca-chat--insert "\n"))
10391044
(let ((ov-label (make-overlay (point) (point) (current-buffer))))
10401045
(overlay-put ov-label 'eca-chat--expandable-content-id id)
10411046
(overlay-put ov-label 'eca-chat--expandable-content-toggle nil)
1042-
(insert (propertize (eca-chat--propertize-only-first-word label
1043-
'line-prefix (unless (string-empty-p content)
1044-
eca-chat-expandable-block-open-symbol))
1045-
'keymap (let ((km (make-sparse-keymap)))
1046-
(define-key km (kbd "<mouse-1>") (lambda () (eca-chat--expandable-content-toggle id)))
1047-
(define-key km (kbd "<tab>") (lambda () (eca-chat--expandable-content-toggle id)))
1048-
km)
1049-
'help-echo "mouse-1 / tab / RET: expand/collapse"))
1050-
(insert "\n")
1047+
(eca-chat--insert (propertize (eca-chat--propertize-only-first-word label
1048+
'line-prefix (unless (string-empty-p content)
1049+
eca-chat-expandable-block-open-symbol))
1050+
'keymap (let ((km (make-sparse-keymap)))
1051+
(define-key km (kbd "<mouse-1>") (lambda () (eca-chat--expandable-content-toggle id)))
1052+
(define-key km (kbd "<tab>") (lambda () (eca-chat--expandable-content-toggle id)))
1053+
km)
1054+
'help-echo "mouse-1 / tab / RET: expand/collapse"))
1055+
(eca-chat--insert "\n")
10511056
(let* ((start-point (point))
1052-
(_ (insert "\n"))
1057+
(_ (eca-chat--insert "\n"))
10531058
(ov-content (make-overlay start-point start-point (current-buffer) nil t)))
10541059
(overlay-put ov-content 'eca-chat--expandable-content-content (propertize content 'line-prefix " "))
10551060
(overlay-put ov-label 'eca-chat--expandable-content-ov-content ov-content))))))
@@ -1070,23 +1075,23 @@ Applies LABEL-FACE to label and CONTENT-FACE to content."
10701075
;; Refresh the label line (cheap even when appending)
10711076
(goto-char (overlay-start ov-label))
10721077
(delete-region (point) (1- (overlay-start ov-content)))
1073-
(insert (propertize (eca-chat--propertize-only-first-word label
1074-
'line-prefix (unless (string-empty-p new-content)
1075-
(if open?
1076-
eca-chat-expandable-block-close-symbol
1077-
eca-chat-expandable-block-open-symbol)))
1078-
'help-echo "mouse-1 / RET / tab: expand/collapse"))
1078+
(eca-chat--insert (propertize (eca-chat--propertize-only-first-word label
1079+
'line-prefix (unless (string-empty-p new-content)
1080+
(if open?
1081+
eca-chat-expandable-block-close-symbol
1082+
eca-chat-expandable-block-open-symbol)))
1083+
'help-echo "mouse-1 / RET / tab: expand/collapse"))
10791084
(when open?
10801085
(if append-content?
10811086
;; Fast path: just append the delta to the visible content
10821087
(progn
10831088
(goto-char (overlay-end ov-content))
1084-
(insert delta))
1089+
(eca-chat--insert delta))
10851090
;; Replace the whole visible content
10861091
(progn
10871092
(delete-region (overlay-start ov-content) (overlay-end ov-content))
10881093
(goto-char (overlay-start ov-content))
1089-
(insert new-content))))))))
1094+
(eca-chat--insert new-content))))))))
10901095

10911096
(defun eca-chat--expandable-content-toggle (id &optional force? close?)
10921097
"Toggle the expandable-content of ID.
@@ -1113,7 +1118,7 @@ If FORCE? decide to CLOSE? or not."
11131118
(put-text-property (point) (line-end-position)
11141119
'line-prefix eca-chat-expandable-block-close-symbol)
11151120
(goto-char (overlay-start ov-content))
1116-
(insert content "\n")
1121+
(eca-chat--insert content "\n")
11171122
(overlay-put ov-label 'eca-chat--expandable-content-toggle t))))
11181123
close?)))
11191124

@@ -1182,11 +1187,11 @@ Show parent upwards if HIDE-FILENAME? is non nil."
11821187
(let ((ov (eca-chat--prompt-progress-field-ov)))
11831188
(goto-char (overlay-start ov))
11841189
(delete-region (point) (overlay-end ov)))
1185-
(insert (propertize (if (string-empty-p eca-chat--progress-text)
1186-
eca-chat-prompt-separator
1187-
(concat eca-chat-prompt-separator "\n" eca-chat--progress-text))
1188-
'font-lock-face 'eca-chat-system-messages-face)
1189-
eca-chat--spinner-string)))))
1190+
(eca-chat--insert (propertize (if (string-empty-p eca-chat--progress-text)
1191+
eca-chat-prompt-separator
1192+
(concat eca-chat-prompt-separator "\n" eca-chat--progress-text))
1193+
'font-lock-face 'eca-chat-system-messages-face)
1194+
eca-chat--spinner-string)))))
11901195

11911196
(defun eca-chat--context->str (context &optional static?)
11921197
"Convert CONTEXT to a presentable str in buffer.
@@ -1263,9 +1268,9 @@ If STATIC? return strs with no dynamic values."
12631268
(goto-char))
12641269
(delete-region (point) (line-end-position))
12651270
(seq-doseq (context eca-chat--context)
1266-
(insert (eca-chat--context->str context))
1267-
(insert " "))
1268-
(insert (propertize eca-chat-context-prefix 'font-lock-face 'eca-chat-context-unlinked-face))))
1271+
(eca-chat--insert (eca-chat--context->str context))
1272+
(eca-chat--insert " "))
1273+
(eca-chat--insert (propertize eca-chat-context-prefix 'font-lock-face 'eca-chat-context-unlinked-face))))
12691274

12701275
(defconst eca-chat--kind->symbol
12711276
'(("file" . file)
@@ -1321,7 +1326,7 @@ If STATIC? return strs with no dynamic values."
13211326

13221327
(defun eca-chat--completion-prompts-annotate (item-label)
13231328
"Annotate prompt ITEM-LABEL."
1324-
(-let (((&plist :description description :arguments args)
1329+
(-let (((&plist :description description :arguments args)
13251330
(get-text-property 0 'eca-chat-completion-item item-label)))
13261331
(concat "(" (string-join (--map (plist-get it :name) args) ", ")
13271332
") "
@@ -1341,8 +1346,8 @@ Add text property to prompt text to match context."
13411346
(search-backward eca-chat-context-prefix (line-beginning-position) t)))
13421347
(end-pos (point)))
13431348
(delete-region start-pos end-pos)
1344-
(insert (eca-chat--context->str context 'static))))
1345-
(insert " "))
1349+
(eca-chat--insert (eca-chat--context->str context 'static))))
1350+
(eca-chat--insert " "))
13461351

13471352
(defun eca-chat--completion-file-from-prompt-exit-function (item _status)
13481353
"Add to files the selected ITEM."
@@ -1351,23 +1356,23 @@ Add text property to prompt text to match context."
13511356
(search-backward eca-chat-filepath-prefix (line-beginning-position) t)))
13521357
(end-pos (point)))
13531358
(delete-region start-pos end-pos)
1354-
(insert (eca-chat--filepath->str (plist-get file :path) nil)))
1355-
(insert " "))
1359+
(eca-chat--insert (eca-chat--filepath->str (plist-get file :path) nil)))
1360+
(eca-chat--insert " "))
13561361

13571362
(defun eca-chat--completion-prompt-exit-function (item _status)
13581363
"Finish prompt completion for ITEM."
13591364
(-let* (((&plist :arguments arguments) (get-text-property 0 'eca-chat-completion-item item)))
13601365
(when (> (length arguments) 0)
13611366
(seq-doseq (arg arguments)
13621367
(-let (((&plist :name name :description description :required required) arg))
1363-
(insert " ")
1368+
(eca-chat--insert " ")
13641369
(let ((arg-text (read-string (format "Arg: %s\nDescription: %s\nValue%s: "
13651370
name
13661371
description
13671372
(if required "" " (leave blank for default)")))))
13681373
(if (and arg-text (string-match-p " " arg-text))
1369-
(insert (format "\"%s\"" arg-text))
1370-
(insert arg-text)))))
1374+
(eca-chat--insert (format "\"%s\"" arg-text))
1375+
(eca-chat--insert arg-text)))))
13711376
(end-of-line))))
13721377

13731378
(defun eca-chat--context-to-completion (context)
@@ -1548,8 +1553,8 @@ string."
15481553
(goto-char (eca-chat--prompt-field-start-point))
15491554
(goto-char (line-end-position))
15501555
(when (= (line-beginning-position) (line-end-position))
1551-
(insert " "))
1552-
(insert text)))
1556+
(eca-chat--insert " "))
1557+
(eca-chat--insert text)))
15531558

15541559
;; Public
15551560

@@ -1562,7 +1567,6 @@ string."
15621567
(read-only-mode -1)
15631568
(setq-local eca-chat--history '())
15641569
(setq-local eca-chat--history-index -1)
1565-
(buffer-disable-undo)
15661570

15671571
;; Show diff blocks in markdown-mode with colors.
15681572
(setq-local markdown-fontify-code-blocks-natively t)
@@ -1582,9 +1586,9 @@ string."
15821586
(when (eq 0 (length (string-trim (buffer-string))))
15831587
(save-excursion
15841588
(goto-char (point-min))
1585-
(insert "\n")
1586-
(insert (propertize (eca--session-chat-welcome-message session)
1587-
'font-lock-face 'eca-chat-welcome-face))
1589+
(eca-chat--insert "\n")
1590+
(eca-chat--insert (propertize (eca--session-chat-welcome-message session)
1591+
'font-lock-face 'eca-chat-welcome-face))
15881592
(eca-chat--insert-prompt-string)))
15891593

15901594
;; TODO is there a better way to do that?
@@ -1729,8 +1733,8 @@ Calls CB with the resulting message."
17291733
(cond
17301734
((eq action 'metadata)
17311735
'(metadata (category . eca-capf)
1732-
(display-sort-function . identity)
1733-
(cycle-sort-function . identity)))
1736+
(display-sort-function . identity)
1737+
(cycle-sort-function . identity)))
17341738
((eq (car-safe action) 'boundaries) nil)
17351739
(t
17361740
(complete-with-action action (funcall candidates-fn) probe pred))))
@@ -2419,7 +2423,7 @@ if ARG is current prefix, ask for file, otherwise drop current file."
24192423
(line-beginning-position)
24202424
(line-end-position))))
24212425
(eca-chat--with-current-buffer (eca-chat--get-last-buffer session)
2422-
(insert transcription)
2426+
(eca-chat--insert transcription)
24232427
(newline)
24242428
(eca-chat--key-pressed-return))))
24252429
nil t)
@@ -2435,7 +2439,7 @@ If MSG has :timestamp, prepends [HH:MM] to the text."
24352439
(let ((timestamp (plist-get msg :timestamp))
24362440
(text (plist-get msg :text)))
24372441
(if timestamp
2438-
(format "[%s] %s"
2442+
(format "[%s] %s"
24392443
(format-time-string "%H:%M" timestamp)
24402444
text)
24412445
text)))
@@ -2488,12 +2492,12 @@ Returns selected message plist or nil if no messages or cancelled."
24882492
(dolist (msg (reverse messages))
24892493
(puthash (eca-chat--format-message-for-completion msg) msg table))
24902494
(when-let ((choice (completing-read
2491-
prompt
2492-
(lambda (string pred action)
2493-
(if (eq action 'metadata)
2494-
`(metadata (display-sort-function . identity))
2495-
(complete-with-action action (hash-table-keys table) string pred)))
2496-
nil t)))
2495+
prompt
2496+
(lambda (string pred action)
2497+
(if (eq action 'metadata)
2498+
`(metadata (display-sort-function . identity))
2499+
(complete-with-action action (hash-table-keys table) string pred)))
2500+
nil t)))
24972501
(gethash choice table)))))
24982502

24992503
;;;###autoload

0 commit comments

Comments
 (0)