diff --git a/vsintegration/src/FSharp.Editor/CodeLens/FSharpCodeLensService.fs b/vsintegration/src/FSharp.Editor/CodeLens/FSharpCodeLensService.fs index ac322ac733f..b3ca42c4334 100644 --- a/vsintegration/src/FSharp.Editor/CodeLens/FSharpCodeLensService.fs +++ b/vsintegration/src/FSharp.Editor/CodeLens/FSharpCodeLensService.fs @@ -33,7 +33,7 @@ open Microsoft.VisualStudio.Text.Classification open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Editor.Shared.Utilities type internal CodeLens(taggedText, computed, fullTypeSignature, uiElement) = - member val TaggedText: Async<(ResizeArray * QuickInfoNavigation) option> = taggedText + member val TaggedText: Async<(ResizeArray * FSharpNavigation) option> = taggedText member val Computed: bool = computed with get, set member val FullTypeSignature: string = fullTypeSignature member val UiElement: UIElement = uiElement with get, set @@ -191,7 +191,7 @@ type internal FSharpCodeLensService let taggedText = ResizeArray() typeLayout |> Seq.iter taggedText.Add let statusBar = StatusBar(serviceProvider.GetService()) - let navigation = QuickInfoNavigation(statusBar, metadataAsSource, document, realPosition) + let navigation = FSharpNavigation(statusBar, metadataAsSource, document, realPosition) // Because the data is available notify that this line should be updated, displaying the results return Some (taggedText, navigation) | None -> diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index 72354d15dc5..3617afce3be 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -82,7 +82,6 @@ - diff --git a/vsintegration/src/FSharp.Editor/LanguageService/IFSharpWorkspaceService.fs b/vsintegration/src/FSharp.Editor/LanguageService/IFSharpWorkspaceService.fs index db1794308b6..3faf3df5132 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/IFSharpWorkspaceService.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/IFSharpWorkspaceService.fs @@ -12,4 +12,5 @@ open Microsoft.CodeAnalysis.Host type internal IFSharpWorkspaceService = inherit IWorkspaceService abstract Checker: FSharpChecker - abstract FSharpProjectOptionsManager: FSharpProjectOptionsManager \ No newline at end of file + abstract FSharpProjectOptionsManager: FSharpProjectOptionsManager + abstract MetadataAsSource: FSharpMetadataAsSourceService \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs index 344ed5ef913..6bfec4e0bee 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs @@ -45,6 +45,7 @@ type internal RoamingProfileStorageLocation(keyName: string) = type internal FSharpWorkspaceServiceFactory [] ( + metadataAsSourceService: FSharpMetadataAsSourceService ) = // We have a lock just in case if multi-threads try to create a new IFSharpWorkspaceService - @@ -120,7 +121,8 @@ type internal FSharpWorkspaceServiceFactory match checkerSingleton with | Some checker -> checker.Value | _ -> failwith "Checker not set." - member _.FSharpProjectOptionsManager = optionsManager.Value } :> _ + member _.FSharpProjectOptionsManager = optionsManager.Value + member _.MetadataAsSource = metadataAsSourceService } :> _ [] type private FSharpSolutionEvents(projectManager: FSharpProjectOptionsManager, metadataAsSource: FSharpMetadataAsSourceService) = diff --git a/vsintegration/src/FSharp.Editor/LanguageService/MetadataAsSource.fs b/vsintegration/src/FSharp.Editor/LanguageService/MetadataAsSource.fs index 1121c0c8609..3c205e68c87 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/MetadataAsSource.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/MetadataAsSource.fs @@ -93,12 +93,14 @@ module internal MetadataAsSource = [] [); Composition.Shared>] -type internal FSharpMetadataAsSourceService [] (projectContextFactory: IWorkspaceProjectContextFactory) = +type internal FSharpMetadataAsSourceService() = let serviceProvider = ServiceProvider.GlobalProvider let projs = System.Collections.Concurrent.ConcurrentDictionary() let createMetadataProjectContext (projInfo: ProjectInfo) (docInfo: DocumentInfo) = + let componentModel = Package.GetGlobalService(typeof) :?> ComponentModelHost.IComponentModel + let projectContextFactory = componentModel.GetService() let projectContext = projectContextFactory.CreateProjectContext(LanguageNames.FSharp, projInfo.Id.ToString(), projInfo.FilePath, Guid.NewGuid(), null, null) projectContext.DisplayName <- projInfo.Name projectContext.AddSourceFile(docInfo.FilePath, sourceCodeKind = SourceCodeKind.Regular) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs index f012f65ed44..b64c7ed3e7f 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs @@ -135,6 +135,11 @@ type Document with let workspaceService = this.Project.Solution.GetFSharpWorkspaceService() workspaceService.Checker + /// Get the instance of the FSharpMetadataAsSourceService from the workspace by the given F# document. + member this.GetFSharpMetadataAsSource() = + let workspaceService = this.Project.Solution.GetFSharpWorkspaceService() + workspaceService.MetadataAsSource + /// A non-async call that quickly gets FSharpParsingOptions of the given F# document. /// This tries to get the FSharpParsingOptions by looking at an internal cache; if it doesn't exist in the cache it will create an inaccurate but usable form of the FSharpParsingOptions. member this.GetFSharpQuickParsingOptions() = diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs index 2b4ab7b6af2..3c33e7d4b62 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs @@ -4,6 +4,7 @@ namespace Microsoft.VisualStudio.FSharp.Editor open System open System.Threading +open System.Threading.Tasks open System.Collections.Immutable open System.Diagnostics open System.IO @@ -21,7 +22,9 @@ open FSharp.Compiler open FSharp.Compiler.CodeAnalysis open FSharp.Compiler.EditorServices open FSharp.Compiler.Text +open FSharp.Compiler.Text.Range open FSharp.Compiler.Symbols +open FSharp.Compiler.Tokenization module private Symbol = @@ -488,4 +491,237 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = if result then statusBar.Clear() else - statusBar.TempMessage (SR.CannotNavigateUnknown()) \ No newline at end of file + statusBar.TempMessage (SR.CannotNavigateUnknown()) + +type internal QuickInfo = + { StructuredText: ToolTipText + Span: TextSpan + Symbol: FSharpSymbol option + SymbolKind: LexerSymbolKind } + +module internal FSharpQuickInfo = + + let userOpName = "QuickInfo" + + // when a construct has been declared in a signature file the documentation comments that are + // written in that file are the ones that go into the generated xml when the project is compiled + // therefore we should include these doccoms in our design time quick info + let getQuickInfoFromRange + ( + document: Document, + declRange: range, + cancellationToken: CancellationToken + ) + : Async = + + asyncMaybe { + let userOpName = "getQuickInfoFromRange" + let solution = document.Project.Solution + // ascertain the location of the target declaration in the signature file + let! extDocId = solution.GetDocumentIdsWithFilePath declRange.FileName |> Seq.tryHead + let extDocument = solution.GetProject(extDocId.ProjectId).GetDocument extDocId + let! extSourceText = extDocument.GetTextAsync cancellationToken + let! extSpan = RoslynHelpers.TryFSharpRangeToTextSpan (extSourceText, declRange) + let extLineText = (extSourceText.Lines.GetLineFromPosition extSpan.Start).ToString() + + // project options need to be retrieved because the signature file could be in another project + let! extLexerSymbol = extDocument.TryFindFSharpLexerSymbolAsync(extSpan.Start, SymbolLookupKind.Greedy, true, true, userOpName) + let! _, extCheckFileResults = extDocument.GetFSharpParseAndCheckResultsAsync(userOpName) |> liftAsync + + let extQuickInfoText = + extCheckFileResults.GetToolTip + (declRange.StartLine, extLexerSymbol.Ident.idRange.EndColumn, extLineText, extLexerSymbol.FullIsland, FSharpTokenTag.IDENT) + + match extQuickInfoText with + | ToolTipText [] + | ToolTipText [ToolTipElement.None] -> return! None + | extQuickInfoText -> + let! extSymbolUse = + extCheckFileResults.GetSymbolUseAtLocation(declRange.StartLine, extLexerSymbol.Ident.idRange.EndColumn, extLineText, extLexerSymbol.FullIsland) + let! span = RoslynHelpers.TryFSharpRangeToTextSpan (extSourceText, extLexerSymbol.Range) + + return { StructuredText = extQuickInfoText + Span = span + Symbol = Some extSymbolUse.Symbol + SymbolKind = extLexerSymbol.Kind } + } + + /// Get QuickInfo combined from doccom of Signature and definition + let getQuickInfo + ( + document: Document, + position: int, + cancellationToken: CancellationToken + ) + : Async<(range * QuickInfo option * QuickInfo option) option> = + + asyncMaybe { + let userOpName = "getQuickInfo" + let! lexerSymbol = document.TryFindFSharpLexerSymbolAsync(position, SymbolLookupKind.Greedy, true, true, userOpName) + let! _, checkFileResults = document.GetFSharpParseAndCheckResultsAsync(userOpName) |> liftAsync + let! sourceText = document.GetTextAsync cancellationToken + let idRange = lexerSymbol.Ident.idRange + let textLinePos = sourceText.Lines.GetLinePosition position + let fcsTextLineNumber = Line.fromZ textLinePos.Line + let lineText = (sourceText.Lines.GetLineFromPosition position).ToString() + + /// Gets the QuickInfo information for the orignal target + let getTargetSymbolQuickInfo (symbol, tag) = + asyncMaybe { + let targetQuickInfo = + checkFileResults.GetToolTip + (fcsTextLineNumber, idRange.EndColumn, lineText, lexerSymbol.FullIsland,tag) + + match targetQuickInfo with + | ToolTipText [] + | ToolTipText [ToolTipElement.None] -> return! None + | _ -> + let! targetTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan (sourceText, lexerSymbol.Range) + return { StructuredText = targetQuickInfo + Span = targetTextSpan + Symbol = symbol + SymbolKind = lexerSymbol.Kind } + } + + match lexerSymbol.Kind with + | LexerSymbolKind.String -> + let! targetQuickInfo = getTargetSymbolQuickInfo (None, FSharpTokenTag.STRING) + return lexerSymbol.Range, None, Some targetQuickInfo + + | _ -> + let! symbolUse = checkFileResults.GetSymbolUseAtLocation (fcsTextLineNumber, idRange.EndColumn, lineText, lexerSymbol.FullIsland) + + // if the target is in a signature file, adjusting the quick info is unnecessary + if isSignatureFile document.FilePath then + let! targetQuickInfo = getTargetSymbolQuickInfo (Some symbolUse.Symbol, FSharpTokenTag.IDENT) + return symbolUse.Range, None, Some targetQuickInfo + else + // find the declaration location of the target symbol, with a preference for signature files + let findSigDeclarationResult = checkFileResults.GetDeclarationLocation (idRange.StartLine, idRange.EndColumn, lineText, lexerSymbol.FullIsland, preferFlag=true) + + // it is necessary to retrieve the backup quick info because this acquires + // the textSpan designating where we want the quick info to appear. + let! targetQuickInfo = getTargetSymbolQuickInfo (Some symbolUse.Symbol, FSharpTokenTag.IDENT) + + let! result = + match findSigDeclarationResult with + | FindDeclResult.DeclFound declRange when isSignatureFile declRange.FileName -> + asyncMaybe { + let! sigQuickInfo = getQuickInfoFromRange(document, declRange, cancellationToken) + + // if the target was declared in a signature file, and the current file + // is not the corresponding module implementation file for that signature, + // the doccoms from the signature will overwrite any doccoms that might be + // present on the definition/implementation + let findImplDefinitionResult = checkFileResults.GetDeclarationLocation (idRange.StartLine, idRange.EndColumn, lineText, lexerSymbol.FullIsland, preferFlag=false) + + match findImplDefinitionResult with + | FindDeclResult.DeclNotFound _ + | FindDeclResult.ExternalDecl _ -> + return symbolUse.Range, Some sigQuickInfo, None + | FindDeclResult.DeclFound declRange -> + let! implQuickInfo = getQuickInfoFromRange(document, declRange, cancellationToken) + return symbolUse.Range, Some sigQuickInfo, Some { implQuickInfo with Span = targetQuickInfo.Span } + } + | _ -> async.Return None + |> liftAsync + + return result |> Option.defaultValue (symbolUse.Range, None, Some targetQuickInfo) + } + +type internal FSharpNavigation + ( + statusBar: StatusBar, + metadataAsSource: FSharpMetadataAsSourceService, + initialDoc: Document, + thisSymbolUseRange: range + ) = + + let workspace = initialDoc.Project.Solution.Workspace + let solution = workspace.CurrentSolution + + member _.IsTargetValid (range: range) = + range <> rangeStartup && + range <> thisSymbolUseRange && + solution.TryGetDocumentIdFromFSharpRange (range, initialDoc.Project.Id) |> Option.isSome + + member _.RelativePath (range: range) = + let relativePathEscaped = + match solution.FilePath with + | null -> range.FileName + | sfp -> + let targetUri = Uri(range.FileName) + Uri(sfp).MakeRelativeUri(targetUri).ToString() + relativePathEscaped |> Uri.UnescapeDataString + + member _.NavigateTo (range: range) = + asyncMaybe { + let targetPath = range.FileName + let! targetDoc = solution.TryGetDocumentFromFSharpRange (range, initialDoc.Project.Id) + let! targetSource = targetDoc.GetTextAsync() + let! targetTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan (targetSource, range) + let gtd = GoToDefinition(metadataAsSource) + + // 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 = + if isSignatureFile filepath then Signature else Implementation + + match initialDoc.FilePath, targetPath with + | Signature, Signature + | Implementation, Implementation -> + return gtd.TryNavigateToTextSpan(targetDoc, targetTextSpan, statusBar) + + // Adjust the target from signature to implementation. + | Implementation, Signature -> + return! gtd.NavigateToSymbolDefinitionAsync(targetDoc, targetSource, range, statusBar) + + // Adjust the target from implmentation to signature. + | Signature, Implementation -> + return! gtd.NavigateToSymbolDeclarationAsync(targetDoc, targetSource, range, statusBar) + } + |> Async.Ignore |> Async.StartImmediate + + member _.FindDefinitions(position, cancellationToken) = + let gtd = GoToDefinition(metadataAsSource) + let task = gtd.FindDefinitionsForPeekTask(initialDoc, position, cancellationToken) + task.Wait(cancellationToken) + let results = task.Result + results + |> Seq.choose(fun (result, _) -> + match result with + | FSharpGoToDefinitionResult.NavigableItem(navItem) -> Some navItem + | _ -> None + ) + |> Task.FromResult + + member this.TryGoToDefinition(position, cancellationToken) = + let gtd = GoToDefinition(metadataAsSource) + let gtdTask = gtd.FindDefinitionTask(initialDoc, 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. + try + // This call to Wait() is fine because we want to be able to provide the error message in the status bar. + gtdTask.Wait(cancellationToken) + if gtdTask.Status = TaskStatus.RanToCompletion && gtdTask.Result.IsSome then + match gtdTask.Result.Value with + | FSharpGoToDefinitionResult.NavigableItem(navItem), _ -> + gtd.NavigateToItem(navItem, statusBar) + // 'true' means do it, like Sheev Palpatine would want us to. + true + | FSharpGoToDefinitionResult.ExternalAssembly(targetSymbolUse, metadataReferences), _ -> + gtd.NavigateToExternalDeclaration(targetSymbolUse, metadataReferences, cancellationToken, statusBar) + // 'true' means do it, like Sheev Palpatine would want us to. + true + else + statusBar.TempMessage (SR.CannotDetermineSymbol()) + false + with exc -> + statusBar.TempMessage(String.Format(SR.NavigateToFailed(), Exception.flattenMessage exc)) + + // Don't show the dialog box as it's most likely that the user cancelled. + // Don't make them click twice. + true \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinitionService.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinitionService.fs index c81957c59b2..db2f3c92274 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinitionService.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinitionService.fs @@ -7,6 +7,8 @@ open System.Composition open System.Threading open System.Threading.Tasks +open FSharp.Compiler.Text.Range + open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Editor @@ -21,22 +23,13 @@ type internal FSharpGoToDefinitionService metadataAsSource: FSharpMetadataAsSourceService ) = - let gtd = GoToDefinition(metadataAsSource) let statusBar = StatusBar(ServiceProvider.GlobalProvider.GetService()) interface IFSharpGoToDefinitionService with /// Invoked with Peek Definition. member _.FindDefinitionsAsync (document: Document, position: int, cancellationToken: CancellationToken) = - let task = gtd.FindDefinitionsForPeekTask(document, position, cancellationToken) - task.Wait(cancellationToken) - let results = task.Result - results - |> Seq.choose(fun (result, _) -> - match result with - | FSharpGoToDefinitionResult.NavigableItem(navItem) -> Some navItem - | _ -> None - ) - |> Task.FromResult + let navigation = FSharpNavigation(statusBar, metadataAsSource, document, rangeStartup) + navigation.FindDefinitions(position, cancellationToken) /// Invoked with Go to Definition. /// Try to navigate to the definiton of the symbol at the symbolRange in the originDocument @@ -44,30 +37,5 @@ type internal FSharpGoToDefinitionService 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. - try - // This call to Wait() is fine because we want to be able to provide the error message in the status bar. - gtdTask.Wait(cancellationToken) - if gtdTask.Status = TaskStatus.RanToCompletion && gtdTask.Result.IsSome then - let result, _ = gtdTask.Result.Value - match result with - | FSharpGoToDefinitionResult.NavigableItem(navItem) -> - gtd.NavigateToItem(navItem, statusBar) - // 'true' means do it, like Sheev Palpatine would want us to. - true - | FSharpGoToDefinitionResult.ExternalAssembly(targetSymbolUse, metadataReferences) -> - gtd.NavigateToExternalDeclaration(targetSymbolUse, metadataReferences, cancellationToken, statusBar) - // 'true' means do it, like Sheev Palpatine would want us to. - true - else - statusBar.TempMessage (SR.CannotDetermineSymbol()) - false - with exc -> - statusBar.TempMessage(String.Format(SR.NavigateToFailed(), Exception.flattenMessage exc)) - - // Don't show the dialog box as it's most likely that the user cancelled. - // Don't make them click twice. - true + let navigation = FSharpNavigation(statusBar, metadataAsSource, document, rangeStartup) + navigation.TryGoToDefinition(position, cancellationToken) diff --git a/vsintegration/src/FSharp.Editor/QuickInfo/Navigation.fs b/vsintegration/src/FSharp.Editor/QuickInfo/Navigation.fs deleted file mode 100644 index 0ea6a4a3a06..00000000000 --- a/vsintegration/src/FSharp.Editor/QuickInfo/Navigation.fs +++ /dev/null @@ -1,66 +0,0 @@ -// 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 Microsoft.CodeAnalysis - -open FSharp.Compiler.CodeAnalysis -open FSharp.Compiler.Text.Range -open FSharp.Compiler.Text -open Microsoft.VisualStudio.Shell.Interop - -type internal QuickInfoNavigation - ( - statusBar: StatusBar, - metadataAsSource: FSharpMetadataAsSourceService, - initialDoc: Document, - thisSymbolUseRange: range - ) = - - let workspace = initialDoc.Project.Solution.Workspace - let solution = workspace.CurrentSolution - - member _.IsTargetValid (range: range) = - range <> rangeStartup && - range <> thisSymbolUseRange && - solution.TryGetDocumentIdFromFSharpRange (range, initialDoc.Project.Id) |> Option.isSome - - member _.RelativePath (range: range) = - let relativePathEscaped = - match solution.FilePath with - | null -> range.FileName - | sfp -> - let targetUri = Uri(range.FileName) - Uri(sfp).MakeRelativeUri(targetUri).ToString() - relativePathEscaped |> Uri.UnescapeDataString - - member _.NavigateTo (range: range) = - asyncMaybe { - let targetPath = range.FileName - let! targetDoc = solution.TryGetDocumentFromFSharpRange (range, initialDoc.Project.Id) - let! targetSource = targetDoc.GetTextAsync() - let! targetTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan (targetSource, range) - let gtd = GoToDefinition(metadataAsSource) - - // 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 = - if isSignatureFile filepath then Signature else Implementation - - match initialDoc.FilePath, targetPath with - | Signature, Signature - | Implementation, Implementation -> - return gtd.TryNavigateToTextSpan(targetDoc, targetTextSpan, statusBar) - - // Adjust the target from signature to implementation. - | Implementation, Signature -> - return! gtd.NavigateToSymbolDefinitionAsync(targetDoc, targetSource, range, statusBar) - - // Adjust the target from implmentation to signature. - | Signature, Implementation -> - return! gtd.NavigateToSymbolDeclarationAsync(targetDoc, targetSource, range, statusBar) - } |> Async.Ignore |> Async.StartImmediate diff --git a/vsintegration/src/FSharp.Editor/QuickInfo/QuickInfoProvider.fs b/vsintegration/src/FSharp.Editor/QuickInfo/QuickInfoProvider.fs index a4b37b7c908..cc2997f552e 100644 --- a/vsintegration/src/FSharp.Editor/QuickInfo/QuickInfoProvider.fs +++ b/vsintegration/src/FSharp.Editor/QuickInfo/QuickInfoProvider.fs @@ -24,142 +24,6 @@ open FSharp.Compiler.Symbols open FSharp.Compiler.Text open FSharp.Compiler.Tokenization -type internal QuickInfo = - { StructuredText: ToolTipText - Span: TextSpan - Symbol: FSharpSymbol option - SymbolKind: LexerSymbolKind } - -module internal FSharpQuickInfo = - - let userOpName = "QuickInfo" - - // when a construct has been declared in a signature file the documentation comments that are - // written in that file are the ones that go into the generated xml when the project is compiled - // therefore we should include these doccoms in our design time quick info - let getQuickInfoFromRange - ( - document: Document, - declRange: range, - cancellationToken: CancellationToken - ) - : Async = - - asyncMaybe { - let userOpName = "getQuickInfoFromRange" - let solution = document.Project.Solution - // ascertain the location of the target declaration in the signature file - let! extDocId = solution.GetDocumentIdsWithFilePath declRange.FileName |> Seq.tryHead - let extDocument = solution.GetProject(extDocId.ProjectId).GetDocument extDocId - let! extSourceText = extDocument.GetTextAsync cancellationToken - let! extSpan = RoslynHelpers.TryFSharpRangeToTextSpan (extSourceText, declRange) - let extLineText = (extSourceText.Lines.GetLineFromPosition extSpan.Start).ToString() - - // project options need to be retrieved because the signature file could be in another project - let! extLexerSymbol = extDocument.TryFindFSharpLexerSymbolAsync(extSpan.Start, SymbolLookupKind.Greedy, true, true, userOpName) - let! _, extCheckFileResults = extDocument.GetFSharpParseAndCheckResultsAsync(userOpName) |> liftAsync - - let extQuickInfoText = - extCheckFileResults.GetToolTip - (declRange.StartLine, extLexerSymbol.Ident.idRange.EndColumn, extLineText, extLexerSymbol.FullIsland, FSharpTokenTag.IDENT) - - match extQuickInfoText with - | ToolTipText [] - | ToolTipText [ToolTipElement.None] -> return! None - | extQuickInfoText -> - let! extSymbolUse = - extCheckFileResults.GetSymbolUseAtLocation(declRange.StartLine, extLexerSymbol.Ident.idRange.EndColumn, extLineText, extLexerSymbol.FullIsland) - let! span = RoslynHelpers.TryFSharpRangeToTextSpan (extSourceText, extLexerSymbol.Range) - - return { StructuredText = extQuickInfoText - Span = span - Symbol = Some extSymbolUse.Symbol - SymbolKind = extLexerSymbol.Kind } - } - - /// Get QuickInfo combined from doccom of Signature and definition - let getQuickInfo - ( - document: Document, - position: int, - cancellationToken: CancellationToken - ) - : Async<(range * QuickInfo option * QuickInfo option) option> = - - asyncMaybe { - let userOpName = "getQuickInfo" - let! lexerSymbol = document.TryFindFSharpLexerSymbolAsync(position, SymbolLookupKind.Greedy, true, true, userOpName) - let! _, checkFileResults = document.GetFSharpParseAndCheckResultsAsync(userOpName) |> liftAsync - let! sourceText = document.GetTextAsync cancellationToken - let idRange = lexerSymbol.Ident.idRange - let textLinePos = sourceText.Lines.GetLinePosition position - let fcsTextLineNumber = Line.fromZ textLinePos.Line - let lineText = (sourceText.Lines.GetLineFromPosition position).ToString() - - /// Gets the QuickInfo information for the orignal target - let getTargetSymbolQuickInfo (symbol, tag) = - asyncMaybe { - let targetQuickInfo = - checkFileResults.GetToolTip - (fcsTextLineNumber, idRange.EndColumn, lineText, lexerSymbol.FullIsland,tag) - - match targetQuickInfo with - | ToolTipText [] - | ToolTipText [ToolTipElement.None] -> return! None - | _ -> - let! targetTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan (sourceText, lexerSymbol.Range) - return { StructuredText = targetQuickInfo - Span = targetTextSpan - Symbol = symbol - SymbolKind = lexerSymbol.Kind } - } - - match lexerSymbol.Kind with - | LexerSymbolKind.String -> - let! targetQuickInfo = getTargetSymbolQuickInfo (None, FSharpTokenTag.STRING) - return lexerSymbol.Range, None, Some targetQuickInfo - - | _ -> - let! symbolUse = checkFileResults.GetSymbolUseAtLocation (fcsTextLineNumber, idRange.EndColumn, lineText, lexerSymbol.FullIsland) - - // if the target is in a signature file, adjusting the quick info is unnecessary - if isSignatureFile document.FilePath then - let! targetQuickInfo = getTargetSymbolQuickInfo (Some symbolUse.Symbol, FSharpTokenTag.IDENT) - return symbolUse.Range, None, Some targetQuickInfo - else - // find the declaration location of the target symbol, with a preference for signature files - let findSigDeclarationResult = checkFileResults.GetDeclarationLocation (idRange.StartLine, idRange.EndColumn, lineText, lexerSymbol.FullIsland, preferFlag=true) - - // it is necessary to retrieve the backup quick info because this acquires - // the textSpan designating where we want the quick info to appear. - let! targetQuickInfo = getTargetSymbolQuickInfo (Some symbolUse.Symbol, FSharpTokenTag.IDENT) - - let! result = - match findSigDeclarationResult with - | FindDeclResult.DeclFound declRange when isSignatureFile declRange.FileName -> - asyncMaybe { - let! sigQuickInfo = getQuickInfoFromRange(document, declRange, cancellationToken) - - // if the target was declared in a signature file, and the current file - // is not the corresponding module implementation file for that signature, - // the doccoms from the signature will overwrite any doccoms that might be - // present on the definition/implementation - let findImplDefinitionResult = checkFileResults.GetDeclarationLocation (idRange.StartLine, idRange.EndColumn, lineText, lexerSymbol.FullIsland, preferFlag=false) - - match findImplDefinitionResult with - | FindDeclResult.DeclNotFound _ - | FindDeclResult.ExternalDecl _ -> - return symbolUse.Range, Some sigQuickInfo, None - | FindDeclResult.DeclFound declRange -> - let! implQuickInfo = getQuickInfoFromRange(document, declRange, cancellationToken) - return symbolUse.Range, Some sigQuickInfo, Some { implQuickInfo with Span = targetQuickInfo.Span } - } - | _ -> async.Return None - |> liftAsync - - return result |> Option.defaultValue (symbolUse.Range, None, Some targetQuickInfo) - } - type internal FSharpAsyncQuickInfoSource ( statusBar: StatusBar, @@ -222,7 +86,7 @@ type internal FSharpAsyncQuickInfoSource | None, Some quickInfo -> let mainDescription, docs = FSharpAsyncQuickInfoSource.BuildSingleQuickInfoItem documentationBuilder quickInfo let imageId = Tokenizer.GetImageIdForSymbol(quickInfo.Symbol, quickInfo.SymbolKind) - let navigation = QuickInfoNavigation(statusBar, metadataAsSource, document, symbolUseRange) + let navigation = FSharpNavigation(statusBar, metadataAsSource, document, symbolUseRange) let content = QuickInfoViewProvider.provideContent(imageId, mainDescription, docs, navigation) let span = getTrackingSpan quickInfo.Span return QuickInfoItem(span, content) @@ -252,7 +116,7 @@ type internal FSharpAsyncQuickInfoSource ] |> ResizeArray let docs = RoslynHelpers.joinWithLineBreaks [documentation; typeParameterMap; usage; exceptions] let imageId = Tokenizer.GetImageIdForSymbol(targetQuickInfo.Symbol, targetQuickInfo.SymbolKind) - let navigation = QuickInfoNavigation(statusBar, metadataAsSource, document, symbolUseRange) + let navigation = FSharpNavigation(statusBar, metadataAsSource, document, symbolUseRange) let content = QuickInfoViewProvider.provideContent(imageId, mainDescription, docs, navigation) let span = getTrackingSpan targetQuickInfo.Span return QuickInfoItem(span, content) diff --git a/vsintegration/src/FSharp.Editor/QuickInfo/Views.fs b/vsintegration/src/FSharp.Editor/QuickInfo/Views.fs index abcc6bde066..db9a4306dbd 100644 --- a/vsintegration/src/FSharp.Editor/QuickInfo/Views.fs +++ b/vsintegration/src/FSharp.Editor/QuickInfo/Views.fs @@ -53,7 +53,7 @@ module internal QuickInfoViewProvider = imageId:ImageId option, description: seq, documentation: seq, - navigation:QuickInfoNavigation + navigation:FSharpNavigation ) = let buildContainerElement (itemGroup: seq) =