LSPCE - LSP Client for Emacs, is a simple lsp client that is implemented as an Emacs module.
It does not want to be a full-featured lsp client. The features it supports are:
- find definitions/references/implementatoins (as a xref backend)
- completion (as a capf function) support snippet and auto import.
- diagnostics (as a flymake backend) process diagnostics when idle.
- hover (triggered by
lspce-help-at-point
) - signature/hover help (as an
eldoc-documentation-functions
function) - code action (triggered by
lspce-code-actions
) - rename (triggered by
lspce-rename
)
All features I need have been implemented now, and my next step is to make it more robust and more performant.
I have tested LSPCE with `pyright` and `rust-analyzer` in Emacs 28, and I’m using it to develop itself :).
Below are some images to illustrate what LSPCE looks like.
Display signature and hover at the same time.
I think that lspce is in good shape now, but bugs are expected.
I develop, test and run lspce with Emacs 28.2 on Linux (manjaro), and since I don’t have any Windows and MacOS computer, lspce may not work as expected. But @fast-90 tested it with pylsp on MacOS and found that it worked almost (except could not recognize virual env).
Lspce depends on f.el, yasnippet and markdown-mode. You should install them first.
At the moment, you can only install LSPCE by cloning this repo and compile rust code manually.
Before installing LSPCE, you should install rust and cargo first.
$ git clone https://github.com/zbelial/lspce.git ~/.emacs.d/site-lisp/lspce
$ cd ~/.emacs.d/site-lisp/lspce
$ cargo build
# or, to build a release version
$ cargo build --release
# then you should rename the .so file (and copy it to another directory )
$ mv target/debug/liblspce_module.so lspce-module.so
# or alternatively, if .d and .dylib files are created for you: (thanks @fast-90 for this)
$ mv target/debug/liblspce_module.d lspce-module.d
$ mv target/debug/liblspce_module.dylib lspce-module.dylib
(straight-use-package
`(lspce :type git :host github :repo "zbelial/lspce"
:files (:defaults ,(pcase system-type
('gnu/linux "lspce-module.so")
('darwin "lspce-module.dylib")))
:pre-build ,(pcase system-type
('gnu/linux '(("cargo" "build" "--release") ("cp" "./target/release/liblspce_module.so" "./lspce-module.so")))
('darwin '(("cargo" "build" "--release") ("cp" "./target/release/liblspce_module.dylib" "./lspce-module.dylib"))))))
(use-package lspce
:load-path "/path/to/lspce"
:config (progn
(setq lspce-send-changes-idle-time 0.1)
(setq lspce-show-log-level-in-modeline t) ;; show log level in mode line
;; You should call this first if you want lspce to write logs
(lspce-set-log-file "/tmp/lspce.log")
;; By default, lspce will not write log out to anywhere.
;; To enable logging, you can add the following line
;; (lspce-enable-logging)
;; You can enable/disable logging on the fly by calling `lspce-enable-logging' or `lspce-disable-logging'.
;; enable lspce in particular buffers
;; (add-hook 'rust-mode-hook 'lspce-mode)
;; modify `lspce-server-programs' to add or change a lsp server, see document
;; of `lspce-lsp-type-function' to understand how to get buffer's lsp type.
;; Bellow is what I use
(setq lspce-server-programs `(("rust" "rust-analyzer" "" lspce-ra-initializationOptions)
("python" "pylsp" "" )
("C" "clangd" "--all-scopes-completion --clang-tidy --enable-config --header-insertion-decorators=0")
("java" "java" lspce-jdtls-cmd-args lspce-jdtls-initializationOptions)
))
)
)
Lspce support writing stderr message from lsp servers to the log file (set up by calling `lspce-set-log-file`) in the log level ERROR, and the log contains string “[stderr]”.
Variable | Default | Description |
---|---|---|
lspce-send-changes-idle-time | 0.5 | How much idle time to wait before sending changes to the lsp server. |
lspce-doc-tooltip-border-width | 1 | The border width of lspce tooltip. |
lspce-doc-tooltip-timeout | 30 | How long to wait before lspce tooltip disappears (only for posframe) |
lspce-completion-ignore-case | t | If non-nil, ignore case when completing. |
lspce-completion-no-annotation | nil | If non-nil, do not display completion item’s annotation. |
lspce-enable-eldoc | t | If non-nil, enable eldoc. |
lspce-eldoc-enable-hover | t | If non-nil, enable hover in eldoc. |
lspce-eldoc-enable-signature | t | If non-nil, enable signature in eldoc. |
lspce-enable-flymake | t | If non-nil, enable flymake. |
lspce-connect-server-timeout | 60 | The timeout of connecting to lsp server, in seconds. |
lspce-modes-enable-single-file-root | `(python-mode) | Major modes where lspce enables even for a single file (IOW no project). |
lspce-enable-logging | nil | If non-nil, enable logging to file. |
lspce-auto-enable-within-project | t | If non-nil, enable lspce when a file is opened if lspce has already been enabled in current project |
lspce-after-text-edit-hook | nil | Functions called after finishing all text edits in a buffer. |
lspce-afte-each-text-edit-hook | nil | Functions called after finishing each text edit in a buffer. |
lspce-show-log-level-in-modeline | t | If non-nil, show log level in modeline. |
lspce-inherit-exec-path | nil | If non-nil, pass `exec-path’ as PATH to rust code to create the lsp subprocess. |
MAX_DIAGNOSTIC_COUNT | 30 | How many diagnostics should be retrieved. |
lspce-xref-append-implementations-to-definitions | nil | If non-nil, also fetch implementations when fetching definitions. |
lspce-envs-pass-to-subprocess | nil | Environment variables that should be passed to rust code to create the lsp subprocess. |
lspce-jdtls-completion-max-results | 30 | how many completion items jdtls can return at the most |
This variable is defined in rust code, you can customize it via lspce-change-max-diagnostics-count
. If it’s negative, then all diagnostics will be retrieved.
On rust code side, there are 5 log level: DISABLED(0), ERROR(1), INFO(2), TRACE(3), DEBUG(4), and the default level is INFO. You can use functions lspce-set-log-level-xxxx
to change log level to specific level.
Before adding log level feature, two commands, lspce-enable-logging
and lspce-disable-logging
, already exist, which enable/disable logging entirely. Now lspce-enable-logging
is equivalent of setting log level to DEBUG, and lspce-disable-logging
is equivalent of setting log level to DISABLED.
ATM, you can use virtual environments with both pylsp and jedi-language-server in lspce, all you need is a variable lspce-jedi-environment
, which can be specified to point to the python interpreter, and a .dir-locals.el
file.
The following is a simple .dir-locals.el:
((python-mode
. ((eval . (progn
(setq-local lspce-jedi-environment "/home/XXXX/tmp/venv/.venv/bin/python")))))
(python-ts-mode
. ((eval . (progn
(setq-local lspce-jedi-environment "/home/XXXX/tmp/venv/.venv/bin/python")))))
)
And you can see how lspce-jedi-environment
is used in lspce-jedi-initializationOptions
and lspce-pylsp-initializationOptions
.
If you want to use pyright with lspce, .dir-locals.el
can also help you. But I haven’t tested it throughly.
((python-mode
. ((eval . (progn
(setq-local exec-path (append (list "/home/XXXX/tmp/venv/.venv/bin/") exec-path))
(setq-local lspce-inherit-exec-path t)))))
(python-ts-mode
. ((eval . (progn
(setq-local exec-path (append (list "/home/XXXX/tmp/venv/.venv/bin/") exec-path))
(setq-local lspce-inherit-exec-path t))))))
I personally do NOT recommend to use lspce with pyright though, and please do NOT create issues about it. Thanks.
Sometime, you may need to pass some environment variables to rust code so that lspce can use these environment variables to start lsp server processes. You can use lspce-envs-pass-to-subprocess
then, which is a list of environment variables, and the values of these environment variables are retrieved from process-environment
, except PATH
, which is from exec-path
.
The reason why this feature works in this way is I don’t want lspce to be tightly bound with other tools, such as direnv, python’s venv, and so on. So you should use someway to populate process-environment
first if you want to use this feature.
lspce-envs-pass-to-subprocess
has made lspce-inherit-exec-path
obsolete, even you can still use it. The logic is: all environment variables will be passed, and if lspce-inherit-exec-path
is set true, PATH will be passed too.
Some notes about the architecture:
- Every project is represented by a
Project
struct in LSPCE (aka the largest Box in the above image). - LSPCE sends requests/notifications to LSP server(rust-analyzer, pyright, etc.) processes via a
Transport
. - Responses/notifications/requests issued by LSP servers are sent to
Transport
and then dispatched into three different queues byMessage Dispatcher
. Note that diagnositcs are disptched into a separate queue, from where LSPCE reads them and shows them using flymake. - After sending a request, LSPCE will read the response from the response queue in an interruptable way, so it won’t block Emacs.
- lspce has less chance to block user input or freeze Emacs
- lspce supports multiple servers in a single project For projects containing, for example python files and rust files, lspce can start a pylsp server and a rust-analyzer server respectively. IIRC, eglot does not support this. Corret me if I’m wrong.
- lspce’s code is easier to understand for elisp newbies like me. Lspce does not use cl-loop, pcase-xxx etc, which I think is not easy to understand.
- lspce supports a smaller set of LSP features
- lspce has only been tested with several lsp servers Eglot supports much more lsp servers than lspce
- lower code quality I’ve heard several times that eglot has high code quality.
- lspce does not support tramp
- lspce has less chance to block user input or freeze Emacs
- lspce’s code is easier to understand for elisp newbies like me. I tried to understand lsp-mode’s code but I failed. I really think lsp-mode’s code is hard to understand, but I’m absolutely not an elisp export at all.
- lspce supports a much smaller set of LSP features
- lspce has only been tested with several lsp servers lsp-mode supports much more lsp servers than lspce
- lspce does not support running multiple servers in a single buffer
- lspce does not support dap
- lspce is compatible with xre/capf/imenu etc.
- lspce support a smaller set of LSP features
- In theory, lspce is slower than lsp-bridge
- lspce does not support running multiple servers in a single buffer
- lsp-bridge has built-in remote editing support
- lsp-bridge provides other completing backends
GPLv3
Thanks to emacs-module-rs, which makes implementing LSPCE possible.
Thanks to eglot and lsp-mode, I learned a lot about LSP from both of them during developing this package.
Thanks to lsp-server from rust-analyzer, I used a lot of code from it (and modified them a little to make it suitable for a client).