diff --git a/src/FsAutoComplete.Core/DocumentationFormatter.fs b/src/FsAutoComplete.Core/DocumentationFormatter.fs index e0f7b1be9..1090e28a0 100644 --- a/src/FsAutoComplete.Core/DocumentationFormatter.fs +++ b/src/FsAutoComplete.Core/DocumentationFormatter.fs @@ -641,11 +641,25 @@ module DocumentationFormatter = |> String.concat ($"{nl} | ")) let delegateTip () = - let invoker = - fse.MembersFunctionsAndValues |> Seq.find (fun f -> f.DisplayName = "Invoke") + let delegateSig = fse.FSharpDelegateSignature - let invokerSig = getFuncSignatureWithIdent displayContext invoker 6 - $" ={nl} delegate of{nl}{invokerSig}" + let args = + if delegateSig.DelegateArguments.Count = 0 then + "unit" + else + delegateSig.DelegateArguments + |> Seq.map (fun (name, ty) -> + let typeName = ty |> format displayContext |> fst + + match name with + | Some n when not (String.IsNullOrWhiteSpace n) -> + let n = FSharpKeywords.NormalizeIdentifierBackticks n + $"{n}: {typeName}" + | _ -> typeName) + |> String.concat " * " + + let retType = delegateSig.DelegateReturnType |> format displayContext |> fst + $" ={nl} delegate of {args} -> {retType}" let typeTip () = let constructors = diff --git a/src/FsAutoComplete.Core/SignatureFormatter.fs b/src/FsAutoComplete.Core/SignatureFormatter.fs index 05fead602..b3c5fdae8 100644 --- a/src/FsAutoComplete.Core/SignatureFormatter.fs +++ b/src/FsAutoComplete.Core/SignatureFormatter.fs @@ -707,11 +707,25 @@ module SignatureFormatter = |> String.concat $"{nl} | ") let delegateTip () = - let invoker = - fse.MembersFunctionsAndValues |> Seq.find (fun f -> f.DisplayName = "Invoke") + let delegateSig = fse.FSharpDelegateSignature - let invokerSig = getFuncSignatureWithIdent displayContext invoker 6 - $" ={nl} delegate of{nl}{invokerSig}" + let args = + if delegateSig.DelegateArguments.Count = 0 then + "unit" + else + delegateSig.DelegateArguments + |> Seq.map (fun (name, ty) -> + let typeName = formatFSharpType displayContext ty + + match name with + | Some n when not (String.IsNullOrWhiteSpace n) -> + let n = FSharpKeywords.NormalizeIdentifierBackticks n + $"{n}: {typeName}" + | _ -> typeName) + |> String.concat " * " + + let retType = formatFSharpType displayContext delegateSig.DelegateReturnType + $" ={nl} delegate of {args} -> {retType}" let typeTip () = let constructors = diff --git a/test/FsAutoComplete.Tests.Lsp/CoreTests.fs b/test/FsAutoComplete.Tests.Lsp/CoreTests.fs index ef164a40d..fd3581da7 100644 --- a/test/FsAutoComplete.Tests.Lsp/CoreTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CoreTests.fs @@ -610,10 +610,21 @@ let tooltipTests state = // Hover must show `string | null`, not just `string`. verifySignature 113u 4u "val nullableStringVal: string | null" + verifySignature 114u 4u (concatLines [ "val returnsNullable:"; " s: string"; " -> string | null" ]) + + // Delegate tooltip format — regression for issue #627. + // Verify that hovering on a delegate type definition shows a clean + // "delegate of -> " signature rather than broken output. + verifySignature 117u 5u (concatLines [ "type NoArgDelegate ="; " delegate of unit -> unit" ]) + + verifySignature 118u 5u (concatLines [ "type SingleArgDelegate ="; " delegate of arg: string -> int" ]) + verifySignature - 114u - 4u - (concatLines [ "val returnsNullable:"; " s: string"; " -> string | null" ]) ] ] + 119u + 5u + (concatLines [ "type MultiArgDelegate ="; " delegate of a: string * b: int -> bool" ]) + + verifySignature 120u 5u (concatLines [ "type UnannotatedDelegate ="; " delegate of string -> int" ]) ] ] let closeTests state = // Note: clear diagnostics also implies clear caches (-> remove file & project options from State). diff --git a/test/FsAutoComplete.Tests.Lsp/InfoPanelTests.fs b/test/FsAutoComplete.Tests.Lsp/InfoPanelTests.fs index 794efb1a8..e7040aa76 100644 --- a/test/FsAutoComplete.Tests.Lsp/InfoPanelTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/InfoPanelTests.fs @@ -60,4 +60,24 @@ let docFormattingTest state = | Result.Ok(Some(As(model: FsAutoComplete.CommandResponse.DocumentationDescription))) -> Expect.stringContains model.Signature "'T1 * 'T2 * 'T3" "Formatted doc contains 3 params separated by ( * )" | Result.Ok _ -> failtest "couldn't parse doc as the json type we expected" + }) + + testCaseAsync + "Delegate type shows clean 'delegate of -> ' signature (issue #627)" + (async { + let! (server, path) = server + + // Line 3 (0-indexed), char 5: hovering on "DelegateWithArgs" type name + let! doc = + server.FSharpDocumentation + { TextDocument = { Uri = path } + Position = { Character = 5u; Line = 3u } } + + match doc with + | Result.Error err -> failtest $"Doc error: {err.Message}" + | Result.Ok(Some(As(model: FsAutoComplete.CommandResponse.DocumentationDescription))) -> + Expect.stringContains model.Signature "delegate of" "Delegate signature should contain 'delegate of'" + Expect.stringContains model.Signature "name" "Delegate signature should contain parameter name 'name'" + Expect.stringContains model.Signature "count" "Delegate signature should contain parameter name 'count'" + | Result.Ok _ -> failtest "couldn't parse doc as the json type we expected" }) ] diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/FormattedDocumentation/Script.fsx b/test/FsAutoComplete.Tests.Lsp/TestCases/FormattedDocumentation/Script.fsx index 78b0856f7..4f827f848 100644 --- a/test/FsAutoComplete.Tests.Lsp/TestCases/FormattedDocumentation/Script.fsx +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/FormattedDocumentation/Script.fsx @@ -1,2 +1,4 @@ Map.map -List.unzip3 \ No newline at end of file +List.unzip3 +// Delegate type for issue #627 tooltip tests +type DelegateWithArgs = delegate of name: string * count: int -> bool \ No newline at end of file diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/Tooltips/Script.fsx b/test/FsAutoComplete.Tests.Lsp/TestCases/Tooltips/Script.fsx index 5506189ab..adb0bf384 100644 --- a/test/FsAutoComplete.Tests.Lsp/TestCases/Tooltips/Script.fsx +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/Tooltips/Script.fsx @@ -113,3 +113,9 @@ let functionAliasValue: FunctionAlias = fun _ -> 2 // Nullable reference types (F# 9 | null syntax) — issue #1352 let nullableStringVal: string | null = null let returnsNullable (s: string) : string | null = null + +// Delegate types — hover tooltip regression for issue #627 +type NoArgDelegate = delegate of unit -> unit +type SingleArgDelegate = delegate of arg: string -> int +type MultiArgDelegate = delegate of a: string * b: int -> bool +type UnannotatedDelegate = delegate of string -> int