From 00037fa14f002bc5301163440be6f3bd067e1b87 Mon Sep 17 00:00:00 2001 From: ijklam <43789618+Tangent-90@users.noreply.github.com> Date: Sat, 21 Oct 2023 00:13:05 +0800 Subject: [PATCH] Change to ExternalAutocomplete functions (#1178) Co-authored-by: Chet Husk --- src/FsAutoComplete.Core/KeywordList.fs | 6 +- src/FsAutoComplete/LspHelpers.fs | 5 + src/FsAutoComplete/LspHelpers.fsi | 2 + .../LspServers/AdaptiveFSharpLspServer.fs | 68 +-- .../CompletionTests.fs | 398 ++++++++++++------ test/FsAutoComplete.Tests.Lsp/Helpers.fs | 1 + test/FsAutoComplete.Tests.Lsp/Program.fs | 1 + .../FullNameExternalAutocompleteTest.fsproj | 10 + .../Script.fs | 6 + .../Script.fsx | 6 + 10 files changed, 347 insertions(+), 156 deletions(-) create mode 100644 test/FsAutoComplete.Tests.Lsp/TestCases/FullNameExternalAutocompleteTest/FullNameExternalAutocompleteTest.fsproj create mode 100644 test/FsAutoComplete.Tests.Lsp/TestCases/FullNameExternalAutocompleteTest/Script.fs create mode 100644 test/FsAutoComplete.Tests.Lsp/TestCases/FullNameExternalAutocompleteTest/Script.fsx diff --git a/src/FsAutoComplete.Core/KeywordList.fs b/src/FsAutoComplete.Core/KeywordList.fs index e9e68f828..a86a18129 100644 --- a/src/FsAutoComplete.Core/KeywordList.fs +++ b/src/FsAutoComplete.Core/KeywordList.fs @@ -42,13 +42,16 @@ module KeywordList = let hashSymbolCompletionItems = hashDirectives |> Seq.map (fun kv -> + let label = "#" + kv.Key + { CompletionItem.Create(kv.Key) with + Data = Some(Newtonsoft.Json.Linq.JValue(label)) Kind = Some CompletionItemKind.Keyword InsertText = Some kv.Key FilterText = Some kv.Key SortText = Some kv.Key Documentation = Some(Documentation.String kv.Value) - Label = "#" + kv.Key }) + Label = label }) |> Seq.toArray let allKeywords: string list = @@ -58,6 +61,7 @@ module KeywordList = allKeywords |> List.mapi (fun id k -> { CompletionItem.Create(k) with + Data = Some(Newtonsoft.Json.Linq.JValue(k)) Kind = Some CompletionItemKind.Keyword InsertText = Some k SortText = Some(sprintf "1000000%d" id) diff --git a/src/FsAutoComplete/LspHelpers.fs b/src/FsAutoComplete/LspHelpers.fs index d83ebf57a..2f0e4308a 100644 --- a/src/FsAutoComplete/LspHelpers.fs +++ b/src/FsAutoComplete/LspHelpers.fs @@ -636,6 +636,7 @@ type FSharpConfigDto = ExcludeProjectDirectories: string[] option KeywordsAutocomplete: bool option ExternalAutocomplete: bool option + FullNameExternalAutocomplete: bool option Linter: bool option LinterConfig: string option IndentationSize: int option @@ -771,6 +772,7 @@ type FSharpConfig = ExcludeProjectDirectories: string[] KeywordsAutocomplete: bool ExternalAutocomplete: bool + FullNameExternalAutocomplete: bool Linter: bool LinterConfig: string option IndentationSize: int @@ -816,6 +818,7 @@ type FSharpConfig = ExcludeProjectDirectories = [||] KeywordsAutocomplete = false ExternalAutocomplete = false + FullNameExternalAutocomplete = false IndentationSize = 4 Linter = false LinterConfig = None @@ -861,6 +864,7 @@ type FSharpConfig = ExcludeProjectDirectories = defaultArg dto.ExcludeProjectDirectories [||] KeywordsAutocomplete = defaultArg dto.KeywordsAutocomplete false ExternalAutocomplete = defaultArg dto.ExternalAutocomplete false + FullNameExternalAutocomplete = defaultArg dto.ExternalAutocomplete false IndentationSize = defaultArg dto.IndentationSize 4 Linter = defaultArg dto.Linter false LinterConfig = dto.LinterConfig @@ -958,6 +962,7 @@ type FSharpConfig = ExcludeProjectDirectories = defaultArg dto.ExcludeProjectDirectories x.ExcludeProjectDirectories KeywordsAutocomplete = defaultArg dto.KeywordsAutocomplete x.KeywordsAutocomplete ExternalAutocomplete = defaultArg dto.ExternalAutocomplete x.ExternalAutocomplete + FullNameExternalAutocomplete = defaultArg dto.FullNameExternalAutocomplete x.FullNameExternalAutocomplete IndentationSize = defaultArg dto.IndentationSize x.IndentationSize Linter = defaultArg dto.Linter x.Linter LinterConfig = dto.LinterConfig diff --git a/src/FsAutoComplete/LspHelpers.fsi b/src/FsAutoComplete/LspHelpers.fsi index bd3d86ed3..94d8d84cd 100644 --- a/src/FsAutoComplete/LspHelpers.fsi +++ b/src/FsAutoComplete/LspHelpers.fsi @@ -269,6 +269,7 @@ type FSharpConfigDto = ExcludeProjectDirectories: string[] option KeywordsAutocomplete: bool option ExternalAutocomplete: bool option + FullNameExternalAutocomplete: bool option Linter: bool option LinterConfig: string option IndentationSize: int option @@ -363,6 +364,7 @@ type FSharpConfig = ExcludeProjectDirectories: string[] KeywordsAutocomplete: bool ExternalAutocomplete: bool + FullNameExternalAutocomplete: bool Linter: bool LinterConfig: string option IndentationSize: int diff --git a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs index 456eaacef..1ee6abf36 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs @@ -2575,6 +2575,33 @@ type AdaptiveFSharpLspServer getCompletions forceGetOpenFileTypeCheckResults | _ -> getCompletions forceGetOpenFileTypeCheckResultsStale + let getCodeToInsert (d: DeclarationListItem) = + match d.NamespaceToOpen with + | Some no when config.FullNameExternalAutocomplete -> sprintf "%s.%s" no d.NameInCode + | _ -> d.NameInCode + + let createCompletionItem (config: FSharpConfig) (id: int) (d: DeclarationListItem) = + let code = getCodeToInsert d + + /// The `label` for completion "System.Math.Ceiling" will be displayed as "Ceiling (System.Math)". This is to bias the viewer towards the member name, + /// with the namespace being less-important. The `filterText` is the text that will be used to filter the list of completions as the user types. + /// Prepending the member name to the filter text makes it so that the text the user is mot likely typing catches more relevant members at the head of the list. + /// e.f. "CeilingSystem.Math.Ceiling" means that the user typing `ceiling` will catch all of the members named ceiling that are in the available namespaces + let label, filterText = + match d.NamespaceToOpen with + | Some no when config.FullNameExternalAutocomplete -> + sprintf "%s (%s)" d.NameInList no, d.NameInList + code + | Some no -> sprintf "%s (open %s)" d.NameInList no, d.NameInList + | None -> d.NameInList, d.NameInList + + { CompletionItem.Create(d.NameInList) with + Data = Some(JValue(d.FullName)) + Kind = (AVal.force glyphToCompletionKind) d.Glyph + InsertText = Some code + SortText = Some(sprintf "%06d" id) + FilterText = Some filterText + Label = label } + match! retryAsyncOption (TimeSpan.FromMilliseconds(15.)) @@ -2592,37 +2619,14 @@ type AdaptiveFSharpLspServer transact (fun () -> HashMap.OfList( [ for d in decls do - d.NameInList, (d, pos, filePath, volatileFile.Source.GetLine, typeCheckResults.GetAST) ] + d.FullName, (d, pos, filePath, volatileFile.Source.GetLine, typeCheckResults.GetAST) ] ) |> autoCompleteItems.UpdateTo) |> ignore let includeKeywords = config.KeywordsAutocomplete && shouldKeywords - let items = - decls - |> Array.mapi (fun id d -> - let code = - if - System.Text.RegularExpressions.Regex.IsMatch(d.NameInList, """^[a-zA-Z][a-zA-Z0-9']+$""") - then - d.NameInList - elif d.NamespaceToOpen.IsSome then - d.NameInList - else - FSharpKeywords.NormalizeIdentifierBackticks d.NameInList - - let label = - match d.NamespaceToOpen with - | Some no -> sprintf "%s (open %s)" d.NameInList no - | None -> d.NameInList - - { CompletionItem.Create(d.NameInList) with - Kind = (AVal.force glyphToCompletionKind) d.Glyph - InsertText = Some code - SortText = Some(sprintf "%06d" id) - FilterText = Some d.NameInList - Label = label }) + let items = decls |> Array.mapi (createCompletionItem config) let its = if not includeKeywords then @@ -2650,6 +2654,8 @@ type AdaptiveFSharpLspServer } override __.CompletionItemResolve(ci: CompletionItem) = + let config = AVal.force config + let mapHelpText (ci: CompletionItem) (text: HelpText) = match text with | HelpText.Simple(symbolName, text) -> @@ -2708,8 +2714,8 @@ type AdaptiveFSharpLspServer let n = match getAutoCompleteNamespacesByDeclName sym |> AVal.force with - | None -> None - | Some s -> Some s + | Some s when not config.FullNameExternalAutocomplete -> Some s + | _ -> None CoreResponse.Res(HelpText.Full(sym, tip, n)) @@ -2725,10 +2731,10 @@ type AdaptiveFSharpLspServer ) return! - match ci.InsertText with - | None -> LspResult.internalError "No InsertText" - | Some insertText -> - helpText insertText + match ci.Data with + | None -> LspResult.internalError "No FullName" + | Some fullName -> + helpText (fullName.ToString()) |> Result.ofCoreResponse |> Result.bimap (function diff --git a/test/FsAutoComplete.Tests.Lsp/CompletionTests.fs b/test/FsAutoComplete.Tests.Lsp/CompletionTests.fs index eb34d6758..a4928143d 100644 --- a/test/FsAutoComplete.Tests.Lsp/CompletionTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CompletionTests.fs @@ -47,7 +47,7 @@ let tests state = let! response = server.TextDocumentCompletion completionParams match response with - | Ok (Some completions) -> + | Ok(Some completions) -> Expect.isLessThan completions.Items.Length 100 @@ -73,23 +73,25 @@ let tests state = let line = 15 let character = lineUnderTest.Length - let textChange : DidChangeTextDocumentParams = - { - TextDocument = { Uri = Path.FilePathToUri path ; Version = 1} - ContentChanges = [| - { - Range = Some { Start = { Line = line; Character = character }; End = { Line = line; Character = character } } - RangeLength = Some 0 - Text = "." - } - |] - } + let textChange: DidChangeTextDocumentParams = + { TextDocument = + { Uri = Path.FilePathToUri path + Version = 1 } + ContentChanges = + [| { Range = + Some + { Start = { Line = line; Character = character } + End = { Line = line; Character = character } } + RangeLength = Some 0 + Text = "." } |] } let! c = server.TextDocumentDidChange textChange |> Async.StartChild let completionParams: CompletionParams = { TextDocument = { Uri = Path.FilePathToUri path } - Position = { Line = line; Character = character + 1 } + Position = + { Line = line + Character = character + 1 } Context = Some { triggerKind = CompletionTriggerKind.TriggerCharacter @@ -99,7 +101,7 @@ let tests state = do! c match response with - | Ok (Some completions) -> + | Ok(Some completions) -> Expect.isLessThan completions.Items.Length 100 @@ -135,7 +137,7 @@ let tests state = let! response = server.TextDocumentCompletion completionParams match response with - | Ok (Some completions) -> + | Ok(Some completions) -> Expect.isLessThan completions.Items.Length 100 @@ -160,23 +162,25 @@ let tests state = let lineUnderTest = "\"bareString\"" let character = lineUnderTest.Length - let textChange : DidChangeTextDocumentParams = - { - TextDocument = { Uri = Path.FilePathToUri path ; Version = 1} - ContentChanges = [| - { - Range = Some { Start = { Line = line; Character = character }; End = { Line = line; Character = character } } - RangeLength = Some 0 - Text = "." - } - |] - } + let textChange: DidChangeTextDocumentParams = + { TextDocument = + { Uri = Path.FilePathToUri path + Version = 1 } + ContentChanges = + [| { Range = + Some + { Start = { Line = line; Character = character } + End = { Line = line; Character = character } } + RangeLength = Some 0 + Text = "." } |] } let! c = server.TextDocumentDidChange textChange |> Async.StartChild let completionParams: CompletionParams = { TextDocument = { Uri = Path.FilePathToUri path } - Position = { Line = line; Character = character + 1 } + Position = + { Line = line + Character = character + 1 } Context = Some { triggerKind = CompletionTriggerKind.TriggerCharacter @@ -186,7 +190,7 @@ let tests state = do! c match response with - | Ok (Some completions) -> + | Ok(Some completions) -> Expect.isLessThan completions.Items.Length 100 @@ -222,7 +226,7 @@ let tests state = let! response = server.TextDocumentCompletion completionParams match response with - | Ok (Some completions) -> + | Ok(Some completions) -> Expect.isLessThan completions.Items.Length 100 @@ -247,23 +251,25 @@ let tests state = let lineUnderTest = "[1;2;3]" let character = lineUnderTest.Length - let textChange : DidChangeTextDocumentParams = - { - TextDocument = { Uri = Path.FilePathToUri path ; Version = 1} - ContentChanges = [| - { - Range = Some { Start = { Line = line; Character = character }; End = { Line = line; Character = character } } - RangeLength = Some 0 - Text = "." - } - |] - } + let textChange: DidChangeTextDocumentParams = + { TextDocument = + { Uri = Path.FilePathToUri path + Version = 1 } + ContentChanges = + [| { Range = + Some + { Start = { Line = line; Character = character } + End = { Line = line; Character = character } } + RangeLength = Some 0 + Text = "." } |] } let! c = server.TextDocumentDidChange textChange |> Async.StartChild let completionParams: CompletionParams = { TextDocument = { Uri = Path.FilePathToUri path } - Position = { Line = line; Character = character + 1 } + Position = + { Line = line + Character = character + 1 } Context = Some { triggerKind = CompletionTriggerKind.TriggerCharacter @@ -273,7 +279,7 @@ let tests state = do! c match response with - | Ok (Some completions) -> + | Ok(Some completions) -> Expect.isLessThan completions.Items.Length 100 @@ -309,7 +315,7 @@ let tests state = let! response = server.TextDocumentCompletion completionParams match response with - | Ok (Some completions) -> + | Ok(Some completions) -> Expect.isLessThan completions.Items.Length 100 @@ -341,7 +347,7 @@ let tests state = let! response = server.TextDocumentCompletion completionParams match response with - | Ok (Some completions) -> + | Ok(Some completions) -> Expect.equal completions.Items.Length 106 "at time of writing the List module has 106 exposed members" let firstItem = completions.Items.[0] @@ -369,7 +375,7 @@ let tests state = let! response = server.TextDocumentCompletion completionParams match response with - | Ok (Some completions) -> + | Ok(Some completions) -> Expect.equal completions.Items.Length 106 "at time of writing the List module has 106 exposed members" let firstItem = completions.Items.[0] @@ -396,67 +402,84 @@ let tests state = let! response = server.TextDocumentCompletion completionParams match response with - | Ok (Some completions) -> + | Ok(Some completions) -> Expect.isLessThan completions.Items.Length 300 "shouldn't have a very long list of completion items that are only types" + Expect.isGreaterThan completions.Items.Length 100 "should have a reasonable number of completion items that are only types" - Expect.exists - completions.Items - (fun item -> item.Label = "list") - "completion should contain the list type" + Expect.exists completions.Items (fun item -> item.Label = "list") "completion should contain the list type" | Ok None -> failtest "Should have gotten some completion items" | Error e -> failtestf "Got an error while retrieving completions: %A" e }) - testCaseAsync "completion before first character of expression" (async { - let! server, path = server - let completionParams : CompletionParams = - { - TextDocument = { Uri = Path.FilePathToUri path } - Position = { Line = 8; Character = 12 } // after the 'L' in 'List.' - Context = Some { triggerKind = CompletionTriggerKind.Invoked; triggerCharacter = None } - } - let! response = server.TextDocumentCompletion completionParams - match response with - | Ok (Some completions) -> - Expect.isGreaterThan completions.Items.Length 100 "should have a very long list of all symbols" - let firstItem = completions.Items.[0] - Expect.equal firstItem.Label "async" "first member should be async, alphabetically first in the full symbol list" - | Ok None -> - failtest "Should have gotten some completion items" - | Error e -> - failtestf "Got an error while retrieving completions: %A" e - }) + testCaseAsync + "completion before first character of expression" + (async { + let! server, path = server - testCaseAsync "completion after first character of expression" (async { - let! server, path = server - let completionParams : CompletionParams = - { - TextDocument = { Uri = Path.FilePathToUri path } - Position = { Line = 8; Character = 11 } // before the 'L' in 'List.' - Context = Some { triggerKind = CompletionTriggerKind.Invoked; triggerCharacter = None } - } - let! response = server.TextDocumentCompletion completionParams - match response with - | Ok (Some completions) -> - Expect.isGreaterThan completions.Items.Length 100 "should have a very long list of all symbols" - let firstItem = completions.Items.[0] - Expect.equal firstItem.Label "async" "first member should be async, alphabetically first in the full symbol list" - | Ok None -> - failtest "Should have gotten some completion items" - | Error e -> - failtestf "Got an error while retrieving completions: %A" e - }) + let completionParams: CompletionParams = + { TextDocument = { Uri = Path.FilePathToUri path } + Position = { Line = 8; Character = 12 } // after the 'L' in 'List.' + Context = + Some + { triggerKind = CompletionTriggerKind.Invoked + triggerCharacter = None } } + + let! response = server.TextDocumentCompletion completionParams + + match response with + | Ok(Some completions) -> + Expect.isGreaterThan completions.Items.Length 100 "should have a very long list of all symbols" + let firstItem = completions.Items.[0] + + Expect.equal + firstItem.Label + "async" + "first member should be async, alphabetically first in the full symbol list" + | Ok None -> failtest "Should have gotten some completion items" + | Error e -> failtestf "Got an error while retrieving completions: %A" e + }) + + testCaseAsync + "completion after first character of expression" + (async { + let! server, path = server + + let completionParams: CompletionParams = + { TextDocument = { Uri = Path.FilePathToUri path } + Position = { Line = 8; Character = 11 } // before the 'L' in 'List.' + Context = + Some + { triggerKind = CompletionTriggerKind.Invoked + triggerCharacter = None } } + + let! response = server.TextDocumentCompletion completionParams + + match response with + | Ok(Some completions) -> + Expect.isGreaterThan completions.Items.Length 100 "should have a very long list of all symbols" + let firstItem = completions.Items.[0] - testCaseAsync "no backticks in completionitem signatures" (asyncResult { - let! server, path = server - let completionParams: CompletionParams = + Expect.equal + firstItem.Label + "async" + "first member should be async, alphabetically first in the full symbol list" + | Ok None -> failtest "Should have gotten some completion items" + | Error e -> failtestf "Got an error while retrieving completions: %A" e + }) + + testCaseAsync + "no backticks in completionitem signatures" + (asyncResult { + let! server, path = server + + let completionParams: CompletionParams = { TextDocument = { Uri = Path.FilePathToUri path } Position = { Line = 3; Character = 9 } // the '.' in 'Async.' Context = @@ -464,13 +487,22 @@ let tests state = { triggerKind = CompletionTriggerKind.TriggerCharacter triggerCharacter = Some '.' } } - let! response = server.TextDocumentCompletion completionParams |> AsyncResult.map Option.get - let ctokMember = response.Items[0] - let! resolved = server.CompletionItemResolve(ctokMember) - Expect.equal resolved.Label "CancellationToken" "Just making sure we're on the right member, one that should have backticks" - Expect.equal resolved.Detail (Some "property Async.CancellationToken: Async with get") "Signature shouldn't have backticks" + let! response = server.TextDocumentCompletion completionParams |> AsyncResult.map Option.get + let ctokMember = response.Items[0] + let! resolved = server.CompletionItemResolve(ctokMember) - } |> AsyncResult.bimap id (fun e -> failwithf "%O" e)) + Expect.equal + resolved.Label + "CancellationToken" + "Just making sure we're on the right member, one that should have backticks" + + Expect.equal + resolved.Detail + (Some "property Async.CancellationToken: Async with get") + "Signature shouldn't have backticks" + + } + |> AsyncResult.bimap id (fun e -> failwithf "%O" e)) testCaseAsync "completion in interpolated string" @@ -488,7 +520,7 @@ let tests state = let! response = server.TextDocumentCompletion completionParams match response with - | Ok (Some completions) -> + | Ok(Some completions) -> Expect.equal completions.Items.Length 106 "at time of writing the List module has 106 exposed members" let firstItem = completions.Items.[0] @@ -516,7 +548,7 @@ let tests state = let! response = server.TextDocumentCompletion completionParams match response with - | Ok (Some completions) -> + | Ok(Some completions) -> Expect.equal completions.Items.Length 106 "at time of writing the List module has 106 exposed members" let firstItem = completions.Items.[0] @@ -526,7 +558,7 @@ let tests state = "first member should be List.Empty, since properties are preferred over functions" | Ok None -> failtest "Should have gotten some completion items" | Error e -> failtestf "Got an error while retrieving completions: %A" e - })] + }) ] ///Tests for getting autocomplete let autocompleteTest state = @@ -572,7 +604,7 @@ let autocompleteTest state = match res with | Result.Error e -> failtestf "Request failed: %A" e | Result.Ok None -> failtest "Request none" - | Result.Ok (Some res) -> + | Result.Ok(Some res) -> Expect.equal res.Items.Length 2 "Autocomplete has all symbols" Expect.exists res.Items (fun n -> n.Label = "func") "Autocomplete contains given symbol" @@ -594,7 +626,7 @@ let autocompleteTest state = match res with | Result.Error e -> failtestf "Request failed: %A" e | Result.Ok None -> failtest "Request none" - | Result.Ok (Some res) -> + | Result.Ok(Some res) -> //TODO // Expect.equal res.Items.Length 1 "Autocomplete has all symbols" Expect.exists res.Items (fun n -> n.Label = "System") "Autocomplete contains given symbol" @@ -616,7 +648,7 @@ let autocompleteTest state = match res with | Result.Error e -> failtestf "Request failed: %A" e | Result.Ok None -> failtest "Request none" - | Result.Ok (Some res) -> + | Result.Ok(Some res) -> //TODO // Expect.equal res.Items.Length 1 "Autocomplete has all symbols" Expect.exists res.Items (fun n -> n.Label = "DateTime") "Autocomplete contains given symbol" @@ -638,7 +670,7 @@ let autocompleteTest state = match res with | Result.Error e -> failtestf "Request failed: %A" e | Result.Ok None -> failtest "Request none" - | Result.Ok (Some res) -> + | Result.Ok(Some res) -> Expect.equal res.Items.Length 1 "Autocomplete has all symbols" Expect.exists res.Items (fun n -> n.Label = "z") "Autocomplete contains given symbol" @@ -659,7 +691,7 @@ let autocompleteTest state = match res with | Result.Error e -> failtestf "Request failed: %A" e | Result.Ok None -> failtest "Request none" - | Result.Ok (Some res) -> + | Result.Ok(Some res) -> Expect.exists res.Items (fun n -> n.Label = "bar") "Autocomplete contains given symbol" Expect.exists res.Items (fun n -> n.Label = "baz") "Autocomplete contains given symbol" }) @@ -679,7 +711,7 @@ let autocompleteTest state = match res with | Result.Error e -> failtestf "Request failed: %A" e | Result.Ok None -> failtest "Request none" - | Result.Ok (Some res) -> + | Result.Ok(Some res) -> Expect.exists res.Items (fun n -> n.Label = "Bar") "Autocomplete contains given symbol" Expect.exists res.Items (fun n -> n.Label = "Baz") "Autocomplete contains given symbol" }) ] @@ -722,9 +754,7 @@ let autoOpenTests state = let text = edit.NewText let pos = edit.Range.Start - let indentation = - pos.Character - + (text.Length - text.TrimStart().Length) + let indentation = pos.Character + (text.Length - text.TrimStart().Length) { Line = pos.Line Character = indentation } @@ -749,21 +779,17 @@ let autoOpenTests state = Only = None TriggerKind = None } } - let (|ContainsOpenAction|_|) (codeActions: CodeAction []) = + let (|ContainsOpenAction|_|) (codeActions: CodeAction[]) = codeActions - |> Array.tryFind (fun ca -> - ca.Kind = Some "quickfix" - && ca.Title.StartsWith "open ") + |> Array.tryFind (fun ca -> ca.Kind = Some "quickfix" && ca.Title.StartsWith "open ") match! server.TextDocumentCodeAction p with | Error e -> return failtestf "Quick fix Request failed: %A" e | Ok None -> return failtest "Quick fix Request none" - | Ok (Some (CodeActions (ContainsOpenAction quickfix))) -> + | Ok(Some(CodeActions(ContainsOpenAction quickfix))) -> let ns = quickfix.Title.Substring("open ".Length) - let edit = - quickfix.Edit.Value.DocumentChanges.Value.[0] - .Edits.[0] + let edit = quickfix.Edit.Value.DocumentChanges.Value.[0].Edits.[0] let openPos = calcOpenPos edit return (edit, ns, openPos) @@ -789,11 +815,7 @@ let autoOpenTests state = (expectedOpen.Line) (expectedOpen.Character)) - let runner = - if pending then - ptestCaseAsync - else - testCaseAsync + let runner = if pending then ptestCaseAsync else testCaseAsync runner name <| async { @@ -808,10 +830,14 @@ let autoOpenTests state = match! server.TextDocumentCompletion p with | Error e -> failtestf "Request failed: %A" e | Ok None -> failtest "Request none" - | Ok (Some res) -> + | Ok(Some res) -> Expect.isFalse res.IsIncomplete "Result is incomplete" let ci = res.Items |> Array.tryFind (fun c -> c.Label = word) - if ci = None then failwithf $"Couldn't find completion item for `{word}` among the items %A{res.Items |> Array.map (fun i -> i.Label)}" |> ignore + + if ci = None then + failwithf + $"Couldn't find completion item for `{word}` among the items %A{res.Items |> Array.map (fun i -> i.Label)}" + |> ignore // now get details: `completionItem/resolve` (previous request was `textDocument/completion` -> List of all completions, but without details) match! server.CompletionItemResolve ci.Value with @@ -938,3 +964,127 @@ let autoOpenTests state = "with implicit top level module with open and new lines" "ImplicitTopLevelModuleWithOpenAndNewLines.fsx" testScript "with root module with comments and new line before open" "ModuleDocsAndNewLineBeforeOpen.fsx" ] + +let fullNameExternalAutocompleteTest state = + let server = + async { + let config = + { defaultConfigDto with + ExternalAutocomplete = Some true + FullNameExternalAutocomplete = Some true } + + let path = + Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "FullNameExternalAutocompleteTest") + + let! (server, event) = serverInitialize path config state + let projectPath = Path.Combine(path, "FullNameExternalAutocompleteTest.fsproj") + do! waitForWorkspaceFinishedParsing event + do! parseProject projectPath server + let path = Path.Combine(path, "Script.fs") + let tdop: DidOpenTextDocumentParams = { TextDocument = loadDocument path } + do! server.TextDocumentDidOpen tdop + return (server, path) + } + |> Async.Cache + + let scriptServer = + async { + let config = + { defaultConfigDto with + ExternalAutocomplete = Some true + FullNameExternalAutocomplete = Some true } + + let path = + Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "FullNameExternalAutocompleteTest") + + let! (server, event) = serverInitialize path config state + do! waitForWorkspaceFinishedParsing event + let path = Path.Combine(path, "Script.fsx") + let tdop: DidOpenTextDocumentParams = { TextDocument = loadDocument path } + do! server.TextDocumentDidOpen tdop + return (server, path) + } + |> Async.Cache + + let makeAutocompleteTest (serverConfig: (IFSharpLspServer * string) Async) testName (line, character) expects = + testCaseAsync + testName + (async { + let! server, path = serverConfig + + let p: CompletionParams = + { TextDocument = { Uri = Path.FilePathToUri path } + Position = { Line = line; Character = character } + Context = + Some + { triggerKind = CompletionTriggerKind.Invoked + triggerCharacter = None } } + + let! res = server.TextDocumentCompletion p + + match res with + | Result.Error e -> failtestf "Request failed: %A" e + | Result.Ok None -> failtest "Request none" + | Result.Ok(Some res) -> expects res + }) + + let makeAutocompleteTestList (serverConfig: (IFSharpLspServer * string) Async) = + [ makeAutocompleteTest serverConfig "Autocomplete for Array.map contains no backticks" (0, 8) (fun res -> + let n = res.Items |> Array.tryFind (fun i -> i.Label = "Array.map") + Expect.isSome n "Completion doesn't exist" + Expect.equal n.Value.InsertText (Some "Array.map") "Autocomplete for Array.map contains backticks") + + makeAutocompleteTest serverConfig "Autocomplete for ``a.b`` contains backticks" (2, 1) (fun res -> + let n = res.Items |> Array.tryFind (fun i -> i.Label = "a.b") + Expect.isSome n "Completion doesn't exist" + Expect.equal n.Value.InsertText (Some "``a.b``") "Autocomplete for a.b contains no backticks") + + testCaseAsync + "Autocompletes with same label have different description" + (asyncResult { + let! server, path = serverConfig + + let p: CompletionParams = + { TextDocument = { Uri = Path.FilePathToUri path } + Position = { Line = 3; Character = 4 } + Context = + Some + { triggerKind = CompletionTriggerKind.Invoked + triggerCharacter = None } } + + let! response = server.TextDocumentCompletion p |> AsyncResult.map Option.get + + let! items = + response.Items + |> Array.filter (fun i -> i.Label = "bind") + |> Array.map (server.CompletionItemResolve) + |> Async.Parallel + + let count = + items + |> Array.distinctBy (function Ok x -> x.Detail | Error _ -> None) + |> Array.length + + Expect.equal count items.Length "These completions doesn't have different description" + } + |> AsyncResult.bimap id (fun e -> failwithf "%O" e)) + + makeAutocompleteTest + serverConfig + "Check Autocomplete for System.Text.RegularExpressions.Regex" + (4, 5) + (fun res -> + let n = res.Items |> Array.tryFind (fun i -> i.Label = "Regex (System.Text.RegularExpressions)") + Expect.isSome n "Completion doesn't exist" + Expect.equal n.Value.InsertText (Some "System.Text.RegularExpressions.Regex") "Autocomplete for Regex is not System.Text.RegularExpressions.Regex" + Expect.equal n.Value.FilterText (Some "RegexSystem.Text.RegularExpressions.Regex") "Autocomplete for Regex is not System.Text.RegularExpressions.Regex") + + makeAutocompleteTest serverConfig "Autocomplete for Result is just Result" (5, 6) (fun res -> + let n = res.Items |> Array.tryFind (fun i -> i.Label = "Result") + Expect.isSome n "Completion doesn't exist" + Expect.equal n.Value.InsertText (Some "Result") "Autocomplete contains given symbol") ] + + testList + "fullNameExternalAutocompleteTest Tests" + [ testList "fullNameExternalAutocompleteTest within project files" (makeAutocompleteTestList server) + testList "fullNameExternalAutocompleteTest within script files" (makeAutocompleteTestList scriptServer) ] diff --git a/test/FsAutoComplete.Tests.Lsp/Helpers.fs b/test/FsAutoComplete.Tests.Lsp/Helpers.fs index 03d099bd5..a6da8adbd 100644 --- a/test/FsAutoComplete.Tests.Lsp/Helpers.fs +++ b/test/FsAutoComplete.Tests.Lsp/Helpers.fs @@ -212,6 +212,7 @@ let defaultConfigDto: FSharpConfigDto = ExcludeProjectDirectories = None KeywordsAutocomplete = None ExternalAutocomplete = None + FullNameExternalAutocomplete = None Linter = None LinterConfig = None IndentationSize = None diff --git a/test/FsAutoComplete.Tests.Lsp/Program.fs b/test/FsAutoComplete.Tests.Lsp/Program.fs index b62faab13..437c073ad 100644 --- a/test/FsAutoComplete.Tests.Lsp/Program.fs +++ b/test/FsAutoComplete.Tests.Lsp/Program.fs @@ -77,6 +77,7 @@ let lspTests = documentSymbolTest createServer Completion.autocompleteTest createServer Completion.autoOpenTests createServer + Completion.fullNameExternalAutocompleteTest createServer foldingTests createServer tooltipTests createServer Highlighting.tests createServer diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/FullNameExternalAutocompleteTest/FullNameExternalAutocompleteTest.fsproj b/test/FsAutoComplete.Tests.Lsp/TestCases/FullNameExternalAutocompleteTest/FullNameExternalAutocompleteTest.fsproj new file mode 100644 index 000000000..3ba0e00ab --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/FullNameExternalAutocompleteTest/FullNameExternalAutocompleteTest.fsproj @@ -0,0 +1,10 @@ + + + + net6.0 + false + + + + + diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/FullNameExternalAutocompleteTest/Script.fs b/test/FsAutoComplete.Tests.Lsp/TestCases/FullNameExternalAutocompleteTest/Script.fs new file mode 100644 index 000000000..44786ba2d --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/FullNameExternalAutocompleteTest/Script.fs @@ -0,0 +1,6 @@ +arraymap +let ``a.b`` = 1 +a +bind +Regex +Result diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/FullNameExternalAutocompleteTest/Script.fsx b/test/FsAutoComplete.Tests.Lsp/TestCases/FullNameExternalAutocompleteTest/Script.fsx new file mode 100644 index 000000000..44786ba2d --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/FullNameExternalAutocompleteTest/Script.fsx @@ -0,0 +1,6 @@ +arraymap +let ``a.b`` = 1 +a +bind +Regex +Result