diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 40d01e7cf51..14ba96ada9f 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -21,12 +21,12 @@ jobs: - name: Restore the compiler service solution env: CI: false - run: ./build.sh -c Release --verbosity quiet + run: ./build.sh -c Release --verbosity quiet || true - name: Restore the language server solution env: CI: false - run: dotnet build ./LSPSolutionSlim.sln -c Release --verbosity quiet + run: dotnet build ./LSPSolutionSlim.sln -c Release --verbosity quiet || true - name: Restore dotnet tools env: CI: false - run: dotnet tool restore + run: dotnet tool restore || true diff --git a/eng/build.sh b/eng/build.sh index 07c3889f33d..9fa3b2959c6 100755 --- a/eng/build.sh +++ b/eng/build.sh @@ -356,6 +356,7 @@ if [[ "$test_core_clr" == true ]]; then Test --testproject "$repo_root/tests/FSharp.Compiler.Private.Scripting.UnitTests/FSharp.Compiler.Private.Scripting.UnitTests.fsproj" --targetframework $coreclrtestframework Test --testproject "$repo_root/tests/FSharp.Build.UnitTests/FSharp.Build.UnitTests.fsproj" --targetframework $coreclrtestframework Test --testproject "$repo_root/tests/FSharp.Core.UnitTests/FSharp.Core.UnitTests.fsproj" --targetframework $coreclrtestframework + Test --testproject "$repo_root/tests/FSharp.Compiler.LanguageServer.Tests/FSharp.Compiler.LanguageServer.Tests.fsproj" --targetframework $coreclrtestframework fi if [[ "$test_compilercomponent_tests" == true ]]; then diff --git a/src/FSharp.Compiler.LanguageServer/Common/CapabilitiesManager.fs b/src/FSharp.Compiler.LanguageServer/Common/CapabilitiesManager.fs index 556a1d96edc..d2beca721a6 100644 --- a/src/FSharp.Compiler.LanguageServer/Common/CapabilitiesManager.fs +++ b/src/FSharp.Compiler.LanguageServer/Common/CapabilitiesManager.fs @@ -33,6 +33,10 @@ type CapabilitiesManager(config: FSharpLanguageServerConfig, scOverrides: IServe WorkspaceDiagnostics = true )), //CompletionProvider = CompletionOptions(TriggerCharacters = [| "."; " " |], ResolveProvider = true, WorkDoneProgress = true), + CompletionProvider = + addIf + config.EnabledFeatures.Completion + (CompletionOptions(TriggerCharacters = [| "." |], ResolveProvider = false, WorkDoneProgress = false)), //HoverProvider = SumType(HoverOptions(WorkDoneProgress = true)) SemanticTokensOptions = addIf diff --git a/src/FSharp.Compiler.LanguageServer/FSharpLanguageServerConfig.fs b/src/FSharp.Compiler.LanguageServer/FSharpLanguageServerConfig.fs index a1acd2b88da..33e89b43e37 100644 --- a/src/FSharp.Compiler.LanguageServer/FSharpLanguageServerConfig.fs +++ b/src/FSharp.Compiler.LanguageServer/FSharpLanguageServerConfig.fs @@ -4,12 +4,14 @@ type FSharpLanguageServerFeatures = { Diagnostics: bool SemanticHighlighting: bool + Completion: bool } static member Default = { Diagnostics = true SemanticHighlighting = true + Completion = true } type FSharpLanguageServerConfig = diff --git a/src/FSharp.Compiler.LanguageServer/Handlers/LanguageFeaturesHandler.fs b/src/FSharp.Compiler.LanguageServer/Handlers/LanguageFeaturesHandler.fs index 474758924d0..74bacf44398 100644 --- a/src/FSharp.Compiler.LanguageServer/Handlers/LanguageFeaturesHandler.fs +++ b/src/FSharp.Compiler.LanguageServer/Handlers/LanguageFeaturesHandler.fs @@ -10,6 +10,12 @@ open System.Threading open System.Collections.Generic open Microsoft.VisualStudio.FSharp.Editor +open FSharp.Compiler.EditorServices +open FSharp.Compiler.Syntax +open FSharp.Compiler.Text +open System +open FSharp.Compiler.Tokenization + #nowarn "57" type LanguageFeaturesHandler() = @@ -57,3 +63,110 @@ type LanguageFeaturesHandler() = return SemanticTokens(Data = tokens) } |> CancellableTask.start cancellationToken + + interface IRequestHandler with + [] + member _.HandleRequestAsync(request: CompletionParams, context: FSharpRequestContext, cancellationToken: CancellationToken) = + cancellableTask { + let file = request.TextDocument.Uri + let position = request.Position + + let! source = context.Workspace.Query.GetSource(file) + let! parseAndCheckResults = context.Workspace.Query.GetParseAndCheckResultsForFile(file) + + match source, parseAndCheckResults with + | Some source, (Some parseResults, Some checkFileResults) -> + try + // Convert LSP position to F# position + let fcsPosition = Position.mkPos (int position.Line + 1) (int position.Character) + + // Get the line text at cursor position + let lineText = + if position.Line < source.GetLineCount() then + source.GetLineString(position.Line).TrimEnd([| '\r'; '\n' |]) + else + "" + + // Get partial name for completion + let partialName = + QuickParse.GetPartialLongNameEx(lineText, int position.Character - 1) + + // Get completion context + let completionContext = + ParsedInput.TryGetCompletionContext(fcsPosition, parseResults.ParseTree, lineText) + + // Get declaration list from compiler services + let declarations = + checkFileResults.GetDeclarationListInfo( + Some(parseResults), + Line.fromZ position.Line, + lineText, + partialName, + (fun _ -> []), // getAllSymbols - simplified for now + (fcsPosition, completionContext), + false // genBodyForOverriddenMeth + ) + + // Convert F# completion items to LSP completion items + let completionItems = + declarations.Items + |> Array.mapi (fun i item -> + let kind = + match item.Kind with + | FSharp.Compiler.EditorServices.CompletionItemKind.Method _ -> + Microsoft.VisualStudio.LanguageServer.Protocol.CompletionItemKind.Method + | FSharp.Compiler.EditorServices.CompletionItemKind.Property -> + Microsoft.VisualStudio.LanguageServer.Protocol.CompletionItemKind.Property + | FSharp.Compiler.EditorServices.CompletionItemKind.Field -> + Microsoft.VisualStudio.LanguageServer.Protocol.CompletionItemKind.Field + | FSharp.Compiler.EditorServices.CompletionItemKind.Event -> + Microsoft.VisualStudio.LanguageServer.Protocol.CompletionItemKind.Event + | FSharp.Compiler.EditorServices.CompletionItemKind.Argument -> + Microsoft.VisualStudio.LanguageServer.Protocol.CompletionItemKind.Variable + | FSharp.Compiler.EditorServices.CompletionItemKind.Other -> + Microsoft.VisualStudio.LanguageServer.Protocol.CompletionItemKind.Value + | _ -> Microsoft.VisualStudio.LanguageServer.Protocol.CompletionItemKind.Value + + let completionItem = Microsoft.VisualStudio.LanguageServer.Protocol.CompletionItem() + completionItem.Label <- item.NameInList + completionItem.Kind <- kind + completionItem.Detail <- null + completionItem.SortText <- sprintf "%06d" i + completionItem.FilterText <- item.NameInList + completionItem.InsertText <- item.NameInCode + completionItem) + + // Add keyword completions if appropriate + let keywordItems = + if + not declarations.IsForType + && not declarations.IsError + && List.isEmpty partialName.QualifyingIdents + then + match completionContext with + | None -> + FSharpKeywords.KeywordsWithDescription + |> List.filter (fun (keyword, _) -> not (PrettyNaming.IsOperatorDisplayName keyword)) + |> List.mapi (fun i (keyword, description) -> + let completionItem = Microsoft.VisualStudio.LanguageServer.Protocol.CompletionItem() + completionItem.Label <- keyword + completionItem.Kind <- Microsoft.VisualStudio.LanguageServer.Protocol.CompletionItemKind.Keyword + completionItem.Detail <- description + completionItem.SortText <- sprintf "%06d" (1000000 + i) // Sort keywords after regular items + completionItem.FilterText <- keyword + completionItem.InsertText <- keyword + completionItem) + |> List.toArray + | _ -> [||] + else + [||] + + let allItems = Array.append completionItems keywordItems + + return CompletionList(IsIncomplete = false, Items = allItems) + with ex -> + context.Logger.LogError("Error in completion: " + ex.Message) + return CompletionList(IsIncomplete = false, Items = [||]) + | _ -> return CompletionList(IsIncomplete = false, Items = [||]) + } + |> CancellableTask.start cancellationToken diff --git a/src/FSharp.VisualStudio.Extension/FSharpLanguageServerProvider.cs b/src/FSharp.VisualStudio.Extension/FSharpLanguageServerProvider.cs index c4f90a7ca1d..631e5b2cd61 100644 --- a/src/FSharp.VisualStudio.Extension/FSharpLanguageServerProvider.cs +++ b/src/FSharp.VisualStudio.Extension/FSharpLanguageServerProvider.cs @@ -292,7 +292,8 @@ await this.Extensibility.Settings().WriteAsync(batch => var serverConfig = new FSharpLanguageServerConfig( new FSharpLanguageServerFeatures( diagnostics: enabled.Contains(settingsReadResult.ValueOrDefault(FSharpExtensionSettings.GetDiagnosticsFrom, defaultValue: FSharpExtensionSettings.BOTH)), - semanticHighlighting: enabled.Contains(settingsReadResult.ValueOrDefault(FSharpExtensionSettings.GetSemanticHighlightingFrom, defaultValue: FSharpExtensionSettings.BOTH)) + semanticHighlighting: enabled.Contains(settingsReadResult.ValueOrDefault(FSharpExtensionSettings.GetSemanticHighlightingFrom, defaultValue: FSharpExtensionSettings.BOTH)), + completion: true )); var disposeToEndSubscription = diff --git a/tests/FSharp.Compiler.LanguageServer.Tests/FSharp.Compiler.LanguageServer.Tests.fsproj b/tests/FSharp.Compiler.LanguageServer.Tests/FSharp.Compiler.LanguageServer.Tests.fsproj index 046357aafe6..6aaecb28bd4 100644 --- a/tests/FSharp.Compiler.LanguageServer.Tests/FSharp.Compiler.LanguageServer.Tests.fsproj +++ b/tests/FSharp.Compiler.LanguageServer.Tests/FSharp.Compiler.LanguageServer.Tests.fsproj @@ -19,6 +19,10 @@ + + + + + diff --git a/tests/FSharp.Compiler.LanguageServer.Tests/Protocol.fs b/tests/FSharp.Compiler.LanguageServer.Tests/Protocol.fs index 9072fbfb460..83a9beb4b08 100644 --- a/tests/FSharp.Compiler.LanguageServer.Tests/Protocol.fs +++ b/tests/FSharp.Compiler.LanguageServer.Tests/Protocol.fs @@ -191,3 +191,37 @@ let ``Shutdown and exit`` () = do! client.JsonRpc.NotifyAsync(Methods.ExitName) } + +[] +let ``Text document completion`` () = + task { + let! client = initializeLanguageServer None + let workspace = client.Workspace + let contentOnDisk = "let x = System." + let fileOnDisk = sourceFileOnDisk contentOnDisk + let _projectIdentifier = + workspace.Projects.AddOrUpdate(ProjectConfig.Create(), [ fileOnDisk.LocalPath ]) + do! + client.JsonRpc.NotifyAsync( + Methods.TextDocumentDidOpenName, + DidOpenTextDocumentParams( + TextDocument = TextDocumentItem(Uri = fileOnDisk, LanguageId = "F#", Version = 1, Text = contentOnDisk) + ) + ) + + let! completionResponse = + client.JsonRpc.InvokeAsync( + Methods.TextDocumentCompletionName, + CompletionParams( + TextDocument = TextDocumentIdentifier(Uri = fileOnDisk), + Position = Position(Line = 0, Character = 15) // Position after "System." + ) + ) + + Assert.NotNull(completionResponse) + Assert.True(completionResponse.Items.Length > 0, "Should have completion items") + + // Check that we have some common System namespace members + let completionLabels = completionResponse.Items |> Array.map (fun item -> item.Label) + Assert.Contains("Console", completionLabels) + } diff --git a/tests/FSharp.Compiler.LanguageServer.Tests/xunit.runner.json b/tests/FSharp.Compiler.LanguageServer.Tests/xunit.runner.json new file mode 100644 index 00000000000..f47fec5d745 --- /dev/null +++ b/tests/FSharp.Compiler.LanguageServer.Tests/xunit.runner.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "appDomain": "denied", + "parallelizeAssembly": true +} \ No newline at end of file