/lsp-copilot

An LSP client for Emacs implemented in Rust.

Primary LanguageRustGNU General Public License v3.0GPL-3.0

LSP-COPILOT

Introducation

Lsp-Copilot is an LSP (Language Server Protocol) client for Emacs, implemented in Rust and inspired by lsp-bridge. It uses jsonrpc.el to facilitate communication between Emacs and the Lsp-Copilot Server. The Lsp-Copilot Server acts as an intermediary between Emacs and various language servers, handling communication with the language servers, processing the responses, and returning them to Emacs.

The features it supports are:

  • find definitions/references/implementatoins/type-definition/declaration (as a xref backend)
  • completion (as a capf function) support snippet and auto import, reuse requests that are already being processed, while caching the results to improve response speed, before returning all the completion candidates, the server will do fuzzy matching and filter out entries with no match.
  • diagnostics (as a flycheck backend default or flymake) process diagnostics when idle.
  • hover (triggered by lsp-copilot-describe-thing-at-point)
  • code action (triggered by lsp-copilot-execute-code-action)
  • rename (triggered by lsp-copilot-rename)
  • format buffer (triggered by lsp-copilot-format-buffer)
  • workspace command, such as typescript.restartTsServer(vtsls)reloadWorkspace(rust-analyzer) (triggered by lsp-copilot-execute-command)
  • signature/inlay hints (triggered by lsp-copilot-signature-modelsp-copilot-inlay-hints-mode), there might be bugs and usability issues, as it is used relatively infrequently.

images/show.gif

Installation

Before installing LSP-COPILOT, you should install rust and cargo first.

Manually

git clone https://github.com/jadestrong/lsp-copilot.git ./your-directory
cd ./your-directory
cargo build --release
# delete old file if exist
rm lsp-copilot
# cp ./target/release/lsp-copilot.exe ./
cp ./target/release/lsp-copilot ./

Doom Emacs

package.el

(package! lsp-copilot :recipe (:host github :repo "jadestrong/lsp-copilot"
                :files ("lsp-copilot.el" "lsp-copilot")
                :pre-build (("cargo" "build" "--release") ("cp" "./target/release/lsp-copilot" "./"))))

Download prebuilt binary

You can download the prebuilt binary from releases. For MacOS users, you should allow this binary to run first time, like this:

The application cannot be opened because it is from an unidentified developer. You can allow this app to run by going to System Settings > Privacy & Security and selecting ‘Allow Anyway’ for this app.

How to use

(use-package lsp-copilot
  ;; :load-path "/path/to/lsp-copilot"
  :config
  (add-hook! '(
               tsx-ts-mode-hook
               js-ts-mode-hook
               typescript-mode-hook
               typescript-ts-mode-hook
               rjsx-mode-hook
               less-css-mode-hook
               web-mode-hook
               python-ts-mode-hook
               rust-mode-hook
               rustic-mode-hook
               rust-ts-mode-hook
               toml-ts-mode-hook
               conf-toml-mode-hook
               bash-ts-mode-hook
               ) #'lsp-copilot-mode))
;; Doom Emacs
(set-lookup-handlers! 'lsp-copilot-mode
    :definition '(lsp-copilot-find-definition :async t)
    :references '(lsp-copilot-find-references :async t)
    :implementations '(lsp-copilot-find-implementations :async t)
    :type-definition '(lsp-copilot-find-type-definition :async t)
    :documentation '(lsp-copilot-describe-thing-at-point :async t))

Add a new language

Performed simple tests on Windows 11 and Arch Linux, it works properly. I have tested it on macOS and use it for daily development in JavaScript, Rust, etc. Therefore, tools like vtsls, typescript-language-server, eslint, tailwindcss, css, and others work fine

The configuration for a new language can refer to the Helix configuration. Supported fields are based on the built-in configuration file, and only LSP-related fields are supported. Open custom language config file by lsp-copilot-open-config-file and add your config, then execute lsp-copilot-restart.

The configuration fields for adding language support are: name、roots、language-id、file-types、language-servers . Other fields in the Helix configuration are not supported.

  • Vue2:
[languge-server.vls]
command = "vls"
args = ["--stdio"]

[[language]]
name = "vue"
roots = ["package.json"]
language-id = "vue"
file-types = ["vue"]
language-servers = ["vls"]
  • Vue3
yarn global add @vue/language-server @vue/typescript-plugin
[language-server.typescript-language-server]
config.plugins = [
  { name = "@vue/typescript-plugin", location = "${your-path}/node_modules/@vue/typescript-plugin", languages = ["vue"]}
]

[language-server.vue-language-server]
command = "vue-language-server"
args = ["--stdio"]
config.typescript = { tsdk = "${your-path}/node_modules/typescript/lib" }
config.vue = { hybridMode = false }

[[language]]
name = "vue"
roots = ["package.json"]
language-id = "vue"
file-types = ["vue", "ts"]
language-servers = ["vue-language-server", "typescript-language-server"]

# Override the build-in config. The built-in configuration uses vtsls, but it seems incompatible with vue-language-server. It could also be that my configuration is incorrect.
# Others, such as JavaScript and TSX, can be added as needed.
[[language]]
name = "typescript"
language-id = "typescript"
file-types = ["ts", "mts", "cts"]
roots = ["package.json"]
language-servers = [
  { name = "typescript-language-server", except-features = [
    "format",
  ] },
  { name = "eslint", support-workspace = true, config-files = [".eslintrc.js", ".eslintrc.cjs", ".eslintrc.yaml", ".eslintrc.yml", ".eslintrc", ".eslintrc.json"] },
]

Debug

Server bug

  • (setq lsp-copilot-log-level 3)
  • M-x lsp-copilot-restart
  • M-x lsp-copilot-open-log-file

Server crash

  • Open *lsp-copilot-events* buffer

Lsp server message

  • Open *lsp-copilot-log*

Commands

  • lsp-copilot-find-definition
  • lsp-copilot-find-references
  • lsp-copilot-find-declaration
  • lsp-copilot-find-type-definition
  • lsp-copilot-find-implementations
  • lsp-copilot-format-buffer
  • lsp-copilot-rename
  • lsp-copilot-execute-code-action
  • lsp-copilot-execute-command
  • lsp-copilot-describe-thing-at-point
  • lsp-copilot-show-project-diagnostics

  • lsp-copilot-open-log-file
  • lsp-copilot-open-config-file
  • lsp-copilot-restart: Restart the server
  • lsp-copilot-restart-workspace: Restart the LSP server for the current project

Customization

VariableDefaultDescription
lsp-copilot-user-languages-config`user-emacs-directory/lsp-copilot/languages.toml`Where custom language server configurations are stored
lsp-copilot-log-file-directorytemporary-file-directoryLog file storage directory
lsp-copilot-log-level1A number indicating the log level. Defaults to 1. Warn = 0, Info = 1, Debug = 2, Trace = 3

Recommend config

Company and Corfu

;; company
(setq company-idle-delay 0)
;; If you encounter issues when typing Vue directives (e.g., v-), you can try setting it to 1. I'm not sure if it's a problem with Volar.
(setq company-minimum-prefix-length 2)
(setq company-tooltip-idle-delay 0)

;; corfu
(setq corfu-auto-delay 0)
(setq corfu-popupinfo-delay '(0.1 . 0.1))

company-box

(defun company-box-icons--lsp-copilot (candidate)
    (-when-let* ((copilot-item (get-text-property 0 'lsp-copilot--item candidate))
                 (lsp-item (plist-get copilot-item :item))
                 (kind-num (plist-get lsp-item :kind)))
      (alist-get kind-num company-box-icons--lsp-alist)))

(setq company-box-icons-functions
      (cons #'company-box-icons--lsp-copilot company-box-icons-functions))

tabnine

Install tabnine package first, then add the following configuration to your config:

(when (fboundp #'tabnine-completion-at-point)
  (add-hook 'lsp-copilot-mode-hook
            (defun lsp-copilot-capf ()
              (remove-hook 'completion-at-point-functions #'lsp-copilot-completion-at-point t)
              (add-hook 'completion-at-point-functions
                        (cape-capf-super
                         #'lsp-copilot-completion-at-point
                         #'tabnine-completion-at-point) nil t))))

flycheck / flymake

Flycheck enabled default if flycheck-mode is installed. You can also select flymake by:

(setq lsp-copilot-diagnostics-provider :flymake)

Acknowledgements

Thanks to Helix, the architecture of Lsp-Copilot Server is entirely based on Helix’s implementation. Language configuration and communication with different language servers are all dependent on Helix. As a Rust beginner, I’ve gained a lot from this approach during the implementation.

Regarding the communication between Emacs and Lsp-Copilot, I would like to especially thank copilot.el and rust-analyzer. The usage of jsonrpc.el was learned from copilot.el, while the approach to receiving and handling Emacs requests was inspired by the implementation in rust-analyzer.

The various methods used to implement LSP-related functionality on the Emacs side were learned from lsp-mode and eglot. Without their guidance, many of these features would have been difficult to implement.

Regarding the communication data format between Emacs and Lsp-Copilot, I would like to especially thank emacs-lsp-booster. The project integrates the implementation of emacs-lsp-booster, which encodes the JSON data returned to Emacs, further reducing the load on Emacs.