* bug#72705: 31.0.50; eglot--dumb-tryc Filters out too much @ 2024-08-19 1:51 Dmitry Gutov 2024-08-19 9:22 ` João Távora 0 siblings, 1 reply; 16+ messages in thread From: Dmitry Gutov @ 2024-08-19 1:51 UTC (permalink / raw) To: 72705; +Cc: joaotavora [-- Attachment #1: Type: text/plain, Size: 2169 bytes --] X-Debbugs-Cc: joaotavora@gmail.com 1. Have a project with just one file, test.go (attached). 2. Visit it, enable go-ts-mode, call 'M-x eglot'. 3. Move point right after "math.As", press C-M-i. 4. Code will get completed to "math.Asin" This is a problem because "math.As" has more completions (one can look at them by pressing TAB after "math.A") - such as "Acos", "Acosh" and "Abs" - in other words, "fuzzy" matches. Expected behavior: 1. When input is "math.As" - keep the string as-is. 2. When input is "math.Asi" - complete to "math.Asin". This problem seems older than 65ea742ed5ec (the change that introduced eglot--dumb-tryc itself, bug#68699), but it doesn't reproduce with Eglot from Emacs 29. Branches emacs-30 and master are affected. There is also another issue there: when there are no completions at all, this style still has completion-try-completion return a non-nil value (the last line of the implementation). Both of these issues is something I came across when working on closer 'try-completion' integration for company-mode (https://github.com/company-mode/company-mode/pull/1488) and testing out the new code with Eglot. Not sure what's the best fix, but the patch below seems to address both problems in my limited testing. WDYT? diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 353877f60c2..e8823a9d2f0 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -3142,8 +3142,14 @@ eglot--dumb-allc (defun eglot--dumb-tryc (pat table pred point) (let ((probe (funcall table pat pred nil))) (cond ((eq probe t) t) - (probe (cons probe (length probe))) - (t (cons pat point))))) + ((and probe + (cl-every + (lambda (s) (string-prefix-p probe s completion-ignore-case)) + (funcall table pat pred t))) + (cons probe (length probe))) + (t + (and (funcall table pat pred t) + (cons pat point)))))) (add-to-list 'completion-category-defaults '(eglot-capf (styles eglot--dumb-flex))) (add-to-list 'completion-styles-alist '(eglot--dumb-flex eglot--dumb-tryc eglot--dumb-allc)) [-- Attachment #2: test.go --] [-- Type: text/x-go, Size: 54 bytes --] package test import "math" func main() { math.As } ^ permalink raw reply related [flat|nested] 16+ messages in thread
* bug#72705: 31.0.50; eglot--dumb-tryc Filters out too much 2024-08-19 1:51 bug#72705: 31.0.50; eglot--dumb-tryc Filters out too much Dmitry Gutov @ 2024-08-19 9:22 ` João Távora 2024-08-19 11:39 ` Dmitry Gutov 0 siblings, 1 reply; 16+ messages in thread From: João Távora @ 2024-08-19 9:22 UTC (permalink / raw) To: Dmitry Gutov; +Cc: 72705 On Mon, Aug 19, 2024 at 2:52 AM Dmitry Gutov <dmitry@gutov.dev> wrote: > has more completions > (one can look at them by pressing TAB after "math.A") That's not relevant. LSP servers can make completely different decisions in each position. What's relevant is to look at the completion list returned in the "math.As" case (via the events buffer, for example). > Not sure what's the best fix, but the patch below seems to address both > problems in my limited testing. WDYT? I would be (pleasantly) surprised if your change doesn't break something else. I tried many many variations in the fairly recent past and each of them seemed to break something. Be wary of :exit-function which is where things like snippet conversion and edits happen. So I'd say this need more testing, especially with more servers. I also don't understand why the "string-prefix" is being used. The "dumb" in eglot--dumb-try should be taken to heart. See logs of commits: d376462c7183752bf44b9bd20bf5020fe7eaf75a 4dcbf61c1518dc53061707aeff8887517e050003 a6ef458e3831001b0acad57cf8fa75b77a4aff3f You should also encode whatever decisions you make in tests in eglot-tests.el. ^ permalink raw reply [flat|nested] 16+ messages in thread
* bug#72705: 31.0.50; eglot--dumb-tryc Filters out too much 2024-08-19 9:22 ` João Távora @ 2024-08-19 11:39 ` Dmitry Gutov 2024-08-19 12:59 ` João Távora 0 siblings, 1 reply; 16+ messages in thread From: Dmitry Gutov @ 2024-08-19 11:39 UTC (permalink / raw) To: João Távora; +Cc: 72705 On 19/08/2024 12:22, João Távora wrote: > On Mon, Aug 19, 2024 at 2:52 AM Dmitry Gutov <dmitry@gutov.dev> wrote: > >> has more completions >> (one can look at them by pressing TAB after "math.A") > > That's not relevant. LSP servers can make completely different > decisions in each position. What's relevant is to look at the completion > list returned in the "math.As" case (via the events buffer, for example). True, but it's hard to look at completions for "math.As" using completion-at-point because try-completion behaves this way. You can also check out Company's completions popup for "math.As" - it shows the extra ones. >> Not sure what's the best fix, but the patch below seems to address both >> problems in my limited testing. WDYT? > > I would be (pleasantly) surprised if your change doesn't break something > else. I tried many many variations in the fairly recent past and each > of them seemed to break something. Be wary of :exit-function > which is where things like snippet conversion and edits happen. > So I'd say this need more testing, especially with more servers. Here's hoping. > I also don't understand why the "string-prefix" is being used. > The "dumb" in eglot--dumb-try should be taken to heart. string-prefix-p is being used because 'try-completion' will ignore non-prefix completions as if they weren't there. We need to make sure that the proposed "probe" is indeed a prefix for all completions. > See logs of commits: > d376462c7183752bf44b9bd20bf5020fe7eaf75a > 4dcbf61c1518dc53061707aeff8887517e050003 > a6ef458e3831001b0acad57cf8fa75b77a4aff3f > > You should also encode whatever decisions you make in > tests in eglot-tests.el. Thanks for the references, I'll dig in more. Surprised to hear that exit-function can be affected. ^ permalink raw reply [flat|nested] 16+ messages in thread
* bug#72705: 31.0.50; eglot--dumb-tryc Filters out too much 2024-08-19 11:39 ` Dmitry Gutov @ 2024-08-19 12:59 ` João Távora 2024-08-20 2:08 ` Dmitry Gutov 0 siblings, 1 reply; 16+ messages in thread From: João Távora @ 2024-08-19 12:59 UTC (permalink / raw) To: Dmitry Gutov; +Cc: 72705 On Mon, Aug 19, 2024 at 12:39 PM Dmitry Gutov <dmitry@gutov.dev> wrote: > > So I'd say this need more testing, especially with more servers. > Here's hoping. If you want this change, you'll have to do the testing. > Thanks for the references, I'll dig in more. > > Surprised to hear that exit-function can be affected. exit-function needs a fully formed completion. try-completion's and Emacs partial complete semantics aren't compatible. ISTR that even a full completion obtained via try-completion would sometimes not run the function. Maybe I'm misremembering. This whole system is held up by very thin wires unfortunately, and a lot of people are relying on those wires. The best choice to use LSP's completions, which were obviously modelled after other IDEs where completions are much simpler and similar to the Company tooltip... is naturally to use something like company (or corfu with some patch applied). I hope you don't change any Company behaviour backward-incompatibly (though I have my fork anyway, so no problem). João ^ permalink raw reply [flat|nested] 16+ messages in thread
* bug#72705: 31.0.50; eglot--dumb-tryc Filters out too much 2024-08-19 12:59 ` João Távora @ 2024-08-20 2:08 ` Dmitry Gutov 2024-08-20 9:40 ` João Távora 0 siblings, 1 reply; 16+ messages in thread From: Dmitry Gutov @ 2024-08-20 2:08 UTC (permalink / raw) To: João Távora; +Cc: 72705 [-- Attachment #1: Type: text/plain, Size: 2890 bytes --] On 19/08/2024 15:59, João Távora wrote: > On Mon, Aug 19, 2024 at 12:39 PM Dmitry Gutov <dmitry@gutov.dev> wrote: > >>> So I'd say this need more testing, especially with more servers. >> Here's hoping. > > If you want this change, you'll have to do the testing. So far I've tested with gopls and clangd (when writing the new tests), typescript-language-server and rust-analyzer. >> Thanks for the references, I'll dig in more. >> >> Surprised to hear that exit-function can be affected. > > exit-function needs a fully formed completion. try-completion's > and Emacs partial complete semantics aren't compatible. ISTR > that even a full completion obtained via try-completion would > sometimes not run the function. Maybe I'm misremembering. All right. The proposed change doesn't alter the kinds of strings that are inserted, only the cases when that happens. When the added predicate returns nil, we fall back to the next clause; and the check at the end also allows us to return nil, which is useful in certain rare contexts. > This whole system is held up by very thin wires unfortunately, > and a lot of people are relying on those wires. The best choice > to use LSP's completions, which were obviously modelled after > other IDEs where completions are much simpler and similar to > the Company tooltip... is naturally to use something like > company (or corfu with some patch applied). > > I hope you don't change any Company behaviour backward-incompatibly > (though I have my fork anyway, so no problem). The change is in a pending PR, it's designed to be opt-in on the backend level, but company-capf is a separate backend, so... you know. It alters what happens when you explicitly press TAB, which previously only did prefix-matching inside Company code, but now can delegate to backends for some extra smarts (longer expansions being the goal), which unfortunately don't work too well with LSP. The report that's referenced in the 3 commits your mentioned does seem to have regressed is https://github.com/joaotavora/eglot/issues/1339 - not to the original behavior (exit-function still works), but C-M-i changes buffer text to v.call1234.1234567890 Not sure how important that case is, but it's a consequence of having the style return nil in try-completion and unavoidable failover to 'basic' because of that (in completion--styles, because a category can't actually override, only prepend styles). I suppose that could be fixed by moving some matching logic from the style into the completion table. Not sure how important/realistic the example is, although somebody did report it... In the meantime, here's an updated patch with 3 new tests (and existing ones all passing). The implementation has been retouched too for better performance (the typescript server in particular made things slower with the previous proposal). [-- Attachment #2: eglot--dumb-tryc-more-checks.diff --] [-- Type: text/x-patch, Size: 3445 bytes --] diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 353877f60c2..744f2b26c3b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -3142,8 +3142,16 @@ eglot--dumb-allc (defun eglot--dumb-tryc (pat table pred point) (let ((probe (funcall table pat pred nil))) (cond ((eq probe t) t) - (probe (cons probe (length probe))) - (t (cons pat point))))) + (probe + (if (and (not (equal probe pat)) + (cl-every + (lambda (s) (string-prefix-p probe s completion-ignore-case)) + (funcall table pat pred t))) + (cons probe (length probe)) + (cons pat point))) + (t + (and (funcall table pat pred t) + (cons pat point)))))) (add-to-list 'completion-category-defaults '(eglot-capf (styles eglot--dumb-flex))) (add-to-list 'completion-styles-alist '(eglot--dumb-flex eglot--dumb-tryc eglot--dumb-allc)) diff --git a/test/lisp/progmodes/eglot-tests.el b/test/lisp/progmodes/eglot-tests.el index 534c47b2646..625be2ea30f 100644 --- a/test/lisp/progmodes/eglot-tests.el +++ b/test/lisp/progmodes/eglot-tests.el @@ -600,6 +600,20 @@ eglot-test-basic-completions (message (buffer-string)) (should (looking-back "fprintf.?"))))) +(ert-deftest eglot-test-common-prefix-completion () + "Test completion appending the common prefix." + (skip-unless (executable-find "clangd")) + (eglot--with-fixture + `(("project" . (("coiso.c" . + ,(concat "int foo_bar; int foo_bar_baz;" + "int main() {foo"))))) + (with-current-buffer + (eglot--find-file-noselect "project/coiso.c") + (eglot--wait-for-clangd) + (goto-char (point-max)) + (completion-at-point) + (should (looking-back "{foo_bar"))))) + (ert-deftest eglot-test-non-unique-completions () "Test completion resulting in 'Complete, but not unique'." (skip-unless (executable-find "clangd")) @@ -619,6 +633,36 @@ eglot-test-non-unique-completions (forward-line -1) (should (looking-at "Complete, but not unique"))))))) +(ert-deftest eglot-test-stop-completion-on-nonprefix () + "Test completion also resulting in 'Complete, but not unique'." + (skip-unless (executable-find "clangd")) + (eglot--with-fixture + `(("project" . (("coiso.c" . + ,(concat "int foot; int footer; int fo_obar;" + "int main() {foo"))))) + (with-current-buffer + (eglot--find-file-noselect "project/coiso.c") + (eglot--wait-for-clangd) + (goto-char (point-max)) + (completion-at-point) + (should (looking-back "foo"))))) + +(ert-deftest eglot-test-try-completion-nomatch () + "Test completion table with non-matching input, returning nil." + (skip-unless (executable-find "clangd")) + (eglot--with-fixture + `(("project" . (("coiso.c" . + ,(concat "int main() {abc"))))) + (with-current-buffer + (eglot--find-file-noselect "project/coiso.c") + (eglot--wait-for-clangd) + (goto-char (point-max)) + (should + (null + (completion-try-completion + "abc" + (nth 2 (eglot-completion-at-point)) nil 3)))))) + (ert-deftest eglot-test-basic-xref () "Test basic xref functionality in a clangd LSP." (skip-unless (executable-find "clangd")) ^ permalink raw reply related [flat|nested] 16+ messages in thread
* bug#72705: 31.0.50; eglot--dumb-tryc Filters out too much 2024-08-20 2:08 ` Dmitry Gutov @ 2024-08-20 9:40 ` João Távora 2024-08-21 0:30 ` Dmitry Gutov 0 siblings, 1 reply; 16+ messages in thread From: João Távora @ 2024-08-20 9:40 UTC (permalink / raw) To: Dmitry Gutov; +Cc: 72705 On Tue, Aug 20, 2024 at 3:08 AM Dmitry Gutov <dmitry@gutov.dev> wrote: > So far I've tested with gopls and clangd (when writing the new tests), > typescript-language-server and rust-analyzer. That's pretty good. If you could add pylsp or pyright (basedpyright), I'd feel even more confident. > All right. > > The proposed change doesn't alter the kinds of strings that are > inserted, only the cases when that happens. When the added predicate > returns nil, we fall back to the next clause; and the check at the end > also allows us to return nil, which is useful in certain rare contexts. I don't have time to spend examining these details, just the final outcome is relevant: partial completions cannot be inserted, but fragments of a completion can be completed to a fully formed completion (eventually running exit-function then). > The report that's referenced in the 3 commits your mentioned does seem > to have regressed is https://github.com/joaotavora/eglot/issues/1339 > not to the original behavior (exit-function still works), but C-M-i > changes buffer text to > > v.call1234.1234567890 I don't have time to investigate right now, but indeed that issue there were two consecutive fixes: the first one (which you say "still works") and a second one which I eventually reverted because it fixes some things and broke others. So is the regression you mention in relation to the current Eglot state or to that intermediate state? Regardless, if possible, make an automated test and mark it "expected failing" to record this fact. > I suppose that could be fixed by moving some matching logic from the > style into the completion table. Not sure how important/realistic the > example is, although somebody did report it... I'd have to have a clearer view on what exactly has regressed and what has advanced. Then a better decision can be made. Thank you for this work. João ^ permalink raw reply [flat|nested] 16+ messages in thread
* bug#72705: 31.0.50; eglot--dumb-tryc Filters out too much 2024-08-20 9:40 ` João Távora @ 2024-08-21 0:30 ` Dmitry Gutov 2024-08-21 16:52 ` João Távora 0 siblings, 1 reply; 16+ messages in thread From: Dmitry Gutov @ 2024-08-21 0:30 UTC (permalink / raw) To: João Távora; +Cc: 72705 [-- Attachment #1: Type: text/plain, Size: 2578 bytes --] On 20/08/2024 12:40, João Távora wrote: > On Tue, Aug 20, 2024 at 3:08 AM Dmitry Gutov <dmitry@gutov.dev> wrote: > >> So far I've tested with gopls and clangd (when writing the new tests), >> typescript-language-server and rust-analyzer. > > That's pretty good. If you could add pylsp or pyright (basedpyright), > I'd feel even more confident. Installed pylsp with apt - the basic things I tested seem to work too. > I don't have time to spend examining these details, just the final > outcome is relevant: partial completions cannot be inserted, but > fragments of a completion can be completed to a fully formed > completion (eventually running exit-function then). And only fragments that prefix-match all completions as prefix. exit-function seems to function. >> The report that's referenced in the 3 commits your mentioned does seem >> to have regressed is https://github.com/joaotavora/eglot/issues/1339 >> not to the original behavior (exit-function still works), but C-M-i >> changes buffer text to >> >> v.call1234.1234567890 > > I don't have time to investigate right now, but indeed that issue there were > two consecutive fixes: the first one (which you say "still works") and a second > one which I eventually reverted because it fixes some things and broke > others. Now that I've looked at them a few more times - all 3 changes were in the completion table, whereas I'm altering the completion style here. And indeed what seems to be the crux of the previous improvement (working exit-function) stays around. > So is the regression you mention in relation to the current Eglot state or > to that intermediate state? In relation to the current state, but I think I've figured it out - see the new revision attached. So what I think what was happening, is since the style was returning nil when the input has a non-matching suffix, the 'emacs22' style came into play (style which only looks at the prefix) and expanded the input from "c" to "call". Anyway, the attached avoids the failover by returning non-nil in the last clause, if at least the prefix matches the table. That makes "C-M-i" keep the input at "v.c|1234" (with two completions: "match" and "call") - but selecting either of the completions using Company or the mouse, or M-RET, expands each into the corresponding snippet, which seems most optimal all-around. See the new test 'eglot-test-try-completion-inside-symbol' inside the patch. Wasn't sure whether to make it use 'completion-try-completion' or 'completion-at-point', but the former seems more explicit. [-- Attachment #2: eglot--dumb-tryc-more-checks-v2.diff --] [-- Type: text/x-patch, Size: 4279 bytes --] diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 353877f60c2..59d9c346424 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -3142,8 +3142,18 @@ eglot--dumb-allc (defun eglot--dumb-tryc (pat table pred point) (let ((probe (funcall table pat pred nil))) (cond ((eq probe t) t) - (probe (cons probe (length probe))) - (t (cons pat point))))) + (probe + (if (and (not (equal probe pat)) + (cl-every + (lambda (s) (string-prefix-p probe s completion-ignore-case)) + (funcall table pat pred t))) + (cons probe (length probe)) + (cons pat point))) + (t + ;; Match ignoring suffix: if there are any completions for + ;; the current prefix at least, keep the current input. + (and (funcall table (substring pat 0 point) pred t) + (cons pat point)))))) (add-to-list 'completion-category-defaults '(eglot-capf (styles eglot--dumb-flex))) (add-to-list 'completion-styles-alist '(eglot--dumb-flex eglot--dumb-tryc eglot--dumb-allc)) diff --git a/test/lisp/progmodes/eglot-tests.el b/test/lisp/progmodes/eglot-tests.el index 534c47b2646..4f745b26b42 100644 --- a/test/lisp/progmodes/eglot-tests.el +++ b/test/lisp/progmodes/eglot-tests.el @@ -600,6 +600,20 @@ eglot-test-basic-completions (message (buffer-string)) (should (looking-back "fprintf.?"))))) +(ert-deftest eglot-test-common-prefix-completion () + "Test completion appending the common prefix." + (skip-unless (executable-find "clangd")) + (eglot--with-fixture + `(("project" . (("coiso.c" . + ,(concat "int foo_bar; int foo_bar_baz;" + "int main() {foo"))))) + (with-current-buffer + (eglot--find-file-noselect "project/coiso.c") + (eglot--wait-for-clangd) + (goto-char (point-max)) + (completion-at-point) + (should (looking-back "{foo_bar"))))) + (ert-deftest eglot-test-non-unique-completions () "Test completion resulting in 'Complete, but not unique'." (skip-unless (executable-find "clangd")) @@ -619,6 +633,55 @@ eglot-test-non-unique-completions (forward-line -1) (should (looking-at "Complete, but not unique"))))))) +(ert-deftest eglot-test-stop-completion-on-nonprefix () + "Test completion also resulting in 'Complete, but not unique'." + (skip-unless (executable-find "clangd")) + (eglot--with-fixture + `(("project" . (("coiso.c" . + ,(concat "int foot; int footer; int fo_obar;" + "int main() {foo"))))) + (with-current-buffer + (eglot--find-file-noselect "project/coiso.c") + (eglot--wait-for-clangd) + (goto-char (point-max)) + (completion-at-point) + (should (looking-back "foo"))))) + +(ert-deftest eglot-test-try-completion-nomatch () + "Test completion table with non-matching input, returning nil." + (skip-unless (executable-find "clangd")) + (eglot--with-fixture + `(("project" . (("coiso.c" . + ,(concat "int main() {abc"))))) + (with-current-buffer + (eglot--find-file-noselect "project/coiso.c") + (eglot--wait-for-clangd) + (goto-char (point-max)) + (should + (null + (completion-try-completion + "abc" + (nth 2 (eglot-completion-at-point)) nil 3)))))) + +(ert-deftest eglot-test-try-completion-inside-symbol () + "Test completion table inside symbol, with only prefix matching." + (skip-unless (executable-find "clangd")) + (eglot--with-fixture + `(("project" . (("coiso.c" . + ,(concat + "int foobar;" + "int main() {foo123"))))) + (with-current-buffer + (eglot--find-file-noselect "project/coiso.c") + (eglot--wait-for-clangd) + (goto-char (- (point-max) 3)) + (should + (equal + '("foo123" . 3) + (completion-try-completion + "foo123" + (nth 2 (eglot-completion-at-point)) nil 3)))))) + (ert-deftest eglot-test-basic-xref () "Test basic xref functionality in a clangd LSP." (skip-unless (executable-find "clangd")) ^ permalink raw reply related [flat|nested] 16+ messages in thread
* bug#72705: 31.0.50; eglot--dumb-tryc Filters out too much 2024-08-21 0:30 ` Dmitry Gutov @ 2024-08-21 16:52 ` João Távora 2024-08-22 0:41 ` Dmitry Gutov 0 siblings, 1 reply; 16+ messages in thread From: João Távora @ 2024-08-21 16:52 UTC (permalink / raw) To: Dmitry Gutov; +Cc: 72705 Dmitry Gutov <dmitry@gutov.dev> writes: > On 20/08/2024 12:40, João Távora wrote: >> On Tue, Aug 20, 2024 at 3:08 AM Dmitry Gutov <dmitry@gutov.dev> wrote: >> >>> So far I've tested with gopls and clangd (when writing the new tests), >>> typescript-language-server and rust-analyzer. >> That's pretty good. If you could add pylsp or pyright >> (basedpyright), >> I'd feel even more confident. > > Installed pylsp with apt - the basic things I tested seem to work too. Great. >> I don't have time to spend examining these details, just the final >> outcome is relevant: partial completions cannot be inserted, but >> fragments of a completion can be completed to a fully formed >> completion (eventually running exit-function then). > > And only fragments that prefix-match all completions as > prefix. exit-function seems to function. Also good. > In relation to the current state, but I think I've figured it out - > see the new revision attached. Great. > See the new test 'eglot-test-try-completion-inside-symbol' inside the > patch. Wasn't sure whether to make it use 'completion-try-completion' > or 'completion-at-point', but the former seems more explicit. This all looks very good, especially the abundant tests. You may push right now if you want. I'll just comment on those: +(ert-deftest eglot-test-common-prefix-completion () + "Test completion appending the common prefix." + (skip-unless (executable-find "clangd")) + (eglot--with-fixture + `(("project" . (("coiso.c" . Eh. You may replace "coiso.c" for the Russian equivalent of "thingy.c" if you want :-). Or something more imaginative. + ,(concat "int foo_bar; int foo_bar_baz;" + "int main() {foo"))))) + (with-current-buffer + (eglot--find-file-noselect "project/coiso.c") + (eglot--wait-for-clangd) + (goto-char (point-max)) + (completion-at-point) + (should (looking-back "{foo_bar"))))) Here, we completed to foo_bar and it's a fully formed completion. I understand you've checked that exit-function does run. This is good. Unfortunately, if the the two ints were named "foo_bar_1" and "foo_bar_2" it would also complete to foo_bar_, which is wrong in the general case, since that text is not a fully formed completion. exit-function cannot run here. It happens to "work" here because clangd supplies the "insertText" as well as the "textEdit" property and that "insertText" isn't meaningless. The LSP spec recommendation is for servers to prefer supplying textEdit. * The `insertText` is subject to interpretation by the client side. * Some tools might not take the string literally. For example * VS Code when code complete is requested in this example * `con<cursor position>` and a completion item with an `insertText` of * `console` is provided it will only insert `sole`. Therefore it is * recommended to use `textEdit` instead since it avoids additional client * side interpretation. Anyway this is _not_ a regression in your patch: the current state also does this mistake. Though, I wonder if you could fix it as well (in a follow-up commit, maybe). +(ert-deftest eglot-test-stop-completion-on-nonprefix () + "Test completion also resulting in 'Complete, but not unique'." + (skip-unless (executable-find "clangd")) + (eglot--with-fixture + `(("project" . (("coiso.c" . + ,(concat "int foot; int footer; int fo_obar;" + "int main() {foo"))))) + (with-current-buffer + (eglot--find-file-noselect "project/coiso.c") + (eglot--wait-for-clangd) + (goto-char (point-max)) + (completion-at-point) + (should (looking-back "foo"))))) Here, we're looking-back to "foo" here because _no_ completion took place. This means a *Completions* buffer has appeared and is now offering foot and footer. The current state does not do this, and chooses one of the two completions indiscrimately. This is, I think, the original bug you reported here. So this is clearly a win. To underline it and defend against regresison, I suggest to also assert that the *Completions* buffer popped up. Though, maybe not going so far as asserting the there are exactly those two completions in that order -- doing it "graphically" might prove too brittle. +(ert-deftest eglot-test-try-completion-inside-symbol () + "Test completion table inside symbol, with only prefix matching." + (skip-unless (executable-find "clangd")) + (eglot--with-fixture + `(("project" . (("coiso.c" . + ,(concat + "int foobar;" + "int main() {foo123"))))) + (with-current-buffer + (eglot--find-file-noselect "project/coiso.c") + (eglot--wait-for-clangd) + (goto-char (- (point-max) 3)) + (should + (equal + '("foo123" . 3) + (completion-try-completion + "foo123" + (nth 2 (eglot-completion-at-point)) nil 3)))))) I don't understand the point of this last test. Are you checking that nothing changes? What are you guarding against? Finally, an interesting test could be the rust-analyzer one from the issue 1339 issue, which starts with. fn main() { let v: usize = 1; 1234.1234567890 } I've manually checked that neither the v.c1234.12345676890 nor the v.count_on1234.1234567890 case has regressed. The tests for rust-analyzer require a bit more boilerplate (they require a "cargo" call to setup a temporary project), but other than that it's mostly the same. They won't run on Hydra (no rust-analyzer there)), but I do run them on occasion. João ^ permalink raw reply [flat|nested] 16+ messages in thread
* bug#72705: 31.0.50; eglot--dumb-tryc Filters out too much 2024-08-21 16:52 ` João Távora @ 2024-08-22 0:41 ` Dmitry Gutov 2024-08-22 16:59 ` João Távora 0 siblings, 1 reply; 16+ messages in thread From: Dmitry Gutov @ 2024-08-22 0:41 UTC (permalink / raw) To: João Távora; +Cc: 72705 On 21/08/2024 19:52, João Távora wrote: >> See the new test 'eglot-test-try-completion-inside-symbol' inside the >> patch. Wasn't sure whether to make it use 'completion-try-completion' >> or 'completion-at-point', but the former seems more explicit. > > This all looks very good, especially the abundant tests. You may push > right now if you want. Thanks! The next question I should ask, though, is can we get it into Emacs 30? Like with the previous discussions about Eglot in Emacs 29, I think there will be a lot of users who just use the version of it that comes with the release, without upgrading. > I'll just comment on those: > > +(ert-deftest eglot-test-common-prefix-completion () > + "Test completion appending the common prefix." > + (skip-unless (executable-find "clangd")) > + (eglot--with-fixture > + `(("project" . (("coiso.c" . > > Eh. You may replace "coiso.c" for the Russian equivalent of "thingy.c" > if you want :-). Or something more imaginative. schtuka? The Portuguese version sounds pretty good to my ear. I'd try the Greek variant, but "pragma" already has a different meaning in C langs. > + ,(concat "int foo_bar; int foo_bar_baz;" > + "int main() {foo"))))) > + (with-current-buffer > + (eglot--find-file-noselect "project/coiso.c") > + (eglot--wait-for-clangd) > + (goto-char (point-max)) > + (completion-at-point) > + (should (looking-back "{foo_bar"))))) > > Here, we completed to foo_bar and it's a fully formed completion. I > understand you've checked that exit-function does run. This is good. I did check, though not in any of the included tests. > Unfortunately, if the the two ints were named "foo_bar_1" and > "foo_bar_2" it would also complete to foo_bar_, which is wrong in the > general case, since that text is not a fully formed completion. > exit-function cannot run here. Is it really a problem? In my testing completing to foo_bar_ usually is a good thing (you have fewer chars left to type), even though nothing else happens. To try a different example, if I have C functions foo_bar_1 and foo_bar_2, with Clang input 'foo_' does get completed to 'foo_bar_', without exit-function running. But to get it called, I only need to additionally type '1' and press C-M-i again. Or, if Company is used, completing to 'foo_bar_' keeps the popup active, so I just press RET to pick the first completion. > It happens to "work" here because clangd supplies the "insertText" as > well as the "textEdit" property and that "insertText" isn't meaningless. > The LSP spec recommendation is for servers to prefer supplying textEdit. > > * The `insertText` is subject to interpretation by the client side. > * Some tools might not take the string literally. For example > * VS Code when code complete is requested in this example > * `con<cursor position>` and a completion item with an `insertText` of > * `console` is provided it will only insert `sole`. Therefore it is > * recommended to use `textEdit` instead since it avoids additional client > * side interpretation. > > Anyway this is _not_ a regression in your patch: the current state also > does this mistake. Though, I wonder if you could fix it as well (in a > follow-up commit, maybe). It seems to me that performing *some* completion is better than always returning the original input, isn't it? Do you have an example of a language server and a scenario where this becomes a problem? > +(ert-deftest eglot-test-stop-completion-on-nonprefix () > + "Test completion also resulting in 'Complete, but not unique'." > + (skip-unless (executable-find "clangd")) > + (eglot--with-fixture > + `(("project" . (("coiso.c" . > + ,(concat "int foot; int footer; int fo_obar;" > + "int main() {foo"))))) > + (with-current-buffer > + (eglot--find-file-noselect "project/coiso.c") > + (eglot--wait-for-clangd) > + (goto-char (point-max)) > + (completion-at-point) > + (should (looking-back "foo"))))) > > Here, we're looking-back to "foo" here because _no_ completion took > place. This means a *Completions* buffer has appeared and is now > offering foot and footer. Right. > The current state does not do this, and chooses one of the two > completions indiscrimately. This is, I think, the original bug you > reported here. So this is clearly a win. To underline it and defend > against regresison, I suggest to also assert that the *Completions* > buffer popped up. Sure. > Though, maybe not going so far as asserting the there > are exactly those two completions in that order -- doing it > "graphically" might prove too brittle. An alternative might be to use a completion-all-completions call, to assert the full list of completions. Or some completions that are in it anyway. > +(ert-deftest eglot-test-try-completion-inside-symbol () > + "Test completion table inside symbol, with only prefix matching." > + (skip-unless (executable-find "clangd")) > + (eglot--with-fixture > + `(("project" . (("coiso.c" . > + ,(concat > + "int foobar;" > + "int main() {foo123"))))) > + (with-current-buffer > + (eglot--find-file-noselect "project/coiso.c") > + (eglot--wait-for-clangd) > + (goto-char (- (point-max) 3)) > + (should > + (equal > + '("foo123" . 3) > + (completion-try-completion > + "foo123" > + (nth 2 (eglot-completion-at-point)) nil 3)))))) > > I don't understand the point of this last test. Are you checking that > nothing changes? What are you guarding against? With the input, I'm describing the actual prefix and suffix that get used. It might help someone who comes later to understand this separate case. Indeed "nothing happens", it's just that something did happen with the previous version of my patch, and we're testing against that. Might be more interesting to have a similar example where the prefix changes while the (non-emtpy) suffix stays the same, but I think we aren't going to support this in this c-style. > Finally, an interesting test could be the rust-analyzer one from the > issue 1339 issue, which starts with. > > fn main() { > let v: usize = 1; > 1234.1234567890 > } > > I've manually checked that neither the v.c1234.12345676890 nor the > v.count_on1234.1234567890 case has regressed. Yep, that's the one I fixed in the last revision. It's the same case as '("foo123" . 3) in the test above, isn't it? > The tests for rust-analyzer require a bit more boilerplate (they require > a "cargo" call to setup a temporary project), but other than that it's > mostly the same. They won't run on Hydra (no rust-analyzer there)), but > I do run them on occasion. Was a bit non-trivial to get it running indeed. ^ permalink raw reply [flat|nested] 16+ messages in thread
* bug#72705: 31.0.50; eglot--dumb-tryc Filters out too much 2024-08-22 0:41 ` Dmitry Gutov @ 2024-08-22 16:59 ` João Távora 2024-08-22 23:16 ` Dmitry Gutov 0 siblings, 1 reply; 16+ messages in thread From: João Távora @ 2024-08-22 16:59 UTC (permalink / raw) To: Dmitry Gutov; +Cc: 72705 Dmitry Gutov <dmitry@gutov.dev> writes: > Like with the previous discussions about Eglot in Emacs 29, I think > there will be a lot of users who just use the version of it that comes > with the release, without upgrading. Hopefully upgrading should be a matter of M-x eglot-upgrade-eglot. But no problem on my side. In a separate "don't merge" commit, look into editing the etc/EGLOT-NEWS there which I've already bumped to 1.17.30 (the version of Eglot to be bundled with Emacs 30). I'll then copy that text to master eventually and release 1.17 to GNU Elpa. >> Unfortunately, if the the two ints were named "foo_bar_1" and >> "foo_bar_2" it would also complete to foo_bar_, which is wrong in the >> general case, since that text is not a fully formed completion. >> exit-function cannot run here. > > Is it really a problem? Yes. Start 'clangd --completion-style=detailed' in this thingy.cpp C++ file: int foobar(int x, char c) { return x + c; } int foobar(int x, float f) { return x + f; } int main() { foob<cursor here> } if you press C-M-i here this will expand to the partial 'foobar(int x, ' which doesn't make C/C++ syntactic sense in that context. Emacs is just inserting the LSP "label", which as the name implies, is just for displaying, not for inserting. The goal of these two completions is to expand a snippet and the snippet can only be expanded through the exit function. Even if we complexified Eglot's capf to insert the 'insertText' instead of the 'label' (assuming it existed), we'd be inserting a partial snippet skeleton, even more nonsensical in C/C++. > completing to 'foo_bar_' keeps the popup active, so I just press RET > to pick the first completion. With the above 'foobar(int x, ' example and with bare C-M-i it's even worse: it's hard (if not impossible) to complete to the full completion and thus get the desired snippet expansion because the common prefix of both completion labels ends with a space. > It seems to me that performing *some* completion is better than always > returning the original input, isn't it? No, not always. LSP just wasn't thought with the "partial completion" scenario in mind. > Do you have an example of a language server and a scenario where this > becomes a problem? The above. >> The current state does not do this, and chooses one of the two >> completions indiscrimately. This is, I think, the original bug you >> reported here. So this is clearly a win. To underline it and defend >> against regresison, I suggest to also assert that the *Completions* >> buffer popped up. > > Sure. Great. >> Though, maybe not going so far as asserting the there >> are exactly those two completions in that order -- doing it >> "graphically" might prove too brittle. > > An alternative might be to use a completion-all-completions call, to > assert the full list of completions. Or some completions that are in > it anyway. Yes, I generally prefer "end-to-end" tests using interactive interfaces. So unless you're duplicating the test I think this part should be skipped. But your call. >> +(ert-deftest eglot-test-try-completion-inside-symbol () >> + "Test completion table inside symbol, with only prefix matching." >> + (skip-unless (executable-find "clangd")) >> + (eglot--with-fixture >> + `(("project" . (("coiso.c" . >> + ,(concat >> + "int foobar;" >> + "int main() {foo123"))))) >> + (with-current-buffer >> + (eglot--find-file-noselect "project/coiso.c") >> + (eglot--wait-for-clangd) >> + (goto-char (- (point-max) 3)) >> + (should >> + (equal >> + '("foo123" . 3) >> + (completion-try-completion >> + "foo123" >> + (nth 2 (eglot-completion-at-point)) nil 3)))))) >> I don't understand the point of this last test. Are you checking >> that >> nothing changes? What are you guarding against? > > With the input, I'm describing the actual prefix and suffix that get > used. It might help someone who comes later to understand this > separate case. Indeed "nothing happens", it's just that something did > happen with the previous version of my patch, and we're testing > against that. Fine. So maybe be a little bit more explicit in the comment or docstring about what you _don't_ want to happen. AFAIU, you don't want 'foobar123' or 'foobar' to be the result of that completion. > Might be more interesting to have a similar example where the prefix > changes while the (non-emtpy) suffix stays the same, but I think we > aren't going to support this in this c-style. > >> Finally, an interesting test could be the rust-analyzer one from the >> issue 1339 issue, which starts with. >> fn main() { >> let v: usize = 1; >> 1234.1234567890 >> } >> I've manually checked that neither the v.c1234.12345676890 nor the >> v.count_on1234.1234567890 case has regressed. > > Yep, that's the one I fixed in the last revision. > > It's the same case as '("foo123" . 3) in the test above, isn't it? I don't think so. In a rust-analyzer hello world (which you can make with cargo init, if I'm not mistaken), both v.c<cursor>1234.12345676890 v.count_on<cursor>1234.12345676890 should expand to v.count_ones()<cursor>.1234567890 In the first case, you'll have to manually select "count_ones" from the completions buffer. The expansion and removal of the 1234 happens via LSP text edits, which are enacte by the :exit-function. This scenario wasn't working a while ago and I'd like to protect it against regression. João ^ permalink raw reply [flat|nested] 16+ messages in thread
* bug#72705: 31.0.50; eglot--dumb-tryc Filters out too much 2024-08-22 16:59 ` João Távora @ 2024-08-22 23:16 ` Dmitry Gutov 2024-08-23 10:23 ` João Távora 0 siblings, 1 reply; 16+ messages in thread From: Dmitry Gutov @ 2024-08-22 23:16 UTC (permalink / raw) To: João Távora; +Cc: 72705 On 22/08/2024 19:59, João Távora wrote: > Dmitry Gutov <dmitry@gutov.dev> writes: > >> Like with the previous discussions about Eglot in Emacs 29, I think >> there will be a lot of users who just use the version of it that comes >> with the release, without upgrading. > > Hopefully upgrading should be a matter of M-x eglot-upgrade-eglot. > > But no problem on my side. In a separate "don't merge" commit, look > into editing the etc/EGLOT-NEWS there which I've already bumped to > 1.17.30 (the version of Eglot to be bundled with Emacs 30). I'll then > copy that text to master eventually and release 1.17 to GNU Elpa. Great! Bump it to 1.17.60? >>> Unfortunately, if the the two ints were named "foo_bar_1" and >>> "foo_bar_2" it would also complete to foo_bar_, which is wrong in the >>> general case, since that text is not a fully formed completion. >>> exit-function cannot run here. >> >> Is it really a problem? > > Yes. Start 'clangd --completion-style=detailed' in this thingy.cpp C++ > file: > > int foobar(int x, char c) { > return x + c; > } > > int foobar(int x, float f) { > return x + f; > } > > int main() { > foob<cursor here> > } > > if you press C-M-i here this will expand to the partial 'foobar(int x, ' > which doesn't make C/C++ syntactic sense in that context. Emacs is just > inserting the LSP "label", which as the name implies, is just for > displaying, not for inserting. The goal of these two completions is to > expand a snippet and the snippet can only be expanded through the exit > function. Even if we complexified Eglot's capf to insert the > 'insertText' instead of the 'label' (assuming it existed), we'd be > inserting a partial snippet skeleton, even more nonsensical in C/C++. Thanks, now I remember: it's https://github.com/company-mode/company-mode/issues/1412. The current proposal doesn't fix it, though it probably makes things a bit better (simply because the "common" string is on average computed to be shorter). A somewhat complete solution would probably include per-language "stop character" map - where we could check the `probe' for chars such as "(" (or whatever the language uses) and shorten the return value accordingly if found. It would replace the currently added "string-prefix-p" check, I guess. >> completing to 'foo_bar_' keeps the popup active, so I just press RET >> to pick the first completion. > > With the above 'foobar(int x, ' example and with bare C-M-i it's even > worse: it's hard (if not impossible) to complete to the full completion > and thus get the desired snippet expansion because the common prefix of > both completion labels ends with a space. Yeah, it'd be great to have C-M-i stop before the paren. >> An alternative might be to use a completion-all-completions call, to >> assert the full list of completions. Or some completions that are in >> it anyway. > > Yes, I generally prefer "end-to-end" tests using interactive interfaces. > So unless you're duplicating the test I think this part should be > skipped. But your call. It's a different test (albeit with the same result as an existing one). I don't mind rewriting it in a different style, if you prefer. The result is kind of messy either way. >>> +(ert-deftest eglot-test-try-completion-inside-symbol () >>> + "Test completion table inside symbol, with only prefix matching." >>> + (skip-unless (executable-find "clangd")) >>> + (eglot--with-fixture >>> + `(("project" . (("coiso.c" . >>> + ,(concat >>> + "int foobar;" >>> + "int main() {foo123"))))) >>> + (with-current-buffer >>> + (eglot--find-file-noselect "project/coiso.c") >>> + (eglot--wait-for-clangd) >>> + (goto-char (- (point-max) 3)) >>> + (should >>> + (equal >>> + '("foo123" . 3) >>> + (completion-try-completion >>> + "foo123" >>> + (nth 2 (eglot-completion-at-point)) nil 3)))))) >>> I don't understand the point of this last test. Are you checking >>> that >>> nothing changes? What are you guarding against? >> >> With the input, I'm describing the actual prefix and suffix that get >> used. It might help someone who comes later to understand this >> separate case. Indeed "nothing happens", it's just that something did >> happen with the previous version of my patch, and we're testing >> against that. > > Fine. So maybe be a little bit more explicit in the comment or > docstring about what you _don't_ want to happen. AFAIU, you don't want > 'foobar123' or 'foobar' to be the result of that completion. Right! >> Might be more interesting to have a similar example where the prefix >> changes while the (non-emtpy) suffix stays the same, but I think we >> aren't going to support this in this c-style. >> >>> Finally, an interesting test could be the rust-analyzer one from the >>> issue 1339 issue, which starts with. >>> fn main() { >>> let v: usize = 1; >>> 1234.1234567890 >>> } >>> I've manually checked that neither the v.c1234.12345676890 nor the >>> v.count_on1234.1234567890 case has regressed. >> >> Yep, that's the one I fixed in the last revision. >> >> It's the same case as '("foo123" . 3) in the test above, isn't it? > > I don't think so. In a rust-analyzer hello world (which you can make > with cargo init, if I'm not mistaken), both > > v.c<cursor>1234.12345676890 > v.count_on<cursor>1234.12345676890 > > should expand to > > v.count_ones()<cursor>.1234567890 > > In the first case, you'll have to manually select "count_ones" from the > completions buffer. The expansion and removal of the 1234 happens via > LSP text edits, which are enacte by the :exit-function. Okay, I am seeing a difference too: C-M-i does eat the suffix in the Rust example (the "1234"). But completion with Company does not :-/ I guess the difference might come from C-M-i always going through try-completion, whereas the Company popup relies on all-completions. There's a similar difference when using gopls. But Clangd actually looks special (just filed separate bug#72765). > This scenario > wasn't working a while ago and I'd like to protect it against > regression. Would we have a test that launches rust-analyzer (and depends on it being installed, and a Cargo project initialized)? ^ permalink raw reply [flat|nested] 16+ messages in thread
* bug#72705: 31.0.50; eglot--dumb-tryc Filters out too much 2024-08-22 23:16 ` Dmitry Gutov @ 2024-08-23 10:23 ` João Távora 2024-08-25 2:49 ` Dmitry Gutov 0 siblings, 1 reply; 16+ messages in thread From: João Távora @ 2024-08-23 10:23 UTC (permalink / raw) To: Dmitry Gutov; +Cc: 72705 Dmitry Gutov <dmitry@gutov.dev> writes: > On 22/08/2024 19:59, João Távora wrote: >> Dmitry Gutov <dmitry@gutov.dev> writes: >> >>> Like with the previous discussions about Eglot in Emacs 29, I think >>> there will be a lot of users who just use the version of it that comes >>> with the release, without upgrading. >> Hopefully upgrading should be a matter of M-x eglot-upgrade-eglot. >> But no problem on my side. In a separate "don't merge" commit, look >> into editing the etc/EGLOT-NEWS there which I've already bumped to >> 1.17.30 (the version of Eglot to be bundled with Emacs 30). I'll then >> copy that text to master eventually and release 1.17 to GNU Elpa. > > Great! Bump it to 1.17.60? No, keep it at 1.17.30. That's the 1.17-ish version of Eglot that will ship with Emacs 30, just as 1.12.29 is the 1.12-ish version of Eglot that shipped with Emacs 29. The change you're proposing will eventually also be merged to master and be part of the 1.18 GNU Elpa release (earlier I wrote 1.17 GNU Elpa by mistake). > The current proposal doesn't fix it, though it probably makes things a > bit better (simply because the "common" string is on average computed > to be shorter). A somewhat complete solution would probably include > per-language "stop character" map - where we could check the `probe' > for chars such as "(" (or whatever the language uses) and shorten the > return value accordingly if found. > > It would replace the currently added "string-prefix-p" check, I guess. Sounds clever but I really doubt that would work in the general case. I'm not getting my point across I think. An LSP label is just a label. Except for in very primitive uses of LSP, it's not meant to be inserted or to be used in the computation of prefixes or commonality between completions. These primitives uses are deprecated as I hope the LSP quote from the spec confirmed. Foreign as it might seem to Emacs devs and users, LSP doesn't want any concept of "prefix" (or suffix or commonality). In general, two very closely related completions in the final insertion result can have two wildly different labels displayed to the user. In this sense, the "completions" popup in LSP is less like our all-too-familiar listing of strings that can help complete the thing at point and more akin to a specialized context menu for "things to _do_ at point". There's not even an LSP mandate that this thing to _do_ must include something to insert at point. Currently it could be just tidying up imports elsewhere in the LSP document. In future LSP versions, it could be just running an arbitrary server action, or sending an email. In conclusion, Emacs's partial completion makes no sense in LSP (except for the aforementioned fringe/deprecated cases). The correct fix for this separate issue to disable partial completion: either a full completion can be inserted or a listing of all applicable completions should be shown. The other "fix" is to lobby with the LSP spec people, but they're very VSCode-inclined. From what I've seen, even other editors which are more popular than Emacs have very little sway there. >>> completing to 'foo_bar_' keeps the popup active, so I just press RET >>> to pick the first completion. >> With the above 'foobar(int x, ' example and with bare C-M-i it's >> even >> worse: it's hard (if not impossible) to complete to the full completion >> and thus get the desired snippet expansion because the common prefix of >> both completion labels ends with a space. > > Yeah, it'd be great to have C-M-i stop before the paren. No, it wouldn't. It'd fix this particular case, but it could break in some other future version of LSP where the LSP label isn't a prefix of the thing that selecting that label would insert. >>> An alternative might be to use a completion-all-completions call, to >>> assert the full list of completions. Or some completions that are in >>> it anyway. >> Yes, I generally prefer "end-to-end" tests using interactive >> interfaces. >> So unless you're duplicating the test I think this part should be >> skipped. But your call. > > It's a different test (albeit with the same result as an existing > one). I don't mind rewriting it in a different style, if you > prefer. The result is kind of messy either way. My suggestion is just test that *Completions* pops up. >>> It's the same case as '("foo123" . 3) in the test above, isn't it? >> I don't think so. In a rust-analyzer hello world (which you can >> make >> with cargo init, if I'm not mistaken), both >> v.c<cursor>1234.12345676890 >> v.count_on<cursor>1234.12345676890 >> should expand to >> v.count_ones()<cursor>.1234567890 >> In the first case, you'll have to manually select "count_ones" from >> the >> completions buffer. The expansion and removal of the 1234 happens via >> LSP text edits, which are enacte by the :exit-function. > > Okay, I am seeing a difference too: C-M-i does eat the suffix in the > Rust example (the "1234"). But completion with Company does not :-/ I can't even get Company to appear in those situations. But I wouldn't bother about it. The bug report was about C-M-i originally. The important thing is that the LSP textEdit is run (via exit-function) with the correct LSP document state that the server expects. As far as I can tell, this is happening right now, so I'd be nice to have a test to shore it up. > I guess the difference might come from C-M-i always going through > try-completion, whereas the Company popup relies on all-completions. > > There's a similar difference when using gopls. > > But Clangd actually looks special (just filed separate bug#72765). > >> This scenario >> wasn't working a while ago and I'd like to protect it against >> regression. > > Would we have a test that launches rust-analyzer (and depends on it > being installed, and a Cargo project initialized)? Yes. Look in the existing eglot-tests.el: there is already such a test: eglot-test-rust-analyzer-watches-files. I'd just cargo-cult the lines until the "cargo init" call. João ^ permalink raw reply [flat|nested] 16+ messages in thread
* bug#72705: 31.0.50; eglot--dumb-tryc Filters out too much 2024-08-23 10:23 ` João Távora @ 2024-08-25 2:49 ` Dmitry Gutov 2024-08-25 9:53 ` João Távora 0 siblings, 1 reply; 16+ messages in thread From: Dmitry Gutov @ 2024-08-25 2:49 UTC (permalink / raw) To: João Távora; +Cc: 72705 [-- Attachment #1: Type: text/plain, Size: 5013 bytes --] On 23/08/2024 13:23, João Távora wrote: >> Great! Bump it to 1.17.60? > > No, keep it at 1.17.30. That's the 1.17-ish version of Eglot that will > ship with Emacs 30, just as 1.12.29 is the 1.12-ish version of Eglot > that shipped with Emacs 29. > > The change you're proposing will eventually also be merged to master > and be part of the 1.18 GNU Elpa release (earlier I wrote 1.17 GNU Elpa > by mistake). Perfect, thanks. > Foreign as it might seem to Emacs devs and users, LSP doesn't want any > concept of "prefix" (or suffix or commonality). In general, two very > closely related completions in the final insertion result can have two > wildly different labels displayed to the user. In this sense, the > "completions" popup in LSP is less like our all-too-familiar listing of > strings that can help complete the thing at point and more akin to a > specialized context menu for "things to _do_ at point". There's not > even an LSP mandate that this thing to _do_ must include something to > insert at point. Currently it could be just tidying up imports > elsewhere in the LSP document. In future LSP versions, it could be just > running an arbitrary server action, or sending an email. I see what you're saying, but insofar that current completion is mostly working out well, adding the special logic for parens would improve a lot of cases, and keep others the same degree of broken (the email-sending ones, for sure). So it might be worth a try. Anyway, stopping any partial completion (at first I didn't understand that you meant a more general notion that the partial-completion c-style) should be similarly easy to what the current patch does. You would just drop the second clause in eglot--dumb-tryc, I think. Or maybe both the first and the second. > The other "fix" is to lobby with the LSP spec people, but they're very > VSCode-inclined. From what I've seen, even other editors which are more > popular than Emacs have very little sway there. It seems we only have our hacks to help. They got pretty advanced, though. >> Yeah, it'd be great to have C-M-i stop before the paren. > > No, it wouldn't. It'd fix this particular case, but it could break in > some other future version of LSP where the LSP label isn't a prefix of > the thing that selecting that label would insert. Quite possibly, yes. Though reverting to the simpler behavior at that point would just require a 3-4 lines diff, as discussed. >>>> An alternative might be to use a completion-all-completions call, to >>>> assert the full list of completions. Or some completions that are in >>>> it anyway. >>> Yes, I generally prefer "end-to-end" tests using interactive >>> interfaces. >>> So unless you're duplicating the test I think this part should be >>> skipped. But your call. >> >> It's a different test (albeit with the same result as an existing >> one). I don't mind rewriting it in a different style, if you >> prefer. The result is kind of messy either way. > > My suggestion is just test that *Completions* pops up. Alas, this check does not succeed: (should (get-buffer-window "*Completions")) This works: (when (buffer-live-p "*Completions*") (kill-buffer "*Completions*")) (completion-at-point) (should (looking-back "foo")) (should (looking-at "123")) (should (get-buffer "*Completions*")) Is that better? >>>> It's the same case as '("foo123" . 3) in the test above, isn't it? >>> I don't think so. In a rust-analyzer hello world (which you can >>> make >>> with cargo init, if I'm not mistaken), both >>> v.c<cursor>1234.12345676890 >>> v.count_on<cursor>1234.12345676890 >>> should expand to >>> v.count_ones()<cursor>.1234567890 >>> In the first case, you'll have to manually select "count_ones" from >>> the >>> completions buffer. The expansion and removal of the 1234 happens via >>> LSP text edits, which are enacte by the :exit-function. >> >> Okay, I am seeing a difference too: C-M-i does eat the suffix in the >> Rust example (the "1234"). But completion with Company does not :-/ > > I can't even get Company to appear in those situations. You might have an older version installed? > But I wouldn't bother about it. The bug report was about C-M-i > originally. The important thing is that the LSP textEdit is run (via > exit-function) with the correct LSP document state that the server > expects. As far as I can tell, this is happening right now, so I'd be > nice to have a test to shore it up. Okay, see the attached patch with the added test. It took way longer than expected. Seems pretty brittle (note how eglot--wait-for-rust-analyzer is defined), but should be better than nothing. I also checked how VS Code behaves in such completions, since we were talking about what LSP's authors designed for or not. Works differently: 1) Completions are not filtered by suffix, ever. 2) Completing count_ones() does not delete the suffix ("1234" remains). It might mean that either behavior is okay, though. [-- Attachment #2: eglot--dumb-tryc-more-checks-v3.diff --] [-- Type: text/x-patch, Size: 7230 bytes --] diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 353877f60c2..59d9c346424 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -3142,8 +3142,18 @@ eglot--dumb-allc (defun eglot--dumb-tryc (pat table pred point) (let ((probe (funcall table pat pred nil))) (cond ((eq probe t) t) - (probe (cons probe (length probe))) - (t (cons pat point))))) + (probe + (if (and (not (equal probe pat)) + (cl-every + (lambda (s) (string-prefix-p probe s completion-ignore-case)) + (funcall table pat pred t))) + (cons probe (length probe)) + (cons pat point))) + (t + ;; Match ignoring suffix: if there are any completions for + ;; the current prefix at least, keep the current input. + (and (funcall table (substring pat 0 point) pred t) + (cons pat point)))))) (add-to-list 'completion-category-defaults '(eglot-capf (styles eglot--dumb-flex))) (add-to-list 'completion-styles-alist '(eglot--dumb-flex eglot--dumb-tryc eglot--dumb-allc)) diff --git a/test/lisp/progmodes/eglot-tests.el b/test/lisp/progmodes/eglot-tests.el index 534c47b2646..41824f477b8 100644 --- a/test/lisp/progmodes/eglot-tests.el +++ b/test/lisp/progmodes/eglot-tests.el @@ -587,6 +587,18 @@ eglot--wait-for-clangd (eglot--wait-for (s-notifs 20) (&key method &allow-other-keys) (string= method "textDocument/publishDiagnostics")))) +(defun eglot--wait-for-rust-analyzer () + (eglot--sniffing (:server-notifications s-notifs) + (should (eglot--tests-connect)) + (eglot--wait-for (s-notifs 20) (&key method params &allow-other-keys) + (and + (string= method "$/progress") + "rustAnalyzer/Indexing" + (equal params + '(:token "rustAnalyzer/Indexing" :value + ;; Could wait for :kind "end" instead, but it's 2 more seconds. + (:kind "begin" :title "Indexing" :cancellable :json-false :percentage 0))))))) + (ert-deftest eglot-test-basic-completions () "Test basic autocompletion in a clangd LSP." (skip-unless (executable-find "clangd")) @@ -600,6 +612,20 @@ eglot-test-basic-completions (message (buffer-string)) (should (looking-back "fprintf.?"))))) +(ert-deftest eglot-test-common-prefix-completion () + "Test completion appending the common prefix." + (skip-unless (executable-find "clangd")) + (eglot--with-fixture + `(("project" . (("coiso.c" . + ,(concat "int foo_bar; int foo_bar_baz;" + "int main() {foo"))))) + (with-current-buffer + (eglot--find-file-noselect "project/coiso.c") + (eglot--wait-for-clangd) + (goto-char (point-max)) + (completion-at-point) + (should (looking-back "{foo_bar"))))) + (ert-deftest eglot-test-non-unique-completions () "Test completion resulting in 'Complete, but not unique'." (skip-unless (executable-find "clangd")) @@ -619,19 +645,99 @@ eglot-test-non-unique-completions (forward-line -1) (should (looking-at "Complete, but not unique"))))))) -(ert-deftest eglot-test-basic-xref () - "Test basic xref functionality in a clangd LSP." +(ert-deftest eglot-test-stop-completion-on-nonprefix () + "Test completion also resulting in 'Complete, but not unique'." + (skip-unless (executable-find "clangd")) + (eglot--with-fixture + `(("project" . (("coiso.c" . + ,(concat "int foot; int footer; int fo_obar;" + "int main() {foo"))))) + (with-current-buffer + (eglot--find-file-noselect "project/coiso.c") + (eglot--wait-for-clangd) + (goto-char (point-max)) + (completion-at-point) + (should (looking-back "foo"))))) + +(ert-deftest eglot-test-try-completion-nomatch () + "Test completion table with non-matching input, returning nil." + (skip-unless (executable-find "clangd")) + (eglot--with-fixture + `(("project" . (("coiso.c" . + ,(concat "int main() {abc"))))) + (with-current-buffer + (eglot--find-file-noselect "project/coiso.c") + (eglot--wait-for-clangd) + (goto-char (point-max)) + (should + (null + (completion-try-completion + "abc" + (nth 2 (eglot-completion-at-point)) nil 3)))))) + +(ert-deftest eglot-test-try-completion-inside-symbol () + "Test completion table inside symbol, with only prefix matching." (skip-unless (executable-find "clangd")) (eglot--with-fixture `(("project" . (("coiso.c" . - ,(concat "int foo=42; int fooey;" - "int main() {foo=82;}"))))) + ,(concat + "int foobar;" + "int main() {foo123"))))) (with-current-buffer (eglot--find-file-noselect "project/coiso.c") - (should (eglot--tests-connect)) - (search-forward "{foo") - (call-interactively 'xref-find-definitions) - (should (looking-at "foo=42"))))) + (eglot--wait-for-clangd) + (goto-char (- (point-max) 3)) + (when (buffer-live-p "*Completions*") + (kill-buffer "*Completions*")) + (completion-at-point) + (should (looking-back "foo")) + (should (looking-at "123")) + (should (get-buffer "*Completions*")) + ))) + +(ert-deftest eglot-test-rust-completion-exit-function () + "Hover and highlightChanges." + (skip-unless (executable-find "rust-analyzer")) + (skip-unless (executable-find "cargo")) + (eglot--with-fixture + '(("cmpl-project" . + (("main.rs" . + "fn test() -> i32 { let v: usize = 1; v.count_on1234.1234567890;")))) + (with-current-buffer + (eglot--find-file-noselect "cmpl-project/main.rs") + (should (zerop (shell-command "cargo init"))) + (eglot--tests-connect) + (goto-char (point-min)) + (search-forward "v.count_on") + (let ((minibuffer-message-timeout 0) + ;; Fail at (ding) if completion fails. + (executing-kbd-macro t)) + (when (buffer-live-p "*Completions*") + (kill-buffer "*Completions*")) + (eglot--wait-for-rust-analyzer) + (completion-at-point) + (should (looking-back "\\.count_on")) + (should (get-buffer "*Completions*")) + (minibuffer-next-completion 1) + (minibuffer-choose-completion t)) + (should + (equal + "fn test() -> i32 { let v: usize = 1; v.count_ones().1234567890;" + (buffer-string)))))) + +(ert-deftest eglot-test-basic-xref () + "Test basic xref functionality in a clangd LSP." + (skip-unless (executable-find "clangd")) + (eglot--with-fixture + `(("project" . (("coiso.c" . + ,(concat "int foo=42; int fooey;" + "int main() {foo=82;}"))))) + (with-current-buffer + (eglot--find-file-noselect "project/coiso.c") + (should (eglot--tests-connect)) + (search-forward "{foo") + (call-interactively 'xref-find-definitions) + (should (looking-at "foo=42"))))) (defvar eglot--test-c-buffer "\ ^ permalink raw reply related [flat|nested] 16+ messages in thread
* bug#72705: 31.0.50; eglot--dumb-tryc Filters out too much 2024-08-25 2:49 ` Dmitry Gutov @ 2024-08-25 9:53 ` João Távora 2024-08-25 15:56 ` Dmitry Gutov 2024-08-25 23:40 ` Dmitry Gutov 0 siblings, 2 replies; 16+ messages in thread From: João Távora @ 2024-08-25 9:53 UTC (permalink / raw) To: Dmitry Gutov; +Cc: 72705 Dmitry Gutov <dmitry@gutov.dev> writes: > On 23/08/2024 13:23, João Távora wrote: > > I see what you're saying, but insofar that current completion is > mostly working out well, adding the special logic for parens would > improve a lot of cases, and keep others the same degree of broken (the > email-sending ones, for sure). So it might be worth a try. No, it's not. It's the kind of complexity Eglot isn't after. It's a losing game with the growing number of servers who aren't bound by these rules in any way. > Anyway, stopping any partial completion (at first I didn't understand > that you meant a more general notion that the partial-completion > c-style) should be similarly easy to what the current patch does. You > would just drop the second clause in eglot--dumb-tryc, I think. Or > maybe both the first and the second. If it's easy, then we should definitely do it. >> The other "fix" is to lobby with the LSP spec people, but they're very >> VSCode-inclined. From what I've seen, even other editors which are more >> popular than Emacs have very little sway there. > > It seems we only have our hacks to help. They got pretty advanced, > though. Yes, but they always lose. >>> Yeah, it'd be great to have C-M-i stop before the paren. >> No, it wouldn't. It'd fix this particular case, but it could break >> in >> some other future version of LSP where the LSP label isn't a prefix of >> the thing that selecting that label would insert. > > Quite possibly, yes. Though reverting to the simpler behavior at that > point would just require a 3-4 lines diff, as discussed. That's more complexity. And you can't easily know _if_ the LSP label is or isn't a prefix of the thing selecting the completion would insert without effectively simulating the completion (which is frequently). > Alas, this check does not succeed: > > (should (get-buffer-window "*Completions")) Window management code, likely. > This works: > > (when (buffer-live-p "*Completions*") > (kill-buffer "*Completions*")) > (completion-at-point) > (should (looking-back "foo")) > (should (looking-at "123")) > (should (get-buffer "*Completions*")) > > Is that better? It's half-decent, I think. Please use that. >>> Okay, I am seeing a difference too: C-M-i does eat the suffix in the >>> Rust example (the "1234"). But completion with Company does not :-/ >> I can't even get Company to appear in those situations. > You might have an older version installed? Maybe. > 1) Completions are not filtered by suffix, ever. > 2) Completing count_ones() does not delete the suffix ("1234" > remains). Interesting, I thought deleting the 1234 was part of the edit, but the user of that bug report was after some Emacs specific behaviour. Anyway, I don't think the test is too brittle, and I think I'd like to know anyway if something about that breaks anyway. Maybe you can just add a comment saying ";; this test might be a bit too brittle, but interesting nonetheless". I think you can commit the patch then. Thanks. João ^ permalink raw reply [flat|nested] 16+ messages in thread
* bug#72705: 31.0.50; eglot--dumb-tryc Filters out too much 2024-08-25 9:53 ` João Távora @ 2024-08-25 15:56 ` Dmitry Gutov 2024-08-25 23:40 ` Dmitry Gutov 1 sibling, 0 replies; 16+ messages in thread From: Dmitry Gutov @ 2024-08-25 15:56 UTC (permalink / raw) To: João Távora; +Cc: 72705 On 25/08/2024 12:53, João Távora wrote: > Dmitry Gutov <dmitry@gutov.dev> writes: > >> On 23/08/2024 13:23, João Távora wrote: >> >> I see what you're saying, but insofar that current completion is >> mostly working out well, adding the special logic for parens would >> improve a lot of cases, and keep others the same degree of broken (the >> email-sending ones, for sure). So it might be worth a try. > > No, it's not. It's the kind of complexity Eglot isn't after. It's a > losing game with the growing number of servers who aren't bound by these > rules in any way. > >> Anyway, stopping any partial completion (at first I didn't understand >> that you meant a more general notion that the partial-completion >> c-style) should be similarly easy to what the current patch does. You >> would just drop the second clause in eglot--dumb-tryc, I think. Or >> maybe both the first and the second. > > If it's easy, then we should definitely do it. The corresponding change probably looks like this: diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 844fc634be9..fcf4e586ead 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -3147,20 +3147,10 @@ eglot--dumb-flex (defun eglot--dumb-allc (pat table pred _point) (funcall table pat pred t)) (defun eglot--dumb-tryc (pat table pred point) - (let ((probe (funcall table pat pred nil))) - (cond ((eq probe t) t) - (probe - (if (and (not (equal probe pat)) - (cl-every - (lambda (s) (string-prefix-p probe s completion-ignore-case)) - (funcall table pat pred t))) - (cons probe (length probe)) - (cons pat point))) - (t - ;; Match ignoring suffix: if there are any completions for - ;; the current prefix at least, keep the current input. - (and (funcall table (substring pat 0 point) pred t) - (cons pat point)))))) + ;; Match ignoring suffix: if there are any completions for + ;; the current prefix at least, keep the current input. + (and (funcall table (substring pat 0 point) pred t) + (cons pat point))) (add-to-list 'completion-category-defaults '(eglot-capf (styles eglot--dumb-flex))) (add-to-list 'completion-styles-alist '(eglot--dumb-flex eglot--dumb-tryc eglot--dumb-allc)) But I'd prefer to hold off on it until specific issues come up. >> This works: >> >> (when (buffer-live-p "*Completions*") >> (kill-buffer "*Completions*")) >> (completion-at-point) >> (should (looking-back "foo")) >> (should (looking-at "123")) >> (should (get-buffer "*Completions*")) >> >> Is that better? > > It's half-decent, I think. Please use that. Okay. >> 1) Completions are not filtered by suffix, ever. >> 2) Completing count_ones() does not delete the suffix ("1234" >> remains). > > Interesting, I thought deleting the 1234 was part of the edit, but the > user of that bug report was after some Emacs specific behaviour. > Anyway, I don't think the test is too brittle, and I think I'd like to > know anyway if something about that breaks anyway. Maybe you can just > add a comment saying ";; this test might be a bit too brittle, but > interesting nonetheless". Pushed to emacs-30 as two parts: new tests for existing behavior and the fix with its own tests (096730510cd, 713069dd7a8). ^ permalink raw reply related [flat|nested] 16+ messages in thread
* bug#72705: 31.0.50; eglot--dumb-tryc Filters out too much 2024-08-25 9:53 ` João Távora 2024-08-25 15:56 ` Dmitry Gutov @ 2024-08-25 23:40 ` Dmitry Gutov 1 sibling, 0 replies; 16+ messages in thread From: Dmitry Gutov @ 2024-08-25 23:40 UTC (permalink / raw) To: João Távora; +Cc: 72705-done On 25/08/2024 12:53, João Távora wrote: >>>> Okay, I am seeing a difference too: C-M-i does eat the suffix in the >>>> Rust example (the "1234"). But completion with Company does not :-/ >>> I can't even get Company to appear in those situations. >> You might have an older version installed? > Maybe. Okay, I've tracked this one down to Company adhering to what completion style is being used. When no completions match the suffix, completion-all-completions falls back to c-style 'emacs22' (if it's in completion-styles anyway). And when that style is used, we don't replace the suffix. There is a report for vanilla completion in bug#70968. That seems to cover all related issues I've found so far. Thanks, and closing. ^ permalink raw reply [flat|nested] 16+ messages in thread
end of thread, other threads:[~2024-08-25 23:40 UTC | newest] Thread overview: 16+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2024-08-19 1:51 bug#72705: 31.0.50; eglot--dumb-tryc Filters out too much Dmitry Gutov 2024-08-19 9:22 ` João Távora 2024-08-19 11:39 ` Dmitry Gutov 2024-08-19 12:59 ` João Távora 2024-08-20 2:08 ` Dmitry Gutov 2024-08-20 9:40 ` João Távora 2024-08-21 0:30 ` Dmitry Gutov 2024-08-21 16:52 ` João Távora 2024-08-22 0:41 ` Dmitry Gutov 2024-08-22 16:59 ` João Távora 2024-08-22 23:16 ` Dmitry Gutov 2024-08-23 10:23 ` João Távora 2024-08-25 2:49 ` Dmitry Gutov 2024-08-25 9:53 ` João Távora 2024-08-25 15:56 ` Dmitry Gutov 2024-08-25 23:40 ` Dmitry Gutov
Code repositories for project(s) associated with this public inbox https://git.savannah.gnu.org/cgit/emacs.git This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox; as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).