Skip to content

Razor review fixes (#399–#407) and Blazor and Razor docs, film and post - #406

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

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

Conversation

@MelbourneDeveloper

@MelbourneDeveloper MelbourneDeveloper commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

TLDR

Razor keeps working when a project's SDK is newer than the server's Roslyn (#399). Completion, signature help and rename now handle unfinished and escaped Razor C# (#400, #401, #402, #404), HTML completion and hover are forwarded only in markup (#405), and the HTML projection blanks every directive attribute and the C# in attribute values (#407). C# and F# semantic highlighting use the standard theme scopes and categories (#403). Blazor gets its docs page, a narrated walkthrough film and an announcement post in English, Japanese and Chinese.

Details

C# sidecar

  • Newer SDKs: Razor/RazorGenerator.cs (new) is wired into RoslynProjectLoading.WithNew.
    • At project load, it swaps out an SDK Razor compiler that references a newer Microsoft.CodeAnalysis than the sidecar runs. The bundled copy and its two companion files take its place, and a warning names both Roslyn versions.
    • SDK 10.0.401's compiler references Roslyn 5.9; the sidecar runs 5.6. Before this fix, Razor C# features went dark for the whole session.
    • Builds still use the SDK's own generator ([RAZOR-ARCHITECTURE-GENERATOR]).
  • Unfinished expressions: Razor/RazorCompletionRecovery.cs (new) handles completion inside an expression the Razor compiler cannot finish: @scores., an @( missing its ), and calls or lambdas inside it.
    • It asks a copy of the file whose expression is finished at the caret. Roslyn's parser supplies the closers, innermost first.
    • RazorSpanMap.Without and RazorProjection.ReadIn read the copy's projection back in the original file ([RAZOR-FEATURES-COMPLETION]).
  • Signature help: SignatureHelpResolver offers no signatures for a call whose opening parenthesis has no Razor mapping, such as the generated AddContent and AddAttribute (Razor signature help exposes generated RenderTreeBuilder calls #400).
  • Rename: RazorRenameEdits compares a mapped span as a parsed identifier's ValueText, so @class gets its edits (Razor rename drops escaped identifiers such as @class #402).
  • HTML forwarding (Razor C# completion in VS Code appends word suggestions from the HTML projection #405): the HTML projection request now carries the caret. The sidecar answers it only where the compiler owns markup, so VS Code (razor-html.ts) forwards completion and hover to its HTML service in markup alone. C# completion lists no longer carry word suggestions taken from the projection.
  • HTML projection (Razor HTML projection retains directive attributes and C# attribute values #407): RazorHtmlProjection blanks every directive attribute and the C# in HTML attribute values, while HTML names, quotes and literal text stay.
    • This covers @onclick, @bind:event, and a minimized @onclick:stopPropagation.
    • A directive's children are never copied back as HTML.
    • The compiler gives a minimized directive parameter no span, so its name is read where the attribute before it ends, and only when the text there holds it.

F# sidecar and VS Code highlighting

  • FSharpFeatures.fs maps values, functions, properties, events and constructors to their standard LSP token categories. Value-type constructors stay struct. Spans cover only the symbol token, found by the real lexer.
  • In package.json, plain C# keywords in csharp, razor and aspnetcorerazor map to keyword.other.cs, so public, class and int no longer take the control-flow colour (Use standard theme keyword colors and correct F# semantic categories #403).

Docs, film and post

  • src/website/src/docs/blazor.md (en, ja, zh) covers:
  • src/website/src/blog/blazor-razor-vs-code.md (en, ja, zh) is the announcement post. Its new text-free illustrated cover shows a polished metal @ sculpture with amber fire and cyan-edged glass; all three languages share the artwork with translated alt text. The superseded infographic generator is removed.
  • The narrated blazor film ships as MP4 and WebM (Git LFS), with a poster, a social card and en/ja/zh captions. films/blazor.mjs shoots it against demos/blazor-storefront. typeThroughCompletion is now shared from kit/acts/language.mjs.
  • The docs sidebar lists Blazor & Razor after F#. A watch page now ignores a non-finite ?t= instead of throwing.

Specs, plans and gates

  • RAZOR-SPEC, SHARPLSP-SPEC and DESIGN-SYSTEM describe the new behaviour.
  • RAZOR-PLAN records each item, and RAZOR-REVIEW is the regression review with its red/green evidence.
  • The C# coverage threshold ratchets up to 95.54%, and the Deslop duplication threshold down to 6.48% (main measures 6.478%, this branch 6.458%).

How Do The Automated Tests Prove It Works?

  • RazorNewerSdkTests gives the fixture a newer SDK's Razor compiler. In a fresh sidecar process:

    • hover answers int at @currentCount;
    • Program.cs has no CS0246, and App resolves to App.razor;
    • every Razor project loads the bundled compiler, while a matched SDK keeps its own;
    • an unreadable compiler leaves the project as loaded.

    Both behaviour tests failed before the fix.

  • test_latest_sdk_razor_generator_remains_usable (Rust e2e) uses an unpinned fixture copy, which runners with SDK 10.0.401 load with that SDK. It answers hover and resolves App.

  • RazorCompletionRecoveryTests: @scores., @(scores., @(basket.MinBy(product => product., …product.Pr and …product.)</p> all failed red. They now return Count or Price, with edits at the Razor caret. Closed @( ) and @{ } controls, an unclosed @code block and a brace closer pass too.

  • Razor C# completion in VS Code appends word suggestions from the HTML projection #405: RazorSidecarTests.Positioned_projection_forwards_markup_but_not_csharp_or_its_whitespace and Incomplete_csharp_keeps_html_forwarding_off. The Rust e2e check assert_html_projection_only_in_markup runs over the real Board.razor, and razor-lsp.test.ts asserts that C# completion lists hold no projection words.

  • Razor HTML projection retains directive attributes and C# attribute values #407: RazorHtmlProjectionTests.Projection_query_preserves_html_and_utf16_offsets gains six rows: <button @onclick=… disabled="@(scores.Count > 9)">, a plain disabled="@(…)", minimized @onclick:stopPropagation after a directive, after an HTML attribute and after a quoted attribute, and @bind:event="oninput". The failing rows were red before the fix. The fixture test also asserts that scores.Count, @bind and oninput never reach Board's projection.

  • RazorProjectionTests: a copy's map keeps only each fragment's part before its inserted closers, so carets past them never map.

  • RazorCSharpFeatureTests: signature help inside @( and disabled="@(…)" lists no scaffolding, while real calls still answer. Renaming @class edits every declaration and use.

  • Highlighting: FSharpSemanticHighlightingTests checks real-project F# categories with exact spans. semantic-highlighting.test.ts checks that plain and control C# keywords use separate standard scopes, with no hard-coded colours.

  • Website (Playwright, desktop plus two phone projects; 901 tests across the affected specs, all passing):

    • docs-nav: the page sits in "Write code" after F#.
    • site-layout: the ja/zh page and post keep the English structure and links.
    • site-videos: the walkthrough plays in every language.
    • video-watch-pages and video-discovery: watch pages and metadata match the real file.
    • video-playback: the film decodes, seeks and resumes.
    • screenshots and mobile-nav cover the new page.
    • "a watch page ignores a ?t= that is not a finite number of seconds, without an error" failed with the currentTime page error before the guard.
  • Local CI legs:

    • Lint passes: Rust, Zed, CSharpier, Prettier, ESLint, tsc, spec citations, and the .NET warnings-as-errors build with 0 warnings.
    • Deslop, audit, Zed, Rider, tooling and the website all pass.
    • .NET passes on the final head: C# 716/716, F# 447/447 and Common 125/125. C# coverage is 96.54%, which ratchets its threshold to 95.54%; every line of the changed projection and IR files is covered.
    • VS Code passes locally on macOS, every chunk instrumented: razor 6, lsp 211, fsharp 122, workspace 446, debug 48, debug-breakpoints 62, debug-inspection 34, debug-tests 80, testexplorer 121, testexplorer-frameworks 51, testexplorer-mtp 19, testexplorer-mtp-runners 13, testexplorer-mtp-parity 16 and profiler 323, with 0 failures. realrepo is left to CI because it clones the corpus. The coverage gate over all 14 chunks reads 96.86% (threshold 96.96%, with its 1-point tolerance).
    • The profiler chunk's first local run failed one test, the FsOrderLedger joint soak's "the burn loop is hot", while the machine's load average was 6. The unchanged rerun passed all 323.
    • The Rust legs are re-checked in this PR's pipeline.

Closes #399, closes #400, closes #401, closes #402, closes #403, closes #404, closes #405, closes #407. Refs #397.

The replacement cover passed 18 desktop/phone layout and localization checks, and its rendered desktop and phone headers were visually verified.

Final validation: CI run 39368330371 passed all 52 jobs on head 925b8962; PR merged as ab8e713d. Both profiler chunks passed unchanged. The pre-existing F# hidden-sequence-point attribution defect found during investigation is tracked with a real-engine reproduction in lspkit#18.

…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
…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
@MelbourneDeveloper MelbourneDeveloper changed the title Fix Razor editing regressions and standard C#/F# highlighting Razor review fixes (#399–#404) and Blazor and Razor docs, film and post Oct 20, 2026
@MelbourneDeveloper
MelbourneDeveloper marked this pull request as ready for review October 20, 2026 03:29
@MelbourneDeveloper
MelbourneDeveloper enabled auto-merge (squash) October 20, 2026 03:29
…te values (#407)

The C# in an HTML attribute value and every directive attribute is now blank in the
projection, while HTML names, quotes and literal text stay. A directive attribute's
children are never copied back as HTML, so a binding parameter's value no longer
reappears. The compiler gives a minimized directive parameter no span, so its name is
read where the attribute before it ends, only when the text there holds it.

The C# sidecar coverage threshold ratchets to 95.54%.
@MelbourneDeveloper
MelbourneDeveloper enabled auto-merge (squash) October 20, 2026 03:30
@MelbourneDeveloper MelbourneDeveloper changed the title Razor review fixes (#399–#404) and Blazor and Razor docs, film and post Razor review fixes (#399–#407) and Blazor and Razor docs, film and post Oct 20, 2026
@MelbourneDeveloper
MelbourneDeveloper enabled auto-merge (squash) October 20, 2026 04:02
@MelbourneDeveloper
MelbourneDeveloper merged commit ab8e713 into main Oct 20, 2026
59 checks passed
@MelbourneDeveloper
MelbourneDeveloper deleted the feature/397-razor-support branch October 20, 2026 04:35
MelbourneDeveloper added a commit to Nimblesite/lspkit that referenced this pull request Oct 20, 2026
#19)

## TLDR
A CPU sample in code the compiler hid, such as an F# `match`'s decision
tree, now keeps its method's file and takes the visible line before it.
A function no longer splits into a located entry and an unlocated twin
(#18).

## Details
- **`Symbolizer.cs`:** `FrameTable.Located` keeps TraceEvent's
`file:line` when the line is visible. It asks the PDB instead when
TraceEvent reports line 0, `SequencePoint.HiddenLine`, or no source at
all (a prolog, IL −1). The answer is the method's nearest visible
sequence point at or before the IL offset, or its first visible point
when none comes before. A method with no visible point, such as a
compiler-generated closure or a union constructor, stays unlocated as
before. `Symbolizer` and `FrameTable` now own one `SourceLocator` for
the whole recording, and `CpuCommand` disposes it.
- **`SourceLocator.cs`:** the new `Line(module, methodToken, ilOffset)`
reuses the per-module PDB cache that memory locations already use.
`MethodPoints` returns nothing for a token that names no method in the
PDB.
- **Fixture:** `HotLeakAppFs/Matching.fs` adds `Matching.classify`, a
non-inlined match over a struct active pattern whose tests compile to
hidden sequence points. The line protocol gains `classify <count>`.
Program.fs keeps its line numbers, so every existing line constant still
holds.
- **Spec and plan:** [PROF-DOTNET-CPU-SYMBOLS] now states the rule. The
plan's "a hidden sequence point is reported without a location" now
points to this defect, which is ticked.

## How Do The Automated Tests Prove It Works?
- `cpu_samples_in_fsharp_match_code_keep_their_file` (F# e2e) profiles
three `classify 50000000` rounds.
- **Before the fix it failed:** `HotLeakAppFs.Matching.classify(int32)`
came back twice. One entry held 948 samples (63.4%) in Matching.fs; the
other was an unlocated twin of 179 samples (12.0%), all self time in the
decision tree.
- **After the fix it passes, unchanged:** one entry, in Matching.fs,
with its line and a hot line inside the match (lines 17–22).
- Against SharpLsp's FsPricingEngine (`run parse`, 12 s),
`Csv.parseLine` went from 1592 located and 2507 unlocated samples to
4708 located and none unlocated. Under `run mixed`, every named function
is located. That split is what failed SharpLsp's Windows profiler suite
on Nimblesite/SharpLsp#406 (run 39364686929).
- Local CI passes:
  - `make fmt CHECK=1`
  - profiler UI build and `test:paths`
  - `make lint`
- `make test`: 413/413 tests; lsp-profiler at 93.29% and
lsp-profiler-dotnet at 92.60% against their 90% thresholds
  - `make build`

Closes #18.
MelbourneDeveloper added a commit that referenced this pull request Oct 20, 2026
…op is sampled where it runs (#409)

## TLDR
Fixes two defects that made the F# profiler suites flaky (#408). An F#
CPU sample in a `match`'s decision tree now keeps its method's file, and
FsOrderLedger's burn loop is now sampled where it actually runs.

## Details
- **`lspkit` e6e4172 → 3cd5896 (Nimblesite/lspkit#19, fixes
lspkit#18):**
- The .NET engine's `Symbolizer` gives a frame on a hidden sequence
point its method's nearest visible sequence point, or the method's
first, read from the PDB through `SourceLocator`. A method with no
visible point, such as a compiler-generated closure, stays unlocated.
- Before this, such frames lost their file, so a function split into a
located hot function and an unlocated twin. That was 61% of
FsPricingEngine's `Csv.parseLine`, and it failed the Windows profiler
chunk on #406 (run 39364686929).
  - lspkit's spec ([PROF-DOTNET-CPU-SYMBOLS]) and plan state the rule.
- Its fixture `HotLeakAppFs` gains `Matching.fs` and a `classify`
command. The Program.fs lines SharpLsp's `profiler-fsharp-e2e` pins (up
to 97) are unchanged.
- **`FsOrderLedger` `Gate.fs` and `Program.fs`:** the burn tick no
longer takes the process-wide gate.
  - It only reads (`priceAll` snapshots under the ledger's own lock).
- EventPipe's sampler stops a thread at its next safe point, so the
uncontended lock call took 98% of the burn thread's samples.
dotnet-counters showed 0 lock contentions per second.
- That left `priceAll` at the 2% hot-function threshold, and "the burn
loop is hot" failed two runs in three on macOS.
- Line counts are unchanged, so `PRICE_ALL` (Ledger.fs 63–70) still
holds.
- **`docs/plans/PROFILER-PLAN.md`:** both defects join the F#
real-program list.

## How Do The Automated Tests Prove It Works?
- **lspkit's `cpu_samples_in_fsharp_match_code_keep_their_file`:**
before the fix, `Matching.classify` came back twice, with 948 samples
located and 179 unlocated. After the fix it passes unchanged. lspkit CI
is green on the PR (run 39371497743) and on main (run 39371510065).
- **FsPricingEngine through this branch's published engine (`run
parse`):** `Csv.parseLine` went from 1592 located and 2507 unlocated
samples to 3271 located and none unlocated.
- **"the burn loop is the hotspot while memory tracks the same
program"** (`profiler-fsharp-soak-cpu-e2e`):
- Unchanged test. It failed two of three local runs before the fixture
fix, both on "the burn loop is hot".
  - It passes three of three after it.
- A direct engine trace puts `priceAll` on 100% of the burn thread's
samples, up from 1.6%.
- **The full `profiler` VS Code chunk passes locally on macOS against
this branch: 323/323.** This PR's pipeline re-checks it on Linux and
Windows, where the F# CPU and soak suites assert each named function's
file.

Closes #408.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment