Skip to content

Razor foundation: Blazor components, Razor Pages and MVC views in the host, the sidecar, VS Code and Zed - #398

Merged
MelbourneDeveloper merged 38 commits into
mainfrom
feature/397-razor-support
Oct 20, 2026
Merged

MelbourneDeveloper merged 38 commits into
mainfrom
feature/397-razor-support

Conversation

@MelbourneDeveloper

@MelbourneDeveloper MelbourneDeveloper commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

TLDR

.razor and .cshtml files become first-class: the C# inside them answers through the SDK generator's projection, markup structure comes from the bundled Razor compiler, editors get an HTML projection, and VS Code and Zed attach to Razor. The branch also carries the website's video search pages and playable social previews.

Details

Spec and plan

  • docs/specs/RAZOR-SPEC.md (new): how the Razor toolchain works; host routing; Razor files as additional documents; the C# projection and its span map; the bundled engine, its three IR capture passes and the markup facts model (typeDiagram); the HTML projection; every feature at Razor positions; Blazor and MVC coverage; F#; editors; degradation; tests.
  • docs/plans/RAZOR-PLAN.md (new): Phase 1 (foundation) and Phase 5 (editors) are complete. Phase 2 is partly done; Phases 3, 4 and 6 are next.

Rust host

  • LangId::Razor for .razor and .cshtml. It has no tree-sitter grammar, and the C# sidecar owns it.
  • For Razor, documentSymbol, foldingRange and linkedEditingRange go to the sidecar (razor.rs, document_symbols::handle_via_sidecar), and selectionRange answers [].
  • < and @ are completion triggers. In C# and F# documents they answer null without reaching a sidecar.
  • Postfix templates (.if, .foreach, …) never expand in Razor files, where the same text can be markup.
  • Razor diagnostics publish under the source sharplsp-razor.
  • sharplsp/razor/htmlProjection takes { textDocument } and answers { text }, or null for a file no project lists.

C# sidecar

  • Razor files are additional documents. Their text syncs in every framework context, including edits replayed after load.
  • The C# projection is the generated document whose #pragma checksum names the file. A verified span map translates positions both ways (RazorProjection, RazorSpanMap).
  • Position queries run in the projection and map back (RazorLocations, RazorResults, RazorRenameEdits, RazorCompletionEdits, which turns using into @using). This covers hover, definition, type definition, declaration, references, highlights, completion, signature help, rename, semantic tokens and diagnostics. Hidden scaffolding is dropped.
  • The Razor compiler is bundled from the build SDK (RazorCompiler.props, MIT, listed in THIRD-PARTY-NOTICES.md). Each project gets its own engine (RazorEngineSettings, WorkspaceRazorFileSystem, RazorTagHelpers).
  • IR capture passes (RazorMarkupCapture, RazorIr) produce the markup facts (RazorMarkup). RazorMarkupSymbols finds the C# symbols behind them.
  • The outline, folds and linked tag names come from the facts (RazorStructure).
  • The HTML projection is built from the IR (RazorHtmlProjection). Component HTML comments are restored from a second parse (RazorHtmlComments). A WHATWG-state tokenizer finds the markup context at a caret (HtmlMarkupContext).
  • RunQueryAsync is one query skeleton shared by documents, projections and markup facts.
  • A Razor file never reports a using as unnecessary (CS8019, IDE0005). Markup resolves components through usings that the generated C# never needs, and an import file's usings serve the files below it.
  • C#-only solutions pay nothing extra. Rename's Razor reference search runs only when a project lists a Razor file. Mapping a location outside the workspace's documents, such as decompiled source, searches only Razor projects for a projection, so no other project compiles to run its generators.

Editors

  • VS Code: syncs razor and aspnetcorerazor files and activates on onLanguage:razor. Markup completion and hover go to the built-in HTML service over the projection (razor-html.ts): the server's items come first, and an HTML hover is used only when the server has none. A new razor end-to-end chunk runs on Ubuntu and Windows.
  • Zed: the server attaches to Razor with language id razor.

Fixture: src/sharplsp/tests/fixtures/razor/ is a real solution with four projects: BlazorApp, Widgets (a Razor class library), PagesApp (Razor Pages and MVC) and Domain (F#). Its own Directory.Build.props and .editorconfig keep the repository's build settings and analyzers out. The end-to-end suites copy it to a temp folder together with the repository's global.json. Without that pin, a newer installed SDK's Razor generator needs a newer Roslyn than the sidecar bundles; #399 tracks that limitation, and [RAZOR-ERRORS] describes it.

Website

  • Every docs film gets a watch page in each language, an embed page, a 1200x630 social card, a video sitemap entry and robots.txt access, per [WEB-DESIGN-MEDIA-DISCOVERY] in DESIGN-SYSTEM.md.
  • Every docs page gets its own description.

lspkit: the pointer moves to main, which ignores the local Deslop index.

Refs #397

How Do The Automated Tests Prove It Works?

Rust end-to-end: razor::test_full_stack_razor_routing_session runs the real host and sidecars over the fixture. It checks that:

  • the server advertises ., < and @ as triggers;
  • hover on caption inside @bind arrives through the projection;
  • the outline of Board.razor reads @page … @code, with Panel as Class nesting Badge, and Add as Method;
  • the folds are (6,9) (7,8) (14,24);
  • linked editing on <Panel returns both tag names, and a self-closing tag returns none;
  • selectionRange answers [];
  • the HTML projection keeps every line's width, blanks @Id, @page and @code {, and keeps <Panel and </Panel>;
  • the projection is null for Program.cs, and < and @ in Program.cs answer null;
  • an edit to Idd publishes CS0103 under sharplsp-razor on line 22, and reverting the edit clears it;
  • the session shuts down cleanly.

Rust unit:

  • razor::tests::*: markup triggers short-circuit only outside Razor; projections answer text or null; without the sidecar, the projection, linked editing and folding requests fail and name themselves.
  • postfix_completion::tests::razor_documents_get_no_postfix_templates: postfix templates never expand in Razor, while the same text in C# offers them.
  • diagnostics::tests::razor_files_publish_under_the_razor_source, tree_sitter_parse::tests::razor_and_cshtml_files_are_razor_without_a_grammar, and the Method and Key kinds in document_symbols.

C# sidecar: these run against the built fixture solution in a real MSBuildWorkspace.

  • RazorCSharpFeatureTests:
    • Hover_answers_csharp_fragments_with_ranges_in_the_razor_file
    • Definition_lands_in_razor_files_from_razor_and_from_csharp
    • References_and_highlights_cover_every_razor_use_and_drop_scaffolding
    • Signature_help_answers_inside_a_razor_call
    • Diagnostics_map_csharp_errors_into_the_razor_file_and_the_solution_scan
    • Completion_in_an_expression_offers_members_with_edits_in_the_razor_file
    • Rename_of_a_code_block_field_edits_every_razor_use_and_prepares_its_range
    • Semantic_tokens_classify_csharp_fragments_at_their_razor_positions
  • Import_files_never_report_their_usings_as_unnecessary and Components_never_report_a_using_their_markup_may_need_as_unnecessary: _Imports.razor, _ViewImports.cshtml and a component using <Virtualize> report no unnecessary using, in the file and in the solution scan. The tag binds through its using, and a C# code-behind file still reports an unused using.
  • RazorStructureTests: folds keep their last line, the outline nests components and members, and start and end tag names edit together.
  • RazorMarkupEngineTests: components bind from the app and its library; attributes and directive parameters carry their descriptors; RZ10012 appears after an edit; names are found at an offset; an edit made before the workspace loads is replayed into both the facts and the projection.
  • RazorCompletionEditsTests: an edit inside a fragment maps back to it; an added using becomes one @using line after the file's own, keeping static and alias spellings and the file's line break; an edit that cannot be placed is dropped.
  • RazorMarkupSymbolsTests: component types, parameter and @bind-X properties, and child content resolve, and @onclick, HTML attributes and the generic @bind set nothing.
  • RazorLocationsTests: a solution without Razor files compiles nothing to look for a projection.
  • RazorHtmlProjectionTests (20 cases) and HtmlMarkupContextTests (36 cases). The tokenizer cases include malformed HTML: after a stray /, a doubled < or a bad </ opening, the next character is reconsumed as the HTML tokenizer does.

These run against in-memory projects (AdhocRazorProject): Razor files as additional documents, MSBuild properties and file metadata in a global analyzer config, and hand-written generator output.

  • RazorProjectionTests:
    • the checksum lookup;
    • hidden, fileless, foreign and out-of-range mappings, which never map;
    • carets at a fragment's end;
    • rename edits only listed Razor files whose span holds the old name.
  • RazorEngineSettingsTests:
    • settings fallbacks and TargetPath metadata;
    • the engine's file system, including missing files;
    • projects and documents without a path;
    • cancellation;
    • an unreadable file answers null and later requests still work.
  • RazorSidecarTests, over the sidecar's real socket: the projection round-trips unsaved text and is null for an unlisted file; the structure requests keep their wire shapes; a malformed payload answers an error and leaves the socket healthy.

VS Code: razor-lsp.test.ts runs on both platforms with these tests:

  • a Razor file opens as Razor, and its unsaved edits reach the server;
  • the server projects Razor as HTML of the same shape;
  • markup completes from the HTML service, and C# from the server;
  • markup hovers come from the HTML service when the server has none;
  • references to a field in Razor land in its Razor file, never in generated code;
  • markup without a projection keeps only the server's answers: an unlisted file gets no HTML items or hover, its projection address reads as empty HTML, and a projection the server refuses is logged and answers nothing.

The lsp chunk (210 tests), the fsharp chunk (122 tests) and the workspace chunk (446 tests) pass with the new middleware.

Zed: the_server_attaches_to_the_csharp_fsharp_and_razor_languages.

Website (desktop and phone): video-discovery.spec.js checks that every docs page shares its film as a playable preview in every language, with unique descriptions. video-watch-pages.spec.js covers the watch pages, embed pages, sitemap and robots.txt.

Coverage of new code: every line the branch adds is hit by a test.

  • C# sidecar and Common: 1530 of 1530 added lines. C# coverage rises to 96.39% over 671 tests, and its threshold ratchets to 95.39%.
  • razor-html.ts: 160 of 160 lines.
  • Rust host: 244 of 244 added lines, from CI's shard coverage.

Real-app check: the host and sidecars were run over a fresh dotnet new blazor app (.NET 10). Every file reports no errors and keeps its shape in the HTML projection. Hover, definition, references, member completion and signature help answer on the template's own code.

…e Razor through the SDK generator's projection

Bundles the SDK's Razor compiler for markup facts, maps hover, definition,
references, highlights, completion, signature help, rename and semantic
tokens between .razor/.cshtml files and their generated C#. Refs #397
…ties out of the Razor fixtures

The root .editorconfig raises every analyzer diagnostic to an error, and
.editorconfig files cascade by folder, so the fixtures' empty
Directory.Build.props did not stop CA1716 failing BlazorApp's build. A
root .editorconfig in the fixture folder does. Refs #397
Every catalogued video gets a watch page in each language, a bare embed
player, a 1200x630 social card, a video sitemap entry and complete
VideoObject data, so Google can index the films and X, LinkedIn and Slack
can play them in link previews. Every docs page gets its own description.
[WEB-DESIGN-MEDIA-DISCOVERY]
…from the sidecar

- LangId::Razor for .razor and .cshtml, with no tree-sitter grammar; the C# sidecar owns it
- documentSymbol, foldingRange and linkedEditingRange for Razor go to the sidecar; selectionRange answers empty
- < and @ completion triggers, answered null outside Razor documents
- Razor diagnostics publish under the source sharplsp-razor
- Sidecar outline, folds and linked tag names from the markup facts and the C# projection

Refs #397
…host foundation

sharplsp/razor/htmlProjection forwards { textDocument } to the C# sidecar and answers
{ text }, or null for a file no project lists. The full-stack Razor session now checks
the projection keeps markup at its positions and blanks Razor.

Refs #397
…cts model as built, and structure as implemented

- The capture passes run at three points, not once at the end of optimization
- Markup facts get a typeDiagram model; they hold no directive tokens or C# fragment spans
- Tag helper discovery covers every compilation reference, project references included
- Document symbols, folds and linked editing are specified as they behave
- The HTML projection restores component HTML comments from a second parse

Refs #397
…nd-to-end chunk

VS Code syncs razor and aspnetcorerazor documents, activates on
onLanguage:razor, and forwards completion and hover in Razor markup to the
built-in HTML language service over the server's HTML projection, following
VS Code's request-forwarding pattern. A razor chunk runs the Razor suite over
the real fixture solution on both platforms. Zed's dev extension attaches to
the Razor language the community Razor extensions define.
[RAZOR-EDITORS] [RAZOR-FEATURES-HTML]
…rkup facts

Find, answer empty when there is nothing, else query, with any exception a failure:
four copies of that skeleton become RunQueryAsync, clearing a Deslop cluster.

Refs #397
- The HTML projection blanks every non-HTML character of a Razor file, keeping offsets
  and newlines, from the compiler's IR; component HTML comments are restored from a
  second parse with component parsing off
- A WHATWG-state tokenizer over the projection finds the markup context at a caret
- The sidecar serves sharplsp/razor/htmlProjection, and the structure handlers are
  registered
- End tag names tolerate whitespace before > and match plain elements case-insensitively

Refs #397
Phase 1 closes with the editor verification of the HTML projection, and
Phase 5 with VS Code sync, HTML forwarding, the razor end-to-end chunk and
Zed's Razor registration, each checked against the test that proves it.
…elper apart by name

Component tags resolve to their types across the app and its class library, parameters
and @bind-x to their properties, a child content tag to its enclosing component's
parameter; @OnClick, HTML attributes and the generic @Bind helper set none.

Refs #397
… coverage

The session dropped its client, which kills the server before LLVM writes its profile:
the Razor routing it exercised measured as uncovered. razor.rs goes from 71% to 99%.

Refs #397
The HTML projection round-trips unsaved text and answers null for an unlisted file; the
structure requests keep their wire shapes and Razor ranges; a malformed payload answers
an error and leaves the socket healthy.

Refs #397
…to the file

The plan's foundation item promised the pending-edit replay for Razor files; nothing
exercised it. The edit now waits for the solution, and both the markup facts and the
C# projection read the edited text once it loads.

Refs #397
An edit inside a C# fragment maps back to it; an added using becomes an @using line after
the file's own, keeping static and alias spellings and the file's line break, never twice;
an edit that cannot be placed is dropped.

Refs #397
Every C# rename, and every rename that starts in F#, searched the whole
solution for references a second time to find Razor uses, even where no
project lists a Razor file. Only a solution with Razor files pays for it now.
[RAZOR-FEATURES-RENAME]
Mapping a location outside the workspace's documents, such as decompiled source, asked
every project for its generated documents, so going to a framework definition compiled
every project and ran its generators, Razor or not. Only Razor projects are searched now.

Refs #397
@MelbourneDeveloper MelbourneDeveloper changed the title Razor support: Blazor components, Razor Pages and MVC views Razor foundation: Blazor components, Razor Pages and MVC views in the host, the sidecar, VS Code and Zed Oct 19, 2026
…built it

A watch page has no file of its own whose date means anything, so its
datePublished and sitemap lastmod took the template's file date, which every
fresh checkout moves. They now carry the video's upload date; every other page
keeps its own. [WEB-DESIGN-MEDIA-DISCOVERY]
…sed documents

- The projection provider joins the extension's subscriptions once the
  client has started, and is disposed when the start fails.
- A closed Razor document's projection is dropped, so projections no
  longer pile up for the whole session.
- The projection's address is built in paths.ts (embeddedUri), the
  extension's one path module [SHARPLSP-ARCHITECTURE-PATHS].
- A failed projection logs a structured event with the URI and no error
  text.

Refs #397
… unreachable ones

- The markup-context tokenizer dispatches on its state and the next
  character. Quoted values close in one place, and the raw-text and
  fallback arms that Advance never reaches are gone.
- An unterminated comment never yields a standalone `<!--` token in the
  IR, so the end-of-file tail case in RazorHtmlComments is removed.
- RazorIr.DirectiveName is tested directly: a directive name with no
  source has no span.
- Seven malformed-HTML caret cases; the plan records 33 tokenizer cases,
  full line coverage of the HTML files, and the editor validation.

Refs #397
… and a stopped client keep the server's answers

An unlisted Razor file gets no HTML items or hover, its projection address reads as
empty HTML, and a projection the server refuses is logged and answers none. The HTML
completion command always resolves to a list, so its undefined fallback goes.
…le system without the fixture

- AdhocRazorProject builds an in-memory project the way MSBuild hands
  one over: Razor files as additional documents, MSBuild properties and
  file metadata in a global analyzer config, and fixed generator output
  in place of the SDK's Razor generator. RazorLocationsTests now uses
  its generator types.
- RazorProjectionTests (6 cases) runs hand-written generator output
  through every kind of line mapping: the checksum lookup; hidden,
  fileless, foreign and out-of-range mappings, which never map; carets
  at a fragment's end; and rename, which edits only listed Razor files
  whose span holds the old name.
- RazorEngineSettingsTests (12 cases) covers the settings fallbacks,
  target-path metadata, the engine's file system and missing files,
  projects and documents without a path, cancellation, and an
  unreadable file that answers null without breaking later requests.
- Code that no input can reach is gone. TypeOf reads a type name's arity
  from its rightmost part and has no dead arms. A projection's Razor
  path is always the one its checksum names. The span map's bound
  already keeps it inside the Razor text. Hover treats a missing
  semantic model as a failure.

Refs #397
…mplates in Razor

- linkedEditingRange and foldingRange fail and name themselves when the
  C# sidecar is missing, instead of answering that nothing folds or
  links.
- Postfix templates never expand in a Razor file, where the same text
  can be markup. [RAZOR-FEATURES-COMPLETION] now says so.

Refs #397
…d a bad end-tag opening

The markup context at a caret now follows the WHATWG tokenizer in three
malformed cases:
- After `/` in a start tag, any character but `>` is reconsumed as the
  start of an attribute, so `<div /x=|` is an attribute value.
- `<<` keeps the second `<` as a tag opening, so `<<div |` is an
  attribute name.
- `</` followed by anything but a letter or `>` opens a bogus comment
  that ends at the next `>`.

Three regression tests failed before the fix and pass unchanged after
it. The tokenizer now has 36 cases and full line coverage.

Refs #397
Both ran the same wrapper around a different DefinitionResolver call:
resolve at the mapped position, then map the location back into the
Razor file. RunLocationQueryAsync is that wrapper, next to the Razor
location mapping, and WorkspaceManager.cs is 26 lines shorter.

Refs #397
The full .NET run measures 96.39% with 671 C# tests, and every line the
Razor branch adds is covered.

Refs #397
@MelbourneDeveloper
MelbourneDeveloper marked this pull request as ready for review October 19, 2026 23:17
@MelbourneDeveloper
MelbourneDeveloper enabled auto-merge (squash) October 19, 2026 23:17
`_Imports.razor` and `_ViewImports.cshtml` hold usings for the files
beside and below them, which their own projections never see. Roslyn
reported every one of them as unnecessary (CS8019), and editors faded
the whole file. MapDiagnostics now drops CS8019 and IDE0005 in import
files. An unused using in a component is still reported there.
NativePaths.IsRazorImport replaces the file system's private check, and
[RAZOR-FEATURES-DIAGNOSTICS] now says this.

Running the host over a fresh `dotnet new blazor` app found this. The
plan records that validation and the debugger chunk's 80 passing cases.

Refs #397
Roslyn judges a Razor file's usings from its projection alone, where
they look unused for two reasons. The generated C# names every component
in full, although the markup resolves `<Virtualize>` and the like through
a using. And an import file's usings serve the files beside and below
it. So a using marked unnecessary there can be one the markup needs.
MapDiagnostics now drops CS8019 and IDE0005 in every Razor file. C#
files, code-behind included, still report them. Unused-using analysis
that understands Razor is future work.

Refs #397
…ch copy

A copy under the temp root has no global.json above it, so CI's newest installed SDK
(10.0.401, past the 10.0.303 pin) built and loaded it, and that SDK's Razor generator
needs a newer Roslyn than the sidecar bundles: no projection, and every Razor hover null.
…copy

A copy under the temp root has no global.json above it, so CI's newest
installed SDK (10.0.401, past the 10.0.303 pin) restored and loaded it.
That SDK's Razor generator needs a newer Roslyn than the sidecar
bundles, so there was no projection and every Razor hover was null.
[RAZOR-ERRORS] now describes the limitation, and #399 tracks loading
the generator with any SDK.

Refs #397
RazorEngineSettings built a Razor project path from MSBuild's TargetPath
by hand. NativePaths.ToRazorProjectPath now has a one-argument overload
that both paths use, so every path string is formed in the path module
[SHARPLSP-ARCHITECTURE-PATHS]. Behaviour is unchanged.

Refs #397
@MelbourneDeveloper
MelbourneDeveloper enabled auto-merge (squash) October 20, 2026 00:13
@MelbourneDeveloper
MelbourneDeveloper merged commit 6408431 into main Oct 20, 2026
109 of 111 checks passed
@MelbourneDeveloper
MelbourneDeveloper deleted the feature/397-razor-support branch October 20, 2026 00:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant