Skip to content

Commit 85c7e98

Browse files
committed
Close joaotavora/eglot#235: play along with LSP's filterText hacks
Reworked important parts of eglot-completion-at-point. One of the tasks was to cleanup the nomenclature so it's easier to spot how LSP and Emacs's views of completion techniques differ. When reading this rather long function, remember an "item" is a plist representing the LSP completionItem object, and "proxy" is a propertized string that Emacs's frontends will use to represent that completion. When the completion is close to done, the :exit-function is called, to potentially rework the inserted text so that the final result might be quite different from the proxy (it might be a snippet, or even a suprising text edit). The most important change in this commit reworks the way the completion "bounds" are calculated in the buffer. This is the region that Emacs needs to know that is being targeted for the completion. A server can specify this region by using textEdit-based completions all consistently pointing to the same range. If it does so, Emacs will use that region instead of its own understanding of symbol boundaries (provided by thingatpt.el and syntax tables). To implement server-side completion filtering, the server can also provide a filterText "cookie" in each completion, which, when prefix-matched to the intended region, selects or rejects the completion. Given the feedback in microsoft/language-server-protocol#651, we have no choice but to play along with that inneficient and grotesque strategy to implement flex-style matching. Like ever in LSP, we do so while being backward-compatible to all previously supported behaviour. * eglot.el (eglot-completion-at-point): rework.
1 parent f2f68a8 commit 85c7e98

File tree

1 file changed

+98
-88
lines changed

1 file changed

+98
-88
lines changed

lisp/progmodes/eglot.el

Lines changed: 98 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1874,57 +1874,69 @@ is not active."
18741874

18751875
(defun eglot-completion-at-point ()
18761876
"EGLOT's `completion-at-point' function."
1877-
(let* ((bounds (bounds-of-thing-at-point 'symbol))
1878-
(server (eglot--current-server-or-lose))
1879-
(completion-capability (eglot--server-capable :completionProvider))
1880-
(sort-completions (lambda (completions)
1881-
(sort completions
1882-
(lambda (a b)
1883-
(string-lessp
1884-
(or (get-text-property 0 :sortText a) "")
1885-
(or (get-text-property 0 :sortText b) ""))))))
1886-
(metadata `(metadata . ((display-sort-function . ,sort-completions))))
1887-
completions)
1888-
(when completion-capability
1877+
;; Commit logs for this function help understand what's going on.
1878+
(when-let (completion-capability (eglot--server-capable :completionProvider))
1879+
(let* ((server (eglot--current-server-or-lose))
1880+
(sort-completions (lambda (completions)
1881+
(sort completions
1882+
(lambda (a b)
1883+
(string-lessp
1884+
(or (get-text-property 0 :sortText a) "")
1885+
(or (get-text-property 0 :sortText b) ""))))))
1886+
(metadata `(metadata . ((display-sort-function . ,sort-completions))))
1887+
(response (jsonrpc-request server
1888+
:textDocument/completion
1889+
(eglot--CompletionParams)
1890+
:deferred :textDocument/completion
1891+
:cancel-on-input t))
1892+
(items (append ; coerce to list
1893+
(if (vectorp response) response (plist-get response :items))
1894+
nil))
1895+
(proxies
1896+
(mapcar (jsonrpc-lambda
1897+
(&rest item &key label insertText insertTextFormat
1898+
&allow-other-keys)
1899+
(let ((proxy
1900+
(cond ((and (eql insertTextFormat 2)
1901+
(eglot--snippet-expansion-fn))
1902+
(string-trim-left label))
1903+
(t
1904+
(or insertText (string-trim-left label))))))
1905+
(put-text-property 0 1 'eglot--lsp-item item proxy)
1906+
proxy))
1907+
items))
1908+
(bounds
1909+
(cl-loop with probe =
1910+
(plist-get (plist-get (car items) :textEdit) :range)
1911+
for item in (cdr items)
1912+
for range = (plist-get (plist-get item :textEdit) :range)
1913+
unless (and range (equal range probe))
1914+
return (bounds-of-thing-at-point 'symbol)
1915+
finally (cl-return (or (and probe
1916+
(eglot--range-region probe))
1917+
(bounds-of-thing-at-point 'symbol))))))
18891918
(list
18901919
(or (car bounds) (point))
18911920
(or (cdr bounds) (point))
1892-
(lambda (comp pred action)
1921+
(lambda (probe pred action)
18931922
(cond
1894-
((eq action 'metadata) metadata) ; metadata
1895-
((eq action 'lambda) (member comp completions)) ; test-completion
1896-
((eq (car-safe action) 'boundaries) nil) ; boundaries
1897-
((and (null action) (member comp completions) t)) ; try-completion
1898-
((eq action t) ; all-completions
1899-
(let* ((resp (jsonrpc-request server
1900-
:textDocument/completion
1901-
(eglot--CompletionParams)
1902-
:deferred :textDocument/completion
1903-
:cancel-on-input t))
1904-
(items (if (vectorp resp) resp (plist-get resp :items))))
1905-
(setq
1906-
completions
1907-
(all-completions ; <-stuck with prefix-comp because <facepalm> LSP
1908-
comp
1909-
(mapcar
1910-
(jsonrpc-lambda
1911-
(&rest all &key label insertText insertTextFormat
1912-
&allow-other-keys)
1913-
(let ((completion
1914-
(cond ((and (eql insertTextFormat 2)
1915-
(eglot--snippet-expansion-fn))
1916-
(string-trim-left label))
1917-
(t
1918-
(or insertText (string-trim-left label))))))
1919-
(put-text-property 0 1 'eglot--lsp-completion
1920-
all completion)
1921-
completion))
1922-
items)
1923-
pred))))))
1923+
((eq action 'metadata) metadata) ; metadata
1924+
((eq action 'lambda) (member probe proxies)) ; test-completion
1925+
((eq (car-safe action) 'boundaries) nil) ; boundaries
1926+
((and (null action) (member probe proxies) t)) ; try-completion
1927+
((eq action t) ; all-completions
1928+
(cl-remove-if-not
1929+
(lambda (proxy)
1930+
(let* ((item (get-text-property 0 'eglot--lsp-item proxy))
1931+
(filterText (plist-get item :filterText)))
1932+
(and (or (null pred) (funcall pred proxy))
1933+
(string-prefix-p
1934+
probe (or filterText proxy) completion-ignore-case))))
1935+
proxies))))
19241936
:annotation-function
1925-
(lambda (obj)
1937+
(lambda (proxy)
19261938
(eglot--dbind ((CompletionItem) detail kind insertTextFormat)
1927-
(get-text-property 0 'eglot--lsp-completion obj)
1939+
(get-text-property 0 'eglot--lsp-item proxy)
19281940
(let* ((detail (and (stringp detail)
19291941
(not (string= detail ""))
19301942
detail))
@@ -1939,10 +1951,9 @@ is not active."
19391951
(eglot--snippet-expansion-fn)
19401952
" (snippet)"))))))
19411953
:company-doc-buffer
1942-
(lambda (obj)
1954+
(lambda (proxy)
19431955
(let* ((documentation
1944-
(let ((lsp-comp
1945-
(get-text-property 0 'eglot--lsp-completion obj)))
1956+
(let ((lsp-comp (get-text-property 0 'eglot--lsp-item proxy)))
19461957
(or (plist-get lsp-comp :documentation)
19471958
(and (eglot--server-capable :completionProvider
19481959
:resolveProvider)
@@ -1966,46 +1977,45 @@ is not active."
19661977
(cl-coerce (cl-getf completion-capability :triggerCharacters) 'list))
19671978
(line-beginning-position))))
19681979
:exit-function
1969-
(lambda (comp _status)
1970-
(let ((comp (if (get-text-property 0 'eglot--lsp-completion comp)
1971-
comp
1972-
;; When selecting from the *Completions*
1973-
;; buffer, `comp' won't have any properties. A
1974-
;; lookup should fix that (github#148)
1975-
(cl-find comp completions :test #'string=))))
1976-
(eglot--dbind ((CompletionItem) insertTextFormat
1977-
insertText
1978-
textEdit
1979-
additionalTextEdits)
1980-
(get-text-property 0 'eglot--lsp-completion comp)
1981-
(let ((snippet-fn (and (eql insertTextFormat 2)
1982-
(eglot--snippet-expansion-fn))))
1983-
(cond (textEdit
1984-
;; Undo the just the completed bit. If before
1985-
;; completion the buffer was "foo.b" and now is
1986-
;; "foo.bar", `comp' will be "bar". We want to
1987-
;; delete only "ar" (`comp' minus the symbol
1988-
;; whose bounds we've calculated before)
1989-
;; (github#160).
1990-
(delete-region (+ (- (point) (length comp))
1991-
(if bounds (- (cdr bounds) (car bounds)) 0))
1992-
(point))
1993-
(eglot--dbind ((TextEdit) range newText) textEdit
1994-
(pcase-let ((`(,beg . ,end) (eglot--range-region range)))
1995-
(delete-region beg end)
1996-
(goto-char beg)
1997-
(funcall (or snippet-fn #'insert) newText)))
1998-
(when (cl-plusp (length additionalTextEdits))
1999-
(eglot--apply-text-edits additionalTextEdits)))
2000-
(snippet-fn
2001-
;; A snippet should be inserted, but using plain
2002-
;; `insertText'. This requires us to delete the
2003-
;; whole completion, since `insertText' is the full
2004-
;; completion's text.
2005-
(delete-region (- (point) (length comp)) (point))
2006-
(funcall snippet-fn insertText))))
2007-
(eglot--signal-textDocument/didChange)
2008-
(eglot-eldoc-function))))))))
1980+
(lambda (proxy _status)
1981+
(eglot--dbind ((CompletionItem) insertTextFormat
1982+
insertText
1983+
textEdit
1984+
additionalTextEdits)
1985+
(or (get-text-property 0 'eglot--lsp-item proxy)
1986+
;; When selecting from the *Completions*
1987+
;; buffer, `proxy' won't have any properties. A
1988+
;; lookup should fix that (github#148)
1989+
(get-text-property
1990+
0 'eglot--lsp-item (cl-find proxy proxies :test #'string=)))
1991+
(let ((snippet-fn (and (eql insertTextFormat 2)
1992+
(eglot--snippet-expansion-fn))))
1993+
(cond (textEdit
1994+
;; Undo (yes, undo) the newly inserted completion.
1995+
;; If before completion the buffer was "foo.b" and
1996+
;; now is "foo.bar", `proxy' will be "bar". We
1997+
;; want to delete only "ar" (`proxy' minus the
1998+
;; symbol whose bounds we've calculated before)
1999+
;; (github#160).
2000+
(delete-region (+ (- (point) (length proxy))
2001+
(if bounds (- (cdr bounds) (car bounds)) 0))
2002+
(point))
2003+
(eglot--dbind ((TextEdit) range newText) textEdit
2004+
(pcase-let ((`(,beg . ,end) (eglot--range-region range)))
2005+
(delete-region beg end)
2006+
(goto-char beg)
2007+
(funcall (or snippet-fn #'insert) newText)))
2008+
(when (cl-plusp (length additionalTextEdits))
2009+
(eglot--apply-text-edits additionalTextEdits)))
2010+
(snippet-fn
2011+
;; A snippet should be inserted, but using plain
2012+
;; `insertText'. This requires us to delete the
2013+
;; whole completion, since `insertText' is the full
2014+
;; completion's text.
2015+
(delete-region (- (point) (length proxy)) (point))
2016+
(funcall snippet-fn insertText))))
2017+
(eglot--signal-textDocument/didChange)
2018+
(eglot-eldoc-function)))))))
20092019

20102020
(defvar eglot--highlights nil "Overlays for textDocument/documentHighlight.")
20112021

0 commit comments

Comments
 (0)