From f72151790654629ff7aa19384886006f9f09ef5c Mon Sep 17 00:00:00 2001 From: Kayle Hinkle Date: Mon, 21 Sep 2026 13:12:24 -0400 Subject: [PATCH] Use public pull diagnostics for F# Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/Compiler/Service/FSharpWorkspaceQuery.fs | 22 ++++----- .../Common/CapabilitiesManager.fs | 4 +- .../Handlers/LanguageFeaturesHandler.fs | 33 ++++++------- .../FSharpLanguageServerProvider.cs | 49 ++----------------- .../Protocol.fs | 23 +++------ 5 files changed, 37 insertions(+), 94 deletions(-) diff --git a/src/Compiler/Service/FSharpWorkspaceQuery.fs b/src/Compiler/Service/FSharpWorkspaceQuery.fs index 39c87094928..88aa5019bb2 100644 --- a/src/Compiler/Service/FSharpWorkspaceQuery.fs +++ b/src/Compiler/Service/FSharpWorkspaceQuery.fs @@ -6,11 +6,11 @@ module FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceQuery open System open System.Collections.Generic open FSharp.Compiler.Diagnostics -open System.Threading open FSharp.Compiler.CodeAnalysis open Internal.Utilities.DependencyGraph +open Internal.Utilities.Hashing open Internal.Utilities.Library.Extras open FSharpWorkspaceState open Internal.Utilities.Library @@ -18,23 +18,16 @@ open Internal.Utilities.Library #nowarn "57" [] -type FSharpDiagnosticReport internal (diagnostics, resultId: int) = +type FSharpDiagnosticReport internal (diagnostics, resultId: string) = member _.Diagnostics = diagnostics /// The result ID of the diagnostics. This needs to be unique for each version of the document in order to be able to clear old diagnostics. - member _.ResultId = resultId.ToString() + member _.ResultId = resultId [] type FSharpWorkspaceQuery internal (depGraph: IThreadSafeDependencyGraph<_, _>, checker: FSharpChecker) = - let mutable resultIdCounter = 0 - - // TODO: we might need something more sophisticated eventually - // for now it's important that the result id is unique every time - // in order to be able to clear previous diagnostics - let getDiagnosticResultId () = Interlocked.Increment(&resultIdCounter) - member internal _.Checker = checker member _.GetProjectSnapshot projectIdentifier = @@ -86,6 +79,8 @@ type FSharpWorkspaceQuery internal (depGraph: IThreadSafeDependencyGraph<_, _>, use _ = Activity.start "GetDiagnosticsForFile" [ Activity.Tags.fileName, file.LocalPath ] + let projectSnapshot = this.GetProjectSnapshotForFile file + this.GetParseAndCheckResultsForFile file |> Async.map (fun results -> let diagnostics = @@ -94,7 +89,12 @@ type FSharpWorkspaceQuery internal (depGraph: IThreadSafeDependencyGraph<_, _>, | Some parseResult, _ -> parseResult.Diagnostics | _ -> [||] - FSharpDiagnosticReport(diagnostics, getDiagnosticResultId ())) + let resultId = + projectSnapshot + |> Option.map (fun snapshot -> snapshot.ProjectSnapshot.FullVersion |> Md5Hasher.toString) + |> Option.defaultValue "empty" + + FSharpDiagnosticReport(diagnostics, resultId)) member this.GetSemanticClassification(file: Uri) = use _ = diff --git a/src/FSharp.Compiler.LanguageServer/Common/CapabilitiesManager.fs b/src/FSharp.Compiler.LanguageServer/Common/CapabilitiesManager.fs index 556a1d96edc..e5b49125d97 100644 --- a/src/FSharp.Compiler.LanguageServer/Common/CapabilitiesManager.fs +++ b/src/FSharp.Compiler.LanguageServer/Common/CapabilitiesManager.fs @@ -29,8 +29,8 @@ type CapabilitiesManager(config: FSharpLanguageServerConfig, scOverrides: IServe (DiagnosticOptions( WorkDoneProgress = true, InterFileDependencies = true, - Identifier = "potato", - WorkspaceDiagnostics = true + Identifier = "syntax", + WorkspaceDiagnostics = false )), //CompletionProvider = CompletionOptions(TriggerCharacters = [| "."; " " |], ResolveProvider = true, WorkDoneProgress = true), //HoverProvider = SumType(HoverOptions(WorkDoneProgress = true)) diff --git a/src/FSharp.Compiler.LanguageServer/Handlers/LanguageFeaturesHandler.fs b/src/FSharp.Compiler.LanguageServer/Handlers/LanguageFeaturesHandler.fs index 474758924d0..028c6015db4 100644 --- a/src/FSharp.Compiler.LanguageServer/Handlers/LanguageFeaturesHandler.fs +++ b/src/FSharp.Compiler.LanguageServer/Handlers/LanguageFeaturesHandler.fs @@ -7,7 +7,6 @@ open FSharp.Compiler.LanguageServer.Common open FSharp.Compiler.LanguageServer open System.Threading.Tasks open System.Threading -open System.Collections.Generic open Microsoft.VisualStudio.FSharp.Editor #nowarn "57" @@ -18,7 +17,7 @@ type LanguageFeaturesHandler() = interface IRequestHandler< DocumentDiagnosticParams, - SumType, + SumType, FSharpRequestContext > with [] @@ -29,23 +28,19 @@ type LanguageFeaturesHandler() = let! fsharpDiagnosticReport = context.Workspace.Query.GetDiagnosticsForFile request.TextDocument.Uri - let report = - FullDocumentDiagnosticReport( - Items = (fsharpDiagnosticReport.Diagnostics |> Array.map (_.ToLspDiagnostic())), - ResultId = fsharpDiagnosticReport.ResultId - ) - - let relatedDocuments = Dictionary() - - relatedDocuments.Add( - request.TextDocument.Uri, - SumType report - ) - - return - SumType( - RelatedFullDocumentDiagnosticReport(RelatedDocuments = relatedDocuments) - ) + if request.PreviousResultId = fsharpDiagnosticReport.ResultId then + return + SumType( + UnchangedDocumentDiagnosticReport(ResultId = fsharpDiagnosticReport.ResultId) + ) + else + return + SumType( + FullDocumentDiagnosticReport( + Items = (fsharpDiagnosticReport.Diagnostics |> Array.map (_.ToLspDiagnostic())), + ResultId = fsharpDiagnosticReport.ResultId + ) + ) } |> CancellableTask.start cancellationToken diff --git a/src/FSharp.VisualStudio.Extension/FSharpLanguageServerProvider.cs b/src/FSharp.VisualStudio.Extension/FSharpLanguageServerProvider.cs index c4f90a7ca1d..21016b5385d 100644 --- a/src/FSharp.VisualStudio.Extension/FSharpLanguageServerProvider.cs +++ b/src/FSharp.VisualStudio.Extension/FSharpLanguageServerProvider.cs @@ -12,7 +12,6 @@ namespace FSharp.VisualStudio.Extension; using System.Threading; using System.Threading.Tasks; using FSharp.Compiler.CodeAnalysis.Workspace; -using FSharp.Compiler.Diagnostics; using FSharp.Compiler.LanguageServer; using FSharp.Compiler.LanguageServer.Common; @@ -47,31 +46,8 @@ public ServerCapabilities OverrideServerCapabilities(FSharpLanguageServerConfig var capabilities = new VSInternalServerCapabilities { TextDocumentSync = value.TextDocumentSync, - SupportsDiagnosticRequests = true, ProjectContextProvider = true, - DiagnosticProvider = - config.EnabledFeatures.Diagnostics ? - - new() - { - SupportsMultipleContextsDiagnostics = true, - DiagnosticKinds = [ - // Support a specialized requests dedicated to task-list items. This way the client can ask just - // for these, independently of other diagnostics. They can also throttle themselves to not ask if - // the task list would not be visible. - //VSInternalDiagnosticKind.Task, - // Dedicated request for workspace-diagnostics only. We will only respond to these if FSA is on. - VSInternalDiagnosticKind.Syntax, - // Fine-grained diagnostics requests. Importantly, this separates out syntactic vs semantic - // requests, allowing the former to quickly reach the user without blocking on the latter. In a - // similar vein, compiler diagnostics are explicitly distinct from analyzer-diagnostics, allowing - // the former to appear as soon as possible as they are much more critical for the user and should - // not be delayed by a slow analyzer. - //new("Semantic"), - //new(PullDiagnosticCategories.DocumentAnalyzerSyntax), - //new(PullDiagnosticCategories.DocumentAnalyzerSemantic), - ] - } : null, + DiagnosticOptions = value.DiagnosticOptions, SemanticTokensOptions = config.EnabledFeatures.SemanticHighlighting ? new() { Legend = new() @@ -95,28 +71,11 @@ public ServerCapabilities OverrideServerCapabilities(FSharpLanguageServerConfig } } -internal class VsDiagnosticsHandler - : IRequestHandler, - IRequestHandler +internal class VsProjectContextHandler + : IRequestHandler { public bool MutatesSolutionState => false; - [LanguageServerEndpoint(VSInternalMethods.DocumentPullDiagnosticName, LanguageServerConstants.DefaultLanguageName)] - public async Task HandleRequestAsync(VSInternalDiagnosticParams request, FSharpRequestContext context, CancellationToken cancellationToken) - { - var report = await context.Workspace.Query.GetDiagnosticsForFile(request!.TextDocument!.Uri).Please(cancellationToken); - - var vsReport = new VSInternalDiagnosticReport - { - ResultId = report.ResultId, - //Identifier = 1, - //Version = 1, - Diagnostics = [.. report.Diagnostics.Select(FSharpDiagnosticExtensions.ToLspDiagnostic)] - }; - - return [vsReport]; - } - [LanguageServerEndpoint("textDocument/_vs_getProjectContexts", LanguageServerConstants.DefaultLanguageName)] public Task HandleRequestAsync(VSGetProjectContextsParams request, FSharpRequestContext context, CancellationToken cancellationToken) { @@ -375,7 +334,7 @@ await this.Extensibility.Settings().WriteAsync(batch => var ((inputStream, outputStream), _server) = FSharpLanguageServer.Create(workspace, serverConfig, (serviceCollection) => { serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); }); var solutions = await ws.QuerySolutionAsync( diff --git a/tests/FSharp.Compiler.LanguageServer.Tests/Protocol.fs b/tests/FSharp.Compiler.LanguageServer.Tests/Protocol.fs index 9072fbfb460..9ebf56df0d6 100644 --- a/tests/FSharp.Compiler.LanguageServer.Tests/Protocol.fs +++ b/tests/FSharp.Compiler.LanguageServer.Tests/Protocol.fs @@ -98,17 +98,12 @@ let ``Basic server workflow`` () = ) let! diagnosticsResponse = - client.JsonRpc.InvokeAsync>( + client.JsonRpc.InvokeAsync>( Methods.TextDocumentDiagnosticName, DocumentDiagnosticParams(TextDocument = TextDocumentIdentifier(Uri = fileOnDisk)) ) - Assert.Equal( - 0, - (diagnosticsResponse.First.RelatedDocuments - |> Seq.head - |> _.Value.First.Items.Length) - ) + Assert.Equal(0, diagnosticsResponse.First.Items.Length) let contentEdit = $"{contentOnDisk}\nx <- 2" @@ -122,13 +117,12 @@ let ``Basic server workflow`` () = ) let! diagnosticsResponse = - client.JsonRpc.InvokeAsync>( + client.JsonRpc.InvokeAsync>( Methods.TextDocumentDiagnosticName, DocumentDiagnosticParams(TextDocument = TextDocumentIdentifier(Uri = fileOnDisk)) ) - let diagnostics = - diagnosticsResponse.First.RelatedDocuments |> Seq.head |> _.Value.First.Items + let diagnostics = diagnosticsResponse.First.Items Assert.Equal(1, diagnostics.Length) Assert.Contains("This value is not mutable", diagnostics[0].Message) @@ -140,18 +134,13 @@ let ``Basic server workflow`` () = ) let! diagnosticsResponse = - client.JsonRpc.InvokeAsync>( + client.JsonRpc.InvokeAsync>( Methods.TextDocumentDiagnosticName, DocumentDiagnosticParams(TextDocument = TextDocumentIdentifier(Uri = fileOnDisk)) ) // We didn't save the file, so it should be again read from disk and have no diagnostics - Assert.Equal( - 0, - (diagnosticsResponse.First.RelatedDocuments - |> Seq.head - |> _.Value.First.Items.Length) - ) + Assert.Equal(0, diagnosticsResponse.First.Items.Length) } []