[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[elpa] externals/eglot 8a17c29 61/69: Close #235: play along with LSP's
From: |
João Távora |
Subject: |
[elpa] externals/eglot 8a17c29 61/69: Close #235: play along with LSP's filterText hacks |
Date: |
Sun, 20 Oct 2019 08:21:54 -0400 (EDT) |
branch: externals/eglot
commit 8a17c29169c83981a31312d0eb17268d5cdbc50d
Author: João Távora <address@hidden>
Commit: João Távora <address@hidden>
Close #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
https://github.com/microsoft/language-server-protocol/issues/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.
---
eglot.el | 186 +++++++++++++++++++++++++++++++++------------------------------
1 file changed, 98 insertions(+), 88 deletions(-)
diff --git a/eglot.el b/eglot.el
index ce3705a..9460e4b 100644
--- a/eglot.el
+++ b/eglot.el
@@ -1874,57 +1874,69 @@ is not active."
(defun eglot-completion-at-point ()
"EGLOT's `completion-at-point' function."
- (let* ((bounds (bounds-of-thing-at-point 'symbol))
- (server (eglot--current-server-or-lose))
- (completion-capability (eglot--server-capable :completionProvider))
- (sort-completions (lambda (completions)
- (sort completions
- (lambda (a b)
- (string-lessp
- (or (get-text-property 0 :sortText a) "")
- (or (get-text-property 0 :sortText b)
""))))))
- (metadata `(metadata . ((display-sort-function . ,sort-completions))))
- completions)
- (when completion-capability
+ ;; Commit logs for this function help understand what's going on.
+ (when-let (completion-capability (eglot--server-capable :completionProvider))
+ (let* ((server (eglot--current-server-or-lose))
+ (sort-completions (lambda (completions)
+ (sort completions
+ (lambda (a b)
+ (string-lessp
+ (or (get-text-property 0 :sortText a)
"")
+ (or (get-text-property 0 :sortText b)
""))))))
+ (metadata `(metadata . ((display-sort-function .
,sort-completions))))
+ (response (jsonrpc-request server
+ :textDocument/completion
+ (eglot--CompletionParams)
+ :deferred :textDocument/completion
+ :cancel-on-input t))
+ (items (append ; coerce to list
+ (if (vectorp response) response (plist-get response :items))
+ nil))
+ (proxies
+ (mapcar (jsonrpc-lambda
+ (&rest item &key label insertText insertTextFormat
+ &allow-other-keys)
+ (let ((proxy
+ (cond ((and (eql insertTextFormat 2)
+ (eglot--snippet-expansion-fn))
+ (string-trim-left label))
+ (t
+ (or insertText (string-trim-left
label))))))
+ (put-text-property 0 1 'eglot--lsp-item item proxy)
+ proxy))
+ items))
+ (bounds
+ (cl-loop with probe =
+ (plist-get (plist-get (car items) :textEdit) :range)
+ for item in (cdr items)
+ for range = (plist-get (plist-get item :textEdit) :range)
+ unless (and range (equal range probe))
+ return (bounds-of-thing-at-point 'symbol)
+ finally (cl-return (or (and probe
+ (eglot--range-region probe))
+ (bounds-of-thing-at-point
'symbol))))))
(list
(or (car bounds) (point))
(or (cdr bounds) (point))
- (lambda (comp pred action)
+ (lambda (probe pred action)
(cond
- ((eq action 'metadata) metadata) ; metadata
- ((eq action 'lambda) (member comp completions)) ; test-completion
- ((eq (car-safe action) 'boundaries) nil) ; boundaries
- ((and (null action) (member comp completions) t)) ; try-completion
- ((eq action t) ; all-completions
- (let* ((resp (jsonrpc-request server
- :textDocument/completion
- (eglot--CompletionParams)
- :deferred :textDocument/completion
- :cancel-on-input t))
- (items (if (vectorp resp) resp (plist-get resp :items))))
- (setq
- completions
- (all-completions ; <-stuck with prefix-comp because <facepalm>
LSP
- comp
- (mapcar
- (jsonrpc-lambda
- (&rest all &key label insertText insertTextFormat
- &allow-other-keys)
- (let ((completion
- (cond ((and (eql insertTextFormat 2)
- (eglot--snippet-expansion-fn))
- (string-trim-left label))
- (t
- (or insertText (string-trim-left label))))))
- (put-text-property 0 1 'eglot--lsp-completion
- all completion)
- completion))
- items)
- pred))))))
+ ((eq action 'metadata) metadata) ; metadata
+ ((eq action 'lambda) (member probe proxies)) ; test-completion
+ ((eq (car-safe action) 'boundaries) nil) ; boundaries
+ ((and (null action) (member probe proxies) t)) ; try-completion
+ ((eq action t) ; all-completions
+ (cl-remove-if-not
+ (lambda (proxy)
+ (let* ((item (get-text-property 0 'eglot--lsp-item proxy))
+ (filterText (plist-get item :filterText)))
+ (and (or (null pred) (funcall pred proxy))
+ (string-prefix-p
+ probe (or filterText proxy) completion-ignore-case))))
+ proxies))))
:annotation-function
- (lambda (obj)
+ (lambda (proxy)
(eglot--dbind ((CompletionItem) detail kind insertTextFormat)
- (get-text-property 0 'eglot--lsp-completion obj)
+ (get-text-property 0 'eglot--lsp-item proxy)
(let* ((detail (and (stringp detail)
(not (string= detail ""))
detail))
@@ -1939,10 +1951,9 @@ is not active."
(eglot--snippet-expansion-fn)
" (snippet)"))))))
:company-doc-buffer
- (lambda (obj)
+ (lambda (proxy)
(let* ((documentation
- (let ((lsp-comp
- (get-text-property 0 'eglot--lsp-completion obj)))
+ (let ((lsp-comp (get-text-property 0 'eglot--lsp-item proxy)))
(or (plist-get lsp-comp :documentation)
(and (eglot--server-capable :completionProvider
:resolveProvider)
@@ -1966,46 +1977,45 @@ is not active."
(cl-coerce (cl-getf completion-capability :triggerCharacters)
'list))
(line-beginning-position))))
:exit-function
- (lambda (comp _status)
- (let ((comp (if (get-text-property 0 'eglot--lsp-completion comp)
- comp
- ;; When selecting from the *Completions*
- ;; buffer, `comp' won't have any properties. A
- ;; lookup should fix that (github#148)
- (cl-find comp completions :test #'string=))))
- (eglot--dbind ((CompletionItem) insertTextFormat
- insertText
- textEdit
- additionalTextEdits)
- (get-text-property 0 'eglot--lsp-completion comp)
- (let ((snippet-fn (and (eql insertTextFormat 2)
- (eglot--snippet-expansion-fn))))
- (cond (textEdit
- ;; Undo the just the completed bit. If before
- ;; completion the buffer was "foo.b" and now is
- ;; "foo.bar", `comp' will be "bar". We want to
- ;; delete only "ar" (`comp' minus the symbol
- ;; whose bounds we've calculated before)
- ;; (github#160).
- (delete-region (+ (- (point) (length comp))
- (if bounds (- (cdr bounds) (car
bounds)) 0))
- (point))
- (eglot--dbind ((TextEdit) range newText) textEdit
- (pcase-let ((`(,beg . ,end) (eglot--range-region
range)))
- (delete-region beg end)
- (goto-char beg)
- (funcall (or snippet-fn #'insert) newText)))
- (when (cl-plusp (length additionalTextEdits))
- (eglot--apply-text-edits additionalTextEdits)))
- (snippet-fn
- ;; A snippet should be inserted, but using plain
- ;; `insertText'. This requires us to delete the
- ;; whole completion, since `insertText' is the full
- ;; completion's text.
- (delete-region (- (point) (length comp)) (point))
- (funcall snippet-fn insertText))))
- (eglot--signal-textDocument/didChange)
- (eglot-eldoc-function))))))))
+ (lambda (proxy _status)
+ (eglot--dbind ((CompletionItem) insertTextFormat
+ insertText
+ textEdit
+ additionalTextEdits)
+ (or (get-text-property 0 'eglot--lsp-item proxy)
+ ;; When selecting from the *Completions*
+ ;; buffer, `proxy' won't have any properties. A
+ ;; lookup should fix that (github#148)
+ (get-text-property
+ 0 'eglot--lsp-item (cl-find proxy proxies :test #'string=)))
+ (let ((snippet-fn (and (eql insertTextFormat 2)
+ (eglot--snippet-expansion-fn))))
+ (cond (textEdit
+ ;; Undo (yes, undo) the newly inserted completion.
+ ;; If before completion the buffer was "foo.b" and
+ ;; now is "foo.bar", `proxy' will be "bar". We
+ ;; want to delete only "ar" (`proxy' minus the
+ ;; symbol whose bounds we've calculated before)
+ ;; (github#160).
+ (delete-region (+ (- (point) (length proxy))
+ (if bounds (- (cdr bounds) (car bounds))
0))
+ (point))
+ (eglot--dbind ((TextEdit) range newText) textEdit
+ (pcase-let ((`(,beg . ,end) (eglot--range-region range)))
+ (delete-region beg end)
+ (goto-char beg)
+ (funcall (or snippet-fn #'insert) newText)))
+ (when (cl-plusp (length additionalTextEdits))
+ (eglot--apply-text-edits additionalTextEdits)))
+ (snippet-fn
+ ;; A snippet should be inserted, but using plain
+ ;; `insertText'. This requires us to delete the
+ ;; whole completion, since `insertText' is the full
+ ;; completion's text.
+ (delete-region (- (point) (length proxy)) (point))
+ (funcall snippet-fn insertText))))
+ (eglot--signal-textDocument/didChange)
+ (eglot-eldoc-function)))))))
(defvar eglot--highlights nil "Overlays for textDocument/documentHighlight.")
- [elpa] externals/eglot 9951dc5 62/69: Add a test for vscode-json-languageserver's completions, (continued)
- [elpa] externals/eglot 9951dc5 62/69: Add a test for vscode-json-languageserver's completions, João Távora, 2019/10/20
- [elpa] externals/eglot 124a833 56/69: Add a test for #311 and #279, João Távora, 2019/10/20
- [elpa] externals/eglot d774754 37/69: Merge pull request #298 from jorams/nil-capabilities-as-false, João Távora, 2019/10/20
- [elpa] externals/eglot 20195e5 42/69: On buffer kill, first send didClose then teardown local structures, João Távora, 2019/10/20
- [elpa] externals/eglot 5a98c5a 55/69: Fix eglot-completion-at-point to work with bare completion-at-point, João Távora, 2019/10/20
- [elpa] externals/eglot 6a7ce66 32/69: Expand directory watcher globs containing ** (#293), João Távora, 2019/10/20
- [elpa] externals/eglot f45fdc6 31/69: Fix invalid guess for php language Server (#288), João Távora, 2019/10/20
- [elpa] externals/eglot 05c68c2 45/69: Don't send dummy JSON object in "initialized" notification (#312), João Távora, 2019/10/20
- [elpa] externals/eglot bff921e 60/69: Per #319: always filter completions client-side by prefix, João Távora, 2019/10/20
- [elpa] externals/eglot 260f152 65/69: Fix #321: don't choke on single-location reply to tD/definition, João Távora, 2019/10/20
- [elpa] externals/eglot 8a17c29 61/69: Close #235: play along with LSP's filterText hacks,
João Távora <=
- [elpa] externals/eglot f5151be 67/69: Don't immediately request completions in eglot-completion-at-point, João Távora, 2019/10/20
- [elpa] externals/eglot 74240c7 66/69: Protect against zero-length completions, João Távora, 2019/10/20