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
Comments
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 What we need to test is a big candidate list, and a benchmark/testing framework, perhaps with |
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
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.
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
It is for the elisp capf at least. Take a profile. Vertico M-x has the same issue. The bottleneck is always
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. |
BTW did you hear of
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. |
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.
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. |
OK I actually tried it in emacs-lisp (with limited, the default). This is actually remarkable! I tested 5 times with
on single char function calls, like 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? |
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.
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. |
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 |
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?
Oh yes. It is crap. I have a good example (minad/consult#272) - when starting multiple
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 |
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. |
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) |
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 |
Yes, only with subsequent narrowing.
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. |
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, I guess this is the case.
Yes, it should be doable.
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
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 |
:). 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 ( (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). |
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. |
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
It won't defeat incremental. The table of the boundary check call is just thrown away.
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 ;) |
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) |
See minad/cape#52 for the discussion.
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! |
Absolutely. Hence the slabbing 2.5 inches thick. Need to air dry for a year then kiln dry. |
Hmmm:
|
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. |
@jdtsmith Maybe you could also comment there and forward my response ;) (EDIT: I saw you already did.) |
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. |
Done. Are you off reddit? Probably a good call. |
Ouch.
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
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. |
Yes, it sends updates similar to what after-change-functions provides, and presumably the server keeps a mirror copy of the buffer in memory.
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:
It works surprisingly well (after quite a bit of tuning). |
From a cursory read, it looks to cache words for the entire project, which is pretty great. |
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) |
My main gripe with the code is that we have to trigger Corfu manually from the asynchronous callback in |
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) |
Whoa, very cool. Is |
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. |
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. |
@jdtsmith Thanks. In the talk it was mentioned that I and the lsp-bridge authors came to common conclusions. This is inaccurate and underspecific.
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". |
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.
In this example, how do you know the process has delivered all its results? An interesting variation of this: what if
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. |
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.
Yes, the parallel json parsing goes in the direction of having separate worker threads. I think this is a good idea.
Exactly. I also mentioned this above. The protocol is unnecessarily inefficient. |
With all due respect,
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
By "common conclusion", I mean:
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. |
@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. |
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:
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:
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. |
@manateelazycat I agree with most things you said.
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.
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
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.
Here I don't agree. One could as well implement caching within a Capf such that recomputations are reduced. |
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. |
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.
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.
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. |
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". |
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.
Agree fully!
I would also like to see this.
Of course! I also appreciate the work going in this direction and it is good to have alternatives and try alternative approaches. |
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. |
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).
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
Fair enough (and I understand why)! |
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. |
@jdtsmith I kind of disagree with your characterization of the state of affairs.
|
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. |
@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. |
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. |
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. |
... That looks interesting. Mind if I use code of it in my capf-wordfreq package? |
On 3/25/23 13:17, Johannes Mueller wrote:
That looks interesting. Mind if I use code of it in my capf-wordfreq
<https://github.com/johannes-mueller/capf-wordfreq.el> package?
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 |
@johannes-mueller Great, thanks! I am sure @jdtsmith will also be interested in this. |
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:
Feedback? Comments? cc @jdtsmith What do you think?
The text was updated successfully, but these errors were encountered: