diff --git a/src/Compiler/Service/FSharpCheckerResults.fs b/src/Compiler/Service/FSharpCheckerResults.fs index 0abff6d4d04..94b3794294b 100644 --- a/src/Compiler/Service/FSharpCheckerResults.fs +++ b/src/Compiler/Service/FSharpCheckerResults.fs @@ -242,6 +242,8 @@ type FSharpSymbolUse(denv: DisplayEnv, symbol: FSharpSymbol, inst: TyparInstanti member _.IsFromDispatchSlotImplementation = itemOcc = ItemOccurence.Implemented + member _.IsFromUse = itemOcc = ItemOccurence.Use + member _.IsFromComputationExpression = match symbol.Item, itemOcc with // 'seq' in 'seq { ... }' gets colored as keywords diff --git a/src/Compiler/Service/FSharpCheckerResults.fsi b/src/Compiler/Service/FSharpCheckerResults.fsi index 6d60a75cc5b..bbf14830f39 100644 --- a/src/Compiler/Service/FSharpCheckerResults.fsi +++ b/src/Compiler/Service/FSharpCheckerResults.fsi @@ -171,6 +171,9 @@ type public FSharpSymbolUse = /// Indicates if the reference is in open statement member IsFromOpenStatement: bool + /// Indicates if the reference is used for example at a call site + member IsFromUse: bool + /// The file name the reference occurs in member FileName: string diff --git a/tests/FSharp.Compiler.Service.Tests/FSharp.CompilerService.SurfaceArea.netstandard.expected b/tests/FSharp.Compiler.Service.Tests/FSharp.CompilerService.SurfaceArea.netstandard.expected index be3829e8faf..52360afde2a 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.CompilerService.SurfaceArea.netstandard.expected +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.CompilerService.SurfaceArea.netstandard.expected @@ -2163,6 +2163,7 @@ FSharp.Compiler.CodeAnalysis.FSharpSymbolUse: Boolean IsFromOpenStatement FSharp.Compiler.CodeAnalysis.FSharpSymbolUse: Boolean IsFromPattern FSharp.Compiler.CodeAnalysis.FSharpSymbolUse: Boolean IsFromType FSharp.Compiler.CodeAnalysis.FSharpSymbolUse: Boolean IsPrivateToFile +FSharp.Compiler.CodeAnalysis.FSharpSymbolUse: Boolean IsFromUse FSharp.Compiler.CodeAnalysis.FSharpSymbolUse: Boolean get_IsFromAttribute() FSharp.Compiler.CodeAnalysis.FSharpSymbolUse: Boolean get_IsFromComputationExpression() FSharp.Compiler.CodeAnalysis.FSharpSymbolUse: Boolean get_IsFromDefinition() @@ -2171,6 +2172,7 @@ FSharp.Compiler.CodeAnalysis.FSharpSymbolUse: Boolean get_IsFromOpenStatement() FSharp.Compiler.CodeAnalysis.FSharpSymbolUse: Boolean get_IsFromPattern() FSharp.Compiler.CodeAnalysis.FSharpSymbolUse: Boolean get_IsFromType() FSharp.Compiler.CodeAnalysis.FSharpSymbolUse: Boolean get_IsPrivateToFile() +FSharp.Compiler.CodeAnalysis.FSharpSymbolUse: Boolean get_IsFromUse() FSharp.Compiler.CodeAnalysis.FSharpSymbolUse: FSharp.Compiler.Symbols.FSharpDisplayContext DisplayContext FSharp.Compiler.CodeAnalysis.FSharpSymbolUse: FSharp.Compiler.Symbols.FSharpDisplayContext get_DisplayContext() FSharp.Compiler.CodeAnalysis.FSharpSymbolUse: FSharp.Compiler.Symbols.FSharpSymbol Symbol diff --git a/vsintegration/src/FSharp.Editor/Hints/HintService.fs b/vsintegration/src/FSharp.Editor/Hints/HintService.fs index c04ccef51fd..2ec037a04d3 100644 --- a/vsintegration/src/FSharp.Editor/Hints/HintService.fs +++ b/vsintegration/src/FSharp.Editor/Hints/HintService.fs @@ -19,9 +19,15 @@ module HintService = | :? FSharpMemberOrFunctionOrValue as symbol when hintKinds |> Set.contains HintKind.ParameterNameHint - && InlineParameterNameHints.isValidForHint symbol -> + && InlineParameterNameHints.isMemberOrFunctionOrValueValidForHint symbol -> - InlineParameterNameHints.getHints parseResults symbol symbolUse + InlineParameterNameHints.getHintsForMemberOrFunctionOrValue parseResults symbol symbolUse + + | :? FSharpUnionCase as symbol + when hintKinds |> Set.contains HintKind.ParameterNameHint + && InlineParameterNameHints.isUnionCaseValidForHint symbol symbolUse -> + + InlineParameterNameHints.getHintsForUnionCase parseResults symbol symbolUse // we'll be adding other stuff gradually here | _ -> diff --git a/vsintegration/src/FSharp.Editor/Hints/InlineParameterNameHints.fs b/vsintegration/src/FSharp.Editor/Hints/InlineParameterNameHints.fs index 4e4baa5687d..42ad4252e75 100644 --- a/vsintegration/src/FSharp.Editor/Hints/InlineParameterNameHints.fs +++ b/vsintegration/src/FSharp.Editor/Hints/InlineParameterNameHints.fs @@ -10,17 +10,27 @@ open Hints module InlineParameterNameHints = - let private getHint (range: range, parameter: FSharpParameter) = + let private getParameterHint (range: range, parameter: FSharpParameter) = { Kind = HintKind.ParameterNameHint Range = range.StartRange Parts = [ TaggedText(TextTag.Text, $"{parameter.DisplayName} = ") ] } + let private getFieldHint (range: range, field: FSharpField) = + { + Kind = HintKind.ParameterNameHint + Range = range.StartRange + Parts = [ TaggedText(TextTag.Text, $"{field.Name} = ") ] + } + let private doesParameterNameExist (parameter: FSharpParameter) = parameter.DisplayName <> "" - let isValidForHint (symbol: FSharpMemberOrFunctionOrValue) = + let private doesFieldNameExist (field: FSharpField) = + not field.IsNameGenerated + + let isMemberOrFunctionOrValueValidForHint (symbol: FSharpMemberOrFunctionOrValue) = // is there a better way? let isNotBuiltInOperator = symbol.DeclaringEntity @@ -29,7 +39,12 @@ module InlineParameterNameHints = symbol.IsFunction && isNotBuiltInOperator // arguably, hints for those would be rather useless - let getHints + let isUnionCaseValidForHint (symbol: FSharpUnionCase) (symbolUse: FSharpSymbolUse) = + // is the union case being used as a constructor and is it not Cons + symbolUse.IsFromUse + && symbol.DisplayName <> "(::)" + + let getHintsForMemberOrFunctionOrValue (parseResults: FSharpParseFileResults) (symbol: FSharpMemberOrFunctionOrValue) (symbolUse: FSharpSymbolUse) = @@ -42,8 +57,31 @@ module InlineParameterNameHints = parameters |> Seq.zip ranges |> Seq.where (snd >> doesParameterNameExist) - |> Seq.map getHint + |> Seq.map getParameterHint |> Seq.toList // this is the case at least for custom operators | None -> [] + + let getHintsForUnionCase + (parseResults: FSharpParseFileResults) + (symbol: FSharpUnionCase) + (symbolUse: FSharpSymbolUse) = + + let fields = Seq.toList symbol.Fields + + // If a case does not use field names, don't even bother getting applied argument ranges + if fields |> List.exists doesFieldNameExist |> not then + [] + else + let ranges = parseResults.GetAllArgumentsForFunctionApplicationAtPosition symbolUse.Range.Start + + // When not all field values are provided (as the user is typing), don't show anything yet + match ranges with + | Some ranges when ranges.Length = fields.Length -> + fields + |> List.zip ranges + |> List.where (snd >> doesFieldNameExist) + |> List.map getFieldHint + + | _ -> [] diff --git a/vsintegration/src/FSharp.Editor/Hints/RoslynAdapter.fs b/vsintegration/src/FSharp.Editor/Hints/RoslynAdapter.fs index cddf8d250f0..b6026d6da44 100644 --- a/vsintegration/src/FSharp.Editor/Hints/RoslynAdapter.fs +++ b/vsintegration/src/FSharp.Editor/Hints/RoslynAdapter.fs @@ -22,19 +22,22 @@ type internal RoslynAdapter interface IFSharpInlineHintsService with member _.GetInlineHintsAsync(document, _, cancellationToken) = async { - let! sourceText = document.GetTextAsync cancellationToken |> Async.AwaitTask let hintKinds = OptionParser.getHintKinds settings.Advanced - let! nativeHints = - HintService.getHintsForDocument - document - hintKinds - userOpName - cancellationToken - - let roslynHints = - nativeHints - |> Seq.map (NativeToRoslynHintConverter.convert sourceText) - - return roslynHints.ToImmutableArray() + if hintKinds.IsEmpty then + return ImmutableArray.Empty + else + let! sourceText = document.GetTextAsync cancellationToken |> Async.AwaitTask + let! nativeHints = + HintService.getHintsForDocument + document + hintKinds + userOpName + cancellationToken + + let roslynHints = + nativeHints + |> Seq.map (NativeToRoslynHintConverter.convert sourceText) + + return roslynHints.ToImmutableArray() } |> RoslynHelpers.StartAsyncAsTask cancellationToken diff --git a/vsintegration/tests/UnitTests/InlineParameterNameHintTests.fs b/vsintegration/tests/UnitTests/InlineParameterNameHintTests.fs index 9fbebcd1396..323c0fd88b8 100644 --- a/vsintegration/tests/UnitTests/InlineParameterNameHintTests.fs +++ b/vsintegration/tests/UnitTests/InlineParameterNameHintTests.fs @@ -159,17 +159,72 @@ let wrapped = WrappedThing 42 Assert.IsEmpty(result) [] -let ``Hints are not (yet) shown for dicrimanted unions`` () = +let ``Hints are shown for discriminated union case fields with explicit names`` () = let code = """ type Shape = - | Square of side : float - | Circle of radius : float + | Square of side: int + | Rectangle of width: int * height: int + +let a = Square 1 +let b = Rectangle (1, 2) +""" + let document = getFsDocument code + + let expected = [ + { Content = "side = "; Location = (5, 16) } + { Content = "width = "; Location = (6, 20) } + { Content = "height = "; Location = (6, 23) } + ] + + let actual = getParameterNameHints document + + Assert.AreEqual(expected, actual) -let circle = Circle 42 +[] +let ``Hints for discriminated union case fields are not shown when names are generated`` () = + let code = """ +type Shape = + | Triangle of side1: int * int * side3: int + | Circle of int + +let c = Triangle (1, 2, 3) +let d = Circle 1 """ let document = getFsDocument code - let result = getParameterNameHints document + let expected = [ + { Content = "side1 = "; Location = (5, 19) } + { Content = "side3 = "; Location = (5, 25) } + ] - Assert.IsEmpty(result) + let actual = getParameterNameHints document + + Assert.AreEqual(expected, actual) + +[] +let ``Hints for discriminated union case fields are not shown when provided arguments don't match the expected count`` () = + let code = """ +type Shape = + | Triangle of side1: int * side2: int * side3: int + | Circle of int + +let c = Triangle (1, 2) +""" + let document = getFsDocument code + + let actual = getParameterNameHints document + + Assert.IsEmpty(actual) + +[] +let ``Hints for discriminated union case fields are not shown for Cons`` () = + let code = """ +type X = + member _.Test() = 42 :: [42; 42] +""" + let document = getFsDocument code + + let actual = getParameterNameHints document + + Assert.IsEmpty(actual) diff --git a/vsintegration/tests/UnitTests/OverallHintExperienceTests.fs b/vsintegration/tests/UnitTests/OverallHintExperienceTests.fs index fb1dcbe8017..65d47a21029 100644 --- a/vsintegration/tests/UnitTests/OverallHintExperienceTests.fs +++ b/vsintegration/tests/UnitTests/OverallHintExperienceTests.fs @@ -15,12 +15,24 @@ type Song = { Artist: string; Title: string } let whoSings song = song.Artist let artist = whoSings { Artist = "Květy"; Title = "Je podzim" } + +type Shape = + | Square of side: int + | Rectangle of width: int * height: int + +let a = Square 1 +let b = Rectangle (1, 2) """ let document = getFsDocument code let expected = [ { Content = ": Song"; Location = (2, 18) } - { Content = ": string"; Location = (4, 11) } { Content = "song = "; Location = (4, 23) } + { Content = ": string"; Location = (4, 11) } + { Content = "side = "; Location = (10, 16) } + { Content = ": Shape"; Location = (10, 6) } + { Content = "width = "; Location = (11, 20) } + { Content = "height = "; Location = (11, 23) } + { Content = ": Shape"; Location = (11, 6) } ] let actual = getAllHints document