diff --git a/src/CSharpLanguageServer/Diagnostics.fs b/src/CSharpLanguageServer/Diagnostics.fs index c0b7495a..e552d76c 100644 --- a/src/CSharpLanguageServer/Diagnostics.fs +++ b/src/CSharpLanguageServer/Diagnostics.fs @@ -65,7 +65,7 @@ module Diagnostics = let lspClient = new LspClientStub() let cwd = string (Directory.GetCurrentDirectory()) - let progressReporter = ProgressReporter lspClient + let progressReporter = ProgressReporter(lspClient, emptyClientCapabilities) let! _sln = solutionLoadSolutionWithPathOrOnDir lspClient progressReporter None cwd logger.LogDebug("diagnose: done") diff --git a/src/CSharpLanguageServer/Lsp/ProgressReporter.fs b/src/CSharpLanguageServer/Lsp/ProgressReporter.fs index d475c15d..061def95 100644 --- a/src/CSharpLanguageServer/Lsp/ProgressReporter.fs +++ b/src/CSharpLanguageServer/Lsp/ProgressReporter.fs @@ -5,33 +5,40 @@ open Ionide.LanguageServerProtocol open Ionide.LanguageServerProtocol.Server open Ionide.LanguageServerProtocol.Types -type ProgressReporter(client: ILspClient) = +type ProgressReporter(client: ILspClient, clientCapabilities: ClientCapabilities) = let mutable canReport = false - let mutable endSent = false + let workDoneProgressSupported = + clientCapabilities.Window + |> Option.bind _.WorkDoneProgress + |> Option.defaultValue false + member val Token = ProgressToken.C2(Guid.NewGuid().ToString()) member this.Begin(title, ?cancellable, ?message, ?percentage) = async { - let! progressCreateResult = client.WindowWorkDoneProgressCreate { Token = this.Token } + if not workDoneProgressSupported then + canReport <- false + else + let! progressCreateResult = client.WindowWorkDoneProgressCreate { Token = this.Token } - match progressCreateResult with - | Error _ -> canReport <- false - | Ok() -> - canReport <- true + match progressCreateResult with + | Error _ -> canReport <- false + | Ok() -> + canReport <- true - let param = - WorkDoneProgressBegin.Create( - title = title, - ?cancellable = cancellable, - ?message = message, - ?percentage = percentage - ) + let param = + WorkDoneProgressBegin.Create( + title = title, + ?cancellable = cancellable, + ?message = message, + ?percentage = percentage + ) - do! - client.Progress - { Token = this.Token - Value = serialize param } + do! + client.Progress + { Token = this.Token + Value = serialize param } } member this.Report(?cancellable, ?message, ?percentage) = async { diff --git a/src/CSharpLanguageServer/Lsp/Workspace.fs b/src/CSharpLanguageServer/Lsp/Workspace.fs index ec774fdb..a47fc41a 100644 --- a/src/CSharpLanguageServer/Lsp/Workspace.fs +++ b/src/CSharpLanguageServer/Lsp/Workspace.fs @@ -421,8 +421,8 @@ let workspaceDocumentSymbol let workspaceDocumentVersion workspace uri = uri |> workspace.OpenDocs.TryFind |> Option.map _.Version -let workspaceWithSolutionsLoaded (settings: ServerSettings) (lspClient: ILspClient) workspace = async { - let progressReporter = ProgressReporter lspClient +let workspaceWithSolutionsLoaded (settings: ServerSettings) (lspClient: ILspClient) (clientCapabilities: ClientCapabilities) workspace = async { + let progressReporter = ProgressReporter(lspClient, clientCapabilities) let beginMessage = sprintf "Loading workspace (%d workspace folders)" workspace.Folders.Length diff --git a/src/CSharpLanguageServer/State/ServerState.fs b/src/CSharpLanguageServer/State/ServerState.fs index b5835cb2..929dce63 100644 --- a/src/CSharpLanguageServer/State/ServerState.fs +++ b/src/CSharpLanguageServer/State/ServerState.fs @@ -452,46 +452,63 @@ let processServerEvent (logger: ILogger) state postSelf msg : Async PushDiagnosticsDocumentBacklog = newBacklog } let wf, docForUri = docUri |> workspaceDocument state.Workspace AnyDocument - let wfPathToUri = workspaceFolderPathToUri wf.Value match wf, docForUri with | Some wf, None -> - let cshtmlPath = workspaceFolderUriToPath wf docUri |> _.Value - - match! solutionGetRazorDocumentForPath wf.Solution.Value cshtmlPath with - | Some(_, compilation, cshtmlTree) -> - let semanticModelMaybe = compilation.GetSemanticModel cshtmlTree |> Option.ofObj - - match semanticModelMaybe with + // Only try to process as a .cshtml file if it actually is one + match workspaceFolderUriToPath wf docUri with + | Some cshtmlPath when cshtmlPath.EndsWith(".cshtml", StringComparison.OrdinalIgnoreCase) -> + match wf.Solution with | None -> - Error(Exception "could not GetSemanticModelAsync") - |> PushDiagnosticsDocumentDiagnosticsResolution - |> postSelf - - | Some semanticModel -> - let diagnostics = - semanticModel.GetDiagnostics() - |> Seq.map (Diagnostic.fromRoslynDiagnostic (workspaceFolderPathToUri wf)) - |> Seq.filter (fun (_, uri) -> uri = docUri) - |> Seq.map fst - |> Array.ofSeq - - Ok(docUri, None, diagnostics) - |> PushDiagnosticsDocumentDiagnosticsResolution - |> postSelf - - | None -> - // could not find document for this enqueued uri + // Solution not loaded yet, rebuild backlog and try again later + postSelf PushDiagnosticsDocumentBacklogUpdate + postSelf PushDiagnosticsProcessPendingDocuments + | Some solution -> + match! solutionGetRazorDocumentForPath solution cshtmlPath with + | Some(_, compilation, cshtmlTree) -> + let semanticModelMaybe = compilation.GetSemanticModel cshtmlTree |> Option.ofObj + + match semanticModelMaybe with + | None -> + Error(Exception "could not GetSemanticModelAsync") + |> PushDiagnosticsDocumentDiagnosticsResolution + |> postSelf + + | Some semanticModel -> + let diagnostics = + semanticModel.GetDiagnostics() + |> Seq.map (Diagnostic.fromRoslynDiagnostic (workspaceFolderPathToUri wf)) + |> Seq.filter (fun (_, uri) -> uri = docUri) + |> Seq.map fst + |> Array.ofSeq + + Ok(docUri, None, diagnostics) + |> PushDiagnosticsDocumentDiagnosticsResolution + |> postSelf + + | None -> + logger.LogDebug( + "PushDiagnosticsProcessPendingDocuments: could not find razor document for \"{cshtmlPath}\"", + cshtmlPath + ) + // Continue with next document + postSelf PushDiagnosticsProcessPendingDocuments + + | _ -> + // Not a .cshtml file or couldn't convert URI + // This can happen if solution hasn't loaded yet - rebuild backlog for retry logger.LogDebug( - "PushDiagnosticsProcessPendingDocuments: could not find document w/ uri \"{docUri}\"", + "PushDiagnosticsProcessPendingDocuments: could not find document w/ uri \"{docUri}\", will retry", string docUri ) - - () + postSelf PushDiagnosticsDocumentBacklogUpdate + postSelf PushDiagnosticsProcessPendingDocuments return newState | Some wf, Some doc -> + let wfPathToUri = workspaceFolderPathToUri wf + let resolveDocumentDiagnostics () : Task = task { let! semanticModelMaybe = doc.GetSemanticModelAsync() @@ -566,7 +583,7 @@ let processServerEvent (logger: ILogger) state postSelf msg : Async match solutionReloadDeadline < DateTime.Now with | true -> - let! updatedWorkspace = workspaceWithSolutionsLoaded state.Settings state.LspClient.Value state.Workspace + let! updatedWorkspace = workspaceWithSolutionsLoaded state.Settings state.LspClient.Value state.ClientCapabilities state.Workspace return { state with diff --git a/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj b/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj index b949e463..58a345fc 100644 --- a/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj +++ b/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj @@ -28,6 +28,7 @@ + diff --git a/tests/CSharpLanguageServer.Tests/ProgressReporterTests.fs b/tests/CSharpLanguageServer.Tests/ProgressReporterTests.fs new file mode 100644 index 00000000..4f050cc7 --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/ProgressReporterTests.fs @@ -0,0 +1,103 @@ +module CSharpLanguageServer.Tests.ProgressReporterTests + +open System +open NUnit.Framework +open Ionide.LanguageServerProtocol +open Ionide.LanguageServerProtocol.Types +open Ionide.LanguageServerProtocol.JsonRpc +open CSharpLanguageServer.Lsp +open CSharpLanguageServer.Tests.Tooling + +/// Client stub that tracks whether WindowWorkDoneProgressCreate was called +type TrackingLspClientStub() = + let mutable createCalled = false + + member _.WasCreateCalled = createCalled + + interface System.IDisposable with + member _.Dispose() = () + + interface ILspClient with + member _.WindowWorkDoneProgressCreate(_: WorkDoneProgressCreateParams) = + createCalled <- true + async { return LspResult.Ok() } + + member _.Progress(_: ProgressParams) = async { return () } + member _.WindowShowMessage(_) = async { return () } + member _.WindowLogMessage(_) = async { return () } + member _.TelemetryEvent(_) = async { return () } + member _.TextDocumentPublishDiagnostics(_) = async { return () } + member _.LogTrace(_) = async { return () } + member _.CancelRequest(_) = async { return () } + member _.WorkspaceWorkspaceFolders() = async { return LspResult.Ok None } + member _.WorkspaceConfiguration(_) = async { return LspResult.Ok [||] } + member _.WorkspaceSemanticTokensRefresh() = async { return LspResult.Ok() } + member _.WindowShowDocument(_) = async { return LspResult.Ok { Success = false } } + member _.WorkspaceInlineValueRefresh() = async { return LspResult.Ok() } + member _.WorkspaceInlayHintRefresh() = async { return LspResult.Ok() } + member _.WorkspaceDiagnosticRefresh() = async { return LspResult.Ok() } + member _.ClientRegisterCapability(_) = async { return LspResult.Ok() } + member _.ClientUnregisterCapability(_) = async { return LspResult.Ok() } + member _.WindowShowMessageRequest(_) = async { return LspResult.Ok None } + member _.WorkspaceCodeLensRefresh() = async { return LspResult.Ok() } + member _.WorkspaceApplyEdit(_) = async { + return LspResult.Ok { Applied = false; FailureReason = None; FailedChange = None } + } + +let capabilitiesWithWorkDoneProgress (supported: bool): ClientCapabilities = + let windowCaps: WindowClientCapabilities = + { WorkDoneProgress = Some supported + ShowMessage = None + ShowDocument = None } + + { Workspace = None + TextDocument = None + NotebookDocument = None + Window = Some windowCaps + General = None + Experimental = None } + +[] +let ``ProgressReporter does not call WindowWorkDoneProgressCreate when capability not supported`` () = + let client = new TrackingLspClientStub() + let reporter = new ProgressReporter(client, emptyClientCapabilities) + + reporter.Begin("Test Title") |> Async.RunSynchronously + + Assert.That(client.WasCreateCalled, Is.False, "WindowWorkDoneProgressCreate should not be called when capability is not supported") + +[] +let ``ProgressReporter does not call WindowWorkDoneProgressCreate when WorkDoneProgress is false`` () = + let client = new TrackingLspClientStub() + let caps = capabilitiesWithWorkDoneProgress false + let reporter = new ProgressReporter(client, caps) + + reporter.Begin("Test Title") |> Async.RunSynchronously + + Assert.That(client.WasCreateCalled, Is.False, "WindowWorkDoneProgressCreate should not be called when WorkDoneProgress is false") + +[] +let ``ProgressReporter calls WindowWorkDoneProgressCreate when capability is supported`` () = + let client = new TrackingLspClientStub() + let caps = capabilitiesWithWorkDoneProgress true + let reporter = new ProgressReporter(client, caps) + + reporter.Begin("Test Title") |> Async.RunSynchronously + + Assert.That(client.WasCreateCalled, Is.True, "WindowWorkDoneProgressCreate should be called when WorkDoneProgress is true") + +[] +let ``ProgressReporter Report and End are no-ops when capability not supported`` () = + let client = new TrackingLspClientStub() + let reporter = new ProgressReporter(client, emptyClientCapabilities) + + // Begin with unsupported capability + reporter.Begin("Test Title") |> Async.RunSynchronously + + // Report and End should not throw + Assert.DoesNotThrowAsync(fun () -> + reporter.Report(message = "Progress") |> Async.StartAsTask :> System.Threading.Tasks.Task + ) + Assert.DoesNotThrowAsync(fun () -> + reporter.End(message = "Done") |> Async.StartAsTask :> System.Threading.Tasks.Task + ) diff --git a/tests/CSharpLanguageServer.Tests/Tooling.fs b/tests/CSharpLanguageServer.Tests/Tooling.fs index af8f8dc2..882f7632 100644 --- a/tests/CSharpLanguageServer.Tests/Tooling.fs +++ b/tests/CSharpLanguageServer.Tests/Tooling.fs @@ -88,6 +88,11 @@ let defaultClientCapabilities = ResolveSupport = None HonorsChangeAnnotations = None CodeActionLiteralSupport = Some { CodeActionKind = { ValueSet = Array.empty } } } } + Window = + Some + { WorkDoneProgress = Some true + ShowMessage = None + ShowDocument = None } Experimental = {| csharp = {| metadataUris = true |} |} |> serialize |> Some } let defaultClientProfile =