Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/zbelial/lspce

LSP Client for Emacs implemented as a module using rust.
https://github.com/zbelial/lspce

Last synced: 4 months ago
JSON representation

LSP Client for Emacs implemented as a module using rust.

Awesome Lists containing this project

README

        

# -*- eval: (org-appear-mode -1); -*-
#+TITLE: LSPCE
#+AUTHOR: zbelial
#+EMAIL: [email protected]
#+DATE: 2022
#+LANGUAGE: en

* 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:
- 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.

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
#+BEGIN_SRC bash
$ 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
#+END_SRC

** Installing using Straight
#+BEGIN_SRC elisp
(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"))))))
#+END_SRC

* Get started
#+BEGIN_SRC elisp
(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)
))
)
)
#+END_SRC

** 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]".

* Customization
| 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 |

** 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:
#+BEGIN_SRC elisp
((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.
#+BEGIN_SRC elisp
((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:
1. Every project is represented by a ~Project~ struct in LSPCE (aka the largest Box in the above image).
2. LSPCE sends requests/notifications to LSP server(rust-analyzer, pyright, etc.) processes via a ~Transport~.
3. Responses/notifications/requests issued by LSP servers are sent to ~Transport~ and then dispatched into three different queues by ~Message Dispatcher~.
Note that diagnositcs are disptched into a separate queue, from where LSPCE reads them and shows them using flymake.
4. After sending a request, LSPCE will read the response from the response queue in an interruptable way, so it won't block Emacs.

* Lspce vs Eglot/lsp-mode/lsp-bridge
** lspce vs eglot
*** pros
- 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.

*** cons
- 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 vs lsp-mode
*** pros
- 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.

*** cons
- 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 vs lsp-bridge
*** pros
- lspce is compatible with xre/capf/imenu etc.

*** cons
- 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

* 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).