Merge remote-tracking branch 'upstream/main' into dev/dawengie/SelfVersionedDocumentSnapshots

This commit is contained in:
David Wengier 2024-08-20 11:01:31 +10:00
Родитель 80ae134f9f e16582150b
Коммит 0c2504b047
52 изменённых файлов: 2024 добавлений и 981 удалений

19
.devcontainer/Dockerfile Normal file
Просмотреть файл

@ -0,0 +1,19 @@
FROM mcr.microsoft.com/dotnet/sdk:8.0
ARG DOTNET_SDK_INSTALL_URL=https://dot.net/v1/dotnet-install.sh
# Install the correct Roslyn SDK into the same directory that the base image installs the SDK in.
ENV DOTNET_INSTALL_DIR=/usr/share/dotnet
# Copy the global.json file so its available in the image before the repo is cloned
COPY global.json /tmp/
RUN cd /tmp \
&& curl --location --output dotnet-install.sh "${DOTNET_SDK_INSTALL_URL}" \
&& chmod +x dotnet-install.sh \
&& mkdir -p "${DOTNET_INSTALL_DIR}" \
&& ./dotnet-install.sh --jsonfile "./global.json" --install-dir "${DOTNET_INSTALL_DIR}" \
&& rm dotnet-install.sh
# Set up machine requirements to build the repo
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install --no-install-recommends curl git

Просмотреть файл

@ -0,0 +1,60 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.140.1/containers/dotnetcore
{
"name": "Razor DevContainer",
"build": {
"dockerfile": "Dockerfile",
// Set the context to the workspace folder to allow us to copy files from it.
"context": ".."
},
"customizations": {
"vscode": {
"settings": {
"files.associations": {
"*.csproj": "msbuild",
"*.fsproj": "msbuild",
"*.globalconfig": "ini",
"*.manifest": "xml",
"*.nuspec": "xml",
"*.pkgdef": "ini",
"*.projitems": "msbuild",
"*.props": "msbuild",
"*.resx": "xml",
"*.rsp": "Powershell",
"*.ruleset": "xml",
"*.settings": "xml",
"*.shproj": "msbuild",
"*.slnf": "json",
"*.targets": "msbuild",
"*.vbproj": "msbuild",
"*.vsixmanifest": "xml",
"*.vstemplate": "xml",
"*.xlf": "xml",
"*.yml": "azure-pipelines"
},
// ms-dotnettools.csharp settings
"omnisharp.disableMSBuildDiagnosticWarning": true,
"omnisharp.enableEditorConfigSupport": true,
"omnisharp.enableImportCompletion": true,
"omnisharp.useModernNet": true,
"omnisharp.enableAsyncCompletion": true,
// ms-dotnettools.csdevkit settings
"dotnet.defaultSolution": "Razor.sln",
// ms-vscode.powershell settings
"powershell.promptToUpdatePowerShell": false,
"powershell.integratedConsole.showOnStartup": false,
"powershell.startAutomatically": false,
// ms-azure-devops.azure-pipelines settings
},
"extensions": [
"ms-dotnettools.csharp",
"ms-dotnettools.csdevkit",
"EditorConfig.EditorConfig",
"ms-vscode.powershell",
"tintoy.msbuild-project-tools",
"ms-azure-devops.azure-pipelines"
]
}
},
"postCreateCommand": "${containerWorkspaceFolder}/restore.sh"
}

3
.vscode/settings.json поставляемый
Просмотреть файл

@ -15,5 +15,6 @@
},
"dotnet.defaultSolution": "Razor.sln",
"omnisharp.defaultLaunchSolution": "Razor.sln",
"files.encoding": "utf8bom"
"files.encoding": "utf8bom",
"dotnet.testWindow.disableBuildOnRefresh": true
}

73
.vscode/tasks.json поставляемый
Просмотреть файл

@ -1,27 +1,52 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "ActivateDotnet",
"type": "shell",
"windows": {
"command": "echo",
"args": [
"Skipping activation on windows"
]
},
"linux": {
"command": "source",
"args": [
"activate.sh"
],
"options": {
"cwd": "${workspaceFolder}"
},
"presentation": {
"reveal": "silent"
}
},
"version": "2.0.0",
"tasks": [
{
"label": "ActivateDotnet",
"type": "shell",
"windows": {
"command": "echo",
"args": [
"Skipping activation on windows"
]
},
"linux": {
"command": "source",
"args": [
"activate.sh"
],
"options": {
"cwd": "${workspaceFolder}"
},
]
"presentation": {
"reveal": "silent"
}
},
},
{
"label": "build.sh",
"command": "./build.sh",
"type": "shell",
"args": [
],
"windows": {
"command": "${workspaceFolder}/build.cmd",
},
"problemMatcher": "$msCompile",
"group": "build"
},
{
"label": "build Rasor.Slim.slnf",
"command": "dotnet",
"type": "shell",
"args": [
"build",
"-p:RunAnalyzersDuringBuild=false",
"-p:GenerateFullPaths=true",
"Razor.Slim.slnf"
],
"problemMatcher": "$msCompile",
"group": "build"
}
]
}

Просмотреть файл

@ -25,6 +25,7 @@
<ServiceHubService Include="Microsoft.VisualStudio.Razor.SignatureHelp" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteSignatureHelpService+Factory" />
<ServiceHubService Include="Microsoft.VisualStudio.Razor.DocumentHighlight" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteDocumentHighlightService+Factory" />
<ServiceHubService Include="Microsoft.VisualStudio.Razor.InlayHint" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteInlayHintService+Factory" />
<ServiceHubService Include="Microsoft.VisualStudio.Razor.GoToDefinition" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteGoToDefinitionService+Factory" />
<ServiceHubService Include="Microsoft.VisualStudio.Razor.Rename" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteRenameService+Factory" />
</ItemGroup>
</Project>

Просмотреть файл

@ -113,12 +113,14 @@ internal abstract class AbstractRazorDelegatingEndpoint<TRequest, TResponse> : I
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 @@ internal abstract class AbstractRazorDelegatingEndpoint<TRequest, TResponse> : I
// 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

Просмотреть файл

@ -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<DocumentPositionInfo?> 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;
}
}

Просмотреть файл

@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Razor.Language.Syntax;
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;

Просмотреть файл

@ -10,6 +10,7 @@ using System.Threading.Tasks;
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;

Просмотреть файл

@ -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<DocumentPositionInfo?> 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);
}
}

Просмотреть файл

@ -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<TextDocumentPositionParams, DefinitionResult?>(languageServerFeatureOptions, documentMappingService, clientConnection, loggerFactory.GetOrCreateLogger<DefinitionEndpoint>()), ICapabilitiesProvider
: AbstractRazorDelegatingEndpoint<TextDocumentPositionParams, DefinitionResult?>(
languageServerFeatureOptions,
documentMappingService,
clientConnection,
loggerFactory.GetOrCreateLogger<DefinitionEndpoint>()), 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 @@ internal sealed class DefinitionEndpoint(
serverCapabilities.DefinitionProvider = new DefinitionOptions();
}
protected async override Task<DefinitionResult?> TryHandleAsync(TextDocumentPositionParams request, RazorRequestContext requestContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken)
protected async override Task<DefinitionResult?> 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<IDelegatedParams?> CreateDelegatedParamsAsync(TextDocumentPositionParams request, RazorRequestContext requestContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken)
protected override Task<IDelegatedParams?> CreateDelegatedParamsAsync(
TextDocumentPositionParams request,
RazorRequestContext requestContext,
DocumentPositionInfo positionInfo,
CancellationToken cancellationToken)
{
var documentContext = requestContext.DocumentContext;
if (documentContext is null)
@ -117,25 +84,30 @@ internal sealed class DefinitionEndpoint(
positionInfo.LanguageKind));
}
protected async override Task<DefinitionResult?> HandleDelegatedResponseAsync(DefinitionResult? response, TextDocumentPositionParams originalRequest, RazorRequestContext requestContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken)
protected async override Task<DefinitionResult?> 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 @@ internal sealed class DefinitionEndpoint(
}
}
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<SyntaxNode>(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: <Component attribute$$></Component>
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 <Component attribute=value />
name = attribute.Name;
propertyName = attribute.TagHelperAttributeInfo.Name;
}
else if (selectedAttribute is MarkupMinimizedTagHelperAttributeSyntax minimizedAttribute)
{
// Minimized attribute, ie <Component attribute />
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<Range> 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<Range?> 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<PropertyDeclarationSyntax>()
.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;
}
}

Просмотреть файл

@ -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<RazorComponentDefinitionService>())
{
}

Просмотреть файл

@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Razor.Language;
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;

Просмотреть файл

@ -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<DocumentPositionInfo?> TryGetPositionInfoAsync(
IDocumentMappingService documentMappingService,
DocumentContext documentContext,
Position position,
CancellationToken cancellationToken);
}

Просмотреть файл

@ -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;
/// <summary>
/// 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.
/// </summary>
internal class PreferAttributeNameDocumentPositionInfoStrategy : IDocumentPositionInfoStrategy
{
public static IDocumentPositionInfoStrategy Instance { get; } = new PreferAttributeNameDocumentPositionInfoStrategy();
public async Task<DocumentPositionInfo?> 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 <Component @bi|nd-Value="Pants" />
// 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);
}
}

Просмотреть файл

@ -156,24 +156,24 @@ internal partial class RazorProjectService : IRazorProjectService, IRazorProject
private void AddDocumentToMiscProjectCore(ProjectSnapshotManager.Updater updater, string filePath)
{
var textDocumentPath = FilePathNormalizer.Normalize(filePath);
_logger.LogDebug($"Asked to add {textDocumentPath} to the miscellaneous files project, because we don't have project info (yet?)");
_logger.LogDebug($"Adding {filePath} to the miscellaneous files project, because we don't have project info (yet?)");
var miscFilesProject = _projectManager.GetMiscellaneousProject();
if (miscFilesProject.GetDocument(FilePathNormalizer.Normalize(textDocumentPath)) is not null)
if (_projectManager.TryResolveDocumentInAnyProject(textDocumentPath, _logger, out var document))
{
// Document already added. This usually occurs when VSCode has already pre-initialized
// open documents and then we try to manually add all known razor documents.
// Already in a known project, so we don't want it in the misc files project
_logger.LogDebug($"File {textDocumentPath} is already in {document.Project.Key}, so we're not adding it to the miscellaneous files project");
return;
}
var miscFilesProject = _projectManager.GetMiscellaneousProject();
// Representing all of our host documents with a re-normalized target path to workaround GetRelatedDocument limitations.
var normalizedTargetFilePath = textDocumentPath.Replace('/', '\\').TrimStart('\\');
var hostDocument = new HostDocument(textDocumentPath, normalizedTargetFilePath);
var textLoader = _remoteTextLoaderFactory.Create(textDocumentPath);
_logger.LogInformation($"Adding document '{filePath}' to project '{miscFilesProject.Key}'.");
_logger.LogInformation($"Adding document '{textDocumentPath}' to project '{miscFilesProject.Key}'.");
updater.DocumentAdded(miscFilesProject.Key, hostDocument, textLoader);
}

Просмотреть файл

@ -25,6 +25,7 @@ using Microsoft.AspNetCore.Razor.LanguageServer.SignatureHelp;
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 @@ internal partial class RazorLanguageServer : SystemTextJsonLanguageServer<RazorR
services.AddHandlerWithCapabilities<ImplementationEndpoint>();
services.AddHandlerWithCapabilities<OnAutoInsertEndpoint>();
services.AddHandlerWithCapabilities<DefinitionEndpoint>();
if (!featureOptions.UseRazorCohostServer)
{
services.AddSingleton<IRazorComponentDefinitionService, RazorComponentDefinitionService>();
services.AddHandlerWithCapabilities<DefinitionEndpoint>();
services.AddSingleton<IRenameService, RenameService>();
services.AddHandlerWithCapabilities<RenameEndpoint>();

Просмотреть файл

@ -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);
}

Просмотреть файл

@ -10,4 +10,4 @@ namespace Microsoft.CodeAnalysis.Razor.DocumentMapping;
/// Represents a position in a document. If <see cref="LanguageKind"/> is Razor then the position will be
/// in the host document, otherwise it will be in the corresponding generated document.
/// </summary>
internal record DocumentPositionInfo(RazorLanguageKind LanguageKind, Position Position, int HostDocumentIndex);
internal readonly record struct DocumentPositionInfo(RazorLanguageKind LanguageKind, Position Position, int HostDocumentIndex);

Просмотреть файл

@ -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);
}

Просмотреть файл

@ -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;
/// <summary>
/// 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.
/// </summary>
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 <Component @bi|nd-Value="Pants" />
// 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);
}
}

Просмотреть файл

@ -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;
}
}

Просмотреть файл

@ -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);
}

Просмотреть файл

@ -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);

Просмотреть файл

@ -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 @@ internal static class RoslynLspFactory
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 };

Просмотреть файл

@ -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);
}

Просмотреть файл

@ -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 @@ internal static class VsLspFactory
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 @@ internal static class VsLspFactory
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;
}
}

Просмотреть файл

@ -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<LspLocation?> 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<LspRange> 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;
}
}

Просмотреть файл

@ -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;
/// <summary>
/// Go to Definition support for Razor components.
/// </summary>
internal interface IRazorComponentDefinitionService
{
Task<LspLocation?> GetDefinitionAsync(IDocumentSnapshot documentSnapshot, DocumentPositionInfo positionInfo, bool ignoreAttributes, CancellationToken cancellationToken);
}

Просмотреть файл

@ -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<RazorSyntaxNode>(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: <Component attribute$$></Component>
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 <Component attribute=value />
nameSpan = attribute.Name.Span;
propertyName = attribute.TagHelperAttributeInfo.Name;
break;
case MarkupMinimizedTagHelperAttributeSyntax minimizedAttribute:
// Minimized attribute, ie <Component attribute />
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<LspRange?> 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<PropertyDeclarationSyntax>()
.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;
}
}
}

Просмотреть файл

@ -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 @@ internal static class RazorSyntaxFacts
return attributeNameSpan != default;
}
private static TextSpan GetFullAttributeNameSpan(SyntaxNode? node)
private static TextSpan GetFullAttributeNameSpan(RazorSyntaxNode? node)
{
return node switch
{
@ -112,7 +113,7 @@ internal static class RazorSyntaxFacts
}
}
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 @@ internal static class RazorSyntaxFacts
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;
}

Просмотреть файл

@ -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<RemoteResponse<RoslynLocation[]?>> GetDefinitionAsync(
JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo,
JsonSerializableDocumentId razorDocumentId,
RoslynPosition position,
CancellationToken cancellationToken);
}

Просмотреть файл

@ -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),

Просмотреть файл

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading;
@ -250,23 +251,7 @@ internal class RenameService(
return default;
}
var node = owner.FirstAncestorOrSelf<RazorSyntaxNode>(n => n.Kind == RazorSyntaxKind.MarkupTagHelperStartTag);
if (node is not MarkupTagHelperStartTagSyntax tagHelperStartTag)
{
return default;
}
// Ensure the rename action was invoked on the component name
// instead of a component parameter. This serves as an issue
// mitigation till `textDocument/prepareRename` is supported
// and we can ensure renames aren't triggered in unsupported
// contexts. (https://github.com/dotnet/aspnetcore/issues/26407)
if (!tagHelperStartTag.Name.FullSpan.IntersectsWith(absoluteIndex))
{
return default;
}
if (tagHelperStartTag.Parent is not MarkupTagHelperElementSyntax { TagHelperInfo.BindingResult: var binding })
if (!TryGetTagHelperBinding(owner, absoluteIndex, out var binding))
{
return default;
}
@ -288,6 +273,43 @@ internal class RenameService(
return [primaryTagHelper, associatedTagHelper];
}
private static bool TryGetTagHelperBinding(RazorSyntaxNode owner, int absoluteIndex, [NotNullWhen(true)] out TagHelperBinding? binding)
{
// End tags are easy, because there is only one possible binding result
if (owner is MarkupTagHelperEndTagSyntax { Parent: MarkupTagHelperElementSyntax { TagHelperInfo.BindingResult: var endTagBindingResult } })
{
binding = endTagBindingResult;
return true;
}
// A rename of a start tag could have an "owner" of one of its attributes, so we do a bit more checking
// to support this case
var node = owner.FirstAncestorOrSelf<RazorSyntaxNode>(n => n.Kind == RazorSyntaxKind.MarkupTagHelperStartTag);
if (node is not MarkupTagHelperStartTagSyntax tagHelperStartTag)
{
binding = null;
return false;
}
// Ensure the rename action was invoked on the component name instead of a component parameter. This serves as an issue
// mitigation till `textDocument/prepareRename` is supported and we can ensure renames aren't triggered in unsupported
// contexts. (https://github.com/dotnet/razor/issues/4285)
if (!tagHelperStartTag.Name.FullSpan.IntersectsWith(absoluteIndex))
{
binding = null;
return false;
}
if (tagHelperStartTag is { Parent: MarkupTagHelperElementSyntax { TagHelperInfo.BindingResult: var startTagBindingResult } })
{
binding = startTagBindingResult;
return true;
}
binding = null;
return false;
}
private static TagHelperDescriptor? FindAssociatedTagHelper(TagHelperDescriptor tagHelper, ImmutableArray<TagHelperDescriptor> tagHelpers)
{
var typeName = tagHelper.GetTypeName();

Просмотреть файл

@ -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<RazorComponentDefinitionService>())
{
}

Просмотреть файл

@ -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<Roslyn.LanguageServer.Protocol.Location[]?>;
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<IRemoteGoToDefinitionService>
{
protected override IRemoteGoToDefinitionService CreateService(in ServiceArgs args)
=> new RemoteGoToDefinitionService(in args);
}
private readonly IRazorComponentDefinitionService _componentDefinitionService = args.ExportProvider.GetExportedValue<IRazorComponentDefinitionService>();
private readonly IFilePathService _filePathService = args.ExportProvider.GetExportedValue<IFilePathService>();
protected override IDocumentPositionInfoStrategy DocumentPositionInfoStrategy => PreferAttributeNameDocumentPositionInfoStrategy.Instance;
public ValueTask<RemoteResponse<RoslynLocation[]?>> GetDefinitionAsync(
JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo,
JsonSerializableDocumentId documentId,
RoslynPosition position,
CancellationToken cancellationToken)
=> RunServiceAsync(
solutionInfo,
documentId,
context => GetDefinitionAsync(context, position, cancellationToken),
cancellationToken);
private async ValueTask<RemoteResponse<RoslynLocation[]?>> 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<RoslynLocation>(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());
}
}

Просмотреть файл

@ -9,7 +9,6 @@ using Microsoft.AspNetCore.Razor.Language.Syntax;
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 @@ internal sealed partial class RemoteInlayHintService(in ServiceArgs args) : Razo
=> new RemoteInlayHintService(in args);
}
private readonly IDocumentMappingService _documentMappingService = args.ExportProvider.GetExportedValue<IDocumentMappingService>();
private readonly IFilePathService _filePathService = args.ExportProvider.GetExportedValue<IFilePathService>();
public ValueTask<InlayHint[]?> GetInlayHintsAsync(JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, JsonSerializableDocumentId razorDocumentId, InlayHintParams inlayHintParams, bool displayAllOverride, CancellationToken cancellationToken)
@ -47,7 +45,7 @@ internal sealed partial class RemoteInlayHintService(in ServiceArgs args) : Razo
// 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 @@ internal sealed partial class RemoteInlayHintService(in ServiceArgs args) : Razo
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 @@ internal sealed partial class RemoteInlayHintService(in ServiceArgs args) : Razo
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();
}

Просмотреть файл

@ -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<DocumentSnapshotFactory>();
protected IDocumentMappingService DocumentMappingService { get; } = args.ExportProvider.GetExportedValue<IDocumentMappingService>();
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<T> RunServiceAsync<T>(
RazorPinnedSolutionInfoWrapper solutionInfo,

Просмотреть файл

@ -11,8 +11,8 @@ using Microsoft.CodeAnalysis.Razor.Rename;
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<Microsoft.VisualStudio.LanguageServer.Protocol.WorkspaceEdit?>;
using ExternalHandlers = Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers;
namespace Microsoft.CodeAnalysis.Remote.Razor;
@ -26,7 +26,6 @@ internal sealed class RemoteRenameService(in ServiceArgs args) : RazorDocumentSe
private readonly IRenameService _renameService = args.ExportProvider.GetExportedValue<IRenameService>();
private readonly IFilePathService _filePathService = args.ExportProvider.GetExportedValue<IFilePathService>();
private readonly IDocumentMappingService _documentMappingService = args.ExportProvider.GetExportedValue<IDocumentMappingService>();
private readonly IEditMappingService _editMappingService = args.ExportProvider.GetExportedValue<IEditMappingService>();
public ValueTask<RemoteResponse<WorkspaceEdit?>> GetRenameEditAsync(
@ -48,10 +47,13 @@ internal sealed class RemoteRenameService(in ServiceArgs args) : RazorDocumentSe
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)

Просмотреть файл

@ -5,7 +5,6 @@ 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.Razor.Remote;
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem;
@ -26,7 +25,6 @@ internal sealed class RemoteSignatureHelpService(in ServiceArgs args) : RazorDoc
}
private readonly IFilePathService _filePathService = args.ExportProvider.GetExportedValue<IFilePathService>();
private readonly IDocumentMappingService _documentMappingService = args.ExportProvider.GetExportedValue<IDocumentMappingService>();
public ValueTask<SignatureHelp?> GetSignatureHelpAsync(JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, JsonSerializableDocumentId documentId, Position position, CancellationToken cancellationToken)
=> RunServiceAsync(
@ -43,7 +41,7 @@ internal sealed class RemoteSignatureHelpService(in ServiceArgs args) : RazorDoc
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);
}

Просмотреть файл

@ -5,7 +5,6 @@ using System;
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 @@ internal sealed partial class RemoteUriPresentationService(in ServiceArgs args)
=> new RemoteUriPresentationService(in args);
}
private readonly IDocumentMappingService _documentMappingService = args.ExportProvider.GetExportedValue<IDocumentMappingService>();
public ValueTask<Response> GetPresentationAsync(
RazorPinnedSolutionInfoWrapper solutionInfo,
DocumentId razorDocumentId,
@ -54,7 +51,7 @@ internal sealed partial class RemoteUriPresentationService(in ServiceArgs args)
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,

Просмотреть файл

@ -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<TextDocumentPositionParams, SumType<RoslynLocation, RoslynLocation[], RoslynDocumentLink[]>?>, 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<SumType<RoslynLocation, RoslynLocation[], RoslynDocumentLink[]>?> HandleRequestAsync(TextDocumentPositionParams request, RazorCohostRequestContext context, CancellationToken cancellationToken)
=> HandleRequestAsync(
request,
context.TextDocument.AssumeNotNull(),
cancellationToken);
private async Task<SumType<RoslynLocation, RoslynLocation[], RoslynDocumentLink[]>?> HandleRequestAsync(TextDocumentPositionParams request, TextDocument razorDocument, CancellationToken cancellationToken)
{
var position = RoslynLspFactory.CreatePosition(request.Position.ToLinePosition());
var response = await _remoteServiceInvoker
.TryInvokeAsync<IRemoteGoToDefinitionService, RemoteResponse<RoslynLocation[]?>>(
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<SumType<RoslynLocation, RoslynLocation[], RoslynDocumentLink[]>?> 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<TextDocumentPositionParams, SumType<VsLspLocation, VsLspLocation[], DocumentLink[]>?>(
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<RoslynDocumentLink>(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<SumType<RoslynLocation, RoslynLocation[], RoslynDocumentLink[]>?> HandleRequestAsync(
TextDocumentPositionParams request, TextDocument razorDocument, CancellationToken cancellationToken)
=> instance.HandleRequestAsync(request, razorDocument, cancellationToken);
}
}

Просмотреть файл

@ -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 @@ public class PreferHtmlInAttributeValuesDocumentPositionInfoStrategyTest(ITestOu
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);

Просмотреть файл

@ -15,8 +15,8 @@ using Microsoft.VisualStudio.LanguageServer.Protocol;
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 = """
<div></div>
@{
var x = Ge$$tX();
}
@functions
<div></div>
@{
var x = Ge$$tX();
}
@functions
{
void [|GetX|]()
{
void [|GetX|]()
{
}
}
""";
}
""";
await VerifyCSharpGoToDefinitionAsync(input);
}
@ -46,19 +46,19 @@ public class DefinitionEndpointDelegationTest(ITestOutputHelper testOutput) : Si
public async Task Handle_SingleServer_CSharp_Local()
{
var input = """
<div></div>
@{
var x = GetX();
}
@functions
<div></div>
@{
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 @@ public class DefinitionEndpointDelegationTest(ITestOutputHelper testOutput) : Si
public async Task Handle_SingleServer_CSharp_MetadataReference()
{
var input = """
<div></div>
@functions
{
private stri$$ng _name;
}
""";
<div></div>
@functions
{
private stri$$ng _name;
}
""";
// Arrange
TestFileMarkupParser.GetPosition(input, out var output, out var cursorPosition);
@ -104,15 +104,15 @@ public class DefinitionEndpointDelegationTest(ITestOutputHelper testOutput) : Si
public async Task Handle_SingleServer_Attribute_SameFile(string method)
{
var input = $$"""
<button @onclick="{{method}}"></div>
<button @onclick="{{method}}"></div>
@code
@code
{
void [|IncrementCount|]()
{
void [|IncrementCount|]()
{
}
}
""";
}
""";
await VerifyCSharpGoToDefinitionAsync(input, "test.razor");
}
@ -146,30 +146,30 @@ public class DefinitionEndpointDelegationTest(ITestOutputHelper testOutput) : Si
public async Task Handle_SingleServer_ComponentAttribute_OtherRazorFile(string attribute)
{
var input = $$"""
<SurveyPrompt {{attribute}}="InputValue" />
<SurveyPrompt {{attribute}}="InputValue" />
@code
@code
{
private string? InputValue { get; set; }
private void BindAfter()
{
private string? InputValue { get; set; }
private void BindAfter()
{
}
}
""";
}
""";
// Need to put this in the right namespace, to match the tag helper defined in our test json
var surveyPrompt = """
@namespace BlazorApp1.Shared
@namespace BlazorApp1.Shared
<div></div>
<div></div>
@code
{
[Parameter]
public string [|Title|] { get; set; }
}
""";
@code
{
[Parameter]
public string [|Title|] { get; set; }
}
""";
TestFileMarkupParser.GetSpan(surveyPrompt, out surveyPrompt, out var expectedSpan);
var additionalRazorDocuments = new[]
@ -237,13 +237,14 @@ public class DefinitionEndpointDelegationTest(ITestOutputHelper testOutput) : Si
rootNamespace: "project"));
});
var searchEngine = new RazorComponentSearchEngine(projectManager, LoggerFactory);
var componentSearchEngine = new RazorComponentSearchEngine(projectManager, LoggerFactory);
var componentDefinitionService = new RazorComponentDefinitionService(componentSearchEngine, DocumentMappingService, LoggerFactory);
var razorUri = new Uri(razorFilePath);
Assert.True(DocumentContextFactory.TryCreate(razorUri, out var documentContext));
var requestContext = CreateRazorRequestContext(documentContext);
var endpoint = new DefinitionEndpoint(searchEngine, DocumentMappingService, LanguageServerFeatureOptions, languageServer, LoggerFactory);
var endpoint = new DefinitionEndpoint(componentDefinitionService, DocumentMappingService, LanguageServerFeatureOptions, languageServer, LoggerFactory);
var request = new TextDocumentPositionParams
{

Просмотреть файл

@ -1,418 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
#nullable disable
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.LanguageServer.Completion;
using Microsoft.AspNetCore.Razor.Test.Common;
using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Moq;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Razor.LanguageServer.Definition;
public class DefinitionEndpointTest(ITestOutputHelper testOutput) : TagHelperServiceTestBase(testOutput)
{
[Fact]
public async Task GetOriginTagHelperBindingAsync_TagHelper_Element()
{
var content = """
@addTagHelper *, TestAssembly
<te$$st1></test1>
""";
await VerifyOriginTagHelperBindingAsync(content, "Test1TagHelper", isRazorFile: false);
}
[Fact]
public async Task GetOriginTagHelperBindingAsync_TagHelper_StartTag_WithAttribute()
{
var content = """
@addTagHelper *, TestAssembly
<Co$$mponent1 @test="Increment"></Component1>
@code {
public void Increment()
{
}
}
""";
await VerifyOriginTagHelperBindingAsync(content, "Component1TagHelper");
}
[Fact]
public async Task GetOriginTagHelperBindingAsync_TagHelper_EndTag_WithAttribute()
{
var content = """
@addTagHelper *, TestAssembly
<Component1 @test="Increment"></Comp$$onent1>
@code {
public void Increment()
{
}
}
""";
await VerifyOriginTagHelperBindingAsync(content, "Component1TagHelper");
}
[Fact]
public async Task GetOriginTagHelperBindingAsync_TagHelper_Attribute_ReturnsNull()
{
var content = """
@addTagHelper *, TestAssembly
<Component1 @te$$st="Increment"></Component1>
@code {
public void Increment()
{
}
}
""";
await VerifyOriginTagHelperBindingAsync(content);
}
[Fact]
public async Task GetOriginTagHelperBindingAsync_TagHelper_AttributeValue_ReturnsNull()
{
var content = """
@addTagHelper *, TestAssembly
<Component1 @test="Increm$$ent"></Component1>
@code {
public void Increment()
{
}
}
""";
await VerifyOriginTagHelperBindingAsync(content);
}
[Fact]
public async Task GetOriginTagHelperBindingAsync_TagHelper_AfterAttributeEquals_ReturnsNull()
{
var content = """
@addTagHelper *, TestAssembly
<Component1 @test="$$Increment"></Component1>
@code {
public void Increment()
{
}
}
""";
await VerifyOriginTagHelperBindingAsync(content);
}
[Fact]
public async Task GetOriginTagHelperBindingAsync_TagHelper_AttributeEnd_ReturnsNull()
{
var content = """
@addTagHelper *, TestAssembly
<Component1 @test="Increment">$$</Component1>
@code {
public void Increment()
{
}
}
""";
await VerifyOriginTagHelperBindingAsync(content);
}
[Fact]
public async Task GetOriginTagHelperBindingAsync_TagHelper_MultipleAttributes()
{
var content = """
@addTagHelper *, TestAssembly
<Co$$mponent1 @test="Increment" @minimized></Component1>
@code {
public void Increment()
{
}
}
""";
await VerifyOriginTagHelperBindingAsync(content, "Component1TagHelper");
}
[Fact]
public async Task GetOriginTagHelperBindingAsync_TagHelper_MalformedElement()
{
var content = """
@addTagHelper *, TestAssembly
<Co$$mponent1</Component1>
@code {
public void Increment()
{
}
}
""";
await VerifyOriginTagHelperBindingAsync(content, "Component1TagHelper");
}
[Fact]
public async Task GetOriginTagHelperBindingAsync_TagHelper_MalformedAttribute()
{
var content = """
@addTagHelper *, TestAssembly
<Co$$mponent1 @test="Increment></Component1>
@code {
public void Increment()
{
}
}
""";
await VerifyOriginTagHelperBindingAsync(content, "Component1TagHelper");
}
[Fact]
public async Task GetOriginTagHelperBindingAsync_HTML_MarkupElement()
{
var content = """
@addTagHelper *, TestAssembly
<p>
<str$$ong></strong>
</p>
""";
await VerifyOriginTagHelperBindingAsync(content);
}
[Fact]
public async Task GetOriginTagHelperBindingAsync_IgnoreAttribute_PropertyAttribute()
{
var content = """
@addTagHelper *, TestAssembly
<Component1 boo$$l-val="true"></Component1>
@code {
public void Increment()
{
}
}
""";
await VerifyOriginTagHelperBindingAsync(content, ignoreAttributes: true, tagHelperDescriptorName: null, attributeDescriptorPropertyName: null);
}
[Fact]
public async Task GetOriginTagHelperBindingAsync_TagHelper_PropertyAttribute()
{
var content = """
@addTagHelper *, TestAssembly
<Component1 boo$$l-val="true"></Component1>
@code {
public void Increment()
{
}
}
""";
await VerifyOriginTagHelperBindingAsync(content, "Component1TagHelper", "BoolVal");
}
[Fact]
public async Task GetOriginTagHelperBindingAsync_TagHelper_MinimizedPropertyAttribute()
{
var content = """
@addTagHelper *, TestAssembly
<Component1 boo$$l-val></Component1>
@code {
public void Increment()
{
}
}
""";
await VerifyOriginTagHelperBindingAsync(content, "Component1TagHelper", "BoolVal");
}
[Fact]
public async Task GetOriginTagHelperBindingAsync_TagHelper_MinimizedPropertyAttributeEdge1()
{
var content = """
@addTagHelper *, TestAssembly
<Component1 $$bool-val></Component1>
@code {
public void Increment()
{
}
}
""";
await VerifyOriginTagHelperBindingAsync(content, "Component1TagHelper", "BoolVal");
}
[Fact]
public async Task GetOriginTagHelperBindingAsync_TagHelper_MinimizedPropertyAttributeEdge2()
{
var content = """
@addTagHelper *, TestAssembly
<Component1 bool-val$$></Component1>
@code {
public void Increment()
{
}
}
""";
await VerifyOriginTagHelperBindingAsync(content, "Component1TagHelper", "BoolVal");
}
[Fact, WorkItem("https://github.com/dotnet/razor-tooling/issues/6775")]
public async Task GetOriginTagHelperBindingAsync_TagHelper_PropertyAttributeEdge()
{
var content = """
@addTagHelper *, TestAssembly
<Component1 bool-val$$="true"></Component1>
@code {
public void Increment()
{
}
}
""";
await VerifyOriginTagHelperBindingAsync(content, "Component1TagHelper", "BoolVal");
}
[Fact]
public async Task GetNavigatePositionAsync_TagHelperProperty_CorrectRange1()
{
var content = """
<div>@Title</div>
@code
{
[Parameter]
public string NotTitle { get; set; }
[Parameter]
public string [|Title|] { get; set; }
}
""";
await VerifyNavigatePositionAsync(content, "Title");
}
[Fact]
public async Task GetNavigatePositionAsync_TagHelperProperty_CorrectRange2()
{
var content = """
<div>@Title</div>
@code
{
[Microsoft.AspNetCore.Components.Parameter]
public string [|Title|] { get; set; }
}
""";
await VerifyNavigatePositionAsync(content, "Title");
}
[Fact]
public async Task GetNavigatePositionAsync_TagHelperProperty_CorrectRange3()
{
var content = """
<div>@Title</div>
@code
{
[Components.ParameterAttribute]
public string [|Title|] { get; set; }
}
""";
await VerifyNavigatePositionAsync(content, "Title");
}
[Fact]
public async Task GetNavigatePositionAsync_TagHelperProperty_IgnoreInnerProperty()
{
var content = """
<div>@Title</div>
@code
{
private class NotTheDroidsYoureLookingFor
{
public string Title { get; set; }
}
public string [|Title|] { get; set; }
}
""";
await VerifyNavigatePositionAsync(content, "Title");
}
#region Helpers
private async Task VerifyOriginTagHelperBindingAsync(string content, string tagHelperDescriptorName = null, string attributeDescriptorPropertyName = null, bool isRazorFile = true, bool ignoreAttributes = false)
{
TestFileMarkupParser.GetPosition(content, out content, out var position);
SetupDocument(out _, out var documentSnapshot, content, isRazorFile);
var documentContext = CreateDocumentContext(new Uri(@"C:\file.razor"), documentSnapshot);
var (descriptor, attributeDescriptor) = await DefinitionEndpoint.GetOriginTagHelperBindingAsync(
documentContext, position, ignoreAttributes, LoggerFactory.GetOrCreateLogger("RazorDefinitionEndpoint"), DisposalToken);
if (tagHelperDescriptorName is null)
{
Assert.Null(descriptor);
}
else
{
Assert.NotNull(descriptor);
Assert.Equal(tagHelperDescriptorName, descriptor!.Name);
}
if (attributeDescriptorPropertyName is null)
{
Assert.Null(attributeDescriptor);
}
else
{
Assert.NotNull(attributeDescriptor);
Assert.Equal(attributeDescriptorPropertyName, attributeDescriptor.GetPropertyName());
}
}
private async Task VerifyNavigatePositionAsync(string content, string propertyName)
{
TestFileMarkupParser.GetSpan(content, out content, out var selection);
SetupDocument(out var codeDocument, out _, content);
var expectedRange = codeDocument.Source.Text.GetRange(selection);
var mappingService = new LspDocumentMappingService(FilePathService, new TestDocumentContextFactory(), LoggerFactory);
var range = await DefinitionEndpoint.TryGetPropertyRangeAsync(codeDocument, propertyName, mappingService, Logger, DisposalToken);
Assert.NotNull(range);
Assert.Equal(expectedRange, range);
}
private void SetupDocument(out RazorCodeDocument codeDocument, out IDocumentSnapshot documentSnapshot, string content, bool isRazorFile = true)
{
var sourceText = SourceText.From(content);
codeDocument = CreateCodeDocument(content, isRazorFile, DefaultTagHelpers);
var outDoc = codeDocument;
documentSnapshot = Mock.Of<IDocumentSnapshot>(
d => d.GetTextAsync() == Task.FromResult(sourceText),
MockBehavior.Strict);
Mock.Get(documentSnapshot)
.Setup(s => s.GetGeneratedOutputAsync())
.ReturnsAsync(outDoc);
}
#endregion
}

Просмотреть файл

@ -0,0 +1,402 @@
// 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.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.LanguageServer.Completion;
using Microsoft.AspNetCore.Razor.Test.Common;
using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer;
using Microsoft.CodeAnalysis.Razor.GoToDefinition;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Razor.LanguageServer.Definition;
public class RazorComponentDefinitionHelpersTest(ITestOutputHelper testOutput) : TagHelperServiceTestBase(testOutput)
{
[Fact]
public void TryGetBoundTagHelpers_TagHelper_Element()
{
var content = """
@addTagHelper *, TestAssembly
<te$$st1></test1>
""";
VerifyTryGetBoundTagHelpers(content, "Test1TagHelper", isRazorFile: false);
}
[Fact]
public void TryGetBoundTagHelpers_TagHelper_StartTag_WithAttribute()
{
var content = """
@addTagHelper *, TestAssembly
<Co$$mponent1 @test="Increment"></Component1>
@code {
public void Increment()
{
}
}
""";
VerifyTryGetBoundTagHelpers(content, "Component1TagHelper");
}
[Fact]
public void TryGetBoundTagHelpers_TagHelper_EndTag_WithAttribute()
{
var content = """
@addTagHelper *, TestAssembly
<Component1 @test="Increment"></Comp$$onent1>
@code {
public void Increment()
{
}
}
""";
VerifyTryGetBoundTagHelpers(content, "Component1TagHelper");
}
[Fact]
public void TryGetBoundTagHelpers_TagHelper_Attribute_ReturnsNull()
{
var content = """
@addTagHelper *, TestAssembly
<Component1 @te$$st="Increment"></Component1>
@code {
public void Increment()
{
}
}
""";
VerifyTryGetBoundTagHelpers(content);
}
[Fact]
public void TryGetBoundTagHelpers_TagHelper_AttributeValue_ReturnsNull()
{
var content = """
@addTagHelper *, TestAssembly
<Component1 @test="Increm$$ent"></Component1>
@code {
public void Increment()
{
}
}
""";
VerifyTryGetBoundTagHelpers(content);
}
[Fact]
public void TryGetBoundTagHelpers_TagHelper_AfterAttributeEquals_ReturnsNull()
{
var content = """
@addTagHelper *, TestAssembly
<Component1 @test="$$Increment"></Component1>
@code {
public void Increment()
{
}
}
""";
VerifyTryGetBoundTagHelpers(content);
}
[Fact]
public void TryGetBoundTagHelpers_TagHelper_AttributeEnd_ReturnsNull()
{
var content = """
@addTagHelper *, TestAssembly
<Component1 @test="Increment">$$</Component1>
@code {
public void Increment()
{
}
}
""";
VerifyTryGetBoundTagHelpers(content);
}
[Fact]
public void TryGetBoundTagHelpers_TagHelper_MultipleAttributes()
{
var content = """
@addTagHelper *, TestAssembly
<Co$$mponent1 @test="Increment" @minimized></Component1>
@code {
public void Increment()
{
}
}
""";
VerifyTryGetBoundTagHelpers(content, "Component1TagHelper");
}
[Fact]
public void TryGetBoundTagHelpers_TagHelper_MalformedElement()
{
var content = """
@addTagHelper *, TestAssembly
<Co$$mponent1</Component1>
@code {
public void Increment()
{
}
}
""";
VerifyTryGetBoundTagHelpers(content, "Component1TagHelper");
}
[Fact]
public void TryGetBoundTagHelpers_TagHelper_MalformedAttribute()
{
var content = """
@addTagHelper *, TestAssembly
<Co$$mponent1 @test="Increment></Component1>
@code {
public void Increment()
{
}
}
""";
VerifyTryGetBoundTagHelpers(content, "Component1TagHelper");
}
[Fact]
public void TryGetBoundTagHelpers_HTML_MarkupElement()
{
var content = """
@addTagHelper *, TestAssembly
<p>
<str$$ong></strong>
</p>
""";
VerifyTryGetBoundTagHelpers(content);
}
[Fact]
public void TryGetBoundTagHelpers_IgnoreAttribute_PropertyAttribute()
{
var content = """
@addTagHelper *, TestAssembly
<Component1 boo$$l-val="true"></Component1>
@code {
public void Increment()
{
}
}
""";
VerifyTryGetBoundTagHelpers(content, ignoreAttributes: true);
}
[Fact]
public void TryGetBoundTagHelpers_TagHelper_PropertyAttribute()
{
var content = """
@addTagHelper *, TestAssembly
<Component1 boo$$l-val="true"></Component1>
@code {
public void Increment()
{
}
}
""";
VerifyTryGetBoundTagHelpers(content, "Component1TagHelper", "BoolVal");
}
[Fact]
public void TryGetBoundTagHelpers_TagHelper_MinimizedPropertyAttribute()
{
var content = """
@addTagHelper *, TestAssembly
<Component1 boo$$l-val></Component1>
@code {
public void Increment()
{
}
}
""";
VerifyTryGetBoundTagHelpers(content, "Component1TagHelper", "BoolVal");
}
[Fact]
public void TryGetBoundTagHelpers_TagHelper_MinimizedPropertyAttributeEdge1()
{
var content = """
@addTagHelper *, TestAssembly
<Component1 $$bool-val></Component1>
@code {
public void Increment()
{
}
}
""";
VerifyTryGetBoundTagHelpers(content, "Component1TagHelper", "BoolVal");
}
[Fact]
public void TryGetBoundTagHelpers_TagHelper_MinimizedPropertyAttributeEdge2()
{
var content = """
@addTagHelper *, TestAssembly
<Component1 bool-val$$></Component1>
@code {
public void Increment()
{
}
}
""";
VerifyTryGetBoundTagHelpers(content, "Component1TagHelper", "BoolVal");
}
[Fact, WorkItem("https://github.com/dotnet/razor-tooling/issues/6775")]
public void TryGetBoundTagHelpers_TagHelper_PropertyAttributeEdge()
{
var content = """
@addTagHelper *, TestAssembly
<Component1 bool-val$$="true"></Component1>
@code {
public void Increment()
{
}
}
""";
VerifyTryGetBoundTagHelpers(content, "Component1TagHelper", "BoolVal");
}
[Fact]
public async Task TryGetPropertyRangeAsync_TagHelperProperty_CorrectRange1()
{
var content = """
<div>@Title</div>
@code
{
[Parameter]
public string NotTitle { get; set; }
[Parameter]
public string [|Title|] { get; set; }
}
""";
await VerifyTryGetPropertyRangeAsync(content, "Title");
}
[Fact]
public async Task TryGetPropertyRangeAsync_TagHelperProperty_CorrectRange2()
{
var content = """
<div>@Title</div>
@code
{
[Microsoft.AspNetCore.Components.Parameter]
public string [|Title|] { get; set; }
}
""";
await VerifyTryGetPropertyRangeAsync(content, "Title");
}
[Fact]
public async Task TryGetPropertyRangeAsync_TagHelperProperty_CorrectRange3()
{
var content = """
<div>@Title</div>
@code
{
[Components.ParameterAttribute]
public string [|Title|] { get; set; }
}
""";
await VerifyTryGetPropertyRangeAsync(content, "Title");
}
[Fact]
public async Task TryGetPropertyRangeAsync_TagHelperProperty_IgnoreInnerProperty()
{
var content = """
<div>@Title</div>
@code
{
private class NotTheDroidsYoureLookingFor
{
public string Title { get; set; }
}
public string [|Title|] { get; set; }
}
""";
await VerifyTryGetPropertyRangeAsync(content, "Title");
}
private void VerifyTryGetBoundTagHelpers(
string content,
string? tagHelperDescriptorName = null,
string? attributeDescriptorPropertyName = null,
bool isRazorFile = true,
bool ignoreAttributes = false)
{
TestFileMarkupParser.GetPosition(content, out content, out var position);
var codeDocument = CreateCodeDocument(content, isRazorFile);
var result = RazorComponentDefinitionHelpers.TryGetBoundTagHelpers(codeDocument, position, ignoreAttributes, Logger, out var boundTagHelper, out var boundAttribute);
if (tagHelperDescriptorName is null)
{
Assert.False(result);
}
else
{
Assert.True(result);
Assert.NotNull(boundTagHelper);
Assert.Equal(tagHelperDescriptorName, boundTagHelper.Name);
}
if (attributeDescriptorPropertyName is not null)
{
Assert.True(result);
Assert.NotNull(boundAttribute);
Assert.Equal(attributeDescriptorPropertyName, boundAttribute.GetPropertyName());
}
}
private async Task VerifyTryGetPropertyRangeAsync(string content, string propertyName)
{
TestFileMarkupParser.GetSpan(content, out content, out var selection);
var codeDocument = CreateCodeDocument(content);
var expectedRange = codeDocument.Source.Text.GetRange(selection);
var documentMappingService = new LspDocumentMappingService(FilePathService, new TestDocumentContextFactory(), LoggerFactory);
var range = await RazorComponentDefinitionHelpers.TryGetPropertyRangeAsync(codeDocument, propertyName, documentMappingService, Logger, DisposalToken);
Assert.NotNull(range);
Assert.Equal(expectedRange, range);
}
private RazorCodeDocument CreateCodeDocument(string content, bool isRazorFile = true)
=> CreateCodeDocument(content, isRazorFile, DefaultTagHelpers);
}

Просмотреть файл

@ -730,6 +730,47 @@ public class RazorProjectServiceTest(ITestOutputHelper testOutput) : LanguageSer
Assert.False(_projectManager.IsDocumentOpen(DocumentFilePath));
}
[Fact]
public async Task AddDocumentToMiscProjectAsync_IgnoresKnownDocument()
{
// Arrange
const string ProjectFilePath = "C:/path/to/project.csproj";
const string IntermediateOutputPath = "C:/path/to/obj";
const string RootNamespace = "TestRootNamespace";
const string DocumentFilePath = "C:/path/to/document.cshtml";
await _projectService.AddProjectAsync(
ProjectFilePath, IntermediateOutputPath, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken);
await _projectService.AddDocumentToPotentialProjectsAsync(DocumentFilePath, DisposalToken);
// Act
using var listener = _projectManager.ListenToNotifications();
// Act
await _projectService.AddDocumentToMiscProjectAsync(DocumentFilePath, DisposalToken);
// Assert
listener.AssertNoNotifications();
}
[Fact]
public async Task AddDocumentToMiscProjectAsync_IgnoresKnownDocument_InMiscFiles()
{
// Arrange
const string DocumentFilePath = "C:/path/to/document.cshtml";
await _projectService.AddDocumentToMiscProjectAsync(DocumentFilePath, DisposalToken);
// Act
using var listener = _projectManager.ListenToNotifications();
// Act
await _projectService.AddDocumentToMiscProjectAsync(DocumentFilePath, DisposalToken);
// Assert
listener.AssertNoNotifications();
}
[Fact]
public async Task RemoveDocument_RemovesDocumentFromOwnerProject()
{

Просмотреть файл

@ -265,6 +265,29 @@ public class RenameEndpointTest(ITestOutputHelper testOutput) : LanguageServerTe
Assert.NotNull(result);
}
[Fact]
public async Task Handle_Rename_OnComponentEndTag_ReturnsResult()
{
// Arrange
var (endpoint, documentContextFactory) = await CreateEndpointAndDocumentContextFactoryAsync();
var uri = PathUtilities.GetUri(s_componentWithParamFilePath);
var request = new RenameParams
{
TextDocument = new() { Uri = uri },
Position = VsLspFactory.CreatePosition(1, 36),
NewName = "Test2"
};
Assert.True(documentContextFactory.TryCreateForOpenDocument(uri, out var documentContext));
var requestContext = CreateRazorRequestContext(documentContext);
// Act
var result = await endpoint.HandleRequestAsync(request, requestContext, DisposalToken);
// Assert
Assert.NotNull(result);
}
[Fact]
public async Task Handle_Rename_OnComponentNameTrailingEdge_ReturnsResult()
{

Просмотреть файл

@ -16,8 +16,8 @@ using Microsoft.CodeAnalysis.Razor.Protocol.Folding;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Xunit;
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 ImplementationResult = Microsoft.VisualStudio.LanguageServer.Protocol.SumType<
Microsoft.VisualStudio.LanguageServer.Protocol.Location[],

Просмотреть файл

@ -0,0 +1,55 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.AspNetCore.Razor.Test.Common;
internal readonly struct TestCode
{
public string Text { get; }
public ImmutableArray<int> Positions { get; }
private readonly ImmutableDictionary<string, ImmutableArray<TextSpan>> _nameToSpanMap;
public TestCode(string input, bool treatPositionIndicatorsAsCode = false)
{
if (treatPositionIndicatorsAsCode)
{
TestFileMarkupParser.GetSpans(input, treatPositionIndicatorsAsCode, out var text, out var nameToSpanMap);
Text = text;
Positions = [];
_nameToSpanMap = nameToSpanMap;
}
else
{
TestFileMarkupParser.GetPositionsAndSpans(input, out var text, out var positions, out var spans);
Text = text;
Positions = positions;
_nameToSpanMap = spans;
}
}
public int Position
=> Positions.Single();
public TextSpan Span
=> Spans.Single();
public ImmutableArray<TextSpan> Spans
=> GetNamedSpans(string.Empty);
public ImmutableArray<TextSpan> GetNamedSpans(string name)
=> _nameToSpanMap[name];
public bool TryGetNamedSpans(string name, out ImmutableArray<TextSpan> spans)
=> _nameToSpanMap.TryGetValue(name, out spans);
public static implicit operator TestCode(string input)
=> new(input);
}

Просмотреть файл

@ -0,0 +1,282 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Test.Common;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Xunit;
using Xunit.Abstractions;
using RoslynDocumentLink = Roslyn.LanguageServer.Protocol.DocumentLink;
using RoslynLocation = Roslyn.LanguageServer.Protocol.Location;
using RoslynLspExtensions = Roslyn.LanguageServer.Protocol.RoslynLspExtensions;
namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
public class CohostGoToDefinitionEndpointTest(ITestOutputHelper testOutputHelper) : CohostEndpointTestBase(testOutputHelper)
{
[Fact]
public async Task CSharp_Method()
{
var input = """
<div></div>
@{
var x = Ge$$tX();
}
@functions
{
void [|GetX|]()
{
}
}
""";
await VerifyGoToDefinitionAsync(input);
}
[Fact]
public async Task CSharp_Local()
{
var input = """
<div></div>
@{
var x = GetX();
}
@functions
{
private string [|_name|];
string GetX()
{
return _na$$me;
}
}
""";
await VerifyGoToDefinitionAsync(input);
}
[Fact]
public async Task CSharp_MetadataReference()
{
var input = """
<div></div>
@functions
{
private stri$$ng _name;
}
""";
var result = await GetGoToDefinitionResultAsync(input);
Assert.NotNull(result.Value.Second);
var locations = result.Value.Second;
var location = Assert.Single(locations);
Assert.EndsWith("String.cs", location.Uri.ToString());
// Note: The location is in a generated C# "metadata-as-source" file, which has a different
// number of using directives in .NET Framework vs. .NET Core, so rather than relying on line
// numbers we do some vague notion of actual navigation and test the actual source line that
// the user would see.
var line = File.ReadLines(location.Uri.LocalPath).ElementAt(location.Range.Start.Line);
Assert.Contains("public sealed class String", line);
}
[Theory]
[InlineData("$$IncrementCount")]
[InlineData("In$$crementCount")]
[InlineData("IncrementCount$$")]
public async Task Attribute_SameFile(string method)
{
var input = $$"""
<button @onclick="{{method}}"></div>
@code
{
void [|IncrementCount|]()
{
}
}
""";
await VerifyGoToDefinitionAsync(input, FileKinds.Component);
}
[Fact]
public async Task AttributeValue_BindAfter()
{
var input = """
<input type="text" @bind="InputValue" @bind:after="() => Af$$ter()">
@code
{
public string InputValue { get; set; }
public void [|After|]()
{
}
}
""";
await VerifyGoToDefinitionAsync(input, FileKinds.Component);
}
[Theory]
[InlineData("Ti$$tle")]
[InlineData("$$@bind-Title")]
[InlineData("@$$bind-Title")]
[InlineData("@bi$$nd-Title")]
[InlineData("@bind$$-Title")]
[InlineData("@bind-Ti$$tle")]
public async Task OtherRazorFile(string attribute)
{
TestCode input = $$"""
<SurveyPrompt {{attribute}}="InputValue" />
@code
{
private string? InputValue { get; set; }
private void BindAfter()
{
}
}
""";
TestCode surveyPrompt = """
@namespace SomeProject
<div></div>
@code
{
[Parameter]
public string [|Title|] { get; set; }
}
""";
#region surveyPromptGeneratedCode
TestCode surveyPromptGeneratedCode = """
// <auto-generated/>
#pragma warning disable 1591
namespace SomeProject
{
#line default
using global::System;
using global::System.Collections.Generic;
using global::System.Linq;
using global::System.Threading.Tasks;
#nullable restore
#line 1 "c:\users\example\src\SomeProject\_Imports.razor"
using Microsoft.AspNetCore.Components;
#nullable disable
#nullable restore
#line 2 "c:\users\example\src\SomeProject\_Imports.razor"
using Microsoft.AspNetCore.Components.Authorization;
#nullable disable
#nullable restore
#line 3 "c:\users\example\src\SomeProject\_Imports.razor"
using Microsoft.AspNetCore.Components.Forms;
#nullable disable
#nullable restore
#line 4 "c:\users\example\src\SomeProject\_Imports.razor"
using Microsoft.AspNetCore.Components.Routing;
#nullable disable
#nullable restore
#line 5 "c:\users\example\src\SomeProject\_Imports.razor"
using Microsoft.AspNetCore.Components.Web;
#line default
#line hidden
#nullable disable
#nullable restore
public partial class SurveyPrompt : global::Microsoft.AspNetCore.Components.ComponentBase
#nullable disable
{
#pragma warning disable 219
private void __RazorDirectiveTokenHelpers__() {
((global::System.Action)(() => {
#nullable restore
#line 1 "c:\users\example\src\SomeProject\SurveyPrompt.razor"
global::System.Object __typeHelper = nameof(SomeProject);
#line default
#line hidden
#nullable disable
}
))();
}
#pragma warning restore 219
#pragma warning disable 0414
private static object __o = null;
#pragma warning restore 0414
#pragma warning disable 1998
protected override void BuildRenderTree(global::Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder)
{
}
#pragma warning restore 1998
#nullable restore
#line 6 "c:\users\example\src\SomeProject\SurveyPrompt.razor"
[Parameter]
public string Title { get; set; }
#line default
#line hidden
#nullable disable
}
}
#pragma warning restore 1591
""";
#endregion
var result = await GetGoToDefinitionResultAsync(input, FileKinds.Component,
(FileName("SurveyPrompt.razor"), surveyPrompt.Text),
(FileName("SurveyPrompt.razor.g.cs"), surveyPromptGeneratedCode.Text));
Assert.NotNull(result.Value.Second);
var locations = result.Value.Second;
var location = Assert.Single(locations);
var text = SourceText.From(surveyPrompt.Text);
var range = RoslynLspExtensions.GetRange(text, surveyPrompt.Span);
Assert.Equal(range, location.Range);
}
private static string FileName(string projectRelativeFileName)
=> Path.Combine(TestProjectData.SomeProjectPath, projectRelativeFileName);
private async Task VerifyGoToDefinitionAsync(TestCode input, string? fileKind = null, params (string fileName, string contents)[]? additionalFiles)
{
var result = await GetGoToDefinitionResultAsync(input, fileKind, additionalFiles);
Assumes.NotNull(result);
}
private async Task<SumType<RoslynLocation, RoslynLocation[], RoslynDocumentLink[]>?> GetGoToDefinitionResultAsync(
TestCode input, string? fileKind = null, params (string fileName, string contents)[]? additionalFiles)
{
var document = CreateProjectAndRazorDocument(input.Text, fileKind, additionalFiles);
var inputText = await document.GetTextAsync(DisposalToken);
var position = inputText.GetPosition(input.Position);
var requestInvoker = new TestLSPRequestInvoker([(Methods.TextDocumentDefinitionName, null)]);
var endpoint = new CohostGoToDefinitionEndpoint(RemoteServiceInvoker, TestHtmlDocumentSynchronizer.Instance, requestInvoker);
var textDocumentPositionParams = new TextDocumentPositionParams
{
Position = position,
TextDocument = new TextDocumentIdentifier { Uri = document.CreateUri() },
};
return await endpoint.GetTestAccessor().HandleRequestAsync(textDocumentPositionParams, document, DisposalToken);
}
}

Просмотреть файл

@ -112,62 +112,62 @@ public class CohostRenameEndpointTest(ITestOutputHelper testOutputHelper) : Coho
""",
renames: [("Component.razor", "DifferentName.razor")]);
[Theory(Skip = "https://github.com/dotnet/razor/issues/10717")]
[Theory]
[InlineData("$$Component")]
[InlineData("Com$$ponent")]
[InlineData("Component$$")]
public Task Component_EndTag(string endTag)
=> VerifyRenamesAsync(
input: $"""
This is a Razor document.
=> VerifyRenamesAsync(
input: $"""
This is a Razor document.
<Component />
<div>
<Component />
<Component>
</Component>
<div>
<Component />
<Component>
</{endTag}>
</Component>
<div>
<Component />
<Component>
</{endTag}>
</div>
</div>
</div>
The end.
""",
additionalFiles: [
// The source generator isn't hooked up to our test project, so we have to manually "compile" the razor file
(File("Component.cs"), """
namespace SomeProject;
The end.
""",
additionalFiles: [
// The source generator isn't hooked up to our test project, so we have to manually "compile" the razor file
(File("Component.cs"), """
namespace SomeProject;
public class Component : Microsoft.AspNetCore.Components.ComponentBase
{
}
"""),
// The above will make the component exist, but the .razor file needs to exist too for Uri presentation
(File("Component.razor"), "")
],
newName: "DifferentName",
expected: """
This is a Razor document.
public class Component : Microsoft.AspNetCore.Components.ComponentBase
{
}
"""),
// The above will make the component exist, but the .razor file needs to exist too for Uri presentation
(File("Component.razor"), "")
],
newName: "DifferentName",
expected: """
This is a Razor document.
<DifferentName />
<div>
<DifferentName />
<DifferentName>
</DifferentName>
<div>
<DifferentName />
<DifferentName>
</DifferentName>
<div>
<DifferentName />
<DifferentName>
</DifferentName>
</div>
</div>
</div>
The end.
""",
renames: [("Component.razor", "DifferentName.razor")]);
The end.
""",
renames: [("Component.razor", "DifferentName.razor")]);
[Fact]
public Task Mvc()