From de8b9880180092e07e5ab767122408644360f0d4 Mon Sep 17 00:00:00 2001 From: baronfel Date: Sun, 20 Jun 2021 12:29:21 -0500 Subject: [PATCH 1/3] use mailbox processors per-file to serialize diagnostics pushes --- .github/workflows/build.yml | 2 +- .github/workflows/release.yml | 2 +- .paket/Paket.Restore.targets | 63 ++++++++- global.json | 2 +- paket.lock | 2 +- src/FsAutoComplete/FsAutoComplete.Lsp.fs | 130 +++++++++++------- test/FsAutoComplete.Tests.Lsp/CoreTests.fs | 7 +- .../ExtensionsTests.fs | 8 +- test/FsAutoComplete.Tests.Lsp/Helpers.fs | 9 +- .../TestCases/Analyzers/AnalyzersTest.fsproj | 10 -- .../Analyzers/{Script.fs => Script.fsx} | 0 .../CodeLensTest/CodeLensTest.fsproj | 10 -- .../CodeLensTest/{Script.fs => Script.fsx} | 0 13 files changed, 159 insertions(+), 86 deletions(-) delete mode 100644 test/FsAutoComplete.Tests.Lsp/TestCases/Analyzers/AnalyzersTest.fsproj rename test/FsAutoComplete.Tests.Lsp/TestCases/Analyzers/{Script.fs => Script.fsx} (100%) delete mode 100644 test/FsAutoComplete.Tests.Lsp/TestCases/CodeLensTest/CodeLensTest.fsproj rename test/FsAutoComplete.Tests.Lsp/TestCases/CodeLensTest/{Script.fs => Script.fsx} (100%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0c665afd0..b51eede09 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: os: [windows-2019, macos-10.15, ubuntu-20.04] - dotnet: [5.0.300] + dotnet: [5.0.301] fail-fast: false # we have timing issues on some OS, so we want them all to run runs-on: ${{ matrix.os }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 335a3f502..2168a1a66 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: release: strategy: matrix: - dotnet: [5.0.300] + dotnet: [5.0.301] runs-on: ubuntu-20.04 diff --git a/.paket/Paket.Restore.targets b/.paket/Paket.Restore.targets index 0ec281627..e55904b25 100644 --- a/.paket/Paket.Restore.targets +++ b/.paket/Paket.Restore.targets @@ -289,14 +289,16 @@ $(MSBuildProjectDirectory)/$(MSBuildProjectFile) true + false + true false - true + true false true false - true + true false - true + true $(PaketIntermediateOutputPath)\$(Configuration) $(PaketIntermediateOutputPath) @@ -314,6 +316,53 @@ + = 4.9) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= net472)) (&& (== netstandard2.0) (>= net5.0)) Microsoft.Build.Framework (16.10) - copy_local: false System.Security.Permissions (>= 4.7) - Microsoft.Build.Locator (1.4.1) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= net5.0)) + Microsoft.Build.Locator (1.4.1) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp3.1)) Microsoft.Build.Tasks.Core (16.10) - copy_local: false Microsoft.Build.Framework (>= 16.10) Microsoft.Build.Utilities.Core (>= 16.10) diff --git a/src/FsAutoComplete/FsAutoComplete.Lsp.fs b/src/FsAutoComplete/FsAutoComplete.Lsp.fs index 480332d50..4048f1655 100644 --- a/src/FsAutoComplete/FsAutoComplete.Lsp.fs +++ b/src/FsAutoComplete/FsAutoComplete.Lsp.fs @@ -18,6 +18,7 @@ open FsToolkit.ErrorHandling open FSharp.UMX open FSharp.Analyzers open FSharp.Compiler.Text +open System.Threading module FcsRange = FSharp.Compiler.Text.Range type FcsRange = FSharp.Compiler.Text.Range @@ -82,6 +83,74 @@ type FSharpLspClient(sendServerNotification: ClientNotificationSender, sendServe member __.NotifyFileParsed (p: PlainNotification) = sendServerNotification "fsharp/fileParsed" (box p) |> Async.Ignore +type DiagnosticMessage = + | Add of source: string * diags: Diagnostic [] + | Clear of source: string + +/// a type that handles bookkeeping for sending file diagnostics. It will debounce calls and handle sending diagnostics via the configured function when safe +type DiagnosticCollection(sendDiagnostics: DocumentUri -> Diagnostic [] -> Async) = + let send uri (diags: Map) = + Map.toArray diags + |> Array.collect snd + |> sendDiagnostics uri + + let agentFor (uri: DocumentUri) cTok = + let logger = LogProvider.getLoggerByName $"Diagnostics/{uri}" + let mailbox = MailboxProcessor.Start((fun inbox -> + let rec loop (state: Map) = async { + match! inbox.Receive() with + | Add (source, diags) -> + let newState = state |> Map.add source diags + do! send uri newState + return! loop newState + | Clear source -> + let newState = state |> Map.remove source + do! send uri newState + return! loop newState + } + loop Map.empty + ), cTok) + mailbox.Error.Add(fun exn -> logger.error (Log.setMessage "Error while sending diagnostics: {message}" >> Log.addExn exn >> Log.addContext "message" exn.Message)) + mailbox + + let agents = System.Collections.Generic.Dictionary>() + let ctoks = System.Collections.Generic.Dictionary() + let getOrAddAgent fileUri = + match agents.TryGetValue fileUri with + | true, mailbox -> mailbox + | false, _ -> + lock agents (fun _ -> + let cts = new CancellationTokenSource() + let mailbox = agentFor fileUri cts.Token + agents.Add(fileUri, mailbox) + ctoks.Add(fileUri, cts) + mailbox + ) + + member x.SetFor(fileUri: DocumentUri, kind: string, values: Diagnostic []) = + let mailbox = getOrAddAgent fileUri + match values with + | [||] -> + mailbox.Post (Clear kind) + | values -> + mailbox.Post (Add(kind, values)) + member x.ClearFor(fileUri: DocumentUri) = + let mailbox = getOrAddAgent fileUri + lock agents (fun _ -> + let ctok = ctoks.[fileUri] + ctok.Cancel() + ctoks.Remove(fileUri) |> ignore + agents.Remove(fileUri) |> ignore + ) + member x.ClearFor(fileUri: DocumentUri, kind: string) = + let mailbox = getOrAddAgent fileUri + mailbox.Post (Clear kind) + + interface IDisposable with + member x.Dispose() = + for KeyValue(fileUri, cts) in ctoks do + cts.Cancel() + type FSharpLspServer(backgroundServiceEnabled: bool, state: State, lspClient: FSharpLspClient) = inherit LspServer() @@ -134,22 +203,13 @@ type FSharpLspServer(backgroundServiceEnabled: bool, state: State, lspClient: FS let parseFileDebuncer = Debounce(500, parseFile) - let diagnosticCollections = System.Collections.Concurrent.ConcurrentDictionary() - let sendDiagnostics (uri: DocumentUri) = - let diags = - diagnosticCollections - |> Seq.collect (fun kv -> - let (u, _) = kv.Key - if u = uri then kv.Value else [||]) - |> Seq.sortBy (fun n -> - n.Range.Start.Line - ) - |> Seq.toArray - logger.info (Log.setMessage "SendDiag for {file}: {diags} entries" >> Log.addContextDestructured "file" uri >> Log.addContextDestructured "diags" diags.Length ) - {Uri = uri; Diagnostics = diags} + let sendDiagnostics (uri: DocumentUri) (diags: Diagnostic []) = + logger.info (Log.setMessage "SendDiag for {file}: {diags} entries" >> Log.addContextDestructured "file" uri >> Log.addContextDestructured "diags" diags.Length) + { Uri = uri; Diagnostics = diags } |> lspClient.TextDocumentPublishDiagnostics - |> Async.Start + + let diagnosticCollections = new DiagnosticCollection(sendDiagnostics) let handleCommandEvents (n: NotificationEvent) = try @@ -174,36 +234,25 @@ type FSharpLspServer(backgroundServiceEnabled: bool, state: State, lspClient: FS | NotificationEvent.ParseError (errors, file) -> let uri = Path.LocalPathToUri file - diagnosticCollections.AddOrUpdate((uri, "F# Compiler"), [||], fun _ _ -> [||]) |> ignore - let diags = errors |> Array.map (fcsErrorToDiagnostic) - diagnosticCollections.AddOrUpdate((uri, "F# Compiler"), diags, fun _ _ -> diags) |> ignore - sendDiagnostics uri + diagnosticCollections.SetFor(uri, "F# Compiler", diags) | NotificationEvent.UnusedOpens (file, opens) -> let uri = Path.LocalPathToUri file - diagnosticCollections.AddOrUpdate((uri, "F# Unused opens"), [||], fun _ _ -> [||]) |> ignore - let diags = opens |> Array.map(fun n -> {Diagnostic.Range = fcsRangeToLsp n; Code = None; Severity = Some DiagnosticSeverity.Hint; Source = "FSAC"; Message = "Unused open statement"; RelatedInformation = Some [||]; Tags = Some [| DiagnosticTag.Unnecessary |] } ) - diagnosticCollections.AddOrUpdate((uri, "F# Unused opens"), diags, fun _ _ -> diags) |> ignore - sendDiagnostics uri + diagnosticCollections.SetFor(uri, "F# Unused opens", diags) | NotificationEvent.UnusedDeclarations (file, decls) -> let uri = Path.LocalPathToUri file - diagnosticCollections.AddOrUpdate((uri, "F# Unused declarations"), [||], fun _ _ -> [||]) |> ignore - let diags = decls |> Array.map(fun (n, t) -> {Diagnostic.Range = fcsRangeToLsp n; Code = (if t then Some "1" else None); Severity = Some DiagnosticSeverity.Hint; Source = "FSAC"; Message = "This value is unused"; RelatedInformation = Some [||]; Tags = Some [| DiagnosticTag.Unnecessary |] } ) - diagnosticCollections.AddOrUpdate((uri, "F# Unused declarations"), diags, fun _ _ -> diags) |> ignore - sendDiagnostics uri + diagnosticCollections.SetFor(uri, "F# Unused declarations", diags) | NotificationEvent.SimplifyNames (file, decls) -> let uri = Path.LocalPathToUri file - diagnosticCollections.AddOrUpdate((uri, "F# simplify names"), [||], fun _ _ -> [||]) |> ignore - let diags = decls |> Array.map(fun ({ Range = range; RelativeName = _relName }) -> { Diagnostic.Range = fcsRangeToLsp range Code = None @@ -213,13 +262,10 @@ type FSharpLspServer(backgroundServiceEnabled: bool, state: State, lspClient: FS RelatedInformation = Some [| |] Tags = Some [| DiagnosticTag.Unnecessary |] } ) - diagnosticCollections.AddOrUpdate((uri, "F# simplify names"), diags, fun _ _ -> diags) |> ignore - sendDiagnostics uri + diagnosticCollections.SetFor(uri, "F# simplify names", diags) | NotificationEvent.Lint (file, warnings) -> let uri = Path.LocalPathToUri file - diagnosticCollections.AddOrUpdate((uri, "F# Linter"), [||], fun _ _ -> [||]) |> ignore - let fs = warnings |> List.choose (fun w -> w.Warning.Details.SuggestedFix @@ -245,8 +291,7 @@ type FSharpLspServer(backgroundServiceEnabled: bool, state: State, lspClient: FS Tags = None } ) |> List.toArray - diagnosticCollections.AddOrUpdate((uri, "F# Linter"), diags, fun _ _ -> diags) |> ignore - sendDiagnostics uri + diagnosticCollections.SetFor(uri, "F# linter", diags) | NotificationEvent.Canceled (msg) -> let ntf = {Content = msg} @@ -257,12 +302,10 @@ type FSharpLspServer(backgroundServiceEnabled: bool, state: State, lspClient: FS |> lspClient.TextDocumentPublishDiagnostics |> Async.Start | NotificationEvent.AnalyzerMessage(messages, file) -> - let uri = Path.LocalPathToUri file - diagnosticCollections.AddOrUpdate((uri, "F# Analyzers"), [||], fun _ _ -> [||]) |> ignore match messages with | [||] -> - diagnosticCollections.AddOrUpdate((uri, "F# Analyzers"), [||], fun _ _ -> [||]) |> ignore + diagnosticCollections.SetFor(uri, "F# Analyzers", [||]) | messages -> let fs = messages @@ -278,7 +321,7 @@ type FSharpLspServer(backgroundServiceEnabled: bool, state: State, lspClient: FS if analyzerFixes.ContainsKey uri then () else analyzerFixes.[uri] <- new System.Collections.Generic.Dictionary<_,_>() analyzerFixes.[uri].[aName] <- fs - let diag = + let diags = messages |> Array.map (fun m -> let range = fcsRangeToLsp m.Range let s = @@ -294,8 +337,7 @@ type FSharpLspServer(backgroundServiceEnabled: bool, state: State, lspClient: FS RelatedInformation = None Tags = None } ) - diagnosticCollections.AddOrUpdate((uri, "F# Analyzers"), diag, fun _ _ -> diag) |> ignore - sendDiagnostics uri + diagnosticCollections.SetFor(uri, "F# Analyzers", diags) with | _ -> () @@ -462,6 +504,7 @@ type FSharpLspServer(backgroundServiceEnabled: bool, state: State, lspClient: FS for (dispose: IDisposable) in commandDisposables do dispose.Dispose() (commands :> IDisposable).Dispose() + (diagnosticCollections :> IDisposable).Dispose() } override __.Initialize(p: InitializeParams) = async { @@ -1304,12 +1347,7 @@ type FSharpLspServer(backgroundServiceEnabled: bool, state: State, lspClient: FS |> Array.iter (fun c -> if c.Type = FileChangeType.Deleted then let uri = c.Uri - diagnosticCollections.AddOrUpdate((uri, "F# Compiler"), [||], fun _ _ -> [||]) |> ignore - diagnosticCollections.AddOrUpdate((uri, "F# Unused opens"), [||], fun _ _ -> [||]) |> ignore - diagnosticCollections.AddOrUpdate((uri, "F# Unused declarations"), [||], fun _ _ -> [||]) |> ignore - diagnosticCollections.AddOrUpdate((uri, "F# simplify names"), [||], fun _ _ -> [||]) |> ignore - diagnosticCollections.AddOrUpdate((uri, "F# Linter"), [||], fun _ _ -> [||]) |> ignore - sendDiagnostics uri + diagnosticCollections.ClearFor uri () ) diff --git a/test/FsAutoComplete.Tests.Lsp/CoreTests.fs b/test/FsAutoComplete.Tests.Lsp/CoreTests.fs index 73d55abe2..7855d2bdf 100644 --- a/test/FsAutoComplete.Tests.Lsp/CoreTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CoreTests.fs @@ -64,12 +64,11 @@ let codeLensTest state = let server = async { let path = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "CodeLensTest") - let! (server, event) = serverInitialize path {defaultConfigDto with EnableReferenceCodeLens = Some true} state - let projectPath = Path.Combine(path, "CodeLensTest.fsproj") - do! parseProject projectPath server - let path = Path.Combine(path, "Script.fs") + let! (server, events) = serverInitialize path {defaultConfigDto with EnableReferenceCodeLens = Some true} state + let path = Path.Combine(path, "Script.fsx") let tdop : DidOpenTextDocumentParams = { TextDocument = loadDocument path} do! server.TextDocumentDidOpen tdop + do! waitForParseResultsForFile "Script.fsx" events |> AsyncResult.bimap id (fun e -> failtest "should have not had check errors") return (server, path) } |> Async.Cache diff --git a/test/FsAutoComplete.Tests.Lsp/ExtensionsTests.fs b/test/FsAutoComplete.Tests.Lsp/ExtensionsTests.fs index ed63f9562..35242561a 100644 --- a/test/FsAutoComplete.Tests.Lsp/ExtensionsTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/ExtensionsTests.fs @@ -332,14 +332,10 @@ let analyzerTests state = let analyzerEnabledConfig = { defaultConfigDto with EnableAnalyzers = Some true - AnalyzersPath = Some [| analyzerPath |] - } - - do! Helpers.runProcess (logDotnetRestore "RenameTest") path "dotnet" "restore" - |> Async.map expectExitCodeZero + AnalyzersPath = Some [| analyzerPath |] } let! (server, events) = serverInitialize path analyzerEnabledConfig state - let scriptPath = Path.Combine(path, "Script.fs") + let scriptPath = Path.Combine(path, "Script.fsx") do! Async.Sleep (TimeSpan.FromSeconds 5.) do! waitForWorkspaceFinishedParsing events do! server.TextDocumentDidOpen { TextDocument = loadDocument scriptPath } diff --git a/test/FsAutoComplete.Tests.Lsp/Helpers.fs b/test/FsAutoComplete.Tests.Lsp/Helpers.fs index 4dba36021..11dd91034 100644 --- a/test/FsAutoComplete.Tests.Lsp/Helpers.fs +++ b/test/FsAutoComplete.Tests.Lsp/Helpers.fs @@ -413,9 +413,12 @@ let (|UnwrappedPlainNotification|_|) eventType (notification: PlainNotification) let waitForWorkspaceFinishedParsing (events : ClientEvents) = let chooser (name, payload) = - match name, unbox payload with - | "fsharp/notifyWorkspace", (UnwrappedPlainNotification "workspaceLoad" (workspaceLoadResponse: FsAutoComplete.CommandResponse.WorkspaceLoadResponse) )-> - if workspaceLoadResponse.Status = "finished" then Some () else None + match name with + | "fsharp/notifyWorkspace" -> + match unbox payload with + | (UnwrappedPlainNotification "workspaceLoad" (workspaceLoadResponse: FsAutoComplete.CommandResponse.WorkspaceLoadResponse) )-> + if workspaceLoadResponse.Status = "finished" then Some () else None + | _ -> None | _ -> None logger.debug (eventX "waiting for workspace to finish loading") diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/Analyzers/AnalyzersTest.fsproj b/test/FsAutoComplete.Tests.Lsp/TestCases/Analyzers/AnalyzersTest.fsproj deleted file mode 100644 index 2b16dfec4..000000000 --- a/test/FsAutoComplete.Tests.Lsp/TestCases/Analyzers/AnalyzersTest.fsproj +++ /dev/null @@ -1,10 +0,0 @@ - - - - net5.0 - false - - - - - diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/Analyzers/Script.fs b/test/FsAutoComplete.Tests.Lsp/TestCases/Analyzers/Script.fsx similarity index 100% rename from test/FsAutoComplete.Tests.Lsp/TestCases/Analyzers/Script.fs rename to test/FsAutoComplete.Tests.Lsp/TestCases/Analyzers/Script.fsx diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/CodeLensTest/CodeLensTest.fsproj b/test/FsAutoComplete.Tests.Lsp/TestCases/CodeLensTest/CodeLensTest.fsproj deleted file mode 100644 index 2b16dfec4..000000000 --- a/test/FsAutoComplete.Tests.Lsp/TestCases/CodeLensTest/CodeLensTest.fsproj +++ /dev/null @@ -1,10 +0,0 @@ - - - - net5.0 - false - - - - - diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/CodeLensTest/Script.fs b/test/FsAutoComplete.Tests.Lsp/TestCases/CodeLensTest/Script.fsx similarity index 100% rename from test/FsAutoComplete.Tests.Lsp/TestCases/CodeLensTest/Script.fs rename to test/FsAutoComplete.Tests.Lsp/TestCases/CodeLensTest/Script.fsx From 52b54255c8d8d7943060007fc0fd80ec06e17b69 Mon Sep 17 00:00:00 2001 From: baronfel Date: Sun, 20 Jun 2021 12:34:03 -0500 Subject: [PATCH 2/3] restart processors on error --- src/FsAutoComplete/FsAutoComplete.Lsp.fs | 35 ++++++++++++++++-------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/FsAutoComplete/FsAutoComplete.Lsp.fs b/src/FsAutoComplete/FsAutoComplete.Lsp.fs index 4048f1655..31b188ff5 100644 --- a/src/FsAutoComplete/FsAutoComplete.Lsp.fs +++ b/src/FsAutoComplete/FsAutoComplete.Lsp.fs @@ -94,7 +94,24 @@ type DiagnosticCollection(sendDiagnostics: DocumentUri -> Diagnostic [] -> Async |> Array.collect snd |> sendDiagnostics uri - let agentFor (uri: DocumentUri) cTok = + let agents = System.Collections.Generic.Dictionary>() + let ctoks = System.Collections.Generic.Dictionary() + + + let rec restartAgent (fileUri: DocumentUri) = + removeAgent fileUri + getOrAddAgent fileUri |> ignore + + and removeAgent (fileUri: DocumentUri) = + let mailbox = getOrAddAgent fileUri + lock agents (fun _ -> + let ctok = ctoks.[fileUri] + ctok.Cancel() + ctoks.Remove(fileUri) |> ignore + agents.Remove(fileUri) |> ignore + ) + + and agentFor (uri: DocumentUri) cTok = let logger = LogProvider.getLoggerByName $"Diagnostics/{uri}" let mailbox = MailboxProcessor.Start((fun inbox -> let rec loop (state: Map) = async { @@ -111,11 +128,10 @@ type DiagnosticCollection(sendDiagnostics: DocumentUri -> Diagnostic [] -> Async loop Map.empty ), cTok) mailbox.Error.Add(fun exn -> logger.error (Log.setMessage "Error while sending diagnostics: {message}" >> Log.addExn exn >> Log.addContext "message" exn.Message)) + mailbox.Error.Add(fun exn -> restartAgent uri) mailbox - let agents = System.Collections.Generic.Dictionary>() - let ctoks = System.Collections.Generic.Dictionary() - let getOrAddAgent fileUri = + and getOrAddAgent fileUri = match agents.TryGetValue fileUri with | true, mailbox -> mailbox | false, _ -> @@ -134,14 +150,9 @@ type DiagnosticCollection(sendDiagnostics: DocumentUri -> Diagnostic [] -> Async mailbox.Post (Clear kind) | values -> mailbox.Post (Add(kind, values)) - member x.ClearFor(fileUri: DocumentUri) = - let mailbox = getOrAddAgent fileUri - lock agents (fun _ -> - let ctok = ctoks.[fileUri] - ctok.Cancel() - ctoks.Remove(fileUri) |> ignore - agents.Remove(fileUri) |> ignore - ) + + member x.ClearFor(fileUri: DocumentUri) = removeAgent fileUri + member x.ClearFor(fileUri: DocumentUri, kind: string) = let mailbox = getOrAddAgent fileUri mailbox.Post (Clear kind) From a51104e420843af153b7534ab87c8ec045d28a4c Mon Sep 17 00:00:00 2001 From: baronfel Date: Sun, 20 Jun 2021 12:46:53 -0500 Subject: [PATCH 3/3] keep linter sort --- src/FsAutoComplete/FsAutoComplete.Lsp.fs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/FsAutoComplete/FsAutoComplete.Lsp.fs b/src/FsAutoComplete/FsAutoComplete.Lsp.fs index 31b188ff5..b7f11c653 100644 --- a/src/FsAutoComplete/FsAutoComplete.Lsp.fs +++ b/src/FsAutoComplete/FsAutoComplete.Lsp.fs @@ -289,7 +289,8 @@ type FSharpLspServer(backgroundServiceEnabled: bool, state: State, lspClient: FS lintFixes.[uri] <- fs let diags = - warnings |> List.map(fun w -> + warnings + |> List.map(fun w -> // ideally we'd be able to include a clickable link to the docs page for this errorlint code, but that is not the case here // neither the Message or the RelatedInformation structures support markdown. let range = fcsRangeToLsp w.Warning.Details.Range @@ -301,6 +302,7 @@ type FSharpLspServer(backgroundServiceEnabled: bool, state: State, lspClient: FS RelatedInformation = None Tags = None } ) + |> List.sortBy (fun diag -> diag.Range) |> List.toArray diagnosticCollections.SetFor(uri, "F# linter", diags)