My Experience with lsp-bridge


Coding
Nov 23, 2025

The experience of using lsp-mode or elgot depends heavily on the lsp server you are using and the project you are working on. With the newly added JSON parser in Emacs 30 and the garbage collecting improvements in the igc branch, the experience of lsp-mode or eglot can be more than good enough. Still, if you are using the more demanding lsp server (like some language servers for frontend development) or working on a larger-size project, the experience could still be unsatisfying or even super frustrating. If you have ever experienced Emacs freezing after checkout to another git branch or big gc lag when using gcmh-mode, you could still consider giving lsp-bridge a try.

In exchange for speed and fluency, lsp-bridge replaces every builtin and well known tools that aren't suitable for async operations, including but not limited to completion-at-point, xref, eldoc. This prevents a lot of users from ever trying it, and that's reasonable. However, the async nature of lsp-bridge also brings its own advantages.

For a small example, consider that even the go-to-definition operation in lsp-bridge is asynchronous. With other lsp clients, Emacs is stuck between the time you call xref-find-definitions and the lsp server returns a response. With lsp-bridge, even if you are waiting for a response, Emacs is free to do other things in the meantime, such as doing a garbage collection, or executing a callback registered by run-with-idle-timer.

Finally, you certainly can use lsp-bridge only for some modes and keep using completion-at-point for other modes. That's also what I'm doing.

Basic Setup

To use lsp-bridge, you first need to install some external dependencies.

pip3 install epc orjson sexpdata six setuptools paramiko rapidfuzz watchdog packaging

If you are using Nix, you need to add a python package with dependencies to your Nix config files.

(python3.withPackages (
      ps: with ps; [
        # ...
        epc
        orjson
        packaging
        paramiko
        rapidfuzz
        setuptools
        sexpdata
        six
        watchdog
      ]
    ))

With external dependencies installed, now we can install the lsp-bridge package. Here's how I have done it with straight and use-package.

(use-package lsp-bridge
  :hook
  (clojure-mode . lsp-bridge-mode)
  (clojure-ts-mode . lsp-bridge-mode)
  (typescript-ts-mode . lsp-bridge-mode)
  (js-ts-mode . lsp-bridge-mode)
  (java-ts-mode . lsp-bridge-mode)
  :straight '(lsp-bridge :type git :host github :repo "manateelazycat/lsp-bridge"
                         :files (:defaults "*.el" "*.py" "acm" "core" "langserver" "multiserver" "resources")
                         :build (:not compile))

  :init
  (defun me/lsp-bridge-corfu-hook ()
    (when (and lsp-bridge-mode
               (boundp 'corfu-mode)
               corfu-mode)
      (corfu-mode -1)))
  (add-hook 'corfu-mode-hook #'me/lsp-bridge-corfu-hook))

Here I only enabled lsp-bridge-mode for modes in the :hook part, and I disabled corfu in lsp-bridge-mode-hook (I'm still using it for other programming or writing tasks).

Unlike lsp-mode, lsp-bridge doesn't provide a lsp server installation function. Check Supported language servers and their corresponding pages for installation methods.

If the server is installed and lsp-bridge-mode is enabled, it should just work without any other configurations. If it doesn't, set lsp-bridge-enable-debug to true and check *lsp-bridge* for error messages.

Configuration

Keymapping

lsp-bridge provides its xef and eldoc equivalents, but it's up to us to mapping them. Here is what I do just for a reference.

(general-evil-define-key '(normal) '(lsp-bridge-mode-map)
  "s S" #'lsp-bridge-workspace-list-symbols
  "s D" #'lsp-bridge-diagnostic-list
  "s R" #'lsp-bridge-rename
  "g d" #'lsp-bridge-find-def
  "g r" #'lsp-bridge-find-references
  "g D" #'lsp-bridge-find-impl
  "C-RET" #'lsp-bridge-code-action
  "C-<return>" #'lsp-bridge-code-action)

a Little Hack (minimum popup width)

In corfu you can set minimum popup width with corfu-min-width. lsp-bridge~/~acm currently doesn't provide a way to do so. However it is relatively easy to hack one.

(setq me/acm-minimum-width 1000)

(defun me/acm-minimum-width-function (oldfun &rest r)
  (let ((result (apply oldfun r)))
    (max me/acm-minimum-width result)))

(advice-add 'acm-menu-max-length :around #'me/acm-minimum-width-function)

flymake

There is actually a third-party lsp-bridge integration for flymake. Check eki3z/flymake-bridge.

Workspace Management

lsp-bridge's workspace detection depends entirely on git. It simply use the nearest directory contains .git as the workspace root. This may not be enough for some use cases. However, it provides a lsp-bridge-get-project-path-by-filepath for us to customize. We can implement something similar to what lsp-mode provides fairly easily.

(require 'f)

;; assume savehist-mode is enabled
(defvar me/lsp-bridge-workspaces (list))
(add-to-list 'savehist-additional-variables 'me/lsp-bridge-workspaces)

(defun me/lsp-bridge-workspace-add (project-root)
  (interactive
   (list (read-directory-name "Select folder:")))
  (add-to-list 'me/lsp-bridge-workspaces project-root))

(defun me/lsp-bridge-workspace-remove (project-root)
  (interactive (list (completing-read "Select folder to remove: "
                                      me/lsp-bridge-workspaces)))
  (setq me/lsp-bridge-workspaces (cl-remove project-root me/lsp-bridge-workspaces :test #'equal)))

(defun me/lsp-bridge-get-workspace (path)
  (cl-first (cl-mapcar #'f-expand
                       (cl-remove-if-not (lambda (workspace)
                                           (if (f-ancestor-of? (f-expand workspace) path)
                                                   workspace                                  
                                             nil))
                                         me/lsp-bridge-workspaces))))

(setq lsp-bridge-get-project-path-by-filepath #'me/lsp-bridge-get-workspace)

Working with Java

We need a few more lines of configuration to make it works with Java.

(use-package lsp-bridge
  ;; ...
  :init
  (require 'lsp-bridge-jdtls)
  (setq lsp-bridge-enable-auto-import t)
  ;; ... other configurations
  )

Working with Clojure and Cider

When writing Clojure, I would like to use cider for completion and let lsp handling everything else. With lsp-mode we can just locally set lsp-enable-completion-at-point to false. With lsp-bridge, well, we can disable lsp-bridge's automatic popup and continue to use corfu.

(use-package lsp-bridge
  ;; ...
  ;; add a new variable
  (defvar me/force-corfu nil)

  ;; change the previous hook to this
  (defun me/lsp-bridge-corfu-hook ()
    (when (and lsp-bridge-mode
               (boundp 'corfu-mode)
               corfu-mode
               (not me/force-corfu))
      (corfu-mode -1))))

(use-package cider
  :after lsp-bridge
  :config
  (defun me/lsp-bridge-cider-hook ()
    (when cider-mode
      (setq-local me/force-corfu t)
      (setq-local lsp-bridge-complete-manually t)
      (corfu-mode 1)))
  (add-hook 'cider-mode-hook #'me/lsp-bridge-cider-hook 80))

Conclusion

Overall I would say I'm satisfied with what lsp-bridge provides and amazed at its fluency. I can now load and browse through graaljs's source code with no stutter, no random freeze I need to C-g, that may or may not work, no nothing. The strategy of off-loading computation to an external process and async everything seems to put very light weight on the Emacs side and work really well.