diff --git a/paket.dependencies b/paket.dependencies index 6fd2c8b24..13c174a37 100644 --- a/paket.dependencies +++ b/paket.dependencies @@ -51,6 +51,7 @@ nuget Dotnet.ReproducibleBuilds copy_local:true nuget Microsoft.NETFramework.ReferenceAssemblies nuget Ionide.KeepAChangelog.Tasks copy_local: true nuget Expecto +nuget Expecto.Diff nuget YoloDev.Expecto.TestSdk nuget AltCover nuget GitHubActionsTestLogger diff --git a/paket.lock b/paket.lock index 21f4ff844..13597c1f1 100644 --- a/paket.lock +++ b/paket.lock @@ -13,6 +13,7 @@ NUGET Destructurama.FSharp (1.2) FSharp.Core (>= 4.3.4) Serilog (>= 2.0 < 3.0) + DiffPlex (1.7.1) DotNet.ReproducibleBuilds (1.1.1) - copy_local: true Microsoft.SourceLink.AzureRepos.Git (>= 1.1.1) Microsoft.SourceLink.Bitbucket.Git (>= 1.1.1) @@ -21,6 +22,10 @@ NUGET Expecto (9.0.4) FSharp.Core (>= 4.6) Mono.Cecil (>= 0.11.3) + Expecto.Diff (9.0.4) + DiffPlex (>= 1.6.3) + Expecto (>= 9.0.4) + FSharp.Core (>= 4.6) Fantomas.Client (0.5.1) FSharp.Core (>= 5.0) SemanticVersioning (>= 2.0) @@ -94,10 +99,11 @@ NUGET System.Collections.Immutable (>= 5.0) System.Reflection.Metadata (>= 5.0) Ionide.KeepAChangelog.Tasks (0.1.8) - copy_local: true - Ionide.LanguageServerProtocol (0.3.1) + Ionide.LanguageServerProtocol (0.4) FSharp.Core (>= 6.0.1) Microsoft.NETFramework.ReferenceAssemblies (>= 1.0.2) Newtonsoft.Json (>= 13.0.1) + StreamJsonRpc (>= 2.10.44) Ionide.ProjInfo (0.59.1) FSharp.Core (>= 6.0.3) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net6.0)) Ionide.ProjInfo.Sln (>= 0.59.1) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net6.0)) diff --git a/src/FsAutoComplete.BackgroundServices/Program.fs b/src/FsAutoComplete.BackgroundServices/Program.fs index 87d7e5f39..9c380f0ac 100644 --- a/src/FsAutoComplete.BackgroundServices/Program.fs +++ b/src/FsAutoComplete.BackgroundServices/Program.fs @@ -3,7 +3,6 @@ open System open System.IO open System.Text -open Ionide.LanguageServerProtocol.Server open Ionide.LanguageServerProtocol.Types open Ionide.LanguageServerProtocol open FSharp.Compiler @@ -18,501 +17,590 @@ open System.Reactive.Linq open FSharp.Compiler.Text type BackgroundFileCheckType = -| SourceFile of filePath: string -| ScriptFile of filePath: string * tfm: FSIRefs.TFM -with - member x.FilePath = - match x with - | SourceFile(path) - | ScriptFile(path, _) -> path + | SourceFile of filePath: string + | ScriptFile of filePath: string * tfm: FSIRefs.TFM + member x.FilePath = + match x with + | SourceFile (path) + | ScriptFile (path, _) -> path -type UpdateFileParms = { - File: BackgroundFileCheckType +type UpdateFileParms = + { File: BackgroundFileCheckType Content: string - Version: int -} + Version: int } -type ProjectParms = { - Options: FSharpProjectOptions +type ProjectParms = + { Options: FSharpProjectOptions ReferencedProjects: string array - File: string -} + File: string } -type FileParms = { - File: BackgroundFileCheckType -} +type FileParms = { File: BackgroundFileCheckType } -type InitParms = { - Ready: bool -} +type InitParms = { Ready: bool } -type Msg = {Value: string} +type Msg = { Value: string } -type State = { - Files : ConcurrentDictionary, VolatileFile> - FileCheckOptions : ConcurrentDictionary, FSharpProjectOptions> -} +type State = + { Files: ConcurrentDictionary, VolatileFile> + FileCheckOptions: ConcurrentDictionary, FSharpProjectOptions> } -with - static member Initial = - { Files = ConcurrentDictionary(); FileCheckOptions = ConcurrentDictionary() } + static member Initial = + { Files = ConcurrentDictionary() + FileCheckOptions = ConcurrentDictionary() } module Helpers = - let fcsSeverityToDiagnostic = function - | FSharpDiagnosticSeverity.Error -> Some DiagnosticSeverity.Error - | FSharpDiagnosticSeverity.Warning -> Some DiagnosticSeverity.Warning - | FSharpDiagnosticSeverity.Hidden -> None - | FSharpDiagnosticSeverity.Info -> Some DiagnosticSeverity.Information - - let urlForCompilerCode (number: int) = - $"https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/compiler-messages/fs%04d{number}" - - let fcsErrorToDiagnostic (error: FSharpDiagnostic) = - { - Range = - { - Start = { Line = error.StartLine - 1; Character = error.StartColumn } - End = { Line = error.StartLine - 1; Character = error.EndColumn } - } - Severity = fcsSeverityToDiagnostic error.Severity - Source = "F# Compiler" - Message = error.Message - Code = Some (string error.ErrorNumber) - RelatedInformation = Some [||] - Tags = None - Data = None - CodeDescription = Some { - Href = Some (Uri (urlForCompilerCode error.ErrorNumber)) - } - } - - /// Algorithm from https://stackoverflow.com/a/35734486/433393 for converting file paths to uris, - /// modified slightly to not rely on the System.Path members because they vary per-platform - let filePathToUri (filePath: string): DocumentUri = - let filePath = UMX.untag filePath - let uri = StringBuilder(filePath.Length) - for c in filePath do - if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || - c = '+' || c = '/' || c = '.' || c = '-' || c = '_' || c = '~' || - c > '\xFF' then - uri.Append(c) |> ignore - // handle windows path separator chars. - // we _would_ use Path.DirectorySeparator/AltDirectorySeparator, but those vary per-platform and we want this - // logic to work cross-platform (for tests) - else if c = '\\' then - uri.Append('/') |> ignore - else - uri.Append('%') |> ignore - uri.Append((int c).ToString("X2")) |> ignore - - if uri.Length >= 2 && uri.[0] = '/' && uri.[1] = '/' then // UNC path - "file:" + uri.ToString() - else - "file:///" + (uri.ToString()).TrimStart('/') - - -type FsacClient(sendServerNotification: ClientNotificationSender, sendServerRequest: ClientRequestSender) = - inherit LspClient () - - member __.SendDiagnostics(p: PublishDiagnosticsParams) = - sendServerNotification "background/diagnostics" (box p) |> Async.Ignore - - member __.Notify(o: Msg) = - sendServerNotification "background/notify" o |> Async.Ignore + let fcsSeverityToDiagnostic = + function + | FSharpDiagnosticSeverity.Error -> Some DiagnosticSeverity.Error + | FSharpDiagnosticSeverity.Warning -> Some DiagnosticSeverity.Warning + | FSharpDiagnosticSeverity.Hidden -> None + | FSharpDiagnosticSeverity.Info -> Some DiagnosticSeverity.Information + + let urlForCompilerCode (number: int) = + $"https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/compiler-messages/fs%04d{number}" + + let fcsErrorToDiagnostic (error: FSharpDiagnostic) = + { Range = + { Start = + { Line = error.StartLine - 1 + Character = error.StartColumn } + End = + { Line = error.StartLine - 1 + Character = error.EndColumn } } + Severity = fcsSeverityToDiagnostic error.Severity + Source = "F# Compiler" + Message = error.Message + Code = Some(string error.ErrorNumber) + RelatedInformation = Some [||] + Tags = None + Data = None + CodeDescription = Some { Href = Some(Uri(urlForCompilerCode error.ErrorNumber)) } } + + /// Algorithm from https://stackoverflow.com/a/35734486/433393 for converting file paths to uris, + /// modified slightly to not rely on the System.Path members because they vary per-platform + let filePathToUri (filePath: string) : DocumentUri = + let filePath = UMX.untag filePath + let uri = StringBuilder(filePath.Length) + + for c in filePath do + if (c >= 'a' && c <= 'z') + || (c >= 'A' && c <= 'Z') + || (c >= '0' && c <= '9') + || c = '+' + || c = '/' + || c = '.' + || c = '-' + || c = '_' + || c = '~' + || c > '\xFF' then + uri.Append(c) |> ignore + // handle windows path separator chars. + // we _would_ use Path.DirectorySeparator/AltDirectorySeparator, but those vary per-platform and we want this + // logic to work cross-platform (for tests) + else if c = '\\' then + uri.Append('/') |> ignore + else + uri.Append('%') |> ignore + uri.Append((int c).ToString("X2")) |> ignore + + if uri.Length >= 2 && uri.[0] = '/' && uri.[1] = '/' then // UNC path + "file:" + uri.ToString() + else + "file:///" + (uri.ToString()).TrimStart('/') + + +type FsacClient(sendServerNotification: Server.ClientNotificationSender, sendServerRequest: Server.ClientRequestSender) = + inherit LspClient() + + member __.SendDiagnostics(p: PublishDiagnosticsParams) = + sendServerNotification "background/diagnostics" (box p) + |> Async.Ignore + + member __.Notify(o: Msg) = + sendServerNotification "background/notify" o + |> Async.Ignore type BackgroundServiceServer(state: State, client: FsacClient) = - inherit LspServer() - - let checker = FSharpChecker.Create(projectCacheSize = 1, keepAllBackgroundResolutions = false, suggestNamesForErrors = false) - let mutable isWorkspaceReady = false - - let mutable latestSdkVersion = lazy None - let mutable latestRuntimeVersion = lazy None - //TODO: does the backgroundservice ever get config updates? - do - let allowedVersionRange = - let maxVersion = System.Environment.Version.Major + 1 - SemanticVersioning.Range.Parse $"< %d{maxVersion}" - let dotnetExe = Ionide.ProjInfo.Paths.dotnetRoot.Value - match dotnetExe with - | Some dotnetExe when dotnetExe.Exists -> - let sdk = lazy ( - Ionide.ProjInfo.SdkDiscovery.sdks dotnetExe - |> Array.map (fun info -> info.Version) - |> Environment.maxVersionWithThreshold (Some allowedVersionRange) true - ) - let runtime = lazy ( - Ionide.ProjInfo.SdkDiscovery.runtimes dotnetExe - |> Array.map (fun info -> info.Version) - |> Environment.maxVersionWithThreshold (Some allowedVersionRange) true + inherit LspServer() + + let checker = + FSharpChecker.Create(projectCacheSize = 1, keepAllBackgroundResolutions = false, suggestNamesForErrors = false) + + let mutable isWorkspaceReady = false + + let mutable latestSdkVersion = lazy None + let mutable latestRuntimeVersion = lazy None + //TODO: does the backgroundservice ever get config updates? + do + let allowedVersionRange = + let maxVersion = System.Environment.Version.Major + 1 + SemanticVersioning.Range.Parse $"< %d{maxVersion}" + + let dotnetExe = Ionide.ProjInfo.Paths.dotnetRoot.Value + + match dotnetExe with + | Some dotnetExe when dotnetExe.Exists -> + let sdk = + lazy + (Ionide.ProjInfo.SdkDiscovery.sdks dotnetExe + |> Array.map (fun info -> info.Version) + |> Environment.maxVersionWithThreshold (Some allowedVersionRange) true) + + let runtime = + lazy + (Ionide.ProjInfo.SdkDiscovery.runtimes dotnetExe + |> Array.map (fun info -> info.Version) + |> Environment.maxVersionWithThreshold (Some allowedVersionRange) true) + + latestSdkVersion <- sdk + latestRuntimeVersion <- runtime + | Some exe -> + let message = + $"A dotnet binary was located at '{exe.FullName}' but doesn't exist. This is...nonsensical to say the least." + + failwith message + | None -> failwith "No dotnet binary could be located" + + let getFilesFromOpts (opts: FSharpProjectOptions) = + (if Array.isEmpty opts.SourceFiles then + opts.OtherOptions + |> Seq.where (fun n -> + not (n.StartsWith "-") + && (n.EndsWith ".fs" || n.EndsWith ".fsi")) + |> Seq.toArray + else + opts.SourceFiles) + |> Array.map Utils.normalizePath + + let getListOfFilesForProjectChecking (file: BackgroundFileCheckType) = + let replaceRefs (projOptions: FSharpProjectOptions) = + let okOtherOpts = + projOptions.OtherOptions + |> Array.filter (fun r -> not <| r.StartsWith("-r")) + + let assemblyPaths = + match latestSdkVersion.Value, latestRuntimeVersion.Value with + | None, _ + | _, None -> [] + | Some sdkVersion, Some runtimeVersion -> + FSIRefs.netCoreRefs + Environment.dotnetSDKRoot.Value + (string sdkVersion) + (string runtimeVersion) + (FSIRefs.tfmForRuntime sdkVersion) + true + + let refs = assemblyPaths |> List.map (fun r -> "-r:" + r) + let finalOpts = Array.append okOtherOpts (Array.ofList refs) + { projOptions with OtherOptions = finalOpts } + + let getScriptOptions file text tfm = + match tfm with + | FSIRefs.NetFx -> + checker.GetProjectOptionsFromScript( + file, + text, + assumeDotNetFramework = true, + useSdkRefs = false, + useFsiAuxLib = true ) + | FSIRefs.NetCore -> + async { + let! (opts, errors) = + checker.GetProjectOptionsFromScript( + file, + text, + assumeDotNetFramework = false, + useSdkRefs = true, + useFsiAuxLib = true + ) + + return replaceRefs opts, errors + } - latestSdkVersion <- sdk - latestRuntimeVersion <- runtime - | Some exe -> - let message = $"A dotnet binary was located at '{exe.FullName}' but doesn't exist. This is...nonsensical to say the least." - failwith message + match file with + | ScriptFile (file, tfm) -> + state.Files.TryFind(Utils.normalizePath file) + |> Option.map (fun st -> + async { + let! (opts, _errors) = getScriptOptions file st.Lines tfm + let sf = getFilesFromOpts opts + + return + sf + |> Array.skipWhile (fun n -> n <> (Utils.normalizePath file)) + |> Array.toList + }) + | SourceFile file -> + match state.FileCheckOptions.TryFind(Utils.normalizePath file) with | None -> - failwith "No dotnet binary could be located" - - let getFilesFromOpts (opts: FSharpProjectOptions) = - (if Array.isEmpty opts.SourceFiles then - opts.OtherOptions - |> Seq.where (fun n -> not (n.StartsWith "-") && (n.EndsWith ".fs" || n.EndsWith ".fsi") ) - |> Seq.toArray - else - opts.SourceFiles) - |> Array.map Utils.normalizePath - - let getListOfFilesForProjectChecking (file: BackgroundFileCheckType) = - let replaceRefs (projOptions: FSharpProjectOptions) = - let okOtherOpts = projOptions.OtherOptions |> Array.filter (fun r -> not <| r.StartsWith("-r")) - let assemblyPaths = - match latestSdkVersion.Value, latestRuntimeVersion.Value with - | None, _ - | _, None -> - [] - | Some sdkVersion, Some runtimeVersion -> - FSIRefs.netCoreRefs Environment.dotnetSDKRoot.Value (string sdkVersion) (string runtimeVersion) (FSIRefs.tfmForRuntime sdkVersion) true - let refs = assemblyPaths |> List.map (fun r -> "-r:" + r) - let finalOpts = Array.append okOtherOpts (Array.ofList refs) - { projOptions with OtherOptions = finalOpts } - - let getScriptOptions file text tfm = - match tfm with - | FSIRefs.NetFx -> - checker.GetProjectOptionsFromScript(file, text, assumeDotNetFramework = true, useSdkRefs = false, useFsiAuxLib = true) - | FSIRefs.NetCore -> - async { - let! (opts, errors) = checker.GetProjectOptionsFromScript(file, text, assumeDotNetFramework = false, useSdkRefs = true, useFsiAuxLib = true) - return replaceRefs opts, errors - } - - match file with - | ScriptFile(file, tfm) -> - state.Files.TryFind (Utils.normalizePath file) |> Option.map (fun st -> - async { - let! (opts, _errors) = getScriptOptions file st.Lines tfm - let sf = getFilesFromOpts opts - - return - sf - |> Array.skipWhile (fun n -> n <> (Utils.normalizePath file)) - |> Array.toList - } - ) - | SourceFile file -> - match state.FileCheckOptions.TryFind (Utils.normalizePath file) with - | None -> - client.Notify {Value = sprintf "Couldn't find file check options for %A" file } |> Async.Start - client.Notify {Value = sprintf "Known files %A" (state.FileCheckOptions.Keys |> Seq.toArray) } |> Async.Start - None - | Some opts -> - let sf = getFilesFromOpts opts - - sf - |> Array.skipWhile (fun n -> n <> (Utils.normalizePath file)) - |> Array.toList - |> async.Return - |> Some - - let typecheckProject (project: FSharpProjectOptions) = - async { - do! client.Notify {Value = sprintf "Project - Typechecking %s" project.ProjectFileName } - let! projectResult = checker.ParseAndCheckProject(project) - do! client.Notify {Value = sprintf "Project - Typechecking project %s done" project.ProjectFileName } - - projectResult.Diagnostics - |> Array.groupBy (fun d -> d.FileName) - |> Array.iter (fun (file, diags) -> - let diags = - diags - |> Array.map Helpers.fcsErrorToDiagnostic - let uri = file |> Utils.normalizePath |> Helpers.filePathToUri + client.Notify { Value = sprintf "Couldn't find file check options for %A" file } + |> Async.Start + + client.Notify { Value = sprintf "Known files %A" (state.FileCheckOptions.Keys |> Seq.toArray) } + |> Async.Start + + None + | Some opts -> + let sf = getFilesFromOpts opts + + sf + |> Array.skipWhile (fun n -> n <> (Utils.normalizePath file)) + |> Array.toList + |> async.Return + |> Some + + let typecheckProject (project: FSharpProjectOptions) = + async { + do! client.Notify { Value = sprintf "Project - Typechecking %s" project.ProjectFileName } + let! projectResult = checker.ParseAndCheckProject(project) + do! client.Notify { Value = sprintf "Project - Typechecking project %s done" project.ProjectFileName } + + projectResult.Diagnostics + |> Array.groupBy (fun d -> d.FileName) + |> Array.iter (fun (file, diags) -> + let diags = diags |> Array.map Helpers.fcsErrorToDiagnostic + + let uri = + file + |> Utils.normalizePath + |> Helpers.filePathToUri + + async { + do! client.Notify { Value = sprintf "Project - Sending diagnostics %s" (UMX.untag file) } + do! client.SendDiagnostics { Uri = uri; Diagnostics = diags } + } + |> Async.Start + + ()) + + projectResult.GetAllUsesOfAllSymbols() + |> Array.groupBy (fun d -> d.FileName) + |> Array.iter (fun (file, symbols) -> + async { + let file = Utils.normalizePath file + + do! + client.Notify + { Value = sprintf "Project - Got symbols for file %s - %d" (UMX.untag file) (symbols |> Seq.length) } + + SymbolCache.updateSymbols file symbols + } + |> Async.Start) + + () + } + + let typecheckFile ignoredFile (file: string) = + async { + do! client.Notify { Value = sprintf "Typechecking %s" (UMX.untag file) } + + match state.Files.TryFind file, state.FileCheckOptions.TryFind file with + | Some vf, Some opts -> + let txt = vf.Lines + let! pr, cr = checker.ParseAndCheckFileInProject(UMX.untag file, defaultArg vf.Version 0, txt, opts) + + match cr with + | FSharpCheckFileAnswer.Aborted -> + do! client.Notify { Value = sprintf "Typechecking aborted %s" (UMX.untag file) } + return () + | FSharpCheckFileAnswer.Succeeded res -> + do! client.Notify { Value = sprintf "Typechecking successful %s" (UMX.untag file) } + async { - do! client.Notify {Value = sprintf "Project - Sending diagnostics %s" (UMX.untag file) } - do! client.SendDiagnostics { Uri = uri; Diagnostics = diags } + let symbols = res.GetAllUsesOfAllSymbolsInFile() + + do! + client.Notify { Value = sprintf "Got symbols for file %s - %d" (UMX.untag file) (symbols |> Seq.length) } + + SymbolCache.updateSymbols file symbols } |> Async.Start - () - ) - projectResult.GetAllUsesOfAllSymbols() - |> Array.groupBy (fun d -> d.FileName) - |> Array.iter (fun (file, symbols) -> + match ignoredFile with + | Some fn when fn = file -> return () + | _ -> + let errors = + Array.append pr.Diagnostics res.Diagnostics + |> Array.map (Helpers.fcsErrorToDiagnostic) + + let msg = + { Diagnostics = errors + Uri = Helpers.filePathToUri file } + + do! client.Notify { Value = sprintf "Sending diagnostics %s" (UMX.untag file) } + do! client.SendDiagnostics msg + return () + | Some vf, None when (UMX.untag file).EndsWith ".fsx" -> + let txt = vf.Lines + + let! (opts, _errors) = + checker.GetProjectOptionsFromScript(UMX.untag file, txt, assumeDotNetFramework = true, useSdkRefs = false) + + let! pr, cr = checker.ParseAndCheckFileInProject(UMX.untag file, defaultArg vf.Version 0, txt, opts) + + match cr with + | FSharpCheckFileAnswer.Aborted -> + do! client.Notify { Value = sprintf "Typechecking aborted %s" (UMX.untag file) } + return () + | FSharpCheckFileAnswer.Succeeded res -> async { - let file = Utils.normalizePath file - do! client.Notify {Value = sprintf "Project - Got symbols for file %s - %d" (UMX.untag file) (symbols |> Seq.length) } + let symbols = res.GetAllUsesOfAllSymbolsInFile() SymbolCache.updateSymbols file symbols } |> Async.Start - ) - () - } - let typecheckFile ignoredFile (file: string) = + match ignoredFile with + | Some fn when fn = file -> return () + | _ -> + let errors = + Array.append pr.Diagnostics res.Diagnostics + |> Array.map (Helpers.fcsErrorToDiagnostic) + + let msg = + { Diagnostics = errors + Uri = Helpers.filePathToUri file } + + do! client.SendDiagnostics msg + return () + | _ -> + do! client.Notify { Value = sprintf "Couldn't find state %s" (UMX.untag file) } + return () + } + + let getDependingProjects (file: string) = + let project = state.FileCheckOptions.TryFind file + + match project with + | None -> [] + | Some s -> + state.FileCheckOptions + |> Seq.map (fun kv -> kv.Value) + |> Seq.distinctBy (fun o -> o.ProjectFileName) + |> Seq.filter (fun o -> + o.ReferencedProjects + |> Array.map (fun p -> Path.GetFullPath p.OutputFile) + |> Array.contains s.ProjectFileName) + |> Seq.toList + + let reactor = + MailboxProcessor.Start (fun agent -> + let rec recieveLast last = async { - do! client.Notify {Value = sprintf "Typechecking %s" (UMX.untag file) } - match state.Files.TryFind file, state.FileCheckOptions.TryFind file with - | Some vf, Some opts -> - let txt = vf.Lines - let! pr, cr = checker.ParseAndCheckFileInProject(UMX.untag file, defaultArg vf.Version 0, txt, opts) - match cr with - | FSharpCheckFileAnswer.Aborted -> - do! client.Notify {Value = sprintf "Typechecking aborted %s" (UMX.untag file) } - return () - | FSharpCheckFileAnswer.Succeeded res -> - do! client.Notify {Value = sprintf "Typechecking successful %s" (UMX.untag file) } - async { - let symbols = res.GetAllUsesOfAllSymbolsInFile() - do! client.Notify {Value = sprintf "Got symbols for file %s - %d" (UMX.untag file) (symbols |> Seq.length) } - SymbolCache.updateSymbols file symbols - } |> Async.Start - match ignoredFile with - | Some fn when fn = file -> return () - | _ -> - let errors = Array.append pr.Diagnostics res.Diagnostics |> Array.map (Helpers.fcsErrorToDiagnostic) - let msg = {Diagnostics = errors; Uri = Helpers.filePathToUri file} - do! client.Notify {Value = sprintf "Sending diagnostics %s" (UMX.untag file) } - do! client.SendDiagnostics msg - return () - | Some vf, None when (UMX.untag file).EndsWith ".fsx" -> - let txt = vf.Lines - let! (opts, _errors) = checker.GetProjectOptionsFromScript(UMX.untag file, txt, assumeDotNetFramework = true, useSdkRefs = false) - let! pr, cr = checker.ParseAndCheckFileInProject(UMX.untag file, defaultArg vf.Version 0, txt, opts) - match cr with - | FSharpCheckFileAnswer.Aborted -> - do! client.Notify {Value = sprintf "Typechecking aborted %s" (UMX.untag file) } - return () - | FSharpCheckFileAnswer.Succeeded res -> - async { - let symbols = res.GetAllUsesOfAllSymbolsInFile() - SymbolCache.updateSymbols file symbols - } |> Async.Start - match ignoredFile with - | Some fn when fn = file -> return () - | _ -> - let errors = Array.append pr.Diagnostics res.Diagnostics |> Array.map (Helpers.fcsErrorToDiagnostic) - let msg = {Diagnostics = errors; Uri = Helpers.filePathToUri file} - do! client.SendDiagnostics msg - return () - | _ -> - do! client.Notify {Value = sprintf "Couldn't find state %s" (UMX.untag file) } - return () - } + let! msg = agent.TryReceive(5) - let getDependingProjects (file: string) = - let project = state.FileCheckOptions.TryFind file - match project with - | None -> [] - | Some s -> - state.FileCheckOptions - |> Seq.map (fun kv -> kv.Value) - |> Seq.distinctBy (fun o -> o.ProjectFileName) - |> Seq.filter (fun o -> - o.ReferencedProjects - |> Array.map (fun p -> Path.GetFullPath p.OutputFile) - |> Array.contains s.ProjectFileName) - |> Seq.toList - - let reactor = MailboxProcessor.Start(fun agent -> - let rec recieveLast last = - async { - let! msg = agent.TryReceive(5) - match msg with - | Some s -> - return! recieveLast (Some s) - | None -> - return last - } - - let rec loop (isFromSave,lst) = async { - let! msg = recieveLast None - match msg, lst with - //Empty - | None, [] -> - - // checker.ClearLanguageServiceRootCachesAndCollectAndFinalizeAllTransients () - do! Async.Sleep 300 - return! loop (false, []) - //Empty - | Some (_,_,[]), [] -> - // checker.ClearLanguageServiceRootCachesAndCollectAndFinalizeAllTransients () - do! Async.Sleep 300 - return! loop (false, []) - //No request we just continue - | None, x::xs -> - do! typecheckFile None x - return! loop (isFromSave, xs) - - //We've ended processing request we start new - | Some(saveRequest,fn, x::xs), [] -> - do! typecheckFile (Some fn) x - return! loop (saveRequest, xs) - - //If incoming is normal update and current is from save request we continue current - | Some (false,_,_), x::xs when isFromSave -> - do! typecheckFile None x - return! loop (isFromSave, xs) - - //If incoming is normal and previous was normal - | Some (false,fn,(x::xs)), _ -> - do! typecheckFile (Some fn) x - return! loop (false, xs) - - //If incoming request is from save we always start it - | Some(true, fn, (x::xs)), _ -> - do! typecheckFile (Some fn) x - return! loop (true, xs) - - //If incoming request doesn't contain any list we just continue previous one - | Some (_,fn,[]), x::xs -> - do! typecheckFile (Some fn) x - return! loop (isFromSave, xs) + match msg with + | Some s -> return! recieveLast (Some s) + | None -> return last } - loop (false, []) - ) - - let bouncer = Debounce(200, reactor.Post) - - let clearOldFilesFromCache () = - async { - let! files = SymbolCache.getKnownFiles () - match files with - | None -> () - | Some files -> - for f in (Seq.distinct files) do - if File.Exists f.FileName then () - else - let! _ = SymbolCache.deleteFile f.FileName - do! client.Notify { Value = sprintf "Cleaned file %s" f.FileName } - () - } - - let clearOldCacheSubscription = - Observable.Interval(TimeSpan.FromMinutes(5.)) - |> Observable.subscribe(fun _ -> clearOldFilesFromCache () |> Async.Start) - - member __.UpdateTextFile(p: UpdateFileParms) = + let rec loop (isFromSave, lst) = async { - do! client.Notify {Value = sprintf "File update %s" p.File.FilePath } - let file = Utils.normalizePath p.File.FilePath - - let vf = - { Lines = NamedText(file, p.Content) - Touched = DateTime.Now - Version = Some p.Version } - state.Files.AddOrUpdate(file, (fun _ -> vf),( fun _ _ -> vf) ) |> ignore - let! filesToCheck = defaultArg (getListOfFilesForProjectChecking p.File) (async.Return []) - if isWorkspaceReady then - do! client.Notify { Value = sprintf "Files to check %A" filesToCheck } - bouncer.Bounce (false, file,filesToCheck) - return LspResult.success () + let! msg = recieveLast None + + match msg, lst with + //Empty + | None, [] -> + + // checker.ClearLanguageServiceRootCachesAndCollectAndFinalizeAllTransients () + do! Async.Sleep 300 + return! loop (false, []) + //Empty + | Some (_, _, []), [] -> + // checker.ClearLanguageServiceRootCachesAndCollectAndFinalizeAllTransients () + do! Async.Sleep 300 + return! loop (false, []) + //No request we just continue + | None, x :: xs -> + do! typecheckFile None x + return! loop (isFromSave, xs) + + //We've ended processing request we start new + | Some (saveRequest, fn, x :: xs), [] -> + do! typecheckFile (Some fn) x + return! loop (saveRequest, xs) + + //If incoming is normal update and current is from save request we continue current + | Some (false, _, _), x :: xs when isFromSave -> + do! typecheckFile None x + return! loop (isFromSave, xs) + + //If incoming is normal and previous was normal + | Some (false, fn, (x :: xs)), _ -> + do! typecheckFile (Some fn) x + return! loop (false, xs) + + //If incoming request is from save we always start it + | Some (true, fn, (x :: xs)), _ -> + do! typecheckFile (Some fn) x + return! loop (true, xs) + + //If incoming request doesn't contain any list we just continue previous one + | Some (_, fn, []), x :: xs -> + do! typecheckFile (Some fn) x + return! loop (isFromSave, xs) } - member __.UpdateProject(p: ProjectParms) = - async { + loop (false, [])) - let knowOptions = - state.FileCheckOptions.Values - - let refs = p.ReferencedProjects |> Array.choose (fun p -> - knowOptions - |> Seq.tryFind (fun o -> - //This is bad check, but no idea how to do it better - let outputOpt = - o.OtherOptions - |> Seq.find (fun oo -> oo.StartsWith "-o:") - let outputDll = outputOpt.Substring(3).Split('\\') |> Array.last - let refDll = p.Split('\\') |> Array.last - outputDll = refDll - ) - |> Option.map (fun opt -> - FSharpReferencedProject.CreateFSharp(p, opt) - ) - ) - let options = {p.Options with ReferencedProjects = refs} + let bouncer = Debounce(200, reactor.Post) + let clearOldFilesFromCache () = + async { + let! files = SymbolCache.getKnownFiles () - let sf = getFilesFromOpts options + match files with + | None -> () + | Some files -> + for f in (Seq.distinct files) do + if File.Exists f.FileName then + () + else + let! _ = SymbolCache.deleteFile f.FileName + do! client.Notify { Value = sprintf "Cleaned file %s" f.FileName } + () + } - sf - |> Seq.iter (fun file -> - state.FileCheckOptions.AddOrUpdate(file, (fun _ -> options), (fun _ _ -> options)) - |> ignore - ) - do! client.Notify {Value = sprintf "Project Updated %s, references: %d" options.ProjectFileName options.ReferencedProjects.Length} - if isWorkspaceReady then - do! client.Notify { Value = sprintf "Files to check from project update %A" sf } - bouncer.Bounce (false, sf |> Array.last,sf |> List.ofArray) - return LspResult.success () - } + let clearOldCacheSubscription = + Observable.Interval(TimeSpan.FromMinutes(5.)) + |> Observable.subscribe (fun _ -> clearOldFilesFromCache () |> Async.Start) - member __.FileSaved(p: FileParms) = - async { + member __.UpdateTextFile(p: UpdateFileParms) = + async { + do! client.Notify { Value = sprintf "File update %s" p.File.FilePath } + let file = Utils.normalizePath p.File.FilePath - let file = Utils.normalizePath p.File.FilePath + let vf = + { Lines = NamedText(file, p.Content) + Touched = DateTime.Now + Version = Some p.Version } - do! client.Notify {Value = sprintf "File Saved %s " (UMX.untag file) } + state.Files.AddOrUpdate(file, (fun _ -> vf), (fun _ _ -> vf)) + |> ignore - let projects = getDependingProjects file - let! filesToCheck = defaultArg (getListOfFilesForProjectChecking p.File) (async.Return []) - let filesToCheck = - [ - yield! filesToCheck - yield! projects |> Seq.collect getFilesFromOpts - ] - if isWorkspaceReady then - bouncer.Bounce (true, file,filesToCheck) - return LspResult.success () - } + let! filesToCheck = defaultArg (getListOfFilesForProjectChecking p.File) (async.Return []) - member __.InitWorkspace(workspaceStateDir) = - async { - do! client.Notify {Value = "Init workspace" } - SymbolCache.initCache workspaceStateDir - do! Async.Sleep 100 - let knownProjects = state.FileCheckOptions.Values |> Seq.distinctBy (fun o -> o.ProjectFileName) - do! client.Notify {Value = sprintf "Init workspace - starting typechecking on %d projects" (knownProjects |> Seq.length) } - knownProjects - |> Seq.iter (fun opts -> typecheckProject opts |> Async.Start) - do! client.Notify {Value = "Init workspace completed" } - isWorkspaceReady <- true - return LspResult.success () - } + if isWorkspaceReady then + do! client.Notify { Value = sprintf "Files to check %A" filesToCheck } + bouncer.Bounce(false, file, filesToCheck) - override _.Dispose () = - clearOldCacheSubscription.Dispose() + return LspResult.success () + } + member __.UpdateProject(p: ProjectParms) = + async { -module Program = - let state = State.Initial + let knowOptions = state.FileCheckOptions.Values + + let refs = + p.ReferencedProjects + |> Array.choose (fun p -> + knowOptions + |> Seq.tryFind (fun o -> + //This is bad check, but no idea how to do it better + let outputOpt = + o.OtherOptions + |> Seq.find (fun oo -> oo.StartsWith "-o:") + + let outputDll = outputOpt.Substring(3).Split('\\') |> Array.last + let refDll = p.Split('\\') |> Array.last + outputDll = refDll) + |> Option.map (fun opt -> FSharpReferencedProject.CreateFSharp(p, opt))) + + let options = { p.Options with ReferencedProjects = refs } - let startCore () = - use input = Console.OpenStandardInput() - use output = Console.OpenStandardOutput() - let requestsHandlings = - Map.empty> - |> Map.add "background/update" (requestHandling (fun s p -> s.UpdateTextFile(p) )) - |> Map.add "background/project" (requestHandling (fun s p -> s.UpdateProject(p) )) - |> Map.add "background/save" (requestHandling (fun s p -> s.FileSaved(p) )) - |> Map.add "background/init" (requestHandling (fun s p -> s.InitWorkspace p)) + let sf = getFilesFromOpts options - Ionide.LanguageServerProtocol.Server.start requestsHandlings input output FsacClient (fun lspClient -> new BackgroundServiceServer(state, lspClient)) + sf + |> Seq.iter (fun file -> + state.FileCheckOptions.AddOrUpdate(file, (fun _ -> options), (fun _ _ -> options)) + |> ignore) - open FSharp.Compiler.IO + do! + client.Notify + { Value = + sprintf "Project Updated %s, references: %d" options.ProjectFileName options.ReferencedProjects.Length } + if isWorkspaceReady then + do! client.Notify { Value = sprintf "Files to check from project update %A" sf } + bouncer.Bounce(false, sf |> Array.last, sf |> List.ofArray) + return LspResult.success () + } - [] - let main argv = - let pid= Int32.Parse argv[0] - let originalFs = FileSystemAutoOpens.FileSystem - let fs = FsAutoComplete.FileSystem(originalFs, state.Files.TryFind) :> IFileSystem - FileSystemAutoOpens.FileSystem <- fs - ProcessWatcher.zombieCheckWithHostPID (fun () -> exit 0) pid - let _ = startCore() - 0 + member __.FileSaved(p: FileParms) = + async { + + let file = Utils.normalizePath p.File.FilePath + + do! client.Notify { Value = sprintf "File Saved %s " (UMX.untag file) } + + let projects = getDependingProjects file + let! filesToCheck = defaultArg (getListOfFilesForProjectChecking p.File) (async.Return []) + + let filesToCheck = + [ yield! filesToCheck + yield! projects |> Seq.collect getFilesFromOpts ] + + if isWorkspaceReady then + bouncer.Bounce(true, file, filesToCheck) + + return LspResult.success () + } + + member __.InitWorkspace(workspaceStateDir) = + async { + do! client.Notify { Value = "Init workspace" } + SymbolCache.initCache workspaceStateDir + do! Async.Sleep 100 + + let knownProjects = + state.FileCheckOptions.Values + |> Seq.distinctBy (fun o -> o.ProjectFileName) + + do! + client.Notify + { Value = sprintf "Init workspace - starting typechecking on %d projects" (knownProjects |> Seq.length) } + + knownProjects + |> Seq.iter (fun opts -> typecheckProject opts |> Async.Start) + + do! client.Notify { Value = "Init workspace completed" } + isWorkspaceReady <- true + return LspResult.success () + } + + override _.Dispose() = clearOldCacheSubscription.Dispose() + +module Program = + open Ionide.LanguageServerProtocol.Server + + let state = State.Initial + + let startCore () = + use input = Console.OpenStandardInput() + use output = Console.OpenStandardOutput() + + let requestsHandlings = + Map.empty> + |> Map.add "background/update" (serverRequestHandling (fun s p -> s.UpdateTextFile(p))) + |> Map.add "background/project" (serverRequestHandling (fun s p -> s.UpdateProject(p))) + |> Map.add "background/save" (serverRequestHandling (fun s p -> s.FileSaved(p))) + |> Map.add "background/init" (serverRequestHandling (fun s p -> s.InitWorkspace p)) + + Ionide.LanguageServerProtocol.Server.start requestsHandlings input output FsacClient (fun lspClient -> + new BackgroundServiceServer(state, lspClient)) + + open FSharp.Compiler.IO + + [] + let main argv = + let pid = Int32.Parse argv[0] + let originalFs = FileSystemAutoOpens.FileSystem + let fs = FsAutoComplete.FileSystem(originalFs, state.Files.TryFind) :> IFileSystem + FileSystemAutoOpens.FileSystem <- fs + ProcessWatcher.zombieCheckWithHostPID (fun () -> exit 0) pid + let _ = startCore () + 0 diff --git a/src/FsAutoComplete/FsAutoComplete.Lsp.fs b/src/FsAutoComplete/FsAutoComplete.Lsp.fs index fe6d879c1..6e4550657 100644 --- a/src/FsAutoComplete/FsAutoComplete.Lsp.fs +++ b/src/FsAutoComplete/FsAutoComplete.Lsp.fs @@ -2724,35 +2724,35 @@ let startCore backgroundServiceEnabled toolsPath stateStorageDir workspaceLoader use output = Console.OpenStandardOutput() let requestsHandlings = - defaultRequestHandlings () - |> Map.add "fsharp/signature" (requestHandling (fun s p -> s.FSharpSignature(p))) - |> Map.add "fsharp/signatureData" (requestHandling (fun s p -> s.FSharpSignatureData(p))) - |> Map.add "fsharp/documentationGenerator" (requestHandling (fun s p -> s.FSharpDocumentationGenerator(p))) - |> Map.add "fsharp/lineLens" (requestHandling (fun s p -> s.FSharpLineLense(p))) - |> Map.add "fsharp/compilerLocation" (requestHandling (fun s p -> s.FSharpCompilerLocation(p))) - |> Map.add "fsharp/workspaceLoad" (requestHandling (fun s p -> s.FSharpWorkspaceLoad(p))) - |> Map.add "fsharp/workspacePeek" (requestHandling (fun s p -> s.FSharpWorkspacePeek(p))) - |> Map.add "fsharp/project" (requestHandling (fun s p -> s.FSharpProject(p))) - |> Map.add "fsharp/fsdn" (requestHandling (fun s p -> s.FSharpFsdn(p))) - |> Map.add "fsharp/dotnetnewlist" (requestHandling (fun s p -> s.FSharpDotnetNewList(p))) - |> Map.add "fsharp/dotnetnewrun" (requestHandling (fun s p -> s.FSharpDotnetNewRun(p))) - |> Map.add "fsharp/dotnetaddproject" (requestHandling (fun s p -> s.FSharpDotnetAddProject(p))) - |> Map.add "fsharp/dotnetremoveproject" (requestHandling (fun s p -> s.FSharpDotnetRemoveProject(p))) - |> Map.add "fsharp/dotnetaddsln" (requestHandling (fun s p -> s.FSharpDotnetSlnAdd(p))) - |> Map.add "fsharp/f1Help" (requestHandling (fun s p -> s.FSharpHelp(p))) - |> Map.add "fsharp/documentation" (requestHandling (fun s p -> s.FSharpDocumentation(p))) - |> Map.add "fsharp/documentationSymbol" (requestHandling (fun s p -> s.FSharpDocumentationSymbol(p))) - |> Map.add "fsharp/loadAnalyzers" (requestHandling (fun s p -> s.LoadAnalyzers(p))) - // |> Map.add "fsharp/fsharpLiterate" (requestHandling (fun s p -> s.FSharpLiterate(p) )) - |> Map.add "fsharp/pipelineHint" (requestHandling (fun s p -> s.FSharpPipelineHints(p))) - // |> Map.add "fake/listTargets" (requestHandling (fun s p -> s.FakeTargets(p) )) - // |> Map.add "fake/runtimePath" (requestHandling (fun s p -> s.FakeRuntimePath(p) )) - |> Map.add "fsproj/moveFileUp" (requestHandling (fun s p -> s.FsProjMoveFileUp(p))) - |> Map.add "fsproj/moveFileDown" (requestHandling (fun s p -> s.FsProjMoveFileDown(p))) - |> Map.add "fsproj/addFileAbove" (requestHandling (fun s p -> s.FsProjAddFileAbove(p))) - |> Map.add "fsproj/addFileBelow" (requestHandling (fun s p -> s.FsProjAddFileBelow(p))) - |> Map.add "fsproj/addFile" (requestHandling (fun s p -> s.FsProjAddFile(p))) - |> Map.add "fsharp/inlayHints" (requestHandling (fun s p -> s.FSharpInlayHints(p))) + (defaultRequestHandlings() : Map>) + |> Map.add "fsharp/signature" (serverRequestHandling (fun s p -> s.FSharpSignature(p))) + |> Map.add "fsharp/signatureData" (serverRequestHandling (fun s p -> s.FSharpSignatureData(p))) + |> Map.add "fsharp/documentationGenerator" (serverRequestHandling (fun s p -> s.FSharpDocumentationGenerator(p))) + |> Map.add "fsharp/lineLens" (serverRequestHandling (fun s p -> s.FSharpLineLense(p))) + |> Map.add "fsharp/compilerLocation" (serverRequestHandling (fun s p -> s.FSharpCompilerLocation(p))) + |> Map.add "fsharp/workspaceLoad" (serverRequestHandling (fun s p -> s.FSharpWorkspaceLoad(p))) + |> Map.add "fsharp/workspacePeek" (serverRequestHandling (fun s p -> s.FSharpWorkspacePeek(p))) + |> Map.add "fsharp/project" (serverRequestHandling (fun s p -> s.FSharpProject(p))) + |> Map.add "fsharp/fsdn" (serverRequestHandling (fun s p -> s.FSharpFsdn(p))) + |> Map.add "fsharp/dotnetnewlist" (serverRequestHandling (fun s p -> s.FSharpDotnetNewList(p))) + |> Map.add "fsharp/dotnetnewrun" (serverRequestHandling (fun s p -> s.FSharpDotnetNewRun(p))) + |> Map.add "fsharp/dotnetaddproject" (serverRequestHandling (fun s p -> s.FSharpDotnetAddProject(p))) + |> Map.add "fsharp/dotnetremoveproject" (serverRequestHandling (fun s p -> s.FSharpDotnetRemoveProject(p))) + |> Map.add "fsharp/dotnetaddsln" (serverRequestHandling (fun s p -> s.FSharpDotnetSlnAdd(p))) + |> Map.add "fsharp/f1Help" (serverRequestHandling (fun s p -> s.FSharpHelp(p))) + |> Map.add "fsharp/documentation" (serverRequestHandling (fun s p -> s.FSharpDocumentation(p))) + |> Map.add "fsharp/documentationSymbol" (serverRequestHandling (fun s p -> s.FSharpDocumentationSymbol(p))) + |> Map.add "fsharp/loadAnalyzers" (serverRequestHandling (fun s p -> s.LoadAnalyzers(p))) + // |> Map.add "fsharp/fsharpLiterate" (serverRequestHandling (fun s p -> s.FSharpLiterate(p) )) + |> Map.add "fsharp/pipelineHint" (serverRequestHandling (fun s p -> s.FSharpPipelineHints(p))) + // |> Map.add "fake/listTargets" (serverRequestHandling (fun s p -> s.FakeTargets(p) )) + // |> Map.add "fake/runtimePath" (serverRequestHandling (fun s p -> s.FakeRuntimePath(p) )) + |> Map.add "fsproj/moveFileUp" (serverRequestHandling (fun s p -> s.FsProjMoveFileUp(p))) + |> Map.add "fsproj/moveFileDown" (serverRequestHandling (fun s p -> s.FsProjMoveFileDown(p))) + |> Map.add "fsproj/addFileAbove" (serverRequestHandling (fun s p -> s.FsProjAddFileAbove(p))) + |> Map.add "fsproj/addFileBelow" (serverRequestHandling (fun s p -> s.FsProjAddFileBelow(p))) + |> Map.add "fsproj/addFile" (serverRequestHandling (fun s p -> s.FsProjAddFile(p))) + |> Map.add "fsharp/inlayHints" (serverRequestHandling (fun s p -> s.FSharpInlayHints(p))) let state = State.Initial toolsPath stateStorageDir workspaceLoaderFactory diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs index 3b779ea7e..5083d3560 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs @@ -15,7 +15,7 @@ let private addMissingEqualsToTypeDefinitionTests state = """ type Person $0{ Name : string; Age : int; City : string } """ - (Diagnostics.expectCode "3360") + (Diagnostics.expectCode "3360") selectCodeFix """ type Person = { Name : string; Age : int; City : string } @@ -25,7 +25,7 @@ let private addMissingEqualsToTypeDefinitionTests state = """ type Name $0Name of string """ - (Diagnostics.expectCode "3360") + (Diagnostics.expectCode "3360") selectCodeFix """ type Name = Name of string @@ -225,7 +225,7 @@ let private changeDerefBangToValueTests state = """ ]) -let private changeDowncastToUpcastTests state = +let private changeDowncastToUpcastTests state = serverTestList (nameof ChangeDowncastToUpcast) state defaultConfigDto None (fun server -> [ let selectOperatorCodeFix = CodeFix.withTitle ChangeDowncastToUpcast.titleUpcastOperator let selectFunctionCodeFix = CodeFix.withTitle ChangeDowncastToUpcast.titleUpcastFunction @@ -237,7 +237,7 @@ let private changeDowncastToUpcastTests state = let v: I = C() $0:?> I """ - (Diagnostics.expectCode "3198") + (Diagnostics.expectCode "3198") selectOperatorCodeFix """ type I = interface end @@ -253,7 +253,7 @@ let private changeDowncastToUpcastTests state = let v: I = $0downcast C() """ - (Diagnostics.expectCode "3198") + (Diagnostics.expectCode "3198") selectFunctionCodeFix """ type I = interface end @@ -264,7 +264,7 @@ let private changeDowncastToUpcastTests state = () ]) -let private changeEqualsInFieldTypeToColonTests state = +let private changeEqualsInFieldTypeToColonTests state = serverTestList (nameof ChangeEqualsInFieldTypeToColon) state defaultConfigDto None (fun server -> [ let selectCodeFix = CodeFix.withTitle ChangeEqualsInFieldTypeToColon.title testCaseAsync "can change = to : in single line" <| @@ -272,7 +272,7 @@ let private changeEqualsInFieldTypeToColonTests state = """ type A = { Name : string; Key $0= int } """ - (Diagnostics.expectCode "10") + (Diagnostics.expectCode "10") selectCodeFix """ type A = { Name : string; Key : int } @@ -280,17 +280,17 @@ let private changeEqualsInFieldTypeToColonTests state = testCaseAsync "can change = to : in multi line" <| CodeFix.check server """ - type A = { + type A = { Name : string - Key $0= int + Key $0= int } """ - (Diagnostics.expectCode "10") + (Diagnostics.expectCode "10") selectCodeFix """ - type A = { + type A = { Name : string - Key : int + Key : int } """ ]) @@ -313,7 +313,7 @@ let private changePrefixNegationToInfixSubtractionTests state = """ ]) -let private changeRefCellDerefToNotTests state = +let private changeRefCellDerefToNotTests state = serverTestList (nameof ChangeRefCellDerefToNot) state defaultConfigDto None (fun server -> [ let selectCodeFix = CodeFix.withTitle ChangeRefCellDerefToNot.title testCaseAsync "can change simple deref to not" <| @@ -322,7 +322,7 @@ let private changeRefCellDerefToNotTests state = let x = 1 !$0x """ - (Diagnostics.expectCode "1") + (Diagnostics.expectCode "1") selectCodeFix """ let x = 1 @@ -334,7 +334,7 @@ let private changeRefCellDerefToNotTests state = let x = 1 !($0x) """ - (Diagnostics.expectCode "1") + (Diagnostics.expectCode "1") selectCodeFix """ let x = 1 @@ -346,7 +346,7 @@ let private changeRefCellDerefToNotTests state = let x = 1 !($0x = false) """ - (Diagnostics.expectCode "1") + (Diagnostics.expectCode "1") selectCodeFix """ let x = 1 @@ -368,7 +368,7 @@ let private changeTypeOfNameToNameOfTests state = """ ]) -let private convertBangEqualsToInequalityTests state = +let private convertBangEqualsToInequalityTests state = serverTestList (nameof ConvertBangEqualsToInequality) state defaultConfigDto None (fun server -> [ let selectCodeFix = CodeFix.withTitle ConvertBangEqualsToInequality.title testCaseAsync "can change != to <>" <| @@ -376,7 +376,7 @@ let private convertBangEqualsToInequalityTests state = """ 1 $0!= 2 """ - (Diagnostics.expectCode "43") + (Diagnostics.expectCode "43") selectCodeFix """ 1 <> 2 @@ -471,7 +471,7 @@ let private convertCSharpLambdaToFSharpLambdaTests state = """ ]) -let private convertDoubleEqualsToSingleEqualsTests state = +let private convertDoubleEqualsToSingleEqualsTests state = serverTestList (nameof ConvertDoubleEqualsToSingleEquals) state defaultConfigDto None (fun server -> [ let selectCodeFix = CodeFix.withTitle ConvertDoubleEqualsToSingleEquals.title testCaseAsync "can replace == with =" <| @@ -479,7 +479,7 @@ let private convertDoubleEqualsToSingleEqualsTests state = """ 1 $0== 1 """ - (Diagnostics.expectCode "43") + (Diagnostics.expectCode "43") selectCodeFix """ 1 = 1 @@ -493,8 +493,8 @@ let private convertDoubleEqualsToSingleEqualsTests state = Diagnostics.acceptAll selectCodeFix ]) - -let private convertInvalidRecordToAnonRecordTests state = + +let private convertInvalidRecordToAnonRecordTests state = serverTestList (nameof ConvertInvalidRecordToAnonRecord) state defaultConfigDto None (fun server -> [ let selectCodeFix = CodeFix.withTitle ConvertInvalidRecordToAnonRecord.title testCaseAsync "can convert single-line record with single field" <| @@ -502,7 +502,7 @@ let private convertInvalidRecordToAnonRecordTests state = """ let v = { $0Name = "foo" } """ - (Diagnostics.expectCode "39") + (Diagnostics.expectCode "39") selectCodeFix """ let v = {| Name = "foo" |} @@ -512,7 +512,7 @@ let private convertInvalidRecordToAnonRecordTests state = """ let v = { $0Name = "foo"; Value = 42 } """ - (Diagnostics.expectCode "39") + (Diagnostics.expectCode "39") selectCodeFix """ let v = {| Name = "foo"; Value = 42 |} @@ -525,7 +525,7 @@ let private convertInvalidRecordToAnonRecordTests state = Value = 42 } """ - (Diagnostics.expectCode "39") + (Diagnostics.expectCode "39") selectCodeFix """ let v = {| @@ -539,14 +539,14 @@ let private convertInvalidRecordToAnonRecordTests state = type V = { Name: string; Value: int } let v = { $0Name = "foo"; Value = 42 } """ - (Diagnostics.acceptAll) + (Diagnostics.acceptAll) selectCodeFix testCaseAsync "doesn't trigger for anon record" <| CodeFix.checkNotApplicable server """ let v = {| $0Name = "foo"; Value = 42 |} """ - (Diagnostics.acceptAll) + (Diagnostics.acceptAll) selectCodeFix ]) @@ -627,7 +627,7 @@ let private convertPositionalDUToNamedTests state = """ ]) -let private generateAbstractClassStubTests state = +let private generateAbstractClassStubTests state = let config = { defaultConfigDto with AbstractClassStubGeneration = Some true } serverTestList (nameof GenerateAbstractClassStub) state config None (fun server -> [ let selectCodeFix = CodeFix.withTitle GenerateAbstractClassStub.title @@ -663,7 +663,7 @@ let private generateAbstractClassStubTests state = member _.Move dx dy = x <- x + dx y <- y + dy - + type $0Square(x,y, sideLength) = inherit Shape(x,y) """ @@ -680,13 +680,13 @@ let private generateAbstractClassStubTests state = member _.Move dx dy = x <- x + dx y <- y + dy - + type Square(x,y, sideLength) = inherit Shape(x,y) - override this.Area: float = + override this.Area: float = failwith "Not Implemented" - override this.Name: string = + override this.Name: string = failwith "Not Implemented" """ ptestCaseAsync "can generate abstract class stub without trailing nl" <| @@ -704,7 +704,7 @@ let private generateAbstractClassStubTests state = member _.Move dx dy = x <- x + dx y <- y + dy - + type $0Square(x,y, sideLength) = inherit Shape(x,y)""" (Diagnostics.expectCode "365") @@ -720,13 +720,13 @@ let private generateAbstractClassStubTests state = member _.Move dx dy = x <- x + dx y <- y + dy - + type Square(x,y, sideLength) = inherit Shape(x,y) - override this.Area: float = + override this.Area: float = failwith "Not Implemented" - override this.Name: string = + override this.Name: string = failwith "Not Implemented" """ ptestCaseAsync "inserts override in correct place" <| @@ -743,7 +743,7 @@ let private generateAbstractClassStubTests state = member _.Move dx dy = x <- x + dx y <- y + dy - + type $0Square(x,y, sideLength) = inherit Shape(x,y) let a = 0 @@ -761,13 +761,13 @@ let private generateAbstractClassStubTests state = member _.Move dx dy = x <- x + dx y <- y + dy - + type Square(x,y, sideLength) = inherit Shape(x,y) - override this.Area: float = + override this.Area: float = failwith "Not Implemented" - override this.Name: string = + override this.Name: string = failwith "Not Implemented" let a = 0 """ @@ -785,7 +785,7 @@ let private generateAbstractClassStubTests state = member _.Move dx dy = x <- x + dx y <- y + dy - + type $0Square(x,y, sideLength) = inherit Shape(x,y) """ @@ -802,21 +802,21 @@ let private generateAbstractClassStubTests state = member _.Move dx dy = x <- x + dx y <- y + dy - + type Square(x,y, sideLength) = inherit Shape(x,y) override this.Name = "Circle" - override this.Area: float = + override this.Area: float = failwith "Not Implemented" """ ]) let private generateRecordStubTests state = - let config = - { defaultConfigDto with - RecordStubGeneration = Some true + let config = + { defaultConfigDto with + RecordStubGeneration = Some true RecordStubGenerationBody = Some "failwith \"---\"" } serverTestList (nameof GenerateRecordStub) state config None (fun server -> [ @@ -838,9 +838,9 @@ let private generateRecordStubTests state = ]) let private generateUnionCasesTests state = - let config = - { defaultConfigDto with - UnionCaseStubGeneration = Some true + let config = + { defaultConfigDto with + UnionCaseStubGeneration = Some true UnionCaseStubGenerationBody = Some "failwith \"---\"" } serverTestList (nameof GenerateUnionCases) state config None (fun server -> [ @@ -878,7 +878,7 @@ let private makeDeclarationMutableTests state = let x = 0 x $0<- 1 """ - (Diagnostics.expectCode "27") + (Diagnostics.expectCode "27") selectCodeFix """ let mutable x = 0 @@ -892,7 +892,7 @@ let private makeDeclarationMutableTests state = x $0<- 1 () """ - (Diagnostics.expectCode "27") + (Diagnostics.expectCode "27") selectCodeFix """ let mutable x = 0 @@ -909,7 +909,7 @@ let private makeDeclarationMutableTests state = counter $0<- counter + 1 counter """ - (Diagnostics.expectCode "27") + (Diagnostics.expectCode "27") selectCodeFix """ let count xs = @@ -924,7 +924,7 @@ let private makeDeclarationMutableTests state = let mutable x = 0 x $0<- 1 """ - Diagnostics.acceptAll + Diagnostics.acceptAll selectCodeFix testCaseAsync "doesn't trigger for immutable parameter" <| CodeFix.checkNotApplicable server @@ -933,7 +933,7 @@ let private makeDeclarationMutableTests state = v $0<- 1 v """ - Diagnostics.acceptAll + Diagnostics.acceptAll selectCodeFix testCaseAsync "doesn't trigger for immutable member parameter" <| CodeFix.checkNotApplicable server @@ -942,7 +942,7 @@ let private makeDeclarationMutableTests state = member _.M(v: int) v $0<- 1 """ - Diagnostics.acceptAll + Diagnostics.acceptAll selectCodeFix ]) @@ -978,7 +978,7 @@ let private removeRedundantQualifierTests state = open System let _ = $0System.String.IsNullOrWhiteSpace "foo" """ - Diagnostics.acceptAll + Diagnostics.acceptAll selectCodeFix """ open System @@ -989,7 +989,7 @@ let private removeRedundantQualifierTests state = """ let _ = $0System.String.IsNullOrWhiteSpace "foo" """ - Diagnostics.acceptAll + Diagnostics.acceptAll selectCodeFix ]) @@ -1086,16 +1086,14 @@ let private removeUnusedBindingTests state = CodeFix.check server """ let container () = - let $0incr x = 2 - () - """ + let $0incr x = 2 // dummy comment to keep spacing + ()""" validateDiags selectRemoveUnusedBinding """ let container () = - - () - """ + // dummy comment to keep spacing + ()""" ]) let private removeUnusedOpensTests state = @@ -1107,7 +1105,7 @@ let private removeUnusedOpensTests state = """ open $0System """ - Diagnostics.acceptAll + Diagnostics.acceptAll selectCodeFix "" testCaseAsync "removes just current unused open" <| @@ -1117,7 +1115,7 @@ let private removeUnusedOpensTests state = open $0System open System.Text """ - Diagnostics.acceptAll + Diagnostics.acceptAll selectCodeFix """ open System.Text @@ -1128,7 +1126,7 @@ let private removeUnusedOpensTests state = open System open $0System.Text """ - Diagnostics.acceptAll + Diagnostics.acceptAll selectCodeFix """ open System @@ -1140,7 +1138,7 @@ let private removeUnusedOpensTests state = let _ = String.IsNullOrWhiteSpace "" """ - Diagnostics.acceptAll + Diagnostics.acceptAll selectCodeFix testCaseAsync "can remove open in nested module" <| CodeFix.check server @@ -1151,7 +1149,7 @@ let private removeUnusedOpensTests state = () () """ - Diagnostics.acceptAll + Diagnostics.acceptAll selectCodeFix """ module A = @@ -1170,7 +1168,7 @@ let private removeUnusedOpensTests state = () () """ - Diagnostics.acceptAll + Diagnostics.acceptAll selectCodeFix """ open System @@ -1192,7 +1190,7 @@ let private removeUnusedOpensTests state = () () """ - Diagnostics.acceptAll + Diagnostics.acceptAll selectCodeFix """ module A = @@ -1208,7 +1206,7 @@ let private removeUnusedOpensTests state = open $0System let x = String.IsNullOrWhiteSpace "" """ - Diagnostics.acceptAll + Diagnostics.acceptAll selectCodeFix ]) @@ -1280,18 +1278,18 @@ let private replaceWithSuggestionTests state = """ let x = $0Min(2.0, 1.0) """ - Diagnostics.acceptAll + Diagnostics.acceptAll (selectCodeFix "min") """ let x = min(2.0, 1.0) """ - testSequenced <| testList "can get multiple suggestions for flout" [ + testList "can get multiple suggestions for flout" [ testCaseAsync "can change flout to float" <| CodeFix.check server """ let x = $0flout 2 """ - Diagnostics.acceptAll + Diagnostics.acceptAll (selectCodeFix "float") """ let x = float 2 @@ -1301,7 +1299,7 @@ let private replaceWithSuggestionTests state = """ let x = $0flout 2 """ - Diagnostics.acceptAll + Diagnostics.acceptAll (selectCodeFix "float32") """ let x = float32 2 @@ -1312,7 +1310,7 @@ let private replaceWithSuggestionTests state = """ let x: $0flout = 2.0 """ - Diagnostics.acceptAll + Diagnostics.acceptAll (selectCodeFix "float") """ let x: float = 2.0 @@ -1322,7 +1320,7 @@ let private replaceWithSuggestionTests state = """ open System.Text.$0RegularEcpressions """ - Diagnostics.acceptAll + Diagnostics.acceptAll (selectCodeFix "RegularExpressions") """ open System.Text.RegularExpressions @@ -1333,7 +1331,7 @@ let private replaceWithSuggestionTests state = open System.Text.RegularExpressions let x = $0Regec() """ - Diagnostics.acceptAll + Diagnostics.acceptAll (selectCodeFix "Regex") """ open System.Text.RegularExpressions @@ -1345,7 +1343,7 @@ let private replaceWithSuggestionTests state = let ``hello world`` = 2 let x = ``$0hello word`` """ - Diagnostics.acceptAll + Diagnostics.acceptAll (selectCodeFix "``hello world``") """ let ``hello world`` = 2 @@ -1357,7 +1355,7 @@ let private replaceWithSuggestionTests state = let ``hello world`` = 2 let x = $0helloword """ - Diagnostics.acceptAll + Diagnostics.acceptAll (selectCodeFix "``hello world``") """ let ``hello world`` = 2 @@ -1368,14 +1366,14 @@ let private replaceWithSuggestionTests state = let private resolveNamespaceTests state = let config = { defaultConfigDto with ResolveNamespaces = Some true } serverTestList (nameof ResolveNamespace) state config None (fun server -> [ - testCaseAsync "doesn't fail when target not in last line" <| + testCaseAsync "doesn't fail when target not in last line" <| CodeFix.checkApplicable server """ let x = $0Min(2.0, 1.0) """ // Note: new line at end! (Diagnostics.log >> Diagnostics.acceptAll) (CodeFix.log >> CodeFix.matching (fun ca -> ca.Title.StartsWith "open") >> Array.take 1) - testCaseAsync "doesn't fail when target in last line" <| + testCaseAsync "doesn't fail when target in last line" <| CodeFix.checkApplicable server "let x = $0Min(2.0, 1.0)" // Note: No new line at end! (Diagnostics.log >> Diagnostics.acceptAll) @@ -1466,7 +1464,7 @@ let private wrapExpressionInParenthesesTests state = """ printfn "%b" System.String.$0IsNullOrWhiteSpace("foo") """ - (Diagnostics.expectCode "597") + (Diagnostics.expectCode "597") selectCodeFix """ printfn "%b" (System.String.IsNullOrWhiteSpace("foo")) @@ -1476,7 +1474,7 @@ let private wrapExpressionInParenthesesTests state = """ printfn "%b" (System.String.$0IsNullOrWhiteSpace("foo")) """ - Diagnostics.acceptAll + Diagnostics.acceptAll selectCodeFix ]) diff --git a/test/FsAutoComplete.Tests.Lsp/CoreTests.fs b/test/FsAutoComplete.Tests.Lsp/CoreTests.fs index 2ef001a59..00b29e580 100644 --- a/test/FsAutoComplete.Tests.Lsp/CoreTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CoreTests.fs @@ -36,6 +36,14 @@ let initTests state = RootUri = None InitializationOptions = Some(Server.serialize defaultConfigDto) Capabilities = Some clientCaps + ClientInfo = Some { + Name = "FSAC Tests" + Version = Some "0.0.0" + } + WorkspaceFolders = Some [| { + Uri = Path.FilePathToUri tempDir + Name = "Test Folder" + } |] trace = None } let! result = server.Initialize p diff --git a/test/FsAutoComplete.Tests.Lsp/Helpers.fs b/test/FsAutoComplete.Tests.Lsp/Helpers.fs index f7aa76d90..584d2078b 100644 --- a/test/FsAutoComplete.Tests.Lsp/Helpers.fs +++ b/test/FsAutoComplete.Tests.Lsp/Helpers.fs @@ -402,6 +402,14 @@ let serverInitialize path (config: FSharpConfigDto) state = RootUri = Some(sprintf "file://%s" path) InitializationOptions = Some(Server.serialize config) Capabilities = Some clientCaps + ClientInfo = Some { + Name = "FSAC Tests" + Version = Some "0.0.0" + } + WorkspaceFolders = Some [| { + Uri = Path.FilePathToUri path + Name = "Test Folder" + } |] trace = None } let! result = server.Initialize p diff --git a/test/FsAutoComplete.Tests.Lsp/Program.fs b/test/FsAutoComplete.Tests.Lsp/Program.fs index 477e9f78f..5000d1126 100644 --- a/test/FsAutoComplete.Tests.Lsp/Program.fs +++ b/test/FsAutoComplete.Tests.Lsp/Program.fs @@ -16,6 +16,8 @@ open System.Threading open Serilog.Filters open System.IO +Expect.defaultDiffPrinter <- Diff.colourisedDiff + let testTimeout = Environment.GetEnvironmentVariable "TEST_TIMEOUT_MINUTES" |> Int32.TryParse diff --git a/test/FsAutoComplete.Tests.Lsp/RenameTests.fs b/test/FsAutoComplete.Tests.Lsp/RenameTests.fs index 60aaa6d8a..16cc3199a 100644 --- a/test/FsAutoComplete.Tests.Lsp/RenameTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/RenameTests.fs @@ -143,7 +143,7 @@ let tests state = |> AsyncResult.foldResult id (fun e -> failtestf "%A" e) let newName = "afterwards" - let sourceFile = { Uri = Path.FilePathToUri path } + let sourceFile: TextDocumentIdentifier = { Uri = Path.FilePathToUri path } let p: RenameParams = { TextDocument = sourceFile @@ -189,7 +189,7 @@ let tests state = |> AsyncResult.foldResult id (fun e -> failtestf "%A" e) let newName = "afterwards" - let sourceFile = { Uri = Path.FilePathToUri path } + let sourceFile: TextDocumentIdentifier = { Uri = Path.FilePathToUri path } let p: RenameParams = { TextDocument = sourceFile diff --git a/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fs b/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fs index f256140a8..f23dbf8c7 100644 --- a/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fs @@ -1,5 +1,6 @@ module Utils.CursorbasedTests open Expecto +open Expecto.Diff open Ionide.LanguageServerProtocol.Types open FsToolkit.ErrorHandling open Utils.Utils @@ -8,7 +9,7 @@ open Utils.TextEdit open Ionide.ProjInfo.Logging /// Checks for CodeFixes, CodeActions -/// +/// /// Prefixes: /// * `check`: Check to use inside a `testCaseAsync`. Not a Test itself! /// * `test`: Returns Expecto Test. Usually combines multiple tests (like: test all positions). @@ -18,8 +19,8 @@ module CodeFix = let private diagnosticsIn (range: Range) (diags: Diagnostic[]) = diags |> Array.filter (fun diag -> range |> Range.overlapsStrictly diag.Range) - - /// Note: Return should be just ONE `CodeAction` (for Applicable) or ZERO `CodeAction` (for Not Applicable). + + /// Note: Return should be just ONE `CodeAction` (for Applicable) or ZERO `CodeAction` (for Not Applicable). /// But actual return type is an array of `CodeAction`s: /// * Easier to successive filter CodeActions down with simple pipe and `Array.filter` /// * Returning `CodeAction option` would mean different filters for `check` (exactly one fix) and `checkNotApplicable` (exactly zero fix). @@ -44,11 +45,11 @@ module CodeFix = let! res = doc |> Document.codeActionAt diags cursorRange let allCodeActions = match res, expected with - | None, (Applicable | After _) -> + | None, (Applicable | After _) -> // error here instead of later to return error noting it was `None` instead of empty CodeAction array Expect.isSome res "No CodeAction returned (`None`)" failwith "unreachable" - | None, NotApplicable -> + | None, NotApplicable -> [||] | Some res, _ -> match res with @@ -66,7 +67,7 @@ module CodeFix = | _ -> Expect.hasLength codeActions 1 "Should be exactly ONE applicable code action" codeActions |> Array.head - + match expected with | NotApplicable -> // Expect.isEmpty codeActions "There should be no applicable code action" // doesn't show `actual` when not empty @@ -81,7 +82,7 @@ module CodeFix = let codeAction = codeActions |> getCodeAction /// Error message is appended by selected `codeAction` - let inline failCodeFixTest (msg: string) = + let inline failCodeFixTest (msg: string) = let msg = if System.String.IsNullOrWhiteSpace msg || System.Char.IsPunctuation(msg, msg.Length-1) then msg @@ -104,8 +105,8 @@ module CodeFix = beforeWithoutCursor |> TextEdits.apply edits |> Result.valueOr failCodeFixTest - - Expect.equal actual expected "Incorrect text after applying the chosen code action" + + Expecto.Diff.equals actual expected "Incorrect text after applying the chosen code action" } let private checkFix @@ -115,8 +116,8 @@ module CodeFix = (chooseFix: ChooseFix) (expected: ExpectedResult) = async { - let (range, text) = - beforeWithCursor + let (range, text) = + beforeWithCursor |> Text.trimTripleQuotation |> Cursor.assertExtractRange // load text file @@ -127,7 +128,7 @@ module CodeFix = } /// Checks a CodeFix (CodeAction) for validity. - /// + /// /// * Extracts cursor position (`$0`) or range (between two `$0`) from `beforeWithCursor` /// * Opens untitled Doc with source `beforeWithCursor` (with cursor removed) /// * Note: untitled Document acts as Script file! @@ -141,21 +142,21 @@ module CodeFix = /// * Selects CodeFix from returned CodeFixes with `chooseFix` /// * Note: `chooseFix` should return a single CodeFix. No CodeFix or multiple CodeFixes count as Failure! /// * Use `checkNotApplicable` when there shouldn't be a CodeFix - /// * Note: Though `chooseFix` should return one CodeFix, the function actually returns an array of CodeFixes. + /// * Note: Though `chooseFix` should return one CodeFix, the function actually returns an array of CodeFixes. /// Reasons: /// * Easier to filter down CodeFixes (`CodeFix.ofKind "..." >> CodeFix.withTitle "..."`) /// * Better error messages: Can differentiate between no CodeFixes and too many CodeFixes /// * Validates selected CodeFix: /// * Applies selected CodeFix to source (`beforeWithCursor` with cursor removed) /// * Compares result with `expected` - /// - /// Note: + /// + /// Note: /// `beforeWithCursor` as well as `expected` get trimmed with `Text.trimTripleQuotation`: Leading empty line and indentation gets removed. - /// - /// Note: - /// `beforeWithCursor` and `expected` MUST use `\n` for linebreaks -- using `\r` (either alone or as `\r\n`) results in test failure! - /// Linebreaks from edits in selected CodeFix are all transformed to just `\n` - /// -> CodeFix can use `\r` and `\r\n` + /// + /// Note: + /// `beforeWithCursor` and `expected` MUST use `\n` for linebreaks -- using `\r` (either alone or as `\r\n`) results in test failure! + /// Linebreaks from edits in selected CodeFix are all transformed to just `\n` + /// -> CodeFix can use `\r` and `\r\n` /// If you want to validate Line Endings of CodeFix, add a validation step to your `chooseFix` let check server @@ -208,12 +209,12 @@ module CodeFix = module private Test = /// One `testCaseAsync` for each cursorRange. /// All test cases use same document (`ServerTests.documentTestList`) with source `beforeWithoutCursor`. - /// - /// Test names: + /// + /// Test names: /// * `name` is name of outer test list. /// * Each test case: `Cursor {i} at {pos or range}` - /// - /// Note: Sharing a common `Document` is just barely faster than using a new `Document` for each test (at least for simple source in `beforeWithoutCursor`). + /// + /// Note: Sharing a common `Document` is just barely faster than using a new `Document` for each test (at least for simple source in `beforeWithoutCursor`). let checkFixAll (name: string) (server: CachedServer) @@ -238,7 +239,7 @@ module CodeFix = ]) /// One test for each Cursor. - /// + /// /// Note: Tests single positions -> each `$0` gets checked. /// -> Every test is for single-position range (`Start=End`)! let checkAllPositions @@ -253,7 +254,7 @@ module CodeFix = let ranges = poss |> List.map (fun p -> { Start = p; End = p }) checkFixAll name server beforeWithoutCursor ranges validateDiagnostics chooseFix expected - let testAllPositions + let testAllPositions name server beforeWithCursors diff --git a/test/FsAutoComplete.Tests.Lsp/Utils/Server.fs b/test/FsAutoComplete.Tests.Lsp/Utils/Server.fs index 3ef4cd4fb..c0018a7dd 100644 --- a/test/FsAutoComplete.Tests.Lsp/Utils/Server.fs +++ b/test/FsAutoComplete.Tests.Lsp/Utils/Server.fs @@ -30,9 +30,9 @@ type Document = Uri: DocumentUri mutable Version: int } - member doc.TextDocumentIdentifier = + member doc.TextDocumentIdentifier: TextDocumentIdentifier = { Uri = doc.Uri } - member doc.VersionedTextDocumentIdentifier = + member doc.VersionedTextDocumentIdentifier: VersionedTextDocumentIdentifier = { Uri = doc.Uri; Version = Some doc.Version } interface IDisposable with @@ -70,6 +70,15 @@ module Server = RootUri = path |> Option.map (sprintf "file://%s") InitializationOptions = Some (Server.serialize config) Capabilities = Some clientCaps + ClientInfo = Some { + Name = "FSAC Tests" + Version = Some "0.0.0" + } + WorkspaceFolders = path |> Option.map (fun p ->[| { + Uri = Path.FilePathToUri p + Name = "Test Folder" + } |] + ) trace = None } match! server.Initialize p with @@ -244,7 +253,7 @@ module Document = |> Observable.timeoutSpan timeout |> Async.AwaitObservable } - let private defaultTimeout = TimeSpan.FromSeconds 5.0 + let private defaultTimeout = TimeSpan.FromSeconds 10.0 /// Note: Mutates passed `doc` let private incrVersion (doc: Document) = diff --git a/test/FsAutoComplete.Tests.Lsp/Utils/ServerTests.fs b/test/FsAutoComplete.Tests.Lsp/Utils/ServerTests.fs index e97b3f9ca..1a573d835 100644 --- a/test/FsAutoComplete.Tests.Lsp/Utils/ServerTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/Utils/ServerTests.fs @@ -7,24 +7,24 @@ open Expecto open Ionide.LanguageServerProtocol.Types /// TestList which creates (in `initialize`) and caches (if `cacheValue`) a value, and runs cleanup after all tests were run (in `cleanup`) -/// +/// /// Note: TestCase for `cleanup` is called `cleanup` -/// +/// /// Note: no value is created when there are no `tests`, and neither gets `cleanup` executed -/// +/// /// Note: Result of `initialize` is only cached when `cacheValue` is `true`. But `cleanup` is called regardless. -/// Use `false` when `initialize` returns an already cached value, otherwise `false`. +/// Use `false` when `initialize` returns an already cached value, otherwise `false`. /// Then in here `Async.Cache` is used to cache value. -let cleanableTestList - runner - (name: string) +let cleanableTestList + runner + (name: string) (initialize: Async<'a>) (cacheValue: bool) (cleanup: Async<'a> -> Async) - (tests: Async<'a> -> Test list) + (tests: Async<'a> -> Test list) = let value = - if cacheValue then + if cacheValue then initialize |> Async.Cache else initialize @@ -33,7 +33,7 @@ let cleanableTestList testSequenced <| runner name [ yield! tests - if not (tests |> List.isEmpty) then + if not (tests |> List.isEmpty) then testCaseAsync "cleanup" (cleanup value) ] @@ -56,7 +56,7 @@ let private serverTestList' name init false - cleanup + cleanup tests /// ## Example @@ -65,7 +65,7 @@ let private serverTestList' /// testCaseAsync "can get diagnostics" <| async { /// let! (doc, diags) = server |> Server.createUntitledDocument "let foo = bar" /// use doc = doc // ensure doc gets closed (disposed) after test -/// +/// /// Expect.exists diags (fun d -> d.Message = "The value or constructor 'bar' is not defined.") "Should have `bar not defined` error" /// } /// ]) @@ -93,7 +93,7 @@ let private documentTestList' name init false - cleanup + cleanup tests /// Note: Not intended for changing document: always same (initial) diags diff --git a/test/FsAutoComplete.Tests.Lsp/paket.references b/test/FsAutoComplete.Tests.Lsp/paket.references index af68b4db7..679ef0dcf 100644 --- a/test/FsAutoComplete.Tests.Lsp/paket.references +++ b/test/FsAutoComplete.Tests.Lsp/paket.references @@ -3,6 +3,7 @@ FSharp.Compiler.Service FSharp.Control.Reactive FSharpx.Async Expecto +Expecto.Diff Microsoft.NET.Test.Sdk YoloDev.Expecto.TestSdk AltCover