Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VS Navigation consolidation #12105

Merged
merged 7 commits into from
Nov 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<TaggedText> * QuickInfoNavigation) option> = taggedText
member val TaggedText: Async<(ResizeArray<TaggedText> * FSharpNavigation) option> = taggedText
member val Computed: bool = computed with get, set
member val FullTypeSignature: string = fullTypeSignature
member val UiElement: UIElement = uiElement with get, set
Expand Down Expand Up @@ -191,7 +191,7 @@ type internal FSharpCodeLensService
let taggedText = ResizeArray()
typeLayout |> Seq.iter taggedText.Add
let statusBar = StatusBar(serviceProvider.GetService<SVsStatusbar, IVsStatusbar>())
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 ->
Expand Down
1 change: 0 additions & 1 deletion vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@
<Compile Include="Navigation\FindUsagesService.fs" />
<Compile Include="QuickInfo\NavigableTextRun.fs" />
<Compile Include="QuickInfo\WpfNagivableTextRunViewElementFactory.fs" />
<Compile Include="QuickInfo\Navigation.fs" />
<Compile Include="QuickInfo\Views.fs" />
<Compile Include="QuickInfo\QuickInfoProvider.fs" />
<Compile Include="Structure\BlockStructureService.fs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ open Microsoft.CodeAnalysis.Host
type internal IFSharpWorkspaceService =
inherit IWorkspaceService
abstract Checker: FSharpChecker
abstract FSharpProjectOptionsManager: FSharpProjectOptionsManager
abstract FSharpProjectOptionsManager: FSharpProjectOptionsManager
abstract MetadataAsSource: FSharpMetadataAsSourceService
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ type internal RoamingProfileStorageLocation(keyName: string) =
type internal FSharpWorkspaceServiceFactory
[<System.Composition.ImportingConstructor>]
(
metadataAsSourceService: FSharpMetadataAsSourceService
) =

// We have a lock just in case if multi-threads try to create a new IFSharpWorkspaceService -
Expand Down Expand Up @@ -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 } :> _

[<Sealed>]
type private FSharpSolutionEvents(projectManager: FSharpProjectOptionsManager, metadataAsSource: FSharpMetadataAsSourceService) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,14 @@ module internal MetadataAsSource =

[<Sealed>]
[<Export(typeof<FSharpMetadataAsSourceService>); Composition.Shared>]
type internal FSharpMetadataAsSourceService [<ImportingConstructor>] (projectContextFactory: IWorkspaceProjectContextFactory) =
type internal FSharpMetadataAsSourceService() =

let serviceProvider = ServiceProvider.GlobalProvider
let projs = System.Collections.Concurrent.ConcurrentDictionary<string, IWorkspaceProjectContext>()

let createMetadataProjectContext (projInfo: ProjectInfo) (docInfo: DocumentInfo) =
let componentModel = Package.GetGlobalService(typeof<ComponentModelHost.SComponentModel>) :?> ComponentModelHost.IComponentModel
let projectContextFactory = componentModel.GetService<IWorkspaceProjectContextFactory>()
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() =
Expand Down
238 changes: 237 additions & 1 deletion vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 =
Expand Down Expand Up @@ -488,4 +491,237 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) =
if result then
statusBar.Clear()
else
statusBar.TempMessage (SR.CannotNavigateUnknown())
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<QuickInfo option> =

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do it

true
| FSharpGoToDefinitionResult.ExternalAssembly(targetSymbolUse, metadataReferences), _ ->
gtd.NavigateToExternalDeclaration(targetSymbolUse, metadataReferences, cancellationToken, statusBar)
// 'true' means do it, like Sheev Palpatine would want us to.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do it

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
Loading