C# language support for the eglot LSP client.
Currently built around csharp-language-server
(csharp-ls), with the intention of supporting additional C# LSP servers
(such as OmniSharp and
roslyn-lsp) in the future.
Requires Emacs 30.1+.
This project has been developed with substantial help from LLMs, including the following models:
anthropic/claude-sonnet-4-6anthropic/claude-sonnet-5
Source files carry an Assisted-by: header line per the
Linux kernel coding-assistants convention.
- Registers
csharp-lsas the eglot server forcsharp-modeandcsharp-ts-mode - Registers
csharp-lsfor.cshtml(Razor) files too, via a thinweb-mode-derivedeglot-csharp-cshtml-mode— sharing the same connection as a project's.csbuffers rather than starting a redundant secondcsharp-lsprocess - Enables
useMetadataUrisso that go-to-definition, go-to-implementation, and find-references all navigate into decompiled BCL and NuGet sources - Enables
razorSupportso that.cshtmldocuments get diagnostics, hover, completion, and navigation fromcsharp-ls - Handles
csharp:/URIs returned bycsharp-lsby fetching decompiled source via thecsharp/metadataextension method and caching it to disk as a plain.csfile
The csharp:/ URI handler is modeled after
eglot-java's jdt:// handler: decompiled
source is written to a per-project cache directory so Emacs opens it as a normal file.
Clone this repo and add it to your load path:
(add-to-list 'load-path "/path/to/eglot-csharp")
(require 'eglot-csharp)(straight-use-package
'(eglot-csharp :type git :host github :repo "razzmatazz/eglot-csharp"))(use-package eglot-csharp
:straight (eglot-csharp :type git :host github :repo "razzmatazz/eglot-csharp")
:hook ((csharp-mode csharp-ts-mode eglot-csharp-cshtml-mode) . eglot-csharp-mode))Add to packages.el:
(package! eglot-csharp
:recipe (:host github :repo "razzmatazz/eglot-csharp"))Add to config.el:
(use-package! eglot-csharp
:hook ((csharp-mode csharp-ts-mode eglot-csharp-cshtml-mode) . eglot-csharp-mode))Then run doom sync and restart Emacs.
If you have cloned the repo locally and want Doom to load it directly from
disk without fetching from GitHub, use :local-repo with the absolute path
in packages.el:
(package! eglot-csharp
:recipe (:local-repo "/path/to/eglot-csharp"))straight.el will symlink that directory into its repos folder instead of
cloning it, so edits to your local checkout are reflected immediately after
doom sync.
Activate eglot-csharp-mode via a hook — this registers csharp-ls, enables
useMetadataUris, and starts eglot automatically when you open a .cs file:
(require 'eglot-csharp)
(add-hook 'csharp-mode-hook #'eglot-csharp-mode)
(add-hook 'csharp-ts-mode-hook #'eglot-csharp-mode)Or activate manually in any .cs buffer:
M-x eglot-csharp-mode
.cshtml files are automatically associated with eglot-csharp-cshtml-mode — a thin
wrapper around web-mode that gives Razor views their own
major-mode symbol without affecting other web-mode buffers (.html, .php, .vue,
...). web-mode itself is not a dependency of this package and must be installed
separately. Wire up eglot the same way as for .cs files:
(add-hook 'eglot-csharp-cshtml-mode-hook #'eglot-csharp-mode)Opening a .cshtml file in a project whose .cs files are already managed by
eglot-csharp-mode (or vice versa) shares the same csharp-ls connection rather than
starting a second one — see DESIGN.md for why that matters and how it works.
Set eglot-csharp-enable-cshtml to nil (before loading the package) if you'd rather
manage .cshtml's major mode / eglot activation yourself.
| Variable | Default | Description |
|---|---|---|
eglot-csharp-server-program |
"csharp-ls" |
Path or name of the csharp-ls executable |
eglot-csharp-use-metadata-uris |
t |
Enable csharp:/ URIs for decompiled sources |
eglot-csharp-metadata-cache-directory |
".cache/eglot-csharp/metadata" |
Cache directory for decompiled files, relative to the project root |
eglot-csharp-enable-cshtml |
t |
Associate .cshtml files with eglot-csharp-cshtml-mode; takes effect at load time |
eglot-csharp-razor-support |
t |
Enable csharp-ls's Razor (.cshtml) document support via workspace configuration |
eglot-csharp-analyzers-enabled |
nil |
Run Roslyn analyzers as part of diagnostics (IDE style rules, NuGet analyzers); increases latency |
eglot-csharp-apply-formatting-options |
nil |
Let csharp-ls use client-supplied formatting options (may override .editorconfig) |
eglot-csharp-locale |
nil |
Force csharp-ls's diagnostics/message locale, e.g. "en-US" or "de"; nil follows the system locale |
eglot-csharp-solution-path-override |
nil |
Force a specific .sln/.slnx file when a workspace has more than one |
eglot-csharp-log-level |
nil |
csharp-ls's own log level ("trace"/"debug"/"info"/"warning"/"error"); nil leaves it at csharp-ls's default ("info") |
eglot-csharp-debug-mode |
nil |
Enable csharp-ls's periodic request-queue statistics logging |
All options are in the eglot-csharp customization group (M-x customize-group RET eglot-csharp).
Every setting above that maps to a csharp.* workspace-configuration key can also be
overridden per project via that project's .dir-locals.el — see
DESIGN.md's "Mechanism 2: workspace configuration" for why that works.
When useMetadataUris is enabled, csharp-ls returns csharp:/ URIs for symbols
defined in compiled assemblies (BCL, NuGet packages) or generated by source
generators. eglot-csharp fetches their source via the csharp/metadata LSP
extension method, caches it to disk, and opens it as an ordinary read-only
buffer — including for navigation performed from within such a buffer
(e.g. go-to-definition on a type used in a decompiled method signature).
See DESIGN.md for how this is implemented under the hood.
Byte-compiling, linting, and running the test suite all happen inside a Docker (or Podman) container, via Eask, so none of it depends on what's installed in your own Emacs. This mirrors the same mechanism used by octocat.el.
make compile # byte-compile with -Wall (eask compile --strict)
make lint # checkdoc + package-lint (eask lint checkdoc / eask lint package)
make test # run the ERT suite in test/eglot-csharp-tests.el
make ci # all three, in parallel (make -j3 ci also works)Each target builds a local eglot-csharp-test image on first use (see
Dockerfile — plain emacs-nox + Eask
on $PATH), mounts the repo into it, and runs eask install-deps --dev
before the actual command, so dependencies (including the dev-only
web-mode, needed to exercise eglot-csharp-cshtml-mode in tests — see
Eask) are resolved inside the container, not on the host.
GitHub Actions (.github/workflows/test.yml) runs the same three make
targets on every push/PR.
The test suite in test/eglot-csharp-tests.el is currently a minimal
starting scaffold covering pure, in-process logic only (the
eglot-server-programs/contact-function resolution, eglot-csharp-- workspace-configuration's server-type gating, language-ID mapping) — it
does not yet spin up a real csharp-ls connection. See the TODO comment
above eglot-csharp-test-workspace-configuration-ignores-other-servers for
the next obvious thing to add.
GPL-3.0-or-later. See LICENSE.