зеркало из https://github.com/dotnet/razor.git
Merge remote-tracking branch 'upstream/main' into dev/dawengie/SelfVersionedDocumentSnapshots
This commit is contained in:
Коммит
0c2504b047
|
@ -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"
|
||||
}
|
|
@ -15,5 +15,6 @@
|
|||
},
|
||||
"dotnet.defaultSolution": "Razor.sln",
|
||||
"omnisharp.defaultLaunchSolution": "Razor.sln",
|
||||
"files.encoding": "utf8bom"
|
||||
"files.encoding": "utf8bom",
|
||||
"dotnet.testWindow.disableBuildOnRefresh": true
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
Загрузка…
Ссылка в новой задаче