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&amp;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;
	}
}