Skip to content

Commit

Permalink
Quickinfo Tooltip and GotoDefinition Navigation Improvements (dotnet#…
Browse files Browse the repository at this point in the history
…2683)

* full type name in tooltip, provisional tab preferred

* more entities made navigable

* use IGoToDefinition service

* this is used only here

* MEF import FSharpGotoDefinitionService into QuickInfoProvider

* speed up gotoDefinition

* additional GotoDefn navigation strategies

* quickinfo navigation stays in its lane

tooltip from .fsi links to .fsi
tooltip from .fs links to .fs
quick navigation if no redirect is necessary

* fix unittests

* restore recursive matchingDoc

* asynchronous navigation from tooltips

* fix cross project .fs -> .fs and .fsi -> .fsi Navigation

* cleanup and extra documentation

fixed bug in cross project .fs -> .fs navigation

* missed this one

* gotodefinition sig <-> impl at declaration location

* fix async workflow

* animate status bar search and timeout on msgs

* Better links styling

* integrate sig doccoms

* fix error introduced by prior merge

* fixed invalid type access in `getUnusedOpens`

* fix invalid span bug in `symbolIsFullyQualified`

* check if normalized doccom text matches

* cleanup status bar usage

* fix underline pen position, code cleanup and formatting

* do not show links for symbol itself
  • Loading branch information
cloudRoutine authored and KevinRansom committed Apr 3, 2017
1 parent 8ed0778 commit eb4e17b
Show file tree
Hide file tree
Showing 10 changed files with 860 additions and 192 deletions.
108 changes: 108 additions & 0 deletions Common/CodeAnalysisExtensions.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
[<AutoOpen>]
module internal Microsoft.VisualStudio.FSharp.Editor.CodeAnalysisExtensions

open Microsoft.CodeAnalysis
open Microsoft.FSharp.Compiler.Range

type Project with

/// Returns the projectIds of all projects within the same solution that directly reference this project
member this.GetDependentProjectIds () =
this.Solution.GetProjectDependencyGraph().GetProjectsThatDirectlyDependOnThisProject this.Id


/// Returns all projects within the same solution that directly reference this project.
member this.GetDependentProjects () =
this.Solution.GetProjectDependencyGraph().GetProjectsThatDirectlyDependOnThisProject this.Id
|> Seq.map this.Solution.GetProject


/// Returns the ProjectIds of all of the projects that this project directly or transitively depneds on
member this.GetProjectIdsOfAllProjectsThisProjectDependsOn () =
let graph = this.Solution.GetProjectDependencyGraph()
let transitiveDependencies = graph.GetProjectsThatThisProjectTransitivelyDependsOn this.Id
let directDependencies = graph.GetProjectsThatThisProjectDirectlyDependsOn this.Id
Seq.append directDependencies transitiveDependencies


/// The list all of the projects that this project directly or transitively depneds on
member this.GetAllProjectsThisProjectDependsOn () =
this.GetProjectIdsOfAllProjectsThisProjectDependsOn ()
|> Seq.map this.Solution.GetProject


type Solution with

/// Try to get a document inside the solution using the document's name
member self.TryGetDocumentNamed docName =
self.Projects |> Seq.tryPick (fun proj ->
proj.Documents |> Seq.tryFind (fun doc -> doc.Name = docName))


/// Try to find the documentId corresponding to the provided filepath within this solution
member self.TryGetDocumentFromPath filePath =
self.GetDocumentIdsWithFilePath filePath
|> Seq.tryHead |> Option.map (fun docId -> self.GetDocument docId)


/// Try to get a project inside the solution using the project's id
member self.TryGetProject (projId:ProjectId) =
if self.ContainsProject projId then Some (self.GetProject projId) else None


/// Returns the projectIds of all projects within this solution that directly reference the provided project
member self.GetDependentProjects (projectId:ProjectId) =
self.GetProjectDependencyGraph().GetProjectsThatDirectlyDependOnThisProject projectId
|> Seq.map self.GetProject


/// Returns the projectIds of all projects within this solution that directly reference the provided project
member self.GetDependentProjectIds (projectId:ProjectId) =
self.GetProjectDependencyGraph().GetProjectsThatDirectlyDependOnThisProject projectId


/// Returns the ProjectIds of all of the projects that directly or transitively depends on
member self.GetProjectIdsOfAllProjectReferences (projectId:ProjectId) =
let graph = self.GetProjectDependencyGraph()
let transitiveDependencies = graph.GetProjectsThatThisProjectTransitivelyDependsOn projectId
let directDependencies = graph.GetProjectsThatThisProjectDirectlyDependsOn projectId
Seq.append directDependencies transitiveDependencies


/// Returns all of the projects that this project that directly or transitively depends on
member self.GetAllProjectsThisProjectDependsOn (projectId:ProjectId) =
self.GetProjectIdsOfAllProjectReferences projectId
|> Seq.map self.GetProject


/// Try to retrieve the corresponding DocumentId for the range's file in the solution
/// and if a projectId is provided, only try to find the document within that project
/// or a project referenced by that project
member self.TryGetDocumentIdFromFSharpRange (range:range,?projectId:ProjectId) =

let filePath = System.IO.Path.GetFullPathSafe range.FileName
let checkProjectId (docId:DocumentId) =
if projectId.IsSome then docId.ProjectId = projectId.Value else false
//The same file may be present in many projects. We choose one from current or referenced project.
let rec matchingDoc = function
| [] -> None
| (docId:DocumentId)::_ when checkProjectId docId -> Some docId
| docId::tail ->
match projectId with
| Some projectId ->
if self.GetDependentProjectIds docId.ProjectId |> Seq.contains projectId
then Some docId
else matchingDoc tail
| None -> Some docId

self.GetDocumentIdsWithFilePath filePath |> List.ofSeq |> matchingDoc


/// Try to retrieve the corresponding Document for the range's file in the solution
/// and if a projectId is provided, only try to find the document within that project
/// or a project referenced by that project
member self.TryGetDocumentFromFSharpRange (range:range,?projectId:ProjectId) =
match projectId with
| Some projectId -> self.TryGetDocumentIdFromFSharpRange (range, projectId)
| None -> self.TryGetDocumentIdFromFSharpRange range
|> Option.map self.GetDocument
27 changes: 26 additions & 1 deletion Common/CommonConstants.fs
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,46 @@ open Microsoft.CodeAnalysis.Classification

[<RequireQualifiedAccess>]
module internal FSharpCommonConstants =

[<Literal>]
/// "871D2A70-12A2-4e42-9440-425DD92A4116"
let packageGuidString = "871D2A70-12A2-4e42-9440-425DD92A4116"

[<Literal>]
/// "BC6DD5A5-D4D6-4dab-A00D-A51242DBAF1B"
let languageServiceGuidString = "BC6DD5A5-D4D6-4dab-A00D-A51242DBAF1B"

[<Literal>]
/// "4EB7CCB7-4336-4FFD-B12B-396E9FD079A9"
let editorFactoryGuidString = "4EB7CCB7-4336-4FFD-B12B-396E9FD079A9"

[<Literal>]
/// "9B164E40-C3A2-4363-9BC5-EB4039DEF653"
let svsSettingsPersistenceManagerGuidString = "9B164E40-C3A2-4363-9BC5-EB4039DEF653"

[<Literal>]
/// "F#"
let FSharpLanguageName = "F#"

[<Literal>]
/// "F#"
let FSharpContentTypeName = "F#"

[<Literal>]
/// "F# Signature Help"
let FSharpSignatureHelpContentTypeName = "F# Signature Help"

[<Literal>]
/// "F# Language Service"
let FSharpLanguageServiceCallbackName = "F# Language Service"

[<Literal>]
/// "FSharp"
let FSharpLanguageLongName = "FSharp"

[<RequireQualifiedAccess>]
module internal FSharpProviderConstants =

[<Literal>]
let FSharpLanguageLongName = "FSharp"
/// "Session Capturing Quick Info Source Provider"
let SessionCapturingProvider = "Session Capturing Quick Info Source Provider"
8 changes: 6 additions & 2 deletions Common/CommonHelpers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -410,8 +410,12 @@ module internal Extensions =
open Microsoft.VisualStudio.FSharp.Editor.Logging

type System.IServiceProvider with

/// Retrieve a MEF Visual Studio Service of type 'T
member x.GetService<'T>() = x.GetService(typeof<'T>) :?> 'T
member x.GetService<'T, 'S>() = x.GetService(typeof<'S>) :?> 'T

/// Retrieve a SVs MEF Service of type 'S and cast it to type 'T
member x.GetService<'S,'T>() = x.GetService(typeof<'S>) :?> 'T

type Path with
static member GetFullPathSafe path =
Expand Down Expand Up @@ -526,7 +530,7 @@ module internal Extensions =
match declarationLocation with
| Some loc ->
let filePath = Path.GetFullPathSafe loc.FileName
let isScript = String.Equals(Path.GetExtension(filePath), ".fsx", StringComparison.OrdinalIgnoreCase)
let isScript = isScriptFile filePath
if isScript && filePath = currentDocument.FilePath then
Some SymbolDeclarationLocation.CurrentDocument
elif isScript then
Expand Down
14 changes: 0 additions & 14 deletions Common/CommonRoslynHelpers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,6 @@ module internal CommonRoslynHelpers =

let CollectTaggedText (list: List<_>) (t:TaggedText) = list.Add(TaggedText(roslynTag t.Tag, t.Text))

let CollectNavigableText (list: List<_>) (t: TaggedText) =
let rangeOpt =
match t with
| :? NavigableTaggedText as n -> Some n.Range
| _ -> None
list.Add(roslynTag t.Tag, t.Text, rangeOpt)

let StartAsyncAsTask cancellationToken computation =
let computation =
async {
Expand Down Expand Up @@ -349,10 +342,3 @@ module internal OpenDeclarationHelper =
else sourceText
sourceText, minPos |> Option.defaultValue 0

[<AutoOpen>]
module internal RoslynExtensions =
type Project with
/// The list of all other projects within the same solution that reference this project.
member this.GetDependentProjects() =
this.Solution.GetProjectDependencyGraph().GetProjectsThatDirectlyDependOnThisProject(this.Id)
|> Seq.map this.Solution.GetProject
3 changes: 3 additions & 0 deletions Common/Logging.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace Microsoft.VisualStudio.FSharp.Editor.Logging

open System
open System.Diagnostics
open System.ComponentModel.Composition
open Microsoft.VisualStudio.Shell
open Microsoft.VisualStudio.Shell.Interop
Expand Down Expand Up @@ -70,6 +71,8 @@ type [<Export>] Logger [<ImportingConstructor>]
[<AutoOpen>]
module Logging =

let inline debug msg = Printf.kprintf Debug.WriteLine msg

let private logger = lazy Logger(Logger.GlobalServiceProvider)
let private log logType msg = logger.Value.Log(logType,msg)

Expand Down
64 changes: 63 additions & 1 deletion Common/Pervasive.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,37 @@
module Microsoft.VisualStudio.FSharp.Editor.Pervasive

open System
open System.IO
open System.Threading
open System.Threading.Tasks
open System.Diagnostics


/// Checks if the filePath ends with ".fsi"
let isSignatureFile (filePath:string) =
Path.GetExtension filePath = ".fsi"

/// Checks if the file paht ends with '.fsx' or '.fsscript'
let isScriptFile (filePath:string) =
let ext = Path.GetExtension filePath
String.Equals (ext,".fsi",StringComparison.OrdinalIgnoreCase) || String.Equals (ext,".fsscript",StringComparison.OrdinalIgnoreCase)

/// Path combination operator
let (</>) path1 path2 = Path.Combine (path1, path2)


type Path with
static member GetFullPathSafe path =
try Path.GetFullPath path
with _ -> path

static member GetFileNameSafe path =
try Path.GetFileName path
with _ -> path


[<RequireQualifiedAccess>]
module String =
open System.IO

let getLines (str: string) =
use reader = new StringReader(str)
Expand Down Expand Up @@ -178,6 +204,8 @@ let inline liftAsync (computation : Async<'T>) : Async<'T option> =
return Some a
}

let liftTaskAsync task = task |> Async.AwaitTask |> liftAsync

module Async =
let map (f: 'T -> 'U) (a: Async<'T>) : Async<'U> =
async {
Expand All @@ -199,6 +227,40 @@ module Async =
}
async { return! agent.PostAndAsyncReply id }


type Async with

/// Better implementation of Async.AwaitTask that correctly passes the exception of a failed task to the async mechanism
static member AwaitTaskCorrect (task:Task) : Async<unit> =
Async.FromContinuations (fun (successCont,exceptionCont,_cancelCont) ->
task.ContinueWith (fun (task:Task) ->
if task.IsFaulted then
let e = task.Exception
if e.InnerExceptions.Count = 1 then
exceptionCont e.InnerExceptions.[0]
else exceptionCont e
elif task.IsCanceled then
exceptionCont(TaskCanceledException ())
else successCont ())
|> ignore)

/// Better implementation of Async.AwaitTask that correctly passes the exception of a failed task to the async mechanism
static member AwaitTaskCorrect (task:'T Task) : Async<'T> =
Async.FromContinuations( fun (successCont,exceptionCont,_cancelCont) ->
task.ContinueWith (fun (task:'T Task) ->
if task.IsFaulted then
let e = task.Exception
if e.InnerExceptions.Count = 1 then
exceptionCont e.InnerExceptions.[0]
else exceptionCont e
elif task.IsCanceled then
exceptionCont (TaskCanceledException ())
else successCont task.Result)
|> ignore)
static member RunTaskSynchronously task =
task |> Async.AwaitTask |> Async.RunSynchronously


type AsyncBuilder with
member __.Bind(computation: System.Threading.Tasks.Task<'a>, binder: 'a -> Async<'b>): Async<'b> =
async {
Expand Down
7 changes: 5 additions & 2 deletions Diagnostics/UnusedOpensDiagnosticAnalyzer.fs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,10 @@ module private UnusedOpens =

let symbolIsFullyQualified (sourceText: SourceText) (sym: FSharpSymbolUse) (fullName: string) =
match CommonRoslynHelpers.TryFSharpRangeToTextSpan(sourceText, sym.RangeAlternate) with
| Some span -> sourceText.ToString(span) = fullName
| Some span // check that the symbol hasn't provided an invalid span
when sourceText.Length < span.Start
|| sourceText.Length < span.End -> false
| Some span -> sourceText.ToString span = fullName
| None -> false

let getUnusedOpens (sourceText: SourceText) (parsedInput: ParsedInput) (symbolUses: FSharpSymbolUse[]) =
Expand Down Expand Up @@ -102,7 +105,7 @@ module private UnusedOpens =
Some ([apc.FullName], apc.Group.EnclosingEntity)
| SymbolUse.UnionCase uc when not (isQualified uc.FullName) ->
Some ([uc.FullName], Some uc.ReturnType.TypeDefinition)
| SymbolUse.Parameter p when not (isQualified p.FullName) ->
| SymbolUse.Parameter p when not (isQualified p.FullName) && p.Type.HasTypeDefinition ->
Some ([p.FullName], Some p.Type.TypeDefinition)
| _ -> None

Expand Down
4 changes: 3 additions & 1 deletion FSharp.Editor.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
<Compile Include="Common\Logging.fs" />
<Compile Include="Common\CommonHelpers.fs" />
<Compile Include="Common\CommonRoslynHelpers.fs" />
<Compile Include="Common\CodeAnalysisExtensions.fs" />
<Compile Include="Common\ContentType.fs" />
<Compile Include="Common\LanguageService.fs" />
<Compile Include="Common\SymbolHelpers.fs" />
Expand All @@ -57,13 +58,13 @@
<Compile Include="Completion\FileSystemCompletion.fs" />
<Compile Include="Completion\CompletionService.fs" />
<Compile Include="Completion\SignatureHelp.fs" />
<Compile Include="QuickInfo\QuickInfoProvider.fs" />
<Compile Include="InlineRename\InlineRenameService.fs" />
<Compile Include="DocumentHighlights\DocumentHighlightsService.fs" />
<Compile Include="Navigation\GoToDefinitionService.fs" />
<Compile Include="Navigation\NavigationBarItemService.fs" />
<Compile Include="Navigation\NavigateToSearchService.fs" />
<Compile Include="Navigation\FindUsagesService.fs" />
<Compile Include="QuickInfo\QuickInfoProvider.fs" />
<Compile Include="BlockComment\CommentUncommentService.fs" />
<Compile Include="Structure\Structure.fs" />
<Compile Include="Structure\BlockStructureService.fs" />
Expand Down Expand Up @@ -113,6 +114,7 @@
<Reference Include="System.Drawing" />
<Reference Include="System.Xaml" />
<Reference Include="System.Xml" />
<Reference Include="UIAutomationTypes" />
<Reference Include="WindowsBase" />
<Reference Include="System" />
<Reference Include="PresentationCore" />
Expand Down
Loading

0 comments on commit eb4e17b

Please sign in to comment.