diff --git a/eng/targets/Services.props b/eng/targets/Services.props
index e4ce786f254..61ba0e77547 100644
--- a/eng/targets/Services.props
+++ b/eng/targets/Services.props
@@ -25,6 +25,7 @@
+
diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AbstractRazorDelegatingEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AbstractRazorDelegatingEndpoint.cs
index dfef778fe71..c84789d378c 100644
--- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AbstractRazorDelegatingEndpoint.cs
+++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AbstractRazorDelegatingEndpoint.cs
@@ -113,12 +113,14 @@ protected virtual Task HandleDelegatedResponseAsync(TResponse delegat
return default;
}
- var positionInfo = await DocumentPositionInfoStrategy.TryGetPositionInfoAsync(_documentMappingService, documentContext, request.Position, cancellationToken).ConfigureAwait(false);
- if (positionInfo is null)
+ var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
+ if (!codeDocument.Source.Text.TryGetAbsoluteIndex(request.Position, out var absoluteIndex))
{
return default;
}
+ var positionInfo = DocumentPositionInfoStrategy.GetPositionInfo(_documentMappingService, codeDocument, absoluteIndex);
+
var response = await TryHandleAsync(request, requestContext, positionInfo, cancellationToken).ConfigureAwait(false);
if (response is not null && response is not ISumType { Value: null })
{
@@ -141,7 +143,6 @@ protected virtual Task HandleDelegatedResponseAsync(TResponse delegat
// Sometimes Html can actually be mapped to C#, like for example component attributes, which map to
// C# properties, even though they appear entirely in a Html context. Since remapping is pretty cheap
// it's easier to just try mapping, and see what happens, rather than checking for specific syntax nodes.
- var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
if (_documentMappingService.TryMapToGeneratedDocumentPosition(codeDocument.GetCSharpDocument(), positionInfo.HostDocumentIndex, out Position? csharpPosition, out _))
{
// We're just gonna pretend this mapped perfectly normally onto C#. Moving this logic to the actual position info
diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AutoInsert/PreferHtmlInAttributeValuesDocumentPositionStrategy.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AutoInsert/PreferHtmlInAttributeValuesDocumentPositionStrategy.cs
deleted file mode 100644
index 8df88d48145..00000000000
--- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AutoInsert/PreferHtmlInAttributeValuesDocumentPositionStrategy.cs
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright (c) .NET Foundation. All rights reserved.
-// Licensed under the MIT license. See License.txt in the project root for license information.
-
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Razor.Language.Syntax;
-using Microsoft.CodeAnalysis.Razor.DocumentMapping;
-using Microsoft.CodeAnalysis.Razor.ProjectSystem;
-using Microsoft.CodeAnalysis.Razor.Protocol;
-using Microsoft.VisualStudio.LanguageServer.Protocol;
-
-namespace Microsoft.AspNetCore.Razor.LanguageServer.AutoInsert;
-
-// The main reason for this service is auto-insert of empty double quotes when a user types
-// equals "=" after Blazor component attribute. We think this is Razor (correctly I guess)
-// and wouldn't forward auto-insert request to HTML in this case. By essentially overriding
-// language info here we allow the request to be sent over to HTML where it will insert empty
-// double-quotes as it would for any other attribute value
-internal class PreferHtmlInAttributeValuesDocumentPositionInfoStrategy : IDocumentPositionInfoStrategy
-{
- public static IDocumentPositionInfoStrategy Instance { get; } = new PreferHtmlInAttributeValuesDocumentPositionInfoStrategy();
-
- public async Task TryGetPositionInfoAsync(IDocumentMappingService documentMappingService, DocumentContext documentContext, Position position, CancellationToken cancellationToken)
- {
- var defaultDocumentPositionInfo = await DefaultDocumentPositionInfoStrategy.Instance.TryGetPositionInfoAsync(documentMappingService, documentContext, position, cancellationToken).ConfigureAwait(false);
- if (defaultDocumentPositionInfo is null)
- {
- return null;
- }
-
- var absolutePosition = defaultDocumentPositionInfo.HostDocumentIndex;
- if (defaultDocumentPositionInfo.LanguageKind != RazorLanguageKind.Razor ||
- absolutePosition < 1)
- {
- return defaultDocumentPositionInfo;
- }
-
- // Get the node at previous position to see if we are after markup tag helper attribute,
- // and more specifically after the EqualsToken of it
- var previousPosition = absolutePosition - 1;
- var owner = await documentContext.GetSyntaxNodeAsync(previousPosition, cancellationToken).ConfigureAwait(false);
- if (owner is MarkupTagHelperAttributeSyntax { EqualsToken: { IsMissing: false } equalsToken } &&
- equalsToken.EndPosition == defaultDocumentPositionInfo.HostDocumentIndex)
- {
- return new DocumentPositionInfo(RazorLanguageKind.Html, defaultDocumentPositionInfo.Position, defaultDocumentPositionInfo.HostDocumentIndex);
- }
-
- return defaultDocumentPositionInfo;
- }
-}
diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/CodeBlockService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/CodeBlockService.cs
index 9cda5df9204..a01cc5ba960 100644
--- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/CodeBlockService.cs
+++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/CodeBlockService.cs
@@ -8,6 +8,7 @@
using Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
using Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;
diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionListProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionListProvider.cs
index 2c8e023b46d..163ec5dad71 100644
--- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionListProvider.cs
+++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionListProvider.cs
@@ -10,6 +10,7 @@
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Syntax;
using Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
+using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Protocol;
diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultDocumentPositionInfoStrategy.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultDocumentPositionInfoStrategy.cs
deleted file mode 100644
index 976829713ad..00000000000
--- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultDocumentPositionInfoStrategy.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (c) .NET Foundation. All rights reserved.
-// Licensed under the MIT license. See License.txt in the project root for license information.
-
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.CodeAnalysis.Razor.DocumentMapping;
-using Microsoft.CodeAnalysis.Razor.ProjectSystem;
-using Microsoft.CodeAnalysis.Text;
-using Microsoft.VisualStudio.LanguageServer.Protocol;
-
-namespace Microsoft.AspNetCore.Razor.LanguageServer;
-
-internal class DefaultDocumentPositionInfoStrategy : IDocumentPositionInfoStrategy
-{
- public static IDocumentPositionInfoStrategy Instance { get; } = new DefaultDocumentPositionInfoStrategy();
-
- public async Task TryGetPositionInfoAsync(
- IDocumentMappingService documentMappingService,
- DocumentContext documentContext,
- Position position,
- CancellationToken cancellationToken)
- {
- var sourceText = await documentContext.GetSourceTextAsync(cancellationToken).ConfigureAwait(false);
- if (!sourceText.TryGetAbsoluteIndex(position, out var absoluteIndex))
- {
- return null;
- }
-
- return await documentMappingService.GetPositionInfoAsync(documentContext, absoluteIndex, cancellationToken).ConfigureAwait(false);
- }
-}
diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Definition/DefinitionEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Definition/DefinitionEndpoint.cs
index 304c7729526..dd22142c0ef 100644
--- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Definition/DefinitionEndpoint.cs
+++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Definition/DefinitionEndpoint.cs
@@ -1,42 +1,38 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
-using System;
-using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using Microsoft.AspNetCore.Razor.Language;
-using Microsoft.AspNetCore.Razor.Language.Syntax;
using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts;
using Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
using Microsoft.AspNetCore.Razor.Threading;
-using Microsoft.CodeAnalysis.CSharp;
-using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
+using Microsoft.CodeAnalysis.Razor.GoToDefinition;
using Microsoft.CodeAnalysis.Razor.Logging;
-using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using DefinitionResult = Microsoft.VisualStudio.LanguageServer.Protocol.SumType<
- Microsoft.VisualStudio.LanguageServer.Protocol.VSInternalLocation,
- Microsoft.VisualStudio.LanguageServer.Protocol.VSInternalLocation[],
+ Microsoft.VisualStudio.LanguageServer.Protocol.Location,
+ Microsoft.VisualStudio.LanguageServer.Protocol.Location[],
Microsoft.VisualStudio.LanguageServer.Protocol.DocumentLink[]>;
-using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range;
-using SyntaxKind = Microsoft.AspNetCore.Razor.Language.SyntaxKind;
namespace Microsoft.AspNetCore.Razor.LanguageServer.Definition;
[RazorLanguageServerEndpoint(Methods.TextDocumentDefinitionName)]
internal sealed class DefinitionEndpoint(
- IRazorComponentSearchEngine componentSearchEngine,
+ IRazorComponentDefinitionService componentDefinitionService,
IDocumentMappingService documentMappingService,
LanguageServerFeatureOptions languageServerFeatureOptions,
IClientConnection clientConnection,
ILoggerFactory loggerFactory)
- : AbstractRazorDelegatingEndpoint(languageServerFeatureOptions, documentMappingService, clientConnection, loggerFactory.GetOrCreateLogger()), ICapabilitiesProvider
+ : AbstractRazorDelegatingEndpoint(
+ languageServerFeatureOptions,
+ documentMappingService,
+ clientConnection,
+ loggerFactory.GetOrCreateLogger()), ICapabilitiesProvider
{
- private readonly IRazorComponentSearchEngine _componentSearchEngine = componentSearchEngine;
+ private readonly IRazorComponentDefinitionService _componentDefinitionService = componentDefinitionService;
private readonly IDocumentMappingService _documentMappingService = documentMappingService;
protected override bool PreferCSharpOverHtmlIfPossible => true;
@@ -50,60 +46,31 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V
serverCapabilities.DefinitionProvider = new DefinitionOptions();
}
- protected async override Task TryHandleAsync(TextDocumentPositionParams request, RazorRequestContext requestContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken)
+ protected async override Task TryHandleAsync(
+ TextDocumentPositionParams request,
+ RazorRequestContext requestContext,
+ DocumentPositionInfo positionInfo,
+ CancellationToken cancellationToken)
{
Logger.LogInformation($"Starting go-to-def endpoint request.");
+
var documentContext = requestContext.DocumentContext;
if (documentContext is null)
{
return null;
}
- if (!FileKinds.IsComponent(documentContext.FileKind))
- {
- Logger.LogInformation($"FileKind '{documentContext.FileKind}' is not a component type.");
- return default;
- }
-
// If single server support is on, then we ignore attributes, as they are better handled by delegating to Roslyn
- var (originTagDescriptor, attributeDescriptor) = await GetOriginTagHelperBindingAsync(documentContext, positionInfo.HostDocumentIndex, SingleServerSupport, Logger, cancellationToken).ConfigureAwait(false);
- if (originTagDescriptor is null)
- {
- Logger.LogInformation($"Origin TagHelper descriptor is null.");
- return default;
- }
-
- var originComponentDocumentSnapshot = await _componentSearchEngine.TryLocateComponentAsync(documentContext.Snapshot, originTagDescriptor).ConfigureAwait(false);
- if (originComponentDocumentSnapshot is null)
- {
- Logger.LogInformation($"Origin TagHelper document snapshot is null.");
- return default;
- }
-
- var originComponentDocumentFilePath = originComponentDocumentSnapshot.FilePath.AssumeNotNull();
-
- Logger.LogInformation($"Definition found at file path: {originComponentDocumentFilePath}");
-
- var range = await GetNavigateRangeAsync(originComponentDocumentSnapshot, attributeDescriptor, cancellationToken).ConfigureAwait(false);
-
- var originComponentUri = new UriBuilder
- {
- Path = originComponentDocumentFilePath,
- Scheme = Uri.UriSchemeFile,
- Host = string.Empty,
- }.Uri;
-
- return new[]
- {
- new VSInternalLocation
- {
- Uri = originComponentUri,
- Range = range,
- },
- };
+ return await _componentDefinitionService
+ .GetDefinitionAsync(documentContext.Snapshot, positionInfo, ignoreAttributes: SingleServerSupport, cancellationToken)
+ .ConfigureAwait(false);
}
- protected override Task CreateDelegatedParamsAsync(TextDocumentPositionParams request, RazorRequestContext requestContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken)
+ protected override Task CreateDelegatedParamsAsync(
+ TextDocumentPositionParams request,
+ RazorRequestContext requestContext,
+ DocumentPositionInfo positionInfo,
+ CancellationToken cancellationToken)
{
var documentContext = requestContext.DocumentContext;
if (documentContext is null)
@@ -117,25 +84,30 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V
positionInfo.LanguageKind));
}
- protected async override Task HandleDelegatedResponseAsync(DefinitionResult? response, TextDocumentPositionParams originalRequest, RazorRequestContext requestContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken)
+ protected async override Task HandleDelegatedResponseAsync(
+ DefinitionResult? response,
+ TextDocumentPositionParams originalRequest,
+ RazorRequestContext requestContext,
+ DocumentPositionInfo positionInfo,
+ CancellationToken cancellationToken)
{
- if (response is null)
+ if (response is not DefinitionResult result)
{
return null;
}
- if (response.Value.TryGetFirst(out var location))
+ if (result.TryGetFirst(out var location))
{
(location.Uri, location.Range) = await _documentMappingService.MapToHostDocumentUriAndRangeAsync(location.Uri, location.Range, cancellationToken).ConfigureAwait(false);
}
- else if (response.Value.TryGetSecond(out var locations))
+ else if (result.TryGetSecond(out var locations))
{
foreach (var loc in locations)
{
(loc.Uri, loc.Range) = await _documentMappingService.MapToHostDocumentUriAndRangeAsync(loc.Uri, loc.Range, cancellationToken).ConfigureAwait(false);
}
}
- else if (response.Value.TryGetThird(out var links))
+ else if (result.TryGetThird(out var links))
{
foreach (var link in links)
{
@@ -146,187 +118,6 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V
}
}
- return response;
- }
-
- internal static async Task<(TagHelperDescriptor?, BoundAttributeDescriptor?)> GetOriginTagHelperBindingAsync(
- DocumentContext documentContext,
- int absoluteIndex,
- bool ignoreAttributes,
- ILogger logger,
- CancellationToken cancellationToken)
- {
- var owner = await documentContext.GetSyntaxNodeAsync(absoluteIndex, cancellationToken).ConfigureAwait(false);
- if (owner is null)
- {
- logger.LogInformation($"Could not locate owner.");
- return (null, null);
- }
-
- var node = owner.FirstAncestorOrSelf(n =>
- n.Kind == SyntaxKind.MarkupTagHelperStartTag ||
- n.Kind == SyntaxKind.MarkupTagHelperEndTag);
- if (node is null)
- {
- logger.LogInformation($"Could not locate ancestor of type MarkupTagHelperStartTag or MarkupTagHelperEndTag.");
- return (null, null);
- }
-
- var name = GetStartOrEndTagName(node);
- if (name is null)
- {
- logger.LogInformation($"Could not retrieve name of start or end tag.");
- return (null, null);
- }
-
- string? propertyName = null;
-
- if (!ignoreAttributes && node is MarkupTagHelperStartTagSyntax startTag)
- {
- // Include attributes where the end index also matches, since GetSyntaxNodeAsync will consider that the start tag but we behave
- // as if the user wants to go to the attribute definition.
- // ie:
- var selectedAttribute = startTag.Attributes.FirstOrDefault(a => a.Span.Contains(absoluteIndex) || a.Span.End == absoluteIndex);
-
- // If we're on an attribute then just validate against the attribute name
- if (selectedAttribute is MarkupTagHelperAttributeSyntax attribute)
- {
- // Normal attribute, ie
- name = attribute.Name;
- propertyName = attribute.TagHelperAttributeInfo.Name;
- }
- else if (selectedAttribute is MarkupMinimizedTagHelperAttributeSyntax minimizedAttribute)
- {
- // Minimized attribute, ie
- name = minimizedAttribute.Name;
- propertyName = minimizedAttribute.TagHelperAttributeInfo.Name;
- }
- }
-
- if (!name.Span.IntersectsWith(absoluteIndex))
- {
- logger.LogInformation($"Tag name or attributes' span does not intersect with location's absolute index ({absoluteIndex}).");
- return (null, null);
- }
-
- if (node.Parent is not MarkupTagHelperElementSyntax tagHelperElement)
- {
- logger.LogInformation($"Parent of start or end tag is not a MarkupTagHelperElement.");
- return (null, null);
- }
-
- if (tagHelperElement.TagHelperInfo?.BindingResult is not TagHelperBinding binding)
- {
- logger.LogInformation($"MarkupTagHelperElement does not contain TagHelperInfo.");
- return (null, null);
- }
-
- var originTagDescriptor = binding.Descriptors.FirstOrDefault(static d => !d.IsAttributeDescriptor());
- if (originTagDescriptor is null)
- {
- logger.LogInformation($"Origin TagHelper descriptor is null.");
- return (null, null);
- }
-
- var attributeDescriptor = (propertyName is not null)
- ? originTagDescriptor.BoundAttributes.FirstOrDefault(a => a.Name?.Equals(propertyName, StringComparison.Ordinal) == true)
- : null;
-
- return (originTagDescriptor, attributeDescriptor);
- }
-
- private static SyntaxNode? GetStartOrEndTagName(SyntaxNode node)
- {
- return node switch
- {
- MarkupTagHelperStartTagSyntax tagHelperStartTag => tagHelperStartTag.Name,
- MarkupTagHelperEndTagSyntax tagHelperEndTag => tagHelperEndTag.Name,
- _ => null
- };
- }
-
- private async Task GetNavigateRangeAsync(IDocumentSnapshot documentSnapshot, BoundAttributeDescriptor? attributeDescriptor, CancellationToken cancellationToken)
- {
- if (attributeDescriptor is not null)
- {
- Logger.LogInformation($"Attempting to get definition from an attribute directly.");
-
- var originCodeDocument = await documentSnapshot.GetGeneratedOutputAsync().ConfigureAwait(false);
- var range = await TryGetPropertyRangeAsync(originCodeDocument, attributeDescriptor.GetPropertyName(), _documentMappingService, Logger, cancellationToken).ConfigureAwait(false);
-
- if (range is not null)
- {
- return range;
- }
- }
-
- // When navigating from a start or end tag, we just take the user to the top of the file.
- // If we were trying to navigate to a property, and we couldn't find it, we can at least take
- // them to the file for the component. If the property was defined in a partial class they can
- // at least then press F7 to go there.
- return VsLspFactory.DefaultRange;
- }
-
- internal static async Task TryGetPropertyRangeAsync(RazorCodeDocument codeDocument, string propertyName, IDocumentMappingService documentMappingService, ILogger logger, CancellationToken cancellationToken)
- {
- // Parse the C# file and find the property that matches the name.
- // We don't worry about parameter attributes here for two main reasons:
- // 1. We don't have symbolic information, so the best we could do would be checking for any
- // attribute named Parameter, regardless of which namespace. It also means we would have
- // to do more checks for all of the various ways that the attribute could be specified
- // (eg fully qualified, aliased, etc.)
- // 2. Since C# doesn't allow multiple properties with the same name, and we're doing a case
- // sensitive search, we know the property we find is the one the user is trying to encode in a
- // tag helper attribute. If they don't have the [Parameter] attribute then the Razor compiler
- // will error, but allowing them to Go To Def on that property regardless, actually helps
- // them fix the error.
- var csharpText = codeDocument.GetCSharpSourceText();
- var syntaxTree = CSharpSyntaxTree.ParseText(csharpText, cancellationToken: cancellationToken);
- var root = await syntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false);
-
- // Since we know how the compiler generates the C# source we can be a little specific here, and avoid
- // long tree walks. If the compiler ever changes how they generate their code, the tests for this will break
- // so we'll know about it.
- if (GetClassDeclaration(root) is { } classDeclaration)
- {
- var property = classDeclaration
- .Members
- .OfType()
- .Where(p => p.Identifier.ValueText.Equals(propertyName, StringComparison.Ordinal))
- .FirstOrDefault();
-
- if (property is null)
- {
- // The property probably exists in a partial class
- logger.LogInformation($"Could not find property in the generated source. Comes from partial?");
- return null;
- }
-
- var range = csharpText.GetRange(property.Identifier.Span);
- if (documentMappingService.TryMapToHostDocumentRange(codeDocument.GetCSharpDocument(), range, out var originalRange))
- {
- return originalRange;
- }
-
- logger.LogInformation($"Property found but couldn't map its location.");
- }
-
- logger.LogInformation($"Generated C# was not in expected shape (CompilationUnit [-> Namespace] -> Class)");
-
- return null;
-
- static ClassDeclarationSyntax? GetClassDeclaration(CodeAnalysis.SyntaxNode root)
- {
- return root switch
- {
- CompilationUnitSyntax unit => unit switch
- {
- { Members: [NamespaceDeclarationSyntax { Members: [ClassDeclarationSyntax c, ..] }, ..] } => c,
- { Members: [ClassDeclarationSyntax c, ..] } => c,
- _ => null,
- },
- _ => null,
- };
- }
+ return result;
}
}
diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Definition/RazorComponentDefinitionService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Definition/RazorComponentDefinitionService.cs
new file mode 100644
index 00000000000..9a330263563
--- /dev/null
+++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Definition/RazorComponentDefinitionService.cs
@@ -0,0 +1,22 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT license. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.Razor.DocumentMapping;
+using Microsoft.CodeAnalysis.Razor.GoToDefinition;
+using Microsoft.CodeAnalysis.Razor.Logging;
+using Microsoft.CodeAnalysis.Razor.Workspaces;
+
+namespace Microsoft.AspNetCore.Razor.LanguageServer.Definition;
+
+internal sealed class RazorComponentDefinitionService(
+ IRazorComponentSearchEngine componentSearchEngine,
+ IDocumentMappingService documentMappingService,
+ ILoggerFactory loggerFactory)
+ : AbstractRazorComponentDefinitionService(componentSearchEngine, documentMappingService, loggerFactory.GetOrCreateLogger())
+{
+}
diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs
index 4fb546a09d6..963f3d96454 100644
--- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs
+++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs
@@ -12,6 +12,7 @@
using Microsoft.AspNetCore.Razor.Language.Legacy;
using Microsoft.AspNetCore.Razor.Language.Syntax;
using Microsoft.AspNetCore.Razor.LanguageServer.Tooltip;
+using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Protocol;
diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/IDocumentPositionInfoStrategy.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/IDocumentPositionInfoStrategy.cs
deleted file mode 100644
index dc7b13c4880..00000000000
--- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/IDocumentPositionInfoStrategy.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright (c) .NET Foundation. All rights reserved.
-// Licensed under the MIT license. See License.txt in the project root for license information.
-
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.CodeAnalysis.Razor.DocumentMapping;
-using Microsoft.CodeAnalysis.Razor.ProjectSystem;
-using Microsoft.VisualStudio.LanguageServer.Protocol;
-
-namespace Microsoft.AspNetCore.Razor.LanguageServer;
-
-internal interface IDocumentPositionInfoStrategy
-{
- Task TryGetPositionInfoAsync(
- IDocumentMappingService documentMappingService,
- DocumentContext documentContext,
- Position position,
- CancellationToken cancellationToken);
-}
diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/PreferAttributeNameDocumentPositionInfoStrategy.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/PreferAttributeNameDocumentPositionInfoStrategy.cs
deleted file mode 100644
index 98b8fcac5d1..00000000000
--- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/PreferAttributeNameDocumentPositionInfoStrategy.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright (c) .NET Foundation. All rights reserved.
-// Licensed under the MIT license. See License.txt in the project root for license information.
-
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.CodeAnalysis.Razor.DocumentMapping;
-using Microsoft.CodeAnalysis.Razor.ProjectSystem;
-using Microsoft.CodeAnalysis.Text;
-using Microsoft.VisualStudio.LanguageServer.Protocol;
-
-namespace Microsoft.AspNetCore.Razor.LanguageServer;
-
-///
-/// A projection strategy that, when given a position that occurs anywhere in an attribute name, will return the projection
-/// for the position at the start of the attribute name, ignoring any prefix or suffix. eg given any location within the
-/// attribute "@bind-Value:after", it will return the projection at the point of the word "Value" therein.
-///
-internal class PreferAttributeNameDocumentPositionInfoStrategy : IDocumentPositionInfoStrategy
-{
- public static IDocumentPositionInfoStrategy Instance { get; } = new PreferAttributeNameDocumentPositionInfoStrategy();
-
- public async Task TryGetPositionInfoAsync(
- IDocumentMappingService documentMappingService,
- DocumentContext documentContext,
- Position position,
- CancellationToken cancellationToken)
- {
- var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
- var sourceText = await documentContext.GetSourceTextAsync(cancellationToken).ConfigureAwait(false);
- if (sourceText.TryGetAbsoluteIndex(position, out var absoluteIndex))
- {
- // First, lets see if we should adjust the location to get a better result from C#. For example given
- // where | is the cursor, we would be unable to map that location to C#. If we pretend the caret was 3 characters to the right though,
- // in the actual component property name, then the C# server would give us a result, so we fake it.
- if (RazorSyntaxFacts.TryGetAttributeNameAbsoluteIndex(codeDocument, absoluteIndex, out var attributeNameIndex))
- {
- position = sourceText.GetPosition(attributeNameIndex);
- }
- }
-
- // We actually don't need a different projection strategy, we just wanted to move the caret position
- return await DefaultDocumentPositionInfoStrategy.Instance
- .TryGetPositionInfoAsync(documentMappingService, documentContext, position, cancellationToken)
- .ConfigureAwait(false);
- }
-}
diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs
index 601df17f06c..535501e83b2 100644
--- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs
+++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs
@@ -25,6 +25,7 @@
using Microsoft.AspNetCore.Razor.LanguageServer.WrapWithTag;
using Microsoft.AspNetCore.Razor.Telemetry;
using Microsoft.CodeAnalysis.Razor.FoldingRanges;
+using Microsoft.CodeAnalysis.Razor.GoToDefinition;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Razor.Rename;
using Microsoft.CodeAnalysis.Razor.Workspaces;
@@ -178,10 +179,11 @@ static void AddHandlers(IServiceCollection services, LanguageServerFeatureOption
services.AddHandlerWithCapabilities();
services.AddHandlerWithCapabilities();
- services.AddHandlerWithCapabilities();
-
if (!featureOptions.UseRazorCohostServer)
{
+ services.AddSingleton();
+ services.AddHandlerWithCapabilities();
+
services.AddSingleton();
services.AddHandlerWithCapabilities();
diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/DefaultDocumentPositionInfoStrategy.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/DefaultDocumentPositionInfoStrategy.cs
new file mode 100644
index 00000000000..c126e2656f1
--- /dev/null
+++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/DefaultDocumentPositionInfoStrategy.cs
@@ -0,0 +1,18 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT license. See License.txt in the project root for license information.
+
+using Microsoft.AspNetCore.Razor.Language;
+
+namespace Microsoft.CodeAnalysis.Razor.DocumentMapping;
+
+internal sealed class DefaultDocumentPositionInfoStrategy : IDocumentPositionInfoStrategy
+{
+ public static IDocumentPositionInfoStrategy Instance { get; } = new DefaultDocumentPositionInfoStrategy();
+
+ private DefaultDocumentPositionInfoStrategy()
+ {
+ }
+
+ public DocumentPositionInfo GetPositionInfo(IDocumentMappingService mappingService, RazorCodeDocument codeDocument, int hostDocumentIndex)
+ => mappingService.GetPositionInfo(codeDocument, hostDocumentIndex);
+}
diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/DocumentPositionInfo.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/DocumentPositionInfo.cs
index cfc2ad81629..bbb745aea2c 100644
--- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/DocumentPositionInfo.cs
+++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/DocumentPositionInfo.cs
@@ -10,4 +10,4 @@ namespace Microsoft.CodeAnalysis.Razor.DocumentMapping;
/// Represents a position in a document. If is Razor then the position will be
/// in the host document, otherwise it will be in the corresponding generated document.
///
-internal record DocumentPositionInfo(RazorLanguageKind LanguageKind, Position Position, int HostDocumentIndex);
+internal readonly record struct DocumentPositionInfo(RazorLanguageKind LanguageKind, Position Position, int HostDocumentIndex);
diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/IDocumentPositionInfoStrategy.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/IDocumentPositionInfoStrategy.cs
new file mode 100644
index 00000000000..9e257a712a8
--- /dev/null
+++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/IDocumentPositionInfoStrategy.cs
@@ -0,0 +1,11 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT license. See License.txt in the project root for license information.
+
+using Microsoft.AspNetCore.Razor.Language;
+
+namespace Microsoft.CodeAnalysis.Razor.DocumentMapping;
+
+internal interface IDocumentPositionInfoStrategy
+{
+ DocumentPositionInfo GetPositionInfo(IDocumentMappingService mappingService, RazorCodeDocument codeDocument, int hostDocumentIndex);
+}
diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/PreferAttributeNameDocumentPositionInfoStrategy.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/PreferAttributeNameDocumentPositionInfoStrategy.cs
new file mode 100644
index 00000000000..86c1cf5017a
--- /dev/null
+++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/PreferAttributeNameDocumentPositionInfoStrategy.cs
@@ -0,0 +1,33 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT license. See License.txt in the project root for license information.
+
+using Microsoft.AspNetCore.Razor.Language;
+
+namespace Microsoft.CodeAnalysis.Razor.DocumentMapping;
+
+///
+/// A projection strategy that, when given a position that occurs anywhere in an attribute name, will return the projection
+/// for the position at the start of the attribute name, ignoring any prefix or suffix. eg given any location within the
+/// attribute "@bind-Value:after", it will return the projection at the point of the word "Value" therein.
+///
+internal sealed class PreferAttributeNameDocumentPositionInfoStrategy : IDocumentPositionInfoStrategy
+{
+ public static IDocumentPositionInfoStrategy Instance { get; } = new PreferAttributeNameDocumentPositionInfoStrategy();
+
+ private PreferAttributeNameDocumentPositionInfoStrategy()
+ {
+ }
+
+ public DocumentPositionInfo GetPositionInfo(IDocumentMappingService mappingService, RazorCodeDocument codeDocument, int hostDocumentIndex)
+ {
+ // First, lets see if we should adjust the location to get a better result from C#. For example given
+ // where | is the cursor, we would be unable to map that location to C#. If we pretend the caret was 3 characters to the right though,
+ // in the actual component property name, then the C# server would give us a result, so we fake it.
+ if (RazorSyntaxFacts.TryGetAttributeNameAbsoluteIndex(codeDocument, hostDocumentIndex, out var attributeNameIndex))
+ {
+ hostDocumentIndex = attributeNameIndex;
+ }
+
+ return DefaultDocumentPositionInfoStrategy.Instance.GetPositionInfo(mappingService, codeDocument, hostDocumentIndex);
+ }
+}
diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/PreferHtmlInAttributeValuesDocumentPositionStrategy.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/PreferHtmlInAttributeValuesDocumentPositionStrategy.cs
new file mode 100644
index 00000000000..7de3f5f7612
--- /dev/null
+++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/PreferHtmlInAttributeValuesDocumentPositionStrategy.cs
@@ -0,0 +1,54 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT license. See License.txt in the project root for license information.
+
+using Microsoft.AspNetCore.Razor;
+using Microsoft.AspNetCore.Razor.Language;
+using Microsoft.AspNetCore.Razor.Language.Syntax;
+using Microsoft.CodeAnalysis.Razor.Protocol;
+using Microsoft.VisualStudio.LanguageServer.Protocol;
+
+namespace Microsoft.CodeAnalysis.Razor.DocumentMapping;
+
+// The main reason for this service is auto-insert of empty double quotes when a user types
+// equals "=" after Blazor component attribute. We think this is Razor (correctly I guess)
+// and wouldn't forward auto-insert request to HTML in this case. By essentially overriding
+// language info here we allow the request to be sent over to HTML where it will insert empty
+// double-quotes as it would for any other attribute value
+internal sealed class PreferHtmlInAttributeValuesDocumentPositionInfoStrategy : IDocumentPositionInfoStrategy
+{
+ public static IDocumentPositionInfoStrategy Instance { get; } = new PreferHtmlInAttributeValuesDocumentPositionInfoStrategy();
+
+ private PreferHtmlInAttributeValuesDocumentPositionInfoStrategy()
+ {
+ }
+
+ public DocumentPositionInfo GetPositionInfo(IDocumentMappingService mappingService, RazorCodeDocument codeDocument, int hostDocumentIndex)
+ {
+ var positionInfo = DefaultDocumentPositionInfoStrategy.Instance.GetPositionInfo(mappingService, codeDocument, hostDocumentIndex);
+
+ var absolutePosition = positionInfo.HostDocumentIndex;
+ if (positionInfo.LanguageKind != RazorLanguageKind.Razor ||
+ absolutePosition < 1)
+ {
+ return positionInfo;
+ }
+
+ // Get the node at previous position to see if we are after markup tag helper attribute,
+ // and more specifically after the EqualsToken of it
+ var previousPosition = absolutePosition - 1;
+
+ var syntaxTree = codeDocument.GetSyntaxTree().AssumeNotNull();
+
+ var owner = syntaxTree.Root is RazorSyntaxNode root
+ ? root.FindInnermostNode(previousPosition)
+ : null;
+
+ if (owner is MarkupTagHelperAttributeSyntax { EqualsToken: { IsMissing: false } equalsToken } &&
+ equalsToken.EndPosition == positionInfo.HostDocumentIndex)
+ {
+ return positionInfo with { LanguageKind = RazorLanguageKind.Html };
+ }
+
+ return positionInfo;
+ }
+}
diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RoslynLspExtensions_Location.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RoslynLspExtensions_Location.cs
new file mode 100644
index 00000000000..d79991b8e22
--- /dev/null
+++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RoslynLspExtensions_Location.cs
@@ -0,0 +1,12 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT license. See License.txt in the project root for license information.
+
+using System;
+
+namespace Roslyn.LanguageServer.Protocol;
+
+internal static partial class RoslynLspExtensions
+{
+ public static void Deconstruct(this Location position, out Uri uri, out Range range)
+ => (uri, range) = (position.Uri, position.Range);
+}
diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RoslynLspExtensions_SourceText.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RoslynLspExtensions_SourceText.cs
index fbeb95c1b9f..5c5d391316f 100644
--- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RoslynLspExtensions_SourceText.cs
+++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RoslynLspExtensions_SourceText.cs
@@ -8,9 +8,21 @@ namespace Roslyn.LanguageServer.Protocol;
internal static partial class RoslynLspExtensions
{
+ public static int GetPosition(this SourceText text, Position position)
+ => text.GetPosition(position.ToLinePosition());
+
+ public static Position GetPosition(this SourceText text, int position)
+ => text.GetLinePosition(position).ToPosition();
+
public static Range GetRange(this SourceText text, TextSpan span)
=> text.GetLinePositionSpan(span).ToRange();
+ public static bool TryGetAbsoluteIndex(this SourceText text, Position position, out int absoluteIndex)
+ => text.TryGetAbsoluteIndex(position.Line, position.Character, out absoluteIndex);
+
+ public static int GetRequiredAbsoluteIndex(this SourceText text, Position position)
+ => text.GetRequiredAbsoluteIndex(position.Line, position.Character);
+
public static TextSpan GetTextSpan(this SourceText text, Range range)
=> text.GetTextSpan(range.Start.Line, range.Start.Character, range.End.Line, range.End.Character);
diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RoslynLspFactory.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RoslynLspFactory.cs
index 841ea8c1ed4..f26c4feb903 100644
--- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RoslynLspFactory.cs
+++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RoslynLspFactory.cs
@@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
+using System;
using System.Diagnostics;
using Microsoft.CodeAnalysis.Text;
@@ -159,6 +160,18 @@ public static Range CreateSingleLineRange(LinePosition start, int length)
public static Range CreateSingleLineRange((int line, int character) start, int length)
=> CreateRange(CreatePosition(start), CreatePosition(start.line, start.character + length));
+ public static Location CreateLocation(Uri uri, Range range)
+ => new() { Uri = uri, Range = range };
+
+ public static Location CreateLocation(Uri uri, LinePositionSpan span)
+ => new() { Uri = uri, Range = CreateRange(span) };
+
+ public static DocumentLink CreateDocumentLink(Uri target, Range range)
+ => new() { Target = target, Range = range };
+
+ public static DocumentLink CreateDocumentLink(Uri target, LinePositionSpan span)
+ => new() { Target = target, Range = CreateRange(span) };
+
public static TextEdit CreateTextEdit(Range range, string newText)
=> new() { Range = range, NewText = newText };
diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/VsLspExtensions_Location.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/VsLspExtensions_Location.cs
new file mode 100644
index 00000000000..e3d77984aae
--- /dev/null
+++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/VsLspExtensions_Location.cs
@@ -0,0 +1,12 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT license. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.VisualStudio.LanguageServer.Protocol;
+
+internal static partial class VsLspExtensions
+{
+ public static void Deconstruct(this Location position, out Uri uri, out Range range)
+ => (uri, range) = (position.Uri, position.Range);
+}
diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/VsLspFactory.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/VsLspFactory.cs
index 5b29d44cc60..53664af6b1f 100644
--- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/VsLspFactory.cs
+++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/VsLspFactory.cs
@@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
+using System;
using System.Diagnostics;
using Microsoft.CodeAnalysis.Text;
@@ -159,6 +160,18 @@ public static Range CreateSingleLineRange(LinePosition start, int length)
public static Range CreateSingleLineRange((int line, int character) start, int length)
=> CreateRange(CreatePosition(start), CreatePosition(start.line, start.character + length));
+ public static Location CreateLocation(string filePath, LinePositionSpan span)
+ => CreateLocation(CreateFilePathUri(filePath), CreateRange(span));
+
+ public static Location CreateLocation(Uri uri, LinePositionSpan span)
+ => CreateLocation(uri, CreateRange(span));
+
+ public static Location CreateLocation(string filePath, Range range)
+ => CreateLocation(CreateFilePathUri(filePath), range);
+
+ public static Location CreateLocation(Uri uri, Range range)
+ => new() { Uri = uri, Range = range };
+
public static TextEdit CreateTextEdit(Range range, string newText)
=> new() { Range = range, NewText = newText };
@@ -185,4 +198,16 @@ public static TextEdit CreateTextEdit(LinePosition position, string newText)
public static TextEdit CreateTextEdit((int line, int character) position, string newText)
=> CreateTextEdit(CreateZeroWidthRange(position), newText);
+
+ public static Uri CreateFilePathUri(string filePath)
+ {
+ var builder = new UriBuilder
+ {
+ Path = filePath,
+ Scheme = Uri.UriSchemeFile,
+ Host = string.Empty,
+ };
+
+ return builder.Uri;
+ }
}
diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/GoToDefinition/AbstractRazorComponentDefinitionService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/GoToDefinition/AbstractRazorComponentDefinitionService.cs
new file mode 100644
index 00000000000..737614147f9
--- /dev/null
+++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/GoToDefinition/AbstractRazorComponentDefinitionService.cs
@@ -0,0 +1,90 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT license. See License.txt in the project root for license information.
+
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Razor;
+using Microsoft.AspNetCore.Razor.Language;
+using Microsoft.CodeAnalysis.Razor.DocumentMapping;
+using Microsoft.CodeAnalysis.Razor.Logging;
+using Microsoft.CodeAnalysis.Razor.ProjectSystem;
+using Microsoft.CodeAnalysis.Razor.Protocol;
+using Microsoft.CodeAnalysis.Razor.Workspaces;
+using Microsoft.VisualStudio.LanguageServer.Protocol;
+using LspLocation = Microsoft.VisualStudio.LanguageServer.Protocol.Location;
+using LspRange = Microsoft.VisualStudio.LanguageServer.Protocol.Range;
+
+namespace Microsoft.CodeAnalysis.Razor.GoToDefinition;
+
+internal abstract class AbstractRazorComponentDefinitionService(
+ IRazorComponentSearchEngine componentSearchEngine,
+ IDocumentMappingService documentMappingService,
+ ILogger logger) : IRazorComponentDefinitionService
+{
+ private readonly IRazorComponentSearchEngine _componentSearchEngine = componentSearchEngine;
+ private readonly IDocumentMappingService _documentMappingService = documentMappingService;
+ private readonly ILogger _logger = logger;
+
+ public async Task GetDefinitionAsync(IDocumentSnapshot documentSnapshot, DocumentPositionInfo positionInfo, bool ignoreAttributes, CancellationToken cancellationToken)
+ {
+ // If we're in C# then there is no point checking for a component tag, because there won't be one
+ if (positionInfo.LanguageKind == RazorLanguageKind.CSharp)
+ {
+ return null;
+ }
+
+ if (!FileKinds.IsComponent(documentSnapshot.FileKind))
+ {
+ _logger.LogInformation($"'{documentSnapshot.FileKind}' is not a component type.");
+ return null;
+ }
+
+ var codeDocument = await documentSnapshot.GetGeneratedOutputAsync().ConfigureAwait(false);
+
+ if (!RazorComponentDefinitionHelpers.TryGetBoundTagHelpers(codeDocument, positionInfo.HostDocumentIndex, ignoreAttributes, _logger, out var boundTagHelper, out var boundAttribute))
+ {
+ _logger.LogInformation($"Could not retrieve bound tag helper information.");
+ return null;
+ }
+
+ var componentDocument = await _componentSearchEngine.TryLocateComponentAsync(documentSnapshot, boundTagHelper).ConfigureAwait(false);
+ if (componentDocument is null)
+ {
+ _logger.LogInformation($"Could not locate component document.");
+ return null;
+ }
+
+ var componentFilePath = componentDocument.FilePath.AssumeNotNull();
+
+ _logger.LogInformation($"Definition found at file path: {componentFilePath}");
+
+ var range = await GetNavigateRangeAsync(componentDocument, boundAttribute, cancellationToken).ConfigureAwait(false);
+
+ return VsLspFactory.CreateLocation(componentFilePath, range);
+ }
+
+ private async Task GetNavigateRangeAsync(IDocumentSnapshot documentSnapshot, BoundAttributeDescriptor? attributeDescriptor, CancellationToken cancellationToken)
+ {
+ if (attributeDescriptor is not null)
+ {
+ _logger.LogInformation($"Attempting to get definition from an attribute directly.");
+
+ var originCodeDocument = await documentSnapshot.GetGeneratedOutputAsync().ConfigureAwait(false);
+
+ var range = await RazorComponentDefinitionHelpers
+ .TryGetPropertyRangeAsync(originCodeDocument, attributeDescriptor.GetPropertyName(), _documentMappingService, _logger, cancellationToken)
+ .ConfigureAwait(false);
+
+ if (range is not null)
+ {
+ return range;
+ }
+ }
+
+ // When navigating from a start or end tag, we just take the user to the top of the file.
+ // If we were trying to navigate to a property, and we couldn't find it, we can at least take
+ // them to the file for the component. If the property was defined in a partial class they can
+ // at least then press F7 to go there.
+ return VsLspFactory.DefaultRange;
+ }
+}
diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/GoToDefinition/IRazorComponentDefinitionService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/GoToDefinition/IRazorComponentDefinitionService.cs
new file mode 100644
index 00000000000..86ebf62d35f
--- /dev/null
+++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/GoToDefinition/IRazorComponentDefinitionService.cs
@@ -0,0 +1,18 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT license. See License.txt in the project root for license information.
+
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.Razor.DocumentMapping;
+using Microsoft.CodeAnalysis.Razor.ProjectSystem;
+using LspLocation = Microsoft.VisualStudio.LanguageServer.Protocol.Location;
+
+namespace Microsoft.CodeAnalysis.Razor.GoToDefinition;
+
+///
+/// Go to Definition support for Razor components.
+///
+internal interface IRazorComponentDefinitionService
+{
+ Task GetDefinitionAsync(IDocumentSnapshot documentSnapshot, DocumentPositionInfo positionInfo, bool ignoreAttributes, CancellationToken cancellationToken);
+}
diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/GoToDefinition/RazorComponentDefinitionHelpers.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/GoToDefinition/RazorComponentDefinitionHelpers.cs
new file mode 100644
index 00000000000..66177fb6be2
--- /dev/null
+++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/GoToDefinition/RazorComponentDefinitionHelpers.cs
@@ -0,0 +1,201 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT license. See License.txt in the project root for license information.
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Razor.Language;
+using Microsoft.AspNetCore.Razor.Language.Syntax;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Razor.DocumentMapping;
+using Microsoft.CodeAnalysis.Razor.Logging;
+using Microsoft.CodeAnalysis.Razor.Workspaces;
+using Microsoft.VisualStudio.LanguageServer.Protocol;
+using LspRange = Microsoft.VisualStudio.LanguageServer.Protocol.Range;
+using RazorSyntaxKind = Microsoft.AspNetCore.Razor.Language.SyntaxKind;
+using RazorSyntaxNode = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode;
+using RazorSyntaxToken = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxToken;
+
+namespace Microsoft.CodeAnalysis.Razor.GoToDefinition;
+
+internal static class RazorComponentDefinitionHelpers
+{
+ public static bool TryGetBoundTagHelpers(
+ RazorCodeDocument codeDocument, int absoluteIndex, bool ignoreAttributes, ILogger logger,
+ [NotNullWhen(true)] out TagHelperDescriptor? boundTagHelper,
+ [MaybeNullWhen(true)] out BoundAttributeDescriptor? boundAttribute)
+ {
+ boundTagHelper = null;
+ boundAttribute = null;
+
+ var syntaxTree = codeDocument.GetSyntaxTree();
+
+ var innermostNode = syntaxTree.Root.FindInnermostNode(absoluteIndex);
+ if (innermostNode is null)
+ {
+ logger.LogInformation($"Could not locate innermost node at index, {absoluteIndex}.");
+ return false;
+ }
+
+ var tagHelperNode = innermostNode.FirstAncestorOrSelf(IsTagHelperNode);
+ if (tagHelperNode is null)
+ {
+ logger.LogInformation($"Could not locate ancestor of type MarkupTagHelperStartTag or MarkupTagHelperEndTag.");
+ return false;
+ }
+
+ if (!TryGetTagName(tagHelperNode, out var tagName))
+ {
+ logger.LogInformation($"Could not retrieve name of start or end tag.");
+ return false;
+ }
+
+ var nameSpan = tagName.Span;
+ string? propertyName = null;
+
+ if (!ignoreAttributes && tagHelperNode is MarkupTagHelperStartTagSyntax startTag)
+ {
+ // Include attributes where the end index also matches, since GetSyntaxNodeAsync will consider that the start tag but we behave
+ // as if the user wants to go to the attribute definition.
+ // ie:
+ var selectedAttribute = startTag.Attributes.FirstOrDefault(a => a.Span.Contains(absoluteIndex) || a.Span.End == absoluteIndex);
+
+ // If we're on an attribute then just validate against the attribute name
+ switch (selectedAttribute)
+ {
+ case MarkupTagHelperAttributeSyntax attribute:
+ // Normal attribute, ie
+ nameSpan = attribute.Name.Span;
+ propertyName = attribute.TagHelperAttributeInfo.Name;
+ break;
+
+ case MarkupMinimizedTagHelperAttributeSyntax minimizedAttribute:
+ // Minimized attribute, ie
+ nameSpan = minimizedAttribute.Name.Span;
+ propertyName = minimizedAttribute.TagHelperAttributeInfo.Name;
+ break;
+ }
+ }
+
+ if (!nameSpan.IntersectsWith(absoluteIndex))
+ {
+ logger.LogInformation($"Tag name or attributes' span does not intersect with index, {absoluteIndex}.");
+ return false;
+ }
+
+ if (tagHelperNode.Parent is not MarkupTagHelperElementSyntax tagHelperElement)
+ {
+ logger.LogInformation($"Parent of start or end tag is not a MarkupTagHelperElement.");
+ return false;
+ }
+
+ if (tagHelperElement.TagHelperInfo?.BindingResult is not TagHelperBinding binding)
+ {
+ logger.LogInformation($"MarkupTagHelperElement does not contain TagHelperInfo.");
+ return false;
+ }
+
+ boundTagHelper = binding.Descriptors.FirstOrDefault(static d => !d.IsAttributeDescriptor());
+ if (boundTagHelper is null)
+ {
+ logger.LogInformation($"Could not locate bound TagHelperDescriptor.");
+ return false;
+ }
+
+ boundAttribute = propertyName is not null
+ ? boundTagHelper.BoundAttributes.FirstOrDefault(a => a.Name?.Equals(propertyName, StringComparison.Ordinal) == true)
+ : null;
+
+ return true;
+
+ static bool IsTagHelperNode(RazorSyntaxNode node)
+ {
+ return node.Kind is RazorSyntaxKind.MarkupTagHelperStartTag or RazorSyntaxKind.MarkupTagHelperEndTag;
+ }
+
+ static bool TryGetTagName(RazorSyntaxNode node, [NotNullWhen(true)] out RazorSyntaxToken? tagName)
+ {
+ tagName = node switch
+ {
+ MarkupTagHelperStartTagSyntax tagHelperStartTag => tagHelperStartTag.Name,
+ MarkupTagHelperEndTagSyntax tagHelperEndTag => tagHelperEndTag.Name,
+ _ => null
+ };
+
+ return tagName is not null;
+ }
+ }
+
+ public static async Task TryGetPropertyRangeAsync(
+ RazorCodeDocument codeDocument,
+ string propertyName,
+ IDocumentMappingService documentMappingService,
+ ILogger logger,
+ CancellationToken cancellationToken)
+ {
+ // Parse the C# file and find the property that matches the name.
+ // We don't worry about parameter attributes here for two main reasons:
+ // 1. We don't have symbolic information, so the best we could do would be checking for any
+ // attribute named Parameter, regardless of which namespace. It also means we would have
+ // to do more checks for all of the various ways that the attribute could be specified
+ // (eg fully qualified, aliased, etc.)
+ // 2. Since C# doesn't allow multiple properties with the same name, and we're doing a case
+ // sensitive search, we know the property we find is the one the user is trying to encode in a
+ // tag helper attribute. If they don't have the [Parameter] attribute then the Razor compiler
+ // will error, but allowing them to Go To Def on that property regardless, actually helps
+ // them fix the error.
+ var csharpText = codeDocument.GetCSharpSourceText();
+ var syntaxTree = CSharpSyntaxTree.ParseText(csharpText, cancellationToken: cancellationToken);
+ var root = await syntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false);
+
+ // Since we know how the compiler generates the C# source we can be a little specific here, and avoid
+ // long tree walks. If the compiler ever changes how they generate their code, the tests for this will break
+ // so we'll know about it.
+ if (TryGetClassDeclaration(root, out var classDeclaration))
+ {
+ var property = classDeclaration
+ .Members
+ .OfType()
+ .Where(p => p.Identifier.ValueText.Equals(propertyName, StringComparison.Ordinal))
+ .FirstOrDefault();
+
+ if (property is null)
+ {
+ // The property probably exists in a partial class
+ logger.LogInformation($"Could not find property in the generated source. Comes from partial?");
+ return null;
+ }
+
+ var range = csharpText.GetRange(property.Identifier.Span);
+ if (documentMappingService.TryMapToHostDocumentRange(codeDocument.GetCSharpDocument(), range, out var originalRange))
+ {
+ return originalRange;
+ }
+
+ logger.LogInformation($"Property found but couldn't map its location.");
+ }
+
+ logger.LogInformation($"Generated C# was not in expected shape (CompilationUnit [-> Namespace] -> Class)");
+
+ return null;
+
+ static bool TryGetClassDeclaration(SyntaxNode root, [NotNullWhen(true)] out ClassDeclarationSyntax? classDeclaration)
+ {
+ classDeclaration = root switch
+ {
+ CompilationUnitSyntax unit => unit switch
+ {
+ { Members: [NamespaceDeclarationSyntax { Members: [ClassDeclarationSyntax c, ..] }, ..] } => c,
+ { Members: [ClassDeclarationSyntax c, ..] } => c,
+ _ => null,
+ },
+ _ => null,
+ };
+
+ return classDeclaration is not null;
+ }
+ }
+}
diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorSyntaxFacts.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorSyntaxFacts.cs
similarity index 93%
rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorSyntaxFacts.cs
rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorSyntaxFacts.cs
index 1e8c122d9f1..36a77a23129 100644
--- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorSyntaxFacts.cs
+++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorSyntaxFacts.cs
@@ -4,8 +4,9 @@
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Syntax;
using Microsoft.CodeAnalysis.Text;
+using RazorSyntaxNode = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode;
-namespace Microsoft.AspNetCore.Razor.LanguageServer;
+namespace Microsoft.CodeAnalysis.Razor;
internal static class RazorSyntaxFacts
{
@@ -79,7 +80,7 @@ public static bool TryGetFullAttributeNameSpan(RazorCodeDocument codeDocument, i
return attributeNameSpan != default;
}
- private static TextSpan GetFullAttributeNameSpan(SyntaxNode? node)
+ private static TextSpan GetFullAttributeNameSpan(RazorSyntaxNode? node)
{
return node switch
{
@@ -112,7 +113,7 @@ static TextSpan CalculateFullSpan(MarkupTextLiteralSyntax attributeName, MarkupT
}
}
- public static CSharpCodeBlockSyntax? TryGetCSharpCodeFromCodeBlock(SyntaxNode node)
+ public static CSharpCodeBlockSyntax? TryGetCSharpCodeFromCodeBlock(RazorSyntaxNode node)
{
if (node is CSharpCodeBlockSyntax block &&
block.Children.FirstOrDefault() is RazorDirectiveSyntax directive &&
@@ -125,10 +126,9 @@ directive.Body is RazorDirectiveBodySyntax directiveBody &&
return null;
}
- public static bool IsAnyStartTag(SyntaxNode n)
+ public static bool IsAnyStartTag(RazorSyntaxNode n)
=> n.Kind is SyntaxKind.MarkupStartTag or SyntaxKind.MarkupTagHelperStartTag;
-
- public static bool IsAnyEndTag(SyntaxNode n)
+ public static bool IsAnyEndTag(RazorSyntaxNode n)
=> n.Kind is SyntaxKind.MarkupEndTag or SyntaxKind.MarkupTagHelperEndTag;
}
diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteGoToDefinitionService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteGoToDefinitionService.cs
new file mode 100644
index 00000000000..b8e91db72c5
--- /dev/null
+++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteGoToDefinitionService.cs
@@ -0,0 +1,19 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT license. See License.txt in the project root for license information.
+
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.ExternalAccess.Razor;
+using RoslynLocation = Roslyn.LanguageServer.Protocol.Location;
+using RoslynPosition = Roslyn.LanguageServer.Protocol.Position;
+
+namespace Microsoft.CodeAnalysis.Razor.Remote;
+
+internal interface IRemoteGoToDefinitionService : IRemoteJsonService
+{
+ ValueTask> GetDefinitionAsync(
+ JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo,
+ JsonSerializableDocumentId razorDocumentId,
+ RoslynPosition position,
+ CancellationToken cancellationToken);
+}
diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs
index 06bf0950825..e1310e1658e 100644
--- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs
+++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs
@@ -26,6 +26,7 @@ internal static class RazorServices
// Internal for testing
internal static readonly IEnumerable<(Type, Type?)> JsonServices =
[
+ (typeof(IRemoteGoToDefinitionService), null),
(typeof(IRemoteSignatureHelpService), null),
(typeof(IRemoteInlayHintService), null),
(typeof(IRemoteRenameService), null),
diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/GoToDefinition/RazorComponentDefinitionService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/GoToDefinition/RazorComponentDefinitionService.cs
new file mode 100644
index 00000000000..790ad2c5f2f
--- /dev/null
+++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/GoToDefinition/RazorComponentDefinitionService.cs
@@ -0,0 +1,20 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT license. See License.txt in the project root for license information.
+
+using System.Composition;
+using Microsoft.CodeAnalysis.Razor.DocumentMapping;
+using Microsoft.CodeAnalysis.Razor.GoToDefinition;
+using Microsoft.CodeAnalysis.Razor.Logging;
+using Microsoft.CodeAnalysis.Razor.Workspaces;
+
+namespace Microsoft.CodeAnalysis.Remote.Razor.GoToDefinition;
+
+[Export(typeof(IRazorComponentDefinitionService)), Shared]
+[method: ImportingConstructor]
+internal sealed class RazorComponentDefinitionService(
+ IRazorComponentSearchEngine componentSearchEngine,
+ IDocumentMappingService documentMappingService,
+ ILoggerFactory loggerFactory)
+ : AbstractRazorComponentDefinitionService(componentSearchEngine, documentMappingService, loggerFactory.GetOrCreateLogger())
+{
+}
diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/GoToDefinition/RemoteGoToDefinitionService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/GoToDefinition/RemoteGoToDefinitionService.cs
new file mode 100644
index 00000000000..02a7aba2217
--- /dev/null
+++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/GoToDefinition/RemoteGoToDefinitionService.cs
@@ -0,0 +1,134 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT license. See License.txt in the project root for license information.
+
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Razor.Language;
+using Microsoft.AspNetCore.Razor.PooledObjects;
+using Microsoft.CodeAnalysis.ExternalAccess.Razor;
+using Microsoft.CodeAnalysis.Razor.DocumentMapping;
+using Microsoft.CodeAnalysis.Razor.GoToDefinition;
+using Microsoft.CodeAnalysis.Razor.Protocol;
+using Microsoft.CodeAnalysis.Razor.Remote;
+using Microsoft.CodeAnalysis.Razor.Workspaces;
+using Microsoft.CodeAnalysis.Remote.Razor.DocumentMapping;
+using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem;
+using Microsoft.CodeAnalysis.Text;
+using Microsoft.VisualStudio.LanguageServer.Protocol;
+using Roslyn.LanguageServer.Protocol;
+using static Microsoft.CodeAnalysis.Razor.Remote.RemoteResponse;
+using ExternalHandlers = Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers;
+using RoslynLocation = Roslyn.LanguageServer.Protocol.Location;
+using RoslynPosition = Roslyn.LanguageServer.Protocol.Position;
+using VsPosition = Microsoft.VisualStudio.LanguageServer.Protocol.Position;
+
+namespace Microsoft.CodeAnalysis.Remote.Razor;
+
+internal sealed class RemoteGoToDefinitionService(in ServiceArgs args) : RazorDocumentServiceBase(in args), IRemoteGoToDefinitionService
+{
+ internal sealed class Factory : FactoryBase
+ {
+ protected override IRemoteGoToDefinitionService CreateService(in ServiceArgs args)
+ => new RemoteGoToDefinitionService(in args);
+ }
+
+ private readonly IRazorComponentDefinitionService _componentDefinitionService = args.ExportProvider.GetExportedValue();
+ private readonly IFilePathService _filePathService = args.ExportProvider.GetExportedValue();
+
+ protected override IDocumentPositionInfoStrategy DocumentPositionInfoStrategy => PreferAttributeNameDocumentPositionInfoStrategy.Instance;
+
+ public ValueTask> GetDefinitionAsync(
+ JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo,
+ JsonSerializableDocumentId documentId,
+ RoslynPosition position,
+ CancellationToken cancellationToken)
+ => RunServiceAsync(
+ solutionInfo,
+ documentId,
+ context => GetDefinitionAsync(context, position, cancellationToken),
+ cancellationToken);
+
+ private async ValueTask> GetDefinitionAsync(
+ RemoteDocumentContext context,
+ RoslynPosition position,
+ CancellationToken cancellationToken)
+ {
+ var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
+
+ if (!codeDocument.Source.Text.TryGetAbsoluteIndex(position, out var hostDocumentIndex))
+ {
+ return NoFurtherHandling;
+ }
+
+ var positionInfo = GetPositionInfo(codeDocument, hostDocumentIndex);
+
+ // First, see if this is a Razor component.
+ var componentLocation = await _componentDefinitionService.GetDefinitionAsync(context.Snapshot, positionInfo, ignoreAttributes: false, cancellationToken).ConfigureAwait(false);
+ if (componentLocation is not null)
+ {
+ // Convert from VS LSP Location to Roslyn. This can be removed when Razor moves fully onto Roslyn's LSP types.
+ return Results([RoslynLspFactory.CreateLocation(componentLocation.Uri, componentLocation.Range.ToLinePositionSpan())]);
+ }
+
+ if (positionInfo.LanguageKind == RazorLanguageKind.Html)
+ {
+ // Sometimes Html can actually be mapped to C#, like for example component attributes, which map to
+ // C# properties, even though they appear entirely in a Html context. Since remapping is pretty cheap
+ // it's easier to just try mapping, and see what happens, rather than checking for specific syntax nodes.
+ if (DocumentMappingService.TryMapToGeneratedDocumentPosition(codeDocument.GetCSharpDocument(), positionInfo.HostDocumentIndex, out VsPosition? csharpPosition, out _))
+ {
+ // We're just gonna pretend this mapped perfectly normally onto C#. Moving this logic to the actual position info
+ // calculating code is possible, but could have untold effects, so opt-in is better (for now?)
+ positionInfo = positionInfo with { LanguageKind = RazorLanguageKind.CSharp, Position = csharpPosition };
+ }
+ }
+
+ // If it isn't a Razor component, and it isn't C#, let the server know to delegate to HTML.
+ if (positionInfo.LanguageKind != RazorLanguageKind.CSharp)
+ {
+ return CallHtml;
+ }
+
+ if (!DocumentMappingService.TryMapToGeneratedDocumentPosition(codeDocument.GetCSharpDocument(), positionInfo.HostDocumentIndex, out var mappedPosition, out _))
+ {
+ // If we can't map to the generated C# file, we're done.
+ return NoFurtherHandling;
+ }
+
+ // Finally, call into C#.
+ var generatedDocument = await context.GetGeneratedDocumentAsync(_filePathService, cancellationToken).ConfigureAwait(false);
+
+ var locations = await ExternalHandlers.GoToDefinition
+ .GetDefinitionsAsync(
+ RemoteWorkspaceAccessor.GetWorkspace(),
+ generatedDocument,
+ typeOnly: false,
+ mappedPosition,
+ cancellationToken)
+ .ConfigureAwait(false);
+
+ if (locations is null and not [])
+ {
+ // C# didn't return anything, so we're done.
+ return NoFurtherHandling;
+ }
+
+ // Map the C# locations back to the Razor file.
+ using var mappedLocations = new PooledArrayBuilder(locations.Length);
+
+ foreach (var location in locations)
+ {
+ var (uri, range) = location;
+
+ var (mappedDocumentUri, mappedRange) = await DocumentMappingService
+ .MapToHostDocumentUriAndRangeAsync((RemoteDocumentSnapshot)context.Snapshot, uri, range.ToLinePositionSpan(), cancellationToken)
+ .ConfigureAwait(false);
+
+ var mappedLocation = RoslynLspFactory.CreateLocation(mappedDocumentUri, mappedRange);
+
+ mappedLocations.Add(mappedLocation);
+ }
+
+ return Results(mappedLocations.ToArray());
+ }
+}
diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/InlayHints/RemoteInlayHintService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/InlayHints/RemoteInlayHintService.cs
index ae06c498c58..2f80dd60b34 100644
--- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/InlayHints/RemoteInlayHintService.cs
+++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/InlayHints/RemoteInlayHintService.cs
@@ -9,7 +9,6 @@
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers;
-using Microsoft.CodeAnalysis.Razor.DocumentMapping;
using Microsoft.CodeAnalysis.Razor.Protocol.InlayHints;
using Microsoft.CodeAnalysis.Razor.Remote;
using Microsoft.CodeAnalysis.Razor.Workspaces;
@@ -27,7 +26,6 @@ protected override IRemoteInlayHintService CreateService(in ServiceArgs args)
=> new RemoteInlayHintService(in args);
}
- private readonly IDocumentMappingService _documentMappingService = args.ExportProvider.GetExportedValue();
private readonly IFilePathService _filePathService = args.ExportProvider.GetExportedValue();
public ValueTask GetInlayHintsAsync(JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, JsonSerializableDocumentId razorDocumentId, InlayHintParams inlayHintParams, bool displayAllOverride, CancellationToken cancellationToken)
@@ -47,7 +45,7 @@ protected override IRemoteInlayHintService CreateService(in ServiceArgs args)
// We are given a range by the client, but our mapping only succeeds if the start and end of the range can both be mapped
// to C#. Since that doesn't logically match what we want from inlay hints, we instead get the minimum range of mappable
// C# to get hints for. We'll filter that later, to remove the sections that can't be mapped back.
- if (!_documentMappingService.TryMapToGeneratedDocumentRange(csharpDocument, span, out var projectedLinePositionSpan) &&
+ if (!DocumentMappingService.TryMapToGeneratedDocumentRange(csharpDocument, span, out var projectedLinePositionSpan) &&
!codeDocument.TryGetMinimalCSharpRange(span, out projectedLinePositionSpan))
{
// There's no C# in the range.
@@ -73,7 +71,7 @@ protected override IRemoteInlayHintService CreateService(in ServiceArgs args)
foreach (var hint in hints)
{
if (csharpSourceText.TryGetAbsoluteIndex(hint.Position.ToLinePosition(), out var absoluteIndex) &&
- _documentMappingService.TryMapToHostDocumentPosition(csharpDocument, absoluteIndex, out var hostDocumentPosition, out var hostDocumentIndex))
+ DocumentMappingService.TryMapToHostDocumentPosition(csharpDocument, absoluteIndex, out var hostDocumentPosition, out var hostDocumentIndex))
{
// We know this C# maps to Razor, but does it map to Razor that we like?
var node = syntaxTree.Root.FindInnermostNode(hostDocumentIndex);
@@ -85,7 +83,7 @@ protected override IRemoteInlayHintService CreateService(in ServiceArgs args)
if (hint.TextEdits is not null)
{
var changes = hint.TextEdits.Select(csharpSourceText.GetTextChange);
- var mappedChanges = _documentMappingService.GetHostDocumentEdits(csharpDocument, changes);
+ var mappedChanges = DocumentMappingService.GetHostDocumentEdits(csharpDocument, changes);
hint.TextEdits = mappedChanges.Select(razorSourceText.GetTextEdit).ToArray();
}
diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorDocumentServiceBase.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorDocumentServiceBase.cs
index 85b747d60a4..9327d9931e7 100644
--- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorDocumentServiceBase.cs
+++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorDocumentServiceBase.cs
@@ -4,14 +4,53 @@
using System;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
+using Microsoft.CodeAnalysis.Razor.DocumentMapping;
using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem;
+using Microsoft.CodeAnalysis.Text;
+using Microsoft.VisualStudio.LanguageServer.Protocol;
+using Roslyn.LanguageServer.Protocol;
+using RoslynPosition = Roslyn.LanguageServer.Protocol.Position;
+using VsPosition = Microsoft.VisualStudio.LanguageServer.Protocol.Position;
namespace Microsoft.CodeAnalysis.Remote.Razor;
internal abstract class RazorDocumentServiceBase(in ServiceArgs args) : RazorBrokeredServiceBase(in args)
{
protected DocumentSnapshotFactory DocumentSnapshotFactory { get; } = args.ExportProvider.GetExportedValue();
+ protected IDocumentMappingService DocumentMappingService { get; } = args.ExportProvider.GetExportedValue();
+
+ protected virtual IDocumentPositionInfoStrategy DocumentPositionInfoStrategy { get; } = DefaultDocumentPositionInfoStrategy.Instance;
+
+ protected DocumentPositionInfo GetPositionInfo(RazorCodeDocument codeDocument, int hostDocumentIndex)
+ {
+ return DocumentPositionInfoStrategy.GetPositionInfo(DocumentMappingService, codeDocument, hostDocumentIndex);
+ }
+
+ protected bool TryGetDocumentPositionInfo(RazorCodeDocument codeDocument, RoslynPosition position, out DocumentPositionInfo positionInfo)
+ {
+ if (!codeDocument.Source.Text.TryGetAbsoluteIndex(position, out var hostDocumentIndex))
+ {
+ positionInfo = default;
+ return false;
+ }
+
+ positionInfo = GetPositionInfo(codeDocument, hostDocumentIndex);
+ return true;
+ }
+
+ protected bool TryGetDocumentPositionInfo(RazorCodeDocument codeDocument, VsPosition position, out DocumentPositionInfo positionInfo)
+ {
+ if (!codeDocument.Source.Text.TryGetAbsoluteIndex(position, out var hostDocumentIndex))
+ {
+ positionInfo = default;
+ return false;
+ }
+
+ positionInfo = GetPositionInfo(codeDocument, hostDocumentIndex);
+ return true;
+ }
protected ValueTask RunServiceAsync(
RazorPinnedSolutionInfoWrapper solutionInfo,
diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Rename/RemoteRenameService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Rename/RemoteRenameService.cs
index 7f095247258..be31a947509 100644
--- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Rename/RemoteRenameService.cs
+++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Rename/RemoteRenameService.cs
@@ -11,8 +11,8 @@
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem;
using Microsoft.VisualStudio.LanguageServer.Protocol;
-using ExternalHandlers = Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers;
using static Microsoft.CodeAnalysis.Razor.Remote.RemoteResponse;
+using ExternalHandlers = Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers;
namespace Microsoft.CodeAnalysis.Remote.Razor;
@@ -26,7 +26,6 @@ protected override IRemoteRenameService CreateService(in ServiceArgs args)
private readonly IRenameService _renameService = args.ExportProvider.GetExportedValue();
private readonly IFilePathService _filePathService = args.ExportProvider.GetExportedValue();
- private readonly IDocumentMappingService _documentMappingService = args.ExportProvider.GetExportedValue();
private readonly IEditMappingService _editMappingService = args.ExportProvider.GetExportedValue();
public ValueTask> GetRenameEditAsync(
@@ -48,10 +47,13 @@ protected override IRemoteRenameService CreateService(in ServiceArgs args)
CancellationToken cancellationToken)
{
var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
- var generatedDocument = await context.GetGeneratedDocumentAsync(_filePathService, cancellationToken).ConfigureAwait(false);
- var hostDocumentIndex = codeDocument.Source.Text.GetRequiredAbsoluteIndex(position);
- var positionInfo = _documentMappingService.GetPositionInfo(codeDocument, hostDocumentIndex);
+ if (!TryGetDocumentPositionInfo(codeDocument, position, out var positionInfo))
+ {
+ return NoFurtherHandling;
+ }
+
+ var generatedDocument = await context.GetGeneratedDocumentAsync(_filePathService, cancellationToken).ConfigureAwait(false);
var razorEdit = await _renameService.TryGetRazorRenameEditsAsync(context, positionInfo, newName, cancellationToken).ConfigureAwait(false);
if (razorEdit is not null)
diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/SignatureHelp/RemoteSignatureHelpService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/SignatureHelp/RemoteSignatureHelpService.cs
index fb4fa23635a..0aa63984efc 100644
--- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/SignatureHelp/RemoteSignatureHelpService.cs
+++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/SignatureHelp/RemoteSignatureHelpService.cs
@@ -5,7 +5,6 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
-using Microsoft.CodeAnalysis.Razor.DocumentMapping;
using Microsoft.CodeAnalysis.Razor.Remote;
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem;
@@ -26,7 +25,6 @@ protected override IRemoteSignatureHelpService CreateService(in ServiceArgs args
}
private readonly IFilePathService _filePathService = args.ExportProvider.GetExportedValue();
- private readonly IDocumentMappingService _documentMappingService = args.ExportProvider.GetExportedValue();
public ValueTask GetSignatureHelpAsync(JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, JsonSerializableDocumentId documentId, Position position, CancellationToken cancellationToken)
=> RunServiceAsync(
@@ -43,7 +41,7 @@ protected override IRemoteSignatureHelpService CreateService(in ServiceArgs args
var generatedDocument = await context.GetGeneratedDocumentAsync(_filePathService, cancellationToken).ConfigureAwait(false);
- if (_documentMappingService.TryMapToGeneratedDocumentPosition(codeDocument.GetCSharpDocument(), absoluteIndex, out var mappedPosition, out _))
+ if (DocumentMappingService.TryMapToGeneratedDocumentPosition(codeDocument.GetCSharpDocument(), absoluteIndex, out var mappedPosition, out _))
{
return await ExternalHandlers.SignatureHelp.GetSignatureHelpAsync(generatedDocument, mappedPosition, supportsVisualStudioExtensions: true, cancellationToken).ConfigureAwait(false);
}
diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/UriPresentation/RemoteUriPresentationService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/UriPresentation/RemoteUriPresentationService.cs
index 393765a7a0a..06dd2c59b0f 100644
--- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/UriPresentation/RemoteUriPresentationService.cs
+++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/UriPresentation/RemoteUriPresentationService.cs
@@ -5,7 +5,6 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
-using Microsoft.CodeAnalysis.Razor.DocumentMapping;
using Microsoft.CodeAnalysis.Razor.DocumentPresentation;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Protocol;
@@ -25,8 +24,6 @@ protected override IRemoteUriPresentationService CreateService(in ServiceArgs ar
=> new RemoteUriPresentationService(in args);
}
- private readonly IDocumentMappingService _documentMappingService = args.ExportProvider.GetExportedValue();
-
public ValueTask GetPresentationAsync(
RazorPinnedSolutionInfoWrapper solutionInfo,
DocumentId razorDocumentId,
@@ -54,7 +51,7 @@ private async ValueTask GetPresentationAsync(
var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
- var languageKind = _documentMappingService.GetLanguageKind(codeDocument, index, rightAssociative: true);
+ var languageKind = DocumentMappingService.GetLanguageKind(codeDocument, index, rightAssociative: true);
if (languageKind is not RazorLanguageKind.Html)
{
// Roslyn doesn't currently support Uri presentation, and whilst it might seem counter intuitive,
diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostGoToDefinitionEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostGoToDefinitionEndpoint.cs
new file mode 100644
index 00000000000..fbab17f6a38
--- /dev/null
+++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostGoToDefinitionEndpoint.cs
@@ -0,0 +1,150 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT license. See License.txt in the project root for license information.
+
+using System;
+using System.Composition;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Razor;
+using Microsoft.AspNetCore.Razor.PooledObjects;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost;
+using Microsoft.CodeAnalysis.Razor.Remote;
+using Microsoft.VisualStudio.LanguageServer.ContainedLanguage;
+using Microsoft.VisualStudio.LanguageServer.Protocol;
+using static Roslyn.LanguageServer.Protocol.RoslynLspExtensions;
+using RoslynDocumentLink = Roslyn.LanguageServer.Protocol.DocumentLink;
+using RoslynLocation = Roslyn.LanguageServer.Protocol.Location;
+using RoslynLspFactory = Roslyn.LanguageServer.Protocol.RoslynLspFactory;
+using VsLspLocation = Microsoft.VisualStudio.LanguageServer.Protocol.Location;
+
+namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
+
+#pragma warning disable RS0030 // Do not use banned APIs
+[Shared]
+[CohostEndpoint(Methods.TextDocumentDefinitionName)]
+[Export(typeof(IDynamicRegistrationProvider))]
+[ExportCohostStatelessLspService(typeof(CohostGoToDefinitionEndpoint))]
+[method: ImportingConstructor]
+#pragma warning restore RS0030 // Do not use banned APIs
+internal sealed class CohostGoToDefinitionEndpoint(
+ IRemoteServiceInvoker remoteServiceInvoker,
+ IHtmlDocumentSynchronizer htmlDocumentSynchronizer,
+ LSPRequestInvoker requestInvoker)
+ : AbstractRazorCohostDocumentRequestHandler?>, IDynamicRegistrationProvider
+{
+ private readonly IRemoteServiceInvoker _remoteServiceInvoker = remoteServiceInvoker;
+ private readonly IHtmlDocumentSynchronizer _htmlDocumentSynchronizer = htmlDocumentSynchronizer;
+ private readonly LSPRequestInvoker _requestInvoker = requestInvoker;
+
+ protected override bool MutatesSolutionState => false;
+
+ protected override bool RequiresLSPSolution => true;
+
+ public Registration? GetRegistration(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext)
+ {
+ if (clientCapabilities.TextDocument?.Definition?.DynamicRegistration == true)
+ {
+ return new Registration
+ {
+ Method = Methods.TextDocumentDefinitionName,
+ RegisterOptions = new DefinitionOptions()
+ };
+ }
+
+ return null;
+ }
+
+ protected override RazorTextDocumentIdentifier? GetRazorTextDocumentIdentifier(TextDocumentPositionParams request)
+ => request.TextDocument.ToRazorTextDocumentIdentifier();
+
+ protected override Task?> HandleRequestAsync(TextDocumentPositionParams request, RazorCohostRequestContext context, CancellationToken cancellationToken)
+ => HandleRequestAsync(
+ request,
+ context.TextDocument.AssumeNotNull(),
+ cancellationToken);
+
+ private async Task?> HandleRequestAsync(TextDocumentPositionParams request, TextDocument razorDocument, CancellationToken cancellationToken)
+ {
+ var position = RoslynLspFactory.CreatePosition(request.Position.ToLinePosition());
+
+ var response = await _remoteServiceInvoker
+ .TryInvokeAsync>(
+ razorDocument.Project.Solution,
+ (service, solutionInfo, cancellationToken) =>
+ service.GetDefinitionAsync(solutionInfo, razorDocument.Id, position, cancellationToken),
+ cancellationToken)
+ .ConfigureAwait(false);
+
+ if (response.Result is RoslynLocation[] locations)
+ {
+ return locations;
+ }
+
+ if (response.StopHandling)
+ {
+ return null;
+ }
+
+ return await GetHtmlDefinitionsAsync(request, razorDocument, cancellationToken).ConfigureAwait(false);
+ }
+
+ private async Task?> GetHtmlDefinitionsAsync(TextDocumentPositionParams request, TextDocument razorDocument, CancellationToken cancellationToken)
+ {
+ var htmlDocument = await _htmlDocumentSynchronizer.TryGetSynchronizedHtmlDocumentAsync(razorDocument, cancellationToken).ConfigureAwait(false);
+ if (htmlDocument is null)
+ {
+ return null;
+ }
+
+ request.TextDocument.Uri = htmlDocument.Uri;
+
+ var result = await _requestInvoker
+ .ReinvokeRequestOnServerAsync?>(
+ htmlDocument.Buffer,
+ Methods.TextDocumentDefinitionName,
+ RazorLSPConstants.HtmlLanguageServerName,
+ request,
+ cancellationToken)
+ .ConfigureAwait(false);
+
+ if (result is not { Response: { } response })
+ {
+ return null;
+ }
+
+ if (response.TryGetFirst(out var singleLocation))
+ {
+ return RoslynLspFactory.CreateLocation(singleLocation.Uri, singleLocation.Range.ToLinePositionSpan());
+ }
+ else if (response.TryGetSecond(out var multipleLocations))
+ {
+ return Array.ConvertAll(multipleLocations, static l => RoslynLspFactory.CreateLocation(l.Uri, l.Range.ToLinePositionSpan()));
+ }
+ else if (response.TryGetThird(out var documentLinks))
+ {
+ using var builder = new PooledArrayBuilder(capacity: documentLinks.Length);
+
+ foreach (var documentLink in documentLinks)
+ {
+ if (documentLink.Target is Uri target)
+ {
+ builder.Add(RoslynLspFactory.CreateDocumentLink(target, documentLink.Range.ToLinePositionSpan()));
+ }
+ }
+
+ return builder.ToArray();
+ }
+
+ return null;
+ }
+
+ internal TestAccessor GetTestAccessor() => new(this);
+
+ internal readonly struct TestAccessor(CohostGoToDefinitionEndpoint instance)
+ {
+ public Task?> HandleRequestAsync(
+ TextDocumentPositionParams request, TextDocument razorDocument, CancellationToken cancellationToken)
+ => instance.HandleRequestAsync(request, razorDocument, cancellationToken);
+ }
+}
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/AutoInsert/PreferHtmlInAttributeValuesDocumentPositionInfoStrategyTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/AutoInsert/PreferHtmlInAttributeValuesDocumentPositionInfoStrategyTest.cs
index 5b72597e19c..c06d4ba3492 100644
--- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/AutoInsert/PreferHtmlInAttributeValuesDocumentPositionInfoStrategyTest.cs
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/AutoInsert/PreferHtmlInAttributeValuesDocumentPositionInfoStrategyTest.cs
@@ -3,7 +3,7 @@
using System;
using System.Threading.Tasks;
-using Microsoft.AspNetCore.Razor.LanguageServer.AutoInsert;
+using Microsoft.CodeAnalysis.Razor.DocumentMapping;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Text;
@@ -46,15 +46,14 @@ internal async Task TryGetPositionInfoAsync_AtVariousPosition_ReturnsCorrectLang
var position = codeDocument.Source.Text.GetPosition(cursorPosition);
var uri = new Uri(razorFilePath);
_ = await CreateLanguageServerAsync(codeDocument, razorFilePath);
- var documentContext = CreateDocumentContext(uri, codeDocument);
// Act
- var result = await PreferHtmlInAttributeValuesDocumentPositionInfoStrategy.Instance.TryGetPositionInfoAsync(
- DocumentMappingService, documentContext, position, DisposalToken);
+ var result = PreferHtmlInAttributeValuesDocumentPositionInfoStrategy.Instance.GetPositionInfo(DocumentMappingService, codeDocument, cursorPosition);
// Assert
- Assert.NotNull(result);
+ Assert.NotEqual(default, result);
Assert.Equal(expectedLanguage, result.LanguageKind);
+
if (expectedLanguage != RazorLanguageKind.CSharp)
{
Assert.Equal(cursorPosition, result.HostDocumentIndex);
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Definition/DefinitionEndpointDelegationTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Definition/DefinitionEndpointDelegationTest.cs
index 2711a8dc359..c5c53d1ff0b 100644
--- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Definition/DefinitionEndpointDelegationTest.cs
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Definition/DefinitionEndpointDelegationTest.cs
@@ -15,8 +15,8 @@
using Xunit;
using Xunit.Abstractions;
using DefinitionResult = Microsoft.VisualStudio.LanguageServer.Protocol.SumType<
- Microsoft.VisualStudio.LanguageServer.Protocol.VSInternalLocation,
- Microsoft.VisualStudio.LanguageServer.Protocol.VSInternalLocation[],
+ Microsoft.VisualStudio.LanguageServer.Protocol.Location,
+ Microsoft.VisualStudio.LanguageServer.Protocol.Location[],
Microsoft.VisualStudio.LanguageServer.Protocol.DocumentLink[]>;
namespace Microsoft.AspNetCore.Razor.LanguageServer.Definition;
@@ -27,17 +27,17 @@ public class DefinitionEndpointDelegationTest(ITestOutputHelper testOutput) : Si
public async Task Handle_SingleServer_CSharp_Method()
{
var input = """
-
- @{
- var x = Ge$$tX();
- }
- @functions
+
+ @{
+ var x = Ge$$tX();
+ }
+ @functions
+ {
+ void [|GetX|]()
{
- void [|GetX|]()
- {
- }
}
- """;
+ }
+ """;
await VerifyCSharpGoToDefinitionAsync(input);
}
@@ -46,19 +46,19 @@ public async Task Handle_SingleServer_CSharp_Method()
public async Task Handle_SingleServer_CSharp_Local()
{
var input = """
-
- @{
- var x = GetX();
- }
- @functions
+
+ @{
+ var x = GetX();
+ }
+ @functions
+ {
+ private string [|_name|];
+ string GetX()
{
- private string [|_name|];
- string GetX()
- {
- return _na$$me;
- }
+ return _na$$me;
}
- """;
+ }
+ """;
await VerifyCSharpGoToDefinitionAsync(input);
}
@@ -67,12 +67,12 @@ string GetX()
public async Task Handle_SingleServer_CSharp_MetadataReference()
{
var input = """
-
- @functions
- {
- private stri$$ng _name;
- }
- """;
+
+ @functions
+ {
+ private stri$$ng _name;
+ }
+ """;
// Arrange
TestFileMarkupParser.GetPosition(input, out var output, out var cursorPosition);
@@ -104,15 +104,15 @@ public async Task Handle_SingleServer_CSharp_MetadataReference()
public async Task Handle_SingleServer_Attribute_SameFile(string method)
{
var input = $$"""
-