Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Blazingly fast completion ;) #52

Closed
minad opened this issue Jun 25, 2022 · 61 comments
Closed

Blazingly fast completion ;) #52

minad opened this issue Jun 25, 2022 · 61 comments

Comments

@minad
Copy link
Owner

minad commented Jun 25, 2022

I just stumbled across two posts where people complained about the slowness of completion in Emacs.

Now I tried to address this in the case of elisp completion with two approaches:

  1. incremental filtering
  2. candidate limiting (leads to problems with sorting obviously)
;; -*- lexical-binding: t -*-

(defun cape--incremental-table (table)
  (let ((last-prefix "\0") last-regexp last-case last-result last-pred)
    (lambda (prefix pred action)
      (cond
       ((eq action t)
        (unless (and (equal last-prefix prefix)
                     (equal last-regexp completion-regexp-list)
                     (eq last-case completion-ignore-case)
                     (eq last-pred pred))
          (setq last-result
                (if (and (equal last-regexp completion-regexp-list)
                         (eq last-case completion-ignore-case)
                         (eq last-pred pred)
                         (string-prefix-p last-prefix prefix))
                    (let (completion-regexp-list)
                      (all-completions prefix last-result))
                  (all-completions prefix table pred))
                last-prefix prefix
                last-case completion-ignore-case
                last-pred pred
                last-regexp (copy-tree completion-regexp-list)))
        (copy-sequence last-result))
       (t (complete-with-action action table prefix pred))))))

(defun cape--limited-table (table)
  (lambda (prefix pred action)
    (if (eq action t)
        (let ((count 0) result)
          (catch 'cape--limit
            (all-completions prefix table
                             (lambda (cand)
                               (when (or (not pred) (funcall pred cand))
                                 (if (> count 100)
                                     (throw 'cape--limit t)
                                   (cl-incf count)
                                   (when (symbolp cand)
                                     (setq cand (symbol-name cand)))
                                   (push cand result)))
                               nil)))
            result)
        (complete-with-action action table prefix pred))))

(defun cape-wrap-incremental (capf)
  (pcase (funcall capf)
    (`(,beg ,end ,table . ,plist)
     `(,beg ,end ,(cape--incremental-table table) ,@plist))))

(defun cape-wrap-limited (capf)
  (pcase (funcall capf)
    (`(,beg ,end ,table . ,plist)
     `(,beg ,end ,(cape--limited-table table) ,@plist))))

(cape--capf-wrapper limited)
(cape--capf-wrapper incremental)

(setq-local
 ;;completion-at-point-functions (list (cape-capf-incremental #'elisp-completion-at-point))
 ;;completion-styles '(emacs21) ;; needed for incremental completion
 completion-at-point-functions (list (cape-capf-limited #'elisp-completion-at-point))
 completion-styles '(orderless)
 corfu-auto-delay 0
 corfu-auto-prefix 0)

Feedback? Comments? cc @jdtsmith What do you think?

@jdtsmith
Copy link
Contributor

I saw those. In the "VSCode completions are fast compared to Emacs" genre, most people are probably talking about LSP-fed candidates; candidate limiting probably won't help much there (unless it's done for the lsp-server itself). I also saw complaints that overlays were slow, which isn't a corfu thing.

Is the idea of incremental for orderless? Looks quite interesting. Is it always true that if a search string is a prefix of another search string, its results are a superset of the longer search's results? Even with, e.g., orderless-without-literal? I personally doubt all-completions is the bottleneck, but I could be wrong.

What we need to test is a big candidate list, and a benchmark/testing framework, perhaps with elp. I once helped Tramp's author with a test of this sort; here's an example of what we ended up with.

@minad
Copy link
Owner Author

minad commented Jun 25, 2022

I saw those. In the "VSCode completions are fast compared to Emacs" genre, most people are probably talking about LSP-fed candidates; candidate limiting probably won't help much there (unless it's done for the lsp-server itself).

Yes, of course. They are talking about lsp and it is the fault of the eglot/lsp-mode capfs, which are inefficient for some reason, which is not know to me. Probably some small overhead scattered over multiple places in the IPC stack. (I've considered writing a lsp-capf package which just provides capf and nothing else and talks efficiently to lsp-servers.) If the culprit is the lsp-server, then I cannot understand the complaint, since all editors will be affected.

In this demo here I am just looking at elisp completion, which usually leads to a very big completion candidate list if you use overly aggressive completion settings like corfu-auto-delay=0 and corfu-auto-prefix=0.

I also saw complaints that overlays were slow, which isn't a corfu thing.

Nonsense. Overlays are not slow, at least not for that use case. Overlays and markers become slow if you have many many of them and Emacs has to update the locations when typing. Emacs uses a linked list instead of some tree structure such that these don't scale well. But for popups this does not matter.

On the other hand, child frames are much slower since they go through the X server. But still, I think they are sufficiently fast in Corfu.

Is the idea of incremental for orderless? Looks quite interesting. Is it always true that if a search string is a prefix of another search string, its results are a superset of the longer search's results? Even with, e.g., orderless-without-literal?

It requires you to pass a prefix into the completion table, so it will work with basic, emacs21 and emacs22. Orderless will only work if you transform the first word into a ^prefix regular expression, which will then be passed to the table. You probably recall the discussion on the Orderless issue tracker about that.

I personally doubt all-completions is the bottleneck, but I could be wrong.

It is for the elisp capf at least. Take a profile. Vertico M-x has the same issue. The bottleneck is always all-completions (the filtering). I've optimized away all other parts, e.g., I optimized sorting using buckets.

What we need to test is a big candidate list, and a benchmark/testing framework, perhaps with elp. I once helped Tramp's author with a test of this sort; here's an example of what we ended up with.

Elisp completion provides such lists. You can simply evaluate the code given above in the scratch buffer. For now I just used the Emacs profiler and subjectively tested if it feels faster.

@jdtsmith
Copy link
Contributor

BTW did you hear of acm-mode?

... acm-mode to replace corfu. It is an auto-complete mode like company that supports async but much lighter.

It's part of lsp-bridge, which seems to achieve much better completion latency with lsp servers by using an external python process as go-between.

@minad
Copy link
Owner Author

minad commented Jun 25, 2022

BTW did you hear of acm-mode?

Yes, the author manateelazycat originally used Corfu for lsp-bridge. I became aware of it in minad/corfu#165. Since acm-mode/lsp-bridge reimplements the entire stack, he then decided to implement his own UI. Unfortunately it seems that there was not much interest in collaboration. From my understanding, there shouldn't be a reason for a separate UI besides having more control. This means that acm/lsp-bridge is just the same as Company or Autocomplete, reinventing the wheel again. Anyway for me the value of Corfu and my other packages is that I have something coherent and simple which works across all of Emacs.

It's part of lsp-bridge, which seems to achieve much better completion latency with lsp servers by using an external python process as go-between.

Why is that? Emacs can already communicate asynchronously with external processes. How does the middleman help here? Also Python is not exactly famous for being fast.

@jdtsmith
Copy link
Contributor

OK I actually tried it in emacs-lisp (with limited, the default). This is actually remarkable! I tested 5 times with

(message "%S" (benchmark-run (call-interactively #'completion-at-point)))

on single char function calls, like (f. It sped it up from 125ms average to 3ms!

In fact I can limit to 5000 candidates, and still it's around 35ms; very acceptable.

One question is sorting? Do the candidates that would be sorted to the top show up in the limited list?

@jdtsmith
Copy link
Contributor

How does the middleman help here? Also Python is not exactly famous for being fast.

Lots of people making this same reasonable point. I haven't actually tried it. Perhaps it's as simple as offloading some of the sorting/selection to another process, which then feels much faster.

Why is that? Emacs can already communicate asynchronously with external processes.

Having hacked for a while on an async process mode for iPython with an interruptible CAPF, as well as some other small Python asyncio and JS async projects, I have the vague feeling that Emacs does not have a very efficient async event loop. Sometimes interruptions take a long time, for example. Async is a "big deal" in JS and Python since they are often used in web apps and to target network resources; they tune async endlessly.

@minad
Copy link
Owner Author

minad commented Jun 25, 2022

One question is sorting? Do the candidates that would be sorted to the top show up in the limited list?

This is the problem with limiting. Since the candidates are filtered before sorting, we won't necessarily get the topmost candidates. But as soon as you enter more input, the set will get smaller and we get the desired candidates.

Without sorting, it does not make much sense to show thousands of candidates for (f. Instead more input should be required before the candidates are shown (corfu-auto-prefix>=3). However when sorting by history, even with many candidates, the topmost ones would be useful. But all-completions is simply not fast enough to filter the entire obarray :(

@minad
Copy link
Owner Author

minad commented Jun 25, 2022

Lots of people making this same reasonable point. I haven't actually tried it. Perhaps it's as simple as offloading some of the sorting/selection to another process, which then feels much faster.

Hmm. Filtering is already offloaded. The lsp server may return 50 candidates. Sorting is also disabled by lsp-mode/eglot to return the set as provided by the server, I guess?

Having hacked for a while on an async process mode for iPython with an interruptible CAPF, as well as some other small Python asyncio and JS async projects, I have the vague feeling that Emacs does not have a very efficient async event loop.

Oh yes. It is crap. I have a good example (minad/consult#272) - when starting multiple consult-ripgrep in recursive minibuffers, which produce TONS of output, it will quickly bring Emacs to its knees. On top, the run time will be distributed unfairly between the different processes (not round robin). But we have only few candidates for lsp??

Sometimes interruptions take a long time, for example. Async is a "big deal" in JS and Python since they are often used in web apps and to target network resources; they tune async endlessly.

Sure. These stacks are well-tuned. But we still have only few candidates and when we push in a middleman we still have to communicate asynchronously with the middleman, so why does it help? I think having a middleman is not a convincing idea.

Did you try my affe package at some point? There I transfer also very few candidates from backend to frontend and the frontend is always nicely responsive. So one reason could be that one has to make sure that the frontend never hits these worst-case scenarios. This is exactly what cape-capf-limited achieves in the case of Elisp completions.

@jdtsmith
Copy link
Contributor

Incremental is somewhat harder to evaluate, since the closure variables are dumped. It only helps on subsequent narrowing, I guess?

BTW, my completion function actually does something very much like this, but the cache persists outside of the CAPF. I found even with corfu, the CAPF gets called quite often (with quit-at-boundary checking), so dumping cache on each new CAPF call was slowing things down.

@minad
Copy link
Owner Author

minad commented Jun 25, 2022

As a separate data point - if we use a very simple capf with the 100 most common words everything is super fast. I also took a profile. So there is not really a bottleneck in the Corfu/Capf stack itself.

(setq-local
 cape--dict-words '(the be to of and a in that have I it for not on with he as you do at this but his by from they we say her she or an will my one all would there their what so up out if about who get which go me when make can like time no just him know take people into year your good some could them see other than then now look only come its over think also back after use two how our work first well way even new want because any these give day most us)
 completion-at-point-functions (list #'cape-dict)
 corfu-auto-delay 0
 corfu-auto-prefix 0)

@jdtsmith
Copy link
Contributor

Filtering is already offloaded... we still have only few candidates and when we push in a middleman we still have to communicate asynchronously with the middleman, so why does it help?

I tend to agree, but people are raving. I do wonder if the same candidates are coming through. Would be funny if it's a complicated cape-limited-table. The one thing I've read is that it evaluates and dumps most LSP responses, since they are by the time of arrival no longer valid. Could this be done with a carefully-tuned interruptible CAPF? Probably. But I can also believe the async (or thread servicing) loop in Python can grab new LSP input, compare a buffer-tick or what have you, and drop it faster than elisp could service a process-filter.

@minad
Copy link
Owner Author

minad commented Jun 25, 2022

Incremental is somewhat harder to evaluate, since the closure variables are dumped. It only helps on subsequent narrowing, I guess?

Yes, only with subsequent narrowing.

BTW, my completion function actually does something very much like this, but the cache persists outside of the CAPF. I found even with corfu, the CAPF gets called quite often (with quit-at-boundary checking), so dumping cache on each new CAPF call was slowing things down.

Of course. But this is because you are doing something wrong ;) We had this discussion numerous times. The capf itself should be very fast and only check for the symbol at point. The filtering and candidate generation itself should happen in the table.

@jdtsmith
Copy link
Contributor

As a separate data point - if we use a very simple capf with the 100 most common words everything is super fast. I also took a profile. So there is not really a bottleneck in the Corfu/Capf stack itself.

Yes! Like 0.5ms using my benchmark. Very good thought to try this. I will also try playing with a shorter auto-prefix and lsp at some point (once I finish some chainsawing I have to do...).

@minad
Copy link
Owner Author

minad commented Jun 25, 2022

I tend to agree, but people are raving. I do wonder if the same candidates are coming through. Would be funny if it's a complicated cape-limited-table.

Haha, I guess this is the case.

The one thing I've read is that it evaluates and dumps most LSP responses, since they are by the time of arrival no longer valid. Could this be done with a carefully-tuned interruptible CAPF? Probably.

Yes, it should be doable.

But I can also believe the async (or thread servicing) loop in Python can grab new LSP input, compare a buffer-tick or what have you, and drop it faster than elisp could service a process-filter.

That could be a reason (or something like that). The python process won't compare a buffer tick, because how should it know. But it will drop the last request as soon as a new request is incoming. But this should be also possible with make-process or make-network-process. I think in the end the point is just that the lsp-mode/eglot Capfs are not as good as they could be.

Yes! Like 0.5ms using my benchmark. Very good thought to try this. I will also try playing with a shorter auto-prefix and lsp at some point (once I finish some chainsawing I have to do...).

Haha, in case you mean actual chainsawing I hope you have a good one and don't lose any fingers. The last time I did chainsawing, I wrecked a new Bosch electrical chainsaw after a few hours. I think it wasn't made for that workload :-P

@jdtsmith
Copy link
Contributor

BTW, my completion function actually does something very much like this, but the cache persists outside of the CAPF. I found even with corfu, the CAPF gets called quite often (with quit-at-boundary checking), so dumping cache on each new CAPF call was slowing things down.

Of course. But this is because you are doing something wrong ;) We had this discussion numerous times. The capf itself should be very fast and only check for the symbol at point. The filtering and candidate generation itself should happen in the table.

:). I know. But still we've never understood each other I feel. I DO calculate results only the table, not in the CAPF; here's the guts of the table code (get-completions-list is the slow bit that calls out to iPython process):

        (ipython-better--with-completion-data
	    (statement position start-guess prefix result)
	  (when statement
	    (unless result ;; first attempt, cache result, or mark 'nomatch
	      (if-let ((res (condition-case nil
				(ipython-better-get-completions-list
				 statement position)
			      (ipython-better-command-interrupted 'interrupted))))
		  (progn
		    (setq result res)
		    (unless (eq res 'interrupted) ; no caching if interrupted
		      (setq prefix ; unmatched prefix string within "guess"
			    (if (> (caar res) start-guess) 
				(substring statement start-guess (caar res)) ""))))
		(setq result 'nomatch)))
	    (unless (or (not result) (eq result 'nomatch) (eq result 'interrupted))
	      (completion-table-with-context
	       prefix (cdr result) (substring probe (length prefix)) pred action))))

My CAPF runs quickly. The issue isn't "how long does it take a CAPF to run", the issue is my (former) belief that "a CAPF will not be called again until the results of the current table are no longer valid". That just isn't true. It is SORT OF true with corfu, without quit-at-boundaries enabled.

I wonder if quit-at-boundaries defeats incremental, for the same reason? Maybe not because the first (real) capf-call's table is still live, while the "am I still valid" capf-call runs quickly on the side, wastefully producing a nothing-burger table closure (or what have you).

@jdtsmith
Copy link
Contributor

in case you mean actual chainsawing I hope you have a good one

Yep, actual chainsawing. Earlier today had some serious guys with a giant chainsaw mill slabbing my old 5 foot diameter maple that came down. I'm on cleanup/firewood splitting duty. I will keep my Emacs-pinky intact.

@minad
Copy link
Owner Author

minad commented Jun 25, 2022

My CAPF runs quickly. The issue isn't "how long does it take a CAPF to run", the issue is my (former) belief that "a CAPF will not be called again until the results of the current table are no longer valid". That just isn't true. It is SORT OF true with corfu, without quit-at-boundaries enabled.

Ah okay I see. Then you are doing everything alright as long as these different calls don't interfere with each other. Now that you tell me this, I think I remember that you already told me before. The capf reentrancy is a bit weird, but it is also an economical design since we just have to compare the bounds of the two different calls. The alternative would have been to add some separate :completion-boundary function to the metadata.

I wonder if quit-at-boundaries defeats incremental, for the same reason? Maybe not because the first (real) capf-call's table is still live, while the "am I still valid" capf-call runs quickly on the side, wastefully producing a nothing-burger table closure (or what have you).

It won't defeat incremental. The table of the boundary check call is just thrown away.

Yep, actual chainsawing. Earlier today had some serious guys with a giant chainsaw mill slabbing my old 5 foot diameter maple that came down. I'm on cleanup/firewood splitting duty. I will keep my Emacs-pinky intact.

Quite a large tree! Isn't the wood usable for something else for such a large tree? Quite funny when people complain about their poor Emacs pinky. One should always chime in there and pivot the discussion to some woodworking discussion. All these Emacsers are not doing enough serious handwork ;)

@minad
Copy link
Owner Author

minad commented Jun 26, 2022

Another approach is to use basic completion for short input. Then we get valid candidates right away since we filter the whole completion table, but we take advantage of the faster prefix filtering.

(defun +fast-orderless-all-completions (string table pred point)
  (if (length> string 4)
      (orderless-all-completions string table pred point)
    (completion-emacs21-all-completions string table pred point)))

(defun +fast-orderless-try-completion (string table pred point)
  (if (length> string 4)
      (orderless-try-completion string table pred point)
    (completion-emacs21-try-completion string table pred point)))

(add-to-list 'completion-styles-alist
             '(+fast-orderless
               +fast-orderless-try-completion
               +fast-orderless-all-completions
               "Fast Orderless completion."))

(setq-local completion-styles '(+fast-orderless)
            corfu-auto-delay 0
            corfu-auto-prefix 0)

Alternatively just always use Orderless prefix filtering for the first word, for monotonous behavior.

(defun +orderless-prefix-dispatch (word index _total)
  (and (= index 0) `(orderless-regexp . ,(concat "^" (regexp-quote word)))))

(defun +orderless-fast-dispatch (word index total)
  (and (= index 0) (= total 1) (length< word 4)
       `(orderless-regexp . ,(concat "^" (regexp-quote word)))))

(orderless-define-completion-style +orderless-prefix
  (orderless-dispatch '(+orderless-prefix-dispatch))
  (orderless-matching-styles '(orderless-literal orderless-regexp)))

(orderless-define-completion-style +orderless-fast
  (orderless-dispatch '(+orderless-fast-dispatch))
  (orderless-matching-styles '(orderless-literal orderless-regexp)))

(setq-local completion-styles '(+orderless-fast)
            corfu-auto-delay 0
            corfu-auto-prefix 0)

minad added a commit to minad/corfu that referenced this issue Jun 26, 2022
@jdtsmith
Copy link
Contributor

jdtsmith commented Jun 26, 2022

Sorting is also disabled by lsp-mode/eglot to return the set as provided by the server, I guess?

Yes. Some of us override that ;). Some LSP servers have config to limit number completions btw.

I think a very small “proof of principle” CAPF for lsp completions would be brilliant. Maybe just write a replacement for eglot’s. Probably it will need some real control of the process to handle the fast paced, randomly interleaved request/response stream efficiently; i.e. to show that emacs async process callback can compete!

@jdtsmith
Copy link
Contributor

Isn't the wood usable for something else for such a large tree?

Absolutely. Hence the slabbing 2.5 inches thick. Need to air dry for a year then kiln dry.

@jdtsmith
Copy link
Contributor

Hmmm:

He told me that they spend hours and hours trying to make lsp-bridge work with corfu. At the end of May he said, “about 50% of my time developing lsp-bridge is trying to fix bugs related to corfu, whether it's on the lsp-bridge side, or the corfu side, it was really painful.”

@minad
Copy link
Owner Author

minad commented Jun 26, 2022

He told me that they spend hours and hours trying to make lsp-bridge work with corfu. At the end of May he said, “about 50% of my time developing lsp-bridge is trying to fix bugs related to corfu, whether it's on the lsp-bridge side, or the corfu side, it was really painful.”

This does not sound right to me. There was no reaching out. The author of lsp-bridge did not report bugs related to Corfu, except for the formatting/resizing issue, for which he presented an insufficient hack as "fix". It could be that the problem is more pronounced if you use backends producing Chinese text, since my focus has been on fixed-pitch fonts (originally Corfu started with overlays where fixed-pitch was the only option anyway). You probably recall that we discussed about pixel-based resizing and alignment. If you look at all the Elisp projects of mine, the bug count is low.

However I acknowledge that the Capf situation is difficult. When I started Corfu, I expected the Capf backends to be of higher quality (comparable to completion backends for completing-read as used by Vertico). I also fixed (or reported bugs) in lsp-mode related to their Capf. Furthermore there are (or were, fixed in Emacs 29?) bugs in the Eshell/Pcomplete mechanism. One reason was that Company was (and still is) the dominating completion UI for Capfs, which led to implementations which didn't follow the API precisely enough. But the situation is improving, also thanks to Corfu/Cape.

The second issue could be that the completion/capf machinery is non-trivial with the completion styles etc. We use that in Corfu and Vertico since it allows us to integrate with all the existing completion tables in Emacs. I like to use the completion system across various modes and it works great for me (Eshell/Pcomplete, Elisp, programming modes, Cape, Dabbrev, Tempel, ...). If you want to use this with Lsp it is possible, but it is non-trivial. There is an impedance mismatch between Lsp and the Emacs completion machinery.

Third, the idea to use a Python middleman sounds dubious. I don't see a reason why this would improve anything. We still need asynchronous communication with the middleman. We could as well communicate directly with the Lsp server.

Fourth, the lsp-bridge author basically copied parts of Corfu, Cape etc verbatim without acknowledging it. I fought quite a bit with the child frame API in Corfu, so he profited from the work which has been put into that. By then throwing away all the completion related machinery, one will end up with a simpler system, but it will be less coherent across Emacs and it won't be as flexible. It is basically another monolith, the antithesis to the components I've made.

@minad
Copy link
Owner Author

minad commented Jun 26, 2022

@jdtsmith Maybe you could also comment there and forward my response ;) (EDIT: I saw you already did.)

@jdtsmith
Copy link
Contributor

Doesn't exactly inspire confidence.

By the way, having taken a closer look at lsp-bridge, my joke was actually correct. Candidate limits to 30 (or 100), may explain much of the speedup. Also implements a python-based word-regex dabbrev-alike caching backend, which could be faster than dabbrev, since it updates on file changes.

@jdtsmith
Copy link
Contributor

forward my response

Done. Are you off reddit? Probably a good call.

@minad
Copy link
Owner Author

minad commented Jun 26, 2022

By the way, having taken a closer look at lsp-bridge, my joke was actually correct. Candidate limits to 30 (or 100), may explain much of the speedup.

Ouch.

Also implements a python-based word-regex dabbrev-alike caching backend, which could be faster than dabbrev, since it updates on file changes.

This is actually a good idea - a dabbrev backend in an external process. It would be nice if someone implements this in a reusable way. In Cape I just reuse what is already there. Unfortunately cape-dabbrev/dabbrev.el is slow. But with cape-dabbrev-check-other-buffers=nil it is okay if you are fine with the current buffer only.

Done. Are you off reddit? Probably a good call.

Yes. I really dislike how user hostile this platform is. It wants to force you more and more into using the app or to login. Also it is meme driven. I still read it from time to time if something interesting is coming up. Anyway, to keep track of Emacs, Sacha Chua's news are wonderful. All the interesting stuff is there.

I thought a bit more about Lsp and the impedance mismatch. From what I remember about lsp, the server always wants to be informed about the current buffer state, such that it has a full picture. Then lsp completion is just asking for candidates at the current point and this is quite different from the Capf two-step process. To adapt Lsp to that model is kind of mind bending. If I wouldn't care about relying on existing Emacs machinery (because of my Vertico work etc), I can understand that one wants to throw this away and just rebuild something exclusively for Lsp.

@minad minad closed this as completed Jun 26, 2022
@jdtsmith
Copy link
Contributor

From what I remember about lsp, the server always wants to be informed about the current buffer state, such that it has a full picture.

Yes, it sends updates similar to what after-change-functions provides, and presumably the server keeps a mirror copy of the buffer in memory.

Then lsp completion is just asking for candidates at the current point and this is quite different from the Capf two-step process.

Yes. Interestingly, this is the same situation for my iPython mode. Perhaps not so surprising: it uses rope as a backend, which is actually the same package pylsp uses for completions!

What I settled on as an impedance-matching filter:

  1. Skip strings/comments/etc. locations.
  2. Guess a completion region, being somewhat conservative.
  3. Send the full command line and point position to iPython's completion engine (~same as sending to LSP).
  4. Await the response; if interrupted, abandon it.
  5. Part of the response is the boundaries of the "thing that is being completed".
  6. Use those to respond to the boundaries CAPF table request, if needed.
  7. Cache the candidates and reply with them for other CAPF table calls.

It works surprisingly well (after quite a bit of tuning).

@jdtsmith
Copy link
Contributor

But with cape-dabbrev-check-other-buffers=nil it is okay if you are fine with the current buffer only.

From a cursory read, it looks to cache words for the entire project, which is pretty great.

@minad
Copy link
Owner Author

minad commented Jun 26, 2022

I played a bit around. I agree with manateelazycat that the mismatch is significant. But it is doable to achieve full compatibility with the lsp way of doing things. I also disable the completion styles. This is the large chunk of code I ended up with:

(defvar-local lsp--begin nil)
(defvar-local lsp--end nil)
(defvar-local lsp--completions nil)

(defun lsp--capf ()
  (when lsp--completions
    (list
     lsp--begin lsp--end
     (lambda (str _pred action)
       (pcase action
         (`(boundaries . ,_) nil)
         ('metadata
          '(metadata
            (category . lsp)
            (display-sort-function . identity)
            (cycle-sort-function . identity)))
         ('t (copy-sequence lsp--completions))
         ('nil (try-completion str lsp--completions))
         (_ t))))))

(defun lsp--update (&rest _)
  ;; Synchronize with the server.
  ;; All the following can happens asynchronously from a callback.
  ;; Update the bounds and completions with some fake results.
  (if-let (bounds (bounds-of-thing-at-point 'symbol))
      (progn
        (setq lsp--begin (car bounds) lsp--end (point)
              lsp--completions (all-completions
                                (buffer-substring-no-properties lsp--begin lsp--end)
                                '(alpha1 alpha2 alpha3 alpha4 alpha5 alpha6 alpha7 alpha8 alpha9 alpha10
                                  beta1 beta2 beta3 beta4 beta5 beta6 beta7 beta8 beta9 beta10
                                  gamma1 gamma2 gamma3 gamma4 gamma5 gamma6 gamma7 gamma8 gamma9 gamma10
                                  delta1 delta2 delta3 delta4 delta5 delta6 delta7 delta8 delta9 delta10)))
        (let ((completion-at-point-functions '(lsp--capf))
              (completion-cycle-threshold nil))
          ;; Either trigger Corfu explicitly or call completion-at-point.
          (let ((corfu-auto-delay 0)
                (corfu-auto-prefix 0))
            (corfu--auto-complete (corfu--auto-tick)))
          ;;(unless completion-in-region-mode
          ;;  (completion-at-point))
          ))
    (setq lsp--completions nil)))

(add-hook 'after-change-functions 'lsp--update nil 'local)

(defun lsp--passthrough-all-completions (str table _pred _pt)
  (funcall table str nil t))

(defun lsp--passthrough-try-completion (str _table _pred pt)
  (let ((res (funcall table str nil nil)))
    (if (stringp res)
        (cons res (length res))
      res)))

(add-to-list 'completion-styles-alist
             '(lsp--passthrough
               lsp--passthrough-try-completion
               lsp--passthrough-all-completions
               "Passthrough style."))
(setq-local completion-category-overrides '((lsp (styles lsp--passthrough))))

;; Enable corfu without auto completion since we trigger in lsp--update.
(setq-local corfu-auto nil)
(corfu-mode -1)
(corfu-mode 1)

@minad
Copy link
Owner Author

minad commented Jun 26, 2022

My main gripe with the code is that we have to trigger Corfu manually from the asynchronous callback in lsp--update. I think this is what ultimately made the lsp-bridge author giving up. This is where we have the actual mismatch between the asynchronous nature of lsp and the capfs. The rest is just boilerplate. It is not nice but it is also not an ultimate blocker.

@minad
Copy link
Owner Author

minad commented Jun 26, 2022

Next attempt - now a bit more natural to Capfs. Nothing Corfu specific left.

;; -*- lexical-binding: t -*-

(defvar-local lsp--begin nil)
(defvar-local lsp--completions nil)
(defvar-local lsp--buffer-tick 0)
(defvar-local lsp--server-tick 0)
(defvar-local lsp--waiting nil)
(defvar-local lsp--timer nil)

(defun lsp--capf ()
  (lsp--wait)
  (when lsp--completions
    (list
     lsp--begin (point)
     (lambda (str _pred action)
       (lsp--wait)
       (pcase action
         (`(boundaries . ,_) nil)
         ('metadata
          '(metadata
            (category . lsp)
            (display-sort-function . identity)
            (cycle-sort-function . identity)))
         ('t (copy-sequence lsp--completions))
         ('nil (try-completion str lsp--completions))
         (_ t))))))

(defun lsp--wait () ;; Interruptible wait
  (while (and (< lsp--server-tick lsp--buffer-tick) (not (input-pending-p)))
    (unwind-protect
        (let ((lsp--waiting t))
          (sit-for 0.5))
      (setq unread-command-events (delete '(t . lsp--tick) (delq 'lsp--tick unread-command-events))))))

(defun lsp--update (&rest _)
  (cl-incf lsp--buffer-tick)
  (when lsp--timer
    (cancel-timer lsp--timer)
    (setq lsp--timer nil))
  (let ((tick lsp--buffer-tick))
    ;; Simulate some server delay
    (setq lsp--timer (run-at-time
                      0.02 nil
                      (lambda ()
                        (setq lsp--timer nil)
                        (if-let (bounds (bounds-of-thing-at-point 'symbol))
                            (setq lsp--begin (car bounds)
                                  lsp--completions (all-completions
                                                    (buffer-substring-no-properties lsp--begin (point))
                                                    '(alphaa1 alphaa2 alphaa3 alphab1 alphab2 alphab3 alphac1 alphac2 alphac3
                                                      betaa1 betaa2 betaa3 betab1 betab2 betab3 betac1 betac2 betac3
                                                      gammaa1 gammaa2 gammaa3 gammab1 gammab2 gammab3 gammac1 gammac2 gammac3)))
                          (setq lsp--completions nil))
                        (setq lsp--server-tick tick)
                        (when lsp--waiting
                          (push 'lsp--tick unread-command-events)))))))

(add-hook 'after-change-functions 'lsp--update nil 'local)

(defun lsp--passthrough-all-completions (str table _pred _pt)
  (funcall table str nil t))

(defun lsp--passthrough-try-completion (str table _pred _pt)
  (let ((res (funcall table str nil nil)))
    (if (stringp res)
        (cons res (length res))
      res)))

(add-to-list 'completion-styles-alist
             '(lsp--passthrough
               lsp--passthrough-try-completion
               lsp--passthrough-all-completions
               "Passthrough style."))

(setq-local completion-category-overrides '((lsp (styles lsp--passthrough)))
            completion-at-point-functions '(lsp--capf)
            corfu-auto t
            corfu-auto-prefix 0
            corfu-auto-delay 0)

@jdtsmith
Copy link
Contributor

Whoa, very cool. Is lsp--wait simulating the server contact and response? That will include the aforementioned json parsing. I wonder if while-no-input can interrupt it.

@minad
Copy link
Owner Author

minad commented Jun 27, 2022

No parsing will happen in an asynchronous callback. However I am not excluding that one could still use a middleman. If lsp sends too much data it makes sense to move this outside the main Emacs process.

@jdtsmith
Copy link
Contributor

jdtsmith commented Dec 4, 2022

FYI, one of the lsp-bridge authors just gave a talk about it and linked this very issue. I do think it would be interesting to make an external process that interfaces with the LSP server to reduce latency, but also "speaks CAPF" to Emacs. You could probably even have the middleman "speak orderless" using its compiled regex matching to pre-filter completion lists outside of Emacs. Another thing they don't seem to mention: it's possible to ask many lsp servers to return a limited number of candidate matches. If you are receiving 5000 matches, you probably won't notice the difference with 500.

@minad
Copy link
Owner Author

minad commented Dec 4, 2022

@jdtsmith Thanks. In the talk it was mentioned that I and the lsp-bridge authors came to common conclusions. This is inaccurate and underspecific.

  1. I agree that off-loading the candidate generation to an external process is a nice idea, such that the work for the single-threaded Elisp runtime is reduced. But instead of Python one could have as well used an external Emacs process, see my Affe package. Ideally Emacs would become a multi-threaded runtime, not with a shared address space, but with worker threads, similar to JS web workers. The candidate limitation of the LSP servers may be a solution. However the protocol overhead may still be large.
  2. I question the conclusion of the lsp-bridge authors to avoid the Capf mechanism. The Capf mechanism can be use with external candidate generation (or with the LSP protocol). See below for an example.
  3. The authors of lsp-bridge neglect the central advantage of the Capf mechanism: Modularity and extensibility. In lsp-bridge there are backends for citre, elisp, lsp, path, dictionaries, tabnine, tailwind, telega, tempel, yas and probably more. In Corfu we just reuse all the existing Capfs. Only a few of them have performance issues and could be rewritten one by one. For me having clear API boundaries and a separation of concerns is a big advantage of Corfu, also with respect to long term maintainability.

Now let's look at how an external process Capf could look like. The candidate computation is completely off-loaded to an external process which observes the buffer. This model is consistent with the LSP protocol. We create an external-capf and configure the completion UI (e.g. Corfu) to use it.

;; -*- lexical-binding: t -*-

;;; External CAPF. The candidate computation is completely off-loaded to an
;;; external process.

(defvar-local external-capf--begin nil)
(defvar-local external-capf--candidates nil)

(defun external-capf ()
  (and external-capf--begin
       external-capf--candidates
    (list external-capf--begin (point)
          (lambda (str pred action)
            (if (eq action 'metadata)
                '(metadata (display-sort-function . identity)
                           (cycle-sort-function . identity))
              (complete-with-action action external-capf--candidates str pred)))
          :company-prefix-length t
          :company-deprecated
          (lambda (x) (get-text-property 0 'deprecated x))
          :annotation-function
          (lambda (x) (get-text-property 0 'annotation x)))))

(setq-local completion-styles '(flex)
            completion-category-overrides nil
            completion-category-defaults nil
            completion-at-point-functions '(external-capf)
            corfu-auto t
            corfu-auto-prefix 0
            corfu-auto-delay 0)

;;; Setup external process. The external process observes buffer modifications
;;; and computes completion candidates.

(defvar-local external-process nil)
(ignore-errors (delete-process external-process))
(add-hook 'after-change-functions #'external-process-observer nil 'local)

(defun external-process-start ()
  (unless (process-live-p external-process)
    (ignore-errors (delete-process external-process))
    (setq external-process
          (make-process :name "external-capf"
                        :command (list (expand-file-name "external-capf.sh"))
                        :noquery t
                        :connection-type 'pty
                        :stderr " *external-stderr*"
                        :filter
                        (let ((buf (current-buffer)))
                          (lambda (_proc output)
                            (when (buffer-live-p buf)
                              (with-current-buffer buf
                                (ignore-errors
                                  (setq output (split-string output))
                                  (setq external-capf--begin (string-to-number (car output))
                                        external-capf--candidates (cddr output))))))))))
  external-process)

(defun external-process-observer (&rest _)
  (if-let* ((bounds (bounds-of-thing-at-point 'symbol))
            (str (buffer-substring-no-properties (car bounds) (cdr bounds))))
      (process-send-string (external-process-start) (format "%s %s\n" (car bounds) str))
    (setq external-capf--begin nil
          external-capf--candidates nil)))

The simple external completion process which queries the dictionary using grep.

#!/bin/bash
while IFS='$\n' read -r line; do
    split=($line)
    beg=${split[0]}
    prefix=${split[1]}
    words=`grep $prefix /usr/share/dict/words | sort | uniq | head -n 20 | tr '\n' ' '`
    echo "$beg $words"
done

Anyway I think the lsp-bridge authors and I just have fundamentally different goals. They are interested in a performant out of the box solution no matter the complexity, no matter if it integrates with existing modes. I call this the VS code approach. In contrast, I am interested in a minimal solution which reuses all the already existing parts of Emacs and integrates with them naturally. I enjoy Corfu in Eshell, Elisp, text buffer (Mainly the Cape Capfs) and certain lightweight programming modes.

The LSP packages on the other hand have performance and correctness issues which should be sorted out, maybe by off-loading work to external processes. However this ship may have sailed given that Eglot was accepted in core with its current architecture. This is also the reason why I am critical of adding more and more packages to the core, which basically determines "canonical solutions".

@jdtsmith
Copy link
Contributor

jdtsmith commented Dec 4, 2022

Anyway I think the lsp-bridge authors and I just have fundamentally different goals. They are interested in a performant out of the box solution no matter the complexity, no matter if it integrates with existing modes. I call this the VS code approach.

That's a good synopsis. The question is whether a "maximal re-use approach" could approach the latency and speed of a fully custom approach. The lsp-mode people are experimenting with a fork of Emacs which drops the global lock while (specifically) processing jsonrpc in a separate thread. Without knowing too much about it, this seems like an interesting idea to me: expand in baby steps into parallel multi-threading in very limited contexts where the benefits are large and the maintenance costs small (json parsing is already external). Sort of similar to what numpy and other C-backed modules do with the Python GIL: when doing linear algebra in an ancient FORTRAN library, the GIL can often safely be dropped and speedups are significant.

The simple external completion process which queries the dictionary using grep.

In this example, how do you know the process has delivered all its results? An interesting variation of this: what if external-capf.sh got results both locally from file but also over the network (from some thesaurus service, say), and then delivered them to Emacs asynchronously upon their arrival? I think lsp-bridge can handle that scenario, updating the UI as results come in. This is also true for "resolving" completion items to get more information about them (docs, etc.).

this ship may have sailed given that Eglot was accepted in core with its current architecture

I haven't looked closely at the Eglot CAPF (other than the all-completions fix I made to allow orderless to work). But no amount of CAPF jiggering can get around the problem of LSP servers that want to dump 50kB of mostly-useless information on each keypress, that must be processed inside the same event loop where keystrokes are read and acted on. I just checked with my usual LSP server Pyright, and it is delivering something like 0.5–4kB per keypress (depending on completion depth). A far cry from 50kB, but that's still a lot of data to parse and memory to allocate & free at 40ms intervals.

@minad
Copy link
Owner Author

minad commented Dec 4, 2022

The question is whether a "maximal re-use approach" could approach the latency and speed of a fully custom approach.

Well, the example from above should have the same latency characteristics as lsp-bridge. For existing Capfs of course you won't get any latency improvements. However I also don't think it is a problem. Most cape-* capfs are very fast, as is pcomplete in eshell, as is css completion and so on. There is no latency problem there. It only matters for external Capfs.

The lsp-mode people are experimenting with a fork of Emacs which drops the global lock while (specifically) processing jsonrpc in a separate thread.

Yes, the parallel json parsing goes in the direction of having separate worker threads. I think this is a good idea.

But no amount of CAPF jiggering can get around the problem of LSP servers that want to dump 50kB of mostly-useless information on each keypress, that must be processed inside the same event loop where keystrokes are read and acted on.

Exactly. I also mentioned this above. The protocol is unnecessarily inefficient.

@MatthewZMD
Copy link

MatthewZMD commented Dec 6, 2022

With all due respect,

FYI, one of the lsp-bridge authors just gave a talk

FYI, I am not one of the lsp-bridge authors, I am not even a maintainer. I am a friend of the author, as per my statement in the first 30s of the talk. The author is @manateelazycat

In the talk it was mentioned that I and the lsp-bridge authors came to common conclusions. This is inaccurate and underspecific.

By "common conclusion", I mean:

My main gripe with the code is that we have to trigger Corfu manually from the asynchronous callback in lsp--update. I think this is what ultimately made the lsp-bridge author giving up. This is where we have the actual mismatch between the asynchronous nature of lsp and the capfs. The rest is just boilerplate. It is not nice but it is also not an ultimate blocker.

Yes, you've found the reason why the lsp-bridge author gave up. This is the common conclusion.

I apologize for not being very specific regarding a topic that I said "deserves its own talk" in a tightly scheduled 20-minute talk. It was the reason why I linked this issue for people to look at to get a full picture.

@minad
Copy link
Owner Author

minad commented Dec 6, 2022

@MatthewZMD Understood. I agree that there is a mismatch between Capf and LSP and that off-loading to worker jobs is a good idea, but this is like the least common denominator. Everybody who hacked on both Emacs and LSP agrees with that.

But I do not agree that it is a good idea to throw away the Capf mechanism completely, since I still want to profit from it for many modes where it works very well. This all leads to backends which have to be replicated again in lsp-bridge.

Also @manateelazycat made the complexity argument in his blog post. Corfu is not as simple as I would like it to be, but the main source of complexity is actually the child frame code. The second chunk of complexity comes from trying to integrate the diverse completion preferences from the Corfu users. Furthermore while acm.el may have started simple, it is now as large as corfu.el.

@manateelazycat
Copy link

Well, lsp-bridge's goal is very different with corfu.

corfu's target is build a light completion menu for capf backends.

lsp-bridge's target is build fastest completion framework for LSP.

I'm not just dislike capf, in principle, I don't think elisp is fast enough for most completion (single-thread and poor performance), you can test my other completion backends:

  1. https://github.com/manateelazycat/lsp-bridge/blob/master/core/search_sdcv_words.py
  2. https://github.com/manateelazycat/lsp-bridge/blob/master/core/search_elisp_symbols.py
  3. https://github.com/manateelazycat/lsp-bridge/blob/master/core/search_file_words.py

Above backends is use Python's multi-thread search and filter ten thousand candidates, is much much faster than search candidates in Elisp.

There have three reasons:

  1. single-thread: Emacs' thread mechanism is like coroutine, and not concurrent threads, that mean if calculate can't finish in 40ms (generic user type two characters in 100ms), main thread haven't chance to switch back like real concurrent thread, then user will feeling block

  2. Random GC: when filter huge data in realtime, Emacs will use many memory for build objects, it will block user once Emacs GC start run, and we don't know when time Emacs will start run GC

  3. Unnecessary search: capf use candidate as key, everytime we render candidates we need search all candidates again, and modern computer have many memory, we can build all key/value in plist when backend finished search, trade space for time to improve performance

Anyway, as I said, acm.el fast is not because acm.el light or it's design, acm.el fast because lsp-bridge's multi-thread design and python search backend.

IMO, Emacs just need provide data and render result, Emacs shouldn't do search job, most backend should implementation in external process for highest performance.

I love Emacs and write 400+ plugins in 17 years, but I don't think Elisp is best choose to do search job, you can test my other search framework https://github.com/manateelazycat/blink-search , it's much faster than ivy and helm, blink-search use same technology like lsp-bridge.

Last, thanks for create corfu, it's a good completion framework, lsp-bridge or acm's target is not race with corfu.

Sorry, my english is too bad can't represent my point exactly, I use my lsp-bridge-sdcv-helper write above words.

@minad
Copy link
Owner Author

minad commented Dec 6, 2022

@manateelazycat I agree with most things you said.

corfu's target is build a light completion menu for capf backends. lsp-bridge's target is build fastest completion framework for LSP.

Yes, the two are different. But I think it would have been nicer if you tried to integrate with the Capf mechanism, such that we could just integrate the fast asynchronous backend of lsp-bridge with Corfu and all the other Capfs. I want to emphasize again - not all Capfs have performance issues and I would like to keep using what is there.

Above backends is use Python's multi-thread search and filter ten thousand candidates, is much much faster than search candidates in Elisp.

The point here is that you off-load to a second process. But Python has a very slow bytecode interpreter and when doing CPU bound computations there is still the Gil. Note that all-completions in Emacs is implemented in C. But if it runs in the GUI thread that is of course not great.

Random GC: when filter huge data in realtime, Emacs will use many memory for build objects, it will block user once Emacs GC start run, and we don't know when time Emacs will start run GC

Yes, this matters. Off-loading memory load to an external process helps a lot if procssing large amounts of data. As has been discussed here, Lsp generates bloated responses.

Unnecessary search: capf use candidate as key, everytime we render candidates we need search all candidates again, and modern computer have many memory, we can build all key/value in plist when backend finished search, trade space for time to improve performance

Here I don't agree. One could as well implement caching within a Capf such that recomputations are reduced.

@manateelazycat
Copy link

manateelazycat commented Dec 6, 2022

capf is too complicated and I tired to write dirty code that mix asynchronous backend and capf backend, I have try did those job when I created lsp-bridge, too much dirty code, I dislike, that why I create acm.el, it design for asynchronous backend, code is clean and easy to maintain, no magic place.

Yes, we can wrote code to caching capf data, but why not store all search results in candidate first? Why we need tweak code for old rule of capf?

Rewrite all backend with asynchronous need time, but I don't think sticking to capf is the right choice, user want fast completion experience.

Sorry, I just want explain my point, no intention of arguing, everyone has the freedom to choose different plugins, I won't follow this thread, sorry.

@minad
Copy link
Owner Author

minad commented Dec 6, 2022

capf is too complicated and I tired to write dirty code that mix asynchronous backend and capf backend, I have try did those job when I created lsp-bridge, too much dirty code, I dislike, that why I create acm.el, it design for asynchronous backend, code is clean and easy to maintain, no magic place.

Understood. But code clarity etc also lie in the eye of the beholder. I also argue that Corfu is reasonably clean. I agree that the Capf and completing-read APIs are complicated, some of it justified, some due to legacy. On the other hand I enjoy for example Eshell as my main shell and the existing pcomplete mechanism works nicely. I get value from reusing what is there.

Yes, we can wrote code to caching capf data, but why not store all search results in candidate first?

That's trivially possible of course within a Capf backend. The advantage of the Capf API is for example that it has the ability of additionally compute annotations lazily on demand.

Rewrite all backend with asynchronous need time, but I don't think sticking to capf is the right choice, user want fast completion experience.

I agree that we all want a fast UI. But I don't agree on the means. I don't think off loading to an external python framework and throwing away the existing Capfs is the solution. Implementing asynchronous backends is a good idea as long as we can keep what is there at the same time. This is my opinion and preference. Some users may share it, some don't.

@jdtsmith
Copy link
Contributor

jdtsmith commented Dec 6, 2022

Having struggled to build a performant external-process-tied CAPF of my own (talking to iPython) I agree that it is quite complicated. On top of that, it is not well documented. Some of the people who have the best understanding of the CAPF multi-stage setup/complete/teardown process disagree with each other on things like "how long should a completion table remain valid", and "whose job is it to invalidate a completion table once the input changes?". And we read various cryptic comments in the Emacs code to try to decide. Not ideal.

But I also strongly agree with @minad that throwing it out (warts and all) is a step in the wrong direction. This is not at all an opinion against lsp-bridge or its impressive gains in latency, just against the idea of moving away from the freedom to customize and mix different functionality that the standard Emacs APIs make possible. I think a lot of people share our opinion, who might otherwise be interested in giving lsp-bridge a try.

I wonder, @manateelazycat, if you could have your same low latency and async communication to an external process — a middleman which protects Emacs from the overly complex and heavy LSP data stream — but also have this functionality be exposed as a normal CAPF, would that be preferable? Then you wouldn't need to reimplement all the other CAPFs out there (past, present, and future) that are already "fast enough". I foresee a flowering of tree-sitter-based CAPFs next year, for example.

I haven't tried, but the CAPF-based "jsonrpc in a thread" experiment the lsp-mode authors are trying is getting good reports. That would seem to indicate that if you can provide completion data fast enough, and without blocking the UI event loop, a CAPF can work well with LSP. I do personally wonder if a Python (or other) process could provide similar benefits without having to abandon CAPF altogether (much as I can imagine why you did so yourself!).

In any case, it is good to discuss these things; IMO, Emacs can't improve without some "productive breakage".

@minad
Copy link
Owner Author

minad commented Dec 6, 2022

@jdtsmith

Some of the people who have the best understanding of the CAPF multi-stage setup/complete/teardown process disagree with each other on things like "how long should a completion table remain valid", and "whose job is it to invalidate a completion table once the input changes?". And we read various cryptic comments in the Emacs code to try to decide. Not ideal.

You keep on repeating this. But the API is clear about it. The table returned by the Capf should not be prefiltered (see the note in the docstring). This implies that the table itself is responsible for filtering. Second, completion-at-point calls the capfs and the calls completion-in-region. As soon as we entered completion-in-region we don't have access to the Capf anymore. We can fix this with a trick, see cape-capf-buster. These are facts based on the existing design of minibuffer.el, which is the reference design. Company uses Capfs in a different incompatible way. If you look at it purely from the position of a Capf author there may seem to be a disagreement since Company throws the table away on every single keypress. If you take the entire design into account then keeping the table alive is the right design. It also makes sense from the design pov. The two step process has been designed such that completion tables are reused by capf and completing-read. The entire filtering is done by the table. Prefiltering in the first step would be an adhoc addition, and as such should be rejected.

This is not at all an opinion against lsp-bridge or its impressive gains in latency, just against the idea of moving away from the freedom to customize and mix different functionality that the standard Emacs APIs make possible. I think a lot of people share our opinion, who might otherwise be interested in giving lsp-bridge a try.

Agree fully!

...but also have this functionality be exposed as a normal CAPF, would that be preferable? Then you wouldn't need to reimplement all the other CAPFs out there (past, present, and future) that are already "fast enough".

I would also like to see this.

In any case, it is good to discuss these things; IMO, Emacs can't improve without some "productive breakage".

Of course! I also appreciate the work going in this direction and it is good to have alternatives and try alternative approaches.

@manateelazycat
Copy link

manateelazycat commented Dec 6, 2022

Having struggled to build a performant external-process-tied CAPF of my own (talking to iPython) I agree that it is quite complicated. On top of that, it is not well documented. Some of the people who have the best understanding of the CAPF multi-stage setup/complete/teardown process disagree with each other on things like "how long should a completion table remain valid", and "whose job is it to invalidate a completion table once the input changes?". And we read various cryptic comments in the Emacs code to try to decide. Not ideal.

But I also strongly agree with @minad that throwing it out (warts and all) is a step in the wrong direction. This is not at all an opinion against lsp-bridge or its impressive gains in latency, just against the idea of moving away from the freedom to customize and mix different functionality that the standard Emacs APIs make possible. I think a lot of people share our opinion, who might otherwise be interested in giving lsp-bridge a try.

I wonder, @manateelazycat, if you could have your same low latency and async communication to an external process — a middleman which protects Emacs from the overly complex and heavy LSP data stream — but also have this functionality be exposed as a normal CAPF, would that be preferable? Then you wouldn't need to reimplement all the other CAPFs out there (past, present, and future) that are already "fast enough". I foresee a flowering of tree-sitter-based CAPFs next year, for example.

I haven't tried, but the CAPF-based "jsonrpc in a thread" experiment the lsp-mode authors are trying is getting good reports. That would seem to indicate that if you can provide completion data fast enough, and without blocking the UI event loop, a CAPF can work well with LSP. I do personally wonder if a Python (or other) process could provide similar benefits without having to abandon CAPF altogether (much as I can imagine why you did so yourself!).

In any case, it is good to discuss these things; IMO, Emacs can't improve without some "productive breakage".

capf is wrong direction if you want fastest completion speed, I create lsp-bridge's target is make Emacs completion speed reach to fastest, same as VSCode, not for capf backends.

I want fastest performance, I don't want fast enough.

I'm not agree capf can merge with async backend, I hate wrote dirty code that hard to understand and hard to maintain.

Anyway, everyone has right to choose capf or not, but I say NO to capf.

@jdtsmith
Copy link
Contributor

jdtsmith commented Dec 6, 2022

@minad

disagree with each other on things like "how long should a completion table remain valid", and "whose job is it to invalidate a completion table once the input changes?".

You keep on repeating this. But the API is clear about it. ... If you take the entire design into account then keeping the table alive is the right design. It also makes sense from the design pov.

My statement is that authors disagree, with firmly held opinions; this is categorically true. I do agree your interpretation is sensible, but some CAPF authors hold defensible alternative views. You have to admit there is not a nice "API contract for CAPFs" in the documentation anywhere, "Programmed Completion" being quite light on details. Maybe the right move is to write up such a CAPF API doc and see if emacs-devel can come to consensus on it. Honestly it would be very useful to have such a document, showing how the flow control goes in various contexts including completion styles, when a CAPF and table-function can expect to be called, etc. I definitely sympathize with @manateelazycat for wanting to avoid CAPF entirely (even if I appreciate its advantages).

Prefiltering in the first step would be an adhoc addition, and as such should be rejected.

My understanding of the disagreement is that it's not about "pre-filtering", which works against fancier completion styles, but about "invalidation": when a table is no longer appropriate for the entity in buffer (maybe you think of that as pre-filtering?). Here there is a difference from completing-read, which starts as a "blank slate". It would be lovely if one could ask an LSP server for "any thing under the sun that could conceivably be completed here, discounting what I've typed already" — effectively the LSP equivalent of an obarray — but that's not how they work (and would make the latency problems we're discussing much worse!).

@manateelazycat

I say NO to capf

Fair enough (and I understand why)!

@MatthewZMD
Copy link

This is not at all an opinion against lsp-bridge or its impressive gains in latency, just against the idea of moving away from the freedom to customize and mix different functionality that the standard Emacs APIs make possible. I think a lot of people share our opinion, who might otherwise be interested in giving lsp-bridge a try.

It is an unfortunate compromise. @manateelazycat biggest focus is practicality that provides a fastest solution instead of coherency. We already have a fragmented-enough ecosystem in Emacs anyways, there's no harm to fragment it a little more if it can provide such immediate benefit ;) Nevertheless capf or lsp-mode or emacs core may still be improved in this direction, but Lazycat is probably not the person who's willing to put effort into it.

@minad
Copy link
Owner Author

minad commented Dec 6, 2022

@jdtsmith I kind of disagree with your characterization of the state of affairs.

  1. Actually there is a contract. It is defined by minibuffer.el. As I wrote above, throwing the table away is impossible by definition of the API. It is only possible with cape-capf-buster which recalls the Capf. But note that cape-capf-buster has more information than completion-in-region since it wraps the Capf directly.
  2. Yes, there is a misunderstanding regarding what prefiltering means. If I compute a table which depends on the entity at point, this already is prefiltering. Again, this is defined by the contract of completion styles which may want to interpret the input in some other way. The Capf should do nothing else than returning a viable completion table at point. And again, we also had that many times - my interpretation is supported by the note in the docstring.
  3. You make this issue sound bigger as it is. As you see, my interpretation is tightly constrained by the APIs and I assume a rather rigid textual interpretation of the completion-at-point-functions docstring. Of course I also acknowledge the imperfection given that external filtering would have to return all possible candidates if we want to rely only on completion style. Anyway I even consider this problem a non-issue, since the problem only occurs as soon as you shorten the prefix. This happens only rarely during completion. It is a minor detail and for users, who care deeply about this, there is a proper solution in the form of cape-capf-buster, which allows fine-grained control over the caching. The problem is solved and yet, every time you bring this up again during completion discussions. Why? To prove the point that Capf is badly designed or badly documented? I don't find the two step design necessarily bad, but we all agree that the documentation is lacking.

@jdtsmith
Copy link
Contributor

jdtsmith commented Dec 6, 2022

The problem is solved and yet, every time you bring this up again during completion discussions. Why? To prove the point that Capf is badly designed or badly documented?

The latter. I feel the poor documentation in this area in general (not only this specific point) is an impediment to wider & more consistent adoption, and probably bears some of the blame for the recurring instinct to "abandon this mess" that people have had over the years. Sadly I don't myself have the depth of knowledge to correct the situation.

@minad
Copy link
Owner Author

minad commented Dec 6, 2022

@jdtsmith There is general agreement that the documentation is bad. If someone is interested in doing something about it, we would all greatly appreciate it. I share your feelings that the lack of documentation may lead to the aforementioned consequences.

But there is no point to discuss this over and over again on the Corfu/Cape/etc trackers. This is not the right place. You can bring it up again on the Emacs mailing list or even send improvements directly. By all the various responses and comments I've read from you, you certainly have an understanding which is more than deep enough.

@jdtsmith
Copy link
Contributor

jdtsmith commented Dec 6, 2022

Apologies, this was in furtherance of a new discussion in an old location with lsp-bridge people coming in from Reddit, who preferred to discuss it here rather than there. It's been productive for me to hear their views, but I will seek other venues in the future.

@minad
Copy link
Owner Author

minad commented Dec 6, 2022

Thanks! I am happy with productive discussions here, we had many in the past. But I am not so fond of reiterating the same points again and again without new insight. Also regarding the lsp-bridge approach vs capf, it does not seem as if there is much to be learned here. The projects are just different with incompatible goals. Anyway I'd like to see the existing APIs pushed further.

@johannes-mueller
Copy link

Now let's look at how an external process Capf could look like. The candidate computation is completely off-loaded to an external process which observes the buffer. This model is consistent with the LSP protocol. We create an external-capf and configure the completion UI (e.g. Corfu) to use it.

...

That looks interesting. Mind if I use code of it in my capf-wordfreq package?

@minad
Copy link
Owner Author

minad commented Mar 25, 2023 via email

@johannes-mueller
Copy link

johannes-mueller commented Mar 26, 2023

I don't mind, go ahead!

Thanks. I gave it a shot. Feel free to test :)

https://github.com/johannes-mueller/capf-wordfreq.el/blob/master/capf-wordfreq.el#L136

@minad
Copy link
Owner Author

minad commented Mar 26, 2023

@johannes-mueller Great, thanks! I am sure @jdtsmith will also be interested in this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants