Skip to content

jadestrong/lsp-proxy

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

340 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

LSP-PROXY (formerly LSP-COPILOT)

Introduction

Lsp-Proxy 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-Proxy Server. The Lsp-Proxy 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-proxy-describe-thing-at-point)
  • code action (triggered by lsp-proxy-execute-code-action)
  • rename (triggered by lsp-proxy-rename)
  • format buffer (triggered by lsp-proxy-format-buffer)
  • workspace command, such as typescript.restartTsServer(vtsls)reloadWorkspace(rust-analyzer) (triggered by lsp-proxy-execute-command)
  • inlay hints (triggered by lsp-proxy-inlay-hints-mode)
  • documentHighlight/signature (baesd on eldoc)
  • documentSymbols (triggered by imenu)
  • GitHub Copilot integration with authentication management and AI-powered code suggestions

Demo

Prerequisites

Emacs30.1 or 29 + eglot@latest (Reused some capabilities of eglot to reduce code duplication.)

Installation

Via npm (Recommended)

LSP-PROXY is now available via npm with prebuilt binaries for all major platforms:

npm install -g emacs-lsp-proxy

This will automatically install the appropriate binary for your platform (Linux x64/ARM64, macOS x64/ARM64, Windows x64) and make the emacs-lsp-proxy command available in your PATH.

From Emacs (M-x lsp-proxy-install-server)

If you don't want to use npm, cargo, or download the binary by hand, install it directly from Emacs:

M-x lsp-proxy-install-server

This downloads the prebuilt binary for your platform from GitHub Releases and installs it into lsp-proxy-install-dir (default ${user-emacs-directory}/lsp-proxy/, next to your languages.toml). That directory is part of the executable search path, so the binary is picked up automatically — no extra configuration needed. Run the command again (or with a prefix argument, C-u M-x lsp-proxy-install-server) to upgrade or reinstall.

By default the latest release is installed; pin a specific version with:

(setq lsp-proxy-server-version "0.8.1")  ; with or without a leading "v"

Notes:

  • macOS / Linux extraction uses the system tar. On Windows the release asset is a .7z, so 7z (p7zip / 7-Zip) must be available in PATH; otherwise use the npm method.
  • The download is fetched over HTTPS via Emacs's built-in url library — no external curl required.

Nix Flake

If you use Nix, you can build an optimized binary directly from the repository:

# Build and run directly
nix run github:jadestrong/lsp-proxy

# Build and install to your profile
nix profile install github:jadestrong/lsp-proxy

# Or build locally if you've cloned the repo
nix build
./result/bin/emacs-lsp-proxy --version

You can add it as a flake input in your NixOS/home-manager configuration:

# flake.nix
{
  inputs.lsp-proxy.url = "github:jadestrong/lsp-proxy";

  outputs = { self, nixpkgs, lsp-proxy, ... }: {
    # Then use lsp-proxy.packages.${system}.default wherever you need it
  };
}

Manually

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

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

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-proxy
  ;; :load-path "/path/to/lsp-proxy"
  :config
  ;; Optional: specify custom server path (if not using default auto-detection)
  ;; (setq lsp-proxy-server-path "/custom/path/to/emacs-lsp-proxy")
  
  (add-hook 'tsx-ts-mode-hook #'lsp-proxy-mode)
  (add-hook 'js-ts-mode-hook #'lsp-proxy-mode)
  (add-hook 'typescript-mode-hook #'lsp-proxy-mode)
  (add-hook 'typescript-ts-mode-hook #'lsp-proxy-mode))

DOOM Emacs

  • Recommend
(package! lsp-proxy :recipe (:host github :repo "jadestrong/lsp-proxy"
                :files ("*.el")))
(use-package! lsp-proxy
  :config
  (set-lookup-handlers! 'lsp-proxy-mode
    :definition '(lsp-proxy-find-definition :async t)
    :references '(lsp-proxy-find-references :async t)
    :implementations '(lsp-proxy-find-implementations :async t)
    :type-definition '(lsp-proxy-find-type-definition :async t)
    :documentation '(lsp-proxy-describe-thing-at-point :async t)))

Language Configuration

LSP-Proxy uses a TOML-based configuration system compatible with Helix editor. The configuration consists of two main sections: language servers and language definitions.

Configuration File

Open your user configuration:

M-x lsp-proxy-open-config-file

This opens ${user-emacs-directory}/lsp-proxy/languages.toml, which merges with built-in defaults (3 levels deep).

Language Server Configuration

Define language servers in the [language-server.<name>] section:

[language-server.mylang-lsp]
command = "mylang-lsp"
args = ["--stdio"]
timeout = 20
environment = { "RUST_BACKTRACE" = "1" }

Available options:

Key Description
command Language server binary name or path (required)
args Arguments passed to the language server
timeout Request timeout in seconds (default: 20)
environment Environment variables as key-value pairs
config LSP initializationOptions and workspace/configuration
experimental Experimental client capabilities (e.g., rust-analyzer)

Configuration syntax:

Use dot notation or TOML tables for nested config:

# Dot notation
[language-server.mylang-lsp]
config.provideFormatter = true
config.lint.enable = true

# Table notation (equivalent)
[language-server.mylang-lsp.config]
provideFormatter = true

[language-server.mylang-lsp.config.lint]
enable = true

Language Configuration

Define languages in [[language]] array sections:

[[language]]
name = "rust"
language-id = "rust"
file-types = ["rs"]
roots = ["Cargo.toml", "Cargo.lock"]
language-servers = ["rust-analyzer"]

Available options:

Key Description
name Unique language identifier (required)
language-id LSP language identifier (required)
file-types File extensions or glob patterns (required)
roots Project root markers for workspace detection
language-servers Associated language servers (required)

File type patterns:

file-types = [
  "js",                    # Extension
  { glob = ".prettierrc" } # Glob pattern
]

Language ID mapping:

The language-id must match LSP specifications:

  • ["js", "mjs", "cjs"]javascript
  • ["jsx"]javascriptreact
  • ["ts"]typescript
  • ["tsx"]typescriptreact

Multiple Language Servers

Configure multiple servers per language:

[[language]]
name = "typescript"
language-servers = [
  { name = "vtsls", except-features = ["format"] },
  { name = "eslint", support-workspace = true }
]

Server-specific options:

Key Description
name Language server name (required)
except-features Disable specific features (blacklist)
only-features Enable only specific features (whitelist)
support-workspace Workspace detection strategy (see below)
library-directories External library paths for navigation
config-files Activate only if config file exists

Workspace Detection Strategies

The support-workspace option controls how LSP-Proxy detects workspace roots and manages server instances:

Traditional Projects (Default)

# Default behavior - no support-workspace specified
{ name = "rust-analyzer" }
# Equivalent to: support-workspace = false
  • Detection: Finds the outermost root marker (e.g., Cargo.toml) within git repository
  • Server Management: One server instance per unique workspace root
  • Best for: Traditional projects, rust-analyzer, gopls, etc.

Cross-Workspace Sharing

{ name = "eslint", support-workspace = true }
  • Detection: Finds the closest root marker (e.g., package.json)
  • Server Management: Can reuse server across different workspace roots
  • Best for: Tools that work across multiple projects like eslint, prettier

Monorepo Module Detection

{ name = "vtsls", support-workspace = ["package.json"] }
  • Detection: First tries to find closest specified markers (package.json), falls back to outermost root markers if not found
  • Server Management: One server per unique workspace root
  • Best for: Monorepos where each module needs its own server instance

Example Configurations

Rust Project:

[[language]]
name = "rust"
roots = ["Cargo.toml", "Cargo.lock"]
language-servers = [
  { name = "rust-analyzer" }  # Uses outermost strategy - entire Cargo workspace
]

JavaScript Monorepo:

[[language]]
name = "javascript"
roots = ["package.json"]
language-servers = [
  { name = "vtsls", support-workspace = ["package.json"] },  # Each package.json gets own server
  { name = "eslint", support-workspace = true }              # Shared across packages
]

Project Structure Example:

monorepo/                    # git repository root
├── .git/
├── package.json            # workspace root marker
├── packages/
│   ├── frontend/
│   │   ├── package.json    # module root marker
│   │   └── src/index.ts    # editing this file
│   └── backend/
│       ├── package.json    # another module
│       └── src/index.ts

With the configuration above:

  • vtsls: workspace = monorepo/packages/frontend/ (closest package.json)
  • eslint: can share server instance across both frontend and backend packages

Supported features:

Navigation: goto-declaration, goto-definition, goto-type-definition, goto-reference, goto-implementation

Code Intelligence: completion, inline-completion, completion-resolve, signature-help, hover, document-highlight, inlay-hints

Code Quality: diagnostics, pull-diagnostics, code-action, rename-symbol, format

Workspace: document-symbols, workspace-symbols, workspace-command

Library directories:

For external dependencies outside project roots:

[[language]]
name = "rust"
language-servers = [
  { name = "rust-analyzer", library-directories = [
    "~/.cargo/registry/src",
    "~/.rustup/toolchains"
  ]}
]

Conditional activation:

Only activate if config file exists:

[[language]]
name = "javascript"
language-servers = [
  { name = "eslint", config-files = [
    ".eslintrc.json",
    "eslint.config.js"
  ]}
]

Complete Example

[language-server.gopls]
command = "gopls"

[language-server.gopls.config]
gofumpt = true

[language-server.gopls.config.hints]
assignVariableTypes = true
parameterNames = true

[[language]]
name = "go"
language-id = "go"
file-types = ["go"]
roots = ["go.mod", "go.work"]
language-servers = ["gopls"]

Built-in Language Servers

LSP-Proxy includes default configurations for:

  • JavaScript/TypeScript: vtsls, typescript-language-server, eslint
  • Web: vscode-html-language-server, vscode-css-language-server, tailwindcss-language-server
  • Rust: rust-analyzer
  • Python: basedpyright
  • Go: gopls
  • C/C++: clangd
  • Ruby: solargraph
  • Lua: lua-language-server
  • Java: jdtls
  • Dart: dart
  • Bash: bash-language-server
  • JSON: vscode-json-language-server
  • TOML: taplo

See the built-in languages.toml for complete configurations.

Troubleshooting

Issue Solution
Server not starting Verify command is in PATH or use absolute path
No completions Check language-id matches server expectations
Project not detected Add appropriate files to roots array
Features not working Check server capabilities and except-features
Debug issues Set (setq lsp-proxy-log-level 3) and run M-x lsp-proxy-open-log-file

After configuration changes, restart:

M-x lsp-proxy-restart

Example

  • 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 typescript
# typescript-language-server
[language-server.typescript-language-server]
config.plugins = [
  { name = "@vue/typescript-plugin", location = "${YOUR-PATH}/node_modules/@vue/typescript-plugin", languages = ["vue"], enableForWorkspaceTypeScriptVersions = true, configNamespace = "typescript" }
]

# or vtsls
[language-server.vtsls.config.vtsls.tsserver]
globalPlugins = [
  { name = "@vue/typescript-plugin", location = "${YOUR-PATH}/node_modules/@vue/typescript-plugin", languages = ["vue"], enableForWorkspaceTypeScriptVersions = true, configNamespace = "typescript" }
]

[language-server.vue-language-server]
command = "vue-language-server"
args = ["--stdio"]

[[language]]
name = "vue"
roots = ["package.json"]
language-id = "vue"
file-types = ["vue"]
language-servers = [
  { name = "vue-language-server", except-features = ["goto-definition", "goto-implementation", "goto-type-definition", "goto-declaration", "goto-reference"] },
  "vtsls"
]
# or
# language-servers = [
#   { name = "vue-language-server", except-features = ["goto-definition", "goto-implementation", "goto-type-definition", "goto-declaration", "goto-reference"] },
#   "typescript-language-server"
# ]

Debug

Server bug

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

Server crash

  • Open *lsp-proxy-events* buffer

Lsp server message

  • Open *lsp-proxy-log*

Commands

Navigation & Code Intelligence

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

GitHub Copilot Integration

  • lsp-proxy-copilot-sign-in: Sign in to GitHub Copilot
  • lsp-proxy-copilot-sign-out: Sign out from GitHub Copilot
  • lsp-proxy-copilot-status: Check current Copilot authentication status

Server Management

  • lsp-proxy-install-server: Download and install the prebuilt server binary from GitHub Releases
  • lsp-proxy-open-log-file: Open the server log file
  • lsp-proxy-open-config-file: Open the language configuration file
  • lsp-proxy-remote-open-config-file: Open the languages.toml config on a remote host
  • lsp-proxy-restart: Restart the server
  • lsp-proxy-workspace-restart: Restart the LSP server for the current project

Customization

Below is a complete list of user-facing customization variables (defcustom) provided by the Emacs side of lsp-proxy. You can inspect or change them via M-x customize-group RET lsp-proxy RET, or set them in your init file with setq / setq-default.

Core & Logging

Variable Default Description
lsp-proxy-log-file-directory temporary-file-directory Directory where the external server writes its log file. Set to a persistent path if you want logs across restarts.
lsp-proxy-user-languages-config ${user-emacs-directory}/lsp-proxy/languages.toml User TOML config overriding/augmenting built-in language server definitions. Edited via M-x lsp-proxy-open-config-file.
lsp-proxy-server-path nil Path to the lsp-proxy server executable. If specified, this path will be used instead of auto-detection. If nil, lsp-proxy will automatically search for the executable in: 1) System PATH 2) lsp-proxy-install-dir 3) Current directory 4) target/release directory.
lsp-proxy-install-dir ${user-emacs-directory}/lsp-proxy/ Directory where M-x lsp-proxy-install-server installs the managed binary. Also searched by the executable auto-detection above.
lsp-proxy-server-version nil Release version installed by M-x lsp-proxy-install-server (e.g. "0.8.1"). nil installs the latest release.
lsp-proxy-log-max 0 Max size (lines/events) of internal events buffer; 0 disables; nil infinite. Enable only while debugging.
lsp-proxy-log-level 0 Verbosity: 0 none, 1 basic, 2 verbose. Increase for more diagnostic output (may impact performance).
lsp-proxy-log-buffer-max message-log-max Controls Emacs-side lsp-proxy-log buffer retention. nil disables logging, integer truncates, t unlimited.

Change / Idle Handling

Variable Default Description
lsp-proxy--send-changes-idle-time 0 Seconds Emacs must be idle before sending buffered didChange events. Raise to reduce traffic in huge files.
lsp-proxy-idle-delay 0.500 Debounce interval for batching after-change hooks before running idle tasks.
lsp-proxy-on-idle-hook nil Hook list run after idle delay (e.g., refresh diagnostics/Xref). Add buffer‑local functions as needed.
lsp-proxy-enable-bytecode t Use bytecode encoding (emacs-lsp-booster style) to reduce JSON parsing overhead. Disable if you see non-ASCII encoding issues.

Completion (Popup & Inline)

Variable Default Description
lsp-proxy-max-completion-item 20 Maximum completion items requested/returned per query. Lower for speed, higher for breadth.
lsp-proxy-inline-completion-enable-predicates (evil-insert-state-p) All zero-arg predicates must return non-nil to allow inline completion. Customize for editing states.
lsp-proxy-inline-completion-disable-predicates nil Any predicate returning non-nil blocks inline completion (override failsafe).
lsp-proxy-inline-completion-trigger-characters () Characters that immediately trigger an inline completion request when typed. Use a list of string/char tokens.
lsp-proxy-inline-completion-idle-delay 0.3 Idle delay (seconds) before showing inline completion suggestions after predicates are satisfied.

Diagnostics

Variable Default Description
lsp-proxy-diagnostics-provider :auto Backend selector: :auto prefers Flycheck if present; :flycheck, :flymake force; :none disable; t prefer Flymake; nil prefer Flycheck.

Navigation & Symbols

Variable Default Description
lsp-proxy-enable-imenu t Enable Imenu outline via textDocument/documentSymbol when server capability is present.
lsp-proxy-lazy-xref-threshold 10000 Line-count threshold above which lazy/optimized Xref evaluation is considered for large buffers.
lsp-proxy-xref-optimization-strategy 'optimized Strategy for Xref processing: eager original; lazy minimal preview; optimized balanced (fast with previews).
lsp-proxy-enable-symbol-highlighting t Highlight occurrences of symbol at point using documentHighlight support.
lsp-proxy-enable-hover-eldoc nil Request hover info automatically and integrate into Eldoc while moving point.

Inlay Hints

Variable Default Description
lsp-proxy-inlay-hints-mode-config nil Controls inlay hint activation: nil disable; t enable globally; list of major mode symbols limits to those modes.

Large File Handling

Variable Default Description
lsp-proxy-large-file-threshold (* 10 1024 1024) Byte size threshold (≈10MB) beyond which files load asynchronously chunk by chunk.
lsp-proxy-large-file-loading-timeout 30 Seconds before aborting a pending/loading large file operation.
lsp-proxy-large-file-chunk-size (* 1 1024 1024) Chunk size (≈1MB) used when streaming large file contents to the server. Adjust for speed vs memory.

Formatting Hooks

Variable Default Description
lsp-proxy-trim-trailing-whitespace t Trim trailing spaces on lines when syncing/saving (align with project style).
lsp-proxy-insert-final-newline t Ensure file ends with a single newline.
lsp-proxy-trim-final-newlines t Remove surplus blank lines after the final newline.

Usage Tips

  • For heavy projects, increase lsp-proxy--send-changes-idle-time and maybe lower lsp-proxy-max-completion-item.
  • If inline completion feels intrusive, add predicates to lsp-proxy-inline-completion-disable-predicates (e.g., (company--active-p) or mode-specific checks).
  • Set lsp-proxy-log-level to 2 temporarily when investigating protocol issues, together with lsp-proxy-log-max > 0.
  • Disabling lsp-proxy-enable-bytecode can help pinpoint serialization issues on bleeding-edge Emacs versions.

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-auto-prefix 0)
(setq corfu-popupinfo-delay '(0.1 . 0.1))

company-box

(defun company-box-icons--lsp-proxy (candidate)
    (-when-let* ((proxy-item (get-text-property 0 'lsp-proxy--item candidate))
                 (lsp-item (plist-get proxy-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-proxy 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-proxy-mode-hook
            (defun lsp-proxy-capf ()
              (remove-hook 'completion-at-point-functions #'lsp-proxy-completion-at-point t)
              (add-hook 'completion-at-point-functions
                        (cape-capf-super
                         #'lsp-proxy-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-proxy-diagnostics-provider :flymake)

GitHub Copilot Integration

LSP-Proxy includes built-in support for GitHub Copilot through the lsp-proxy-copilot.el module, providing seamless integration with GitHub Copilot's AI-powered code suggestions.

Features

  • Authentication Management: Sign in and out of GitHub Copilot directly from Emacs
  • Status Monitoring: Check your current Copilot authentication status
  • Browser Integration: Automatic browser launch for device authentication flow
  • Cross-platform Support: Works in both GUI and console modes

Configuration

First, install the GitHub Copilot Language Server globally using npm:

npm i @github/copilot-language-server -g

To enable GitHub Copilot integration, you need to configure the Copilot language server in your languages.toml file:

# this is already included in the built-in languages.toml
[language-server.copilot]
command = "copilot-langauge-server"
args = ["--stdio"]


[[language]]
name = "markdown"
language-id = "markdown"
file-types = ["md"]
language-servers = [{ name = "copilot", support-workspace = true }]

Available Commands

Authentication Commands

  • M-x lsp-proxy-copilot-sign-in: Sign in to GitHub Copilot

    • Displays a device code for authentication
    • Opens your browser automatically (in GUI mode)
    • Guides you through the authentication process
  • M-x lsp-proxy-copilot-sign-out: Sign out from GitHub Copilot

    • Clears your authentication session
  • M-x lsp-proxy-copilot-status: Check current Copilot status

    • Shows authentication state and connection information

Customization

;; Customize the Copilot server name (default: "copilot") , if you changed the name in languages.toml, you need to set it here too.
(setq lsp-proxy-copilot-server-name "copilot")

Technical Implementation

The GitHub Copilot integration uses LSP-Proxy's generic request forwarding mechanism (emacs/forwardRequest), allowing seamless communication with any configured language server, including GitHub Copilot's language server.

Org-mode Integration

LSP-Proxy provides comprehensive support for org-mode through the lsp-proxy-org.el module, enabling LSP features within org-babel source blocks.

Enabling Org-babel LSP Support

To enable LSP support in org-babel code blocks, add the following to your configuration:

;; Enable LSP Proxy support in org-mode
(add-hook 'org-mode-hook #'lsp-proxy-mode)

;; Enable LSP support in org-babel code blocks
(setq lsp-proxy-enable-org-babel t)

;; Enable LSP support in org-edit-special buffers (default: t)
(setq lsp-proxy-org-edit-special-enable-lsp t)

Configuration Variables

Language Support Configuration

;; Specify which languages to enable LSP support for in org-babel blocks
(setq lsp-proxy-org-babel-enabled-languages
      '("python" "typescript" "javascript" "tsx" "bash" "rust" "go"))

;; Map org-babel language names to LSP language IDs
(setq lsp-proxy-org-babel-language-map
      '(("shell" . "bash")
        ("sh" . "bash")
        ("tsx-ts" . "tsx")
        ("typescript-ts" . "typescript")))

Features

1. Direct Org-babel Block Editing

  • Code completion within org-mode source blocks
  • Hover documentation and signature help
  • Diagnostics (syntax errors, warnings)
  • Symbol navigation and references
  • Automatic LSP server startup when entering code blocks

2. Org-edit-special Buffer Support

  • Full LSP functionality in org-edit-special buffers (C-c ')
  • Seamless integration with existing org-mode workflow
  • Automatic language detection from source block headers

Example Usage

Create an org-mode file with source blocks:

#+BEGIN_SRC python
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

# LSP features work here: completion, hover, diagnostics
print(fibonacci(10))
#+END_SRC

#+BEGIN_SRC typescript
interface User {
    name: string;
    age: number;
}

// Full TypeScript LSP support available
const user: User = {
    name: "Alice",
    age: 30
};
#+END_SRC

How It Works

  1. Automatic Detection: LSP-Proxy detects when the cursor enters an org-babel source block
  2. Virtual Documents: Code blocks are sent to language servers as virtual documents
  3. Position Translation: Cursor positions are automatically translated between the org file and virtual document
  4. Efficient Caching: Block information is cached to minimize parsing overhead
  5. Smart Cleanup: Resources are cleaned up when leaving code blocks

Advanced Configuration

Custom Language Mappings

For specialized setups, you can extend the language mapping:

(add-to-list 'lsp-proxy-org-babel-language-map '("myshell" . "bash"))
(add-to-list 'lsp-proxy-org-babel-enabled-languages "myshell")

Troubleshooting

Issue Solution
LSP not working in code blocks Ensure language is in lsp-proxy-org-babel-enabled-languages
Wrong language server started Check lsp-proxy-org-babel-language-map for correct mapping
Edit buffer not getting LSP Verify lsp-proxy-org-edit-special-enable-lsp is t

Remote Development

LSP-Proxy supports transparent remote development over SSH via Emacs TRAMP. When you open a TRAMP file, the local lsp-proxy process checks whether a compatible binary exists on the remote host, starts a remote server process, and routes all LSP traffic through the SSH tunnel. Binary deployment is controlled by lsp-proxy-remote-deploy-mode — either manually on demand or automatically in the background.

How It Works

  1. You open a file with a TRAMP path (e.g., /ssh:myserver:/home/user/project/main.rs)
  2. lsp-proxy detects the /ssh: prefix and establishes an SSH ControlMaster connection
  3. It checks whether emacs-lsp-proxy is available in the remote PATH; if the version matches, it is used directly without uploading any file
  4. If the global command is absent or has a version mismatch, lsp-proxy checks the fixed deploy path (lsp-proxy-remote-binary-path). If neither location has a matching binary, deployment is triggered according to lsp-proxy-remote-deploy-mode (see Binary Deployment)
  5. The remote binary is started with --remote-server; LSP traffic is forwarded through the tunnel
  6. From Emacs's perspective, everything works the same as a local buffer

Supported TRAMP Methods

TRAMP path form Description
/ssh:host:/path SSH config alias (user resolved via ~/.ssh/config)
/ssh:user@host:/path Explicit username
/ssh:user@host#port:/path Custom SSH port
/rpc:user@host:/path RPC tunnel (alternative to ssh method)

Prerequisites

  • SSH access to the remote host (password-less key auth recommended)
  • The local emacs-lsp-proxy binary must be compatible with the remote host's architecture (Linux x86-64 or ARM64)
  • The remote user needs write permission to the deploy directory (default: ~/.cache/emacs/lsp-proxy/)

Basic Usage

No special configuration is required. Enable lsp-proxy-mode normally, then open any TRAMP file:

;; In your init file — same hook as local files
(add-hook 'typescript-ts-mode-hook #'lsp-proxy-mode)
;; Open a remote file — lsp-proxy-mode activates automatically
(find-file "/ssh:myserver:/home/user/project/main.rs")

lsp-proxy handles binary deployment and tunnel setup on first access. Subsequent files on the same host reuse the existing SSH connection.

Customization

;; Change where the binary is deployed on the remote host
;; Default: "~/.cache/emacs/lsp-proxy/emacs-lsp-proxy"
(setq lsp-proxy-remote-binary-path "/opt/tools/emacs-lsp-proxy")

;; Deploy mode: 'manual (default) or 'auto
(setq lsp-proxy-remote-deploy-mode 'manual)

Binary Deployment

When the binary check fails (missing or version mismatch on both the global command and the deploy path), lsp-proxy behaves according to lsp-proxy-remote-deploy-mode.

manual (default)

lsp-proxy prints a hint in the minibuffer and waits for you to act:

[lsp-proxy] Remote binary unavailable on myserver (…). Run M-x lsp-proxy-remote-deploy to deploy v0.7.2 to ~/.cache/…

Run M-x lsp-proxy-remote-deploy to open the *lsp-proxy-deploy* buffer. It shows the check result for both locations, then asks for confirmation before uploading:

Deploy log — myserver  [manual]  (started 2026-05-19 10:30:00)
------------------------------------------------------------

[10:30:00] Checking remote binary...

[10:30:01] Local version : 0.7.2
[10:30:01] Deploy path   : ~/.cache/emacs/lsp-proxy/emacs-lsp-proxy

[10:30:01] Global command (emacs-lsp-proxy): ✗ not found in PATH
[10:30:01] Deploy path   (~/.cache/…):       ✗ not found

[10:30:01] Binary not available or outdated. Deploy required.

Confirm the [lsp-proxy] Deploy v0.7.2 to myserver? prompt and the upload begins, with each step appended to the same buffer. Re-open the remote file once the deploy succeeds.

auto

lsp-proxy starts the upload immediately as soon as the check fails, streaming each step to the *lsp-proxy-deploy* buffer so you can follow along without doing anything:

Deploy log — myserver  [auto]  (started 2026-05-19 10:30:00)
------------------------------------------------------------

[10:30:00] Binary unavailable (…). Starting automatic deploy of v0.7.2...
[10:30:00] Target path: ~/.cache/emacs/lsp-proxy/emacs-lsp-proxy
[10:30:00] Starting upload...
[10:30:01] Checking global emacs-lsp-proxy on remote (need v0.7.2)...
[10:30:01] Global emacs-lsp-proxy not found in remote PATH.
[10:30:01] Checking ~/.cache/emacs/lsp-proxy/emacs-lsp-proxy...
[10:30:01] Uploading /usr/local/bin/emacs-lsp-proxy (8388608 bytes)...
[10:30:04] Upload complete, verifying...
[10:30:04] ✓ Deploy succeeded. Binary: ~/.cache/emacs/lsp-proxy/emacs-lsp-proxy

Re-open the remote file once the deploy completes.

M-x lsp-proxy-remote-deploy

This command is always available regardless of the deploy mode. It runs the full check-and-deploy flow interactively, defaulting to the host that most recently requested a deploy. Use it to re-deploy after a version upgrade or to recover from a failed auto-deploy.

Remote Configuration

Each remote host has its own languages.toml, located in the same directory as the remote binary (lsp-proxy-remote-binary-path, default ~/.cache/emacs/lsp-proxy/languages.toml). This is the remote counterpart to the local M-x lsp-proxy-open-config-file, letting you customize language servers per host without touching your local config.

Run M-x lsp-proxy-remote-open-config-file to edit it:

  • When the current buffer visits a remote file (a TRAMP path such as /ssh:myserver:/home/user/project/main.rs), the command opens that host's config directly over TRAMP — no prompt.
  • Otherwise it prompts for a host, offering completion over your known SSH connections (the same candidates as M-x lsp-proxy-remote-deploy).

The file is opened through TRAMP, so you edit and save it as a normal buffer. After changing it, run M-x lsp-proxy-restart to reload the configuration on the remote session.

;; While visiting a remote file — opens that host's languages.toml directly
M-x lsp-proxy-remote-open-config-file

;; From a local buffer — prompts for the host first
M-x lsp-proxy-remote-open-config-file

Diagnostics

Connection status — run M-x lsp-proxy-doctor and check the Remote Connection Status section:

Field Meaning
Binary Path Command or path actually used (emacs-lsp-proxy if the global command was selected, otherwise the deploy path)
Deploy Status deployed, missing, version_mismatch, or unknown
Local Version Version of your local lsp-proxy binary
Remote Version Version reported by the remote binary

Check the remote binary manually — verify which binary is in use and its version:

# Global command (preferred when available)
ssh myserver 'emacs-lsp-proxy --version'

# Deployed binary at the default path
ssh myserver '~/.cache/emacs/lsp-proxy/emacs-lsp-proxy --version'

View the remote log — each server start creates a new timestamped log file next to the binary (~/.cache/emacs/lsp-proxy/remote-server-YYYYMMDD-HHMMSS.log). First set the log level so output is written, then open the remote file to trigger a fresh start:

(setq lsp-proxy-log-level 2)
# Print the latest log
ssh myserver 'ls -t ~/.cache/emacs/lsp-proxy/remote-server-*.log | head -1 | xargs cat'

# Follow in real time while reproducing the issue
ssh myserver 'ls -t ~/.cache/emacs/lsp-proxy/remote-server-*.log | head -1 | xargs tail -f'

# Clean up old logs, keeping the 5 most recent
ssh myserver 'ls -t ~/.cache/emacs/lsp-proxy/remote-server-*.log | tail -n +6 | xargs rm -f'

Troubleshooting

Issue Solution
Remote binary missing, no prompt shown Check lsp-proxy-remote-deploy-mode; in manual mode the hint appears in the minibuffer — run M-x lsp-proxy-remote-deploy
Deploy fails with permission error Ensure the remote directory is writable; set lsp-proxy-remote-binary-path to a writable path
Binary architecture mismatch Cross-platform deploy is not supported; the local binary must match the remote OS/arch
SSH connection times out Configure ServerAliveInterval / ServerAliveCountMax in ~/.ssh/config
Remote LSP not starting Set lsp-proxy-log-level to 2, reopen the file, check the log for SSH/deploy errors
Features stop working after reconnect Run M-x lsp-proxy-restart to re-establish the remote session

Acknowledgements

Thanks to Helix, the architecture of Lsp-Proxy 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-Proxy, 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-Proxy, 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.

About

An LSP client for Emacs implemented in Rust.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors