Skip to content

Commit

Permalink
Navigable symbols (ctrl+click to go to definition) (dotnet#4906)
Browse files Browse the repository at this point in the history
* First cut

* fix

* Order in the court

* Rename

* get rid of unused gtdservice

* wut

* Add file

* Remove unused opens

* do it right (marolf)

* Cleanup

* unused value

* Make the navigable URL the decl symbol

* refactor

* IT WAS A DIFFERENT MEF

* clean up a bit
  • Loading branch information
cartermp authored and TIHan committed Jun 2, 2018
1 parent 7bd0502 commit 2c317c1
Show file tree
Hide file tree
Showing 6 changed files with 544 additions and 420 deletions.
2 changes: 2 additions & 0 deletions FSharp.Editor.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@
<Compile Include="Completion\SignatureHelp.fs" />
<Compile Include="InlineRename\InlineRenameService.fs" />
<Compile Include="DocumentHighlights\DocumentHighlightsService.fs" />
<Compile Include="Navigation\GoToDefinition.fs" />
<Compile Include="Navigation\NavigableSymbolsService.fs" />
<Compile Include="Navigation\GoToDefinitionService.fs" />
<Compile Include="Navigation\NavigationBarItemService.fs" />
<Compile Include="Navigation\NavigateToSearchService.fs" />
Expand Down
383 changes: 383 additions & 0 deletions Navigation/GoToDefinition.fs

Large diffs are not rendered by default.

435 changes: 30 additions & 405 deletions Navigation/GoToDefinitionService.fs

Large diffs are not rendered by default.

103 changes: 103 additions & 0 deletions Navigation/NavigableSymbolsService.fs
Original file line number Diff line number Diff line change
@@ -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

[<AllowNullLiteral>]
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<SVsStatusbar,IVsStatusbar>())

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

[<Export(typeof<INavigableSymbolSourceProvider>)>]
[<Name("F# Navigable Symbol Service")>]
[<ContentType(Constants.FSharpContentType)>]
[<Order>]
type internal FSharpNavigableSymbolService
[<ImportingConstructor>]
(
[<Import(typeof<SVsServiceProvider>)>] serviceProvider: IServiceProvider,
checkerProvider: FSharpCheckerProvider,
projectInfoManager: FSharpProjectOptionsManager
) =

interface INavigableSymbolSourceProvider with
member __.TryCreateNavigableSymbolSource(_: ITextView, _: ITextBuffer) =
new FSharpNavigableSymbolSource(checkerProvider, projectInfoManager, serviceProvider) :> INavigableSymbolSource
28 changes: 20 additions & 8 deletions QuickInfo/Navigation.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SVsStatusbar,IVsStatusbar>())

member __.IsTargetValid (range: range) =
range <> rangeStartup &&
Expand All @@ -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 =
Expand All @@ -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
13 changes: 6 additions & 7 deletions QuickInfo/QuickInfoProvider.fs
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,10 @@ module private FSharpQuickInfo =

type internal FSharpAsyncQuickInfoSource
(
serviceProvider: IServiceProvider,
xmlMemberIndexService: IVsXMLMemberIndexService,
checkerProvider:FSharpCheckerProvider,
projectInfoManager:FSharpProjectOptionsManager,
gotoDefinitionService:FSharpGoToDefinitionService,
textBuffer:ITextBuffer
) =

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -256,11 +256,10 @@ type internal FSharpAsyncQuickInfoSource
type internal FSharpAsyncQuickInfoSourceProvider
[<ImportingConstructor>]
(
[<Import(typeof<SVsServiceProvider>)>] serviceProvider:IServiceProvider,
[<Import(typeof<SVsServiceProvider>)>] 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

0 comments on commit 2c317c1

Please sign in to comment.