diff --git a/vsintegration/src/FSharp.Editor/Hints/Hints.fs b/vsintegration/src/FSharp.Editor/Hints/Hints.fs index dcdb17e5eee..9b356db9855 100644 --- a/vsintegration/src/FSharp.Editor/Hints/Hints.fs +++ b/vsintegration/src/FSharp.Editor/Hints/Hints.fs @@ -2,6 +2,8 @@ namespace Microsoft.VisualStudio.FSharp.Editor.Hints +open System.Threading +open Microsoft.CodeAnalysis open FSharp.Compiler.Text module Hints = @@ -17,6 +19,7 @@ module Hints = Kind: HintKind Range: range Parts: TaggedText list + GetTooltip: Document -> Async } let serialize kind = diff --git a/vsintegration/src/FSharp.Editor/Hints/InlineParameterNameHints.fs b/vsintegration/src/FSharp.Editor/Hints/InlineParameterNameHints.fs index 35601c2799b..ef6a49413dc 100644 --- a/vsintegration/src/FSharp.Editor/Hints/InlineParameterNameHints.fs +++ b/vsintegration/src/FSharp.Editor/Hints/InlineParameterNameHints.fs @@ -12,11 +12,39 @@ open Hints type InlineParameterNameHints(parseResults: FSharpParseFileResults) = + let getTooltip (symbol: FSharpSymbol) _ = + async { + // This brings little value as of now. Basically just discerns fields from parameters + // and fills the tooltip bubble which otherwise looks like a visual glitch. + // + // Now, we could add some type information here, like C# does, for example: + // (parameter) int number + // + // This would work for simple cases but can get weird in more complex ones. + // Consider this code: + // + // let rev list = list |> List.rev + // let reversed = rev [ 42 ] + // + // With the trivial implementation, the tooltip for hint before [ 42 ] will look like: + // parameter 'a list list + // + // Arguably, this can look confusing. + // Hence, I wouldn't add type info to the text until we have some coloring plugged in here. + // + // Some alignment with C# also needs to be kept in mind, + // e.g. taking the type in braces would be opposite to what C# does which can be confusing. + let text = symbol.ToString() + + return [ TaggedText(TextTag.Text, text) ] + } + let getParameterHint (range: range, parameter: FSharpParameter) = { Kind = HintKind.ParameterNameHint Range = range.StartRange Parts = [ TaggedText(TextTag.Text, $"{parameter.DisplayName} = ") ] + GetTooltip = getTooltip parameter } let getFieldHint (range: range, field: FSharpField) = @@ -24,6 +52,7 @@ type InlineParameterNameHints(parseResults: FSharpParseFileResults) = Kind = HintKind.ParameterNameHint Range = range.StartRange Parts = [ TaggedText(TextTag.Text, $"{field.Name} = ") ] + GetTooltip = getTooltip field } let parameterNameExists (parameter: FSharpParameter) = parameter.DisplayName <> "" diff --git a/vsintegration/src/FSharp.Editor/Hints/InlineReturnTypeHints.fs b/vsintegration/src/FSharp.Editor/Hints/InlineReturnTypeHints.fs index e0a0a9e8f32..a5796b6d196 100644 --- a/vsintegration/src/FSharp.Editor/Hints/InlineReturnTypeHints.fs +++ b/vsintegration/src/FSharp.Editor/Hints/InlineReturnTypeHints.fs @@ -19,6 +19,13 @@ type InlineReturnTypeHints(parseFileResults: FSharpParseFileResults, symbol: FSh TaggedText(TextTag.Space, " ") ]) + let getTooltip _ = + async { + let typeAsString = symbol.ReturnParameter.Type.TypeDefinition.ToString() + let text = $"type {typeAsString}" + return [ TaggedText(TextTag.Text, text) ] + } + let getHint symbolUse range = getHintParts symbolUse |> Option.map (fun parts -> @@ -26,6 +33,7 @@ type InlineReturnTypeHints(parseFileResults: FSharpParseFileResults, symbol: FSh Kind = HintKind.ReturnTypeHint Range = range Parts = parts + GetTooltip = getTooltip }) let isValidForHint (symbol: FSharpMemberOrFunctionOrValue) = symbol.IsFunction diff --git a/vsintegration/src/FSharp.Editor/Hints/InlineTypeHints.fs b/vsintegration/src/FSharp.Editor/Hints/InlineTypeHints.fs index 93501e19a79..a29b2302bb7 100644 --- a/vsintegration/src/FSharp.Editor/Hints/InlineTypeHints.fs +++ b/vsintegration/src/FSharp.Editor/Hints/InlineTypeHints.fs @@ -21,11 +21,29 @@ type InlineTypeHints(parseResults: FSharpParseFileResults, symbol: FSharpMemberO // not sure when this can happen | None -> [] + let getTooltip _ = + async { + // Done this way because I am not sure if we want to show full-blown types everywhere, + // e.g. Microsoft.FSharp.Core.string instead of string. + // On the other hand, for user types this could be useful. + // Then there should be some smarter algorithm here. + let text = + if symbol.FullType.HasTypeDefinition then + let typeAsString = symbol.FullType.TypeDefinition.ToString() + $"type {typeAsString}" + else + // already includes the word "type" + symbol.FullType.ToString() + + return [ TaggedText(TextTag.Text, text) ] + } + let getHint symbol (symbolUse: FSharpSymbolUse) = { Kind = HintKind.TypeHint Range = symbolUse.Range.EndRange Parts = getHintParts symbol symbolUse + GetTooltip = getTooltip } let isSolved (symbol: FSharpMemberOrFunctionOrValue) = diff --git a/vsintegration/src/FSharp.Editor/Hints/NativeToRoslynHintConverter.fs b/vsintegration/src/FSharp.Editor/Hints/NativeToRoslynHintConverter.fs index 5b9c56d6f9a..1eb05429ea6 100644 --- a/vsintegration/src/FSharp.Editor/Hints/NativeToRoslynHintConverter.fs +++ b/vsintegration/src/FSharp.Editor/Hints/NativeToRoslynHintConverter.fs @@ -2,10 +2,14 @@ namespace Microsoft.VisualStudio.FSharp.Editor.Hints +open System open System.Collections.Immutable +open System.Threading +open System.Threading.Tasks open Microsoft.CodeAnalysis.Text open Microsoft.CodeAnalysis.ExternalAccess.FSharp.InlineHints open Microsoft.VisualStudio.FSharp.Editor +open Microsoft.CodeAnalysis open FSharp.Compiler.Text open Hints @@ -21,7 +25,14 @@ module NativeToRoslynHintConverter = let text = taggedText.Text RoslynTaggedText(tag, text) + let nativeToRoslynFunc nativeFunc = + Func>>(fun doc ct -> + nativeFunc doc + |> Async.map (List.map nativeToRoslynText >> ImmutableArray.CreateRange) + |> fun comp -> Async.StartAsTask(comp, cancellationToken = ct)) + let convert sourceText hint = let span = rangeToSpan hint.Range sourceText let displayParts = hint.Parts |> Seq.map nativeToRoslynText - FSharpInlineHint(span, displayParts.ToImmutableArray()) + let getDescription = hint.GetTooltip |> nativeToRoslynFunc + FSharpInlineHint(span, displayParts.ToImmutableArray(), getDescription) diff --git a/vsintegration/tests/FSharp.Editor.Tests/Hints/HintTestFramework.fs b/vsintegration/tests/FSharp.Editor.Tests/Hints/HintTestFramework.fs index 4717e0f9e65..9a0111810dd 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/Hints/HintTestFramework.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/Hints/HintTestFramework.fs @@ -12,9 +12,13 @@ module HintTestFramework = // another representation for extra convenience type TestHint = - { Content: string; Location: int * int } + { + Content: string + Location: int * int + Tooltip: string + } - let private convert hint = + let private convert (hint, tooltip) = let content = hint.Parts |> Seq.map (fun hintPart -> hintPart.Text) |> String.concat "" @@ -26,6 +30,7 @@ module HintTestFramework = { Content = content Location = location + Tooltip = tooltip } let getFsDocument code = @@ -57,9 +62,17 @@ module HintTestFramework = let getHints (document: Document) hintKinds = async { let! ct = Async.CancellationToken + + let getTooltip hint = + async { + let! roslynTexts = hint.GetTooltip document + return roslynTexts |> Seq.map (fun roslynText -> roslynText.Text) |> String.concat "" + } + let! sourceText = document.GetTextAsync ct |> Async.AwaitTask let! hints = HintService.getHintsForDocument sourceText document hintKinds "test" ct - return hints |> Seq.map convert + let! tooltips = hints |> Seq.map getTooltip |> Async.Parallel + return tooltips |> Seq.zip hints |> Seq.map convert } |> Async.RunSynchronously diff --git a/vsintegration/tests/FSharp.Editor.Tests/Hints/InlineParameterNameHintTests.fs b/vsintegration/tests/FSharp.Editor.Tests/Hints/InlineParameterNameHintTests.fs index 7d58b05bd81..d179a792629 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/Hints/InlineParameterNameHintTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/Hints/InlineParameterNameHintTests.fs @@ -22,6 +22,7 @@ let greeting = greet "darkness" { Content = "friend = " Location = (2, 22) + Tooltip = "parameter friend" } ] @@ -45,10 +46,12 @@ let greeting2 = greet "Liam" { Content = "friend = " Location = (2, 23) + Tooltip = "parameter friend" } { Content = "friend = " Location = (3, 23) + Tooltip = "parameter friend" } ] @@ -71,10 +74,12 @@ let greeting = greet "Liam" "Noel" { Content = "friend1 = " Location = (2, 22) + Tooltip = "parameter friend1" } { Content = "friend2 = " Location = (2, 29) + Tooltip = "parameter friend2" } ] @@ -97,10 +102,12 @@ let greeting = greet ("Liam", "Noel") { Content = "friend1 = " Location = (2, 23) + Tooltip = "parameter friend1" } { Content = "friend2 = " Location = (2, 31) + Tooltip = "parameter friend2" } ] @@ -132,10 +139,12 @@ let odd = evenOrOdd 41 { Content = "number = " Location = (10, 22) + Tooltip = "parameter number" } { Content = "number = " Location = (11, 21) + Tooltip = "parameter number" } ] @@ -201,6 +210,7 @@ let theAnswer = System.Console.WriteLine 42 { Content = "value = " Location = (1, 42) + Tooltip = "parameter value" } ] @@ -231,23 +241,32 @@ let a = c.Normal "hmm" { Content = "curr1 = " Location = (8, 20) + Tooltip = "parameter curr1" } { Content = "curr2 = " Location = (8, 27) + Tooltip = "parameter curr2" + } + { + Content = "x = " + Location = (8, 30) + Tooltip = "parameter x" } - { Content = "x = "; Location = (8, 30) } { Content = "what = " Location = (9, 19) + Tooltip = "parameter what" } { Content = "what2 = " Location = (9, 26) + Tooltip = "parameter what2" } { Content = "alone = " Location = (10, 18) + Tooltip = "parameter alone" } ] @@ -272,14 +291,17 @@ let a = C (1, "") { Content = "blahFirst = " Location = (2, 40) + Tooltip = "parameter blahFirst" } { Content = "blah = " Location = (4, 12) + Tooltip = "parameter blah" } { Content = "blah2 = " Location = (4, 15) + Tooltip = "parameter blah2" } ] @@ -306,14 +328,17 @@ let b = Rectangle (1, 2) { Content = "side = " Location = (5, 16) + Tooltip = "field side" } { Content = "width = " Location = (6, 20) + Tooltip = "field width" } { Content = "height = " Location = (6, 23) + Tooltip = "field height" } ] @@ -340,10 +365,12 @@ let d = Circle 1 { Content = "side1 = " Location = (5, 19) + Tooltip = "field side1" } { Content = "side3 = " Location = (5, 25) + Tooltip = "field side3" } ] @@ -396,10 +423,12 @@ let x = "test".Split("").[0].Split(""); { Content = "separator = " Location = (1, 22) + Tooltip = "parameter separator" } { Content = "separator = " Location = (1, 36) + Tooltip = "parameter separator" } ] @@ -425,6 +454,7 @@ type MyType() = { Content = "beep = " Location = (5, 37) + Tooltip = "parameter beep" } ] @@ -465,14 +495,17 @@ let test sequences = { Content = "mapping = " Location = (3, 16) + Tooltip = "parameter mapping" } { Content = "mapping = " Location = (3, 53) + Tooltip = "parameter mapping" } { Content = "mapping = " Location = (3, 92) + Tooltip = "parameter mapping" } ] @@ -494,6 +527,7 @@ let q = query { for x in { 1 .. 10 } do select x } { Content = "projection = " Location = (1, 48) + Tooltip = "parameter projection" } ] @@ -532,6 +566,7 @@ let fullName = getFullName name lastName { Content = "surname = " Location = (5, 33) + Tooltip = "parameter surname" } ] @@ -557,6 +592,7 @@ None { Content = "mapping = " Location = (2, 15) + Tooltip = "parameter mapping" } ] diff --git a/vsintegration/tests/FSharp.Editor.Tests/Hints/InlineReturnTypeHintTests.fs b/vsintegration/tests/FSharp.Editor.Tests/Hints/InlineReturnTypeHintTests.fs index 2f419d749be..5aad963721a 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/Hints/InlineReturnTypeHintTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/Hints/InlineReturnTypeHintTests.fs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. module FSharp.Editor.Tests.Hints.InlineReturnTypeHintTests @@ -24,14 +24,41 @@ let setConsoleOut = System.Console.SetOut { Content = ": int " Location = (1, 13) + Tooltip = "type int" } { Content = ": int " Location = (2, 13) + Tooltip = "type int" } { Content = ": unit " Location = (3, 19) + Tooltip = "type unit" + } + ] + + Assert.Equal(expected, result) + +[] +let ``Hints are correct for user types`` () = + let code = + """ +type Answer = { Text: string } + +let getAnswer() = { Text = "42" } +""" + + let document = getFsDocument code + + let result = getReturnTypeHints document + + let expected = + [ + { + Content = ": Answer " + Location = (3, 17) + Tooltip = "type Answer" } ] @@ -54,6 +81,7 @@ type Test() = { Content = ": int " Location = (2, 24) + Tooltip = "type int" } ] @@ -72,6 +100,7 @@ let ``Hints are shown for generic functions`` () = { Content = ": int " Location = (0, 13) + Tooltip = "type int" } ] @@ -94,6 +123,7 @@ let ``Hints are shown for functions within expressions`` () = { Content = ": int " Location = (2, 21) + Tooltip = "type int" } ] diff --git a/vsintegration/tests/FSharp.Editor.Tests/Hints/InlineTypeHintTests.fs b/vsintegration/tests/FSharp.Editor.Tests/Hints/InlineTypeHintTests.fs index d995223fbd3..f669e8bb800 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/Hints/InlineTypeHintTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/Hints/InlineTypeHintTests.fs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. namespace FSharp.Editor.Tests.Hints @@ -24,6 +24,7 @@ let s = { Artist = "Moby"; Title = "Porcelain" } { Content = ": Song" Location = (3, 6) + Tooltip = "type Song" } ] @@ -31,6 +32,28 @@ let s = { Artist = "Moby"; Title = "Porcelain" } Assert.Equal(expected, actual) + [] + let ``Hints are correct for builtin types`` () = + let code = + """ +let songName = "Happy House" + """ + + let document = getFsDocument code + + let result = getTypeHints document + + let expected = + [ + { + Content = ": string" + Location = (1, 13) + Tooltip = "type string" + } + ] + + Assert.Equal(expected, result) + [] let ``Hint is shown for a parameter`` () = let code = @@ -47,6 +70,7 @@ let whoSings s = s.Artist { Content = ": Song" Location = (3, 15) + Tooltip = "type Song" } ] @@ -156,7 +180,15 @@ let iamboring() = """ let document = getFsDocument code - let expected = [ { Content = ": 'a"; Location = (2, 10) } ] + + let expected = + [ + { + Content = ": 'a" + Location = (2, 10) + Tooltip = "type 'a" + } + ] let actual = getTypeHints document @@ -175,10 +207,26 @@ let zip4 (l1: 'a list) (l2: 'b list) (l3: 'c list) (l4: 'd list) = let expected = [ - { Content = ": 'a"; Location = (3, 25) } - { Content = ": 'b"; Location = (3, 30) } - { Content = ": 'c"; Location = (3, 34) } - { Content = ": 'd"; Location = (3, 38) } + { + Content = ": 'a" + Location = (3, 25) + Tooltip = "type 'a" + } + { + Content = ": 'b" + Location = (3, 30) + Tooltip = "type 'b" + } + { + Content = ": 'c" + Location = (3, 34) + Tooltip = "type 'c" + } + { + Content = ": 'd" + Location = (3, 38) + Tooltip = "type 'd" + } ] let actual = getTypeHints document @@ -261,10 +309,12 @@ type Number<'T when IAddition<'T>>(value: 'T) = { Content = ": Number<'T>" Location = (7, 36) + Tooltip = "type Number`1" } { Content = ": Number<'T>" Location = (7, 39) + Tooltip = "type Number`1" } ] diff --git a/vsintegration/tests/FSharp.Editor.Tests/Hints/OverallHintExperienceTests.fs b/vsintegration/tests/FSharp.Editor.Tests/Hints/OverallHintExperienceTests.fs index c6c85d9decc..195902a7878 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/Hints/OverallHintExperienceTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/Hints/OverallHintExperienceTests.fs @@ -10,7 +10,7 @@ open FSharp.Test module OverallHintExperienceTests = [] - let ``Current baseline hints`` () = + let ``Baseline hints`` () = let code = """ type Song = { Artist: string; Title: string } @@ -39,55 +39,72 @@ let cc = a.Normal "hmm" { Content = ": Song" Location = (2, 18) + Tooltip = "type Song" } { Content = ": string " Location = (2, 19) + Tooltip = "type string" } { Content = "song = " Location = (4, 23) + Tooltip = "parameter song" } { Content = ": string" Location = (4, 11) + Tooltip = "type string" } { Content = "side = " Location = (10, 16) + Tooltip = "field side" } { Content = ": Shape" Location = (10, 6) + Tooltip = "type Shape" } { Content = "width = " Location = (11, 20) + Tooltip = "field width" } { Content = "height = " Location = (11, 23) + Tooltip = "field height" } { Content = ": Shape" Location = (11, 6) + Tooltip = "type Shape" } { Content = ": int " Location = (14, 36) + Tooltip = "type int" } { Content = "blahFirst = " Location = (16, 11) + Tooltip = "parameter blahFirst" + } + { + Content = ": C" + Location = (16, 6) + Tooltip = "type C" } - { Content = ": C"; Location = (16, 6) } { Content = "what = " Location = (17, 19) + Tooltip = "parameter what" } { Content = ": int" Location = (17, 7) + Tooltip = "type int" } ]