Skip to content
/ lspce Public

LSP Client for Emacs implemented as a module using rust.

License

Notifications You must be signed in to change notification settings

zbelial/lspce

Repository files navigation

LSPCE

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. images/completion.png

Use xref to find references. images/references.png

Display signature and hover at the same time. 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 f.el, yasnippet and 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]”.

Customization

VariableDefaultDescription
lspce-send-changes-idle-time0.5How much idle time to wait before sending changes to the lsp server.
lspce-doc-tooltip-border-width1The border width of lspce tooltip.
lspce-doc-tooltip-timeout30How long to wait before lspce tooltip disappears (only for posframe)
lspce-completion-ignore-casetIf non-nil, ignore case when completing.
lspce-completion-no-annotationnilIf non-nil, do not display completion item’s annotation.
lspce-enable-eldoctIf non-nil, enable eldoc.
lspce-eldoc-enable-hovertIf non-nil, enable hover in eldoc.
lspce-eldoc-enable-signaturetIf non-nil, enable signature in eldoc.
lspce-enable-flymaketIf non-nil, enable flymake.
lspce-connect-server-timeout60The 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-loggingnilIf non-nil, enable logging to file.
lspce-auto-enable-within-projecttIf non-nil, enable lspce when a file is opened if lspce has already been enabled in current project
lspce-after-text-edit-hooknilFunctions called after finishing all text edits in a buffer.
lspce-afte-each-text-edit-hooknilFunctions called after finishing each text edit in a buffer.
lspce-show-log-level-in-modelinetIf non-nil, show log level in modeline.
lspce-inherit-exec-pathnilIf non-nil, pass `exec-path’ as PATH to rust code to create the lsp subprocess.
MAX_DIAGNOSTIC_COUNT30How many diagnostics should be retrieved.
lspce-xref-append-implementations-to-definitionsnilIf non-nil, also fetch implementations when fetching definitions.
lspce-envs-pass-to-subprocessnilEnvironment variables that should be passed to rust code to create the lsp subprocess.
lspce-jdtls-completion-max-results30how 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")))))
 )

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

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

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

About

LSP Client for Emacs implemented as a module using rust.

Resources

License

Stars

Watchers

Forks

Packages

No packages published