Skip to content
This repository was archived by the owner on Dec 23, 2024. It is now read-only.

Commit bbbcfb5

Browse files
majochaKevinRansom
authored andcommitted
Clickable QuickInfo with "go to type" (dotnet#2574)
* custom tooltip test * tagClass augmented * kinda works * getting there * module, alias * not sure if works * fix build * go away! * parens out * halleluyah it builds * blank space on empty doc removed * more sensible * handle linked files * collapse empty TextBlocks * styling, simple tooltip, handle scripts * dismiss quickinfo on navigation * reliably dismiss quickinfo session * interface instead of boxing
1 parent 2bd4721 commit bbbcfb5

File tree

3 files changed

+162
-51
lines changed

3 files changed

+162
-51
lines changed

Common/CommonRoslynHelpers.fs

Lines changed: 43 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -44,44 +44,50 @@ module internal CommonRoslynHelpers =
4444
Assert.Exception(task.Exception.GetBaseException())
4545
raise(task.Exception.GetBaseException())
4646

47-
/// Converts `TaggedText` from the F# Compiler to `Microsoft.CodeAnalysis.TaggedText` format for use in tooltips
48-
let TaggedTextToRoslyn t =
49-
match t with
50-
| TaggedText.ActivePatternCase t
51-
| TaggedText.ActivePatternResult t -> TaggedText(TextTags.Enum, t)
52-
| TaggedText.Alias t -> TaggedText(TextTags.Class, t)
53-
| TaggedText.Class t -> TaggedText(TextTags.Class, t)
54-
| TaggedText.Delegate t -> TaggedText(TextTags.Delegate, t)
55-
| TaggedText.Enum t -> TaggedText(TextTags.Enum, t)
56-
| TaggedText.Event t -> TaggedText(TextTags.Event, t)
57-
| TaggedText.Field t -> TaggedText(TextTags.Field, t)
58-
| TaggedText.Interface t -> TaggedText(TextTags.Interface, t)
59-
| TaggedText.Keyword t -> TaggedText(TextTags.Keyword, t)
60-
| TaggedText.LineBreak t -> TaggedText(TextTags.LineBreak, t)
61-
| TaggedText.Local t -> TaggedText(TextTags.Local, t)
62-
| TaggedText.Member t -> TaggedText(TextTags.Property, t)
63-
| TaggedText.Method t -> TaggedText(TextTags.Method, t)
64-
| TaggedText.Module t -> TaggedText(TextTags.Module, t)
65-
| TaggedText.ModuleBinding t -> TaggedText(TextTags.Property, t)
66-
| TaggedText.Namespace t -> TaggedText(TextTags.Namespace, t)
67-
| TaggedText.NumericLiteral t -> TaggedText(TextTags.NumericLiteral, t)
68-
| TaggedText.Operator t -> TaggedText(TextTags.Operator, t)
69-
| TaggedText.Parameter t -> TaggedText(TextTags.Parameter, t)
70-
| TaggedText.Property t -> TaggedText(TextTags.Property, t)
71-
| TaggedText.Punctuation t -> TaggedText(TextTags.Punctuation, t)
72-
| TaggedText.Record t -> TaggedText(TextTags.Class, t)
73-
| TaggedText.RecordField t -> TaggedText(TextTags.Property, t)
74-
| TaggedText.Space t -> TaggedText(TextTags.Space, t)
75-
| TaggedText.StringLiteral t -> TaggedText(TextTags.StringLiteral, t)
76-
| TaggedText.Struct t -> TaggedText(TextTags.Struct, t)
77-
| TaggedText.Text t -> TaggedText(TextTags.Text, t)
78-
| TaggedText.TypeParameter t -> TaggedText(TextTags.TypeParameter, t)
79-
| TaggedText.Union t -> TaggedText(TextTags.Class, t)
80-
| TaggedText.UnionCase t -> TaggedText(TextTags.Enum, t)
81-
| TaggedText.UnknownEntity t -> TaggedText(TextTags.Property, t)
82-
| TaggedText.UnknownType t -> TaggedText(TextTags.Class, t)
47+
/// maps from `LayoutTag` of the F# Compiler to Roslyn `TextTags` for use in tooltips
48+
let roslynTag = function
49+
| LayoutTag.ActivePatternCase
50+
| LayoutTag.ActivePatternResult
51+
| LayoutTag.UnionCase
52+
| LayoutTag.Enum -> TextTags.Enum
53+
| LayoutTag.Alias
54+
| LayoutTag.Class
55+
| LayoutTag.Union
56+
| LayoutTag.Record
57+
| LayoutTag.UnknownType -> TextTags.Class
58+
| LayoutTag.Delegate -> TextTags.Delegate
59+
| LayoutTag.Event -> TextTags.Event
60+
| LayoutTag.Field -> TextTags.Field
61+
| LayoutTag.Interface -> TextTags.Interface
62+
| LayoutTag.Struct -> TextTags.Struct
63+
| LayoutTag.Keyword -> TextTags.Keyword
64+
| LayoutTag.Local -> TextTags.Local
65+
| LayoutTag.Member
66+
| LayoutTag.ModuleBinding
67+
| LayoutTag.RecordField
68+
| LayoutTag.Property -> TextTags.Property
69+
| LayoutTag.Method -> TextTags.Method
70+
| LayoutTag.Namespace -> TextTags.Namespace
71+
| LayoutTag.Module -> TextTags.Module
72+
| LayoutTag.LineBreak -> TextTags.LineBreak
73+
| LayoutTag.Space -> TextTags.Space
74+
| LayoutTag.NumericLiteral -> TextTags.NumericLiteral
75+
| LayoutTag.Operator -> TextTags.Operator
76+
| LayoutTag.Parameter -> TextTags.Parameter
77+
| LayoutTag.TypeParameter -> TextTags.TypeParameter
78+
| LayoutTag.Punctuation -> TextTags.Punctuation
79+
| LayoutTag.StringLiteral -> TextTags.StringLiteral
80+
| LayoutTag.Text
81+
| LayoutTag.UnknownEntity -> TextTags.Text
8382

84-
let CollectTaggedText (list: List<_>) t = list.Add(TaggedTextToRoslyn t)
83+
let CollectTaggedText (list: List<_>) (t:TaggedText) = list.Add(TaggedText(roslynTag t.Tag, t.Text))
84+
85+
let CollectNavigableText (list: List<_>) (t: TaggedText) =
86+
let rangeOpt =
87+
match t with
88+
| :? NavigableTaggedText as n -> Some n.Range
89+
| _ -> None
90+
list.Add(roslynTag t.Tag, t.Text, rangeOpt)
8591

8692
let StartAsyncAsTask cancellationToken computation =
8793
let computation =

FSharp.Editor.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@
108108
<Reference Include="mscorlib" />
109109
<Reference Include="PresentationFramework" />
110110
<Reference Include="System.Drawing" />
111+
<Reference Include="System.Xaml" />
111112
<Reference Include="System.Xml" />
112113
<Reference Include="WindowsBase" />
113114
<Reference Include="System" />

QuickInfo/QuickInfoProvider.fs

Lines changed: 118 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,133 @@
33
namespace Microsoft.VisualStudio.FSharp.Editor
44

55
open System
6-
open System.Composition
76
open System.Threading
87
open System.Threading.Tasks
8+
open System.Windows
9+
open System.Windows.Controls
10+
open System.ComponentModel.Composition
911

1012
open Microsoft.CodeAnalysis
13+
open Microsoft.CodeAnalysis.Classification
1114
open Microsoft.CodeAnalysis.Editor
15+
open Microsoft.CodeAnalysis.Editor.Shared.Utilities
16+
open Microsoft.CodeAnalysis.Editor.Shared.Extensions
1217
open Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.QuickInfo
18+
open Microsoft.CodeAnalysis.Navigation
1319
open Microsoft.CodeAnalysis.Text
1420

1521
open Microsoft.VisualStudio.FSharp.LanguageService
1622
open Microsoft.VisualStudio.Shell
1723
open Microsoft.VisualStudio.Shell.Interop
24+
open Microsoft.VisualStudio.Utilities
1825
open Microsoft.VisualStudio.Language.Intellisense
1926

2027
open Microsoft.FSharp.Compiler.SourceCodeServices
28+
open Microsoft.FSharp.Compiler.Range
29+
open Microsoft.FSharp.Compiler.CompileOps
30+
31+
open CommonRoslynHelpers
32+
33+
module internal FSharpQuickInfo =
34+
35+
[<Literal>]
36+
let SessionCapturingProviderName = "Session Capturing Quick Info Source Provider"
37+
38+
let mutable currentSession = None
39+
40+
[<Export(typeof<IQuickInfoSourceProvider>)>]
41+
[<Name(SessionCapturingProviderName)>]
42+
[<Order(After = PredefinedQuickInfoProviderNames.Semantic)>]
43+
[<ContentType(FSharpCommonConstants.FSharpContentTypeName)>]
44+
type SourceProviderForCapturingSession() =
45+
interface IQuickInfoSourceProvider with
46+
member __.TryCreateQuickInfoSource _ =
47+
{ new IQuickInfoSource with
48+
member __.AugmentQuickInfoSession(session,_,_) = currentSession <- Some session
49+
member __.Dispose() = () }
50+
51+
let fragment(content, typemap: ClassificationTypeMap, thisDoc: Document) =
52+
53+
let workspace = thisDoc.Project.Solution.Workspace
54+
let documentNavigationService = workspace.Services.GetService<IDocumentNavigationService>()
55+
let solution = workspace.CurrentSolution
56+
57+
let documentId (range: range) =
58+
let filePath = System.IO.Path.GetFullPathSafe range.FileName
59+
let projectOf (id : DocumentId) = solution.GetDocument(id).Project
60+
61+
//The same file may be present in many projects. We choose one from current or referenced project.
62+
let rec matchingDoc = function
63+
| [] -> None
64+
| id::_ when projectOf id = thisDoc.Project || IsScript thisDoc.FilePath -> Some id
65+
| id::tail ->
66+
if (projectOf id).GetDependentProjects() |> Seq.contains thisDoc.Project then Some id
67+
else matchingDoc tail
68+
solution.GetDocumentIdsWithFilePath(filePath) |> List.ofSeq |> matchingDoc
69+
70+
let canGoTo range =
71+
range <> rangeStartup && documentId range |> Option.isSome
72+
73+
let goTo range =
74+
asyncMaybe {
75+
let! id = documentId range
76+
let! src = solution.GetDocument(id).GetTextAsync()
77+
let! span = CommonRoslynHelpers.TryFSharpRangeToTextSpan(src, range)
78+
if documentNavigationService.TryNavigateToSpan(workspace, id, span) then
79+
let! session = currentSession
80+
session.Dismiss()
81+
} |> Async.Ignore |> Async.StartImmediate
82+
83+
let formatMap = typemap.ClassificationFormatMapService.GetClassificationFormatMap("tooltip")
84+
85+
let props =
86+
ClassificationTags.GetClassificationTypeName
87+
>> typemap.GetClassificationType
88+
>> formatMap.GetTextProperties
89+
90+
let inlines = seq {
91+
for (tag, text, rangeOpt) in content do
92+
let run =
93+
match rangeOpt with
94+
| Some(range) when canGoTo range ->
95+
let h = Documents.Hyperlink(Documents.Run(text), ToolTip = range.FileName)
96+
h.Click.Add <| fun _ -> goTo range
97+
h :> Documents.Inline
98+
| _ ->
99+
Documents.Run(text) :> Documents.Inline
100+
DependencyObjectExtensions.SetTextProperties(run, props tag)
101+
yield run
102+
}
103+
104+
let create() =
105+
let tb = TextBlock(TextWrapping = TextWrapping.Wrap, TextTrimming = TextTrimming.None)
106+
DependencyObjectExtensions.SetDefaultTextProperties(tb, formatMap)
107+
tb.Inlines.AddRange(inlines)
108+
if tb.Inlines.Count = 0 then tb.Visibility <- Visibility.Collapsed
109+
tb :> FrameworkElement
110+
111+
{ new IDeferredQuickInfoContent with member x.Create() = create() }
112+
113+
let tooltip(symbolGlyph, mainDescription, documentation) =
114+
115+
let empty =
116+
{ new IDeferredQuickInfoContent with
117+
member x.Create() = TextBlock(Visibility = Visibility.Collapsed) :> FrameworkElement }
118+
119+
let roslynQuickInfo = QuickInfoDisplayDeferredContent(symbolGlyph, null, mainDescription, documentation, empty, empty, empty, empty)
120+
121+
let create() =
122+
let qi = roslynQuickInfo.Create()
123+
let style = Style(typeof<Documents.Hyperlink>)
124+
style.Setters.Add(Setter(Documents.Inline.TextDecorationsProperty, null))
125+
let trigger = DataTrigger(Binding = Data.Binding("IsMouseOver", Source = qi), Value = true)
126+
trigger.Setters.Add(Setter(Documents.Inline.TextDecorationsProperty, TextDecorations.Underline))
127+
style.Triggers.Add(trigger)
128+
qi.Resources.Add(typeof<Documents.Hyperlink>, style)
129+
qi
130+
131+
{ new IDeferredQuickInfoContent with member x.Create() = create() }
21132

22-
[<Shared>]
23133
[<ExportQuickInfoProvider(PredefinedQuickInfoProviderNames.Semantic, FSharpCommonConstants.FSharpLanguageName)>]
24134
type internal FSharpQuickInfoProvider
25135
[<System.ComponentModel.Composition.ImportingConstructor>]
@@ -64,21 +174,15 @@ type internal FSharpQuickInfoProvider
64174
let documentation = Collections.Generic.List()
65175
XmlDocumentation.BuildDataTipText(
66176
documentationBuilder,
67-
CommonRoslynHelpers.CollectTaggedText mainDescription,
68-
CommonRoslynHelpers.CollectTaggedText documentation,
177+
CommonRoslynHelpers.CollectNavigableText mainDescription,
178+
CommonRoslynHelpers.CollectNavigableText documentation,
69179
toolTipElement)
70-
let empty = ClassifiableDeferredContent(Array.Empty<TaggedText>(), typeMap);
71180
let content =
72-
QuickInfoDisplayDeferredContent
181+
FSharpQuickInfo.tooltip
73182
(
74-
symbolGlyph = SymbolGlyphDeferredContent(CommonRoslynHelpers.GetGlyphForSymbol(symbol, symbolKind), glyphService),
75-
warningGlyph = null,
76-
mainDescription = ClassifiableDeferredContent(mainDescription, typeMap),
77-
documentation = ClassifiableDeferredContent(documentation, typeMap),
78-
typeParameterMap = empty,
79-
anonymousTypes = empty,
80-
usageText = empty,
81-
exceptionText = empty
183+
SymbolGlyphDeferredContent(CommonRoslynHelpers.GetGlyphForSymbol(symbol, symbolKind), glyphService),
184+
FSharpQuickInfo.fragment(mainDescription, typeMap, document),
185+
FSharpQuickInfo.fragment(documentation, typeMap, document)
82186
)
83187
return QuickInfoItem(textSpan, content)
84188
}

0 commit comments

Comments
 (0)