Introduction 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:
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.
Complete using company-capf. [[file:images/completion.png]]
Use xref to find references. [[file:images/references.png]]
Display signature and hover at the same time. [[file:images/eldoc.png]]
CAUTION 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).
Dependencies Lspce depends on [[https://github.com/rejeep/f.el][f.el]], [[https://github.com/joaotavora/yasnippet][yasnippet]] and [[https://github.com/jrblevin/markdown-mode][markdown-mode]]. You should install them first.
Installation 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.
** Installing from the Git Repository
$ 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
** Installing using Straight
(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"))))))
Get started
(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)
))
)
)
** About lsp servers' stderr
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]".
(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 |** MAX_DIAGNOSTIC_COUNT 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.
** Logging 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.
** About python virtual environment *** pylsp and jedi-language-server 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")))))
)
#+END_SRC
And you can see how =lspce-jedi-environment= is used in =lspce-jedi-initializationOptions= and =lspce-pylsp-initializationOptions=.
*** pyright 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))))))
#+END_SRC
I personally do NOT recommend to use lspce with pyright though, and please do NOT create issues about it. Thanks.
** About environment variables 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.
Architecture [[file:images/Architecture_of_LSPCE.png]]
Some notes about the architecture:
Lspce vs Eglot/lsp-mode/lsp-bridge ** lspce vs eglot *** pros
*** cons
** lspce vs lsp-mode *** pros
*** cons
** lspce vs lsp-bridge *** pros
*** cons
License GPLv3
Acknowledgements Thanks to [[https://github.com/ubolonton/emacs-module-rs][emacs-module-rs]], which makes implementing LSPCE possible.
Thanks to [[https://github.com/joaotavora/eglot][eglot]] and [[https://github.com/emacs-lsp/lsp-mode][lsp-mode]], I learned a lot about LSP from both of them during developing this package.
Thanks to [[https://crates.io/crates/lsp-server][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).