Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/CSharpLanguageServer/Diagnostics.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
43 changes: 25 additions & 18 deletions src/CSharpLanguageServer/Lsp/ProgressReporter.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions src/CSharpLanguageServer/Lsp/Workspace.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
77 changes: 47 additions & 30 deletions src/CSharpLanguageServer/State/ServerState.fs
Original file line number Diff line number Diff line change
Expand Up @@ -452,46 +452,63 @@ let processServerEvent (logger: ILogger) state postSelf msg : Async<ServerState>
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()

Expand Down Expand Up @@ -566,7 +583,7 @@ let processServerEvent (logger: ILogger) state postSelf msg : Async<ServerState>

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<Compile Include="TypeDefinitionTests.fs" />
<Compile Include="ImplementationTests.fs" />
<Compile Include="CSharpMetadataTests.fs" />
<Compile Include="ProgressReporterTests.fs" />
</ItemGroup>

<ItemGroup>
Expand Down
103 changes: 103 additions & 0 deletions tests/CSharpLanguageServer.Tests/ProgressReporterTests.fs
Original file line number Diff line number Diff line change
@@ -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 }

[<Test>]
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")

[<Test>]
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")

[<Test>]
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")

[<Test>]
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
)
5 changes: 5 additions & 0 deletions tests/CSharpLanguageServer.Tests/Tooling.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down