Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,63 @@

A [C#](https://learn.microsoft.com/en-us/dotnet/csharp/) extension for [Zed](https://zed.dev).

## Debugging

The extension provides the [netcoredbg](https://github.com/Samsung/netcoredbg)
debug adapter. The adapter binary is downloaded automatically on first use; to
use your own build instead:

```jsonc
{
"dap": {
"netcoredbg": {
"binary": "/usr/local/bin/netcoredbg"
}
}
}
```

Create `.zed/debug.json` in your project for explicit configurations:

```jsonc
[
{
"label": "Debug console app",
"adapter": "netcoredbg",
"request": "launch",
"program": "$ZED_WORKTREE_ROOT/bin/Debug/net10.0/App.dll",
"cwd": "$ZED_WORKTREE_ROOT",
"stopAtEntry": false,
"justMyCode": true
},
{
"label": "Attach to process",
"adapter": "netcoredbg",
"request": "attach",
"processId": 0
}
]
```

Starting a debug session from a `dotnet run` task uses the bundled `dotnet`
locator: it runs the build first, asks MSBuild for the produced assembly with
`dotnet msbuild -getProperty:TargetPath`, and launches netcoredbg against it.

### Debugging a single test

The locator deliberately does not handle `dotnet test`: the test assembly is a
library driven by VSTest, and launching it directly exits without running
anything. Instead, run the **"dotnet test $ZED_SYMBOL (wait for debugger)"**
task from the gutter button on any test. The test host prints its process id
and waits; start an `attach` configuration with that id and breakpoints inside
the test are hit normally.

## Runnables

Test methods (`[Fact]`, `[Theory]`, `[Test]`, `[TestMethod]`, `[TestCase]`),
test classes (including xUnit classes, which carry no class-level attribute),
and `Main` methods get a run button in the gutter.

## Development

To develop this extension, see the [Developing Extensions](https://zed.dev/docs/extensions/developing-extensions) section of the Zed docs.
81 changes: 81 additions & 0 deletions debug_adapter_schemas/netcoredbg.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "netcoredbg debug configuration",
"description": "Debug configuration for .NET applications, handled by netcoredbg.",
"oneOf": [
{
"type": "object",
"required": ["request", "program"],
"properties": {
"request": {
"type": "string",
"const": "launch",
"description": "Start a new .NET process under the debugger."
},
"program": {
"type": "string",
"description": "Absolute path to the managed assembly to debug, e.g. bin/Debug/net10.0/App.dll."
},
"args": {
"type": "array",
"items": { "type": "string" },
"default": [],
"description": "Command-line arguments passed to the program."
},
"cwd": {
"type": "string",
"description": "Working directory for the debuggee. Defaults to the worktree root."
},
"env": {
"type": "object",
"additionalProperties": { "type": "string" },
"default": {},
"description": "Environment variables set for the debuggee."
},
"stopAtEntry": {
"type": "boolean",
"default": false,
"description": "Break as soon as the entry point is reached."
},
"justMyCode": {
"type": "boolean",
"default": true,
"description": "Restrict stepping and breakpoints to code built from this solution."
},
"enableStepFiltering": {
"type": "boolean",
"default": true,
"description": "Step over property getters, setters, and operators."
},
"console": {
"type": "string",
"enum": ["internalConsole", "integratedTerminal", "externalTerminal"],
"default": "internalConsole",
"description": "Where the debuggee's stdio is attached."
}
},
"additionalProperties": true
},
{
"type": "object",
"required": ["request"],
"properties": {
"request": {
"type": "string",
"const": "attach",
"description": "Attach to an already running .NET process."
},
"processId": {
"type": ["integer", "string"],
"description": "Process id to attach to."
},
"justMyCode": {
"type": "boolean",
"default": true,
"description": "Restrict stepping and breakpoints to code built from this solution."
}
},
"additionalProperties": true
}
]
}
25 changes: 25 additions & 0 deletions extension.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,31 @@ language = "CSharp"
name = "csharp-ls"
language = "CSharp"

[debug_adapters.netcoredbg]
schema_path = "debug_adapter_schemas/netcoredbg.json"

# Turns a `dotnet run` task into a build-then-debug scenario.
[debug_locators.dotnet]

# netcoredbg release archives; GitHub asset downloads redirect to
# objects.githubusercontent.com.
[[capabilities]]
kind = "download_file"
host = "github.com"
path = ["Samsung", "netcoredbg", "releases", "**"]

[[capabilities]]
kind = "download_file"
host = "objects.githubusercontent.com"
path = ["**"]

# Used by the `dotnet` debug locator to ask MSBuild where the build output
# landed (`dotnet msbuild -getProperty:TargetPath`).
[[capabilities]]
kind = "process:exec"
command = "dotnet"
args = ["**"]

[grammars.c_sharp]
repository = "https://github.com/tree-sitter/tree-sitter-c-sharp"
commit = "485f0bae0274ac9114797fc10db6f7034e4086e3"
Expand Down
1 change: 1 addition & 0 deletions languages/csharp/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ code_fence_block_name = "csharp"
grammar = "c_sharp"
path_suffixes = ["cs"]
line_comments = ["// ", "/// "]
debuggers = ["netcoredbg"]
autoclose_before = ";:.,=}])>"
brackets = [
{ start = "{", end = "}", close = true, newline = true },
Expand Down
73 changes: 73 additions & 0 deletions languages/csharp/debugger.scm
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
; Identifiers whose value is worth showing while a debug session is paused.
;
; The `#not-match?` guards drop PascalCase identifiers in positions where a
; type name is as likely as a variable, since evaluating a type produces noise
; rather than a value.

(parameter
name: (identifier) @debug-variable)

(variable_declarator
name: (identifier) @debug-variable)

(declaration_expression
name: (identifier) @debug-variable)

(catch_declaration
name: (identifier) @debug-variable)

(foreach_statement
left: (identifier) @debug-variable)

(assignment_expression
left: (identifier) @debug-variable)

(assignment_expression
left: (member_access_expression) @debug-variable)

(element_access_expression
expression: (identifier) @debug-variable)

(member_access_expression
expression: (identifier) @debug-variable
(#not-match? @debug-variable "^[A-Z]"))

(argument
(identifier) @debug-variable
(#not-match? @debug-variable "^[A-Z]"))

(binary_expression
(identifier) @debug-variable
(#not-match? @debug-variable "^[A-Z]"))

(prefix_unary_expression
(identifier) @debug-variable)

(postfix_unary_expression
(identifier) @debug-variable)

(return_statement
(identifier) @debug-variable)

(interpolation
(identifier) @debug-variable)

(if_statement
condition: (identifier) @debug-variable)

(while_statement
condition: (identifier) @debug-variable)

(switch_statement
value: (identifier) @debug-variable)

(conditional_expression
condition: (identifier) @debug-variable)

(await_expression
(identifier) @debug-variable)

[
(block)
(declaration_list)
] @debug-scope
35 changes: 35 additions & 0 deletions languages/csharp/runnables.scm
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
; A test method: xUnit `[Fact]`/`[Theory]`, NUnit `[Test]`, MSTest `[TestMethod]`.
((method_declaration
(attribute_list
(attribute
name: (identifier) @_attribute))
name: (_) @run)
(#match? @_attribute "^(Fact|Theory|Test|TestMethod|TestCase)$")
(#set! tag csharp-test))

; A test class declared as such: MSTest `[TestClass]`, NUnit `[TestFixture]`.
((class_declaration
(attribute_list
(attribute
name: (identifier) @_attribute))
name: (_) @run)
(#match? @_attribute "^(TestClass|TestFixture)$")
(#set! tag csharp-test-class))

; A test class recognised by its contents. xUnit marks no class-level
; attribute, so the only way to spot one is that it holds a test method.
((class_declaration
name: (_) @run
body: (declaration_list
(method_declaration
(attribute_list
(attribute
name: (identifier) @_attribute)))))
(#match? @_attribute "^(Fact|Theory|Test|TestMethod|TestCase)$")
(#set! tag csharp-test-class))

; The entry point of an executable project.
((method_declaration
name: (_) @run)
(#eq? @run "Main")
(#set! tag csharp-main))
25 changes: 25 additions & 0 deletions languages/csharp/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[
{
"label": "dotnet test $ZED_SYMBOL",
"command": "dotnet",
"args": ["test", "--filter", "FullyQualifiedName~$ZED_SYMBOL"],
"cwd": "$ZED_WORKTREE_ROOT",
"tags": ["csharp-test", "csharp-test-class"]
},
{
"label": "dotnet test $ZED_SYMBOL (wait for debugger)",
"command": "dotnet",
"args": ["test", "--filter", "FullyQualifiedName~$ZED_SYMBOL"],
"env": { "VSTEST_HOST_DEBUG": "1" },
"cwd": "$ZED_WORKTREE_ROOT",
"reveal": "always",
"tags": ["csharp-test", "csharp-test-class"]
},
{
"label": "dotnet run",
"command": "dotnet",
"args": ["run"],
"cwd": "$ZED_DIRNAME",
"tags": ["csharp-main"]
}
]
Loading