Proyectos

zed-dotnet

Rust 1 Actualizado el 28 de julio de 2026
portfolio

zed-dotnet

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.


Why this exists

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

Features

Working today

  • 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.*, and nuget.config are recognised as MSBuild XML with their own outline and highlights.
  • Razor language definition.razor and .cshtml files get syntax, outline, and indentation. The grammar embeds C# outright, so code inside @code blocks 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 on rzls under Planned.
  • Roslyn language server — Microsoft's Microsoft.CodeAnalysis.LanguageServer, downloaded from NuGet for your platform, kept up to date, and started with --autoLoadProjects so solutions and projects load without any manual solution/open handshake.
  • Sensible defaults — inlay hints for parameters, types, lambdas, and implicit object creation are turned on server-side, so flipping Zed's inlay_hints switch 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 launch and attach, plus a debugger.scm query so paused sessions can show inline values for locals, parameters, and fields.
  • dotnet debug locator — turns a dotnet run / dotnet test task into a build-then-debug scenario by asking MSBuild for the produced assembly (-getProperty:TargetPath).
  • Runnables and tasks[Fact], [Theory], [Test], [TestMethod], [TestClass], [TestFixture], and Main get a gutter run button wired to dotnet test/dotnet run; dotnet build, restore, test, and format ship as tasks for both C# and MSBuild files.
  • C# snippets — a starter set for the constructs you type most.

Planned

  • 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 .sln highlighting at all. .slnx, its XML replacement, is already covered.
  • csharpier formatter integration (Roslyn already handles textDocument/formatting, so "formatter": "language_server" works today)
  • Test explorer runnables backed by dotnet test

Requirements

  • Zed recent enough to support extension API 0.7 and 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-sdks

Installation

As a dev extension (current path)

git clone https://github.com/Gambl3r08/zed-dotnet.git

Then in Zed:

  1. Open the command palette and run zed: extensions.
  2. Click Install Dev Extension.
  3. Select the cloned zed-dotnet directory.

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.

From the extension registry

Not yet published. Once it is, zed: extensions → search for .NET.


Configuration

All settings go in Zed's settings.json (zed: open settings).

Language server

{
  "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
        }
      }
    }
  }
}

Pin a specific server version instead of tracking the latest:

{
  "lsp": {
    "roslyn": {
      "initialization_options": {
        "package_version": "5.9.0-1.26303.1"
      }
    }
  }
}

Solution-wide diagnostics

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"
        }
      }
    }
  }
}

Code lenses

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 IntelliSense

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.05.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.

Editor defaults for C#

{
  "languages": {
    "C#": {
      "tab_size": 4,
      "formatter": "language_server",
      "format_on_save": "on",
      "inlay_hints": {
        "enabled": true,
        "show_parameter_hints": true,
        "show_type_hints": true
      }
    }
  }
}

Debugging

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 and run tasks

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.

Debugging a single test

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.


Repository layout

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

How the pieces fit together

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.


Development

# 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 test

tests/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 --foreground

Roslyn writes its own logs to the directory passed via --extensionLogDirectory, which the extension places next to the downloaded server.

Updating grammars

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.


Contributing

Issues and pull requests are welcome. Useful references:


Acknowledgements

License

MIT © Roberto Lozada