diff --git a/.github/workflows/bump_version.yml b/.github/workflows/bump_version.yml deleted file mode 100644 index 4c348ae..0000000 --- a/.github/workflows/bump_version.yml +++ /dev/null @@ -1,61 +0,0 @@ -# Generated from xtask::workflows::extensions::bump_version within the Zed repository. -# Rebuild with `cargo xtask workflows`. -name: extensions::bump_version -on: - pull_request: - types: - - labeled - push: - branches: - - main - paths-ignore: - - .github/** - workflow_dispatch: {} -jobs: - determine_bump_type: - if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') - runs-on: namespace-profile-2x4-ubuntu-2404 - permissions: {} - steps: - - id: get-bump-type - name: extensions::bump_version::get_bump_type - run: | - if [ "$HAS_MAJOR_LABEL" = "true" ]; then - bump_type="major" - elif [ "$HAS_MINOR_LABEL" = "true" ]; then - bump_type="minor" - else - bump_type="patch" - fi - echo "bump_type=$bump_type" >> $GITHUB_OUTPUT - env: - HAS_MAJOR_LABEL: |- - ${{ (github.event.action == 'labeled' && github.event.label.name == 'major') || - (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'major')) }} - HAS_MINOR_LABEL: |- - ${{ (github.event.action == 'labeled' && github.event.label.name == 'minor') || - (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'minor')) }} - outputs: - bump_type: ${{ steps.get-bump-type.outputs.bump_type }} - call_bump_version: - needs: - - determine_bump_type - if: github.event.action != 'labeled' || needs.determine_bump_type.outputs.bump_type != 'patch' - permissions: - actions: write - contents: write - issues: write - pull-requests: write - uses: zed-industries/zed/.github/workflows/extension_bump.yml@3f5705b98542f597d5ffe4d984636c7e0c68fa11 - secrets: - app-id: ${{ secrets.ZED_ZIPPY_APP_ID }} - app-secret: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }} - with: - bump-type: ${{ needs.determine_bump_type.outputs.bump_type }} - force-bump: ${{ github.event_name != 'push' }} -concurrency: - group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}labels - cancel-in-progress: true -defaults: - run: - shell: bash -euxo pipefail {0} diff --git a/.gitignore b/.gitignore index 4c7bad8..4400a6f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,15 @@ # *.wasm +# Zed's development builds clone declared grammars into `grammars/`. /grammars +# +# Node (grammar tooling) +# + +node_modules/ + # # Rust # diff --git a/BACKLOG.md b/BACKLOG.md new file mode 100644 index 0000000..28f617a --- /dev/null +++ b/BACKLOG.md @@ -0,0 +1,536 @@ +# C# Web Development Backlog + +## Product direction + +Make this the integrated C# web-development extension for Zed. It owns C#, +Razor, .NET project files, and the everyday .NET workflow; existing Zed +extensions continue to own HTML, CSS, JavaScript, TypeScript, and debugging. + +## Decision 0 — Delivery target (DECIDED: fork) + +This work ships as a **fork** of `zed-extensions/csharp`, published as a +distinct extension: id `csharp-plus`, name "C# Plus", crate `zed_csharp_plus`, +forked at upstream commit `88597e1` (v1.2.2). The alternatives — upstreaming +into `zed-extensions/csharp`, or shipping a Razor-only extension alongside it — +were considered and rejected. + +**Why the fork wins.** The product goal is one integrated extension owning C#, +Razor, project files, and the .NET workflow. A Razor-only extension cannot +deliver that: from Milestone 3 onward it would have to launch its own +`Microsoft.CodeAnalysis.LanguageServer`, giving two Roslyn processes over one +solution — double project load, double memory, two divergent views of the same +compilation. Upstreaming avoids that but puts every step behind review latency +this project does not control, on a roadmap whose feasibility (G0) is not yet +established; a maintainer who declines Razor scope at M1, or a proxy binary at +M4, invalidates the plan retroactively. The fork keeps the integrated product +*and* full control of sequencing, and it inherits working OmniSharp, csharp-ls, +NuGet-download and MSBuild code rather than reimplementing it. + +**The cost, stated plainly.** C# Plus is a superset of the upstream extension, +not a companion to it. It registers `.cs`, `.csproj`, `.slnx` and MSBuild files, +so installing both produces duplicate language ownership. Users must uninstall +"C#" and install "C# Plus" — a one-time, documented migration (see M1.4), not a +silent conflict, which keeps faith with the decision below that this project +does not modify a user's installed extensions. The fork also inherits +maintenance of the OmniSharp, `csharp-ls` and NuGet code paths, and takes on a +merge burden against upstream. Both are managed by D0.1 rather than wished away. + +- [x] **D0.1 — Establish fork hygiene before M1 starts** *(done 2026-09-05)* + - `upstream` remote points at `zed-extensions/csharp`; `origin` points at the + fork. (Remote rename done; `origin` is added once the GitHub fork exists.) + - **Upstream sync policy:** fetch and merge `upstream/main` at the start of + every milestone, and before any release. Record the merged upstream commit + in the changelog. Divergence is cheapest to resolve while small; the + files most likely to conflict are `languages/csharp/*.scm`, + `src/language_servers/roslyn.rs`, and `extension.toml`. + - **Attribution:** LICENSE retained unmodified; upstream authors retained in + `extension.toml`; provenance and fork commit recorded in README. Apache-2.0 + requires the notices be preserved and modifications be marked — done, and + it must stay done through every merge. + - **CI is currently dead in the fork.** `.github/workflows/bump_version.yml` + is gated on `repository_owner == 'zed-industries' || 'zed-extensions'`, so + it will never run under the fork's owner. Either adopt the gate to the fork + owner or replace it with a manual version-bump convention. Decide before + M1.0 adds the first real CI job, so there is one workflow story rather than + two. *(DECIDED: replaced with the manual convention — `scripts/bump-version.sh` + + `docs/versioning.md`; the workflow was removed. One workflow story: + `ci.yml`.)* + - **Registry:** publishing requires a PR to `zed-industries/extensions` + adding `csharp-plus` as a new entry. Verify the id is free before M1.5. + *(Verified free 2026-09-05 — registry then contained `csharp` and + `csharp-snippets` only.)* + - Acceptance: a clean `git merge upstream/main` runs green, the licence and + attribution obligations are satisfied, and the version-bump story is one + documented mechanism. + +## Decisions already made + +- Support both ASP.NET Core MVC/Razor Pages (`.cshtml`) and Blazor (`.razor`). +- Deliver in small releases: Razor editing, workflow improvements, experimental + Razor semantics, then stable semantics. +- Use Roslyn as the only supported semantic backend for Razor. OmniSharp and + `csharp-ls` remain C#-only alternatives. +- Guarantee .NET 8 LTS and newer. Legacy ASP.NET/.NET Framework projects may + receive syntax support but are not promised project-aware Razor semantics. +- Syntax editing is on by default. Razor semantics remain opt-in until stable. + See M0.4: the *mechanism* for this is not yet known to exist. +- On unsupported SDK/project/server conditions, preserve syntax editing and + explain the next action; never silently switch language servers. +- Reuse the existing Razor extension's assets only after recording the source, + commit, licence, notices, and any local modifications. +- Do not modify a user's settings or installed extensions. Explain Razor file + association conflicts and provide migration instructions instead. +- Do not add a debugger, application scaffolding, deployment/cloud tooling, or + legacy-framework semantic support in this roadmap. +- Do not collect telemetry or upload project contents. Opening a Razor file + must not itself trigger restore or build work. + +### Platform constraints taken as given + +These are properties of Zed's extension model, not open questions. They are +recorded here because two milestones below were originally written as though +they did not hold. + +- A Zed extension is a WASM plugin. Its entire LSP surface is + `language_server_command`, `language_server_initialization_options`, + `language_server_workspace_configuration`, the two `additional_*` variants, + and `label_for_completion` / `label_for_symbol`. There is no hook to observe, + rewrite, or synthesise LSP traffic, and no concept of a virtual or projected + document. See zed-industries/zed#21133, closed as not planned. +- Consequently, any Razor request/range bridging must run in a **separate proxy + executable** launched by `language_server_command`, which itself spawns + Roslyn and speaks LSP on both sides. See M4.2B. +- Zed only ever opens real files, so such a proxy owns the virtual-document + lifecycle entirely — no virtual document can leak into ordinary editor + workflows unless the proxy emits one. +- Attaching a server to a language is a manifest fact (`languages = [...]` plus + a `language_ids` map). Opting out is a user-side setting + (`"language_servers": ["!roslyn"]`), not an extension-side toggle. +- Extensions cannot override query files shipped in a grammar's own repository + (zed#40532, closed as not planned). Queries under `languages/razor/*.scm` in + this repository do load; upstreaming query fixes to a grammar repo is not a + path. + +## Definition of done + +Every release must build cleanly, include user-facing documentation, preserve +ordinary `.cs` behavior, and have regression coverage proportionate to its +scope — where "coverage" means the harness built in M1.0, not an aspiration. +Razor semantic support cannot be called stable until it passes the matrix in +[Quality gates](#quality-gates). + +Each user-visible release also requires a version bump in `extension.toml` and +`Cargo.toml` (the `bump_version` workflow handles this on merge) and a +publishing PR to `zed-industries/extensions`. Batch M1.x into a single release; +do not ship M3.4 in the same version as M1.5. + +## Decision gates + +The plan has three points where a negative answer changes the roadmap rather +than delaying it. Each has a written else-branch. Do not proceed past a gate +without recording the outcome in this file. + +| Gate | After | Question | Else-branch | +| --- | --- | --- | --- | +| G0 | M0.0 | Can the shipped `roslyn-language-server` package serve Razor at all? | See M0.0's three outcomes | +| G1 | M0.2 | Which band does the best candidate land in? | Adopt / fork and vendor / descope M1 to injection-only | +| G2 | M4.1 | Upstream route accepted within 30 days of a complete M3.3? | Extension-owned proxy route, or stop at experimental | + +Recorded outcomes: **G1 = fork and vendor** (2026-09-05, +`docs/razor-grammar-audit.md`) — no candidate passed Adopt as-is; the chosen +grammar was vendored and fixed to Adopt-band criteria. G0 and G2 pending. + +M3.3 additionally times out on its own: if there is no substantive Zed staff +response within 30 days of posting, treat it as a decline for planning purposes +and start the G2 clock. + +## Milestone 0 — Feasibility, foundation, and compatibility audit + +Goal: answer the questions that decide whether Milestones 3 and 4 exist, then +remove the remaining unknowns before changing user-visible behavior. + +- [ ] **M0.0 — Razor/Roslyn feasibility spike (1–2 days, do this first)** + - Take the `roslyn-language-server.` package this extension already + downloads. Launch it by hand with `--stdio --autoLoadProjects` plus + `--razorSourceGenerator` and `--razorDesignTimePath`, against one Blazor + page and one `.cshtml` page. + - Drive it with a raw LSP client (a script is fine): `initialize`, + `textDocument/didOpen` with language id `aspnetcorerazor`, then + `textDocument/completion` and `textDocument/hover`. + - Context: dotnet/roslyn#82535 establishes that these flags exist but are + undocumented, and there is no maintainer statement that the standalone + `Microsoft.CodeAnalysis.LanguageServer` package serves Razor outside the + VS Code C# extension, which ships the Razor components separately. + - Acceptance: a written outcome, one of — + - **Works.** M3 is real; proceed as planned and record the exact flag + values and component versions in M3.1. + - **Needs components not in the package.** M3.1 becomes "acquire, licence, + and repackage the Razor components", a materially larger and + licence-sensitive job. Re-cost M3 before committing. + - **Does not work.** M3 and M4 collapse to "track upstream". M1 and M2 + become the whole roadmap — an acceptable product outcome. Record it and + move on rather than retrying against newer packages ad hoc. + +- [x] **M0.1 — Inventory the extension's current contract** *(done 2026-09-05 — + `docs/inventory.md`)* + - Document language registrations, server installation/launch behavior, + project tasks, supported platforms, and user settings. + - Capture a baseline for `.cs`, `.csproj`, MSBuild, and `.slnx` behavior. + - Acceptance: a committed table mapping every later backlog item to the + source files and test files it touches. The table is the deliverable; a + prose audit is not. + +- [x] **M0.2 — Audit Razor grammar fitness and licence compatibility** + *(decided 2026-09-05 — G1 outcome: **fork and vendor** + `tris203/tree-sitter-razor` @ `d4664e4` (MIT) into + [joeizang/tree-sitter-razor](https://github.com/joeizang/tree-sitter-razor); three named grammar fixes applied; post-fix + the fork parses 14/14 well-formed corpus files with zero errors and 79/79 + upstream tests. Full numbers, rejected candidates (licence-unverifiable + grammar; query-over-html non-candidate), and known pathological-containment + limits: `docs/razor-grammar-audit.md`.)* + - There is no canonical `tree-sitter-razor`. Candidates are small, + lightly-maintained third-party repositories (noundry/zed-razor, + IbrahimSabriOrene/zed-razor-treesitter, NoisKung/razor-syntex-zed). + Pinning one means adopting its bugs permanently. + - Verify licences for the grammar and every copied/adapted query file; add + required attribution/notices and record the upstream source revision. + - Decide now, in writing, whether this project is willing to maintain a Razor + grammar fork indefinitely. On current evidence it probably will have to. + - Acceptance: licence obligations are explicit, the grammar revision is + reproducible by automation, and the fitness numbers plus the fork decision + are recorded here. + + **Fitness test and thresholds (set before running, not after).** + + Parse each candidate over the M0.3a corpus at a pinned revision and record, + per file: whether the tree contains any `ERROR` or `MISSING` node, and the + share of file bytes covered by `ERROR` nodes. The two halves of the corpus + are judged differently, because a deliberately broken file *should* produce + errors. + + *Core construct subset* — roughly fifteen well-formed files that must parse + cleanly for the extension to be worth shipping at all: implicit expression + (`@model.Name`), explicit expression (`@(a + b)`), `@code` and `@functions` + blocks, `@if` / `@foreach` / `@switch`, the directive set (`@page`, `@model`, + `@using`, `@inject`, `@inherits`, `@attribute`), a component with parameters + (``), a Tag Helper (`asp-for`), `@bind` and `@onclick`, + layouts and `@section`, Razor comments (`@*…*@`), the `@@` escape, an email + address in body text, an `@media` rule inside ` + +

Styled page.

diff --git a/corpus/razor/pathological/at-in-text.cshtml b/corpus/razor/pathological/at-in-text.cshtml new file mode 100644 index 0000000..8e48953 --- /dev/null +++ b/corpus/razor/pathological/at-in-text.cshtml @@ -0,0 +1,8 @@ +@page "/social" + +

Meeting at 5@@3.

+

Email bob@@example.com and alice@@example.org today.

+

Handle: @@developer

+

Almost email: bob@@

+

Stray at: @@

+

Normal text after the stray at-sign.

diff --git a/corpus/razor/pathological/broken-attribute.cshtml b/corpus/razor/pathological/broken-attribute.cshtml new file mode 100644 index 0000000..b69e82b --- /dev/null +++ b/corpus/razor/pathological/broken-attribute.cshtml @@ -0,0 +1,7 @@ +@page "/quote-trouble" + +Logo +Escaped in attribute + +@if (Model.Ready) +{ +
    + @foreach (var item in Model.Items) + { +
  • @item +
+ +after the collapse +@{ +

paragraph in open block

diff --git a/corpus/razor/pathological/dangling-expressions.cshtml b/corpus/razor/pathological/dangling-expressions.cshtml new file mode 100644 index 0000000..5555fd5 --- /dev/null +++ b/corpus/razor/pathological/dangling-expressions.cshtml @@ -0,0 +1,12 @@ +@page "/open-expression" + +

The value is @(Model.Value and the sentence keeps going.

+

@Model.Unterminated

+

Text after the dangling expressions.

+ +@{ + var a = 1; +

Paragraph inside an open code block.

+} + +

Final paragraph.

diff --git a/corpus/razor/pathological/half-typed-component-tag.razor b/corpus/razor/pathological/half-typed-component-tag.razor new file mode 100644 index 0000000..4f0d06a --- /dev/null +++ b/corpus/razor/pathological/half-typed-component-tag.razor @@ -0,0 +1,12 @@ +@page "/new-component" + +

Create

+ + + inner + + + + +

Content after the half-typed tags.

diff --git a/corpus/razor/pathological/mismatched-tags.cshtml b/corpus/razor/pathological/mismatched-tags.cshtml new file mode 100644 index 0000000..e661f31 --- /dev/null +++ b/corpus/razor/pathological/mismatched-tags.cshtml @@ -0,0 +1,18 @@ +@page "/grid" + +
+ + + + +
Col
+
+ +@foreach (var row in Model.Rows) +{ + + @row.First + +} + +

Recovered paragraph after mismatched tags.

diff --git a/corpus/razor/pathological/odd-directives.cshtml b/corpus/razor/pathological/odd-directives.cshtml new file mode 100644 index 0000000..9a85367 --- /dev/null +++ b/corpus/razor/pathological/odd-directives.cshtml @@ -0,0 +1,10 @@ +@page "/odd-directives" + +@whatever +@123 +@@ + +

@Model.Title

+@Tag value in text + +

Final line after odd directives.

diff --git a/corpus/razor/pathological/unterminated-comment.cshtml b/corpus/razor/pathological/unterminated-comment.cshtml new file mode 100644 index 0000000..0612db5 --- /dev/null +++ b/corpus/razor/pathological/unterminated-comment.cshtml @@ -0,0 +1,8 @@ +@page "/comment-tangle" + +@* Comment that never terminates +
+

Inside the unterminated comment

+
+ +

Paragraph after the unterminated comment.

diff --git a/corpus/razor/pathological/unterminated-if-block.cshtml b/corpus/razor/pathological/unterminated-if-block.cshtml new file mode 100644 index 0000000..143524e --- /dev/null +++ b/corpus/razor/pathological/unterminated-if-block.cshtml @@ -0,0 +1,10 @@ +@page "/broken" + +
+ @if (Model.Open) + { +

Started but the closing brace never arrives + + Nested content after the broken block +

+

This paragraph follows the unterminated block and must parse as an element.

diff --git a/corpus/razor/well-formed/at-escapes-and-email.cshtml b/corpus/razor/well-formed/at-escapes-and-email.cshtml new file mode 100644 index 0000000..548dac7 --- /dev/null +++ b/corpus/razor/well-formed/at-escapes-and-email.cshtml @@ -0,0 +1,6 @@ +@page "/pricing" + +

Contact us at support@example.com or sales@example.com.

+

The @@ escape renders a literal at-sign: user@@example.com

+

Prices start at @@5 per seat, up to @@25 for enterprise.

+

Social handles: @@zed_admin and @@dotnet.

diff --git a/corpus/razor/well-formed/bind-and-events.razor b/corpus/razor/well-formed/bind-and-events.razor new file mode 100644 index 0000000..7295a27 --- /dev/null +++ b/corpus/razor/well-formed/bind-and-events.razor @@ -0,0 +1,28 @@ +@page "/settings" +@using System.ComponentModel.DataAnnotations + +

Settings

+ + + + + + + + + +@code { + private bool Enabled { get; set; } + private string Region { get; set; } = "eu"; + + private void Toggle(bool value) + { + Enabled = !value; + } + + private Task Save(System.EventArgs e) => Task.CompletedTask; + private Task SaveQuantity() => Task.CompletedTask; +} diff --git a/corpus/razor/well-formed/code-block.razor b/corpus/razor/well-formed/code-block.razor new file mode 100644 index 0000000..87f45f9 --- /dev/null +++ b/corpus/razor/well-formed/code-block.razor @@ -0,0 +1,24 @@ +@page "/counter" +@using System.ComponentModel.DataAnnotations +@inject CounterState State + +

Counter

+ + + +@code { + private int currentCount = 0; + + [Parameter] + public int StartFrom { get; set; } + + protected override void OnInitialized() + { + currentCount = StartFrom; + } + + private void IncrementCount() + { + currentCount += State.Step; + } +} diff --git a/corpus/razor/well-formed/component-parameters.razor b/corpus/razor/well-formed/component-parameters.razor new file mode 100644 index 0000000..64f54a3 --- /dev/null +++ b/corpus/razor/well-formed/component-parameters.razor @@ -0,0 +1,15 @@ +@page "/orders" + + + + Previous order + + +@code { + private Dictionary extra = new() + { + ["data-testid"] = "order-card" + }; +} + + diff --git a/corpus/razor/well-formed/directives-and-control-flow.cshtml b/corpus/razor/well-formed/directives-and-control-flow.cshtml new file mode 100644 index 0000000..0c38335 --- /dev/null +++ b/corpus/razor/well-formed/directives-and-control-flow.cshtml @@ -0,0 +1,45 @@ +@page "/products" +@model ProductsModel +@using MyApp.Models +@inject IProductRepository Repository +@inherits BasePage +@attribute [Authorize] + +

Products

+ +@foreach (var product in Model.Products) +{ +
+ @product.Name + @product.Price +
+} + +@switch (Model.Category) +{ + case "books": +

Browse books

+ break; + case "toys": +

Browse toys

+ break; + default: +

Browse everything

+ break; +} + +@if (Model.Products.Any()) +{ + @if (Model.Products.Count > 10) + { +

Large catalog

+ } + else + { +

Small catalog

+ } +} +else +{ +

Catalog is empty

+} diff --git a/corpus/razor/well-formed/explicit-expressions.cshtml b/corpus/razor/well-formed/explicit-expressions.cshtml new file mode 100644 index 0000000..2682fac --- /dev/null +++ b/corpus/razor/well-formed/explicit-expressions.cshtml @@ -0,0 +1,13 @@ +@page "/cart" +@model CartModel + +

Subtotal: @(Model.Subtotal.ToString("C"))

+

Tax: @(Model.Subtotal * 0.08m)

+

Total: @(Model.Subtotal + Model.Subtotal * 0.08m)

+

Items: @(Model.Items?.Count ?? 0)

+ +@{ + var label = Model.Items.Count == 1 ? "item" : "items"; +} + +@(Model.Items.Count) @label diff --git a/corpus/razor/well-formed/functions-block.razor b/corpus/razor/well-formed/functions-block.razor new file mode 100644 index 0000000..6383e7a --- /dev/null +++ b/corpus/razor/well-formed/functions-block.razor @@ -0,0 +1,12 @@ +@functions { + public string Greeting { get; set; } = "hello"; + + public void Reset() + { + Greeting = ""; + } +} + +

@Greeting

+ + diff --git a/corpus/razor/well-formed/layouts-and-sections.cshtml b/corpus/razor/well-formed/layouts-and-sections.cshtml new file mode 100644 index 0000000..e178d38 --- /dev/null +++ b/corpus/razor/well-formed/layouts-and-sections.cshtml @@ -0,0 +1,21 @@ +@page "/dashboard" +@model DashboardModel +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers + +

@ViewData["Title"]

+ +
+ @await Html.PartialAsync("_Summary", Model) +
+ +
+ @await Component.InvokeAsync("Stats") +
+ +@section Scripts { + +} + +@section Styles { + +} diff --git a/corpus/razor/well-formed/mixed-statements.cshtml b/corpus/razor/well-formed/mixed-statements.cshtml new file mode 100644 index 0000000..22af460 --- /dev/null +++ b/corpus/razor/well-formed/mixed-statements.cshtml @@ -0,0 +1,35 @@ +@page "/layout-demo" + +

Page content

+ +@{ + Layout = "_ColumnLayout"; + var columns = new[] { "alpha", "beta", "gamma" }; +} + +
+ @for (var i = 0; i < columns.Length; i++) + { +
+ @columns[i] +
+ } +
+ +@while (false) +{ + never +} + +@try +{ + @Model.Risky +} +catch (InvalidOperationException ex) +{ + @ex.Message +} +finally +{ + done +} diff --git a/corpus/razor/well-formed/pages-implicit-expressions.cshtml b/corpus/razor/well-formed/pages-implicit-expressions.cshtml new file mode 100644 index 0000000..fa1fb29 --- /dev/null +++ b/corpus/razor/well-formed/pages-implicit-expressions.cshtml @@ -0,0 +1,27 @@ +@page "/contacts" +@model ContactsModel +@{ + ViewData["Title"] = "Contacts"; +} + +

Contacts

+ + + + + + + @foreach (var contact in Model.Contacts) + { + + + + + } + +
NameEmail
@contact.Name@contact.Email
+ +@if (Model.Contacts.Count == 0) +{ +

No contacts yet.

+} diff --git a/corpus/razor/well-formed/razor-comments.cshtml b/corpus/razor/well-formed/razor-comments.cshtml new file mode 100644 index 0000000..dd88725 --- /dev/null +++ b/corpus/razor/well-formed/razor-comments.cshtml @@ -0,0 +1,17 @@ +@page "/notes" +@model NotesModel + +@* This page shows archived notes *@ +@* Multi-line comment + spanning several lines + @inject Nothing Here *@ +
+ @foreach (var note in Model.Notes) + { + @* Render the note title and body *@ +
+

@note.Title

+

@note.Body

+
+ } +
diff --git a/corpus/razor/well-formed/script-block.cshtml b/corpus/razor/well-formed/script-block.cshtml new file mode 100644 index 0000000..3a0bfe9 --- /dev/null +++ b/corpus/razor/well-formed/script-block.cshtml @@ -0,0 +1,13 @@ +@page "/analytics" + + + +

Analytics @Model.Status

diff --git a/corpus/razor/well-formed/style-media-rule.razor b/corpus/razor/well-formed/style-media-rule.razor new file mode 100644 index 0000000..5333b97 --- /dev/null +++ b/corpus/razor/well-formed/style-media-rule.razor @@ -0,0 +1,24 @@ +@page "/theme" + + + +
Themed content
+ +@code { + public string AccentColor { get; set; } = "#3fb27f"; + public string QuotedValue { get; set; } = "css-safe"; +} diff --git a/corpus/razor/well-formed/tag-helpers.cshtml b/corpus/razor/well-formed/tag-helpers.cshtml new file mode 100644 index 0000000..76794dc --- /dev/null +++ b/corpus/razor/well-formed/tag-helpers.cshtml @@ -0,0 +1,20 @@ +@page "/profile" +@model ProfileModel + +
+
+ + + +
+
+ + + +
+ +
+ + +

Development mode diagnostics enabled

+
diff --git a/docs/inventory.md b/docs/inventory.md new file mode 100644 index 0000000..0fcc258 --- /dev/null +++ b/docs/inventory.md @@ -0,0 +1,71 @@ +# Extension inventory and contract (M0.1) + +Baseline of what the extension registers and how it behaves, and the mapping +from every later backlog item to the source and test files it touches. **This +table is the M0.1 deliverable**; update it as items land. + +## Current contract (baseline at fork point `88597e1`) + +### Language registrations + +| Language | Path suffixes | Grammar | Tasks | Notes | +| --- | --- | --- | --- | --- | +| `CSharp` | `cs` | `tree-sitter-c-sharp` @ `485f0bae` | — | `autoclose_before = ";:.,=}])>"`, `//` and `///` comments, string-aware quote pairs | +| `C# Project File` | `csproj` | `tree-sitter-xml` @ `863dbc38` (xml) | restore/build current project | | +| `MSBuild File` | `proj`, `props`, `targets` | `tree-sitter-xml` (xml) | — | | +| `C# Solution File` | `slnx` | `tree-sitter-xml` (xml) | restore/build current solution | | + +### Language servers (all attached to `CSharp` only, singular `language =` form) + +| Server id | Install | Launch | Settings surface | +| --- | --- | --- | --- | +| `omnisharp` | GitHub release asset `OmniSharp/omnisharp-roslyn` (`omnisharp-{os}-{arch}-net6.0.{tar.gz,zip}`), cached dir `omnisharp-{version}` | binary directly, `-lsp` default arg; `which OmniSharp` short-circuit; `binary.path` override | `binary` only | +| `roslyn` | NuGet `roslyn-language-server.{win,linux,osx}-{x64,arm64}`, cached dir `roslyn-{version}`, self-contained exe or `dotnet exec` fallback (`any` rid) | `--stdio --autoLoadProjects` + user args; `binary.path` override | `binary`; `settings` mapped through `csharp\|` key flattening, inlay hints and reference/test code lenses defaulted | +| `csharp-ls` | NuGet `csharp-ls`, cached dir `csharp-ls-{version}`; `which csharp-ls` short-circuit; requires `dotnet` on PATH otherwise | `dotnet exec .../CSharpLanguageServer.dll` | `binary`; `settings` wrapped as `{"csharp": ...}` | + +Supported platform matrix in code: `{win,linux,osx} × {x64,arm64}` (six RIDs), +plus a `dotnet exec` fallback path (`rid = any`). Extension API surface used: +`language_server_command`, +`language_server_workspace_configuration` (Roslyn, csharp-ls only), +`set_language_server_installation_status`, `download_file`, +`latest_github_release`, `make_file_executable`, `which`, `http_client`. + +### What the extension does NOT do today + +- No tests, no CI of its own (only the generated `bump_version.yml`). +- No Razor anything: no `.razor`/`.cshtml` registration, no grammar, no queries. +- No `.sln` (classic) association. +- No initialization-options hook usage. + +## Backlog-item → file map + +The authoritative mapping; every item below must keep this table accurate. + +| Item | Source files touched | Test/verification files | +| --- | --- | --- | +| D0.1 | `.github/workflows/*`, `scripts/bump-version.sh`, `CHANGELOG.md`, `README.md`, `extension.toml` (authors), `docs/{versioning,upstream-sync,publishing}.md` | CI green; registry verification date in `docs/publishing.md` | +| M0.0 | — (spike; no product code) | `docs/feasibility-spike.md`, `scripts/spike/*.py` artifacts | +| M0.1 | `docs/inventory.md` (this file) | — | +| M0.2 | `extension.toml` (`[grammars.razor]`), `docs/razor-grammar-audit.md` | `harness/` fitness numbers, `corpus/razor/**` | +| M0.3a | `corpus/razor/**` | input to M0.2 + M1.0 harness | +| M0.3b | `fixtures/**`, `fixtures/README.md` | `scripts/verify-fixtures.sh`, release smoke | +| M0.4 | `docs/razor-contract.md`, `BACKLOG.md` (decision), `extension.toml` (per decision) | M1.5 smoke: no server for Razor | +| M1.0 | `harness/**`, `.github/workflows/ci.yml`, `scripts/snapshot.sh` | golden outputs `harness/golden/**`; deliberate-regression check | +| M1.1 | `extension.toml`, `languages/razor/config.toml` | M1.0 harness; M1.5 smoke | +| M1.2 | `languages/razor/{highlights,injections}.scm` | `harness/golden/razor/**` | +| M1.3 | `languages/razor/{brackets,indents,outline,textobjects,runes}.scm`, `languages/razor/config.toml` | `harness/golden/razor/**` (outline/textobjects/indents snapshots) | +| M1.4 | `languages/razor/snippets.json`, `README.md`, `docs/{migration,known-limits}.md` | manual smoke checklist | +| M1.5 | `CHANGELOG.md`, `extension.toml` + `Cargo.toml` versions, `docs/publishing.md`, `docs/release-smoke.md` | macOS/Linux/Windows smoke, dated in changelog | +| M2.1 | `src/language_servers/roslyn.rs` (error messages), `docs/dotnet-workflow.md` | manual: each failure keeps files editable; documented remediation | +| M2.2 | `languages/csproj/tasks.json`, `languages/slnx/tasks.json`, `docs/dotnet-workflow.md` | task definitions against `fixtures/**` (release smoke) | +| M2.3 | `docs/debugging.md` | dated doc decision | +| M2.4 | `docs/project-files.md`, possibly `languages/msbuild/config.toml` | documented associations | +| M3.1 | `src/language_servers/roslyn.rs` + `razor_support.rs`, `docs/roslyn-razor-pinning.md` | reproducible startup script `scripts/spike/*`; version-mismatch error | +| M3.2 | `scripts/probe_razor.py`, `src/language_servers/razor_support.rs` | `docs/razor-probe-results.md` evidence table | +| M3.3 | `docs/upstream-proposal.md` (discussion artifact) | outcome recorded in `BACKLOG.md` G2 | +| M3.4 | `extension.toml` (`language_ids` map), `src/language_servers/roslyn.rs` (initialization options gating), `CHANGELOG.md` (own release) | opt-in/opt-out documented and demonstrated | +| M4.1 | `docs/gate-g2-decision.md` | decision record completeness | +| M4.2A | (conditional; not taken) | — | +| M4.2B | `proxy/**` (separate crate), `src/language_servers/proxy.rs` | `proxy/tests/**` range-mapping + conformance fixtures; kill-test | +| M4.3 | `docs/quality-gates.md`, `CHANGELOG.md` (own release), `src/language_servers/*` default flip | gate matrix executed | +| Quality gates | `docs/{quality-gates,manual-checklist,release-smoke}.md`, `harness/`, `.github/workflows/ci.yml`, `scripts/release-smoke.sh` | the gates themselves | diff --git a/docs/publishing.md b/docs/publishing.md new file mode 100644 index 0000000..02404d5 --- /dev/null +++ b/docs/publishing.md @@ -0,0 +1,30 @@ +# Publishing + +## One-time registry entry + +Publishing requires a PR to [zed-industries/extensions](https://github.com/zed-industries/extensions) +adding `csharp-plus` as a new entry in `extensions.toml`: + +```toml +[csharp-plus] +submodule = "extensions/csharp-plus" +version = "1.3.0" +``` + +The id `csharp-plus` was verified free on **2026-09-05** (the registry then +contained `csharp` and `csharp-snippets` only). Re-verify before opening the +PR; if it has been taken, stop and reconsider the extension id before +releasing — a renamed id after first publish breaks upgrades. + +## Per-release checklist + +1. Upstream merged per [upstream-sync.md](upstream-sync.md); commit recorded in + the changelog. +2. Version bumped via `scripts/bump-version.sh` (see [versioning.md](versioning.md)). +3. CI green on the release commit (harness + builds on all three OSes). +4. Release smoke pass per [release-smoke.md](release-smoke.md) on the + developer's primary OS; the other two OSes covered by CI-executable smoke. +5. Manual checklist from [manual-checklist.md](manual-checklist.md) executed + and dated in the changelog entry. +6. Publishing PR opened; merge order: registry PR after the version commit + lands on `origin/main`. diff --git a/docs/razor-grammar-audit.md b/docs/razor-grammar-audit.md new file mode 100644 index 0000000..1b5170b --- /dev/null +++ b/docs/razor-grammar-audit.md @@ -0,0 +1,116 @@ +# Razor grammar audit and decision (M0.2, gate G1) + +Status: **decided 2026-09-05**. Outcome recorded in `BACKLOG.md`. + +## Candidates considered + +There is no canonical `tree-sitter-razor`. The three candidates named in the +backlog were evaluated at the revisions below: + +| Candidate | Kind | Licence | Verdict | +| --- | --- | --- | --- | +| [tris203/tree-sitter-razor](https://github.com/tris203/tree-sitter-razor) @ `d4664e409caaea12f73c9525484e3cf88b1cf718` | real grammar (embeds `tree-sitter-c-sharp`) | MIT © 2023 Tristan Knight, LICENSE file present, actively maintained (July 2026) | **chosen — forked** | +| [IbrahimSabriOrene/zed-razor-treesitter](https://github.com/IbrahimSabriOrene/zed-razor-treesitter) @ `ed8e4c6e9bb9749352fd6197457f2406f9290a88` | real grammar at repo root | package.json declares MIT but **no LICENSE file exists**; author field is template residue ("Your Name") | rejected — licence obligations cannot be verified; also fails the `@@`-escape core construct (45.77% of file in ERROR) | +| [NoisKung/razor-syntex-zed](https://github.com/NoisKung/razor-syntex-zed) @ `07d7334a8987a6323544c532c7127e3e010d7291` | **not a grammar** — queries over the stock `html` grammar | MIT | not a grammar candidate; it is the G1 "injection-only" else-branch shape, kept as a design reference only | + +## Fitness test method + +`scripts/grammar-fitness.py` parses the M0.3a corpus (`corpus/razor/`) with the +candidate grammar and reports, per file: presence of `ERROR`/`MISSING` nodes, +share of file bytes inside outermost `ERROR` spans, and (pathological half) +whether the error extends to end-of-file. Bands were fixed in the backlog +**before** running. The corpus at audit time: 14 well-formed + 10 pathological +files. + +## Results at the pinned upstream revision (tris203, `d4664e4`) + +- Well-formed half: **3/14 files affected (21.43%)**, worst single `ERROR` + span **99.73%** of a file (`razor-comments.cshtml`) — auto-Reject by the + >50% rule as-is. +- Pathological half: **4/10 (40%)** errors extend to EOF (gate: ≤20%). +- Upstream's own corpus: 78/79 (one pre-existing failure fixed by the fork, + see below). +- Upstream's tests never exercise a `@* … *@` comment inside a braced razor + block; that construct fails on upstream too (verified against pristine + `d4664e4`). + +All three well-formed failures were **specific, nameable constructs**: + +1. A `@* … *@` comment as the first item of any braced razor block + (`@code`, `@if`, `@{ … }`, `@foreach` body) derailed the whole file. +2. `@media`/`@supports`/… in `