diff --git a/FSharp.Editor.fsproj b/FSharp.Editor.fsproj index 3cb82ad13b9..ca16dd8b9a6 100644 --- a/FSharp.Editor.fsproj +++ b/FSharp.Editor.fsproj @@ -73,6 +73,8 @@ + + diff --git a/Navigation/GoToDefinition.fs b/Navigation/GoToDefinition.fs new file mode 100644 index 00000000000..703d11407cd --- /dev/null +++ b/Navigation/GoToDefinition.fs @@ -0,0 +1,383 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace Microsoft.VisualStudio.FSharp.Editor + +open System +open System.Threading +open System.Collections.Immutable +open System.Diagnostics +open System.IO +open System.Linq +open System.Runtime.InteropServices + +open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.FindSymbols +open Microsoft.CodeAnalysis.Text +open Microsoft.CodeAnalysis.Navigation + +open Microsoft.VisualStudio.Shell.Interop + +open Microsoft.FSharp.Compiler.Range +open Microsoft.FSharp.Compiler.SourceCodeServices + +module private Symbol = + let fullName (root: ISymbol) : string = + let rec inner parts (sym: ISymbol) = + match sym with + | null -> + parts + // TODO: do we have any other terminating cases? + | sym when sym.Kind = SymbolKind.NetModule || sym.Kind = SymbolKind.Assembly -> + parts + | sym when sym.MetadataName <> "" -> + inner (sym.MetadataName :: parts) sym.ContainingSymbol + | sym -> + inner parts sym.ContainingSymbol + + inner [] root |> String.concat "." + +module private ExternalType = + let rec tryOfRoslynType (typesym: ITypeSymbol): ExternalType option = + match typesym with + | :? IPointerTypeSymbol as ptrparam -> + tryOfRoslynType ptrparam.PointedAtType |> Option.map ExternalType.Pointer + | :? IArrayTypeSymbol as arrparam -> + tryOfRoslynType arrparam.ElementType |> Option.map ExternalType.Array + | :? ITypeParameterSymbol as typaram -> + Some (ExternalType.TypeVar typaram.Name) + | :? INamedTypeSymbol as namedTypeSym -> + namedTypeSym.TypeArguments + |> Seq.map tryOfRoslynType + |> List.ofSeq + |> Option.ofOptionList + |> Option.map (fun genericArgs -> + ExternalType.Type (Symbol.fullName typesym, genericArgs)) + | _ -> + Debug.Assert(false, sprintf "GoToDefinitionService: Unexpected Roslyn type symbol subclass: %O" (typesym.GetType())) + None + +module private ParamTypeSymbol = + + let tryOfRoslynParameter (param: IParameterSymbol): ParamTypeSymbol option = + ExternalType.tryOfRoslynType param.Type + |> Option.map ( + if param.RefKind = RefKind.None then ParamTypeSymbol.Param + else ParamTypeSymbol.Byref) + + let tryOfRoslynParameters (paramSyms: ImmutableArray): ParamTypeSymbol list option = + paramSyms + |> Seq.map tryOfRoslynParameter + |> Seq.toList + |> Option.ofOptionList + +module private ExternalSymbol = + let rec ofRoslynSymbol (symbol: ISymbol) : (ISymbol * ExternalSymbol) list = + let container = Symbol.fullName symbol.ContainingSymbol + + match symbol with + | :? INamedTypeSymbol as typesym -> + let fullTypeName = Symbol.fullName typesym + + let constructors = + typesym.InstanceConstructors + |> Seq.choose<_,ISymbol * ExternalSymbol> (fun methsym -> + ParamTypeSymbol.tryOfRoslynParameters methsym.Parameters + |> Option.map (fun args -> upcast methsym, ExternalSymbol.Constructor(fullTypeName, args)) + ) + |> List.ofSeq + + (symbol, ExternalSymbol.Type fullTypeName) :: constructors + + | :? IMethodSymbol as methsym -> + ParamTypeSymbol.tryOfRoslynParameters methsym.Parameters + |> Option.map (fun args -> + symbol, ExternalSymbol.Method(container, methsym.MetadataName, args, methsym.TypeParameters.Length)) + |> Option.toList + + | :? IPropertySymbol as propsym -> + [upcast propsym, ExternalSymbol.Property(container, propsym.MetadataName)] + + | :? IFieldSymbol as fieldsym -> + [upcast fieldsym, ExternalSymbol.Field(container, fieldsym.MetadataName)] + + | :? IEventSymbol as eventsym -> + [upcast eventsym, ExternalSymbol.Event(container, eventsym.MetadataName)] + + | _ -> [] + +type internal FSharpNavigableItem(document: Document, textSpan: TextSpan) = + interface INavigableItem with + member __.Glyph = Glyph.BasicFile + member __.DisplayFileLocation = true + member __.IsImplicitlyDeclared = false + member __.Document = document + member __.SourceSpan = textSpan + member __.DisplayTaggedParts = ImmutableArray.Empty + member __.ChildItems = ImmutableArray.Empty + +type internal StatusBar(statusBar: IVsStatusbar) = + let mutable searchIcon = int16 Microsoft.VisualStudio.Shell.Interop.Constants.SBAI_Find :> obj + + let clear() = + // unfreeze the statusbar + statusBar.FreezeOutput 0 |> ignore + statusBar.Clear() |> ignore + + member __.Message(msg: string) = + let _, frozen = statusBar.IsFrozen() + // unfreeze the status bar + if frozen <> 0 then statusBar.FreezeOutput 0 |> ignore + statusBar.SetText msg |> ignore + // freeze the status bar + statusBar.FreezeOutput 1 |> ignore + + member this.TempMessage(msg: string) = + this.Message msg + async { + do! Async.Sleep 4000 + match statusBar.GetText() with + | 0, currentText when currentText <> msg -> () + | _ -> clear() + }|> Async.Start + + member __.Clear() = clear() + + /// Animated magnifying glass that displays on the status bar while a symbol search is in progress. + member __.Animate() : IDisposable = + statusBar.Animation (1, &searchIcon) |> ignore + { new IDisposable with + member __.Dispose() = statusBar.Animation(0, &searchIcon) |> ignore } + +type internal GoToDefinition(checker: FSharpChecker, projectInfoManager: FSharpProjectOptionsManager) = + let userOpName = "GoToDefinition" + + /// Use an origin document to provide the solution & workspace used to + /// find the corresponding textSpan and INavigableItem for the range + let rangeToNavigableItem (range: range, document: Document) = + async { + let fileName = try System.IO.Path.GetFullPath range.FileName with _ -> range.FileName + let refDocumentIds = document.Project.Solution.GetDocumentIdsWithFilePath fileName + if not refDocumentIds.IsEmpty then + let refDocumentId = refDocumentIds.First() + let refDocument = document.Project.Solution.GetDocument refDocumentId + let! cancellationToken = Async.CancellationToken + let! refSourceText = refDocument.GetTextAsync(cancellationToken) |> Async.AwaitTask + match RoslynHelpers.TryFSharpRangeToTextSpan (refSourceText, range) with + | None -> return None + | Some refTextSpan -> return Some (FSharpNavigableItem (refDocument, refTextSpan)) + else return None + } + + /// Helper function that is used to determine the navigation strategy to apply, can be tuned towards signatures or implementation files. + member private __.FindSymbolHelper (originDocument: Document, originRange: range, sourceText: SourceText, preferSignature: bool) = + asyncMaybe { + let! parsingOptions, projectOptions = projectInfoManager.TryGetOptionsForEditingDocumentOrProject originDocument + let defines = CompilerEnvironment.GetCompilationDefinesForEditing parsingOptions + let! originTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan (sourceText, originRange) + let position = originTextSpan.Start + let! lexerSymbol = Tokenizer.getSymbolAtPosition (originDocument.Id, sourceText, position, originDocument.FilePath, defines, SymbolLookupKind.Greedy, false) + + let textLinePos = sourceText.Lines.GetLinePosition position + let fcsTextLineNumber = Line.fromZ textLinePos.Line + let lineText = (sourceText.Lines.GetLineFromPosition position).ToString() + + let! _, _, checkFileResults = checker.ParseAndCheckDocument (originDocument, projectOptions, allowStaleResults=true,sourceText=sourceText, userOpName=userOpName) + let idRange = lexerSymbol.Ident.idRange + let! fsSymbolUse = checkFileResults.GetSymbolUseAtLocation (fcsTextLineNumber, idRange.EndColumn, lineText, lexerSymbol.FullIsland, userOpName=userOpName) + let symbol = fsSymbolUse.Symbol + // if the tooltip was spawned in an implementation file and we have a range targeting + // a signature file, try to find the corresponding implementation file and target the + // desired symbol + if isSignatureFile fsSymbolUse.FileName && preferSignature = false then + let fsfilePath = Path.ChangeExtension (originRange.FileName,"fs") + if not (File.Exists fsfilePath) then return! None else + let! implDoc = originDocument.Project.Solution.TryGetDocumentFromPath fsfilePath + let! implSourceText = implDoc.GetTextAsync () + let! _parsingOptions, projectOptions = projectInfoManager.TryGetOptionsForEditingDocumentOrProject implDoc + let! _, _, checkFileResults = checker.ParseAndCheckDocument (implDoc, projectOptions, allowStaleResults=true, sourceText=implSourceText, userOpName=userOpName) + let! symbolUses = checkFileResults.GetUsesOfSymbolInFile symbol |> liftAsync + let! implSymbol = symbolUses |> Array.tryHead + let! implTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan (implSourceText, implSymbol.RangeAlternate) + return FSharpNavigableItem (implDoc, implTextSpan) + else + let! targetDocument = originDocument.Project.Solution.TryGetDocumentFromFSharpRange fsSymbolUse.RangeAlternate + return! rangeToNavigableItem (fsSymbolUse.RangeAlternate, targetDocument) + } + + /// Use the targetSymbol to find the first instance of its presence in the provided source file. + member private __.FindSymbolDeclarationInFile(targetSymbolUse: FSharpSymbolUse, filePath: string, source: string, options: FSharpProjectOptions, fileVersion:int) = + asyncMaybe { + let! _, checkFileAnswer = checker.ParseAndCheckFileInProject (filePath, fileVersion, source, options, userOpName=userOpName) |> liftAsync + match checkFileAnswer with + | FSharpCheckFileAnswer.Aborted -> return! None + | FSharpCheckFileAnswer.Succeeded checkFileResults -> + let! symbolUses = checkFileResults.GetUsesOfSymbolInFile targetSymbolUse.Symbol |> liftAsync + let! implSymbol = symbolUses |> Array.tryHead + return implSymbol.RangeAlternate + } + + member private this.FindDefinitionAtPosition(originDocument: Document, position: int) = + asyncMaybe { + let! parsingOptions, projectOptions = projectInfoManager.TryGetOptionsForEditingDocumentOrProject originDocument + let! sourceText = originDocument.GetTextAsync () |> liftTaskAsync + let defines = CompilerEnvironment.GetCompilationDefinesForEditing parsingOptions + let textLine = sourceText.Lines.GetLineFromPosition position + let textLinePos = sourceText.Lines.GetLinePosition position + let fcsTextLineNumber = Line.fromZ textLinePos.Line + let lineText = (sourceText.Lines.GetLineFromPosition position).ToString() + + let preferSignature = isSignatureFile originDocument.FilePath + + let! _, _, checkFileResults = checker.ParseAndCheckDocument (originDocument, projectOptions, allowStaleResults=true, sourceText=sourceText, userOpName=userOpName) + + let! lexerSymbol = Tokenizer.getSymbolAtPosition (originDocument.Id, sourceText, position,originDocument.FilePath, defines, SymbolLookupKind.Greedy, false) + let idRange = lexerSymbol.Ident.idRange + + let! declarations = checkFileResults.GetDeclarationLocation (fcsTextLineNumber, lexerSymbol.Ident.idRange.EndColumn, textLine.ToString(), lexerSymbol.FullIsland, preferSignature, userOpName=userOpName) |> liftAsync + let! targetSymbolUse = checkFileResults.GetSymbolUseAtLocation (fcsTextLineNumber, idRange.EndColumn, lineText, lexerSymbol.FullIsland, userOpName=userOpName) + + match declarations with + | FSharpFindDeclResult.ExternalDecl (assy, targetExternalSym) -> + let! project = originDocument.Project.Solution.Projects |> Seq.tryFind (fun p -> p.AssemblyName.Equals(assy, StringComparison.OrdinalIgnoreCase)) + let! symbols = SymbolFinder.FindSourceDeclarationsAsync(project, fun _ -> true) + + let roslynSymbols = + symbols + |> Seq.collect ExternalSymbol.ofRoslynSymbol + |> Array.ofSeq + + let! symbol = + roslynSymbols + |> Seq.tryPick (fun (sym, externalSym) -> + if externalSym = targetExternalSym then Some sym + else None + ) + + let! location = symbol.Locations |> Seq.tryHead + return (FSharpNavigableItem(project.GetDocument(location.SourceTree), location.SourceSpan), idRange) + + | FSharpFindDeclResult.DeclFound targetRange -> + // if goto definition is called at we are alread at the declaration location of a symbol in + // either a signature or an implementation file then we jump to it's respective postion in thethe + if lexerSymbol.Range = targetRange then + // jump from signature to the corresponding implementation + if isSignatureFile originDocument.FilePath then + let implFilePath = Path.ChangeExtension (originDocument.FilePath,"fs") + if not (File.Exists implFilePath) then return! None else + let! implDocument = originDocument.Project.Solution.TryGetDocumentFromPath implFilePath + let! implSourceText = implDocument.GetTextAsync () |> liftTaskAsync + let! implVersion = implDocument.GetTextVersionAsync () |> liftTaskAsync + + let! targetRange = this.FindSymbolDeclarationInFile(targetSymbolUse, implFilePath, implSourceText.ToString(), projectOptions, implVersion.GetHashCode()) + + let! implTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan (implSourceText, targetRange) + let navItem = FSharpNavigableItem (implDocument, implTextSpan) + return (navItem, idRange) + else // jump from implementation to the corresponding signature + let! declarations = checkFileResults.GetDeclarationLocation (fcsTextLineNumber, lexerSymbol.Ident.idRange.EndColumn, textLine.ToString(), lexerSymbol.FullIsland, true, userOpName=userOpName) |> liftAsync + match declarations with + | FSharpFindDeclResult.DeclFound targetRange -> + let! sigDocument = originDocument.Project.Solution.TryGetDocumentFromPath targetRange.FileName + let! sigSourceText = sigDocument.GetTextAsync () |> liftTaskAsync + let! sigTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan (sigSourceText, targetRange) + let navItem = FSharpNavigableItem (sigDocument, sigTextSpan) + return (navItem, idRange) + | _ -> + return! None + // when the target range is different follow the navigation convention of + // - gotoDefn origin = signature , gotoDefn destination = signature + // - gotoDefn origin = implementation, gotoDefn destination = implementation + else + let! sigDocument = originDocument.Project.Solution.TryGetDocumentFromPath targetRange.FileName + let! sigSourceText = sigDocument.GetTextAsync () |> liftTaskAsync + let! sigTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan (sigSourceText, targetRange) + // if the gotodef call originated from a signature and the returned target is a signature, navigate there + if isSignatureFile targetRange.FileName && preferSignature then + let navItem = FSharpNavigableItem (sigDocument, sigTextSpan) + return (navItem, idRange) + else // we need to get an FSharpSymbol from the targetRange found in the signature + // that symbol will be used to find the destination in the corresponding implementation file + let implFilePath = + // Bugfix: apparently sigDocument not always is a signature file + if isSignatureFile sigDocument.FilePath then Path.ChangeExtension (sigDocument.FilePath, "fs") + else sigDocument.FilePath + + let! implDocument = originDocument.Project.Solution.TryGetDocumentFromPath implFilePath + let! implVersion = implDocument.GetTextVersionAsync () |> liftTaskAsync + let! implSourceText = implDocument.GetTextAsync () |> liftTaskAsync + let! _parsingOptions, projectOptions = projectInfoManager.TryGetOptionsForEditingDocumentOrProject implDocument + + let! targetRange = this.FindSymbolDeclarationInFile(targetSymbolUse, implFilePath, implSourceText.ToString(), projectOptions, implVersion.GetHashCode()) + + let! implTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan (implSourceText, targetRange) + let navItem = FSharpNavigableItem (implDocument, implTextSpan) + return (navItem, idRange) + | _ -> + return! None + } + + /// find the declaration location (signature file/.fsi) of the target symbol if possible, fall back to definition + member this.FindDeclarationOfSymbolAtRange(targetDocument: Document, symbolRange: range, targetSource: SourceText) = + this.FindSymbolHelper(targetDocument, symbolRange, targetSource, preferSignature=true) + + /// find the definition location (implementation file/.fs) of the target symbol + member this.FindDefinitionOfSymbolAtRange(targetDocument: Document, symbolRange: range, targetSourceText: SourceText) = + this.FindSymbolHelper(targetDocument, symbolRange, targetSourceText, preferSignature=false) + + member this.FindDefinitionsForPeekTask(originDocument: Document, position: int, cancellationToken: CancellationToken) = + this.FindDefinitionAtPosition(originDocument, position) + |> Async.map ( + Option.map (fun (navItem, _) -> (navItem :> INavigableItem)) + >> Option.toArray + >> Array.toSeq) + |> RoslynHelpers.StartAsyncAsTask cancellationToken + + /// Construct a task that will return a navigation target for the implementation definition of the symbol + /// at the provided position in the document. + member this.FindDefinitionTask(originDocument: Document, position: int, cancellationToken: CancellationToken) = + this.FindDefinitionAtPosition(originDocument, position) + |> Async.map (Option.map (fun (navItem, range) -> (navItem :> INavigableItem, range))) + |> RoslynHelpers.StartAsyncAsTask cancellationToken + + /// Navigate to the positon of the textSpan in the provided document + /// used by quickinfo link navigation when the tooltip contains the correct destination range. + member __.TryNavigateToTextSpan(document: Document, textSpan: TextSpan, statusBar: StatusBar) = + let navigableItem = FSharpNavigableItem(document, textSpan) :> INavigableItem + let workspace = document.Project.Solution.Workspace + let navigationService = workspace.Services.GetService() + let options = workspace.Options.WithChangedOption(NavigationOptions.PreferProvisionalTab, true) + let navigationSucceeded = navigationService.TryNavigateToSpan(workspace, navigableItem.Document.Id, navigableItem.SourceSpan, options) + + if not navigationSucceeded then + statusBar.TempMessage (SR.CannotNavigateUnknown()) + + member __.NavigateToItem(navigableItem: #INavigableItem, statusBar: StatusBar) = + use __ = statusBar.Animate() + + statusBar.Message (SR.NavigatingTo()) + + let workspace = navigableItem.Document.Project.Solution.Workspace + let navigationService = workspace.Services.GetService() + + // Prefer open documents in the preview tab. + let options = workspace.Options.WithChangedOption(NavigationOptions.PreferProvisionalTab, true) + let result = navigationService.TryNavigateToSpan(workspace, navigableItem.Document.Id, navigableItem.SourceSpan, options) + + if result then + statusBar.Clear() + else + statusBar.TempMessage (SR.CannotNavigateUnknown()) + + /// Find the declaration location (signature file/.fsi) of the target symbol if possible, fall back to definition + member this.NavigateToSymbolDeclarationAsync(targetDocument: Document, targetSourceText: SourceText, symbolRange: range, statusBar: StatusBar) = + asyncMaybe { + let! item = this.FindDeclarationOfSymbolAtRange(targetDocument, symbolRange, targetSourceText) + return this.NavigateToItem(item, statusBar) + } + + /// Find the definition location (implementation file/.fs) of the target symbol + member this.NavigateToSymbolDefinitionAsync(targetDocument: Document, targetSourceText: SourceText, symbolRange: range, statusBar: StatusBar) = + asyncMaybe { + let! item = this.FindDefinitionOfSymbolAtRange(targetDocument, symbolRange, targetSourceText) + return this.NavigateToItem(item, statusBar) + } diff --git a/Navigation/GoToDefinitionService.fs b/Navigation/GoToDefinitionService.fs index 91bc81690d3..dd201ab7750 100644 --- a/Navigation/GoToDefinitionService.fs +++ b/Navigation/GoToDefinitionService.fs @@ -3,235 +3,16 @@ namespace Microsoft.VisualStudio.FSharp.Editor open System.Composition -open System.IO -open System.Collections.Immutable -open System.Linq open System.Threading open System.Threading.Tasks open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.Editor -open Microsoft.CodeAnalysis.Navigation open Microsoft.CodeAnalysis.Host.Mef -open Microsoft.CodeAnalysis.Text -open Microsoft.CodeAnalysis.FindSymbols -open Microsoft.FSharp.Compiler.Range -open Microsoft.FSharp.Compiler.SourceCodeServices open Microsoft.VisualStudio.Shell open Microsoft.VisualStudio.Shell.Interop open System -open System.Diagnostics - -type internal FSharpNavigableItem(document: Document, textSpan: TextSpan) = - interface INavigableItem with - member this.Glyph = Glyph.BasicFile - member this.DisplayFileLocation = true - member this.IsImplicitlyDeclared = false - member this.Document = document - member this.SourceSpan = textSpan - member this.DisplayTaggedParts = ImmutableArray.Empty - member this.ChildItems = ImmutableArray.Empty - -type internal GoToDefinition(checker: FSharpChecker, projectInfoManager: FSharpProjectOptionsManager) = - - static let userOpName = "GoToDefinition" - - /// Use an origin document to provide the solution & workspace used to - /// find the corresponding textSpan and INavigableItem for the range - let rangeToNavigableItem (range: range, document: Document) = - async { - let fileName = try System.IO.Path.GetFullPath range.FileName with _ -> range.FileName - let refDocumentIds = document.Project.Solution.GetDocumentIdsWithFilePath fileName - if not refDocumentIds.IsEmpty then - let refDocumentId = refDocumentIds.First() - let refDocument = document.Project.Solution.GetDocument refDocumentId - let! cancellationToken = Async.CancellationToken - let! refSourceText = refDocument.GetTextAsync(cancellationToken) |> Async.AwaitTask - match RoslynHelpers.TryFSharpRangeToTextSpan (refSourceText, range) with - | None -> return None - | Some refTextSpan -> return Some (FSharpNavigableItem (refDocument, refTextSpan)) - else return None - } - - /// Helper function that is used to determine the navigation strategy to apply, can be tuned towards signatures or implementation files. - let findSymbolHelper (originDocument: Document, originRange: range, sourceText: SourceText, preferSignature: bool) : Async = - asyncMaybe { - let! parsingOptions, projectOptions = projectInfoManager.TryGetOptionsForEditingDocumentOrProject originDocument - let defines = CompilerEnvironment.GetCompilationDefinesForEditing parsingOptions - let! originTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan (sourceText, originRange) - let position = originTextSpan.Start - let! lexerSymbol = Tokenizer.getSymbolAtPosition (originDocument.Id, sourceText, position, originDocument.FilePath, defines, SymbolLookupKind.Greedy, false) - - let textLinePos = sourceText.Lines.GetLinePosition position - let fcsTextLineNumber = Line.fromZ textLinePos.Line - let lineText = (sourceText.Lines.GetLineFromPosition position).ToString() - - let! _, _, checkFileResults = checker.ParseAndCheckDocument (originDocument, projectOptions, allowStaleResults=true,sourceText=sourceText, userOpName = userOpName) - let idRange = lexerSymbol.Ident.idRange - let! fsSymbolUse = checkFileResults.GetSymbolUseAtLocation (fcsTextLineNumber, idRange.EndColumn, lineText, lexerSymbol.FullIsland, userOpName=userOpName) - let symbol = fsSymbolUse.Symbol - // if the tooltip was spawned in an implementation file and we have a range targeting - // a signature file, try to find the corresponding implementation file and target the - // desired symbol - if isSignatureFile fsSymbolUse.FileName && preferSignature = false then - let fsfilePath = Path.ChangeExtension (originRange.FileName,"fs") - if not (File.Exists fsfilePath) then return! None else - let! implDoc = originDocument.Project.Solution.TryGetDocumentFromPath fsfilePath - let! implSourceText = implDoc.GetTextAsync () - let! _parsingOptions, projectOptions = projectInfoManager.TryGetOptionsForEditingDocumentOrProject implDoc - let! _, _, checkFileResults = checker.ParseAndCheckDocument (implDoc, projectOptions, allowStaleResults=true, sourceText=implSourceText, userOpName = userOpName) - let! symbolUses = checkFileResults.GetUsesOfSymbolInFile symbol |> liftAsync - let! implSymbol = symbolUses |> Array.tryHead - let! implTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan (implSourceText, implSymbol.RangeAlternate) - return FSharpNavigableItem (implDoc, implTextSpan) - else - let! targetDocument = originDocument.Project.Solution.TryGetDocumentFromFSharpRange fsSymbolUse.RangeAlternate - return! rangeToNavigableItem (fsSymbolUse.RangeAlternate, targetDocument) - } - - /// find the declaration location (signature file/.fsi) of the target symbol if possible, fall back to definition - member __.FindDeclarationOfSymbolAtRange(targetDocument: Document, symbolRange: range, targetSource: SourceText) = - findSymbolHelper (targetDocument, symbolRange, targetSource, true) - - /// find the definition location (implementation file/.fs) of the target symbol - member __.FindDefinitionOfSymbolAtRange(targetDocument: Document, symbolRange: range, targetSourceText: SourceText) = - findSymbolHelper (targetDocument, symbolRange, targetSourceText, false) - - /// use the targetSymbol to find the first instance of its presence in the provided source file - member __.FindSymbolDeclarationInFile(targetSymbolUse: FSharpSymbolUse, filePath: string, source: string, options: FSharpProjectOptions, fileVersion:int) = - asyncMaybe { - let! _, checkFileAnswer = checker.ParseAndCheckFileInProject (filePath, fileVersion, source, options, userOpName = userOpName) |> liftAsync - match checkFileAnswer with - | FSharpCheckFileAnswer.Aborted -> return! None - | FSharpCheckFileAnswer.Succeeded checkFileResults -> - let! symbolUses = checkFileResults.GetUsesOfSymbolInFile targetSymbolUse.Symbol |> liftAsync - let! implSymbol = symbolUses |> Array.tryHead - return implSymbol.RangeAlternate - } - -type private StatusBar(statusBar: IVsStatusbar) = - - let mutable searchIcon = int16 Microsoft.VisualStudio.Shell.Interop.Constants.SBAI_Find :> obj - - let clear() = - // unfreeze the statusbar - statusBar.FreezeOutput 0 |> ignore - statusBar.Clear() |> ignore - - member __.Message(msg: string) = - let _, frozen = statusBar.IsFrozen() - // unfreeze the status bar - if frozen <> 0 then statusBar.FreezeOutput 0 |> ignore - statusBar.SetText msg |> ignore - // freeze the status bar - statusBar.FreezeOutput 1 |> ignore - - member this.TempMessage(msg: string) = - this.Message msg - async { - do! Async.Sleep 4000 - match statusBar.GetText() with - | 0, currentText when currentText <> msg -> () - | _ -> clear() - }|> Async.Start - - member __.Clear() = clear() - - /// Animated magnifying glass that displays on the status bar while a symbol search is in progress. - member __.Animate() : IDisposable = - statusBar.Animation (1, &searchIcon) |> ignore - { new IDisposable with - member __.Dispose() = statusBar.Animation(0, &searchIcon) |> ignore } - -module internal Symbol = - - let fullName (root: ISymbol) : string = - - let rec inner parts (sym: ISymbol) = - match sym with - | null -> - parts - // TODO: do we have any other terminating cases? - | sym when sym.Kind = SymbolKind.NetModule || sym.Kind = SymbolKind.Assembly -> - parts - | sym when sym.MetadataName <> "" -> - inner (sym.MetadataName :: parts) sym.ContainingSymbol - | sym -> - inner parts sym.ContainingSymbol - - inner [] root |> String.concat "." - -module internal ExternalType = - - let rec tryOfRoslynType (typesym: ITypeSymbol): ExternalType option = - match typesym with - | :? IPointerTypeSymbol as ptrparam -> - tryOfRoslynType ptrparam.PointedAtType |> Option.map ExternalType.Pointer - | :? IArrayTypeSymbol as arrparam -> - tryOfRoslynType arrparam.ElementType |> Option.map ExternalType.Array - | :? ITypeParameterSymbol as typaram -> - Some (ExternalType.TypeVar typaram.Name) - | :? INamedTypeSymbol as namedTypeSym -> - namedTypeSym.TypeArguments - |> Seq.map tryOfRoslynType - |> List.ofSeq - |> Option.ofOptionList - |> Option.map (fun genericArgs -> - ExternalType.Type (Symbol.fullName typesym, genericArgs) - ) - | _ -> - Debug.Assert(false, sprintf "GoToDefinitionService: Unexpected Roslyn type symbol subclass: %O" (typesym.GetType())) - None - -module internal ParamTypeSymbol = - - let tryOfRoslynParameter (param: IParameterSymbol): ParamTypeSymbol option = - ExternalType.tryOfRoslynType param.Type - |> Option.map ( - if param.RefKind = RefKind.None then ParamTypeSymbol.Param - else ParamTypeSymbol.Byref - ) - - let tryOfRoslynParameters (paramSyms: ImmutableArray): ParamTypeSymbol list option = - paramSyms |> Seq.map tryOfRoslynParameter |> Seq.toList |> Option.ofOptionList - -module internal ExternalSymbol = - - let rec ofRoslynSymbol (symbol: ISymbol) : (ISymbol * ExternalSymbol) list = - let container = Symbol.fullName symbol.ContainingSymbol - - match symbol with - | :? INamedTypeSymbol as typesym -> - let fullTypeName = Symbol.fullName typesym - - let constructors = - typesym.InstanceConstructors - |> Seq.choose<_,ISymbol * ExternalSymbol> (fun methsym -> - ParamTypeSymbol.tryOfRoslynParameters methsym.Parameters - |> Option.map (fun args -> upcast methsym, ExternalSymbol.Constructor(fullTypeName, args)) - ) - |> List.ofSeq - - (symbol, ExternalSymbol.Type fullTypeName) :: constructors - - | :? IMethodSymbol as methsym -> - ParamTypeSymbol.tryOfRoslynParameters methsym.Parameters - |> Option.map (fun args -> - symbol, ExternalSymbol.Method(container, methsym.MetadataName, args, methsym.TypeParameters.Length)) - |> Option.toList - - | :? IPropertySymbol as propsym -> - [upcast propsym, ExternalSymbol.Property(container, propsym.MetadataName)] - - | :? IFieldSymbol as fieldsym -> - [upcast fieldsym, ExternalSymbol.Field(container, fieldsym.MetadataName)] - - | :? IEventSymbol as eventsym -> - [upcast eventsym, ExternalSymbol.Event(container, eventsym.MetadataName)] - - | _ -> [] - [, FSharpConstants.FSharpLanguageName)>] [)>] @@ -242,202 +23,46 @@ type internal FSharpGoToDefinitionService projectInfoManager: FSharpProjectOptionsManager ) = - static let userOpName = "GoToDefinition" - let gotoDefinition = GoToDefinition(checkerProvider.Checker, projectInfoManager) - let serviceProvider = ServiceProvider.GlobalProvider - let statusBar = StatusBar(serviceProvider.GetService()) - - let tryNavigateToItem (navigableItem: #INavigableItem option) = - use __ = statusBar.Animate() - - match navigableItem with - | Some navigableItem -> - statusBar.Message (SR.NavigatingTo()) - - let workspace = navigableItem.Document.Project.Solution.Workspace - let navigationService = workspace.Services.GetService() - // prefer open documents in the preview tab - let options = workspace.Options.WithChangedOption (NavigationOptions.PreferProvisionalTab, true) - let result = navigationService.TryNavigateToSpan (workspace, navigableItem.Document.Id, navigableItem.SourceSpan, options) - - if result then - statusBar.Clear() - else - statusBar.TempMessage (SR.CannotNavigateUnknown()) - - result - | None -> - statusBar.TempMessage (SR.CannotDetermineSymbol()) - true - - /// Navigate to the positon of the textSpan in the provided document - /// used by quickinfo link navigation when the tooltip contains the correct destination range. - member this.TryNavigateToTextSpan (document: Document, textSpan: TextSpan) = - let navigableItem = FSharpNavigableItem (document, textSpan) :> INavigableItem - let workspace = document.Project.Solution.Workspace - let navigationService = workspace.Services.GetService() - let options = workspace.Options.WithChangedOption (NavigationOptions.PreferProvisionalTab, true) - if navigationService.TryNavigateToSpan (workspace, navigableItem.Document.Id, navigableItem.SourceSpan, options) then - true - else - statusBar.TempMessage (SR.CannotNavigateUnknown()) - false - - /// find the declaration location (signature file/.fsi) of the target symbol if possible, fall back to definition - member __.NavigateToSymbolDeclarationAsync (targetDocument: Document, targetSourceText: SourceText, symbolRange: range) = - gotoDefinition.FindDeclarationOfSymbolAtRange(targetDocument, symbolRange, targetSourceText) |> Async.map tryNavigateToItem - - /// find the definition location (implementation file/.fs) of the target symbol - member this.NavigateToSymbolDefinitionAsync (targetDocument: Document, targetSourceText: SourceText, symbolRange: range) = - gotoDefinition.FindDefinitionOfSymbolAtRange(targetDocument, symbolRange, targetSourceText) |> Async.map tryNavigateToItem - - /// Construct a task that will return a navigation target for the implementation definition of the symbol - /// at the provided position in the document. - member __.FindDefinitionsTask(originDocument: Document, position: int, cancellationToken: CancellationToken) = - asyncMaybe { - let! parsingOptions, projectOptions = projectInfoManager.TryGetOptionsForEditingDocumentOrProject originDocument - let! sourceText = originDocument.GetTextAsync () |> liftTaskAsync - let defines = CompilerEnvironment.GetCompilationDefinesForEditing parsingOptions - let textLine = sourceText.Lines.GetLineFromPosition position - let textLinePos = sourceText.Lines.GetLinePosition position - let fcsTextLineNumber = Line.fromZ textLinePos.Line - let lineText = (sourceText.Lines.GetLineFromPosition position).ToString() - - let preferSignature = isSignatureFile originDocument.FilePath - - let! _, _, checkFileResults = checkerProvider.Checker.ParseAndCheckDocument (originDocument, projectOptions, allowStaleResults=true, sourceText=sourceText, userOpName=userOpName) - - let! lexerSymbol = Tokenizer.getSymbolAtPosition (originDocument.Id, sourceText, position,originDocument.FilePath, defines, SymbolLookupKind.Greedy, false) - let idRange = lexerSymbol.Ident.idRange - - let! declarations = checkFileResults.GetDeclarationLocation (fcsTextLineNumber, lexerSymbol.Ident.idRange.EndColumn, textLine.ToString(), lexerSymbol.FullIsland, preferSignature, userOpName=userOpName) |> liftAsync - let! targetSymbolUse = checkFileResults.GetSymbolUseAtLocation (fcsTextLineNumber, idRange.EndColumn, lineText, lexerSymbol.FullIsland, userOpName=userOpName) - - match declarations with - | FSharpFindDeclResult.ExternalDecl (assy, targetExternalSym) -> - let! project = originDocument.Project.Solution.Projects |> Seq.tryFind (fun p -> p.AssemblyName.Equals(assy, StringComparison.OrdinalIgnoreCase)) - let! symbols = SymbolFinder.FindSourceDeclarationsAsync(project, fun _ -> true) - - let roslynSymbols = - symbols - |> Seq.collect ExternalSymbol.ofRoslynSymbol - |> Array.ofSeq - - let! symbol = - roslynSymbols - |> Seq.tryPick (fun (sym, externalSym) -> - if externalSym = targetExternalSym then Some sym - else None - ) - - let! location = symbol.Locations |> Seq.tryHead - return FSharpNavigableItem(project.GetDocument(location.SourceTree), location.SourceSpan) - - | FSharpFindDeclResult.DeclFound targetRange -> - // if goto definition is called at we are alread at the declaration location of a symbol in - // either a signature or an implementation file then we jump to it's respective postion in thethe - if lexerSymbol.Range = targetRange then - // jump from signature to the corresponding implementation - if isSignatureFile originDocument.FilePath then - let implFilePath = Path.ChangeExtension (originDocument.FilePath,"fs") - if not (File.Exists implFilePath) then return! None else - let! implDocument = originDocument.Project.Solution.TryGetDocumentFromPath implFilePath - let! implSourceText = implDocument.GetTextAsync () |> liftTaskAsync - let! implVersion = implDocument.GetTextVersionAsync () |> liftTaskAsync - - let! targetRange = - gotoDefinition.FindSymbolDeclarationInFile(targetSymbolUse, implFilePath, implSourceText.ToString(), projectOptions, implVersion.GetHashCode()) - - let! implTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan (implSourceText, targetRange) - let navItem = FSharpNavigableItem (implDocument, implTextSpan) - return navItem - else // jump from implementation to the corresponding signature - let! declarations = checkFileResults.GetDeclarationLocation (fcsTextLineNumber, lexerSymbol.Ident.idRange.EndColumn, textLine.ToString(), lexerSymbol.FullIsland, true, userOpName=userOpName) |> liftAsync - match declarations with - | FSharpFindDeclResult.DeclFound targetRange -> - let! sigDocument = originDocument.Project.Solution.TryGetDocumentFromPath targetRange.FileName - let! sigSourceText = sigDocument.GetTextAsync () |> liftTaskAsync - let! sigTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan (sigSourceText, targetRange) - let navItem = FSharpNavigableItem (sigDocument, sigTextSpan) - return navItem - | _ -> return! None - // when the target range is different follow the navigation convention of - // - gotoDefn origin = signature , gotoDefn destination = signature - // - gotoDefn origin = implementation, gotoDefn destination = implementation - else - let! sigDocument = originDocument.Project.Solution.TryGetDocumentFromPath targetRange.FileName - let! sigSourceText = sigDocument.GetTextAsync () |> liftTaskAsync - let! sigTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan (sigSourceText, targetRange) - // if the gotodef call originated from a signature and the returned target is a signature, navigate there - if isSignatureFile targetRange.FileName && preferSignature then - let navItem = FSharpNavigableItem (sigDocument, sigTextSpan) - return navItem - else // we need to get an FSharpSymbol from the targetRange found in the signature - // that symbol will be used to find the destination in the corresponding implementation file - let implFilePath = - // Bugfix: apparently sigDocument not always is a signature file - if isSignatureFile sigDocument.FilePath then Path.ChangeExtension (sigDocument.FilePath, "fs") - else sigDocument.FilePath - - let! implDocument = originDocument.Project.Solution.TryGetDocumentFromPath implFilePath - let! implVersion = implDocument.GetTextVersionAsync () |> liftTaskAsync - let! implSourceText = implDocument.GetTextAsync () |> liftTaskAsync - let! _parsingOptions, projectOptions = projectInfoManager.TryGetOptionsForEditingDocumentOrProject implDocument - - let! targetRange = - gotoDefinition.FindSymbolDeclarationInFile(targetSymbolUse, implFilePath, implSourceText.ToString(), projectOptions, implVersion.GetHashCode()) - - let! implTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan (implSourceText, targetRange) - let navItem = FSharpNavigableItem (implDocument, implTextSpan) - return navItem - | _ -> return! None - } - |> Async.map (Option.map (fun x -> x :> INavigableItem) >> Option.toArray >> Array.toSeq) - |> RoslynHelpers.StartAsyncAsTask cancellationToken + let gtd = GoToDefinition(checkerProvider.Checker, projectInfoManager) + let statusBar = StatusBar(ServiceProvider.GlobalProvider.GetService()) interface IGoToDefinitionService with - // used for 'definition peek' - member this.FindDefinitionsAsync (document: Document, position: int, cancellationToken: CancellationToken) = - this.FindDefinitionsTask (document, position, cancellationToken) + /// Invoked with Peek Definition. + member __.FindDefinitionsAsync (document: Document, position: int, cancellationToken: CancellationToken) = + gtd.FindDefinitionsForPeekTask(document, position, cancellationToken) - // used for 'goto definition' proper + /// Invoked with Go to Definition. /// Try to navigate to the definiton of the symbol at the symbolRange in the originDocument - member this.TryGoToDefinition(document: Document, position: int, cancellationToken: CancellationToken) = - let definitionTask = this.FindDefinitionsTask (document, position, cancellationToken) - - statusBar.Message (SR.LocatingSymbol()) + member __.TryGoToDefinition(document: Document, position: int, cancellationToken: CancellationToken) = + statusBar.Message(SR.LocatingSymbol()) use __ = statusBar.Animate() + let gtdTask = gtd.FindDefinitionTask(document, position, cancellationToken) + // Wrap this in a try/with as if the user clicks "Cancel" on the thread dialog, we'll be cancelled // Task.Wait throws an exception if the task is cancelled, so be sure to catch it. - let completionError = + let gtdCompletionOrError = try - // REVIEW: document this use of a blocking wait on the cancellation token, explaining why it is ok - definitionTask.Wait() - None - with exc -> Some <| Exception.flattenMessage exc + // This call to Wait() is fine because we want to be able to provide the error message in the status bar. + gtdTask.Wait() + Ok gtdTask + with exc -> + Error(Exception.flattenMessage exc) - match completionError with - | Some message -> - statusBar.TempMessage <| String.Format(SR.NavigateToFailed(), message) + match gtdCompletionOrError with + | Ok task -> + if task.Status = TaskStatus.RanToCompletion && task.Result.IsSome then + let item, _ = task.Result.Value + gtd.NavigateToItem(item, statusBar) + + // 'true' means do it, like Sheev Palpatine would want us to. + true + else + statusBar.TempMessage (SR.CannotDetermineSymbol()) + false + | Error message -> + statusBar.TempMessage(String.Format(SR.NavigateToFailed(), message)) // Don't show the dialog box as it's most likely that the user cancelled. // Don't make them click twice. - true - | None -> - if definitionTask.Status = TaskStatus.RanToCompletion && definitionTask.Result <> null && definitionTask.Result.Any() then - let navigableItem = definitionTask.Result.First() // F# API provides only one INavigableItem - tryNavigateToItem (Some navigableItem) - - // FSROSLYNTODO: potentially display multiple results here - // If GotoDef returns one result then it should try to jump to a discovered location. If it returns multiple results then it should use - // presenters to render items so user can choose whatever he needs. Given that per comment F# API always returns only one item then we - // should always navigate to definition and get rid of presenters. - // - //let refDisplayString = refSourceText.GetSubText(refTextSpan).ToString() - //for presenter in presenters do - // presenter.DisplayResult(navigableItem.DisplayString, definitionTask.Result) - //true - else - statusBar.TempMessage (SR.CannotDetermineSymbol()) - false \ No newline at end of file + true \ No newline at end of file diff --git a/Navigation/NavigableSymbolsService.fs b/Navigation/NavigableSymbolsService.fs new file mode 100644 index 00000000000..4ec1ef238fb --- /dev/null +++ b/Navigation/NavigableSymbolsService.fs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace Microsoft.VisualStudio.FSharp.Editor + +open System +open System.Threading +open System.Threading.Tasks +open System.ComponentModel.Composition + +open Microsoft.CodeAnalysis.Text +open Microsoft.CodeAnalysis.Navigation + +open Microsoft.VisualStudio.Language.Intellisense +open Microsoft.VisualStudio.Text +open Microsoft.VisualStudio.Text.Editor +open Microsoft.VisualStudio.Shell.Interop +open Microsoft.VisualStudio.Utilities +open Microsoft.VisualStudio.Shell + +[] +type internal FSharpNavigableSymbol(item: INavigableItem, span: SnapshotSpan, gtd: GoToDefinition, statusBar: StatusBar) = + interface INavigableSymbol with + member __.Navigate(_: INavigableRelationship) = + gtd.NavigateToItem(item, statusBar) + + member __.Relationships = seq { yield PredefinedNavigableRelationships.Definition } + + member __.SymbolSpan = span + +type internal FSharpNavigableSymbolSource(checkerProvider: FSharpCheckerProvider, projectInfoManager: FSharpProjectOptionsManager, serviceProvider: IServiceProvider) = + + let mutable disposed = false + let gtd = GoToDefinition(checkerProvider.Checker, projectInfoManager) + let statusBar = StatusBar(serviceProvider.GetService()) + + interface INavigableSymbolSource with + member __.GetNavigableSymbolAsync(triggerSpan: SnapshotSpan, cancellationToken: CancellationToken) = + // Yes, this is a code smell. But this is how the editor API accepts what we would treat as None. + if disposed then null + else + asyncMaybe { + let snapshot = triggerSpan.Snapshot + let position = triggerSpan.Start.Position + let document = snapshot.GetOpenDocumentInCurrentContextWithChanges() + let! sourceText = document.GetTextAsync () |> liftTaskAsync + + statusBar.Message(SR.LocatingSymbol()) + use _ = statusBar.Animate() + + let gtdTask = gtd.FindDefinitionTask(document, position, cancellationToken) + + // Wrap this in a try/with as if the user clicks "Cancel" on the thread dialog, we'll be cancelled + // Task.Wait throws an exception if the task is cancelled, so be sure to catch it. + let gtdCompletedOrError = + try + // This call to Wait() is fine because we want to be able to provide the error message in the status bar. + gtdTask.Wait() + Ok gtdTask + with exc -> + Error(Exception.flattenMessage exc) + + match gtdCompletedOrError with + | Ok task -> + if task.Status = TaskStatus.RanToCompletion && task.Result.IsSome then + let (navigableItem, range) = task.Result.Value + + let declarationTextSpan = RoslynHelpers.FSharpRangeToTextSpan(sourceText, range) + let declarationSpan = Span(declarationTextSpan.Start, declarationTextSpan.Length) + let symbolSpan = SnapshotSpan(snapshot, declarationSpan) + + return FSharpNavigableSymbol(navigableItem, symbolSpan, gtd, statusBar) :> INavigableSymbol + else + statusBar.TempMessage (SR.CannotDetermineSymbol()) + + // The NavigableSymbols API accepts 'null' when there's nothing to navigate to. + return null + | Error message -> + statusBar.TempMessage (String.Format(SR.NavigateToFailed(), message)) + + // The NavigableSymbols API accepts 'null' when there's nothing to navigate to. + return null + } + |> Async.map Option.toObj + |> RoslynHelpers.StartAsyncAsTask cancellationToken + + member __.Dispose() = + disposed <- true + +[)>] +[] +[] +[] +type internal FSharpNavigableSymbolService + [] + ( + [)>] serviceProvider: IServiceProvider, + checkerProvider: FSharpCheckerProvider, + projectInfoManager: FSharpProjectOptionsManager + ) = + + interface INavigableSymbolSourceProvider with + member __.TryCreateNavigableSymbolSource(_: ITextView, _: ITextBuffer) = + new FSharpNavigableSymbolSource(checkerProvider, projectInfoManager, serviceProvider) :> INavigableSymbolSource \ No newline at end of file diff --git a/QuickInfo/Navigation.fs b/QuickInfo/Navigation.fs index 6b8773e1542..d1904b96cd6 100644 --- a/QuickInfo/Navigation.fs +++ b/QuickInfo/Navigation.fs @@ -3,18 +3,26 @@ namespace Microsoft.VisualStudio.FSharp.Editor open System + open Microsoft.CodeAnalysis + +open Microsoft.FSharp.Compiler.SourceCodeServices + open Microsoft.FSharp.Compiler.Range +open Microsoft.VisualStudio.Shell.Interop type internal QuickInfoNavigation ( - gotoDefinitionService: FSharpGoToDefinitionService, + serviceProvider: IServiceProvider, + checker: FSharpChecker, + projectInfoManager: FSharpProjectOptionsManager, initialDoc: Document, thisSymbolUseRange: range ) = let workspace = initialDoc.Project.Solution.Workspace let solution = workspace.CurrentSolution + let statusBar = StatusBar(serviceProvider.GetService()) member __.IsTargetValid (range: range) = range <> rangeStartup && @@ -36,8 +44,10 @@ type internal QuickInfoNavigation let! targetDoc = solution.TryGetDocumentFromFSharpRange (range, initialDoc.Project.Id) let! targetSource = targetDoc.GetTextAsync() let! targetTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan (targetSource, range) - // to ensure proper navigation decsions we need to check the type of document the navigation call - // is originating from and the target we're provided by default + let gtd = GoToDefinition(checker, projectInfoManager) + + // To ensure proper navigation decsions, we need to check the type of document the navigation call + // is originating from and the target we're provided by default: // - signature files (.fsi) should navigate to other signature files // - implementation files (.fs) should navigate to other implementation files let (|Signature|Implementation|) filepath = @@ -46,11 +56,13 @@ type internal QuickInfoNavigation match initialDoc.FilePath, targetPath with | Signature, Signature | Implementation, Implementation -> - return gotoDefinitionService.TryNavigateToTextSpan (targetDoc, targetTextSpan) - // adjust the target from signature to implementation + return gtd.TryNavigateToTextSpan(targetDoc, targetTextSpan, statusBar) + + // Adjust the target from signature to implementation. | Implementation, Signature -> - return! gotoDefinitionService.NavigateToSymbolDefinitionAsync (targetDoc, targetSource, range) |> liftAsync - // adjust the target from implmentation to signature + return! gtd.NavigateToSymbolDefinitionAsync(targetDoc, targetSource, range, statusBar) + + // Adjust the target from implmentation to signature. | Signature, Implementation -> - return! gotoDefinitionService.NavigateToSymbolDeclarationAsync (targetDoc, targetSource, range) |> liftAsync + return! gtd.NavigateToSymbolDeclarationAsync(targetDoc, targetSource, range, statusBar) } |> Async.Ignore |> Async.StartImmediate diff --git a/QuickInfo/QuickInfoProvider.fs b/QuickInfo/QuickInfoProvider.fs index 6deb00f4d1c..a41fa05a5d3 100644 --- a/QuickInfo/QuickInfoProvider.fs +++ b/QuickInfo/QuickInfoProvider.fs @@ -159,10 +159,10 @@ module private FSharpQuickInfo = type internal FSharpAsyncQuickInfoSource ( + serviceProvider: IServiceProvider, xmlMemberIndexService: IVsXMLMemberIndexService, checkerProvider:FSharpCheckerProvider, projectInfoManager:FSharpProjectOptionsManager, - gotoDefinitionService:FSharpGoToDefinitionService, textBuffer:ITextBuffer ) = @@ -211,7 +211,7 @@ type internal FSharpAsyncQuickInfoSource let mainDescription, documentation, typeParameterMap, usage, exceptions = ResizeArray(), ResizeArray(), ResizeArray(), ResizeArray(), ResizeArray() XmlDocumentation.BuildDataTipText(documentationBuilder, mainDescription.Add, documentation.Add, typeParameterMap.Add, usage.Add, exceptions.Add, quickInfo.StructuredText) let imageId = Tokenizer.GetImageIdForSymbol(quickInfo.Symbol, quickInfo.SymbolKind) - let navigation = QuickInfoNavigation(gotoDefinitionService, document, symbolUse.RangeAlternate) + let navigation = QuickInfoNavigation(serviceProvider, checkerProvider.Checker, projectInfoManager, document, symbolUse.RangeAlternate) let docs = joinWithLineBreaks [documentation; typeParameterMap; usage; exceptions] let content = QuickInfoViewProvider.provideContent(imageId, mainDescription, docs, navigation) let span = getTrackingSpan quickInfo.Span @@ -242,7 +242,7 @@ type internal FSharpAsyncQuickInfoSource ] |> ResizeArray let docs = joinWithLineBreaks [documentation; typeParameterMap; usage; exceptions] let imageId = Tokenizer.GetImageIdForSymbol(targetQuickInfo.Symbol, targetQuickInfo.SymbolKind) - let navigation = QuickInfoNavigation(gotoDefinitionService, document, symbolUse.RangeAlternate) + let navigation = QuickInfoNavigation(serviceProvider, checkerProvider.Checker, projectInfoManager, document, symbolUse.RangeAlternate) let content = QuickInfoViewProvider.provideContent(imageId, mainDescription, docs, navigation) let span = getTrackingSpan targetQuickInfo.Span return QuickInfoItem(span, content) @@ -256,11 +256,10 @@ type internal FSharpAsyncQuickInfoSource type internal FSharpAsyncQuickInfoSourceProvider [] ( - [)>] serviceProvider:IServiceProvider, + [)>] serviceProvider: IServiceProvider, checkerProvider:FSharpCheckerProvider, - projectInfoManager:FSharpProjectOptionsManager, - gotoDefinitionService:FSharpGoToDefinitionService + projectInfoManager:FSharpProjectOptionsManager ) = interface IAsyncQuickInfoSourceProvider with override __.TryCreateQuickInfoSource(textBuffer:ITextBuffer) : IAsyncQuickInfoSource = - new FSharpAsyncQuickInfoSource(serviceProvider.XMLMemberIndexService, checkerProvider, projectInfoManager, gotoDefinitionService, textBuffer) :> IAsyncQuickInfoSource + new FSharpAsyncQuickInfoSource(serviceProvider, serviceProvider.XMLMemberIndexService, checkerProvider, projectInfoManager, textBuffer) :> IAsyncQuickInfoSource