Skip to content

About

Customization for eglot to support csharp (C#) language servers

Resources

Contributing

Stars

3 stars

Watchers

0 watching

Forks

Repository files navigation

eglot-csharp

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+ and csharp-ls 0.29.0+, which reads the settings below from initializationOptions; older csharp-ls versions ignore them.

AI Assistance

This project has been developed with substantial help from LLMs, including the following models:

  • anthropic/claude-sonnet-4-6
  • anthropic/claude-sonnet-5
  • anthropic/claude-opus-5-5

Source files carry an Assisted-by: header line per the Linux kernel coding-assistants convention.

Features

  • Registers csharp-ls as the eglot server for csharp-mode and csharp-ts-mode
  • Registers csharp-ls for .cshtml (Razor) files too, via a thin web-mode-derived eglot-csharp-cshtml-mode — sharing the same connection as a project's .cs buffers rather than starting a redundant second csharp-ls process
  • Enables useMetadataUris so that go-to-definition, go-to-implementation, and find-references all navigate into decompiled BCL and NuGet sources
  • Enables razorSupport so that .cshtml documents get diagnostics, hover, completion, and navigation from csharp-ls
  • Handles csharp:/ URIs returned by csharp-ls by fetching decompiled source via the csharp/metadata extension method and caching it to disk as a plain .cs file

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.

Installation

Manual

Clone this repo and add it to your load path:

(add-to-list 'load-path "/path/to/eglot-csharp")
(require 'eglot-csharp)

straight.el

(straight-use-package
 '(eglot-csharp :type git :host github :repo "razzmatazz/eglot-csharp"))

use-package + straight.el

(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))

Doom Emacs

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.

Local checkout (Doom 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.

Configuration

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 (Razor) support

.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 (early in your init file, or with setopt) if you'd rather manage .cshtml's major mode / eglot activation yourself.

Customization

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
eglot-csharp-razor-support t Enable csharp-ls's Razor (.cshtml) document support
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.* key can also be overridden per project via that project's .dir-locals.el. A :csharp section in your own eglot-workspace-configuration also works and wins key by key, since csharp-ls reads it after the settings eglot-csharp sends; see DESIGN.md's "Mechanism 2: server settings".

How decompilation 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.

Development

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 ci        # build the image, install deps, then compile, lint and test in parallel
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 builds a local eglot-csharp-test:emacs-$EMACS image (see Dockerfile — Emacs built from the GNU release, plus Eask on $PATH) and runs eask install-deps --dev in it, 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. compile, lint and test can then run on their own.

EMACS selects the release, 31.1 by default:

make ci EMACS=30.2

GitHub Actions (.github/workflows/test.yml) runs the same targets on every push/PR against Emacs 30.2 and 31.1.

The test suite in test/eglot-csharp-tests.el covers pure, in-process logic (the eglot-server-programs/contact-function resolution, the initializationOptions plist and its JSON encoding, language-ID mapping). It doesn't start a real csharp-ls; tests that need an eglot-csharp--server instance get one backed by a throwaway pipe process.

See also CONTRIBUTING.md.

License

GPL-3.0-or-later. See LICENSE.

About

Customization for eglot to support csharp (C#) language servers

Resources

Contributing

Stars

3 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages