- C# 99%
- Makefile 1%
|
|
||
|---|---|---|
| .forgejo/workflows | ||
| packaging/mason | ||
| server | ||
| server.tests | ||
| .editorconfig | ||
| .gitattributes | ||
| .gitignore | ||
| Makefile | ||
| nuget.config | ||
| README.md | ||
legacy-razor-ls
A language server for classic ASP.NET MVC 5 (System.Web) Razor views - hover, completion, document-highlight, and live diagnostics over LSP. Editor-agnostic: any LSP client can drive it. (Neovim: legacy-razor.nvim.)
@model BalanceHistoryViewModel
...
@Model.TodayBalance -- hover -> "Currency BalanceHistoryViewModel.TodayBalance { get; set; }"
@Model. -- completion -> AccountHistoryItems, TodayBalance, FromDate, ...
Why
"Razor" is really two unrelated stacks that share only a file extension:
- Classic (this):
System.Web.Mvc.WebViewPage<T>, compiled bySystem.Web.Razorv3, configured viaViews/web.config- .NET Framework, every MVC 5 app. - Core:
RazorPage<T>, the Razor SDK,_ViewImports.cshtml.
Every modern Razor tool - the Roslyn language server's co-hosted Razor engine,
rzls, VS Code's C# extension - only understands the Core stack. Point one at
a classic WebViewPage<T> view and it resolves nothing: not @model, not the base
type, not the web.config imports. The one tool that ever handled classic Razor was
full Visual Studio, through in-process "contained language" machinery welded into
the IDE - no process boundary, nothing an external editor can attach to.
So a classic view in any LSP-based editor gets nothing. This fills that gap, and
crucially it does not reimplement Razor or C# analysis - it hosts the real
production machinery (System.Web.Razor generates the C#, Roslyn analyzes it) and
adds only the glue:
- Generate the view's C# with the real
System.Web.Razorv3 pipeline (MvcWebPageRazorHost, so@model/@Html/web.configimports all resolve), in design-time mode so every code region is emitted verbatim. - Host it as a Roslyn
Documentcompiled against the app'sbin/. - Map positions both ways through Razor's design-time line mappings, which
record the exact source and generated columns of each code region - so a
cursor in the
.cshtmlbecomes a position in the generated C#, and Roslyn's answers map back to.cshtml. - Serve it over LSP so any editor's client and completion engine consume it.
What it does
Statuses: done = implemented + tested, partial = works but scoped, planned = on the roadmap.
| Feature | Status | Notes |
|---|---|---|
| Diagnostics | done | Live - Roslyn semantics (CS****) + Razor parse errors (RZ1000). |
| Hover | done | Roslyn QuickInfo on @Model.X, helpers, locals. |
| Completion | done | Members after .; no completionItem/resolve yet. |
| Document highlight | done | Occurrences of the symbol under the cursor. |
| Go-to-definition | partial | Within-view only; @Model.X resolves to bin metadata until app sources load. |
| Find references | partial | Within-view only, same limit. |
| Signature help | planned | Needs a manual semantic-model impl; no app sources required. |
| Rename | planned | Reuses find-refs; within-view until app sources land. |
| Code actions | planned | Roslyn code-fix providers → map edits back to .cshtml. |
| Semantic tokens | planned | Accurate semantic coloring of the C# regions. |
| App C# sources in workspace | planned | The flagship - cross-file goto-def/refs/rename into ViewModels. |
| Request cancellation / async | planned | $/cancelRequest; don't block the read loop. |
Build
dotnet build -c Release server/LegacyRazor.Server.csproj
Requires the .NET SDK to build; the output targets net472 and runs on Windows with the preinstalled .NET Framework 4.x. Prebuilt binaries ship on Releases.
Launch contract
The editor integration API is a single process per app root, speaking LSP over stdio:
LegacyRazor.Server <app-root> --lsp
<app-root> is the classic app's root (the directory with Global.asax /
Views/web.config). --version prints the package version. The app must have been
built (bin/ populated) so views resolve types.
Editor integration
- Neovim - legacy-razor.nvim
(launches the server, routes
.cshtmlbuffers, adds a whole-app compile check). - Any LSP client - start the process with the launch contract above and point
it at
.cshtmlbuffers under a classic app root.
Architecture
LegacyRazor.Server (net472) server/
├─ RoslynHost design-time Razor gen +
│ ViewSession Roslyn workspace, region map,
│ hover / complete / highlight / definition / references / diagnostics
├─ References .NET Framework + facades + app bin/
├─ WebConfig Views/web.config imports + base type
├─ RazorGen CodeDOM -> C# text
├─ Protocol the 0-based position DTOs
└─ LspServer JSON-RPC framing + handlers (Program launches it)
Development
make test # xUnit engine suite (dotnet test)
make fmt # dotnet format (./.editorconfig)
make fmt-check # dotnet format --verify-no-changes (CI)
The engine tests are self-contained: they use a framework type as the model
(@model System.String) so no fixture assembly needs compiling.
Limitations
- Go-to-definition / find-references are within-view until the app's
.cssources are loaded into the workspace. - Hovering the bare
Modelkeyword (not a member) may be empty; members work. - Classic Razor only; ASP.NET Core Razor is already served by rzls/Roslyn.
- UTF-16 column edge cases in non-ASCII lines are approximated.