A .NET extension for the Zed editor.
The goal of this project is simple: make writing C# and .NET in Zed feel as first-class as writing Rust or C++ does today — real IDE semantics out of the box, no manual language-server wiring, no external terminal for building, running, or debugging.
Warning
Status: early development. Both main paths now work end to end: the extension downloads and starts Roslyn, and a netcoredbg session runs to completion against a real project. The project is not published to the Zed extension registry. Expect breaking changes.
Zed ships deep, built-in support for Rust and C++: syntax, outline, runnables, inlay hints, a debugger, and a language server that is installed and updated for you. For .NET the experience today is thinner — you get highlighting, and whatever you configure by hand.
zed-dotnet aims to close that gap by bundling the whole toolchain
integration into one extension:
| Capability | Rust / C++ in Zed | Goal for .NET |
|---|---|---|
| Language server auto-install | ✅ | ✅ Roslyn, downloaded and updated automatically |
| Semantic highlighting & inlay hints | ✅ | ✅ enabled by default |
| Outline, brackets, indents, text objects | ✅ | ✅ tree-sitter queries |
| Project-file awareness | ✅ (Cargo.toml) |
✅ MSBuild (.csproj, .props, .targets, .slnx) |
| Template/markup language | n/a | ✅ Razor (.razor, .cshtml), syntax only |
| Debugging | ✅ | ✅ netcoredbg DAP adapter |
| Build/run/test from the editor | ✅ | ✅ dotnet tasks + debug locator |
- C# language definition — tree-sitter grammar
(
tree-sitter-c-sharp) with highlights, brackets, indents, outline, injections, overrides, and Vim text objects. - MSBuild language definition —
.csproj,.fsproj,.vbproj,.props,.targets,.slnx,Directory.Build.*, andnuget.configare recognised as MSBuild XML with their own outline and highlights. - Razor language definition —
.razorand.cshtmlfiles get syntax, outline, and indentation. The grammar embeds C# outright, so code inside@codeblocks and@expressions is parsed as C# rather than guessed at, and the surrounding markup is handed to the HTML grammar by injection. Syntax only: see the note onrzlsunder Planned. - Roslyn language server — Microsoft's
Microsoft.CodeAnalysis.LanguageServer, downloaded from NuGet for your platform, kept up to date, and started with--autoLoadProjectsso solutions and projects load without any manualsolution/openhandshake. - Sensible defaults — inlay hints for parameters, types, lambdas, and
implicit object creation are turned on server-side, so flipping Zed's
inlay_hintsswitch is all you need. - Semantic highlighting — Roslyn's C#-specific token types (
field,controlKeyword,recordStruct,extensionMethod,excludedCode, …) are mapped onto theme styles, the same mechanism Zed's built-in Rust, Go, and C++ support uses. This also colours XML doc comments, regular expressions, and JSON embedded in string literals, which tree-sitter alone cannot reach. - netcoredbg debug adapter — downloaded from GitHub releases, wired into
Zed's debugger UI with a JSON schema for
launchandattach, plus adebugger.scmquery so paused sessions can show inline values for locals, parameters, and fields. dotnetdebug locator — turns adotnet run/dotnet testtask into a build-then-debug scenario by asking MSBuild for the produced assembly (-getProperty:TargetPath).- Runnables and tasks —
[Fact],[Theory],[Test],[TestMethod],[TestClass],[TestFixture], andMainget a gutter run button wired todotnet test/dotnet run;dotnet build,restore,test, andformatship as tasks for both C# and MSBuild files. - C# snippets — a starter set for the constructs you type most.
- Razor language server support out of the box — blocked on Microsoft's packaging, not on Zed. See the section below.
- F# and Visual Basic language definitions
.sln, the classic solution format — deliberately skipped for now. The three tree-sitter grammars that exist for it have between zero and one star and no maintenance history, and pinning an unmaintained grammar as a hard dependency of the manifest is worse than shipping no.slnhighlighting at all..slnx, its XML replacement, is already covered.csharpierformatter integration (Roslyn already handlestextDocument/formatting, so"formatter": "language_server"works today)- Test explorer runnables backed by
dotnet test
- Zed recent enough to support extension API
0.7and WASI Preview 2 components. If the extension fails to load, update Zed first. - .NET SDK 10.0 or newer on your
PATH. The Roslyn server is framework-dependent (net10.0,rollForward: Major); older SDKs will not start it. - Rust toolchain (
rustup) — only if you are building the extension from source.
Verify your SDK with:
dotnet --list-sdksgit clone https://github.com/Gambl3r08/zed-dotnet.gitThen in Zed:
- Open the command palette and run
zed: extensions. - Click Install Dev Extension.
- Select the cloned
zed-dotnetdirectory.
Zed compiles the Rust crate to WebAssembly and loads it. If a published version of the extension is installed, it is automatically marked Overridden by dev extension.
Not yet published. Once it is, zed: extensions → search for .NET.
All settings go in Zed's settings.json (zed: open settings).
Pin a specific server version instead of tracking the latest:
{
"lsp": {
"roslyn": {
"initialization_options": {
"package_version": "5.9.0-1.26303.1"
}
}
}
}By default only open files are analysed, which keeps a large solution responsive. To get errors for files you have not opened — closer to how Rider behaves — at the cost of noticeably more CPU and memory:
{
"lsp": {
"roslyn": {
"settings": {
"csharp|background_analysis": {
"dotnet_analyzer_diagnostics_scope": "fullSolution",
"dotnet_compiler_diagnostics_scope": "fullSolution"
}
}
}
}
}Turned off by default, and it is worth knowing why. Zed renders code lenses and
dispatches the commands it recognises (editor.action.showReferences and
friends), but Roslyn emits roslyn.client.peekReferences for the reference lens
and its own commands for the test lens. Zed cannot dispatch either, so the
lenses appear as labels that do nothing when clicked.
The reference count is still useful at a glance, so if you want it:
{
"lsp": {
"roslyn": {
"settings": {
"csharp|code_lens": {
"dotnet_enable_references_code_lens": true
}
}
}
}
}Razor files get syntax, outline, and indentation out of the box. Completion,
diagnostics, and go-to-definition need Roslyn co-hosting: the Razor
component DLLs are loaded into the Roslyn process with --extension, after
which Roslyn serves .razor over ordinary LSP. This is exactly how the VS Code
C# extension does it — it passes only --extension and --csharpDesignTimePath,
both of which the stock server accepts.
It is not enabled by default because Microsoft does not publish a compatible pair:
| Piece | Published |
|---|---|
Microsoft.VisualStudioCode.RazorExtension |
one version, 10.0.0-preview.25277.114 |
| …which depends on Roslyn | 5.0.0-1.25230.6, through the version-locked ExternalAccess.Razor.Features |
roslyn-language-server on nuget.org |
5.5.0 … 5.9.0 — no 5.0.0 |
Loading that Razor build into the 5.9.0 server does nothing at all: no extra capabilities, no dynamic registrations, and no error. It fails silently, which is worse than not trying.
If you have a matched pair — for instance extracted from the VS Code C# extension's VSIX, which ships both together — you can point the extension at it and Razor files will be routed to that server:
{
"lsp": {
"roslyn": {
"binary": {
"path": "/path/to/Microsoft.CodeAnalysis.LanguageServer",
"arguments": [
"--stdio",
"--autoLoadProjects",
"--extension",
"/path/to/.razorExtension/Microsoft.VisualStudioCode.RazorExtension.dll"
]
}
}
}
}Other Zed Razor extensions solve this by downloading a third-party rebuild of Roslyn that bundles matched versions. That works, and it is a reasonable choice; it is not made here because it moves the trust boundary off Microsoft, which should be your decision rather than a default.
{
"languages": {
"C#": {
"tab_size": 4,
"formatter": "language_server",
"format_on_save": "on",
"inlay_hints": {
"enabled": true,
"show_parameter_hints": true,
"show_type_hints": true
}
}
}
}Create .zed/debug.json in your project:
[
{
"label": "Debug console app",
"adapter": "netcoredbg",
"request": "launch",
"program": "$ZED_WORKTREE_ROOT/src/App/bin/Debug/net10.0/App.dll",
"cwd": "$ZED_WORKTREE_ROOT/src/App",
"args": [],
"stopAtEntry": false,
"justMyCode": true
},
{
"label": "Attach to process",
"adapter": "netcoredbg",
"request": "attach",
"processId": 0
}
]The adapter binary is downloaded on first use; you can point at your own with:
{
"dap": {
"netcoredbg": {
"binary": "/usr/local/bin/netcoredbg"
}
}
}Build, restore, test, format, hot reload (dotnet watch), and package
management (add package, list package --outdated, --vulnerable) are all
available from task: spawn whenever a C# or MSBuild file is open — no
.zed/tasks.json needed. Test methods, test classes, and Main also get a run
button in the gutter.
The package tasks take the package name from the current selection, so
selecting Serilog and running dotnet add package (selección) adds it.
Note
dotnet watch needs the ASP.NET Core shared runtime
(Microsoft.AspNetCore.App), which is a separate install from the SDK on some
distributions. If the task fails with "No frameworks were found", install
your distribution's aspnet-runtime package.
Starting a debug session from a dotnet run task uses the bundled dotnet
locator: it runs the build first, asks MSBuild for TargetPath, and launches
netcoredbg against the resulting assembly.
The locator deliberately does not handle dotnet test. MSBuild will report a
TargetPath for a test project, but that assembly is a library driven by
VSTest — launching it directly exits immediately without running anything.
The working route is to have the test host wait for a debugger, which is what the "dotnet test $ZED_SYMBOL (esperar al depurador)" task does. Run it from the gutter button on any test, and it prints:
La depuración host está habilitada. Asocie el depurador al proceso de host de prueba para continuar.
Process Id: 176149, Name: dotnet
Then start an attach configuration with that process id. Breakpoints inside
the test are hit normally. It is two steps rather than one; a single-click
version would need Zed to start a debug session from a task's output, which the
extension API does not expose.
zed-dotnet/
├── extension.toml # Zed manifest: languages, grammars, LSPs, DAPs
├── Cargo.toml # Rust crate compiled to a WASM component
├── rust-toolchain.toml
├── src/
│ ├── lib.rs # Extension trait implementation + registration
│ ├── roslyn.rs # Roslyn language server acquisition & config
│ ├── netcoredbg.rs # Debug adapter acquisition & DAP translation
│ ├── nuget.rs # NuGet flat-container client
│ └── util.rs # Filesystem helpers
├── languages/
│ ├── csharp/ # config.toml, .scm queries, runnables, tasks,
│ │ # semantic_token_rules.json
│ ├── msbuild/ # config.toml, .scm queries, tasks
│ └── razor/ # config.toml, .scm queries
├── debug_adapter_schemas/
│ └── netcoredbg.json # JSON schema for debug configurations
├── snippets/
│ └── C#.json # file name must match the language name
└── tests/
└── queries.rs # compiles every .scm and checks its captures
extension.toml is the manifest Zed reads first. It declares which
tree-sitter grammars to fetch, which language directories exist, which
language servers this extension can start, and which debug adapters it
provides. Anything requiring logic at runtime — deciding where the Roslyn
binary lives, downloading it, translating a debug configuration — lives in the
Rust crate, which Zed compiles to a WebAssembly component and calls through
the zed_extension_api interface.
# Once, so the WASI target is available.
rustup target add wasm32-wasip2
# Build the WASM component the way Zed does.
cargo build --release --target wasm32-wasip2
# Lint
cargo clippy --all-targets
cargo fmt --check
# Validate the tree-sitter queries against the real grammars.
cargo testtests/queries.rs checks two separate things, because a query can be wrong in
two ways. It can fail to compile against the grammar — a bad node name or field
— or it can compile fine but use capture names Zed does not recognise, in which
case Zed loads the query and silently drops the feature. Only the second kind
shows up at runtime, buried in the Zed log, which is exactly why it is worth
catching in a test. The allowed capture names mirror Grammar in
zed/crates/language_core/src/grammar.rs.
After editing Rust, .scm, or .toml files, reload the extension from the
Zed extensions page (Rebuild on the dev extension entry).
Run Zed with verbose logging to see the extension's stdout and the language server handshake:
zed --foregroundRoslyn writes its own logs to the directory passed via
--extensionLogDirectory, which the extension places next to the downloaded
server.
Grammar revisions are pinned by commit SHA in extension.toml. To point at a
local checkout while iterating on queries, swap the repository for a file://
URL.
Issues and pull requests are welcome. Useful references:
- dotnet/roslyn — the C# language server.
- Samsung/netcoredbg — the .NET debugger.
- tree-sitter/tree-sitter-c-sharp — C# grammar; the bundled
highlights.scmis adapted from its upstream queries (MIT). - tree-sitter-grammars/tree-sitter-xml — XML grammar used for MSBuild files.
MIT © Roberto Lozada
{ "lsp": { "roslyn": { // Override the auto-downloaded server with your own build. "binary": { "path": "/path/to/Microsoft.CodeAnalysis.LanguageServer", "arguments": ["--stdio", "--autoLoadProjects"] }, // Roslyn settings use the `language|category` key format. "settings": { "csharp|background_analysis": { "dotnet_analyzer_diagnostics_scope": "fullSolution", "dotnet_compiler_diagnostics_scope": "fullSolution" }, "csharp|completion": { "dotnet_show_completion_items_from_unimported_namespaces": true, "dotnet_show_name_completion_suggestions": true }, "csharp|inlay_hints": { "csharp_enable_inlay_hints_for_types": true, "dotnet_enable_inlay_hints_for_parameters": true } } } } }