Editor/HotCodeEditor/Navigation/DefinitionFinder.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
/// <summary>
/// Where a symbol is declared. Line and Column are zero-based.
/// </summary>
public record DefinitionLocation( string Path, int Line, int Column, string Preview );
/// <summary>
/// The answer to "go to definition": source locations, or the assembly a symbol comes from
/// when there's no source for it (like s&box's own types).
/// </summary>
public record DefinitionResult( string Symbol, IReadOnlyList<DefinitionLocation> Locations, string Assembly, string Error )
{
public static DefinitionResult Fail( string error ) => new( null, Array.Empty<DefinitionLocation>(), null, error );
}
/// <summary>
/// Go to definition, using the compilation from <see cref="RoslynProject"/>.
/// </summary>
public static class DefinitionFinder
{
/// <summary>
/// Finds the definition of whatever is at (line, column) in the snapshot's file. Safe to call off the main thread.
/// </summary>
public static DefinitionResult Find( RoslynProject.Snapshot snapshot, int line, int column )
{
try
{
return FindInternal( snapshot, line, column );
}
catch ( Exception e )
{
return DefinitionResult.Fail( $"Couldn't look that up: {e.Message}" );
}
}
private static DefinitionResult FindInternal( RoslynProject.Snapshot snapshot, int line, int column )
{
var (compilation, target) = RoslynProject.GetCompilation( snapshot );
if ( target is null )
return DefinitionResult.Fail( "Go to Definition works in C# files inside the project's code folders." );
var model = compilation.GetSemanticModel( target );
var symbol = SymbolAt( model, target, line, column );
if ( symbol is null )
return DefinitionResult.Fail( "Nothing to go to here." );
symbol = Unwrap( symbol );
var display = symbol.ToDisplayString( SymbolDisplayFormat.CSharpShortErrorMessageFormat );
var locations = symbol.Locations
.Where( l => l.IsInSource && System.IO.File.Exists( l.SourceTree?.FilePath ) )
.Select( l =>
{
var span = l.GetLineSpan();
var source = l.SourceTree.GetText().Lines[span.StartLinePosition.Line].ToString().Trim();
return new DefinitionLocation( l.SourceTree.FilePath, span.StartLinePosition.Line, span.StartLinePosition.Character, source );
} )
.ToList();
return new DefinitionResult( display, locations, symbol.ContainingAssembly?.Name, null );
}
/// <summary>
/// The symbol for the identifier at a position. Also works with the caret just after a word.
/// </summary>
private static ISymbol SymbolAt( SemanticModel model, SyntaxTree tree, int line, int column )
{
var text = tree.GetText();
if ( line < 0 || line >= text.Lines.Count ) return null;
var lineInfo = text.Lines[line];
var position = Math.Min( lineInfo.Start + column, lineInfo.End );
var root = tree.GetRoot();
foreach ( var candidate in new[] { position, position - 1 } )
{
if ( candidate < lineInfo.Start ) continue;
var token = root.FindToken( candidate );
if ( !token.Span.Contains( candidate ) && token.Span.End != candidate ) continue;
if ( !token.IsKind( SyntaxKind.IdentifierToken ) && !SyntaxFacts.IsPredefinedType( token.Kind() ) && !token.IsKind( SyntaxKind.BaseKeyword ) && !token.IsKind( SyntaxKind.ThisKeyword ) )
continue;
var symbol = SymbolFor( model, token );
if ( symbol is not null ) return symbol;
}
return null;
}
private static ISymbol SymbolFor( SemanticModel model, SyntaxToken token )
{
var node = token.Parent;
if ( node is null ) return null;
// On a declaration's own name: go to that declaration (useful for partial classes and overrides)
var declared = model.GetDeclaredSymbol( node );
if ( declared is not null && token == DeclarationIdentifier( node ) )
{
// An override's name jumps to what it overrides, since you're already at its definition
return declared switch
{
IMethodSymbol { OverriddenMethod: { } m } => m,
IPropertySymbol { OverriddenProperty: { } p } => p,
IEventSymbol { OverriddenEvent: { } ev } => ev,
_ => declared
};
}
var info = model.GetSymbolInfo( node );
var symbol = info.Symbol ?? info.CandidateSymbols.FirstOrDefault();
if ( symbol is null && node.Parent is not null )
{
info = model.GetSymbolInfo( node.Parent );
symbol = info.Symbol ?? info.CandidateSymbols.FirstOrDefault();
}
// `new Foo()` with no constructor written lands on Foo itself
if ( symbol is IMethodSymbol { MethodKind: MethodKind.Constructor, IsImplicitlyDeclared: true } ctor )
symbol = ctor.ContainingType;
// `var` resolves to the type it stands for
symbol ??= model.GetTypeInfo( node ).Type;
return symbol;
}
private static SyntaxToken? DeclarationIdentifier( SyntaxNode node ) => node switch
{
BaseTypeDeclarationSyntax t => t.Identifier,
DelegateDeclarationSyntax d => d.Identifier,
MethodDeclarationSyntax m => m.Identifier,
PropertyDeclarationSyntax p => p.Identifier,
EventDeclarationSyntax e => e.Identifier,
VariableDeclaratorSyntax v => v.Identifier,
ParameterSyntax p => p.Identifier,
ConstructorDeclarationSyntax c => c.Identifier,
EnumMemberDeclarationSyntax e => e.Identifier,
_ => null
};
/// <summary>
/// From a use of a generic or extension method back to the thing that was declared.
/// </summary>
private static ISymbol Unwrap( ISymbol symbol )
{
if ( symbol is IMethodSymbol { ReducedFrom: { } reduced } ) symbol = reduced;
if ( symbol is IAliasSymbol alias ) symbol = alias.Target;
return symbol.OriginalDefinition ?? symbol;
}
}