-
Notifications
You must be signed in to change notification settings - Fork 46
Expand file tree
/
Copy patheca-completion.el
More file actions
443 lines (398 loc) · 18.3 KB
/
eca-completion.el
File metadata and controls
443 lines (398 loc) · 18.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
;;; eca-completion.el --- ECA (Editor Code Assistant) completion -*- lexical-binding: t; -*-
;; Copyright (C) 2025 Eric Dallo
;;
;; SPDX-License-Identifier: Apache-2.0
;;
;; This file is not part of GNU Emacs.
;;
;;; Commentary:
;;
;; The ECA (Editor Code Assistant) completion.
;;
;;; Code:
(require 'cl-lib)
(require 'color)
(require 'map)
(require 'eca-util)
(require 'eca-api)
;; Variables
(defcustom eca-completion-idle-delay 0.2
"Time in seconds to wait before starting completion.
Complete immediately if set to 0.
Disable idle completion if set to nil."
:type '(choice
(number :tag "Seconds of delay")
(const :tag "Idle completion disabled" nil))
:group 'eca)
(defcustom eca-completion-syntax-highlight t
"Whether to syntax-highlight the inline completion overlay.
When non-nil, the suggestion is fontified using the buffer's
`major-mode' before being displayed as ghost text, and
`eca-completion-overlay-face' is composed on top of those
font-lock faces (rather than overriding them) so syntax colors
remain visible."
:type 'boolean
:group 'eca)
(defcustom eca-completion-overlay-dim-ratio 0.5
"How far to blend each fontified span toward the default background.
A float in [0.0, 1.0]: 1.0 keeps the original syntax color, 0.0
collapses it to the background, and intermediate values produce
a dimmed \"ghost\" look similar to Cursor or Copilot. Set to nil
to disable dimming and show fully saturated syntax colors.
Effective only when `eca-completion-syntax-highlight' is non-nil."
:type '(choice
(float :tag "Blend ratio (0.0..1.0)")
(const :tag "Disable dimming" nil))
:group 'eca)
(defface eca-completion-overlay-face
'((t :inherit shadow))
"Face for the ECA completion inline overlay.
Composed with APPEND priority on top of any font-lock faces
already applied to the suggestion text, so syntax-highlighted
foregrounds win over this face's foreground. The face's other
attributes (e.g. italic from `shadow') still apply across the
whole suggestion, including spans that have no font-lock face."
:group 'eca)
;; Internal
(defvar-local eca-completion--real-posn nil
"Posn information without overlay.
To work around posn problems with after-string property.")
(defvar-local eca-completion--overlay nil
"Overlay for ECA completion.")
(defvar-local eca-completion--keymap-overlay nil
"Overlay used to surround point and make eca-completion-keymap activate.")
(defvar-local eca-completion--doc-version 0
"The document version of the current buffer.
Incremented after each change.")
(defvar-local eca-completion--last-doc-version 0
"The document version of the last completion.")
(defvar-local eca-completion--line-bias 1
"Line bias for completion.")
(defvar eca-completion--post-command-timer nil)
(defvar eca-completion-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "<tab>") #'eca-completion-accept)
(define-key map (kbd "TAB") #'eca-completion-accept)
map)
"Keymap for ECA completion overlay.")
(defun eca-completion--string-common-prefix (str1 str2)
"Find the common prefix of STR1 and STR2 directly."
(let ((min-len (min (length str1) (length str2)))
(i 0))
(while (and (< i min-len)
(= (aref str1 i) (aref str2 i)))
(setq i (1+ i)))
(substring str1 0 i)))
(defun eca-completion--get-or-create-keymap-overlay ()
"Make or return the local keymap overlay."
(unless (overlayp eca-completion--keymap-overlay)
(setq eca-completion--keymap-overlay (make-overlay 1 1 nil nil t))
(overlay-put eca-completion--keymap-overlay 'keymap eca-completion-map)
(overlay-put eca-completion--keymap-overlay 'local-map eca-completion-map)
(overlay-put eca-completion--keymap-overlay 'priority 101))
eca-completion--keymap-overlay)
(defun eca-completion--get-overlay ()
"Create or get overlay for ECA completion."
(unless (overlayp eca-completion--overlay)
(setq eca-completion--overlay (make-overlay 1 1 nil nil t))
(overlay-put eca-completion--overlay 'keymap-overlay (eca-completion--get-or-create-keymap-overlay)))
eca-completion--overlay)
(defun eca-completion--fontify-as-mode (text mode)
"Return a fresh copy of TEXT fontified in a temp buffer using MODE.
MODE is a major-mode symbol. Returns a copy of TEXT unmodified
when MODE is nil, unbound, or signals an error during activation
or fontification."
(if (and mode (fboundp mode))
(condition-case nil
(with-temp-buffer
(let ((inhibit-modification-hooks t))
(insert text)
(delay-mode-hooks (funcall mode))
(font-lock-ensure))
(buffer-substring (point-min) (point-max)))
(error (copy-sequence text)))
(copy-sequence text)))
(defun eca-completion--blend-color (fg bg ratio)
"Blend foreground color FG toward background color BG by RATIO.
RATIO is a float in [0.0, 1.0]: 1.0 returns FG, 0.0 returns BG.
Returns a hex string like \"#rrggbb\", or nil when either color
cannot be resolved (e.g. on low-color terminals)."
(let ((fg-rgb (and fg (color-name-to-rgb fg)))
(bg-rgb (and bg (color-name-to-rgb bg))))
(when (and fg-rgb bg-rgb)
(apply #'color-rgb-to-hex
(append (cl-mapcar (lambda (f b)
(+ (* f ratio) (* b (- 1.0 ratio))))
fg-rgb bg-rgb)
;; 2 digits per component for portable #rrggbb output.
'(2))))))
(defun eca-completion--resolve-foreground (face)
"Return the foreground color implied by FACE, or nil.
FACE may be a face name symbol, a list of faces (symbols and/or
anonymous face plists like (:foreground COLOR)), or a single
anonymous face plist. Anonymous plists are inspected directly
because `face-foreground' rejects them on Emacs 28."
(cond
((null face) nil)
((and (listp face) (keywordp (car face)))
(plist-get face :foreground))
((listp face)
(let (result)
(while (and face (not result))
(setq result (eca-completion--resolve-foreground (car face))
face (cdr face)))
result))
((symbolp face)
(face-foreground face nil 'default))))
(defun eca-completion--dim-fontified (text bg ratio)
"Return a copy of TEXT with each face span's foreground dimmed.
TEXT is expected to already carry font-lock face properties.
Each span's resolved foreground (falling back to the `default'
face's foreground) is blended toward BG by RATIO via
`eca-completion--blend-color', and the result is layered on the
span as a higher-priority anonymous face so the original
font-lock face still contributes its non-color attributes.
Spans whose colors cannot be resolved are left untouched."
(let* ((result (copy-sequence text))
(len (length result))
(default-fg (face-foreground 'default))
(pos 0))
(while (< pos len)
(let* ((next (or (next-single-property-change pos 'face result)
len))
(face-prop (get-text-property pos 'face result))
(fg (or (eca-completion--resolve-foreground face-prop)
default-fg))
(blended (and fg bg
(eca-completion--blend-color fg bg ratio))))
(when blended
(add-face-text-property pos next
(list :foreground blended)
nil result))
(setq pos next)))
result))
(defun eca-completion--prepare-display-text (text)
"Return TEXT prepared for the inline overlay.
Applies syntax fontification and foreground dimming according to
`eca-completion-syntax-highlight' and
`eca-completion-overlay-dim-ratio'. The returned string is safe
to mutate and does not yet carry `eca-completion-overlay-face'."
(let ((mode major-mode)
(bg (face-background 'default nil 'default)))
(cond
((and eca-completion-syntax-highlight
(numberp eca-completion-overlay-dim-ratio))
(eca-completion--dim-fontified
(eca-completion--fontify-as-mode text mode)
bg
eca-completion-overlay-dim-ratio))
(eca-completion-syntax-highlight
(eca-completion--fontify-as-mode text mode))
(t (copy-sequence text)))))
(defun eca-completion--set-overlay-text (ov text)
"Set overlay OV with TEXT."
(move-overlay ov (point) (line-end-position))
(move-overlay (overlay-get ov 'keymap-overlay) (point) (min (point-max) (+ 1 (point))))
(let* ((display-text (eca-completion--prepare-display-text text))
(tail (buffer-substring (- (line-end-position) (overlay-get ov 'tail-length)) (line-end-position)))
(text-p (concat display-text tail)))
;; Layer the overlay face with APPEND priority so any font-lock
;; foregrounds set during fontification remain visible. Apply only
;; to the suggestion portion; the trailing buffer text keeps its
;; own faces untouched.
(add-face-text-property 0 (length display-text)
'eca-completion-overlay-face t text-p)
(if (eolp)
(progn
(overlay-put ov 'after-string "")
(setq eca-completion--real-posn (cons (point) (posn-at-point)))
(put-text-property 0 1 'cursor t text-p)
(overlay-put ov 'display "")
(overlay-put ov 'after-string text-p))
(overlay-put ov 'display (substring text-p 0 1))
(overlay-put ov 'after-string (substring text-p 1)))
(overlay-put ov 'completion text)
(overlay-put ov 'start (point))))
(defun eca-completion--display-overlay-completion (text id start end)
"Show TEXT with ID between START and END."
(eca-completion--clear-overlay)
(when (and (not (string-blank-p text))
(or (<= start (point))))
(let* ((ov (eca-completion--get-overlay)))
(overlay-put ov 'tail-length (- (line-end-position) end))
(eca-completion--set-overlay-text ov text)
(overlay-put ov 'id id)
(overlay-put ov 'completion-start start))))
(defun eca-completion--overlay-visible ()
"Return whether completion overlay is being shown."
(and (overlayp eca-completion--overlay)
(overlay-buffer eca-completion--overlay)))
(defun eca-completion--clear-overlay ()
"Clear ECA completion overlay."
(interactive)
(when (eca-completion--overlay-visible)
(delete-overlay eca-completion--overlay)
(delete-overlay eca-completion--keymap-overlay)
(setq eca-completion--real-posn nil)))
(declare-function vterm-delete-region "ext:vterm.el")
(declare-function vterm-insert "ext:vterm.el")
(defun eca-completion-accept ()
"Accept completion."
(interactive)
(when (eca-completion--overlay-visible)
(let* ((text (overlay-get eca-completion--overlay 'completion))
(start (overlay-get eca-completion--overlay 'start))
(end (- (line-end-position) (overlay-get eca-completion--overlay 'tail-length)))
(completion-start (overlay-get eca-completion--overlay 'completion-start)))
;; If there is extra indentation before the point, delete it and shift the completion
(when (and (< completion-start (point))
;; Region we are about to delete contains only blanks …
(string-blank-p (buffer-substring-no-properties completion-start (point)))
;; … *and* everything from BOL to completion-start is blank
;; as well — i.e. we are really inside the leading indentation.
(string-blank-p (buffer-substring-no-properties (line-beginning-position) completion-start)))
(setq start completion-start)
(setq end (- end (- (point) completion-start)))
(delete-region completion-start (point)))
(eca-completion--clear-overlay)
(if (derived-mode-p 'vterm-mode)
(progn
(vterm-delete-region start end)
(vterm-insert text))
(delete-region start end)
(insert text))
t)))
(cl-defun eca-completion--find-completion (&key on-success on-error)
"Find completion requesting server calling ON-SUCCESS for the response.
Call ON-ERROR when error."
(let ((line (+ (1- (line-number-at-pos)) eca-completion--line-bias))
(character (1+ (- (point) (line-beginning-position)))))
(when-let ((session (eca-session)))
(eca-api-request-async session
:method "completion/inline"
:params (list :doc-text (buffer-substring-no-properties (point-min) (point-max))
:doc-version eca-completion--doc-version
:position (list :line line :character character))
:success-callback (-lambda ((&plist :items items :error error))
(if error
(funcall on-error error)
(funcall on-success items)))
:error-callback (-lambda (error)
(let ((type (plist-get error :type))
(msg (plist-get error :message)))
(pcase type
("error" (eca-error msg))
("warning" (eca-warn msg))
("info" (eca-info msg)))))))))
(defun eca-completion--post-command-debounce (buffer)
"Complete in BUFFER."
(when (and (buffer-live-p buffer)
(equal (current-buffer) buffer)
(boundp 'eca-completion-mode)
eca-completion-mode
(not (derived-mode-p 'eca-chat-mode))
(eca-session))
(eca-complete)))
(defun eca-completion--inserted-next-overlay-char-p (command)
"Handle the case where the char just inserted is the start of the completion.
If so, update the overlays and continue. COMMAND is the command that triggered
in `post-command-hook'."
(when (and (symbolp this-command)
(eq command 'self-insert-command)
(eca-completion--overlay-visible))
(let* ((ov eca-completion--overlay)
(completion (overlay-get ov 'completion)))
;; The char just inserted is the next char of completion
(when (eq last-command-event (elt completion 0))
(if (= (length completion) 1)
;; If there is only one char in the completion, accept it
(eca-completion-accept)
(eca-completion--set-overlay-text ov (substring completion 1)))))))
(defun eca-completion--post-command ()
"Auto complete when idle."
(unless (eca-completion--inserted-next-overlay-char-p this-command)
(eca-completion--clear-overlay)
(when eca-completion--post-command-timer
(cancel-timer eca-completion--post-command-timer))
(when (eq 'self-insert-command this-command)
(when (numberp eca-completion-idle-delay)
(setq eca-completion--post-command-timer
(run-with-idle-timer eca-completion-idle-delay
nil
#'eca-completion--post-command-debounce
(current-buffer)))))))
(defun eca-completion--mode-activate ()
"Initial setup for eca-completion-mode."
(add-hook 'post-command-hook #'eca-completion--post-command nil 'local))
(defun eca-completion--mode-deactivate ()
"Teardown for eca-completion-mode."
(when eca-completion--post-command-timer
(cancel-timer eca-completion--post-command-timer))
(remove-hook 'post-command-hook #'eca-completion--post-command 'local))
(defun eca-completion--posn-advice (&rest args)
"Remap posn if in eca-completion-mode with ARGS."
(when (derived-mode-p 'eca-completion-mode)
(let ((pos (or (car-safe args) (point))))
(when (and eca-completion--real-posn
(eq pos (car eca-completion--real-posn)))
(cdr eca-completion--real-posn)))))
(defun eca-completion--show-completion (completion)
"Show COMPLETION."
(-let* (((&plist :text text :id id :range range :docVersion doc-version) completion)
((&plist :start start :end end) range)
((&plist :line start-line :character start-char) start)
((&plist :character end-char) end))
(when (= doc-version eca-completion--doc-version)
(save-excursion
(save-restriction
(widen)
(let* ((p (point))
(goto-line! (lambda ()
(goto-char (point-min))
(forward-line (1- (+ (1- start-line) eca-completion--line-bias)))))
(start (progn
(funcall goto-line!)
(forward-char (1- start-char))
(let* ((cur-line (buffer-substring-no-properties (line-beginning-position) (line-end-position)))
(common-prefix-len (length (eca-completion--string-common-prefix text cur-line))))
(setq text (substring text common-prefix-len))
(forward-char common-prefix-len)
(point))))
(end (progn
(funcall goto-line!)
(forward-char (1- end-char))
(point))))
(goto-char p)
(eca-completion--display-overlay-completion text id start end)))))))
;; Public
;;;###autoload
(define-minor-mode eca-completion-mode
"Minor mode for ECA completions."
:init-value nil
:lighter " ECA completion"
(eca-completion--clear-overlay)
(advice-add 'posn-at-point :before-until #'eca-completion--posn-advice)
(if eca-completion-mode
(eca-completion--mode-activate)
(eca-completion--mode-deactivate)))
;;;###autoload
(defun eca-complete ()
"Complete at the current point."
(interactive)
(when (derived-mode-p 'eca-chat-mode)
(user-error "Completion is not available in eca-chat buffers"))
(eca-assert-session-running (eca-session))
(setq eca-completion--last-doc-version eca-completion--doc-version)
;; TODO add cache
;; (setq eca-completion--completion-cache nil)
(let ((called-interactively (called-interactively-p 'interactive)))
(eca-completion--find-completion
:on-success
(lambda (items)
(let ((item (if (seq-empty-p items) nil (seq-elt items 0))))
(if item
(eca-completion--show-completion item)
(when called-interactively
(eca-warn "No completion is available."))))))))
(provide 'eca-completion)
;;; eca-completion.el ends here