Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ public void Method(string? value)

// Use value here
}

// ✅ Use GetRequiredAbsoluteIndex for converting LinePosition to absolute index
// This correctly handles positions past the end of the file per LSP spec
var absoluteIndex = sourceText.GetRequiredAbsoluteIndex(linePosition);
// ❌ Don't use: sourceText.Lines.GetPosition(linePosition)
```

### Testing Patterns
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.CodeAnalysis.Text;
using CSharpSyntaxKind = Microsoft.CodeAnalysis.CSharp.SyntaxKind;

namespace Microsoft.CodeAnalysis.Razor.GoToDefinition;

Expand Down Expand Up @@ -100,4 +103,93 @@ private async Task<LspRange> GetNavigateRangeAsync(IDocumentSnapshot documentSna
// at least then press F7 to go there.
return LspFactory.DefaultRange;
}

public async Task<LspLocation[]?> TryGetDefinitionFromStringLiteralAsync(
IDocumentSnapshot documentSnapshot,
Position position,
CancellationToken cancellationToken)
{
_logger.LogDebug($"Attempting to get definition from string literal at position {position}.");

// Get the C# syntax tree to analyze the string literal
var syntaxTree = await documentSnapshot.GetCSharpSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
var root = await syntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false);
var sourceText = await syntaxTree.GetTextAsync(cancellationToken).ConfigureAwait(false);

// Convert position to absolute index
var absoluteIndex = sourceText.GetRequiredAbsoluteIndex(position);

// Find the token at the current position
var token = root.FindToken(absoluteIndex);

// Check if we're in a string literal
if (token.IsKind(CSharpSyntaxKind.StringLiteralToken))
{
var literalText = token.ValueText;
_logger.LogDebug($"Found string literal: {literalText}");

// Try to resolve the file path
if (TryResolveFilePath(documentSnapshot, literalText, out var resolvedPath))
{
_logger.LogDebug($"Resolved file path: {resolvedPath}");
return [LspFactory.CreateLocation(resolvedPath, LspFactory.DefaultRange)];
}
}

return null;
}

private bool TryResolveFilePath(IDocumentSnapshot documentSnapshot, string filePath, out string resolvedPath)
{
resolvedPath = string.Empty;

if (string.IsNullOrWhiteSpace(filePath))
{
return false;
}

// Only process if it looks like a Razor file path
if (!filePath.IsRazorFilePath())
{
return false;
}

var project = documentSnapshot.Project;

// Handle tilde paths (~/ or ~\) - these are relative to the project root
if (filePath is ['~', '/' or '\\', ..])
{
var projectDirectory = Path.GetDirectoryName(project.FilePath);
if (projectDirectory is null)
{
return false;
}

// Remove the tilde and normalize path separators
var relativePath = filePath.Substring(2).Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar);
var candidatePath = Path.GetFullPath(Path.Combine(projectDirectory, relativePath));

if (project.ContainsDocument(candidatePath))
{
resolvedPath = candidatePath;
return true;
}
}

// Handle relative paths - relative to the current document
var currentDocumentDirectory = Path.GetDirectoryName(documentSnapshot.FilePath);
if (currentDocumentDirectory is not null)
{
var normalizedPath = filePath.Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar);
var candidatePath = Path.GetFullPath(Path.Combine(currentDocumentDirectory, normalizedPath));

if (project.ContainsDocument(candidatePath))
{
resolvedPath = candidatePath;
return true;
}
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,9 @@ internal interface IDefinitionService
bool ignoreComponentAttributes,
bool includeMvcTagHelpers,
CancellationToken cancellationToken);

Task<LspLocation[]?> TryGetDefinitionFromStringLiteralAsync(
IDocumentSnapshot documentSnapshot,
Position position,
CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,21 @@ protected override IRemoteGoToDefinitionService CreateService(in ServiceArgs arg
return Results(componentLocations);
}

// Check if we're in a string literal with a file path (before calling C# which would navigate to String class)
if (positionInfo.LanguageKind is RazorLanguageKind.CSharp)
{
var stringLiteralLocations = await _definitionService.TryGetDefinitionFromStringLiteralAsync(
context.Snapshot,
positionInfo.Position,
cancellationToken)
.ConfigureAwait(false);

if (stringLiteralLocations is { Length: > 0 })
{
return Results(stringLiteralLocations);
}
}

if (positionInfo.LanguageKind is RazorLanguageKind.Html or RazorLanguageKind.Razor)
{
// If it isn't a Razor construct, and it isn't C#, let the server know to delegate to HTML.
Expand Down
Loading
Loading