A dynamic, project-aware LSP dispatcher for Eglot, designed to support multiple language server configurations for the same major mode — including simultaneous LSPs via LSPX.
Eglot is a lightweight and elegant client for Language Server Protocol (LSP), but it assumes a one-to-one mapping between a major mode and a single language server. In practice, however, real-world projects often require more flexibility:
- Different LSPs for the same language (e.g., TypeScript projects that use Angular, React, or no framework at all)
- Running multiple LSPs simultaneously for the same buffer (e.g., a language server plus a linter)
- Project-specific configurations that vary based on files like
angular.json
,package.json
, orvite.config.ts
eglot-dispatcher
solves this by letting you define a declarative list of strategies that determine which LSP(s) to launch based on file type and project context — all without modifying eglot
itself.
- 🔄 Dynamically chooses the correct LSP for a file based on project structure
- 🧠 Supports multiple major modes per strategy
- 🔁 Automatically runs multiple LSPs per buffer via lspx
- 🔍 Supports
:initializationOptions
,:command
, and other extended configuration options - 💡 Clean, declarative interface designed for scalability and reuse
Clone the repo into your load-path
:
git clone https://github.com/Askath/eglot-dispatcher ~/emacs.d/lisp/eglot-dispatcher
Then in your Emacs configuration:
(add-to-list 'load-path "~/emacs.d/lisp/eglot-dispatcher")
(require 'eglot-dispatcher)
(add-hook 'typescript-ts-mode-hook #'eglot-dispatcher-dispatch)
(add-hook 'html-ts-mode-hook #'eglot-dispatcher-dispatch)
(setq eglot-strategies-list
'((:name "Angular + ESLint"
:modes (typescript-ts-mode html-ts-mode)
:project-root "angular.json"
:command (lambda (root)
(list
`("npx" "ngserver"
"--stdio"
"--tsProbeLocations" ,(expand-file-name "node_modules" root)
"--ngProbeLocations" ,(expand-file-name "node_modules" root))
'("vscode-eslint-language-server" "--stdio"))))
(:name "React"
:modes (typescript-ts-mode)
:project-root "package.json"
:predicate (lambda (root)
(file-exists-p (expand-file-name "node_modules/react" root)))
:command (lambda (_root)
'("typescript-language-server" "--stdio")))
(:name "Default TypeScript"
:modes (typescript-ts-mode)
:command (lambda (_root)
'("typescript-language-server" "--stdio")))))
Field | Description |
---|---|
:name |
Strategy name (used for logging/debugging) |
:modes |
List of major modes this strategy applies to |
:project-root |
A filename to search upward for to detect the project root |
:predicate (optional) |
A function (fn root) that returns non-nil if the strategy should be used |
:command |
A function (fn root) that returns:
|
If your command function returns multiple command lists, eglot-dispatcher
automatically invokes lspx to run them in parallel:
:command (lambda (_root)
(list
'("angular-language-server" "--stdio")
'("eslint-language-server" "--stdio")))
This becomes:
("lspx" "--lsp" "angular-language-server --stdio"
"--lsp" "eslint-language-server --stdio")
You can specify Eglot-compatible initialization options like so:
:command (lambda (_root)
'(:command ("typescript-language-server" "--stdio")
:initializationOptions (:tsserver (:logVerbosity "verbose"))))
- Use
angular-language-server
+eslint-language-server
in Angular projects - Use
typescript-language-server
in React projects with framework-specific flags - Fall back to the default TypeScript server in generic projects
- Extend to Vue, Python, Go, or any other Eglot-supported language
MIT
Feedback and contributions are very welcome. Please feel free to:
- Report issues
- Suggest new strategies or use cases
- Contribute improvements or enhancements