Good morning folks, I'm Matthew.
Welcome to another year of EmacsConf.
It's looking fantastic this year.
Firstly, I have to apologize for my voice
and occasional cough today.
I am currently recovering from a cold,
hopefully it's not Covid or flu,
so please bear with me today.
Actually, this talk was supposed to be brought to you
by Manatee Lazycat, the author of lsp-bridge.
But verbal English isn't Lazycat's strongest skill,
and we are good friends as we maintain
the Emacs Application Framework together,
so here I am today presenting to you this package.
Welcome to my talk on lsp-bridge:
a smooth-as-butter asynchronous LSP client.
What is LSP?
The first question is, what is LSP?
For anyone who doesn't know here,
LSP stands for Language Server Protocol,
it is a set of protocols defined by Microsoft
that provides smart features like autocomplete,
go to definition, documentation, etc.,
that can be implemented across different editors and IDEs.
It was initially created
for their Visual Studio Code product,
then publically shared with everyone.
So there are language servers out there
that implemented this procotol,
and editors need to implement the same procotols
to talk to the language servers
in order to retrieve necessary information.
Emacs has 2 LSP clients already, the lsp-mode and eglot,
both implemented the protocols and both are very good.
Now comes to the second question, of course,
given lsp-mode and eglot, why another LSP client?
I used to use lsp-mode all the time,
I have to say I really appreciate Ivan Yonchovski
and the team's efforts. Also, I'd like to congratuate eglot
for making into Emacs 29! These are fantastic packages,
they are very mature and robust.
However, with all due respect, both of the implementation
are fundamentally limited
by the single-threaded nature of Emacs,
it is neither the fault of lsp-mode nor eglot.
Although in recent years there have been
improvements to Emacs core such as native JSON support,
there are still scenarios where Emacs clog
for a brief second when processing large amounts of data,
as Emacs is processing everything in the single thread.
This problem is especially apparent in some LSP servers
that feeds in tens of thousands of JSON data
with every single key press.
Additionally, the large amount of data
sent by the LSP server, such as the completion candidates,
the diagnostics and documentation,
they are temporarily stored in the Emacs memory,
which will trigger garbage collection very frequently,
this also causes stuttering user experience.
Increasing the gc-cons-threshold helps,
but doesn't eliminate the problem.
For something like the LSP,
the language servers need time to compute,
and Emacs needs capacity to process and filter
all the data coming from the language servers.
A large codebase project with a slow language server
that sends tens of thousands of JSON
will significantly increase the time needed to process it,
when we don't have a multi-thread,
the single thread originally allocated for perhaps,
handling user input will be used to process all the data,
and don't even talk about the garbage collection along the way.
The unfortunate truth is that the size of the codebase
and the efficiency of the language server
is completely out of Emacs' control,
it is also out of both the lsp-mode and eglot's control.
If there's an LSP client
that can completely eliminate stuttering
and provide a seamless feedback,
that would be great, isn't it?
However, we're vaguely talking about speed right now,
what is considered fast?
What is considered seamless?
What we really mean when we say
the current LSP implementation is slow?
Let's first look at the problem fundamentally.
We interact with Emacs through a keyboard,
so what we perceive as a fast and smooth feedback
completely depends on how long it takes
for a keyboard input to display on the Emacs buffer.
From a pure graphical perspective,
we need a minimum of 24 frames per second,
the standard in the media industry,
for us humans to perceive something as seamless.
Say we need 25 frames per second, this means,
if we divide 1000 milliseconds by 25,
we only have approximately 40 millisecond window
for the response time to spare.
Even if we relax the constraint a bit more,
on average a typist takes about 100 to 200 milliseconds
between typing each character,
so as long as we see a response within this timeframe,
it is tolerable. However, using a slow language server
on a large codebase easily exceeds
the hundred millisecond mark,
and sometimes takes more than 200 milliseconds,
and inevitably will cause an inconsistent delay
for the end user.
At this point, someone might want to point out
that nobody is gonna type at the maximum pace all the time.
That's right, frankly speaking most of my time
spent at programming is not writing code,
but staring at the screen
thinking about how to write the code.
However, when we do actually type,
maybe only a sentence, a variable name, a keyword,
or just performing keybinding shortcuts,
that's when we want to see our input feedback immediately.
We've already spend so much time
thinking about how to write,
we don't want to waste any more time waiting for Emacs
to process and show us what we've written
half a second ago. Otherwise the frustration will build up.
In the past two years of EmacsConf, I've talked about
the Emacs Application Framework, a project that extended
Emacs Lisp to Python, Qt and JavaScript ecosystems.
The EAF project specializes in improving
the graphical and multimedia capabilities of Emacs
through other languages, it was a great success.
It demonstrated the endless possibilities of Emacs
by embracing the strengths in other ecosystems.
If anyone is interested for more information on EAF,
please see the EAF repo and refer to my talks
from EmacsConf2020 and 2021.
The EAF project was created by Manatee Lazycat as well,
so he thought if there is a way to design
an LSP client similar to EAF
that takes the advantage of Python's multi-threading,
it will be able to solve our problem.
Conveniently EAF had already done most of the ground work
and demonstrated the possibility
of cooperating Elisp and Python using the Emacs RPC effectively.
LSP Bridge has several goals in mind.
Firstly, performance is the number one priority.
Secondly, use Python multi-threading to bypass
the aforementioned bottlenecks of a single-threaded Emacs.
Thirdly, provide a simple solution that requires
minimal setup for someone who just wants to have
a fast autocomplete system in Emacs.
This means, LSP Bridge does not intend
and will not implement the entire LSP protocol,
which is a vastly different approach
than a solution like lsp-mode,
we do not want to compete this way.
We also believe some of the LSP Protocol features
are unnecessary, or we already have better solutions
in the Emacs ecosystem,
such as tree-sitter for syntax highlighting.
So we will not reinvent the wheel.
Ultimately, we want to provide the fastest, butter-smooth
and performant LSP client out of the box.
Design.
Now let's look at the design architecture diagram.
As you can see, it is split into
the top half and bottom half.
The top is the design for a single file model,
and the bottom half is for project model.
We make this distinction because we don't want a new user
to be troubled on choosing a project root directory
as the first impression to LSP
before even start writing code.
From a new user's perspective,
they've just installed this package,
and all they are expecting
is using a smart autocomplete system,
what does root directory even mean in this context?
So we make the decision for them
based on whether this file is part of a git repository.
Often times we write code in its own standalone file,
this is extremely common for scripting languages
like bash or python. So in the single file model,
LSP Bridge will start a dedicated LSP server
for this particular file based on file type,
and every file corresponds to a LSP server,
so each server doesn't interfere with one another.
The project model will have every file of the same type
under the same project share one server.
We believe this is a positive trade-off for user experience.
LSP Bridge internally implemented two main threads,
one is the Request Thread, the other is Response Thread.
The Request Thread is used to handle all the requests
coming from Emacs, it does not answer immediately,
this is important because Emacs doesn't need to wait
for any response under any reason,
even if the server is buggy or died out,
it shouldn't matter to the performance of Emacs.
The Response Thread is used to handle
the response coming from LSP servers.
After retrieving a response, regardless of the JSON size,
it sends to its own thread for computation,
such as candidate filtering and renaming.
Once the computation is finished,
it will determine if this information is expired,
if not, then push it to Emacs.
From the Emacs side, when it receives the LSP information,
it only needs to determine the course of action,
either popup completion, jump to definition,
renaming action, or show references and show documentions.
You see, from a user, all LSP Bridge doing
is these 5 things, the user doesn't need to care about
anything else like the complicated
Language Server Protocols.
Python side caches heavy data
such as candidate documentation and diagnostics.
We process as much server data as possible in Python,
and only pass to Emacs as little data as possible
so it doesn't clog the Emacs thread
and triggers garbage collection.
This design is critical, because all Emacs needs to do
is sending LSP requests to LSP Bridge,
it doesn't wait for a response,
it simply knows what to do *when* there is a response.
So the user's input immediately displays on the buffer
well within the 40 millisecond window,
and in the mean time, the user can continue to type
if he doesn't need the help from LSP right away,
it fundamentally resolves the stuttering problem.
Now I want to talk about acm-mode,
which stands for asynchronous completion menu,
it is a completion framework
that currently bundled with LSP Bridge
designed to accomodate for
the asynchronous nature of LSP servers.
It is a replacement for the built-in capf,
short for completion-at-point-functions,
used in almost everywhere
including company-mode and corfu-mode.
Yes, we unfortunately reinvented a very fundamental wheel.
No, it wasn't an easy decision.
However we still believe it's worth it.
LSP Bridge initially used company-mode,
then moved on to corfu-mode for a while,
but eventually Lazycat determined
that it is much more painful to write
a lot of workaround code to force LSP Bridge
to handle capf nicely than to just fork Corfu,
remove all the capf code,
and write a new completion framework from the remainings.
Performance wise, capf requires Emacs to store
the entire candidate list
when looking up candidate annotations.
It needs to search through the entire candidate list first,
then use the candidate as a key
to search for the actual information.
This entire process will be repeated every time
when drawing the completion menu.
This is truly intensive computing task for Emacs to handle.
On top of that, the existing capf frameworks assume
the candidate list, which is retrieved from the LSP server,
to be ready and finalized in place
when the completion popup occurred.
However given the design of LSP Bridge,
Emacs will not sit there and wait for the server response,
instead the Response Thread may feed Emacs data
whenever it's ready. This makes capf almost impossible
to form a finalized candidate list during popup.
The complete reasons regarding why capf is incompatible
with the asynchronous nature of LSP servers
are very complicated and deserves its own talk.
Lazycat wrote an entire blog post detailing his reasonings,
while Corfu's author Daniel Mendler a.k.a minad
also done his own investigations and experiments,
and reached a common conclusion.
For anyone interested, I've pasted the links
to the corresponding posts here.
Therefore, keep in mind that LSP Bridge
can only use acm-mode to work nicely,
so please disable other completion frameworks
like company and corfu before trying LSP Bridge.
By designing ACM with asynchronous server response in mind,
this unlocks LSP Bridge project's potential
to provide completions from almost any backends.
ACM has blended all the backends together,
and configured a priority to display
important completion results like LSP before other backends.
It can autocomplete LSP, TabNine, Elisp symbols, yasnippets,
even English dictionaries and much more.
As long as you have the backends installed,
they all work out-of-the-box!
Although LSP Bridge is a relatively new package
with just over 7 months old, it is already a success!
As of December of 2022, we have 67 contributors
making more than 1000 commits,
and we reached more than 600 stars on Github!
LSP Bridge is easily extensible,
developing a new language backend is very simple too,
feel free to join us!
LSP Bridge is another successful example
of extending Emacs Lisp with Python, and just like EAF,
it demonstrated the potential Emacs can achieve
when we jump out of the Lisp-only world
and embrace other ecosystems.
Recently Lazycat created a package called blink-search
that leveraged similar ideas
but an asynchronous search framework,
as well as a package called deno-bridge
that extended Emacs Lisp
with Deno JavaScript TypeScript runtimes.
Please check it out,
if consider joining the development too!
This is the entirety of my presentation, thanks for joining!
Me and Lazycat will be available
to answer questions on IRC and Etherpad.