Skip to content

Provide IntelliSense for ~/ asset paths in Razor components - #85058

Draft
chsienki wants to merge 4 commits into
dotnet:mainfrom
chsienki:chsienki/asset-path-intellisense
Draft

Provide IntelliSense for ~/ asset paths in Razor components#85058
chsienki wants to merge 4 commits into
dotnet:mainfrom
chsienki:chsienki/asset-path-intellisense

Conversation

@chsienki

@chsienki chsienki commented Aug 26, 2026

Copy link
Copy Markdown
Member

Adds IntelliSense for ~/ asset paths in Razor components, the tooling half of
the compile-time expansion that landed in #84796.

Fixes #84793

What this does

Inside an attribute that opted into asset-path expansion, typing ~/ now offers
the project's static web assets:

<img src="~/im|" />        <!-- images/logo.png, images/hero.png, app.css, ... -->
<Image Source="~/im|" />   <!-- [AssetPath] component parameters too -->

The HTML language server currently fills this gap with relative file path
completions, which are the wrong thing entirely for a value resolved against the
static web assets manifest rather than the file system. Those are now suppressed
once the value starts with ~/.

Where the data comes from

The set of keys that resolve through ResourceAssetCollection arrives as an
AdditionalFile tagged IsStaticWebAssetsManifest, emitted by a companion
change in dotnet/sdk. Riding the workspace snapshot means invalidation is by
document version, with no file watcher and no disk access from OOP.

This PR is inert until that SDK change ships — an absent manifest yields an
empty asset set and no completions — so it can merge independently.

Notes for reviewers

  • The allowlist comes from the project-wide tag helper set, not the
    document's.
    The carriers produced from [AcceptsAssetPath] live on a type in
    the runtime's namespace, so scoping them by the document's @using directives
    would let completion and compilation disagree about which attributes opted in.
    ComponentTildePathPass reads the full set for the same reason.
  • Two gates keep the offer honest. The producer that discovers
    [AcceptsAssetPath] is registered from Razor 3.0 while expansion only happens
    from 11.0, so a project can hold the opt-in metadata with nothing rewritten.
    Note RazorLanguageVersion.Latest is still 9.0, so today this only lights up
    under preview. Mixed literal/expression values are excluded too, matching the
    pass.
  • Trigger characters. ~ and / are added to the Razor set. Both already
    reach the client's registration through the C# set, so what the editor triggers
    on is unchanged; this only widens what is routed to the Razor providers, all of
    which early-out. Nothing pinned these sets before, so the last commit adds a
    test that does — worth a look, since / now reaches the providers on every
    closing tag.
  • Explicit TextEdit on items. Asset keys contain / and ., so the
    editor's word-boundary heuristic would otherwise replace only the last segment
    of a partially typed path.

Testing

AssetPathCompletionItemProviderTest covers the HTML allowlist, [AssetPath]
parameters, non-opted-in element/attribute, cursor inside the ~/ prefix, cursor
in the attribute name, empty asset set, mixed content, legacy documents, and the
language-version gate. StaticWebAssetsManifestReaderTest includes verbatim real
SDK output so a shape change there fails loudly rather than silently costing
completions.

Green locally: Remote.Razor 519/519, Workspaces 423/423,
VisualStudioCode.RazorExtension 1525/1525.

Each of the three commits builds and tests independently.

Microsoft Reviewers: Open in CodeFlow

chsienki and others added 3 commits August 26, 2026 15:02
Razor tooling has no awareness of static web assets. The SDK flows the set of
keys that resolve through ResourceAssetCollection as an additional file tagged
with IsStaticWebAssetsManifest, which is what makes this cheap to keep current:
the manifest is part of the workspace snapshot, so a rebuild that changes it
produces a new document version and invalidates the cached parse without a file
watcher or any disk access from OOP.

A manifest that cannot be understood costs completions rather than failing the
request that asked for them, so a newer schema version or a file caught midway
through being written degrades quietly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a8730b9d-cc1b-4c5d-b22e-b444c2917af2
ComponentTildePathPass rewrites a '~/' literal into Assets["..."] wherever an
(element, attribute) pair opted in via [AcceptsAssetPath] or a component
parameter is marked [AssetPath]. Nothing offered those paths, and the HTML
language server filled the gap with relative file path completions, which are
the wrong thing entirely for a value resolved against a manifest rather than the
file system. Those items are now suppressed for an opted-in '~/' value.

The allowlist comes from the project-wide tag helper set rather than the
document's. The carriers produced from [AcceptsAssetPath] live on a type in the
runtime's namespace, so scoping them by the document's @using directives would
let completion and compilation disagree about which attributes opted in.

Two gates keep the offer honest. The tag helper producer that discovers
[AcceptsAssetPath] is registered from Razor 3.0 while expansion only happens
from 11.0, so a project can hold the opt-in metadata and still have nothing
rewritten. A value mixing a literal with a C# expression never expands either.

Resolving the project's asset data is gated behind a textual check for '~/',
because completion runs on most keystrokes and this data is project-scoped.

Items carry an explicit TextEdit: asset keys contain '/' and '.', so the
editor's word-boundary heuristic would replace only the last path segment.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a8730b9d-cc1b-4c5d-b22e-b444c2917af2
Without these, the asset path list only appears on an explicit Ctrl+Space,
because the Razor trigger set is limited to '@', '<', ':' and space.

Both characters already reach the editor's registration through the C# set, so
the set of characters the client triggers on is unchanged; this only widens
which triggers are routed to the Razor providers. Providers that don't care
return immediately.

The accompanying test pins all three trigger sets. They flow straight into the
LSP registration, where widening one changes how often the editor asks for
completions across every Razor document and narrowing one silently stops a
feature from ever being offered while typing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a8730b9d-cc1b-4c5d-b22e-b444c2917af2
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 2 pipeline(s).
There may be pipelines that require an authorized user to comment /azp run to run.

The file-kind check moved into AssetPathCompletionFacts.IsSupported, leaving
nothing in this file that needs Microsoft.AspNetCore.Razor.Language. IDE0005 is
an error under the correctness leg's RoslynEnforceCodeStyle build.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a8730b9d-cc1b-4c5d-b22e-b444c2917af2
@chsienki

Copy link
Copy Markdown
Member Author

/pr-validation

@github-actions

Copy link
Copy Markdown
Contributor

View PR Validation Run triggered by @chsienki

Parameters
  • Validation Type: pr-val
  • Pipeline ID: 8972
  • Pipeline Version: main
  • PR Number: 85058
  • Commit SHA: 6c599acffafb80d545c68800f29ea99f62af5929
  • Source Branch: chsienki/asset-path-intellisense
  • Target Branch: main
  • Build ID: 15111928


namespace Microsoft.CodeAnalysis.Remote.Razor.Completion;

public class AssetPathCompletionItemProviderTest : RazorToolingIntegrationTestBase

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than, or perhaps in addition to, these provider specific unit tests, it would be good to have some end-to-end tests in CohostDocumentCompletionEndpointTest so its easy to see that this all works together as a complete system.

{
TestCode testCode = """<img src="~$$/" />""";

Assert.Empty(GetCompletionItems(testCode));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see why we couldn't offer completion here? Seems like it would be helpful. I also don't see a test for an explicit invocation in an empty attribute, but completion in there would be helpful too.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Provide IntelliSense for ~/ asset paths in Razor components

2 participants