From 0f513d8d2787891d2e21e0ee28c184049aa7638e Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Fri, 1 Mar 2024 10:02:57 -0500 Subject: [PATCH 01/60] Ignore warn on debug --- Directory.Build.props | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index ed5de172f..d0fe85bb3 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -7,8 +7,10 @@ $(NoWarn);3186,0042 $(NoWarn);NU1902 - $(WarnOn);1182 + $(WarnOn);3390 true $(MSBuildThisFileDirectory)CHANGELOG.md From 1e365e9586ffb275c38432ff59cd0e2aa5e2a904 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Fri, 1 Mar 2024 10:03:07 -0500 Subject: [PATCH 02/60] global.json nonsense --- global.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/global.json b/global.json index 5c6dc58bc..428b67156 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { "version": "7.0.400", - "rollForward": "major", + "rollForward": "latestMajor", "allowPrerelease": true } -} \ No newline at end of file +} From 973d47a642ecd7b3a858d557230e9b904b4fd691 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Fri, 1 Mar 2024 10:04:33 -0500 Subject: [PATCH 03/60] Transparent compiler changes --- .../CompilerServiceInterface.fs | 55 +++++++++++-------- .../CompilerServiceInterface.fsi | 6 +- .../LspServers/AdaptiveServerState.fs | 24 +++++++- 3 files changed, 57 insertions(+), 28 deletions(-) diff --git a/src/FsAutoComplete.Core/CompilerServiceInterface.fs b/src/FsAutoComplete.Core/CompilerServiceInterface.fs index d27bddcee..fa2c92507 100644 --- a/src/FsAutoComplete.Core/CompilerServiceInterface.fs +++ b/src/FsAutoComplete.Core/CompilerServiceInterface.fs @@ -17,7 +17,7 @@ open FsToolkit.ErrorHandling type Version = int -type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelReferenceResolution) = +type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelReferenceResolution, documentSource) = let checker = FSharpChecker.Create( projectCacheSize = 200, @@ -29,7 +29,8 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe enablePartialTypeChecking = not hasAnalyzers, parallelReferenceResolution = parallelReferenceResolution, captureIdentifiersWhenParsing = true, - useSyntaxTreeCache = true + useSyntaxTreeCache = true, + useTransparentCompiler = true ) let entityCache = EntityCache() @@ -233,7 +234,7 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe /// The source to be parsed. /// Parsing options for the project or script. /// - member __.ParseFile(filePath: string, source: ISourceText, options: FSharpParsingOptions) = + member __.ParseFile(filePath: string, source: ISourceText, options: FSharpProjectOptions) = async { checkerLogger.info ( Log.setMessage "ParseFile - {file}" @@ -241,7 +242,9 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe ) let path = UMX.untag filePath - return! checker.ParseFile(path, source, options) + let! snapshot = FSharpProjectSnapshot.FromOptions(options, documentSource) + return! checker.ParseFile(path, snapshot) + // return! checker.ParseFile(path, source, options) } /// Parse and check a source code file, returning a handle to the results @@ -255,9 +258,9 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe member __.ParseAndCheckFileInProject ( filePath: string, - version, + version: int, source: ISourceText, - options, + options: FSharpProjectOptions, ?shouldCache: bool ) = asyncResult { @@ -270,7 +273,9 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe let path = UMX.untag filePath try - let! (p, c) = checker.ParseAndCheckFileInProject(path, version, source, options, userOpName = opName) + let! snapshot = FSharpProjectSnapshot.FromOptions(options, documentSource) + let! (p,c) = checker.ParseAndCheckFileInProject(path,snapshot, userOpName = opName) + // let! (p, c) = checker.ParseAndCheckFileInProject(path, version, source, options, userOpName = opName) let parseErrors = p.Diagnostics |> Array.map (fun p -> p.Message) @@ -339,6 +344,7 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe let options = clearProjectReferences options let result = + // TODO: Should this be snapshottable? (is that a word?) checker.TryGetRecentCheckResultsForFile(UMX.untag file, options, sourceText = source, userOpName = opName) |> Option.map (fun (pr, cr, version) -> checkerLogger.info ( @@ -374,15 +380,19 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe | None -> return [||] | Some(opts, []) -> let opts = clearProjectReferences opts - let! res = checker.ParseAndCheckProject opts + let! snapshot = FSharpProjectSnapshot.FromOptions(opts, documentSource) + let! res = checker.ParseAndCheckProject(snapshot) + // let! res = checker.ParseAndCheckProject opts return res.GetUsesOfSymbol symbol | Some(opts, dependentProjects) -> let! res = opts :: dependentProjects |> List.map (fun (opts) -> async { - let opts = clearProjectReferences opts - let! res = checker.ParseAndCheckProject opts + + let! snapshot = FSharpProjectSnapshot.FromOptions(opts, documentSource) + let! res = checker.ParseAndCheckProject(snapshot) + // let! res = checker.ParseAndCheckProject opts return res.GetUsesOfSymbol symbol }) |> Async.parallel75 @@ -396,26 +406,27 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe Log.setMessage "FindReferencesForSymbolInFile - {file}" >> Log.addContextDestructured "file" file ) - - return! - checker.FindBackgroundReferencesInFile( - file, - project, - symbol, - canInvalidateProject = false, - // fastCheck = true, - userOpName = "find references" - ) + let! snapshot = FSharpProjectSnapshot.FromOptions(project, documentSource) + return! checker.FindBackgroundReferencesInFile(file, snapshot, symbol, userOpName = "find references") + // return! + // checker.FindBackgroundReferencesInFile( + // file, + // project, + // symbol, + // canInvalidateProject = false, + // // fastCheck = true, + // userOpName = "find references" + // ) } - member __.GetDeclarations(fileName: string, source: ISourceText, options: FSharpParsingOptions, _) = + member this.GetDeclarations(fileName: string, source: ISourceText, options: FSharpProjectOptions, _) = async { checkerLogger.info ( Log.setMessage "GetDeclarations - {file}" >> Log.addContextDestructured "file" fileName ) - let! parseResult = checker.ParseFile(UMX.untag fileName, source, options, userOpName = "getDeclarations") + let! parseResult = this.ParseFile(fileName, source, options) return parseResult.GetNavigationItems().Declarations } diff --git a/src/FsAutoComplete.Core/CompilerServiceInterface.fsi b/src/FsAutoComplete.Core/CompilerServiceInterface.fsi index f332a9a27..c82ad1f47 100644 --- a/src/FsAutoComplete.Core/CompilerServiceInterface.fsi +++ b/src/FsAutoComplete.Core/CompilerServiceInterface.fsi @@ -14,7 +14,7 @@ type Version = int type FSharpCompilerServiceChecker = new: - hasAnalyzers: bool * typecheckCacheSize: int64 * parallelReferenceResolution: bool -> FSharpCompilerServiceChecker + hasAnalyzers: bool * typecheckCacheSize: int64 * parallelReferenceResolution: bool * documentSource : DocumentSource -> FSharpCompilerServiceChecker member DisableInMemoryProjectReferences: bool with get, set @@ -40,7 +40,7 @@ type FSharpCompilerServiceChecker = /// Parsing options for the project or script. /// member ParseFile: - filePath: string * source: ISourceText * options: FSharpParsingOptions -> Async + filePath: string * source: ISourceText * options: FSharpProjectOptions -> Async /// Parse and check a source code file, returning a handle to the results /// The name of the file in the project whose source is being checked. @@ -78,7 +78,7 @@ type FSharpCompilerServiceChecker = file: string * project: FSharpProjectOptions * symbol: FSharpSymbol -> Async> member GetDeclarations: - fileName: string * source: ISourceText * options: FSharpParsingOptions * version: 'a -> + fileName: string * source: ISourceText * options: FSharpProjectOptions * version: 'a -> Async member SetDotnetRoot: dotnetBinary: FileInfo * cwd: DirectoryInfo -> unit diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index a490792a5..374e358e4 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -36,6 +36,7 @@ open FsAutoComplete.FCSPatches open FsAutoComplete.Lsp open FsAutoComplete.Lsp.Helpers open FSharp.Compiler.Syntax +open FSharp.Compiler.CodeAnalysis [] @@ -59,15 +60,21 @@ type LoadedProject = override x.GetHashCode() = x.FSharpProjectOptions.GetHashCode() + override x.Equals(other: obj) = match other with | :? LoadedProject as other -> (x :> IEquatable<_>).Equals other | _ -> false + member x.GetSnapshot(documentSource) = + FSharpProjectSnapshot.FromOptions(x.FSharpProjectOptions, documentSource) + member x.SourceFiles = x.FSharpProjectOptions.SourceFiles member x.ProjectFileName = x.FSharpProjectOptions.ProjectFileName static member op_Implicit(x: LoadedProject) = x.FSharpProjectOptions + + /// The reality is a file can be in multiple projects /// This is extracted to make it easier to do some type of customized select in the future type IFindProject = @@ -96,10 +103,12 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let rootPath = cval None let config = cval FSharpConfig.Default + let documentSource = cval DocumentSource.FileSystem let checker = - config - |> AVal.map (fun c -> c.EnableAnalyzers, c.Fsac.CachedTypeCheckCount, c.Fsac.ParallelReferenceResolution) + + (config, documentSource) + ||> AVal.map2 (fun c ds -> c.EnableAnalyzers, c.Fsac.CachedTypeCheckCount, c.Fsac.ParallelReferenceResolution, ds) |> AVal.map (FSharpCompilerServiceChecker) let configChanges = @@ -1138,7 +1147,16 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac } |> Async.map (Result.ofOption (fun () -> $"Could not read file: {file}")) + let documentSourceLookup (filePath: string) = + asyncOption { + let! file = forceFindOpenFileOrRead (Utils.normalizePath filePath) + let! file = Result.toOption file + return file.Source :> ISourceText + } + do + transact ( fun () -> + documentSource.Value <- DocumentSource.Custom documentSourceLookup ) let fileShimChanges = openFilesWithChanges |> AMap.mapA (fun _ v -> v) // let cachedFileContents = cachedFileContents |> cmap.mapA (fun _ v -> v) @@ -1160,7 +1178,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac /// let parseFile (checker: FSharpCompilerServiceChecker) (source: VolatileFile) parseOpts options = async { - let! result = checker.ParseFile(source.FileName, source.Source, parseOpts) + let! result = checker.ParseFile(source.FileName, source.Source, options) let! ct = Async.CancellationToken fileParsed.Trigger(result, options, ct) From fe41e484730791af0a41bd54bc2aef1221fd433b Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Fri, 1 Mar 2024 10:40:37 -0500 Subject: [PATCH 04/60] formatting --- .../CompilerServiceInterface.fs | 74 ++++++++++--------- .../CompilerServiceInterface.fsi | 5 +- .../LspServers/AdaptiveServerState.fs | 12 ++- 3 files changed, 47 insertions(+), 44 deletions(-) diff --git a/src/FsAutoComplete.Core/CompilerServiceInterface.fs b/src/FsAutoComplete.Core/CompilerServiceInterface.fs index fa2c92507..76cdd345d 100644 --- a/src/FsAutoComplete.Core/CompilerServiceInterface.fs +++ b/src/FsAutoComplete.Core/CompilerServiceInterface.fs @@ -244,7 +244,7 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe let path = UMX.untag filePath let! snapshot = FSharpProjectSnapshot.FromOptions(options, documentSource) return! checker.ParseFile(path, snapshot) - // return! checker.ParseFile(path, source, options) + // return! checker.ParseFile(path, source, options) } /// Parse and check a source code file, returning a handle to the results @@ -274,7 +274,7 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe try let! snapshot = FSharpProjectSnapshot.FromOptions(options, documentSource) - let! (p,c) = checker.ParseAndCheckFileInProject(path,snapshot, userOpName = opName) + let! (p, c) = checker.ParseAndCheckFileInProject(path, snapshot, userOpName = opName) // let! (p, c) = checker.ParseAndCheckFileInProject(path, version, source, options, userOpName = opName) let parseErrors = p.Diagnostics |> Array.map (fun p -> p.Message) @@ -332,37 +332,40 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe | _ -> None member __.TryGetRecentCheckResultsForFile(file: string, options, source: ISourceText) = - let opName = sprintf "TryGetRecentCheckResultsForFile - %A" file + async { + let opName = sprintf "TryGetRecentCheckResultsForFile - %A" file - checkerLogger.info ( - Log.setMessage "{opName} - {hash}" - >> Log.addContextDestructured "opName" opName - >> Log.addContextDestructured "hash" (source.GetHashCode() |> int) + checkerLogger.info ( + Log.setMessage "{opName} - {hash}" + >> Log.addContextDestructured "opName" opName + >> Log.addContextDestructured "hash" (source.GetHashCode() |> int) - ) + ) - let options = clearProjectReferences options + let options = clearProjectReferences options - let result = - // TODO: Should this be snapshottable? (is that a word?) - checker.TryGetRecentCheckResultsForFile(UMX.untag file, options, sourceText = source, userOpName = opName) - |> Option.map (fun (pr, cr, version) -> - checkerLogger.info ( - Log.setMessage "{opName} - got results - {version}" - >> Log.addContextDestructured "opName" opName - >> Log.addContextDestructured "version" version - ) + let! snapshot = FSharpProjectSnapshot.FromOptions(options, documentSource) - ParseAndCheckResults(pr, cr, entityCache)) + return + checker.TryGetRecentCheckResultsForFile(UMX.untag file, snapshot, opName) + // checker.TryGetRecentCheckResultsForFile(UMX.untag file, options, sourceText = source, userOpName = opName) + |> Option.map (fun (pr, cr) -> + checkerLogger.info ( + Log.setMessage "{opName} - got results - {version}" + >> Log.addContextDestructured "opName" opName + ) - checkerLogger.info ( - Log.setMessage "{opName} - {hash} - cacheHit {cacheHit}" - >> Log.addContextDestructured "opName" opName - >> Log.addContextDestructured "hash" (source.GetHashCode() |> int) - >> Log.addContextDestructured "cacheHit" result.IsSome - ) + ParseAndCheckResults(pr, cr, entityCache)) + } + + // checkerLogger.info ( + // Log.setMessage "{opName} - {hash} - cacheHit {cacheHit}" + // >> Log.addContextDestructured "opName" opName + // >> Log.addContextDestructured "hash" (source.GetHashCode() |> int) + // >> Log.addContextDestructured "cacheHit" result.IsSome + // ) - result + // result member x.GetUsesOfSymbol ( @@ -406,17 +409,18 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe Log.setMessage "FindReferencesForSymbolInFile - {file}" >> Log.addContextDestructured "file" file ) + let! snapshot = FSharpProjectSnapshot.FromOptions(project, documentSource) return! checker.FindBackgroundReferencesInFile(file, snapshot, symbol, userOpName = "find references") - // return! - // checker.FindBackgroundReferencesInFile( - // file, - // project, - // symbol, - // canInvalidateProject = false, - // // fastCheck = true, - // userOpName = "find references" - // ) + // return! + // checker.FindBackgroundReferencesInFile( + // file, + // project, + // symbol, + // canInvalidateProject = false, + // // fastCheck = true, + // userOpName = "find references" + // ) } member this.GetDeclarations(fileName: string, source: ISourceText, options: FSharpProjectOptions, _) = diff --git a/src/FsAutoComplete.Core/CompilerServiceInterface.fsi b/src/FsAutoComplete.Core/CompilerServiceInterface.fsi index c82ad1f47..c4a2e3d1d 100644 --- a/src/FsAutoComplete.Core/CompilerServiceInterface.fsi +++ b/src/FsAutoComplete.Core/CompilerServiceInterface.fsi @@ -14,7 +14,8 @@ type Version = int type FSharpCompilerServiceChecker = new: - hasAnalyzers: bool * typecheckCacheSize: int64 * parallelReferenceResolution: bool * documentSource : DocumentSource -> FSharpCompilerServiceChecker + hasAnalyzers: bool * typecheckCacheSize: int64 * parallelReferenceResolution: bool * documentSource: DocumentSource -> + FSharpCompilerServiceChecker member DisableInMemoryProjectReferences: bool with get, set @@ -68,7 +69,7 @@ type FSharpCompilerServiceChecker = member TryGetLastCheckResultForFile: file: string -> ParseAndCheckResults option member TryGetRecentCheckResultsForFile: - file: string * options: FSharpProjectOptions * source: ISourceText -> ParseAndCheckResults option + file: string * options: FSharpProjectOptions * source: ISourceText -> Async member GetUsesOfSymbol: file: string * options: (string * FSharpProjectOptions) seq * symbol: FSharpSymbol -> diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index 374e358e4..e958dd965 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -66,8 +66,7 @@ type LoadedProject = | :? LoadedProject as other -> (x :> IEquatable<_>).Equals other | _ -> false - member x.GetSnapshot(documentSource) = - FSharpProjectSnapshot.FromOptions(x.FSharpProjectOptions, documentSource) + member x.GetSnapshot(documentSource) = FSharpProjectSnapshot.FromOptions(x.FSharpProjectOptions, documentSource) member x.SourceFiles = x.FSharpProjectOptions.SourceFiles member x.ProjectFileName = x.FSharpProjectOptions.ProjectFileName @@ -1155,8 +1154,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac } do - transact ( fun () -> - documentSource.Value <- DocumentSource.Custom documentSourceLookup ) + transact (fun () -> documentSource.Value <- DocumentSource.Custom documentSourceLookup) let fileShimChanges = openFilesWithChanges |> AMap.mapA (fun _ v -> v) // let cachedFileContents = cachedFileContents |> cmap.mapA (fun _ v -> v) @@ -1491,14 +1489,14 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let! checker = checker and! selectProject = projectSelector - return - result { + return! + asyncResult { let! projectOptions = projectOptions let! opts = selectProject.FindProject(file, projectOptions) return! checker.TryGetRecentCheckResultsForFile(file, opts.FSharpProjectOptions, info.Source) - |> Result.ofOption (fun () -> + |> AsyncResult.ofOption (fun () -> $"No recent typecheck results for {file}. This may be ok if the file has not been checked yet.") } }) From 3732365953575421390cf70ca52a1fa352ff79d9 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Fri, 15 Mar 2024 15:15:59 -0400 Subject: [PATCH 05/60] Fixing snapshots/caching --- src/FsAutoComplete.Core/AdaptiveExtensions.fs | 180 ++++-- .../AdaptiveExtensions.fsi | 12 +- .../CompilerServiceInterface.fs | 157 ++++-- .../CompilerServiceInterface.fsi | 25 +- .../FsAutoComplete.Core.fsproj | 2 + src/FsAutoComplete.Core/SemaphoreSlimLocks.fs | 38 ++ .../SemaphoreSlimLocks.fsi | 23 + src/FsAutoComplete.Core/SymbolLocation.fs | 6 +- .../LspServers/AdaptiveServerState.fs | 515 ++++++++++-------- .../LspServers/AdaptiveServerState.fsi | 3 +- .../LspServers/FSharpLspClient.fs | 35 +- 11 files changed, 614 insertions(+), 382 deletions(-) create mode 100644 src/FsAutoComplete.Core/SemaphoreSlimLocks.fs create mode 100644 src/FsAutoComplete.Core/SemaphoreSlimLocks.fsi diff --git a/src/FsAutoComplete.Core/AdaptiveExtensions.fs b/src/FsAutoComplete.Core/AdaptiveExtensions.fs index 96b49c766..f637ccca1 100644 --- a/src/FsAutoComplete.Core/AdaptiveExtensions.fs +++ b/src/FsAutoComplete.Core/AdaptiveExtensions.fs @@ -13,19 +13,18 @@ module AdaptiveExtensions = type CancellationTokenSource with - /// Communicates a request for cancellation. Ignores ObjectDisposedException member cts.TryCancel() = try cts.Cancel() - with :? ObjectDisposedException -> - () + with + | :? ObjectDisposedException + | :? NullReferenceException -> () - /// Releases all resources used by the current instance of the System.Threading.CancellationTokenSource class. member cts.TryDispose() = - try - cts.Dispose() - with _ -> - () + // try + cts.Dispose() + // with _ -> () + type TaskCompletionSource<'a> with @@ -148,7 +147,7 @@ module AVal = /// Creates an observable with the given object and will be executed whenever the object gets marked out-of-date. Note that it does not trigger when the object is currently out-of-date. /// /// The aval to get out-of-date information from. - let onOutOfDateWeak (aval: #aval<_>) = + let onOutOfDateWeak (aval: #IAdaptiveObject) = Observable.Create(fun (obs: IObserver<_>) -> aval.AddWeakMarkingCallback(fun _ -> obs.OnNext aval)) @@ -531,53 +530,90 @@ module AsyncAVal = let ofTask (value: Task<'a>) = ConstantVal(value) :> asyncaval<_> let ofCancellableTask (value: CancellableTask<'a>) = - let mutable cache: Option> = None { new AbstractVal<'a>() with - member x.Compute t = - if x.OutOfDate || Option.isNone cache then - let cts = new CancellationTokenSource() - - let cancel () = - cts.TryCancel() - cts.TryDispose() + member x.Compute _ = + let cts = new CancellationTokenSource() + + let cancel () = + cts.TryCancel() + cts.TryDispose() + + let real = + task { + try + return! value cts.Token + finally + cts.TryDispose() + } + + AdaptiveCancellableTask(cancel, real) } + :> asyncaval<_> - let real = - task { - try - return! value cts.Token - finally - cts.TryDispose() - } - cache <- Some(AdaptiveCancellableTask(cancel, real)) + let ofCancellableValueTask (value: CancellableValueTask<'a>) = - cache.Value } + { new AbstractVal<'a>() with + member x.Compute _ = + let cts = new CancellationTokenSource() + + let cancel () = + cts.TryCancel() + cts.TryDispose() + + let real = + task { + try + return! value cts.Token + finally + cts.TryDispose() + } + + AdaptiveCancellableTask(cancel, real) } :> asyncaval<_> - let ofAsync (value: Async<'a>) = - let mutable cache: Option> = None - { new AbstractVal<'a>() with - member x.Compute t = - if x.OutOfDate || Option.isNone cache then - let cts = new CancellationTokenSource() - let cancel () = - cts.TryCancel() - cts.TryDispose() + let _ofAsyncAValSeq (maxDegreeOfParallelism: int) (input: #seq<#asyncaval<'a>>) = + let mutable cache: option> = None - let real = - task { - try - return! Async.StartImmediateAsTask(value, cts.Token) - finally - cts.TryDispose() - } + { new AbstractVal<_>() with + member x.Compute t = + if x.OutOfDate || Option.isNone cache then + let ref = + RefCountingTaskCreator( + cancellableTask { + return! + input + |> Seq.map (fun v -> cancellableTask { return! v.GetValue t }) + |> CancellableTask.whenAllThrottled maxDegreeOfParallelism + } + ) - cache <- Some(AdaptiveCancellableTask(cancel, real)) + cache <- Some ref + ref.New() + else + cache.Value.New() } + :> asyncaval<_> - cache.Value } + let ofAsync (value: Async<'a>) = + { new AbstractVal<'a>() with + member x.Compute _ = + let cts = new CancellationTokenSource() + + let cancel () = + cts.TryCancel() + cts.TryDispose() + + let real = + task { + try + return! Async.StartImmediateAsTask(value, cts.Token) + finally + cts.TryDispose() + } + + AdaptiveCancellableTask(cancel, real) } :> asyncaval<_> /// @@ -589,7 +625,9 @@ module AsyncAVal = else { new AbstractVal<'a>() with member x.Compute t = - let real = Task.Run(fun () -> value.GetValue t) + let real = + // if out of date, assume it needs to run on the threadpool and not the current thread + Task.Run(fun () -> value.GetValue t) AdaptiveCancellableTask(id, real) } :> asyncaval<_> @@ -600,6 +638,7 @@ module AsyncAVal = /// let map (mapping: 'a -> CancellationToken -> Task<'b>) (input: asyncaval<'a>) = let mutable cache: option> = None + let mutable dataCache = ValueNone { new AbstractVal<'b>() with member x.Compute t = @@ -608,7 +647,13 @@ module AsyncAVal = RefCountingTaskCreator( cancellableTask { let! i = input.GetValue t - return! mapping i + + match dataCache with + | ValueSome(struct (oa, ob)) when Utils.cheapEqual oa i -> return ob + | _ -> + let! b = mapping i + dataCache <- ValueSome(struct (i, b)) + return b } ) @@ -631,13 +676,7 @@ module AsyncAVal = /// adaptive inputs. /// let mapSync (mapping: 'a -> CancellationToken -> 'b) (input: asyncaval<'a>) = - map - (fun a ct -> - if ct.IsCancellationRequested then - Task.FromCanceled<_>(ct) - else - Task.FromResult(mapping a ct)) - input + map (fun a ct -> Task.FromResult(mapping a ct)) input /// /// Returns a new async adaptive value that adaptively applies the mapping function to the given @@ -645,6 +684,7 @@ module AsyncAVal = /// let map2 (mapping: 'a -> 'b -> CancellationToken -> Task<'c>) (ca: asyncaval<'a>) (cb: asyncaval<'b>) = let mutable cache: option> = None + let mutable dataCache = ValueNone { new AbstractVal<'c>() with member x.Compute t = @@ -662,9 +702,15 @@ module AsyncAVal = ta.Cancel() tb.Cancel()) - let! va = ta.Task - let! vb = tb.Task - return! mapping va vb + let! ia = ta.Task + let! ib = tb.Task + + match dataCache with + | ValueSome(struct (va, vb, vc)) when Utils.cheapEqual va ia && Utils.cheapEqual vb ib -> return vc + | _ -> + let! vc = mapping ia ib ct + dataCache <- ValueSome(struct (ia, ib, vc)) + return vc } ) @@ -680,6 +726,7 @@ module AsyncAVal = let bind (mapping: 'a -> CancellationToken -> asyncaval<'b>) (value: asyncaval<'a>) = let mutable cache: option<_> = None let mutable innerCache: option<_> = None + let mutable outerDataCache: option<_> = None let mutable inputChanged = 0 let inners: ref>> = ref HashSet.empty @@ -702,9 +749,14 @@ module AsyncAVal = RefCountingTaskCreator( cancellableTask { let! i = value.GetValue t - let! ct = CancellableTask.getCancellationToken () - let inner = mapping i ct - return inner + + match outerDataCache with + | Some(struct (oa, ob)) when Utils.cheapEqual oa i -> return ob + | _ -> + let! ct = CancellableTask.getCancellationToken () + let inner = mapping i ct + outerDataCache <- Some(i, inner) + return inner } ) @@ -800,7 +852,8 @@ module AsyncAValBuilderExtensions = member inline x.Source(value: aval<'T>) = AsyncAVal.ofAVal value member inline x.Source(value: Task<'T>) = AsyncAVal.ofTask value member inline x.Source(value: Async<'T>) = AsyncAVal.ofAsync value - member inline x.Source(value: CancellableTask<'T>) = AsyncAVal.ofCancellableTask value + member inline x.Source([] value: CancellableTask<'T>) = AsyncAVal.ofCancellableTask value + member inline x.Source([] value: CancellableValueTask<'T>) = AsyncAVal.ofCancellableValueTask value member inline x.BindReturn(value: asyncaval<'T1>, [] mapping: 'T1 -> CancellationToken -> 'T2) = AsyncAVal.mapSync (fun data ctok -> mapping data ctok) value @@ -854,3 +907,10 @@ module AMapAsync = | Some x -> return! x | None -> return Error reason } + + + let _filterValuesByKey (key: 'Key) (map: amap<'Key, #asyncaval<'Value>>) = + asyncAVal { + let! values = map |> AMap.filter (fun k _ -> k = key) |> AMap.toASetValues |> ASet.toAVal + return! AsyncAVal._ofAsyncAValSeq 1 values + } diff --git a/src/FsAutoComplete.Core/AdaptiveExtensions.fsi b/src/FsAutoComplete.Core/AdaptiveExtensions.fsi index 5a758f0cc..32991c961 100644 --- a/src/FsAutoComplete.Core/AdaptiveExtensions.fsi +++ b/src/FsAutoComplete.Core/AdaptiveExtensions.fsi @@ -2,6 +2,14 @@ namespace FsAutoComplete.Adaptive [] module AdaptiveExtensions = + + type System.Threading.CancellationTokenSource with + + /// Communicates a request for cancellation. Ignores ObjectDisposedException + member TryCancel: unit -> unit + /// Releases all resources used by the current instance of the System.Threading.CancellationTokenSource class. + member TryDispose: unit -> unit + type FSharp.Data.Adaptive.ChangeableHashMap<'Key, 'Value> with /// @@ -63,7 +71,7 @@ module AVal = /// Creates an observable with the given object and will be executed whenever the object gets marked out-of-date. Note that it does not trigger when the object is currently out-of-date. /// /// The aval to get out-of-date information from. - val onOutOfDateWeak: aval: 'a -> System.IObservable<'a> when 'a :> FSharp.Data.Adaptive.aval<'b> + val onOutOfDateWeak: aval: 'a -> System.IObservable<'a> when 'a :> FSharp.Data.Adaptive.IAdaptiveObject /// Creates an observable on the aval that will be executed whenever the avals value changed. /// The aval to get out-of-date information from. @@ -268,6 +276,7 @@ module AsyncAVal = val ofTask: value: System.Threading.Tasks.Task<'a> -> asyncaval<'a> val ofCancellableTask: value: IcedTasks.CancellableTasks.CancellableTask<'a> -> asyncaval<'a> + val ofCancellableValueTask: value: IcedTasks.CancellableValueTasks.CancellableValueTask<'a> -> asyncaval<'a> val ofAsync: value: Async<'a> -> asyncaval<'a> @@ -355,6 +364,7 @@ module AsyncAValBuilderExtensions = member inline Source: value: System.Threading.Tasks.Task<'T> -> asyncaval<'T> member inline Source: value: Async<'T> -> asyncaval<'T> member inline Source: value: CancellableTask<'T> -> asyncaval<'T> + member inline Source: value: CancellableValueTask<'T> -> asyncaval<'T> member inline BindReturn: value: asyncaval<'T1> * mapping: ('T1 -> System.Threading.CancellationToken -> 'T2) -> asyncaval<'T2> diff --git a/src/FsAutoComplete.Core/CompilerServiceInterface.fs b/src/FsAutoComplete.Core/CompilerServiceInterface.fs index 76cdd345d..6b51be745 100644 --- a/src/FsAutoComplete.Core/CompilerServiceInterface.fs +++ b/src/FsAutoComplete.Core/CompilerServiceInterface.fs @@ -12,6 +12,10 @@ open FSharp.Compiler.Symbols open Microsoft.Extensions.Caching.Memory open System open FsToolkit.ErrorHandling +open FSharp.Compiler.CodeAnalysis.ProjectSnapshot +open System.Collections.Generic +open System.Threading +open IcedTasks @@ -89,11 +93,6 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe | StartsWith "--load:" file -> args, Array.append files [| file |] | arg -> Array.append args [| arg |], files) - let clearProjectReferences (opts: FSharpProjectOptions) = - if disableInMemoryProjectReferences then - { opts with ReferencedProjects = [||] } - else - opts /// ensures that any user-configured include/load files are added to the typechecking context let addLoadedFiles (projectOptions: FSharpProjectOptions) = @@ -124,6 +123,68 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe | Reference r -> $"-r:{Path.GetFullPath r}" | opt -> opt) } + let snapshotAccumulatorDef = + Dictionary() + + let optionsLocker = new SemaphoreSlim(1, 1) + + member _.FromOptions(options: FSharpProjectOptions array, documentSource: DocumentSource) = + asyncEx { + + let! ct = Async.CancellationToken + use! _lock = optionsLocker.LockAsync(ct) + snapshotAccumulatorDef.Clear() + // TODO: Figure out why we can't just recreate the subgraph of snapshots + // for opt in options do + // snapshotAccumulatorDef.Remove(opt) |> ignore + // checker.ClearCache(options) + return! + options + |> Array.map (fun opt -> + async { + let! sn = + FSharpProjectSnapshot.FromOptions( + opt, + (fun _ fileName -> + + FSharpFileSnapshot.CreateFromDocumentSource(fileName, documentSource) + |> async.Return), + snapshotAccumulator = snapshotAccumulatorDef + ) + + return opt, sn + }) + |> Async.Sequential + } + + member _.FromOption + ( + options: FSharpProjectOptions, + documentSource: DocumentSource, + ?snapshotAccumulator: Dictionary + ) = + asyncEx { + let! ct = Async.CancellationToken + use! _lock = optionsLocker.LockAsync(ct) + let useDefaultCache = snapshotAccumulator.IsNone + let snapshotAccumulator = defaultArg snapshotAccumulator snapshotAccumulatorDef + + if useDefaultCache then + snapshotAccumulator.Remove options |> ignore // We need to recreate the snapshot when files change + + let! sn = + FSharpProjectSnapshot.FromOptions( + options, + (fun _ fileName -> + + FSharpFileSnapshot.CreateFromDocumentSource(fileName, documentSource) + |> async.Return), + snapshotAccumulator = snapshotAccumulator + ) + + return sn + } + member __.DisableInMemoryProjectReferences with get () = disableInMemoryProjectReferences and set (value) = disableInMemoryProjectReferences <- value @@ -226,6 +287,7 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe member _.ClearCaches() = lastCheckResults.Dispose() lastCheckResults <- memoryCache () + snapshotAccumulatorDef.Clear() checker.InvalidateAll() checker.ClearLanguageServiceRootCachesAndCollectAndFinalizeAllTransients() @@ -234,17 +296,18 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe /// The source to be parsed. /// Parsing options for the project or script. /// - member __.ParseFile(filePath: string, source: ISourceText, options: FSharpProjectOptions) = + member x.ParseFile(filePath: string, source: ISourceText, options: FSharpProjectSnapshot) = async { + let _source = source + checkerLogger.info ( Log.setMessage "ParseFile - {file}" >> Log.addContextDestructured "file" filePath ) let path = UMX.untag filePath - let! snapshot = FSharpProjectSnapshot.FromOptions(options, documentSource) - return! checker.ParseFile(path, snapshot) - // return! checker.ParseFile(path, source, options) + // let! snapshot = x.FromOption(options, documentSource) + return! checker.ParseFile(path, options) } /// Parse and check a source code file, returning a handle to the results @@ -253,29 +316,32 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe /// The source for the file. /// The options for the project or script. /// Determines if the typecheck should be cached for autocompletions. + /// A dictionary to store the snapshots for the project options /// Note: all files except the one being checked are read from the FileSystem API /// Result of ParseAndCheckResults - member __.ParseAndCheckFileInProject + member x.ParseAndCheckFileInProject ( filePath: string, version: int, source: ISourceText, - options: FSharpProjectOptions, - ?shouldCache: bool + options: FSharpProjectSnapshot, + ?shouldCache: bool, + ?snapshotAccumulator: Dictionary ) = asyncResult { + let _source = source + let _snapshotAccumulator = snapshotAccumulator + let _version = version let shouldCache = defaultArg shouldCache false let opName = sprintf "ParseAndCheckFileInProject - %A" filePath checkerLogger.info (Log.setMessage "{opName}" >> Log.addContextDestructured "opName" opName) - let options = clearProjectReferences options let path = UMX.untag filePath try - let! snapshot = FSharpProjectSnapshot.FromOptions(options, documentSource) - let! (p, c) = checker.ParseAndCheckFileInProject(path, snapshot, userOpName = opName) - // let! (p, c) = checker.ParseAndCheckFileInProject(path, version, source, options, userOpName = opName) + // let! snapshot = x.FromOptions(options, documentSource, ?snapshotAccumulator = snapshotAccumulator) + let! (p, c) = checker.ParseAndCheckFileInProject(path, options, userOpName = opName) let parseErrors = p.Diagnostics |> Array.map (fun p -> p.Message) @@ -331,7 +397,12 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe | (true, v) -> Some v | _ -> None - member __.TryGetRecentCheckResultsForFile(file: string, options, source: ISourceText) = + member x.TryGetRecentCheckResultsForFile + ( + file: string, + options: FSharpProjectOptions, + source: ISourceText + ) = async { let opName = sprintf "TryGetRecentCheckResultsForFile - %A" file @@ -342,13 +413,10 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe ) - let options = clearProjectReferences options - - let! snapshot = FSharpProjectSnapshot.FromOptions(options, documentSource) + let! snapshot = x.FromOption(options, documentSource) return checker.TryGetRecentCheckResultsForFile(UMX.untag file, snapshot, opName) - // checker.TryGetRecentCheckResultsForFile(UMX.untag file, options, sourceText = source, userOpName = opName) |> Option.map (fun (pr, cr) -> checkerLogger.info ( Log.setMessage "{opName} - got results - {version}" @@ -358,15 +426,6 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe ParseAndCheckResults(pr, cr, entityCache)) } - // checkerLogger.info ( - // Log.setMessage "{opName} - {hash} - cacheHit {cacheHit}" - // >> Log.addContextDestructured "opName" opName - // >> Log.addContextDestructured "hash" (source.GetHashCode() |> int) - // >> Log.addContextDestructured "cacheHit" result.IsSome - // ) - - // result - member x.GetUsesOfSymbol ( file: string, @@ -382,10 +441,8 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe match FSharpCompilerServiceChecker.GetDependingProjects file options with | None -> return [||] | Some(opts, []) -> - let opts = clearProjectReferences opts - let! snapshot = FSharpProjectSnapshot.FromOptions(opts, documentSource) + let! snapshot = x.FromOption(opts, documentSource) let! res = checker.ParseAndCheckProject(snapshot) - // let! res = checker.ParseAndCheckProject opts return res.GetUsesOfSymbol symbol | Some(opts, dependentProjects) -> let! res = @@ -393,9 +450,8 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe |> List.map (fun (opts) -> async { - let! snapshot = FSharpProjectSnapshot.FromOptions(opts, documentSource) + let! snapshot = x.FromOption(opts, documentSource) let! res = checker.ParseAndCheckProject(snapshot) - // let! res = checker.ParseAndCheckProject opts return res.GetUsesOfSymbol symbol }) |> Async.parallel75 @@ -403,36 +459,27 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe return res |> Array.concat } - member _.FindReferencesForSymbolInFile(file, project, symbol) = + member x.FindReferencesForSymbolInFile(file, project: FSharpProjectOptions, symbol) = async { checkerLogger.info ( Log.setMessage "FindReferencesForSymbolInFile - {file}" >> Log.addContextDestructured "file" file ) - let! snapshot = FSharpProjectSnapshot.FromOptions(project, documentSource) + let! snapshot = x.FromOption(project, documentSource) return! checker.FindBackgroundReferencesInFile(file, snapshot, symbol, userOpName = "find references") - // return! - // checker.FindBackgroundReferencesInFile( - // file, - // project, - // symbol, - // canInvalidateProject = false, - // // fastCheck = true, - // userOpName = "find references" - // ) } - member this.GetDeclarations(fileName: string, source: ISourceText, options: FSharpProjectOptions, _) = - async { - checkerLogger.info ( - Log.setMessage "GetDeclarations - {file}" - >> Log.addContextDestructured "file" fileName - ) + // member this.GetDeclarations(fileName: string, source: ISourceText, options: FSharpProjectOptions, _) = + // async { + // checkerLogger.info ( + // Log.setMessage "GetDeclarations - {file}" + // >> Log.addContextDestructured "file" fileName + // ) - let! parseResult = this.ParseFile(fileName, source, options) - return parseResult.GetNavigationItems().Declarations - } + // let! parseResult = this.ParseFile(fileName, source, options) + // return parseResult.GetNavigationItems().Declarations + // } member __.SetDotnetRoot(dotnetBinary: FileInfo, cwd: DirectoryInfo) = match Ionide.ProjInfo.SdkDiscovery.versionAt cwd dotnetBinary with diff --git a/src/FsAutoComplete.Core/CompilerServiceInterface.fsi b/src/FsAutoComplete.Core/CompilerServiceInterface.fsi index c4a2e3d1d..bf3c145fb 100644 --- a/src/FsAutoComplete.Core/CompilerServiceInterface.fsi +++ b/src/FsAutoComplete.Core/CompilerServiceInterface.fsi @@ -1,6 +1,7 @@ namespace FsAutoComplete open System.IO +open System.Collections.Generic open FSharp.Compiler.CodeAnalysis open Utils open FSharp.Compiler.Text @@ -35,13 +36,23 @@ type FSharpCompilerServiceChecker = /// This function is called when the entire environment is known to have changed for reasons not encoded in the ProjectOptions of any project/compilation. member ClearCaches: unit -> unit + member FromOptions: + options: FSharpProjectOptions array * documentSource: DocumentSource -> + Async<(FSharpProjectOptions * FSharpProjectSnapshot) array> + + member FromOption: + options: FSharpProjectOptions * + documentSource: DocumentSource * + ?snapshotAccumulator: Dictionary -> + Async + /// Parses a source code for a file and caches the results. Returns an AST that can be traversed for various features. /// The path for the file. The file name is used as a module name for implicit top level modules (e.g. in scripts). /// The source to be parsed. /// Parsing options for the project or script. /// member ParseFile: - filePath: string * source: ISourceText * options: FSharpProjectOptions -> Async + filePath: string * source: ISourceText * options: FSharpProjectSnapshot -> Async /// Parse and check a source code file, returning a handle to the results /// The name of the file in the project whose source is being checked. @@ -49,14 +60,16 @@ type FSharpCompilerServiceChecker = /// The source for the file. /// The options for the project or script. /// Determines if the typecheck should be cached for autocompletions. + /// A dictionary of FSharpProjectOptions to FSharpProjectSnapshot that will be used to accumulate snapshots for the project. This is used to avoid re-reading the project file from disk for every file in the project. /// Note: all files except the one being checked are read from the FileSystem API /// Result of ParseAndCheckResults member ParseAndCheckFileInProject: filePath: string * version: int * source: ISourceText * - options: FSharpProjectOptions * - ?shouldCache: bool -> + options: FSharpProjectSnapshot * + ?shouldCache: bool * + ?snapshotAccumulator: Dictionary -> Async> /// @@ -78,9 +91,9 @@ type FSharpCompilerServiceChecker = member FindReferencesForSymbolInFile: file: string * project: FSharpProjectOptions * symbol: FSharpSymbol -> Async> - member GetDeclarations: - fileName: string * source: ISourceText * options: FSharpProjectOptions * version: 'a -> - Async + // member GetDeclarations: + // fileName: string * source: ISourceText * options: FSharpProjectOptions * version: 'a -> + // Async member SetDotnetRoot: dotnetBinary: FileInfo * cwd: DirectoryInfo -> unit member GetDotnetRoot: unit -> DirectoryInfo option diff --git a/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj b/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj index 115b3ae1e..7c17d65c9 100644 --- a/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj +++ b/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj @@ -11,6 +11,8 @@ + + diff --git a/src/FsAutoComplete.Core/SemaphoreSlimLocks.fs b/src/FsAutoComplete.Core/SemaphoreSlimLocks.fs new file mode 100644 index 000000000..17c7e3a5e --- /dev/null +++ b/src/FsAutoComplete.Core/SemaphoreSlimLocks.fs @@ -0,0 +1,38 @@ +namespace FsAutoComplete + +open System +open System.Threading.Tasks + +/// +/// An awaitable wrapper around a task whose result is disposable. The wrapper is not disposable, so this prevents usage errors like "use _lock = myAsync()" when the appropriate usage should be "use! _lock = myAsync())". +/// +[] +type AwaitableDisposable<'T when 'T :> IDisposable>(t: Task<'T>) = + member x.GetAwaiter() = t.GetAwaiter() + member x.AsTask() = t + static member op_Implicit(source: AwaitableDisposable<'T>) = source.AsTask() + +[] +module SemaphoreSlimExtensions = + open System.Threading + // Based on https://gist.github.com/StephenCleary/7dd1c0fc2a6594ba0ed7fb7ad6b590d6 + // and https://gist.github.com/brendankowitz/5949970076952746a083054559377e56 + type SemaphoreSlim with + + member x.LockAsync(?ct: CancellationToken) = + AwaitableDisposable( + task { + let ct = defaultArg ct CancellationToken.None + let t = x.WaitAsync(ct) + + do! t + + return + { new IDisposable with + member _.Dispose() = + // only release if the task completed successfully + // otherwise, we could be releasing a semaphore that was never acquired + if t.Status = TaskStatus.RanToCompletion then + x.Release() |> ignore } + } + ) diff --git a/src/FsAutoComplete.Core/SemaphoreSlimLocks.fsi b/src/FsAutoComplete.Core/SemaphoreSlimLocks.fsi new file mode 100644 index 000000000..d8b22d3b2 --- /dev/null +++ b/src/FsAutoComplete.Core/SemaphoreSlimLocks.fsi @@ -0,0 +1,23 @@ +namespace FsAutoComplete + +open System +open System.Threading.Tasks + +/// +/// An awaitable wrapper around a task whose result is disposable. The wrapper is not disposable, so this prevents usage errors like "use _lock = myAsync()" when the appropriate usage should be "use! _lock = myAsync())". +/// +[] +[] +type AwaitableDisposable<'T when 'T :> IDisposable> = + new: t: Task<'T> -> AwaitableDisposable<'T> + member GetAwaiter: unit -> Runtime.CompilerServices.TaskAwaiter<'T> + member AsTask: unit -> Task<'T> + static member op_Implicit: source: AwaitableDisposable<'T> -> Task<'T> + +[] +module SemaphoreSlimExtensions = + open System.Threading + + type SemaphoreSlim with + + member LockAsync: ?ct: CancellationToken -> AwaitableDisposable diff --git a/src/FsAutoComplete.Core/SymbolLocation.fs b/src/FsAutoComplete.Core/SymbolLocation.fs index 8b6e33ce5..a598afd25 100644 --- a/src/FsAutoComplete.Core/SymbolLocation.fs +++ b/src/FsAutoComplete.Core/SymbolLocation.fs @@ -17,8 +17,7 @@ let getDeclarationLocation currentDocument: IFSACSourceText, getProjectOptions, projectsThatContainFile: string -> Async, - getDependentProjectsOfProjects - // state: State + getDependentProjectsOfProjects: FSharpProjectOptions list -> Async ) : Async> = asyncOption { @@ -61,8 +60,7 @@ let getDeclarationLocation match! projectsThatContainFile (taggedFilePath) with | [] -> return! None | projectsThatContainFile -> - let projectsThatDependOnContainingProjects = - getDependentProjectsOfProjects projectsThatContainFile + let! projectsThatDependOnContainingProjects = getDependentProjectsOfProjects projectsThatContainFile match projectsThatDependOnContainingProjects with | [] -> return (SymbolDeclarationLocation.Projects(projectsThatContainFile, isSymbolLocalForProject)) diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index e958dd965..5ce218b7a 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -415,7 +415,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac checkUnnecessaryParentheses ] async { - do! analyzers |> Async.parallel75 |> Async.Ignore + do! analyzers |> Async.Sequential |> Async.Ignore do! lspClient.NotifyDocumentAnalyzed @@ -467,14 +467,18 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac Loggers.analyzers.error (Log.setMessageI $"Run failed for {file:file}" >> Log.addExn ex) } + let analyzerLock = new SemaphoreSlim(1, 1) + do disposables.Add <| fileChecked.Publish.Subscribe(fun (parseAndCheck, volatileFile, ct) -> if volatileFile.Source.Length = 0 then () // Don't analyze and error on an empty file else - async { + asyncEx { let config = config |> AVal.force + let! ct = Async.CancellationToken + use! _lock = analyzerLock.LockAsync(ct) do! builtInCompilerAnalyzers config volatileFile parseAndCheck do! runAnalyzers config parseAndCheck volatileFile @@ -715,11 +719,11 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac |> AVal.map (fun writeTime -> filePath, writeTime) let readFileFromDisk lastTouched (file: string) = - async { + cancellableValueTask { if File.Exists(UMX.untag file) then use s = File.openFileStreamForReadingAsync file - let! source = sourceTextFactory.Create(file, s) |> Async.AwaitCancellableValueTask + let! source = sourceTextFactory.Create(file, s) return { LastTouched = lastTouched @@ -772,6 +776,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac { new IDisposable with member this.Dispose() : unit = () } + let adaptiveWorkspacePaths = workspacePaths |> AVal.map (fun wsp -> @@ -816,16 +821,26 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac tryFindProp "MSBuildAllProjects" props |> Option.map (fun v -> v.Split(';', StringSplitOptions.RemoveEmptyEntries)) + + let loadedProjectOptions = - aval { - let! loader = loader - and! wsp = adaptiveWorkspacePaths + asyncAVal { + let! loader = + loader + |> addAValLogging (fun () -> logger.info (Log.setMessage "Loading projects because WorkspaceLoader change")) + + and! wsp = + adaptiveWorkspacePaths + |> addAValLogging (fun () -> + logger.info (Log.setMessage "Loading projects because adaptiveWorkspacePaths change")) + + and! binlogConfig = + binlogConfig + |> addAValLogging (fun () -> logger.info (Log.setMessage "Loading projects because binlogConfig change")) match wsp with | AdaptiveWorkspaceChosen.NotChosen -> return [] | AdaptiveWorkspaceChosen.Projs projects -> - let! binlogConfig = binlogConfig - let! projectOptions = projects |> AMap.mapWithAdditionalDependencies (fun projects -> @@ -950,19 +965,19 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac /// This should not be used inside the adaptive evaluation of other AdaptiveObjects since it does not track dependencies. /// /// A list of FSharpProjectOptions - let forceLoadProjects () = loadedProjectOptions |> AVal.force + let forceLoadProjects () = loadedProjectOptions |> AsyncAVal.forceAsync do // Reload Projects with some debouncing if `loadedProjectOptions` is out of date. AVal.Observable.onOutOfDateWeak loadedProjectOptions |> Observable.throttleOn Concurrency.NewThreadScheduler.Default (TimeSpan.FromMilliseconds(200.)) |> Observable.observeOn Concurrency.NewThreadScheduler.Default - |> Observable.subscribe (fun _ -> forceLoadProjects () |> ignore>) + |> Observable.subscribe (fun _ -> forceLoadProjects () |> Async.Ignore> |> Async.Start) |> disposables.Add let sourceFileToProjectOptions = - aval { + asyncAVal { let! options = loadedProjectOptions return @@ -973,8 +988,9 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac |> Array.toList) |> List.groupByFst + |> AMap.ofList + } - |> AMap.ofAVal let openFilesTokens = @@ -1067,29 +1083,22 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac }) - let cancelToken filePath (cts: CancellationTokenSource) = - - try - logger.info ( - Log.setMessage "Cancelling {filePath} - {version}" - >> Log.addContextDestructured "filePath" filePath - // >> Log.addContextDestructured "version" oldFile.Version - ) + let cancelToken filePath version (cts: CancellationTokenSource) = + logger.info ( + Log.setMessage "Cancelling {filePath} - {version}" + >> Log.addContextDestructured "filePath" filePath + >> Log.addContextDestructured "version" version + ) - cts.Cancel() - cts.Dispose() - with - | :? OperationCanceledException - | :? ObjectDisposedException as e when e.Message.Contains("CancellationTokenSource has been disposed") -> - // ignore if already cancelled - () + cts.TryCancel() + cts.TryDispose() - let resetCancellationToken (filePath: string) = + let resetCancellationToken (filePath: string) version = let adder _ = new CancellationTokenSource() let updater _key value = - cancelToken filePath value + cancelToken filePath version value new CancellationTokenSource() openFilesTokens.AddOrUpdate(filePath, adder, updater) @@ -1101,48 +1110,107 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let updater _ (v: cval<_>) = v.Value <- file - resetCancellationToken file.FileName + resetCancellationToken file.FileName file.Version transact (fun () -> openFiles.AddOrElse(file.Source.FileName, adder, updater)) - let updateTextChanges filePath p = + let updateTextChanges filePath ((changes: DidChangeTextDocumentParams, _) as p) = + let adder _ = cset<_> [ p ] let updater _ (v: cset<_>) = v.Add p |> ignore - resetCancellationToken filePath + resetCancellationToken filePath changes.TextDocument.Version transact (fun () -> textChanges.AddOrElse(filePath, adder, updater)) let isFileOpen file = openFiles |> AMap.tryFindA file |> AVal.map (Option.isSome) - let findFileInOpenFiles file = openFilesWithChanges |> AMap.tryFindA file - let forceFindOpenFile filePath = findFileInOpenFiles filePath |> AVal.force + let openFilesToChangesAndProjectOptions = + openFilesWithChanges + |> AMapAsync.mapAVal (fun filePath file ctok -> + asyncAVal { + if Utils.isAScript (UMX.untag filePath) then + let! (checker: FSharpCompilerServiceChecker) = checker + and! tfmConfig = tfmConfig - let forceFindOpenFileOrRead file = - asyncOption { + let! projs = + asyncResult { + let cts = getOpenFileTokenOrDefault filePath + use linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ctok, cts) - match findFileInOpenFiles file |> AVal.force with - | Some s -> return s - | None -> - // TODO: Log how many times this kind area gets hit and possibly if this should be rethought - try - logger.debug ( - Log.setMessage "forceFindOpenFileOrRead else - {file}" - >> Log.addContextDestructured "file" file - ) + try + let! (opts, errors) = + checker.GetProjectOptionsFromScript(filePath, file.Source, tfmConfig) + |> Async.withCancellation linkedCts.Token - let lastTouched = File.getLastWriteTimeOrDefaultNow file + opts |> scriptFileProjectOptions.Trigger + let diags = errors |> Array.ofList |> Array.map fcsErrorToDiagnostic - return! readFileFromDisk lastTouched file + diagnosticCollections.SetFor( + Path.LocalPathToUri filePath, + "F# Script Project Options", + file.Version, + diags + ) - with e -> - logger.warn ( - Log.setMessage "Could not read file {file}" - >> Log.addContextDestructured "file" file - >> Log.addExn e - ) + return + { FSharpProjectOptions = opts + LanguageVersion = LanguageVersionShim.fromFSharpProjectOptions opts } + |> List.singleton + with e -> + logger.error ( + Log.setMessage "Error getting project options for {filePath}" + >> Log.addContextDestructured "filePath" filePath + >> Log.addExn e + ) + + return! Error $"Error getting project options for {filePath} - {e.Message}" + } + + return file, projs + else + let! sourceFileToProjectOptions = sourceFileToProjectOptions + + let! projs = + sourceFileToProjectOptions + |> AMap.tryFindR + $"Couldn't find {filePath} in LoadedProjects. Have the projects loaded yet or have you tried restoring your project/solution?" + filePath + + return file, projs + }) - return! None + let allFSharpFilesAndProjectOptions = + asyncAVal { + let wins = + openFilesToChangesAndProjectOptions + |> AMap.map (fun _k v -> v |> AsyncAVal.mapSync (fun (file, projects) _ -> file, projects)) + + let! sourceFileToProjectOptions = sourceFileToProjectOptions + + let loses = + sourceFileToProjectOptions + |> AMap.map (fun filePath v -> + asyncAVal { + let! file = getLatestFileChange filePath + return (file, Ok v) + }) + + return AMap.union loses wins + } + + + + let forceFindOpenFileOrRead (file: string) : Async> = + asyncOption { + let! allFSharpFilesAndProjectOptions = allFSharpFilesAndProjectOptions |> AsyncAVal.forceAsync + + let! (file, _) = + allFSharpFilesAndProjectOptions + |> AMapAsync.tryFindA file + |> AsyncAVal.forceAsync + + return file } |> Async.map (Result.ofOption (fun () -> $"Could not read file: {file}")) @@ -1153,30 +1221,30 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac return file.Source :> ISourceText } - do - transact (fun () -> documentSource.Value <- DocumentSource.Custom documentSourceLookup) - let fileShimChanges = openFilesWithChanges |> AMap.mapA (fun _ v -> v) - // let cachedFileContents = cachedFileContents |> cmap.mapA (fun _ v -> v) + do transact (fun () -> documentSource.Value <- DocumentSource.Custom documentSourceLookup) + let fileShimChanges = openFilesWithChanges |> AMap.mapA (fun _ v -> v) + // let cachedFileContents = cachedFileContents |> cmap.mapA (fun _ v -> v) - let filesystemShim file = - // GetLastWriteTimeShim gets called _a lot_ and when we do checks on save we use Async.Parallel for type checking. - // Adaptive uses lots of locks under the covers, so many threads can get blocked waiting for data. - // flattening openFilesWithChanges makes this check a lot quicker as it's not needing to recalculate each value. + let filesystemShim file = + // GetLastWriteTimeShim gets called _a lot_ and when we do checks on save we use Async.Parallel for type checking. + // Adaptive uses lots of locks under the covers, so many threads can get blocked waiting for data. + // flattening openFilesWithChanges makes this check a lot quicker as it's not needing to recalculate each value. - fileShimChanges |> AMap.force |> HashMap.tryFind file + fileShimChanges |> AMap.force |> HashMap.tryFind file + do FSharp.Compiler.IO.FileSystemAutoOpens.FileSystem <- FileSystem(FSharp.Compiler.IO.FileSystemAutoOpens.FileSystem, filesystemShim) /// Parses a source code for a file and caches the results. Returns an AST that can be traversed for various features. /// The FSharpCompilerServiceChecker. /// The source to be parsed. - /// Parsing options for the project or script /// The options for the project or script. + /// /// - let parseFile (checker: FSharpCompilerServiceChecker) (source: VolatileFile) parseOpts options = + let parseFile (checker: FSharpCompilerServiceChecker) (source: VolatileFile) options snap = async { - let! result = checker.ParseFile(source.FileName, source.Source, options) + let! result = checker.ParseFile(source.FileName, source.Source, snap) let! ct = Async.CancellationToken fileParsed.Trigger(result, options, ct) @@ -1190,19 +1258,24 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac asyncAVal { let! projects = loadedProjectOptions and! (checker: FSharpCompilerServiceChecker) = checker + and! documentSource = documentSource + let fromOpts opts = checker.FromOptions(opts, documentSource) + + let! projects = + projects + |> List.map (fun p -> p.FSharpProjectOptions) + |> List.toArray + |> fromOpts return projects - |> Array.ofList - |> Array.Parallel.collect (fun p -> - let parseOpts = Utils.projectOptionsToParseOptions p.FSharpProjectOptions - p.SourceFiles |> Array.Parallel.map (fun s -> p, parseOpts, s)) - |> Array.Parallel.map (fun (opts, parseOpts, fileName) -> + |> Array.collect (fun (p, snap) -> p.SourceFiles |> Array.map (fun s -> p, snap, s)) + |> Array.map (fun (opts, snap, fileName) -> let fileName = UMX.tag fileName asyncResult { let! file = forceFindOpenFileOrRead fileName - return! parseFile checker file parseOpts opts.FSharpProjectOptions + return! parseFile checker file opts snap } |> Async.map Result.toOption) |> Async.parallel75 @@ -1211,108 +1284,57 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let forceFindSourceText filePath = forceFindOpenFileOrRead filePath |> AsyncResult.map (fun f -> f.Source) - let openFilesToChangesAndProjectOptions = - openFilesWithChanges - |> AMapAsync.mapAVal (fun filePath file ctok -> - asyncAVal { - if Utils.isAScript (UMX.untag filePath) then - let! (checker: FSharpCompilerServiceChecker) = checker - and! tfmConfig = tfmConfig - - let! projs = - asyncResult { - let cts = getOpenFileTokenOrDefault filePath - use linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ctok, cts) - - try - let! (opts, errors) = - checker.GetProjectOptionsFromScript(filePath, file.Source, tfmConfig) - |> Async.withCancellation linkedCts.Token - - opts |> scriptFileProjectOptions.Trigger - let diags = errors |> Array.ofList |> Array.map fcsErrorToDiagnostic - - diagnosticCollections.SetFor( - Path.LocalPathToUri filePath, - "F# Script Project Options", - file.Version, - diags - ) - - return - { FSharpProjectOptions = opts - LanguageVersion = LanguageVersionShim.fromFSharpProjectOptions opts } - |> List.singleton - with e -> - logger.error ( - Log.setMessage "Error getting project options for {filePath}" - >> Log.addContextDestructured "filePath" filePath - >> Log.addExn e - ) - - return! Error $"Error getting project options for {filePath} - {e.Message}" - } - - return file, projs - else - let! projs = - sourceFileToProjectOptions - |> AMap.tryFindR - $"Couldn't find {filePath} in LoadedProjects. Have the projects loaded yet or have you tried restoring your project/solution?" - filePath + let allFilesToFSharpProjectOptions = + asyncAVal { + let! allFSharpFilesAndProjectOptions = allFSharpFilesAndProjectOptions + return + allFSharpFilesAndProjectOptions + |> AMapAsync.mapAsyncAVal (fun _filePath (_file, options) _ctok -> AsyncAVal.constant options) + } - return file, projs - }) - let allFSharpFilesAndProjectOptions = - let wins = - openFilesToChangesAndProjectOptions - |> AMap.map (fun _k v -> v |> AsyncAVal.mapSync (fun (file, projects) _ -> file, projects)) + let allFilesParsed = + asyncAVal { + let! allFSharpFilesAndProjectOptions = allFSharpFilesAndProjectOptions - let loses = - sourceFileToProjectOptions - |> AMap.map (fun filePath v -> - asyncAVal { - let! file = getLatestFileChange filePath - return (file, Ok v) - }) + return + allFSharpFilesAndProjectOptions + |> AMapAsync.mapAsyncAVal (fun _filePath (file, options: Result) _ctok -> + asyncAVal { + let! (checker: FSharpCompilerServiceChecker) = checker + and! selectProject = projectSelector + and! documentSource = documentSource - AMap.union loses wins + return! + asyncResult { + let! options = options + let! project = selectProject.FindProject(file.FileName, options) + let! snap = checker.FromOption(project.FSharpProjectOptions, documentSource) + let options = project.FSharpProjectOptions + return! parseFile checker file options snap + } - let allFilesToFSharpProjectOptions = - allFSharpFilesAndProjectOptions - |> AMapAsync.mapAsyncAVal (fun _filePath (_file, options) _ctok -> AsyncAVal.constant options) + }) + } - let allFilesParsed = - allFSharpFilesAndProjectOptions - |> AMapAsync.mapAsyncAVal (fun _filePath (file, options: Result) _ctok -> - asyncAVal { - let! (checker: FSharpCompilerServiceChecker) = checker - and! selectProject = projectSelector - return! - asyncResult { - let! options = options - let! project = selectProject.FindProject(file.FileName, options) - let options = project.FSharpProjectOptions - let parseOpts = Utils.projectOptionsToParseOptions project.FSharpProjectOptions - return! parseFile checker file parseOpts options - } + let getAllFilesToProjectOptions () = + asyncEx { + let! allFilesToFSharpProjectOptions = allFilesToFSharpProjectOptions |> AsyncAVal.forceAsync - }) + return! + allFilesToFSharpProjectOptions + |> AMap.force + |> HashMap.toArray + |> Array.map (fun (sourceTextPath, projects) -> + async { + let! projs = AsyncAVal.forceAsync projects + return sourceTextPath, projs + }) + |> Async.parallel75 + } - let getAllFilesToProjectOptions () = - allFilesToFSharpProjectOptions - // |> AMap.toASetValues - |> AMap.force - |> HashMap.toArray - |> Array.map (fun (sourceTextPath, projects) -> - async { - let! projs = AsyncAVal.forceAsync projects - return sourceTextPath, projs - }) - |> Async.parallel75 let getAllFilesToProjectOptionsSelected () = async { @@ -1331,6 +1353,8 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let getAllProjectOptions () = async { + let! allFilesToFSharpProjectOptions = allFilesToFSharpProjectOptions |> AsyncAVal.forceAsync + let! set = allFilesToFSharpProjectOptions |> AMap.toASetValues @@ -1351,6 +1375,8 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let getProjectOptionsForFile (filePath: string) = asyncAVal { + let! allFilesToFSharpProjectOptions = allFilesToFSharpProjectOptions + match! allFilesToFSharpProjectOptions |> AMapAsync.tryFindA filePath with | Some projs -> return projs | None -> return Error $"Couldn't find project for {filePath}. Have you tried restoring your project/solution?" @@ -1380,9 +1406,16 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac /// The name of the file in the project whose source to find a typecheck. /// The options for the project or script. /// Determines if the typecheck should be cached for autocompletions. + /// The cache to use for autocompletions. /// - let parseAndCheckFile (checker: FSharpCompilerServiceChecker) (file: VolatileFile) options shouldCache = - async { + let parseAndCheckFile + (checker: FSharpCompilerServiceChecker) + (file: VolatileFile) + (options: FSharpProjectSnapshot) + shouldCache + snapshotCache + = + asyncEx { let tags = [ SemanticConventions.fsac_sourceCodePath, box (UMX.untag file.Source.FileName) SemanticConventions.projectFilePath, box (options.ProjectFileName) ] @@ -1410,7 +1443,8 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac (file.Source.GetHashCode()), file.Source, options, - shouldCache = shouldCache + shouldCache = shouldCache, + ?snapshotAccumulator = snapshotCache ) |> Debug.measureAsync $"checker.ParseAndCheckFileInProject - {file.Source.FileName}" @@ -1436,7 +1470,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac Async.Start( async { - fileParsed.Trigger(parseAndCheck.GetParseResults, options, ct) + // fileParsed.Trigger(parseAndCheck.GetParseResults, options.To, ct) fileChecked.Trigger(parseAndCheck, file, ct) let checkErrors = parseAndCheck.GetParseResults.Diagnostics let parseErrors = parseAndCheck.GetCheckResults.Diagnostics @@ -1456,7 +1490,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac } /// Bypass Adaptive checking and tell the checker to check a file - let bypassAdaptiveTypeCheck (filePath: string) opts = + let bypassAdaptiveTypeCheck (filePath: string) opts snapshotCache = asyncResult { try logger.info ( @@ -1468,7 +1502,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let! fileInfo = forceFindOpenFileOrRead filePath // Don't cache for autocompletions as we really only want to cache "Opened" files. - return! parseAndCheckFile checker fileInfo opts false + return! parseAndCheckFile checker fileInfo opts false snapshotCache with e -> @@ -1508,6 +1542,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let file = info.FileName let! checker = checker and! selectProject = projectSelector + and! documentSource = documentSource return! asyncResult { @@ -1515,17 +1550,24 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let! opts = selectProject.FindProject(file, projectOptions) let cts = getOpenFileTokenOrDefault file use linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ctok, cts) + let opts = opts.FSharpProjectOptions + let! sn = checker.FromOption(opts, documentSource) return! - parseAndCheckFile checker info opts.FSharpProjectOptions true + parseAndCheckFile checker info sn true None |> Async.withCancellation linkedCts.Token } }) let getParseResults filePath = - allFilesParsed - |> AMapAsync.tryFindAndFlattenR $"No parse results found for {filePath}" filePath + asyncAVal { + let! allFilesParsed = allFilesParsed + + return! + allFilesParsed + |> AMapAsync.tryFindAndFlattenR $"No parse results found for {filePath}" filePath + } let getOpenFileTypeCheckResults filePath = openFilesToCheckedFilesResults @@ -1573,7 +1615,11 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac | Ok x -> return Ok x | Error _ -> match! forceGetFSharpProjectOptions file with - | Ok opts -> return! bypassAdaptiveTypeCheck file opts + | Ok opts -> + let checker = checker |> AVal.force + let documentSource = documentSource |> AVal.force + let! sn = checker.FromOption(opts, documentSource) + return! bypassAdaptiveTypeCheck file sn None | Error e -> return Error e } @@ -1621,11 +1667,18 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac |> AsyncAVal.forceAsync let allFilesToDeclarations = - allFilesParsed - |> AMap.map (fun _k v -> v |> AsyncAVal.mapResult (fun p _ -> p.GetNavigationItems().Declarations)) + asyncAVal { + let! allFilesParsed = allFilesParsed + + return + allFilesParsed + |> AMap.map (fun _k v -> v |> AsyncAVal.mapResult (fun p _ -> p.GetNavigationItems().Declarations)) + } let getAllDeclarations () = async { + let! allFilesToDeclarations = allFilesToDeclarations |> AsyncAVal.forceAsync + let! results = allFilesToDeclarations |> AMap.force @@ -1643,8 +1696,13 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac } let getDeclarations filename = - allFilesToDeclarations - |> AMapAsync.tryFindAndFlattenR $"Could not find getDeclarations for {filename}" filename + asyncAVal { + let! allFilesToDeclarations = allFilesToDeclarations + + return! + allFilesToDeclarations + |> AMapAsync.tryFindAndFlattenR $"Could not find getDeclarations for {filename}" filename + } let codeGenServer = { new ICodeGenerationService with @@ -1686,36 +1744,40 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac member x.ParseFileInProject(file) = forceGetParseResults file |> Async.map (Option.ofResult) } let getDependentProjectsOfProjects ps = - let projectSnapshot = forceLoadProjects () + asyncEx { + let! projectSnapshot = forceLoadProjects () - let allDependents = System.Collections.Generic.HashSet() + let allDependents = System.Collections.Generic.HashSet() - let currentPass = ResizeArray() - currentPass.AddRange(ps |> List.map (fun p -> p.ProjectFileName)) + let currentPass = ResizeArray() + currentPass.AddRange(ps |> List.map (fun p -> p.ProjectFileName)) - let mutable continueAlong = true + let mutable continueAlong = true - while continueAlong do - let dependents = - projectSnapshot - |> Seq.filter (fun p -> - p.FSharpProjectOptions.ReferencedProjects - |> Seq.exists (fun r -> - match r.ProjectFilePath with - | None -> false - | Some p -> currentPass.Contains(p))) + while continueAlong do + let dependents = + projectSnapshot + |> Seq.filter (fun p -> + p.FSharpProjectOptions.ReferencedProjects + |> Seq.exists (fun r -> + match r.ProjectFilePath with + | None -> false + | Some p -> currentPass.Contains(p))) - if Seq.isEmpty dependents then - continueAlong <- false - currentPass.Clear() - else - for d in dependents do - allDependents.Add d.FSharpProjectOptions |> ignore + if Seq.isEmpty dependents then + continueAlong <- false + currentPass.Clear() + else + for d in dependents do + allDependents.Add d.FSharpProjectOptions |> ignore - currentPass.Clear() - currentPass.AddRange(dependents |> Seq.map (fun p -> p.ProjectFileName)) + currentPass.Clear() + currentPass.AddRange(dependents |> Seq.map (fun p -> p.ProjectFileName)) - Seq.toList allDependents + return + Seq.toList allDependents + |> List.filter (fun p -> p.ProjectFileName.EndsWith(".fsproj")) + } let getDeclarationLocation (symbolUse, text) = let getProjectOptions file = @@ -1997,7 +2059,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac openFiles.Remove filePath |> ignore match openFilesTokens.TryRemove(filePath) with - | (true, cts) -> cancelToken filePath cts + | (true, cts) -> cancelToken filePath 0 cts | _ -> () textChanges.Remove filePath |> ignore) @@ -2049,19 +2111,29 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let bypassAdaptiveAndCheckDependenciesForFile (filePath: string) = async { + // let snapshotCache = System.Collections.Generic.Dictionary<_, _>() let tags = [ SemanticConventions.fsac_sourceCodePath, box (UMX.untag filePath) ] use _ = fsacActivitySource.StartActivityForType(thisType, tags = tags) - let! dependentFiles = getDependentFilesForFile filePath + let! _dependentFiles = getDependentFilesForFile filePath let! projs = getProjectOptionsForFile filePath |> AsyncAVal.forceAsync - let projs = projs |> Result.toOption |> Option.defaultValue [] - let dependentProjects = + let projs = projs + |> Result.toOption + |> Option.defaultValue [] |> List.map (fun x -> x.FSharpProjectOptions) - |> getDependentProjectsOfProjects - |> List.toArray - |> Array.collect (fun proj -> proj.SourceFiles |> Array.map (fun sourceFile -> proj, sourceFile)) + + let! dependentProjects = projs |> getDependentProjectsOfProjects + + let checker = checker |> AVal.force + + let docSource = documentSource |> AVal.force + let! optsAndSnaps = checker.FromOptions(Array.ofList (List.append projs dependentProjects), docSource) + + let dependentProjectsAndSourceFiles = + optsAndSnaps + |> Array.collect (fun (proj, snap) -> proj.SourceFiles |> Array.map (fun sourceFile -> snap, proj, sourceFile)) let mutable checksCompleted = 0 @@ -2075,20 +2147,20 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let checksToPerform = let innerChecks = - Array.concat [| dependentFiles; dependentProjects |] - |> Array.filter (fun (_, file) -> + Array.concat [| dependentProjectsAndSourceFiles |] + |> Array.filter (fun (_, _, file) -> file.Contains "AssemblyInfo.fs" |> not && file.Contains "AssemblyAttributes.fs" |> not) let checksToPerformLength = innerChecks.Length innerChecks - |> Array.map (fun (proj, file) -> + |> Array.map (fun (snap, _, file) -> let file = UMX.tag file let token = getOpenFileTokenOrDefault filePath - bypassAdaptiveTypeCheck (file) (proj) + bypassAdaptiveTypeCheck (file) (snap) (None) |> Async.withCancellation token |> Async.Ignore |> Async.bind (fun _ -> @@ -2169,9 +2241,9 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac member x.SaveDocument(filePath: string, text: string option) = cancellableTask { - let file = - option { - let! oldFile = forceFindOpenFile filePath + let! file = + asyncResult { + let! oldFile = forceFindOpenFileOrRead filePath let oldFile = text @@ -2181,7 +2253,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac return oldFile.UpdateTouched() } - |> Option.defaultWith (fun () -> + |> AsyncResult.defaultWith (fun _ -> // Very unlikely to get here VolatileFile.Create(sourceTextFactory.Create(filePath, text.Value), 0)) @@ -2199,8 +2271,6 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac member x.ParseAllFiles() = parseAllFiles () |> AsyncAVal.forceAsync - member x.GetOpenFile(filePath) = forceFindOpenFile filePath - member x.GetOpenFileSource(filePath) = forceFindSourceText filePath member x.GetOpenFileOrRead(filePath) = forceFindOpenFileOrRead filePath @@ -2213,12 +2283,15 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac member x.GetProjectOptionsForFile(filePath) = forceGetFSharpProjectOptions filePath - member x.GetTypeCheckResultsForFile(filePath, opts) = bypassAdaptiveTypeCheck filePath opts + member x.GetTypeCheckResultsForFile(filePath, opts) = bypassAdaptiveTypeCheck filePath opts None member x.GetTypeCheckResultsForFile(filePath) = asyncResult { let! opts = forceGetProjectOptions filePath - return! x.GetTypeCheckResultsForFile(filePath, opts) + let checker = checker |> AVal.force + let documentSource = documentSource |> AVal.force + let! sn = checker.FromOption(opts, documentSource) + return! x.GetTypeCheckResultsForFile(filePath, sn) } member x.GetFilesToProject() = getAllFilesToProjectOptionsSelected () diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi b/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi index 3ed4e9ebf..28d75ed52 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi @@ -59,7 +59,6 @@ type AdaptiveState = member SaveDocument: filePath: string * text: string option -> CancellableTask member ForgetDocument: filePath: DocumentUri -> Async member ParseAllFiles: unit -> Async - member GetOpenFile: filePath: string -> VolatileFile option member GetOpenFileSource: filePath: string -> Async> member GetOpenFileOrRead: filePath: string -> Async> member GetParseResults: filePath: string -> Async> @@ -68,7 +67,7 @@ type AdaptiveState = member GetProjectOptionsForFile: filePath: string -> Async> member GetTypeCheckResultsForFile: - filePath: string * opts: FSharpProjectOptions -> Async> + filePath: string * opts: FSharpProjectSnapshot -> Async> member GetTypeCheckResultsForFile: filePath: string -> Async> member GetFilesToProject: unit -> Async<(string * LoadedProject) array> diff --git a/src/FsAutoComplete/LspServers/FSharpLspClient.fs b/src/FsAutoComplete/LspServers/FSharpLspClient.fs index cbd1e2fcb..a2a7ad1b9 100644 --- a/src/FsAutoComplete/LspServers/FSharpLspClient.fs +++ b/src/FsAutoComplete/LspServers/FSharpLspClient.fs @@ -1,6 +1,7 @@ namespace FsAutoComplete.Lsp +open FsAutoComplete open Ionide.LanguageServerProtocol open Ionide.LanguageServerProtocol.Types.LspResult open Ionide.LanguageServerProtocol.Server @@ -81,38 +82,6 @@ type FSharpLspClient(sendServerNotification: ClientNotificationSender, sendServe -/// -/// An awaitable wrapper around a task whose result is disposable. The wrapper is not disposable, so this prevents usage errors like "use _lock = myAsync()" when the appropriate usage should be "use! _lock = myAsync())". -/// -[] -type AwaitableDisposable<'T when 'T :> IDisposable>(t: Task<'T>) = - member x.GetAwaiter() = t.GetAwaiter() - member x.AsTask() = t - static member op_Implicit(source: AwaitableDisposable<'T>) = source.AsTask() - -[] -module private SemaphoreSlimExtensions = - // Based on https://gist.github.com/StephenCleary/7dd1c0fc2a6594ba0ed7fb7ad6b590d6 - // and https://gist.github.com/brendankowitz/5949970076952746a083054559377e56 - type SemaphoreSlim with - - member x.LockAsync(?ct: CancellationToken) = - AwaitableDisposable( - task { - let ct = defaultArg ct CancellationToken.None - let t = x.WaitAsync(ct) - - do! t - - return - { new IDisposable with - member _.Dispose() = - // only release if the task completed successfully - // otherwise, we could be releasing a semaphore that was never acquired - if t.Status = TaskStatus.RanToCompletion then - x.Release() |> ignore } - } - ) type ServerProgressReport(lspClient: FSharpLspClient, ?token: ProgressToken) = @@ -125,7 +94,7 @@ type ServerProgressReport(lspClient: FSharpLspClient, ?token: ProgressToken) = member x.Begin(title, ?cancellable, ?message, ?percentage) = cancellableTask { - use! __ = fun ct -> locker.LockAsync(ct) + use! __ = fun (ct: CancellationToken) -> locker.LockAsync(ct) if not endSent then let! result = lspClient.WorkDoneProgressCreate x.Token From 058f2cf79fb1da2ea9cf56cbff49055fc8b3b9a4 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Mon, 18 Mar 2024 22:05:27 -0400 Subject: [PATCH 06/60] prototype adaptive snapshotting --- src/FsAutoComplete.Core/FileSystem.fs | 3 + src/FsAutoComplete.Core/FileSystem.fsi | 2 + test/FsAutoComplete.Tests.Lsp/Helpers.fs | 43 +- test/FsAutoComplete.Tests.Lsp/Helpers.fsi | 6 +- test/FsAutoComplete.Tests.Lsp/Program.fs | 218 +++--- .../FsAutoComplete.Tests.Lsp/SnapshotTests.fs | 621 ++++++++++++++++++ .../Console1/Console1.fsproj | 16 + .../MultiProjectScenario1/Console1/Program.fs | 2 + .../MultiProjectScenario1/Library1/Library.fs | 5 + .../Library1/Library1.fsproj | 12 + .../ProjectSnapshot/SimpleProject/Program.fs | 2 + .../SimpleProject/SimpleProject.fsproj | 14 + 12 files changed, 819 insertions(+), 125 deletions(-) create mode 100644 test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs create mode 100644 test/FsAutoComplete.Tests.Lsp/TestCases/ProjectSnapshot/MultiProjectScenario1/Console1/Console1.fsproj create mode 100644 test/FsAutoComplete.Tests.Lsp/TestCases/ProjectSnapshot/MultiProjectScenario1/Console1/Program.fs create mode 100644 test/FsAutoComplete.Tests.Lsp/TestCases/ProjectSnapshot/MultiProjectScenario1/Library1/Library.fs create mode 100644 test/FsAutoComplete.Tests.Lsp/TestCases/ProjectSnapshot/MultiProjectScenario1/Library1/Library1.fsproj create mode 100644 test/FsAutoComplete.Tests.Lsp/TestCases/ProjectSnapshot/SimpleProject/Program.fs create mode 100644 test/FsAutoComplete.Tests.Lsp/TestCases/ProjectSnapshot/SimpleProject/SimpleProject.fsproj diff --git a/src/FsAutoComplete.Core/FileSystem.fs b/src/FsAutoComplete.Core/FileSystem.fs index 533589b0a..f58b44b0d 100644 --- a/src/FsAutoComplete.Core/FileSystem.fs +++ b/src/FsAutoComplete.Core/FileSystem.fs @@ -140,6 +140,7 @@ type IFSACSourceText = position: Position * terminal: (char -> bool) * condition: (char -> bool) -> option inherit ISourceText + inherit ISourceTextNew module RoslynSourceText = open Microsoft.CodeAnalysis.Text @@ -384,6 +385,8 @@ module RoslynSourceText = member _.CopyTo(sourceIndex, destination, destinationIndex, count) = sourceText.CopyTo(sourceIndex, destination, destinationIndex, count) + interface ISourceTextNew with + member this.GetChecksum() = sourceText.GetChecksum() type ISourceTextFactory = abstract member Create: fileName: string * text: string -> IFSACSourceText diff --git a/src/FsAutoComplete.Core/FileSystem.fsi b/src/FsAutoComplete.Core/FileSystem.fsi index 3c62dfa57..1ca968dfa 100644 --- a/src/FsAutoComplete.Core/FileSystem.fsi +++ b/src/FsAutoComplete.Core/FileSystem.fsi @@ -98,6 +98,8 @@ type IFSACSourceText = inherit ISourceText + inherit ISourceTextNew + type ISourceTextFactory = abstract member Create: fileName: string * text: string -> IFSACSourceText abstract member Create: fileName: string * stream: Stream -> CancellableValueTask diff --git a/test/FsAutoComplete.Tests.Lsp/Helpers.fs b/test/FsAutoComplete.Tests.Lsp/Helpers.fs index 23a459b20..51be396fa 100644 --- a/test/FsAutoComplete.Tests.Lsp/Helpers.fs +++ b/test/FsAutoComplete.Tests.Lsp/Helpers.fs @@ -57,20 +57,20 @@ module Expecto = let ptestCaseAsync = ptestCaseAsyncWithTimeout DEFAULT_TIMEOUT let ftestCaseAsync = ptestCaseAsyncWithTimeout DEFAULT_TIMEOUT -let rec private copyDirectory sourceDir destDir = +let rec private copyDirectory (sourceDir : DirectoryInfo) destDir = // Get the subdirectories for the specified directory. - let dir = DirectoryInfo(sourceDir) + // let dir = DirectoryInfo(sourceDir) - if not dir.Exists then - raise (DirectoryNotFoundException("Source directory does not exist or could not be found: " + sourceDir)) + if not sourceDir.Exists then + raise (DirectoryNotFoundException("Source directory does not exist or could not be found: " + sourceDir.FullName)) - let dirs = dir.GetDirectories() + let dirs = sourceDir.GetDirectories() // If the destination directory doesn't exist, create it. Directory.CreateDirectory(destDir) |> ignore // Get the files in the directory and copy them to the new location. - dir.GetFiles() + sourceDir.GetFiles() |> Seq.iter (fun file -> let tempPath = Path.Combine(destDir, file.Name) file.CopyTo(tempPath, false) |> ignore) @@ -79,17 +79,22 @@ let rec private copyDirectory sourceDir destDir = dirs |> Seq.iter (fun dir -> let tempPath = Path.Combine(destDir, dir.Name) - copyDirectory dir.FullName tempPath) - -type DisposableDirectory(directory: string) = - static member Create() = - let tempPath = IO.Path.Combine(IO.Path.GetTempPath(), Guid.NewGuid().ToString("n")) + copyDirectory dir tempPath) + +type DisposableDirectory(directory: string, deleteParentDir) = + static member Create(?name : string) = + let tempPath, deleteParentDir = + match name with + | Some name -> + IO.Path.GetTempPath() Guid.NewGuid().ToString("n") name, true + | None -> + IO.Path.Combine(IO.Path.GetTempPath(), Guid.NewGuid().ToString("n")), false printfn "Creating directory %s" tempPath IO.Directory.CreateDirectory tempPath |> ignore - new DisposableDirectory(tempPath) + new DisposableDirectory(tempPath, deleteParentDir) - static member From sourceDir = - let self = DisposableDirectory.Create() + static member From (sourceDir: DirectoryInfo) = + let self = DisposableDirectory.Create(sourceDir.Name) copyDirectory sourceDir self.DirectoryInfo.FullName self @@ -97,8 +102,14 @@ type DisposableDirectory(directory: string) = interface IDisposable with member x.Dispose() = - printfn "Deleting directory %s" x.DirectoryInfo.FullName - IO.Directory.Delete(x.DirectoryInfo.FullName, true) + let dirToDelete = + if deleteParentDir then + x.DirectoryInfo.Parent + else + x.DirectoryInfo + + printfn "Deleting directory %s" dirToDelete.FullName + IO.Directory.Delete(dirToDelete.FullName, true) type Async = /// Behaves like AwaitObservable, but calls the specified guarding function diff --git a/test/FsAutoComplete.Tests.Lsp/Helpers.fsi b/test/FsAutoComplete.Tests.Lsp/Helpers.fsi index 7dc01c165..208f4d491 100644 --- a/test/FsAutoComplete.Tests.Lsp/Helpers.fsi +++ b/test/FsAutoComplete.Tests.Lsp/Helpers.fsi @@ -36,9 +36,9 @@ module Expecto = val ftestCaseAsync: (string -> Async -> Test) type DisposableDirectory = - new: directory: string -> DisposableDirectory - static member Create: unit -> DisposableDirectory - static member From: sourceDir: string -> DisposableDirectory + new: directory: string * deleteParentDir : bool -> DisposableDirectory + static member Create: ?name : string -> DisposableDirectory + static member From: sourceDir: DirectoryInfo -> DisposableDirectory member DirectoryInfo: DirectoryInfo interface IDisposable diff --git a/test/FsAutoComplete.Tests.Lsp/Program.fs b/test/FsAutoComplete.Tests.Lsp/Program.fs index 9e09c51e9..362d160e7 100644 --- a/test/FsAutoComplete.Tests.Lsp/Program.fs +++ b/test/FsAutoComplete.Tests.Lsp/Program.fs @@ -16,9 +16,11 @@ open System.Threading open Serilog.Filters open System.IO open FsAutoComplete +open Helpers Expect.defaultDiffPrinter <- Diff.colourisedDiff + let testTimeout = Environment.GetEnvironmentVariable "TEST_TIMEOUT_MINUTES" |> Int32.TryParse @@ -34,7 +36,7 @@ Environment.SetEnvironmentVariable("FSAC_WORKSPACELOAD_DELAY", "250") let loaders = [ "Ionide WorkspaceLoader", (fun toolpath -> WorkspaceLoader.Create(toolpath, FsAutoComplete.Core.ProjectLoader.globalProperties)) - // "MSBuild Project Graph WorkspaceLoader", (fun toolpath -> WorkspaceLoaderViaProjectGraph.Create(toolpath, FsAutoComplete.Core.ProjectLoader.globalProperties)) + "MSBuild Project Graph WorkspaceLoader", (fun toolpath -> WorkspaceLoaderViaProjectGraph.Create(toolpath, FsAutoComplete.Core.ProjectLoader.globalProperties)) ] @@ -113,117 +115,121 @@ let generalTests = testList "general" [ ] [] -let tests = testList "FSAC" [ generalTests; lspTests ] +let tests = testList "FSAC" [ + // generalTests; lspTests + SnapshotTests.snapshotTests loaders toolsPath + ] [] let main args = - let outputTemplate = - "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}" - - let parseLogLevel (args: string[]) = - let logMarker = "--log=" - - let logLevel = - match - args - |> Array.tryFind (fun arg -> arg.StartsWith(logMarker, StringComparison.Ordinal)) - |> Option.map (fun log -> log.Substring(logMarker.Length)) - with - | Some("warn" | "warning") -> Logging.LogLevel.Warn - | Some "error" -> Logging.LogLevel.Error - | Some "fatal" -> Logging.LogLevel.Fatal - | Some "info" -> Logging.LogLevel.Info - | Some "verbose" -> Logging.LogLevel.Verbose - | Some "debug" -> Logging.LogLevel.Debug - | _ -> Logging.LogLevel.Warn - - let args = - args - |> Array.filter (fun arg -> not <| arg.StartsWith(logMarker, StringComparison.Ordinal)) - - logLevel, args - - let expectoToSerilogLevel = - function - | Logging.LogLevel.Debug -> LogEventLevel.Debug - | Logging.LogLevel.Verbose -> LogEventLevel.Verbose - | Logging.LogLevel.Info -> LogEventLevel.Information - | Logging.LogLevel.Warn -> LogEventLevel.Warning - | Logging.LogLevel.Error -> LogEventLevel.Error - | Logging.LogLevel.Fatal -> LogEventLevel.Fatal - - let parseLogExcludes (args: string[]) = - let excludeMarker = "--exclude-from-log=" - - let toExclude = - args - |> Array.filter (fun arg -> arg.StartsWith(excludeMarker, StringComparison.Ordinal)) - |> Array.collect (fun arg -> arg.Substring(excludeMarker.Length).Split(',')) - - let args = - args - |> Array.filter (fun arg -> not <| arg.StartsWith(excludeMarker, StringComparison.Ordinal)) - - toExclude, args - - let logLevel, args = parseLogLevel args - let switch = LoggingLevelSwitch(expectoToSerilogLevel logLevel) - let logSourcesToExclude, args = parseLogExcludes args - - let sourcesToExclude = - Matching.WithProperty( - Constants.SourceContextPropertyName, - fun s -> s <> null && logSourcesToExclude |> Array.contains s - ) - - let argsToRemove, _loaders = - args - |> Array.windowed 2 - |> Array.tryPick (function - | [| "--loader"; "ionide" |] as args -> Some(args, [ "Ionide WorkspaceLoader", WorkspaceLoader.Create ]) - | [| "--loader"; "graph" |] as args -> - Some(args, [ "MSBuild Project Graph WorkspaceLoader", WorkspaceLoaderViaProjectGraph.Create ]) - | _ -> None) - |> Option.defaultValue ([||], loaders) - - let serilogLogger = - LoggerConfiguration() - .Enrich.FromLogContext() - .MinimumLevel.ControlledBy(switch) - .Filter.ByExcluding(Matching.FromSource("FileSystem")) - .Filter.ByExcluding(sourcesToExclude) - - .Destructure.FSharpTypes() - .Destructure.ByTransforming(fun r -> - box - {| FileName = r.FileName - Start = r.Start - End = r.End |}) - .Destructure.ByTransforming(fun r -> box {| Line = r.Line; Column = r.Column |}) - .Destructure.ByTransforming(fun tok -> tok.ToString() |> box) - .Destructure.ByTransforming(fun di -> box di.FullName) - .WriteTo.Async(fun c -> - c.Console( - outputTemplate = outputTemplate, - standardErrorFromLevel = Nullable<_>(LogEventLevel.Verbose), - theme = Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme.Code - ) - |> ignore) - .CreateLogger() // make it so that every console log is logged to stderr - - // uncomment these next two lines if you want verbose output from the LSP server _during_ your tests - Serilog.Log.Logger <- serilogLogger - LogProvider.setLoggerProvider (Providers.SerilogProvider.create ()) - - let fixedUpArgs = args |> Array.except argsToRemove + // let outputTemplate = + // "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}" + + // let parseLogLevel (args: string[]) = + // let logMarker = "--log=" + + // let logLevel = + // match + // args + // |> Array.tryFind (fun arg -> arg.StartsWith(logMarker, StringComparison.Ordinal)) + // |> Option.map (fun log -> log.Substring(logMarker.Length)) + // with + // | Some("warn" | "warning") -> Logging.LogLevel.Warn + // | Some "error" -> Logging.LogLevel.Error + // | Some "fatal" -> Logging.LogLevel.Fatal + // | Some "info" -> Logging.LogLevel.Info + // | Some "verbose" -> Logging.LogLevel.Verbose + // | Some "debug" -> Logging.LogLevel.Debug + // | _ -> Logging.LogLevel.Warn + + // let args = + // args + // |> Array.filter (fun arg -> not <| arg.StartsWith(logMarker, StringComparison.Ordinal)) + + // logLevel, args + + // let expectoToSerilogLevel = + // function + // | Logging.LogLevel.Debug -> LogEventLevel.Debug + // | Logging.LogLevel.Verbose -> LogEventLevel.Verbose + // | Logging.LogLevel.Info -> LogEventLevel.Information + // | Logging.LogLevel.Warn -> LogEventLevel.Warning + // | Logging.LogLevel.Error -> LogEventLevel.Error + // | Logging.LogLevel.Fatal -> LogEventLevel.Fatal + + // let parseLogExcludes (args: string[]) = + // let excludeMarker = "--exclude-from-log=" + + // let toExclude = + // args + // |> Array.filter (fun arg -> arg.StartsWith(excludeMarker, StringComparison.Ordinal)) + // |> Array.collect (fun arg -> arg.Substring(excludeMarker.Length).Split(',')) + + // let args = + // args + // |> Array.filter (fun arg -> not <| arg.StartsWith(excludeMarker, StringComparison.Ordinal)) + + // toExclude, args + + // let logLevel, args = parseLogLevel args + // let switch = LoggingLevelSwitch(expectoToSerilogLevel logLevel) + // let logSourcesToExclude, args = parseLogExcludes args + + // let sourcesToExclude = + // Matching.WithProperty( + // Constants.SourceContextPropertyName, + // fun s -> s <> null && logSourcesToExclude |> Array.contains s + // ) + + // let argsToRemove, _loaders = + // args + // |> Array.windowed 2 + // |> Array.tryPick (function + // | [| "--loader"; "ionide" |] as args -> Some(args, [ "Ionide WorkspaceLoader", WorkspaceLoader.Create ]) + // | [| "--loader"; "graph" |] as args -> + // Some(args, [ "MSBuild Project Graph WorkspaceLoader", WorkspaceLoaderViaProjectGraph.Create ]) + // | _ -> None) + // |> Option.defaultValue ([||], loaders) + + // let serilogLogger = + // LoggerConfiguration() + // .Enrich.FromLogContext() + // .MinimumLevel.ControlledBy(switch) + // .Filter.ByExcluding(Matching.FromSource("FileSystem")) + // .Filter.ByExcluding(sourcesToExclude) + + // .Destructure.FSharpTypes() + // .Destructure.ByTransforming(fun r -> + // box + // {| FileName = r.FileName + // Start = r.Start + // End = r.End |}) + // .Destructure.ByTransforming(fun r -> box {| Line = r.Line; Column = r.Column |}) + // .Destructure.ByTransforming(fun tok -> tok.ToString() |> box) + // .Destructure.ByTransforming(fun di -> box di.FullName) + // .WriteTo.Async(fun c -> + // c.Console( + // outputTemplate = outputTemplate, + // standardErrorFromLevel = Nullable<_>(LogEventLevel.Verbose), + // theme = Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme.Code + // ) + // |> ignore) + // .CreateLogger() // make it so that every console log is logged to stderr + + // // uncomment these next two lines if you want verbose output from the LSP server _during_ your tests + // Serilog.Log.Logger <- serilogLogger + // LogProvider.setLoggerProvider (Providers.SerilogProvider.create ()) + + // let fixedUpArgs = args |> Array.except argsToRemove let cts = new CancellationTokenSource(testTimeout) - let args = - [ CLIArguments.Printer(Expecto.Impl.TestPrinters.summaryWithLocationPrinter defaultConfig.printer) - CLIArguments.Verbosity logLevel - // CLIArguments.Parallel + let cliArgs = + [ + // CLIArguments.Printer(Expecto.Impl.TestPrinters.summaryWithLocationPrinter defaultConfig.printer) + CLIArguments.Verbosity Expecto.Logging.LogLevel.Info + // // CLIArguments.Parallel ] - runTestsWithCLIArgsAndCancel cts.Token args fixedUpArgs tests + runTestsWithCLIArgsAndCancel cts.Token cliArgs args tests diff --git a/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs new file mode 100644 index 000000000..10e988dcf --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs @@ -0,0 +1,621 @@ +module FsAutoComplete.Tests.SnapshotTests +open Expecto +open System.IO +open Ionide.ProjInfo +open FsAutoComplete.Utils +open FSharp.Data.Adaptive +open FsAutoComplete.Adaptive +open System +open FSharp.Compiler.Text +open Ionide.ProjInfo.ProjectLoader +open Ionide.ProjInfo.Types +open FSharp.Compiler.CodeAnalysis.ProjectSnapshot +open FSharp.Compiler.CodeAnalysis +open IcedTasks + +module FcsRange = FSharp.Compiler.Text.Range +type FcsRange = FSharp.Compiler.Text.Range +type FcsPos = FSharp.Compiler.Text.Position + +// type ProjectSnapshotLike = { +// ProjectFileName: string +// ProjectId : string option +// SourceFiles: string list +// ReferencesOnDisk : string list +// OtherOptions : string list +// ReferencedProjects : string list +// IsIncompleteTypeCheckEnvironment : bool +// UseScriptResolutionRules : bool +// LoadTime : DateTime +// UnresolvedReferences : string list +// OriginalLoadReferences: (FcsRange * string * string) list +// Stamp: int64 option +// } +// with +// static member Create(p : ProjectOptions) = + +// { +// ProjectFileName = p.ProjectFileName +// ProjectId = p.ProjectId +// SourceFiles = p.SourceFiles +// ReferencesOnDisk = p.PackageReferences |> List.map (fun x -> x.FullPath) +// OtherOptions = p.OtherOptions +// ReferencedProjects = p.ReferencedProjects |> List.map (fun x -> x.ProjectFileName) +// IsIncompleteTypeCheckEnvironment = false +// UseScriptResolutionRules = false +// LoadTime = p.LoadTime +// UnresolvedReferences = [] +// OriginalLoadReferences = [] +// Stamp = None +// } + + + +let rec findAllDependenciesOfAndIncluding key findNextKeys items = + amap { + printfn "findAllDependenciesOfAndIncluding %A" key + let! item = AMap.tryFind key items + match item with + | None -> + () + | Some item -> + yield key, item + let! dependencies = + item + |> AVal.map( + findNextKeys + >> Seq.map (fun newKey -> findAllDependenciesOfAndIncluding newKey findNextKeys items) + >> Seq.fold(AMap.union) AMap.empty + ) + yield! dependencies + } + +let rec findAllDependentsOfAndIncluding key findNextKeys items = + amap { + printfn "findAllDependentsOfAndIncluding %A" key + let immediateDependents = + items + |> AMap.filterA (fun _ v -> v |> AVal.map (findNextKeys >> Seq.exists ((=) key))) + yield! immediateDependents + let! dependentOfDependents = + immediateDependents + |> AMap.map (fun nextKey _ -> findAllDependentsOfAndIncluding nextKey findNextKeys items) + |> AMap.fold(fun acc _ x -> AMap.union acc x) AMap.empty + yield! dependentOfDependents + } + +module Dotnet = + let restore (projectPath : FileInfo) = async { + let! cmd = Helpers.runProcess projectPath.Directory.FullName "dotnet" "restore" + Helpers.expectExitCodeZero cmd + } + + let restoreAll (projectPaths : FileInfo seq) = async { + do! + projectPaths + |> Seq.map(fun p -> restore p) + |> Async.Sequential + |> Async.Ignore + } + + let addPackage (projectPath : FileInfo) (packageName : string) (version : string) = async { + let! cmd = Helpers.runProcess projectPath.Directory.FullName "dotnet" $"add package {packageName} --version {version}" + Helpers.expectExitCodeZero cmd + } + + let removePackage (projectPath : FileInfo) (packageName : string) = async { + let! cmd = Helpers.runProcess projectPath.Directory.FullName "dotnet" $"remove package {packageName}" + Helpers.expectExitCodeZero cmd + } + +module Projects = + module Simple = + let simpleProjectDir = DirectoryInfo(__SOURCE_DIRECTORY__ "TestCases/ProjectSnapshot/SimpleProject") + let simpleProject = "SimpleProject.fsproj" + let projects (srcDir : DirectoryInfo) = + [ + FileInfo(srcDir.FullName simpleProject) + ] + + module MultiProjectScenario1 = + let multiProjectScenario1Dir = DirectoryInfo(__SOURCE_DIRECTORY__ "TestCases/ProjectSnapshot/MultiProjectScenario1") + + module Console1 = + let dir = "Console1" + let project = "Console1.fsproj" + let ProgramFile = "Program.fs" + module Library1 = + let dir = "Library1" + let project = "Library1.fsproj" + let LibraryFile = "Library.fs" + let libraryFileIn (srcDir : DirectoryInfo) = FileInfo(srcDir.FullName dir LibraryFile) + + let projects (srcDir : DirectoryInfo) = + [ + FileInfo(srcDir.FullName Console1.dir Console1.project) + FileInfo(srcDir.FullName Library1.dir Library1.project) + ] + +let createProjectA (projects : FileInfo seq) (loader : IWorkspaceLoader) onLoadCallback = + let projectsA = + projects + |> ASet.ofSeq + |> ASet.mapAtoAMap (fun x -> AdaptiveFile.GetLastWriteTimeUtc x.FullName) + + let loadedProjectsA = + projectsA + |> AMap.toAVal + |> AVal.map(fun kvp -> + let projects = kvp.ToKeyList() + let loaded = projects |> List.map (fun p -> p.FullName) |> loader.LoadProjects |> Seq.cache + onLoadCallback () + projects + |> List.map(fun p -> p.FullName, AVal.constant(p, loaded |> Seq.find(fun l -> l.ProjectFileName = p.FullName))) + ) + |> AMap.ofAVal + + loadedProjectsA + + + +module Snapshots = + + let loadFromDotnetDll (p: Types.ProjectOptions) : ProjectSnapshot.FSharpReferencedProjectSnapshot = + /// because only a successful compilation will be written to a DLL, we can rely on + /// the file metadata for things like write times + let projectFile = FileInfo p.TargetPath + + let getStamp () = + projectFile.Refresh () + projectFile.LastWriteTimeUtc + + let getStream (_ctok: System.Threading.CancellationToken) = + try + projectFile.OpenRead() :> Stream |> Some + with _ -> + None + + let delayedReader = DelayedILModuleReader(p.TargetPath, getStream) + + ProjectSnapshot.FSharpReferencedProjectSnapshot.PEReference(getStamp, delayedReader) + + let makeFCSSnapshot2 + (cache : ChangeableHashMap<_,_>) + projectFileName + projectId + sourceFiles + referencePaths + otherOptions + referencedProjects + isIncompleteTypeCheckEnvironment + useScriptResolutionRules + loadTime + unresolvedReferences + originalLoadReferences + stamp + = + let foo = + aval { + let! projectFileName = projectFileName + and! projectId = projectId + and! sourceFiles = sourceFiles + and! referencePaths = referencePaths + and! otherOptions = otherOptions + and! referencedProjects = referencedProjects + and! isIncompleteTypeCheckEnvironment = isIncompleteTypeCheckEnvironment + and! useScriptResolutionRules = useScriptResolutionRules + and! loadTime = loadTime + and! unresolvedReferences = unresolvedReferences + and! originalLoadReferences = originalLoadReferences + and! stamp = stamp + + + printfn "Snapshot %A" projectFileName + let snap = FSharpProjectSnapshot.Create( + projectFileName, + projectId, + sourceFiles, + referencePaths, + otherOptions, + referencedProjects , + isIncompleteTypeCheckEnvironment, + useScriptResolutionRules, + loadTime, + unresolvedReferences, + originalLoadReferences, + stamp + ) + + return snap + } + aval { + let! projectFileName = projectFileName + transact <| fun () -> + cache.Add(projectFileName, foo) |> ignore<_> + return! foo + } + + + let private makeFCSSnapshot + makeFileSnapshot + makeFileOnDisk + mapProjectToReference + (project: Types.ProjectOptions) + : FSharpProjectSnapshot = + let references, otherOptions = + project.OtherOptions |> List.partition (fun x -> x.StartsWith("-r:")) + + let referencePaths = references |> List.map (fun x -> x.Substring(3)) + + FSharpProjectSnapshot.Create( + project.ProjectFileName, + project.ProjectId, + project.SourceFiles |> List.map makeFileSnapshot, + referencePaths |> List.map makeFileOnDisk, + otherOptions, + project.ReferencedProjects |> List.choose mapProjectToReference, + isIncompleteTypeCheckEnvironment = false, + useScriptResolutionRules = false, + loadTime = project.LoadTime, + unresolvedReferences = None, + originalLoadReferences = [], + stamp = Some DateTime.UtcNow.Ticks + ) + + + let private makeProjectReference + isKnownProject + makeFSharpProjectReference + (p: Types.ProjectReference) + : ProjectSnapshot.FSharpReferencedProjectSnapshot option = + let knownProject = isKnownProject p + + let isDotnetProject (knownProject: Types.ProjectOptions option) = + match knownProject with + | Some p -> + (p.ProjectFileName.EndsWith(".csproj") || p.ProjectFileName.EndsWith(".vbproj")) + && File.Exists p.ResolvedTargetPath + | None -> false + + if p.ProjectFileName.EndsWith ".fsproj" then + knownProject + |> Option.map (fun (p: Types.ProjectOptions) -> + let theseOptions = makeFSharpProjectReference p + ProjectSnapshot.FSharpReferencedProjectSnapshot.FSharpReference(p.ResolvedTargetPath, theseOptions)) + elif isDotnetProject knownProject then + knownProject |> Option.map loadFromDotnetDll + else + None + + let mapManySnapshots + makeFile + makeDiskReference + (allKnownProjects: Types.ProjectOptions seq) + : FSharpProjectSnapshot seq = + seq { + let dict = + System.Collections.Concurrent.ConcurrentDictionary() + + let isKnownProject (p: Types.ProjectReference) = + allKnownProjects + |> Seq.tryFind (fun kp -> kp.ProjectFileName = p.ProjectFileName) + + let rec makeFSharpProjectReference (p: Types.ProjectOptions) = + let factory = makeProjectReference isKnownProject makeFSharpProjectReference + let makeSnapshot = makeFCSSnapshot makeFile makeDiskReference factory + dict.GetOrAdd(p, makeSnapshot) + + for project in allKnownProjects do + let thisProject = dict.GetOrAdd(project, makeFSharpProjectReference) + + yield thisProject + } + + + +let createsnapshot mapProjectToReference documentSource (project : ProjectOptions) = + + + let makeDiskReference referencePath : ProjectSnapshot.ReferenceOnDisk = + { Path = referencePath + LastModified = FSharp.Compiler.IO.FileSystemAutoOpens.FileSystem.GetLastWriteTimeShim(referencePath) } + let makeFileSnapshot filePath : ProjectSnapshot.FSharpFileSnapshot = + ProjectSnapshot.FSharpFileSnapshot.CreateFromDocumentSource(filePath, documentSource) + + let references, otherOptions = + project.OtherOptions |> List.partition (fun x -> x.StartsWith("-r:")) + let referencePaths = references |> List.map (fun x -> x.Substring(3)) + let referenceSnapshots = project.ReferencedProjects |> List.choose mapProjectToReference + FSharpProjectSnapshot.Create( + project.ProjectFileName, + project.ProjectId, + project.SourceFiles |> List.map makeFileSnapshot, + referencePaths |> List.map makeDiskReference, + otherOptions = otherOptions, + referencedProjects = referenceSnapshots, + isIncompleteTypeCheckEnvironment = false, + useScriptResolutionRules = false, + loadTime = project.LoadTime, + unresolvedReferences = None, + originalLoadReferences = [], + stamp = Some DateTime.UtcNow.Ticks + + ) + +let snapshotTests loaders toolsPath = + testSequenced <| + testList "SnapshotTests" [ + for (loaderName, workspaceLoaderFactory) in loaders do + + testList $"{loaderName}" [ + testCaseAsync "Simple Project Load" <| async { + let (loader : IWorkspaceLoader) = workspaceLoaderFactory toolsPath + let srcDir = Projects.Simple.simpleProjectDir + use dDir = Helpers.DisposableDirectory.From srcDir + let projects = Projects.Simple.projects dDir.DirectoryInfo + do! Dotnet.restoreAll projects + + let mutable loadedCalls = 0 + + let loadedProjectsA = createProjectA projects loader (fun () -> loadedCalls <- loadedCalls + 1) + + let loadedProjects = loadedProjectsA |> AMap.force + Expect.equal 1 loadedCalls "Load Projects should only get called once" + + // No interaction with fsproj should not cause a reload + let loadedProjects = loadedProjectsA |> AMap.force + Expect.equal 1 loadedCalls "Load Projects should only get called once after doing nothing that should trigger a reload" + } + + testCaseAsync "Adding nuget package should cause project load" <| async { + let (loader : IWorkspaceLoader) = workspaceLoaderFactory toolsPath + let srcDir = Projects.Simple.simpleProjectDir + use dDir = Helpers.DisposableDirectory.From srcDir + let projects = Projects.Simple.projects dDir.DirectoryInfo + do! Dotnet.restoreAll projects + + let mutable loadedCalls = 0 + let loadedProjectsA = createProjectA projects loader (fun () -> loadedCalls <- loadedCalls + 1) + + let loadedProjects = loadedProjectsA |> AMap.force + Expect.equal 1 loadedCalls "Loaded Projects should only get called 1 time" + + do! Dotnet.addPackage (projects |> List.head) "Newtonsoft.Json" "12.0.3" + + let loadedProjects = loadedProjectsA |> AMap.force + Expect.equal 2 loadedCalls "Load Projects should have gotten called again after adding a nuget package" + } + + testCaseAsync "Create snapshot" <| async { + let (loader : IWorkspaceLoader) = workspaceLoaderFactory toolsPath + let srcDir = Projects.Simple.simpleProjectDir + use dDir = Helpers.DisposableDirectory.From srcDir + let projects = Projects.Simple.projects dDir.DirectoryInfo + do! Dotnet.restoreAll projects + + let mutable loadedCalls = 0 + + + let loadedProjectsA = + createProjectA projects loader (fun () -> loadedCalls <- loadedCalls + 1) + |> AMap.map (fun _ (project) -> project |> AVal.map(snd >> createsnapshot (fun _ -> None) (DocumentSource.FileSystem))) + + let snapshots = loadedProjectsA |> AMap.force + + let (project, snapshotA) = snapshots |> Seq.head + let snapshot = snapshotA |> AVal.force + Expect.equal 1 loadedCalls "Loaded Projects should only get called 1 time" + Expect.equal snapshot.ProjectFileName project "Snapshot should have the same project file name as the project" + Expect.equal (Seq.length snapshot.SourceFiles) 3 "Snapshot should have the same number of source files as the project" + Expect.equal (Seq.length snapshot.ReferencedProjects) 0 "Snapshot should have the same number of referenced projects as the project" + } + + testCaseAsync "Create snapshot from multiple projects" <| async { + let (loader : IWorkspaceLoader) = workspaceLoaderFactory toolsPath + let srcDir = Projects.MultiProjectScenario1.multiProjectScenario1Dir + use dDir = Helpers.DisposableDirectory.From srcDir + let projects = Projects.MultiProjectScenario1.projects dDir.DirectoryInfo + do! Dotnet.restoreAll projects + + let mutable loadedCalls = 0 + + let loadedProjectsA = + createProjectA projects loader (fun () -> loadedCalls <- loadedCalls + 1) + let snapsA = + loadedProjectsA + |> AMap.map (fun k (project) -> + let references = + findAllDependenciesOfAndIncluding k (fun (_, p : ProjectOptions) -> p.ReferencedProjects |> List.map(_.ProjectFileName)) loadedProjectsA + aval { + + let! (_, project) = project + and! references = references |> AMap.mapA (fun k v -> v) |> AMap.toAVal + let makeFile filePath : ProjectSnapshot.FSharpFileSnapshot = + ProjectSnapshot.FSharpFileSnapshot.CreateFromDocumentSource(filePath, DocumentSource.FileSystem) + + let makeDiskReference referencePath : ProjectSnapshot.ReferenceOnDisk = + { Path = referencePath + LastModified = FSharp.Compiler.IO.FileSystemAutoOpens.FileSystem.GetLastWriteTimeShim(referencePath) } + + let lol = + references + |> HashMap.toValueList + |> Seq.map snd + |> Snapshots.mapManySnapshots makeFile makeDiskReference + |> Seq.tryFind (fun x -> x.ProjectFileName = k) + return lol + } + + ) + + let snapshots = snapsA |> AMap.force |> HashMap.map (fun k v -> k, v |> AVal.force) + + let (project, snapshot) = snapshots |> HashMap.choose (fun _ (_,v) -> v) |> Seq.find(fun (_, s) -> s.ProjectFileName.EndsWith Projects.MultiProjectScenario1.Console1.project) + Expect.equal 1 loadedCalls "Loaded Projects should only get called 1 time" + Expect.equal snapshot.ProjectFileName project "Snapshot should have the same project file name as the project" + Expect.equal (Seq.length snapshot.SourceFiles) 3 "Snapshot should have the same number of source files as the project" + Expect.equal (Seq.length snapshot.ReferencedProjects) 1 "Snapshot should have the same number of referenced projects as the project" + } + + ftestCaseAsync "LOL" <| asyncEx { + let (loader : IWorkspaceLoader) = workspaceLoaderFactory toolsPath + let sourceTextFactory : FsAutoComplete.ISourceTextFactory = FsAutoComplete.RoslynSourceTextFactory() + let srcDir = Projects.MultiProjectScenario1.multiProjectScenario1Dir + use dDir = Helpers.DisposableDirectory.From srcDir + let projects = Projects.MultiProjectScenario1.projects dDir.DirectoryInfo + do! Dotnet.restoreAll projects + + let mutable loadedCalls = 0 + + let loadedProjectsA = + createProjectA projects loader (fun () -> loadedCalls <- loadedCalls + 1) + + let createFSharpFileSnapshot fileName version getSource = + aval { + let! fileName = fileName + and! version = version + and! getSource = getSource + + return ProjectSnapshot.FSharpFileSnapshot.Create(fileName,version, getSource) + } + let createReferenceOnDisk path lastModified : aval = + aval { + let! path = path + and! lastModified = lastModified + return { LastModified = lastModified; Path = path } + } + + let createReferencedProjectsFSharpReference projectOutputFile (snapshot: aval) = + aval { + let! projectOutputFile = projectOutputFile + and! snapshot = snapshot + return FSharpReferencedProjectSnapshot.FSharpReference(projectOutputFile, snapshot) + } + let rec mapReferences cached loadedProjectsA (p : ProjectOptions) = + aval { + let references = + findAllDependenciesOfAndIncluding + p.ProjectFileName + (fun (_, p : ProjectOptions) -> p.ReferencedProjects |> List.map(_.ProjectFileName)) loadedProjectsA + |> AMap.filter (fun k _ -> k <> p.ProjectFileName) + let! refSnapshots = + references + |> AMap.mapAVal(fun _ (_,p) -> aval { + if p.ProjectFileName.EndsWith ".fsproj" then + let opt = aval { + match! cached |> AMap.tryFind p.ProjectFileName with + | Some x -> + printfn "Cache hit mapReferences %A" p.ProjectFileName + return! x + | None -> + printfn "Cache miss mapReferences %A" p.ProjectFileName + return! (optToSnap cached (mapReferences cached references) p) + } + return! createReferencedProjectsFSharpReference (AVal.constant p.ResolvedTargetPath) opt + else + return Snapshots.loadFromDotnetDll p + }) + |> AMap.toASetValues + |> ASet.mapA id + |> ASet.toAVal + |> AVal.map HashSet.toList + return refSnapshots + } + + and optToSnap cache (mapReferences: ProjectOptions -> aval) (p : ProjectOptions) = + + printfn "optToSnap %A" p.ProjectFileName + aval { + match! cache |> AMap.tryFind p.ProjectFileName with + | Some x -> + printfn "Cache hit optToSnap %A" p.ProjectFileName + return! x + | None -> + printfn "Cache miss optToSnap %A" p.ProjectFileName + let references, otherOptions = + p.OtherOptions |> List.partition (fun x -> x.StartsWith("-r:")) + let projectName = p.ProjectFileName |> AVal.constant + let projectId = p.ProjectId |> AVal.constant + + let sourceFiles = + // TODO Make it use "open files" and file system files + p.SourceFiles + |> ASet.ofList + |> ASet.mapA(fun fileName -> + aval { + let! writeTime = AdaptiveFile.GetLastWriteTimeUtc fileName + let getSource () = task { + let! text = File.ReadAllTextAsync fileName + return sourceTextFactory.Create((normalizePath fileName), text) :> ISourceTextNew + } + printfn "Creating source text for %s" fileName + return ProjectSnapshot.FSharpFileSnapshot.Create(fileName, string writeTime.Ticks, getSource) + } + ) + |> ASet.toAVal + |> AVal.map HashSet.toList + + let otherOptions = otherOptions |> AVal.constant + let referencePaths = + references + |> ASet.ofList + |> ASet.mapA(fun path -> + let path = path.Substring(3) + let writeTime = AdaptiveFile.GetLastWriteTimeUtc path + createReferenceOnDisk (AVal.constant path) writeTime + ) + |> ASet.toAVal + |> AVal.map HashSet.toList + let referencedProjects = + p + |> mapReferences + let isIncompleteTypeCheckEnvironment = false |> AVal.constant + let useScriptResolutionRules = false |> AVal.constant + let loadTime = p.LoadTime |> AVal.constant + let unresolvedReferences = None |> AVal.constant + let originalLoadReferences = [] |> AVal.constant + let stamp = Some DateTime.UtcNow.Ticks |> AVal.constant + + + return! Snapshots.makeFCSSnapshot2 + cache + projectName + projectId + sourceFiles + referencePaths + otherOptions + referencedProjects + isIncompleteTypeCheckEnvironment + useScriptResolutionRules + loadTime + unresolvedReferences + originalLoadReferences + stamp + } + + + + let cache = ChangeableHashMap() + + let snapsA = + loadedProjectsA + |> AMap.mapAVal (fun k (_,v) -> optToSnap cache (mapReferences cache loadedProjectsA) v) + + let snapshots = snapsA |> AMap.mapA (fun _ v -> v) |> AMap.force + + let libraryFile = Projects.MultiProjectScenario1.Library1.libraryFileIn dDir.DirectoryInfo + printfn "Setting last write time for %s %A" libraryFile.FullName libraryFile.LastWriteTime + + do! File.WriteAllTextAsync(libraryFile.FullName, "let x = 1") + libraryFile.Refresh() + printfn "last write time for %s %A" libraryFile.FullName libraryFile.LastWriteTime + + do! Async.Sleep 100 + + let snapshots2 = snapsA |> AMap.mapA (fun _ v -> v) |> AMap.force + () + } + + + ] +] diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/ProjectSnapshot/MultiProjectScenario1/Console1/Console1.fsproj b/test/FsAutoComplete.Tests.Lsp/TestCases/ProjectSnapshot/MultiProjectScenario1/Console1/Console1.fsproj new file mode 100644 index 000000000..0a554df10 --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/ProjectSnapshot/MultiProjectScenario1/Console1/Console1.fsproj @@ -0,0 +1,16 @@ + + + + Exe + net8.0 + + + + + + + + + + + diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/ProjectSnapshot/MultiProjectScenario1/Console1/Program.fs b/test/FsAutoComplete.Tests.Lsp/TestCases/ProjectSnapshot/MultiProjectScenario1/Console1/Program.fs new file mode 100644 index 000000000..d6818aba8 --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/ProjectSnapshot/MultiProjectScenario1/Console1/Program.fs @@ -0,0 +1,2 @@ +// For more information see https://aka.ms/fsharp-console-apps +printfn "Hello from F#" diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/ProjectSnapshot/MultiProjectScenario1/Library1/Library.fs b/test/FsAutoComplete.Tests.Lsp/TestCases/ProjectSnapshot/MultiProjectScenario1/Library1/Library.fs new file mode 100644 index 000000000..53e0da501 --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/ProjectSnapshot/MultiProjectScenario1/Library1/Library.fs @@ -0,0 +1,5 @@ +namespace Project2 + +module Say = + let hello name = + printfn "Hello %s" name diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/ProjectSnapshot/MultiProjectScenario1/Library1/Library1.fsproj b/test/FsAutoComplete.Tests.Lsp/TestCases/ProjectSnapshot/MultiProjectScenario1/Library1/Library1.fsproj new file mode 100644 index 000000000..f81f7f5b8 --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/ProjectSnapshot/MultiProjectScenario1/Library1/Library1.fsproj @@ -0,0 +1,12 @@ + + + + net8.0 + true + + + + + + + diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/ProjectSnapshot/SimpleProject/Program.fs b/test/FsAutoComplete.Tests.Lsp/TestCases/ProjectSnapshot/SimpleProject/Program.fs new file mode 100644 index 000000000..d6818aba8 --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/ProjectSnapshot/SimpleProject/Program.fs @@ -0,0 +1,2 @@ +// For more information see https://aka.ms/fsharp-console-apps +printfn "Hello from F#" diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/ProjectSnapshot/SimpleProject/SimpleProject.fsproj b/test/FsAutoComplete.Tests.Lsp/TestCases/ProjectSnapshot/SimpleProject/SimpleProject.fsproj new file mode 100644 index 000000000..be6c38acf --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/ProjectSnapshot/SimpleProject/SimpleProject.fsproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + + + + + + + + + From a23793069cdb63a6dd0899a872a124df98732731 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Tue, 19 Mar 2024 00:22:59 -0400 Subject: [PATCH 07/60] Cleanup adaptive snapshots --- .../FsAutoComplete.Tests.Lsp/SnapshotTests.fs | 419 +++++++++++------- 1 file changed, 271 insertions(+), 148 deletions(-) diff --git a/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs index 10e988dcf..c8bf0839b 100644 --- a/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs @@ -123,17 +123,20 @@ module Projects = module Console1 = let dir = "Console1" let project = "Console1.fsproj" - let ProgramFile = "Program.fs" + let projectIn (srcDir : DirectoryInfo) = FileInfo(srcDir.FullName dir project) + let programFile = "Program.fs" + let programFileIn (srcDir : DirectoryInfo) = FileInfo(srcDir.FullName dir programFile) module Library1 = let dir = "Library1" let project = "Library1.fsproj" + let projectIn (srcDir : DirectoryInfo) = FileInfo(srcDir.FullName dir project) let LibraryFile = "Library.fs" let libraryFileIn (srcDir : DirectoryInfo) = FileInfo(srcDir.FullName dir LibraryFile) let projects (srcDir : DirectoryInfo) = [ - FileInfo(srcDir.FullName Console1.dir Console1.project) - FileInfo(srcDir.FullName Library1.dir Library1.project) + Console1.projectIn srcDir + Library1.projectIn srcDir ] let createProjectA (projects : FileInfo seq) (loader : IWorkspaceLoader) onLoadCallback = @@ -159,6 +162,7 @@ let createProjectA (projects : FileInfo seq) (loader : IWorkspaceLoader) onLoadC module Snapshots = + open FsAutoComplete let loadFromDotnetDll (p: Types.ProjectOptions) : ProjectSnapshot.FSharpReferencedProjectSnapshot = /// because only a successful compilation will be written to a DLL, we can rely on @@ -179,8 +183,7 @@ module Snapshots = ProjectSnapshot.FSharpReferencedProjectSnapshot.PEReference(getStamp, delayedReader) - let makeFCSSnapshot2 - (cache : ChangeableHashMap<_,_>) + let makeAdaptiveFCSSnapshot projectFileName projectId sourceFiles @@ -192,9 +195,7 @@ module Snapshots = loadTime unresolvedReferences originalLoadReferences - stamp = - let foo = aval { let! projectFileName = projectFileName and! projectId = projectId @@ -207,8 +208,6 @@ module Snapshots = and! loadTime = loadTime and! unresolvedReferences = unresolvedReferences and! originalLoadReferences = originalLoadReferences - and! stamp = stamp - printfn "Snapshot %A" projectFileName let snap = FSharpProjectSnapshot.Create( @@ -223,16 +222,144 @@ module Snapshots = loadTime, unresolvedReferences, originalLoadReferences, - stamp + Some (DateTime.UtcNow.Ticks) ) return snap } + + let makeAdaptiveFCSSnapshot2 + projectFileName + projectId + (sourceFiles: aset>) + (referencePaths: aset>) + (otherOptions: aset>) + (referencedProjects: aset>) + isIncompleteTypeCheckEnvironment + useScriptResolutionRules + loadTime + unresolvedReferences + originalLoadReferences + = + makeAdaptiveFCSSnapshot + projectFileName + projectId + (sourceFiles |> ASet.mapA id |> ASet.toAVal |> AVal.map HashSet.toList) + (referencePaths |> ASet.mapA id |> ASet.toAVal |> AVal.map HashSet.toList) + (otherOptions |> ASet.mapA id |> ASet.toAVal |> AVal.map HashSet.toList) + (referencedProjects |> ASet.mapA id |> ASet.toAVal |> AVal.map HashSet.toList) + isIncompleteTypeCheckEnvironment + useScriptResolutionRules + loadTime + unresolvedReferences + originalLoadReferences + + + let createFSharpFileSnapshotOnDisk (sourceTextFactory : ISourceTextFactory) fileName = + aval { + let! writeTime = AdaptiveFile.GetLastWriteTimeUtc fileName + let getSource () = task { + let! text = File.ReadAllTextAsync fileName + return sourceTextFactory.Create((normalizePath fileName), text) :> ISourceTextNew + } + printfn "Creating source text for %s" fileName + return ProjectSnapshot.FSharpFileSnapshot.Create(fileName, string writeTime.Ticks, getSource) + } + let createReferenceOnDisk path : aval = + aval { + let! lastModified = AdaptiveFile.GetLastWriteTimeUtc path + return { LastModified = lastModified; Path = path } + } + + let createReferencedProjectsFSharpReference projectOutputFile (snapshot: aval) = aval { - let! projectFileName = projectFileName - transact <| fun () -> - cache.Add(projectFileName, foo) |> ignore<_> - return! foo + let! projectOutputFile = projectOutputFile + and! snapshot = snapshot + return FSharpReferencedProjectSnapshot.FSharpReference(projectOutputFile, snapshot) + } + + let rec createReferences cached sourceTextFactory loadedProjectsA (p : ProjectOptions) = + findAllDependenciesOfAndIncluding + p.ProjectFileName + (fun (_, p : ProjectOptions) -> p.ReferencedProjects |> List.map(fun x -> x.ProjectFileName)) loadedProjectsA + |> AMap.filter (fun k _ -> k <> p.ProjectFileName) + |> AMap.mapAVal(fun _ (_,p) -> aval { + if p.ProjectFileName.EndsWith ".fsproj" then + let opt = aval { + match! cached |> AMap.tryFind p.ProjectFileName with + | Some x -> + printfn "mapReferences - Cache hit %A" p.ProjectFileName + return! x + | None -> + printfn "mapReferences - Cache miss mapReferences %A" p.ProjectFileName + return! (optionsToSnapshot cached sourceTextFactory (createReferences cached sourceTextFactory loadedProjectsA) p) + } + return! createReferencedProjectsFSharpReference (AVal.constant p.ResolvedTargetPath) opt + else + return loadFromDotnetDll p + }) + |> AMap.toASetValues + + + and optionsToSnapshot (cache : ChangeableHashMap<_,_>) (sourceTextFactory : ISourceTextFactory) (mapReferences: ProjectOptions -> aset>) (p : ProjectOptions) = + + printfn "optionsToSnapshot - enter %A" p.ProjectFileName + aval { + match! cache |> AMap.tryFind p.ProjectFileName with + | Some x -> + printfn "optionsToSnapshot - Cache hit %A" p.ProjectFileName + return! x + | None -> + printfn "optionsToSnapshot - Cache miss %A" p.ProjectFileName + let projectName = p.ProjectFileName |> AVal.constant + let projectId = p.ProjectId |> AVal.constant + + let sourceFiles = + // TODO Make it use "open files" and file system files + p.SourceFiles + |> ASet.ofList + |> ASet.map(createFSharpFileSnapshotOnDisk sourceTextFactory) + + let references, otherOptions = p.OtherOptions |> List.partition (fun x -> x.StartsWith("-r:")) + let otherOptions = otherOptions |> ASet.ofList |> ASet.map(AVal.constant) + let referencePaths = + references + |> ASet.ofList + |> ASet.map(fun referencePath -> + let path = referencePath.Substring(3) // remove "-r:" + createReferenceOnDisk path + ) + let referencedProjects = mapReferences p + let isIncompleteTypeCheckEnvironment = AVal.constant false + let useScriptResolutionRules = AVal.constant false + let loadTime = AVal.constant p.LoadTime + let unresolvedReferences = AVal.constant None + let originalLoadReferences = AVal.constant [] + + + let snap = + makeAdaptiveFCSSnapshot2 + projectName + projectId + sourceFiles + referencePaths + otherOptions + referencedProjects + isIncompleteTypeCheckEnvironment + useScriptResolutionRules + loadTime + unresolvedReferences + originalLoadReferences + + let! snap = + aval { + let! projectName = projectName + transact <| fun () -> + cache.Add(projectName, snap) |> ignore<_> + return! snap + } + + return snap } @@ -348,7 +475,7 @@ let snapshotTests loaders toolsPath = for (loaderName, workspaceLoaderFactory) in loaders do testList $"{loaderName}" [ - testCaseAsync "Simple Project Load" <| async { + ptestCaseAsync "Simple Project Load" <| async { let (loader : IWorkspaceLoader) = workspaceLoaderFactory toolsPath let srcDir = Projects.Simple.simpleProjectDir use dDir = Helpers.DisposableDirectory.From srcDir @@ -359,15 +486,15 @@ let snapshotTests loaders toolsPath = let loadedProjectsA = createProjectA projects loader (fun () -> loadedCalls <- loadedCalls + 1) - let loadedProjects = loadedProjectsA |> AMap.force + let _loadedProjects = loadedProjectsA |> AMap.force Expect.equal 1 loadedCalls "Load Projects should only get called once" // No interaction with fsproj should not cause a reload - let loadedProjects = loadedProjectsA |> AMap.force + let _loadedProjects = loadedProjectsA |> AMap.force Expect.equal 1 loadedCalls "Load Projects should only get called once after doing nothing that should trigger a reload" } - testCaseAsync "Adding nuget package should cause project load" <| async { + ptestCaseAsync "Adding nuget package should cause project load" <| async { let (loader : IWorkspaceLoader) = workspaceLoaderFactory toolsPath let srcDir = Projects.Simple.simpleProjectDir use dDir = Helpers.DisposableDirectory.From srcDir @@ -377,16 +504,16 @@ let snapshotTests loaders toolsPath = let mutable loadedCalls = 0 let loadedProjectsA = createProjectA projects loader (fun () -> loadedCalls <- loadedCalls + 1) - let loadedProjects = loadedProjectsA |> AMap.force + let _loadedProjects = loadedProjectsA |> AMap.force Expect.equal 1 loadedCalls "Loaded Projects should only get called 1 time" do! Dotnet.addPackage (projects |> List.head) "Newtonsoft.Json" "12.0.3" - let loadedProjects = loadedProjectsA |> AMap.force + let _loadedProjects = loadedProjectsA |> AMap.force Expect.equal 2 loadedCalls "Load Projects should have gotten called again after adding a nuget package" } - testCaseAsync "Create snapshot" <| async { + ptestCaseAsync "Create snapshot" <| async { let (loader : IWorkspaceLoader) = workspaceLoaderFactory toolsPath let srcDir = Projects.Simple.simpleProjectDir use dDir = Helpers.DisposableDirectory.From srcDir @@ -410,7 +537,7 @@ let snapshotTests loaders toolsPath = Expect.equal (Seq.length snapshot.ReferencedProjects) 0 "Snapshot should have the same number of referenced projects as the project" } - testCaseAsync "Create snapshot from multiple projects" <| async { + ptestCaseAsync "Create snapshot from multiple projects" <| async { let (loader : IWorkspaceLoader) = workspaceLoaderFactory toolsPath let srcDir = Projects.MultiProjectScenario1.multiProjectScenario1Dir use dDir = Helpers.DisposableDirectory.From srcDir @@ -428,8 +555,8 @@ let snapshotTests loaders toolsPath = findAllDependenciesOfAndIncluding k (fun (_, p : ProjectOptions) -> p.ReferencedProjects |> List.map(_.ProjectFileName)) loadedProjectsA aval { - let! (_, project) = project - and! references = references |> AMap.mapA (fun k v -> v) |> AMap.toAVal + let! (_, _) = project + and! references = references |> AMap.mapA (fun _ v -> v) |> AMap.toAVal let makeFile filePath : ProjectSnapshot.FSharpFileSnapshot = ProjectSnapshot.FSharpFileSnapshot.CreateFromDocumentSource(filePath, DocumentSource.FileSystem) @@ -457,7 +584,7 @@ let snapshotTests loaders toolsPath = Expect.equal (Seq.length snapshot.ReferencedProjects) 1 "Snapshot should have the same number of referenced projects as the project" } - ftestCaseAsync "LOL" <| asyncEx { + testCaseAsync "Cached Adaptive Snapshot - MultiProject - Updating nothing shouldn't cause recalculation" <| asyncEx { let (loader : IWorkspaceLoader) = workspaceLoaderFactory toolsPath let sourceTextFactory : FsAutoComplete.ISourceTextFactory = FsAutoComplete.RoslynSourceTextFactory() let srcDir = Projects.MultiProjectScenario1.multiProjectScenario1Dir @@ -467,155 +594,151 @@ let snapshotTests loaders toolsPath = let mutable loadedCalls = 0 - let loadedProjectsA = - createProjectA projects loader (fun () -> loadedCalls <- loadedCalls + 1) + let loadedProjectsA = createProjectA projects loader (fun () -> loadedCalls <- loadedCalls + 1) - let createFSharpFileSnapshot fileName version getSource = - aval { - let! fileName = fileName - and! version = version - and! getSource = getSource + let cache = ChangeableHashMap() - return ProjectSnapshot.FSharpFileSnapshot.Create(fileName,version, getSource) - } - let createReferenceOnDisk path lastModified : aval = - aval { - let! path = path - and! lastModified = lastModified - return { LastModified = lastModified; Path = path } - } + let snapsA = + loadedProjectsA + |> AMap.mapAVal (fun _ (_,v) -> Snapshots.optionsToSnapshot cache sourceTextFactory (Snapshots.createReferences cache sourceTextFactory loadedProjectsA) v) - let createReferencedProjectsFSharpReference projectOutputFile (snapshot: aval) = - aval { - let! projectOutputFile = projectOutputFile - and! snapshot = snapshot - return FSharpReferencedProjectSnapshot.FSharpReference(projectOutputFile, snapshot) - } - let rec mapReferences cached loadedProjectsA (p : ProjectOptions) = - aval { - let references = - findAllDependenciesOfAndIncluding - p.ProjectFileName - (fun (_, p : ProjectOptions) -> p.ReferencedProjects |> List.map(_.ProjectFileName)) loadedProjectsA - |> AMap.filter (fun k _ -> k <> p.ProjectFileName) - let! refSnapshots = - references - |> AMap.mapAVal(fun _ (_,p) -> aval { - if p.ProjectFileName.EndsWith ".fsproj" then - let opt = aval { - match! cached |> AMap.tryFind p.ProjectFileName with - | Some x -> - printfn "Cache hit mapReferences %A" p.ProjectFileName - return! x - | None -> - printfn "Cache miss mapReferences %A" p.ProjectFileName - return! (optToSnap cached (mapReferences cached references) p) - } - return! createReferencedProjectsFSharpReference (AVal.constant p.ResolvedTargetPath) opt - else - return Snapshots.loadFromDotnetDll p - }) - |> AMap.toASetValues - |> ASet.mapA id - |> ASet.toAVal - |> AVal.map HashSet.toList - return refSnapshots - } + let snapshots = snapsA |> AMap.mapA (fun _ v -> v) |> AMap.force - and optToSnap cache (mapReferences: ProjectOptions -> aval) (p : ProjectOptions) = + let snapshots2 = snapsA |> AMap.mapA (fun _ v -> v) |> AMap.force - printfn "optToSnap %A" p.ProjectFileName - aval { - match! cache |> AMap.tryFind p.ProjectFileName with - | Some x -> - printfn "Cache hit optToSnap %A" p.ProjectFileName - return! x - | None -> - printfn "Cache miss optToSnap %A" p.ProjectFileName - let references, otherOptions = - p.OtherOptions |> List.partition (fun x -> x.StartsWith("-r:")) - let projectName = p.ProjectFileName |> AVal.constant - let projectId = p.ProjectId |> AVal.constant - - let sourceFiles = - // TODO Make it use "open files" and file system files - p.SourceFiles - |> ASet.ofList - |> ASet.mapA(fun fileName -> - aval { - let! writeTime = AdaptiveFile.GetLastWriteTimeUtc fileName - let getSource () = task { - let! text = File.ReadAllTextAsync fileName - return sourceTextFactory.Create((normalizePath fileName), text) :> ISourceTextNew - } - printfn "Creating source text for %s" fileName - return ProjectSnapshot.FSharpFileSnapshot.Create(fileName, string writeTime.Ticks, getSource) - } - ) - |> ASet.toAVal - |> AVal.map HashSet.toList + let ls1 = snapshots |> HashMap.find ((Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) + let ls2 = snapshots2 |> HashMap.find ((Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) - let otherOptions = otherOptions |> AVal.constant - let referencePaths = - references - |> ASet.ofList - |> ASet.mapA(fun path -> - let path = path.Substring(3) - let writeTime = AdaptiveFile.GetLastWriteTimeUtc path - createReferenceOnDisk (AVal.constant path) writeTime - ) - |> ASet.toAVal - |> AVal.map HashSet.toList - let referencedProjects = - p - |> mapReferences - let isIncompleteTypeCheckEnvironment = false |> AVal.constant - let useScriptResolutionRules = false |> AVal.constant - let loadTime = p.LoadTime |> AVal.constant - let unresolvedReferences = None |> AVal.constant - let originalLoadReferences = [] |> AVal.constant - let stamp = Some DateTime.UtcNow.Ticks |> AVal.constant - - - return! Snapshots.makeFCSSnapshot2 - cache - projectName - projectId - sourceFiles - referencePaths - otherOptions - referencedProjects - isIncompleteTypeCheckEnvironment - useScriptResolutionRules - loadTime - unresolvedReferences - originalLoadReferences - stamp - } + Expect.equal ls1.ProjectFileName ls2.ProjectFileName "Project file name should be the same" + Expect.equal ls1.ProjectId ls2.ProjectId "Project Id name should be the same" + Expect.equal ls1.SourceFiles.Length 3 "Source files length should be 3" + Expect.equal ls1.SourceFiles.Length ls2.SourceFiles.Length "Source files length should be the same" + Expect.equal ls1.ReferencedProjects.Length ls2.ReferencedProjects.Length "Referenced projects length should be the same" + Expect.equal ls1.ReferencedProjects.Length 0 "Referenced projects length should be 0" + Expect.equal ls1.Stamp ls2.Stamp "Stamp should be the same" + + let cs1 = snapshots |> HashMap.find ((Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) + let cs2 = snapshots2 |> HashMap.find ((Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) + + Expect.equal cs1.ProjectFileName cs2.ProjectFileName "Project file name should be the same" + Expect.equal cs1.ProjectId cs2.ProjectId "Project Id name should be the same" + Expect.equal cs1.SourceFiles.Length 3 "Source files length should be 3" + Expect.equal cs1.SourceFiles.Length cs2.SourceFiles.Length "Source files length should be the same" + Expect.equal cs1.ReferencedProjects.Length cs2.ReferencedProjects.Length "Referenced projects length should be the same" + Expect.equal cs1.ReferencedProjects.Length 1 "Referenced projects length should be 1" + Expect.equal cs1.Stamp cs2.Stamp "Stamp should be the same" + + } + + + ftestCaseAsync "Cached Adaptive Snapshot - MultiProject - Updating Source file in Console recreates Console snapshot" <| asyncEx { + let (loader : IWorkspaceLoader) = workspaceLoaderFactory toolsPath + let sourceTextFactory : FsAutoComplete.ISourceTextFactory = FsAutoComplete.RoslynSourceTextFactory() + let srcDir = Projects.MultiProjectScenario1.multiProjectScenario1Dir + use dDir = Helpers.DisposableDirectory.From srcDir + let projects = Projects.MultiProjectScenario1.projects dDir.DirectoryInfo + do! Dotnet.restoreAll projects + let mutable loadedCalls = 0 + let loadedProjectsA = createProjectA projects loader (fun () -> loadedCalls <- loadedCalls + 1) let cache = ChangeableHashMap() let snapsA = loadedProjectsA - |> AMap.mapAVal (fun k (_,v) -> optToSnap cache (mapReferences cache loadedProjectsA) v) + |> AMap.mapAVal (fun _ (_,v) -> Snapshots.optionsToSnapshot cache sourceTextFactory (Snapshots.createReferences cache sourceTextFactory loadedProjectsA) v) let snapshots = snapsA |> AMap.mapA (fun _ v -> v) |> AMap.force - let libraryFile = Projects.MultiProjectScenario1.Library1.libraryFileIn dDir.DirectoryInfo + let libraryFile = Projects.MultiProjectScenario1.Console1.programFileIn dDir.DirectoryInfo printfn "Setting last write time for %s %A" libraryFile.FullName libraryFile.LastWriteTime do! File.WriteAllTextAsync(libraryFile.FullName, "let x = 1") libraryFile.Refresh() printfn "last write time for %s %A" libraryFile.FullName libraryFile.LastWriteTime - do! Async.Sleep 100 let snapshots2 = snapsA |> AMap.mapA (fun _ v -> v) |> AMap.force - () + + let ls1 = snapshots |> HashMap.find ((Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) + let ls2 = snapshots2 |> HashMap.find ((Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) + + Expect.equal ls1.ProjectFileName ls2.ProjectFileName "Project file name should be the same" + Expect.equal ls1.ProjectId ls2.ProjectId "Project Id name should be the same" + Expect.equal ls1.SourceFiles.Length 3 "Source files length should be 3" + Expect.equal ls1.SourceFiles.Length ls2.SourceFiles.Length "Source files length should be the same" + Expect.equal ls1.ReferencedProjects.Length ls2.ReferencedProjects.Length "Referenced projects length should be the same" + Expect.equal ls1.ReferencedProjects.Length 0 "Referenced projects length should be 0" + Expect.equal ls1.Stamp ls2.Stamp "Stamp should be the same" + + + let cs1 = snapshots |> HashMap.find ((Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) + let cs2 = snapshots2 |> HashMap.find ((Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) + + Expect.equal cs1.ProjectFileName cs2.ProjectFileName "Project file name should be the same" + Expect.equal cs1.ProjectId cs2.ProjectId "Project Id name should be the same" + Expect.equal cs1.SourceFiles.Length 3 "Source files length should be 3" + Expect.equal cs1.SourceFiles.Length cs2.SourceFiles.Length "Source files length should be the same" + Expect.equal cs1.ReferencedProjects.Length cs2.ReferencedProjects.Length "Referenced projects length should be the same" + Expect.equal cs1.ReferencedProjects.Length 1 "Referenced projects length should be 1" + Expect.notEqual cs1.Stamp cs2.Stamp "Stamp should not be the same" + } + testCaseAsync "Cached Adaptive Snapshot - MultiProject - Updating Source file in Library recreates Library and Console snapshot" <| asyncEx { + let (loader : IWorkspaceLoader) = workspaceLoaderFactory toolsPath + let sourceTextFactory : FsAutoComplete.ISourceTextFactory = FsAutoComplete.RoslynSourceTextFactory() + let srcDir = Projects.MultiProjectScenario1.multiProjectScenario1Dir + use dDir = Helpers.DisposableDirectory.From srcDir + let projects = Projects.MultiProjectScenario1.projects dDir.DirectoryInfo + do! Dotnet.restoreAll projects + + let mutable loadedCalls = 0 + + let loadedProjectsA = createProjectA projects loader (fun () -> loadedCalls <- loadedCalls + 1) + + let cache = ChangeableHashMap() + + let snapsA = + loadedProjectsA + |> AMap.mapAVal (fun _ (_,v) -> Snapshots.optionsToSnapshot cache sourceTextFactory (Snapshots.createReferences cache sourceTextFactory loadedProjectsA) v) + + let snapshots = snapsA |> AMap.mapA (fun _ v -> v) |> AMap.force + + let libraryFile = Projects.MultiProjectScenario1.Library1.libraryFileIn dDir.DirectoryInfo + printfn "Setting last write time for %s %A" libraryFile.FullName libraryFile.LastWriteTime + do! File.WriteAllTextAsync(libraryFile.FullName, "let x = 1") + libraryFile.Refresh() + printfn "last write time for %s %A" libraryFile.FullName libraryFile.LastWriteTime + + + let snapshots2 = snapsA |> AMap.mapA (fun _ v -> v) |> AMap.force + + let ls1 = snapshots |> HashMap.find ((Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) + let ls2 = snapshots2 |> HashMap.find ((Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) + + Expect.equal ls1.ProjectFileName ls2.ProjectFileName "Project file name should be the same" + Expect.equal ls1.ProjectId ls2.ProjectId "Project Id name should be the same" + Expect.equal ls1.SourceFiles.Length 3 "Source files length should be 3" + Expect.equal ls1.SourceFiles.Length ls2.SourceFiles.Length "Source files length should be the same" + Expect.equal ls1.ReferencedProjects.Length ls2.ReferencedProjects.Length "Referenced projects length should be the same" + Expect.equal ls1.ReferencedProjects.Length 0 "Referenced projects length should be 0" + Expect.notEqual ls1.Stamp ls2.Stamp "Stamp should not be the same" + + + let cs1 = snapshots |> HashMap.find ((Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) + let cs2 = snapshots2 |> HashMap.find ((Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) + + Expect.equal cs1.ProjectFileName cs2.ProjectFileName "Project file name should be the same" + Expect.equal cs1.ProjectId cs2.ProjectId "Project Id name should be the same" + Expect.equal cs1.SourceFiles.Length 3 "Source files length should be 3" + Expect.equal cs1.SourceFiles.Length cs2.SourceFiles.Length "Source files length should be the same" + Expect.equal cs1.ReferencedProjects.Length cs2.ReferencedProjects.Length "Referenced projects length should be the same" + Expect.equal cs1.ReferencedProjects.Length 1 "Referenced projects length should be 1" + Expect.notEqual cs1.Stamp cs2.Stamp "Stamp should not be the same" + + } ] ] From b946e2c7b2472ddc28305f9741bc24a892d002af Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Tue, 19 Mar 2024 06:46:19 -0400 Subject: [PATCH 08/60] Much refactoring snapshot tests --- test/FsAutoComplete.Tests.Lsp/Helpers.fs | 4 +- .../FsAutoComplete.Tests.Lsp/SnapshotTests.fs | 373 +++++------------- 2 files changed, 95 insertions(+), 282 deletions(-) diff --git a/test/FsAutoComplete.Tests.Lsp/Helpers.fs b/test/FsAutoComplete.Tests.Lsp/Helpers.fs index 51be396fa..9cc715411 100644 --- a/test/FsAutoComplete.Tests.Lsp/Helpers.fs +++ b/test/FsAutoComplete.Tests.Lsp/Helpers.fs @@ -89,7 +89,7 @@ type DisposableDirectory(directory: string, deleteParentDir) = IO.Path.GetTempPath() Guid.NewGuid().ToString("n") name, true | None -> IO.Path.Combine(IO.Path.GetTempPath(), Guid.NewGuid().ToString("n")), false - printfn "Creating directory %s" tempPath + // printfn "Creating directory %s" tempPath IO.Directory.CreateDirectory tempPath |> ignore new DisposableDirectory(tempPath, deleteParentDir) @@ -108,7 +108,7 @@ type DisposableDirectory(directory: string, deleteParentDir) = else x.DirectoryInfo - printfn "Deleting directory %s" dirToDelete.FullName + // printfn "Deleting directory %s" dirToDelete.FullName IO.Directory.Delete(dirToDelete.FullName, true) type Async = diff --git a/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs index c8bf0839b..2f8b5fd9b 100644 --- a/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs @@ -17,43 +17,9 @@ module FcsRange = FSharp.Compiler.Text.Range type FcsRange = FSharp.Compiler.Text.Range type FcsPos = FSharp.Compiler.Text.Position -// type ProjectSnapshotLike = { -// ProjectFileName: string -// ProjectId : string option -// SourceFiles: string list -// ReferencesOnDisk : string list -// OtherOptions : string list -// ReferencedProjects : string list -// IsIncompleteTypeCheckEnvironment : bool -// UseScriptResolutionRules : bool -// LoadTime : DateTime -// UnresolvedReferences : string list -// OriginalLoadReferences: (FcsRange * string * string) list -// Stamp: int64 option -// } -// with -// static member Create(p : ProjectOptions) = - -// { -// ProjectFileName = p.ProjectFileName -// ProjectId = p.ProjectId -// SourceFiles = p.SourceFiles -// ReferencesOnDisk = p.PackageReferences |> List.map (fun x -> x.FullPath) -// OtherOptions = p.OtherOptions -// ReferencedProjects = p.ReferencedProjects |> List.map (fun x -> x.ProjectFileName) -// IsIncompleteTypeCheckEnvironment = false -// UseScriptResolutionRules = false -// LoadTime = p.LoadTime -// UnresolvedReferences = [] -// OriginalLoadReferences = [] -// Stamp = None -// } - - - let rec findAllDependenciesOfAndIncluding key findNextKeys items = amap { - printfn "findAllDependenciesOfAndIncluding %A" key + // printfn "findAllDependenciesOfAndIncluding %A" key let! item = AMap.tryFind key items match item with | None -> @@ -72,7 +38,7 @@ let rec findAllDependenciesOfAndIncluding key findNextKeys items = let rec findAllDependentsOfAndIncluding key findNextKeys items = amap { - printfn "findAllDependentsOfAndIncluding %A" key + // printfn "findAllDependentsOfAndIncluding %A" key let immediateDependents = items |> AMap.filterA (fun _ v -> v |> AVal.map (findNextKeys >> Seq.exists ((=) key))) @@ -152,8 +118,10 @@ let createProjectA (projects : FileInfo seq) (loader : IWorkspaceLoader) onLoadC let projects = kvp.ToKeyList() let loaded = projects |> List.map (fun p -> p.FullName) |> loader.LoadProjects |> Seq.cache onLoadCallback () - projects - |> List.map(fun p -> p.FullName, AVal.constant(p, loaded |> Seq.find(fun l -> l.ProjectFileName = p.FullName))) + loaded + |> Seq.map(fun l -> l.ProjectFileName, AVal.constant l) + // projects + // |> List.map(fun p -> p.FullName, AVal.constant(p, loaded |> Seq.find(fun l -> l.ProjectFileName = p.FullName))) ) |> AMap.ofAVal @@ -209,7 +177,7 @@ module Snapshots = and! unresolvedReferences = unresolvedReferences and! originalLoadReferences = originalLoadReferences - printfn "Snapshot %A" projectFileName + // printfn "Snapshot %A" projectFileName let snap = FSharpProjectSnapshot.Create( projectFileName, projectId, @@ -241,13 +209,14 @@ module Snapshots = unresolvedReferences originalLoadReferences = + let flattenASet (s: aset>) = s |> ASet.mapA id |> ASet.toAVal |> AVal.map HashSet.toList makeAdaptiveFCSSnapshot projectFileName projectId - (sourceFiles |> ASet.mapA id |> ASet.toAVal |> AVal.map HashSet.toList) - (referencePaths |> ASet.mapA id |> ASet.toAVal |> AVal.map HashSet.toList) - (otherOptions |> ASet.mapA id |> ASet.toAVal |> AVal.map HashSet.toList) - (referencedProjects |> ASet.mapA id |> ASet.toAVal |> AVal.map HashSet.toList) + (flattenASet sourceFiles) + (flattenASet referencePaths) + (flattenASet otherOptions) + (flattenASet referencedProjects) isIncompleteTypeCheckEnvironment useScriptResolutionRules loadTime @@ -255,63 +224,68 @@ module Snapshots = originalLoadReferences - let createFSharpFileSnapshotOnDisk (sourceTextFactory : ISourceTextFactory) fileName = + let private createFSharpFileSnapshotOnDisk (sourceTextFactory : aval) fileName = aval { let! writeTime = AdaptiveFile.GetLastWriteTimeUtc fileName + and! sourceTextFactory = sourceTextFactory let getSource () = task { let! text = File.ReadAllTextAsync fileName return sourceTextFactory.Create((normalizePath fileName), text) :> ISourceTextNew } - printfn "Creating source text for %s" fileName + // printfn "Creating source text for %s" fileName return ProjectSnapshot.FSharpFileSnapshot.Create(fileName, string writeTime.Ticks, getSource) } - let createReferenceOnDisk path : aval = + + let private createReferenceOnDisk path : aval = aval { let! lastModified = AdaptiveFile.GetLastWriteTimeUtc path return { LastModified = lastModified; Path = path } } - let createReferencedProjectsFSharpReference projectOutputFile (snapshot: aval) = + let private createReferencedProjectsFSharpReference projectOutputFile (snapshot: aval) = aval { let! projectOutputFile = projectOutputFile and! snapshot = snapshot return FSharpReferencedProjectSnapshot.FSharpReference(projectOutputFile, snapshot) } - let rec createReferences cached sourceTextFactory loadedProjectsA (p : ProjectOptions) = - findAllDependenciesOfAndIncluding - p.ProjectFileName - (fun (_, p : ProjectOptions) -> p.ReferencedProjects |> List.map(fun x -> x.ProjectFileName)) loadedProjectsA - |> AMap.filter (fun k _ -> k <> p.ProjectFileName) - |> AMap.mapAVal(fun _ (_,p) -> aval { + let rec private createReferences + (cachedSnapshots: ChangeableHashMap>) + (sourceTextFactory: aval) + (loadedProjectsA: amap>) + (p : ProjectOptions) = + let deps = + loadedProjectsA + |> findAllDependenciesOfAndIncluding + p.ProjectFileName + (fun p -> p.ReferencedProjects |> List.map(_.ProjectFileName)) + deps + |> AMap.filter(fun k _ -> k <> p.ProjectFileName) + |> AMap.mapAVal(fun _ p -> aval { if p.ProjectFileName.EndsWith ".fsproj" then - let opt = aval { - match! cached |> AMap.tryFind p.ProjectFileName with - | Some x -> - printfn "mapReferences - Cache hit %A" p.ProjectFileName - return! x - | None -> - printfn "mapReferences - Cache miss mapReferences %A" p.ProjectFileName - return! (optionsToSnapshot cached sourceTextFactory (createReferences cached sourceTextFactory loadedProjectsA) p) - } - return! createReferencedProjectsFSharpReference (AVal.constant p.ResolvedTargetPath) opt + let snapshot = optionsToSnapshot cachedSnapshots sourceTextFactory (createReferences cachedSnapshots sourceTextFactory deps) p + return! createReferencedProjectsFSharpReference (AVal.constant p.ResolvedTargetPath) snapshot else + // TODO: Find if this needs to be adaptive or if `getStamp` in a PEReference will be enough return loadFromDotnetDll p }) |> AMap.toASetValues + and optionsToSnapshot + (cachedSnapshots : ChangeableHashMap<_,_>) + (sourceTextFactory: aval) + (mapReferences: ProjectOptions -> aset>) + (p : ProjectOptions) = - and optionsToSnapshot (cache : ChangeableHashMap<_,_>) (sourceTextFactory : ISourceTextFactory) (mapReferences: ProjectOptions -> aset>) (p : ProjectOptions) = - - printfn "optionsToSnapshot - enter %A" p.ProjectFileName + // printfn "optionsToSnapshot - enter %A" p.ProjectFileName aval { - match! cache |> AMap.tryFind p.ProjectFileName with + match! cachedSnapshots |> AMap.tryFind p.ProjectFileName with | Some x -> - printfn "optionsToSnapshot - Cache hit %A" p.ProjectFileName + // printfn "optionsToSnapshot - Cache hit %A" p.ProjectFileName return! x | None -> - printfn "optionsToSnapshot - Cache miss %A" p.ProjectFileName - let projectName = p.ProjectFileName |> AVal.constant + // printfn "optionsToSnapshot - Cache miss %A" p.ProjectFileName + let projectName = p.ProjectFileName let projectId = p.ProjectId |> AVal.constant let sourceFiles = @@ -336,10 +310,9 @@ module Snapshots = let unresolvedReferences = AVal.constant None let originalLoadReferences = AVal.constant [] - let snap = makeAdaptiveFCSSnapshot2 - projectName + (AVal.constant projectName) projectId sourceFiles referencePaths @@ -351,131 +324,28 @@ module Snapshots = unresolvedReferences originalLoadReferences - let! snap = - aval { - let! projectName = projectName - transact <| fun () -> - cache.Add(projectName, snap) |> ignore<_> - return! snap - } - - return snap - } - - - let private makeFCSSnapshot - makeFileSnapshot - makeFileOnDisk - mapProjectToReference - (project: Types.ProjectOptions) - : FSharpProjectSnapshot = - let references, otherOptions = - project.OtherOptions |> List.partition (fun x -> x.StartsWith("-r:")) - - let referencePaths = references |> List.map (fun x -> x.Substring(3)) - - FSharpProjectSnapshot.Create( - project.ProjectFileName, - project.ProjectId, - project.SourceFiles |> List.map makeFileSnapshot, - referencePaths |> List.map makeFileOnDisk, - otherOptions, - project.ReferencedProjects |> List.choose mapProjectToReference, - isIncompleteTypeCheckEnvironment = false, - useScriptResolutionRules = false, - loadTime = project.LoadTime, - unresolvedReferences = None, - originalLoadReferences = [], - stamp = Some DateTime.UtcNow.Ticks - ) + transact <| fun () -> + cachedSnapshots.Add(projectName, snap) |> ignore<_> - - let private makeProjectReference - isKnownProject - makeFSharpProjectReference - (p: Types.ProjectReference) - : ProjectSnapshot.FSharpReferencedProjectSnapshot option = - let knownProject = isKnownProject p - - let isDotnetProject (knownProject: Types.ProjectOptions option) = - match knownProject with - | Some p -> - (p.ProjectFileName.EndsWith(".csproj") || p.ProjectFileName.EndsWith(".vbproj")) - && File.Exists p.ResolvedTargetPath - | None -> false - - if p.ProjectFileName.EndsWith ".fsproj" then - knownProject - |> Option.map (fun (p: Types.ProjectOptions) -> - let theseOptions = makeFSharpProjectReference p - ProjectSnapshot.FSharpReferencedProjectSnapshot.FSharpReference(p.ResolvedTargetPath, theseOptions)) - elif isDotnetProject knownProject then - knownProject |> Option.map loadFromDotnetDll - else - None - - let mapManySnapshots - makeFile - makeDiskReference - (allKnownProjects: Types.ProjectOptions seq) - : FSharpProjectSnapshot seq = - seq { - let dict = - System.Collections.Concurrent.ConcurrentDictionary() - - let isKnownProject (p: Types.ProjectReference) = - allKnownProjects - |> Seq.tryFind (fun kp -> kp.ProjectFileName = p.ProjectFileName) - - let rec makeFSharpProjectReference (p: Types.ProjectOptions) = - let factory = makeProjectReference isKnownProject makeFSharpProjectReference - let makeSnapshot = makeFCSSnapshot makeFile makeDiskReference factory - dict.GetOrAdd(p, makeSnapshot) - - for project in allKnownProjects do - let thisProject = dict.GetOrAdd(project, makeFSharpProjectReference) - - yield thisProject + return! snap } + let createSnapshot + (cachedSnapshots: ChangeableHashMap>) + (sourceTextFactory: aval) + (loadedProjectsA: amap>) + (projectOptions: ProjectOptions) = + let mapReferences = createReferences cachedSnapshots sourceTextFactory loadedProjectsA + optionsToSnapshot cachedSnapshots sourceTextFactory mapReferences projectOptions -let createsnapshot mapProjectToReference documentSource (project : ProjectOptions) = - - - let makeDiskReference referencePath : ProjectSnapshot.ReferenceOnDisk = - { Path = referencePath - LastModified = FSharp.Compiler.IO.FileSystemAutoOpens.FileSystem.GetLastWriteTimeShim(referencePath) } - let makeFileSnapshot filePath : ProjectSnapshot.FSharpFileSnapshot = - ProjectSnapshot.FSharpFileSnapshot.CreateFromDocumentSource(filePath, documentSource) - - let references, otherOptions = - project.OtherOptions |> List.partition (fun x -> x.StartsWith("-r:")) - let referencePaths = references |> List.map (fun x -> x.Substring(3)) - let referenceSnapshots = project.ReferencedProjects |> List.choose mapProjectToReference - FSharpProjectSnapshot.Create( - project.ProjectFileName, - project.ProjectId, - project.SourceFiles |> List.map makeFileSnapshot, - referencePaths |> List.map makeDiskReference, - otherOptions = otherOptions, - referencedProjects = referenceSnapshots, - isIncompleteTypeCheckEnvironment = false, - useScriptResolutionRules = false, - loadTime = project.LoadTime, - unresolvedReferences = None, - originalLoadReferences = [], - stamp = Some DateTime.UtcNow.Ticks - - ) - let snapshotTests loaders toolsPath = - testSequenced <| + testList "SnapshotTests" [ for (loaderName, workspaceLoaderFactory) in loaders do - + testSequencedGroup loaderName <| testList $"{loaderName}" [ - ptestCaseAsync "Simple Project Load" <| async { + testCaseAsync "Simple Project Load" <| async { let (loader : IWorkspaceLoader) = workspaceLoaderFactory toolsPath let srcDir = Projects.Simple.simpleProjectDir use dDir = Helpers.DisposableDirectory.From srcDir @@ -494,7 +364,7 @@ let snapshotTests loaders toolsPath = Expect.equal 1 loadedCalls "Load Projects should only get called once after doing nothing that should trigger a reload" } - ptestCaseAsync "Adding nuget package should cause project load" <| async { + testCaseAsync "Adding nuget package should cause project load" <| async { let (loader : IWorkspaceLoader) = workspaceLoaderFactory toolsPath let srcDir = Projects.Simple.simpleProjectDir use dDir = Helpers.DisposableDirectory.From srcDir @@ -513,21 +383,25 @@ let snapshotTests loaders toolsPath = Expect.equal 2 loadedCalls "Load Projects should have gotten called again after adding a nuget package" } - ptestCaseAsync "Create snapshot" <| async { + testCaseAsync "Create snapshot" <| async { let (loader : IWorkspaceLoader) = workspaceLoaderFactory toolsPath let srcDir = Projects.Simple.simpleProjectDir + let sourceTextFactory : FsAutoComplete.ISourceTextFactory = FsAutoComplete.RoslynSourceTextFactory() use dDir = Helpers.DisposableDirectory.From srcDir let projects = Projects.Simple.projects dDir.DirectoryInfo do! Dotnet.restoreAll projects let mutable loadedCalls = 0 - let loadedProjectsA = createProjectA projects loader (fun () -> loadedCalls <- loadedCalls + 1) - |> AMap.map (fun _ (project) -> project |> AVal.map(snd >> createsnapshot (fun _ -> None) (DocumentSource.FileSystem))) - let snapshots = loadedProjectsA |> AMap.force + + let snaps = + let cache = ChangeableHashMap() + loadedProjectsA |> AMap.mapAVal (fun _ v -> Snapshots.createSnapshot cache (AVal.constant sourceTextFactory) loadedProjectsA v) + + let snapshots = snaps |> AMap.force let (project, snapshotA) = snapshots |> Seq.head let snapshot = snapshotA |> AVal.force @@ -537,58 +411,11 @@ let snapshotTests loaders toolsPath = Expect.equal (Seq.length snapshot.ReferencedProjects) 0 "Snapshot should have the same number of referenced projects as the project" } - ptestCaseAsync "Create snapshot from multiple projects" <| async { - let (loader : IWorkspaceLoader) = workspaceLoaderFactory toolsPath - let srcDir = Projects.MultiProjectScenario1.multiProjectScenario1Dir - use dDir = Helpers.DisposableDirectory.From srcDir - let projects = Projects.MultiProjectScenario1.projects dDir.DirectoryInfo - do! Dotnet.restoreAll projects - - let mutable loadedCalls = 0 - - let loadedProjectsA = - createProjectA projects loader (fun () -> loadedCalls <- loadedCalls + 1) - let snapsA = - loadedProjectsA - |> AMap.map (fun k (project) -> - let references = - findAllDependenciesOfAndIncluding k (fun (_, p : ProjectOptions) -> p.ReferencedProjects |> List.map(_.ProjectFileName)) loadedProjectsA - aval { - - let! (_, _) = project - and! references = references |> AMap.mapA (fun _ v -> v) |> AMap.toAVal - let makeFile filePath : ProjectSnapshot.FSharpFileSnapshot = - ProjectSnapshot.FSharpFileSnapshot.CreateFromDocumentSource(filePath, DocumentSource.FileSystem) - - let makeDiskReference referencePath : ProjectSnapshot.ReferenceOnDisk = - { Path = referencePath - LastModified = FSharp.Compiler.IO.FileSystemAutoOpens.FileSystem.GetLastWriteTimeShim(referencePath) } - - let lol = - references - |> HashMap.toValueList - |> Seq.map snd - |> Snapshots.mapManySnapshots makeFile makeDiskReference - |> Seq.tryFind (fun x -> x.ProjectFileName = k) - return lol - } - - ) - - let snapshots = snapsA |> AMap.force |> HashMap.map (fun k v -> k, v |> AVal.force) - - let (project, snapshot) = snapshots |> HashMap.choose (fun _ (_,v) -> v) |> Seq.find(fun (_, s) -> s.ProjectFileName.EndsWith Projects.MultiProjectScenario1.Console1.project) - Expect.equal 1 loadedCalls "Loaded Projects should only get called 1 time" - Expect.equal snapshot.ProjectFileName project "Snapshot should have the same project file name as the project" - Expect.equal (Seq.length snapshot.SourceFiles) 3 "Snapshot should have the same number of source files as the project" - Expect.equal (Seq.length snapshot.ReferencedProjects) 1 "Snapshot should have the same number of referenced projects as the project" - } testCaseAsync "Cached Adaptive Snapshot - MultiProject - Updating nothing shouldn't cause recalculation" <| asyncEx { let (loader : IWorkspaceLoader) = workspaceLoaderFactory toolsPath let sourceTextFactory : FsAutoComplete.ISourceTextFactory = FsAutoComplete.RoslynSourceTextFactory() - let srcDir = Projects.MultiProjectScenario1.multiProjectScenario1Dir - use dDir = Helpers.DisposableDirectory.From srcDir + use dDir = Helpers.DisposableDirectory.From Projects.MultiProjectScenario1.multiProjectScenario1Dir let projects = Projects.MultiProjectScenario1.projects dDir.DirectoryInfo do! Dotnet.restoreAll projects @@ -600,7 +427,7 @@ let snapshotTests loaders toolsPath = let snapsA = loadedProjectsA - |> AMap.mapAVal (fun _ (_,v) -> Snapshots.optionsToSnapshot cache sourceTextFactory (Snapshots.createReferences cache sourceTextFactory loadedProjectsA) v) + |> AMap.mapAVal (fun _ v -> Snapshots.createSnapshot cache (AVal.constant sourceTextFactory) loadedProjectsA v) let snapshots = snapsA |> AMap.mapA (fun _ v -> v) |> AMap.force @@ -609,33 +436,18 @@ let snapshotTests loaders toolsPath = let ls1 = snapshots |> HashMap.find ((Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) let ls2 = snapshots2 |> HashMap.find ((Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) - Expect.equal ls1.ProjectFileName ls2.ProjectFileName "Project file name should be the same" - Expect.equal ls1.ProjectId ls2.ProjectId "Project Id name should be the same" - Expect.equal ls1.SourceFiles.Length 3 "Source files length should be 3" - Expect.equal ls1.SourceFiles.Length ls2.SourceFiles.Length "Source files length should be the same" - Expect.equal ls1.ReferencedProjects.Length ls2.ReferencedProjects.Length "Referenced projects length should be the same" - Expect.equal ls1.ReferencedProjects.Length 0 "Referenced projects length should be 0" - Expect.equal ls1.Stamp ls2.Stamp "Stamp should be the same" + Expect.equal ls1 ls2 "library should be the same" let cs1 = snapshots |> HashMap.find ((Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) let cs2 = snapshots2 |> HashMap.find ((Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) - Expect.equal cs1.ProjectFileName cs2.ProjectFileName "Project file name should be the same" - Expect.equal cs1.ProjectId cs2.ProjectId "Project Id name should be the same" - Expect.equal cs1.SourceFiles.Length 3 "Source files length should be 3" - Expect.equal cs1.SourceFiles.Length cs2.SourceFiles.Length "Source files length should be the same" - Expect.equal cs1.ReferencedProjects.Length cs2.ReferencedProjects.Length "Referenced projects length should be the same" - Expect.equal cs1.ReferencedProjects.Length 1 "Referenced projects length should be 1" - Expect.equal cs1.Stamp cs2.Stamp "Stamp should be the same" - + Expect.equal cs1 cs2 "console should be the same" } - - ftestCaseAsync "Cached Adaptive Snapshot - MultiProject - Updating Source file in Console recreates Console snapshot" <| asyncEx { + testCaseAsync "Cached Adaptive Snapshot - MultiProject - Updating Source file in Console recreates Console snapshot" <| asyncEx { let (loader : IWorkspaceLoader) = workspaceLoaderFactory toolsPath let sourceTextFactory : FsAutoComplete.ISourceTextFactory = FsAutoComplete.RoslynSourceTextFactory() - let srcDir = Projects.MultiProjectScenario1.multiProjectScenario1Dir - use dDir = Helpers.DisposableDirectory.From srcDir + use dDir = Helpers.DisposableDirectory.From Projects.MultiProjectScenario1.multiProjectScenario1Dir let projects = Projects.MultiProjectScenario1.projects dDir.DirectoryInfo do! Dotnet.restoreAll projects @@ -647,16 +459,16 @@ let snapshotTests loaders toolsPath = let snapsA = loadedProjectsA - |> AMap.mapAVal (fun _ (_,v) -> Snapshots.optionsToSnapshot cache sourceTextFactory (Snapshots.createReferences cache sourceTextFactory loadedProjectsA) v) + |> AMap.mapAVal (fun _ v -> Snapshots.createSnapshot cache (AVal.constant sourceTextFactory) loadedProjectsA v) let snapshots = snapsA |> AMap.mapA (fun _ v -> v) |> AMap.force let libraryFile = Projects.MultiProjectScenario1.Console1.programFileIn dDir.DirectoryInfo - printfn "Setting last write time for %s %A" libraryFile.FullName libraryFile.LastWriteTime + // printfn "Setting last write time for %s %A" libraryFile.FullName libraryFile.LastWriteTime do! File.WriteAllTextAsync(libraryFile.FullName, "let x = 1") libraryFile.Refresh() - printfn "last write time for %s %A" libraryFile.FullName libraryFile.LastWriteTime + // printfn "last write time for %s %A" libraryFile.FullName libraryFile.LastWriteTime let snapshots2 = snapsA |> AMap.mapA (fun _ v -> v) |> AMap.force @@ -664,14 +476,7 @@ let snapshotTests loaders toolsPath = let ls1 = snapshots |> HashMap.find ((Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) let ls2 = snapshots2 |> HashMap.find ((Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) - Expect.equal ls1.ProjectFileName ls2.ProjectFileName "Project file name should be the same" - Expect.equal ls1.ProjectId ls2.ProjectId "Project Id name should be the same" - Expect.equal ls1.SourceFiles.Length 3 "Source files length should be 3" - Expect.equal ls1.SourceFiles.Length ls2.SourceFiles.Length "Source files length should be the same" - Expect.equal ls1.ReferencedProjects.Length ls2.ReferencedProjects.Length "Referenced projects length should be the same" - Expect.equal ls1.ReferencedProjects.Length 0 "Referenced projects length should be 0" - Expect.equal ls1.Stamp ls2.Stamp "Stamp should be the same" - + Expect.equal ls1 ls2 "library should be the same" let cs1 = snapshots |> HashMap.find ((Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) let cs2 = snapshots2 |> HashMap.find ((Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) @@ -682,6 +487,11 @@ let snapshotTests loaders toolsPath = Expect.equal cs1.SourceFiles.Length cs2.SourceFiles.Length "Source files length should be the same" Expect.equal cs1.ReferencedProjects.Length cs2.ReferencedProjects.Length "Referenced projects length should be the same" Expect.equal cs1.ReferencedProjects.Length 1 "Referenced projects length should be 1" + let refLib1 = cs1.ReferencedProjects |> Seq.tryPick (fun x -> match x with | FSharpReferencedProjectSnapshot.FSharpReference(_, x) -> Some x | _ -> None) |> Option.get + Expect.equal refLib1 ls1 "Referenced library should be the same as library snapshot" + let refLib2 = cs2.ReferencedProjects |> Seq.tryPick (fun x -> match x with | FSharpReferencedProjectSnapshot.FSharpReference(_, x) -> Some x | _ -> None) |> Option.get + Expect.equal refLib2 ls2 "Referenced library should be the same as library snapshot" + Expect.equal refLib1 refLib2 "Referenced library in both snapshots should be the same as library did not change in this test" Expect.notEqual cs1.Stamp cs2.Stamp "Stamp should not be the same" } @@ -689,9 +499,8 @@ let snapshotTests loaders toolsPath = testCaseAsync "Cached Adaptive Snapshot - MultiProject - Updating Source file in Library recreates Library and Console snapshot" <| asyncEx { let (loader : IWorkspaceLoader) = workspaceLoaderFactory toolsPath let sourceTextFactory : FsAutoComplete.ISourceTextFactory = FsAutoComplete.RoslynSourceTextFactory() - let srcDir = Projects.MultiProjectScenario1.multiProjectScenario1Dir - use dDir = Helpers.DisposableDirectory.From srcDir - let projects = Projects.MultiProjectScenario1.projects dDir.DirectoryInfo + use dDir = Helpers.DisposableDirectory.From Projects.MultiProjectScenario1.multiProjectScenario1Dir + let projects = Projects.MultiProjectScenario1.projects dDir.DirectoryInfo do! Dotnet.restoreAll projects let mutable loadedCalls = 0 @@ -702,23 +511,23 @@ let snapshotTests loaders toolsPath = let snapsA = loadedProjectsA - |> AMap.mapAVal (fun _ (_,v) -> Snapshots.optionsToSnapshot cache sourceTextFactory (Snapshots.createReferences cache sourceTextFactory loadedProjectsA) v) + |> AMap.mapAVal (fun _ v -> Snapshots.createSnapshot cache (AVal.constant sourceTextFactory) loadedProjectsA v) let snapshots = snapsA |> AMap.mapA (fun _ v -> v) |> AMap.force let libraryFile = Projects.MultiProjectScenario1.Library1.libraryFileIn dDir.DirectoryInfo - printfn "Setting last write time for %s %A" libraryFile.FullName libraryFile.LastWriteTime + // printfn "Setting last write time for %s %A" libraryFile.FullName libraryFile.LastWriteTime do! File.WriteAllTextAsync(libraryFile.FullName, "let x = 1") libraryFile.Refresh() - printfn "last write time for %s %A" libraryFile.FullName libraryFile.LastWriteTime - + // printfn "last write time for %s %A" libraryFile.FullName libraryFile.LastWriteTime let snapshots2 = snapsA |> AMap.mapA (fun _ v -> v) |> AMap.force let ls1 = snapshots |> HashMap.find ((Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) let ls2 = snapshots2 |> HashMap.find ((Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) + Expect.notEqual ls1 ls2 "library should not be the same" Expect.equal ls1.ProjectFileName ls2.ProjectFileName "Project file name should be the same" Expect.equal ls1.ProjectId ls2.ProjectId "Project Id name should be the same" Expect.equal ls1.SourceFiles.Length 3 "Source files length should be 3" @@ -727,7 +536,6 @@ let snapshotTests loaders toolsPath = Expect.equal ls1.ReferencedProjects.Length 0 "Referenced projects length should be 0" Expect.notEqual ls1.Stamp ls2.Stamp "Stamp should not be the same" - let cs1 = snapshots |> HashMap.find ((Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) let cs2 = snapshots2 |> HashMap.find ((Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) @@ -737,6 +545,11 @@ let snapshotTests loaders toolsPath = Expect.equal cs1.SourceFiles.Length cs2.SourceFiles.Length "Source files length should be the same" Expect.equal cs1.ReferencedProjects.Length cs2.ReferencedProjects.Length "Referenced projects length should be the same" Expect.equal cs1.ReferencedProjects.Length 1 "Referenced projects length should be 1" + let refLib1 = cs1.ReferencedProjects |> Seq.tryPick (fun x -> match x with | FSharpReferencedProjectSnapshot.FSharpReference(_, x) -> Some x | _ -> None) |> Option.get + Expect.equal refLib1 ls1 "Referenced library should be the same as library snapshot" + let refLib2 = cs2.ReferencedProjects |> Seq.tryPick (fun x -> match x with | FSharpReferencedProjectSnapshot.FSharpReference(_, x) -> Some x | _ -> None) |> Option.get + Expect.equal refLib2 ls2 "Referenced library should be the same as library snapshot" + Expect.notEqual refLib1 refLib2 "Referenced library from different snapshot should not be the same as library source file changed" Expect.notEqual cs1.Stamp cs2.Stamp "Stamp should not be the same" } From 6e6ef626fd4f134babc846e47282a0fe7fba8f73 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Tue, 19 Mar 2024 07:10:43 -0400 Subject: [PATCH 09/60] Much refactoring snapshot tests --- test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs index 2f8b5fd9b..6fcc5a409 100644 --- a/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs @@ -131,6 +131,7 @@ let createProjectA (projects : FileInfo seq) (loader : IWorkspaceLoader) onLoadC module Snapshots = open FsAutoComplete + open System.Threading let loadFromDotnetDll (p: Types.ProjectOptions) : ProjectSnapshot.FSharpReferencedProjectSnapshot = /// because only a successful compilation will be written to a DLL, we can rely on @@ -229,8 +230,12 @@ module Snapshots = let! writeTime = AdaptiveFile.GetLastWriteTimeUtc fileName and! sourceTextFactory = sourceTextFactory let getSource () = task { - let! text = File.ReadAllTextAsync fileName - return sourceTextFactory.Create((normalizePath fileName), text) :> ISourceTextNew + // let! text = File.ReadAllTextAsync fileName + let fileName = normalizePath fileName + use s = File.openFileStreamForReadingAsync fileName + + let! source = sourceTextFactory.Create(fileName, s) CancellationToken.None + return source :> ISourceTextNew } // printfn "Creating source text for %s" fileName return ProjectSnapshot.FSharpFileSnapshot.Create(fileName, string writeTime.Ticks, getSource) @@ -532,6 +537,9 @@ let snapshotTests loaders toolsPath = Expect.equal ls1.ProjectId ls2.ProjectId "Project Id name should be the same" Expect.equal ls1.SourceFiles.Length 3 "Source files length should be 3" Expect.equal ls1.SourceFiles.Length ls2.SourceFiles.Length "Source files length should be the same" + let ls1File = ls1.SourceFiles |> Seq.find (fun x -> x.FileName = libraryFile.FullName) + let ls2File = ls2.SourceFiles |> Seq.find (fun x -> x.FileName = libraryFile.FullName) + Expect.notEqual ls1File.Version ls2File.Version "Library source file version should not be the same" Expect.equal ls1.ReferencedProjects.Length ls2.ReferencedProjects.Length "Referenced projects length should be the same" Expect.equal ls1.ReferencedProjects.Length 0 "Referenced projects length should be 0" Expect.notEqual ls1.Stamp ls2.Stamp "Stamp should not be the same" @@ -543,6 +551,11 @@ let snapshotTests loaders toolsPath = Expect.equal cs1.ProjectId cs2.ProjectId "Project Id name should be the same" Expect.equal cs1.SourceFiles.Length 3 "Source files length should be 3" Expect.equal cs1.SourceFiles.Length cs2.SourceFiles.Length "Source files length should be the same" + let consoleFile = Projects.MultiProjectScenario1.Console1.programFileIn dDir.DirectoryInfo + let cs1File = cs1.SourceFiles |> Seq.find (fun x -> x.FileName = consoleFile.FullName) + let cs2File = cs2.SourceFiles |> Seq.find (fun x -> x.FileName = consoleFile.FullName) + Expect.equal cs1File.Version cs2File.Version "Console source file version should be the same" + Expect.equal cs1.ReferencedProjects.Length cs2.ReferencedProjects.Length "Referenced projects length should be the same" Expect.equal cs1.ReferencedProjects.Length 1 "Referenced projects length should be 1" let refLib1 = cs1.ReferencedProjects |> Seq.tryPick (fun x -> match x with | FSharpReferencedProjectSnapshot.FSharpReference(_, x) -> Some x | _ -> None) |> Option.get From fa38c5facc0d6e9cd394fe4c7f25c848087b0a43 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Wed, 20 Mar 2024 22:45:35 -0400 Subject: [PATCH 10/60] Add adaptive snapshots to lsp server --- src/FsAutoComplete.Core/AdaptiveExtensions.fs | 2 +- src/FsAutoComplete.Core/Commands.fs | 22 +- .../CompilerServiceInterface.fs | 61 +- .../CompilerServiceInterface.fsi | 21 +- src/FsAutoComplete.Core/FCSPatches.fs | 20 +- src/FsAutoComplete.Core/FCSPatches.fsi | 1 + src/FsAutoComplete.Core/SymbolLocation.fs | 6 +- src/FsAutoComplete/CodeFixes.fs | 3 +- src/FsAutoComplete/CodeFixes.fsi | 2 +- .../CodeFixes/AddTypeAliasToSignatureFile.fs | 22 + .../CodeFixes/AddTypeToIndeterminateValue.fs | 2 +- .../CodeFixes/MakeDeclarationMutable.fs | 2 +- src/FsAutoComplete/FsAutoComplete.fsproj | 20 +- .../LspServers/AdaptiveFSharpLspServer.fsi | 2 +- .../LspServers/AdaptiveServerState.fs | 684 +++++++++++------- .../LspServers/AdaptiveServerState.fsi | 10 +- .../LspServers/FSharpLspClient.fs | 5 +- .../LspServers/ProjectWorkspace.fs | 330 +++++++++ .../FsAutoComplete.Tests.Lsp/SnapshotTests.fs | 313 +------- 19 files changed, 897 insertions(+), 631 deletions(-) create mode 100644 src/FsAutoComplete/LspServers/ProjectWorkspace.fs diff --git a/src/FsAutoComplete.Core/AdaptiveExtensions.fs b/src/FsAutoComplete.Core/AdaptiveExtensions.fs index f637ccca1..db856c6c2 100644 --- a/src/FsAutoComplete.Core/AdaptiveExtensions.fs +++ b/src/FsAutoComplete.Core/AdaptiveExtensions.fs @@ -676,7 +676,7 @@ module AsyncAVal = /// adaptive inputs. /// let mapSync (mapping: 'a -> CancellationToken -> 'b) (input: asyncaval<'a>) = - map (fun a ct -> Task.FromResult(mapping a ct)) input + map (fun a ct -> Task.Run(fun () -> mapping a ct)) input /// /// Returns a new async adaptive value that adaptively applies the mapping function to the given diff --git a/src/FsAutoComplete.Core/Commands.fs b/src/FsAutoComplete.Core/Commands.fs index a30caa016..069e8983a 100644 --- a/src/FsAutoComplete.Core/Commands.fs +++ b/src/FsAutoComplete.Core/Commands.fs @@ -731,10 +731,10 @@ module Commands = let symbolUseWorkspaceAux (getDeclarationLocation: FSharpSymbolUse * IFSACSourceText -> Async) - (findReferencesForSymbolInFile: (string * FSharpProjectOptions * FSharpSymbol) -> Async) + (findReferencesForSymbolInFile: (string * FSharpProjectSnapshot * FSharpSymbol) -> Async) (tryGetFileSource: string -> Async>) - (tryGetProjectOptionsForFsproj: string -> Async) - (getAllProjectOptions: unit -> Async) + (tryGetProjectOptionsForFsproj: string -> Async) + (getAllProjectOptions: unit -> Async) (includeDeclarations: bool) (includeBackticks: bool) (errorOnFailureToFixRange: bool) @@ -787,7 +787,7 @@ module Commands = return (symbol, ranges) | scope -> - let projectsToCheck: Async = + let projectsToCheck: Async = async { match scope with | Some(SymbolDeclarationLocation.Projects(projects (*isLocalForProject=*) , true)) -> return projects @@ -799,7 +799,7 @@ module Commands = yield! project.ReferencedProjects - |> Array.map (fun p -> UMX.tag p.OutputFile |> tryGetProjectOptionsForFsproj) ] + |> List.map (fun p -> UMX.tag p.OutputFile |> tryGetProjectOptionsForFsproj) ] |> Async.parallel75 @@ -839,7 +839,7 @@ module Commands = /// Adds References of `symbol` in `file` to `dict` /// /// `Error` iff adjusting ranges failed (including cannot get source) and `errorOnFailureToFixRange`. Otherwise always `Ok` - let tryFindReferencesInFile (file: string, project: FSharpProjectOptions) = + let tryFindReferencesInFile (file: string, project: FSharpProjectSnapshot) = async { if dict.ContainsKey file then return Ok() @@ -882,7 +882,7 @@ module Commands = if errorOnFailureToFixRange then Error e else Ok()) - let iterProjects (projects: FSharpProjectOptions seq) = + let iterProjects (projects: FSharpProjectSnapshot seq) = // should: // * check files in parallel // * stop when error occurs @@ -890,7 +890,7 @@ module Commands = // -> map `Error` to `Some` for `Async.Choice`, afterwards map `Some` back to `Error` [ for project in projects do for file in project.SourceFiles do - let file = UMX.tag file + let file = UMX.tag file.FileName async { match! tryFindReferencesInFile (file, project) with @@ -930,10 +930,10 @@ module Commands = /// -> for "Rename" let symbolUseWorkspace (getDeclarationLocation: FSharpSymbolUse * IFSACSourceText -> Async) - (findReferencesForSymbolInFile: (string * FSharpProjectOptions * FSharpSymbol) -> Async) + (findReferencesForSymbolInFile: (string * FSharpProjectSnapshot * FSharpSymbol) -> Async) (tryGetFileSource: string -> Async>) - (tryGetProjectOptionsForFsproj: string -> Async) - (getAllProjectOptions: unit -> Async) + (tryGetProjectOptionsForFsproj: string -> Async) + (getAllProjectOptions: unit -> Async) (includeDeclarations: bool) (includeBackticks: bool) (errorOnFailureToFixRange: bool) diff --git a/src/FsAutoComplete.Core/CompilerServiceInterface.fs b/src/FsAutoComplete.Core/CompilerServiceInterface.fs index 6b51be745..bd377fd77 100644 --- a/src/FsAutoComplete.Core/CompilerServiceInterface.fs +++ b/src/FsAutoComplete.Core/CompilerServiceInterface.fs @@ -21,7 +21,7 @@ open IcedTasks type Version = int -type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelReferenceResolution, documentSource) = +type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelReferenceResolution) = let checker = FSharpChecker.Create( projectCacheSize = 200, @@ -189,7 +189,7 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe with get () = disableInMemoryProjectReferences and set (value) = disableInMemoryProjectReferences <- value - static member GetDependingProjects (file: string) (options: seq) = + static member GetDependingProjects (file: string) (options: seq) = let project = options |> Seq.tryFind (fun (k, _) -> (UMX.untag k).ToUpperInvariant() = (UMX.untag file).ToUpperInvariant()) @@ -203,8 +203,8 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe |> Seq.distinctBy (fun o -> o.ProjectFileName) |> Seq.filter (fun o -> o.ReferencedProjects - |> Array.map (fun p -> Path.GetFullPath p.OutputFile) - |> Array.contains option.ProjectFileName) ]) + |> List.map (fun p -> Path.GetFullPath p.OutputFile) + |> List.contains option.ProjectFileName) ]) member private __.GetNetFxScriptOptions(file: string, source) = async { @@ -278,11 +278,43 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe | FSIRefs.TFM.NetCore -> self.GetNetCoreScriptOptions(file, source) + member self.GetProjectSnapshotFromScript(file: string, source, tfm : FSIRefs.TFM) = async { + let _tfm = tfm + let allFlags = + Array.append [| "--targetprofile:netstandard" |] fsiAdditionalArguments + let! (snap, errors) = checker. GetProjectSnapshotFromScript( + UMX.untag file, + source, + assumeDotNetFramework = false, + useSdkRefs = true, + useFsiAuxLib = true, + otherFlags = allFlags, + userOpName = "getNetCoreScriptOptions" + ) + match errors with + | [] -> () + | errs -> + optsLogger.info ( + Log.setLogLevel LogLevel.Error + >> Log.setMessage "Resolved {opts} with {errors}" + >> Log.addContextDestructured "opts" snap + >> Log.addContextDestructured "errors" errs + ) + + return snap + } + + member __.ScriptTypecheckRequirementsChanged = scriptTypecheckRequirementsChanged.Publish member _.RemoveFileFromCache(file: string) = lastCheckResults.Remove(file) + member _.ClearCache(snap : FSharpProjectSnapshot seq) = + snap + |> Seq.map(_.Identifier) + |> checker.ClearCache + /// This function is called when the entire environment is known to have changed for reasons not encoded in the ProjectOptions of any project/compilation. member _.ClearCaches() = lastCheckResults.Dispose() @@ -400,7 +432,7 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe member x.TryGetRecentCheckResultsForFile ( file: string, - options: FSharpProjectOptions, + options: FSharpProjectSnapshot, source: ISourceText ) = async { @@ -413,10 +445,9 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe ) - let! snapshot = x.FromOption(options, documentSource) return - checker.TryGetRecentCheckResultsForFile(UMX.untag file, snapshot, opName) + checker.TryGetRecentCheckResultsForFile(UMX.untag file, options, opName) |> Option.map (fun (pr, cr) -> checkerLogger.info ( Log.setMessage "{opName} - got results - {version}" @@ -429,7 +460,7 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe member x.GetUsesOfSymbol ( file: string, - options: (string * FSharpProjectOptions) seq, + options: (string * FSharpProjectSnapshot) seq, symbol: FSharpSymbol ) = async { @@ -441,8 +472,8 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe match FSharpCompilerServiceChecker.GetDependingProjects file options with | None -> return [||] | Some(opts, []) -> - let! snapshot = x.FromOption(opts, documentSource) - let! res = checker.ParseAndCheckProject(snapshot) + // let! snapshot = x.FromOption(opts, documentSource) + let! res = checker.ParseAndCheckProject(opts) return res.GetUsesOfSymbol symbol | Some(opts, dependentProjects) -> let! res = @@ -450,8 +481,8 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe |> List.map (fun (opts) -> async { - let! snapshot = x.FromOption(opts, documentSource) - let! res = checker.ParseAndCheckProject(snapshot) + // let! snapshot = x.FromOption(opts, documentSource) + let! res = checker.ParseAndCheckProject(opts) return res.GetUsesOfSymbol symbol }) |> Async.parallel75 @@ -459,15 +490,15 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe return res |> Array.concat } - member x.FindReferencesForSymbolInFile(file, project: FSharpProjectOptions, symbol) = + member x.FindReferencesForSymbolInFile(file, project: FSharpProjectSnapshot, symbol) = async { checkerLogger.info ( Log.setMessage "FindReferencesForSymbolInFile - {file}" >> Log.addContextDestructured "file" file ) - let! snapshot = x.FromOption(project, documentSource) - return! checker.FindBackgroundReferencesInFile(file, snapshot, symbol, userOpName = "find references") + // let! snapshot = x.FromOption(project, documentSource) + return! checker.FindBackgroundReferencesInFile(file, project, symbol, userOpName = "find references") } // member this.GetDeclarations(fileName: string, source: ISourceText, options: FSharpProjectOptions, _) = diff --git a/src/FsAutoComplete.Core/CompilerServiceInterface.fsi b/src/FsAutoComplete.Core/CompilerServiceInterface.fsi index bf3c145fb..8e454b16a 100644 --- a/src/FsAutoComplete.Core/CompilerServiceInterface.fsi +++ b/src/FsAutoComplete.Core/CompilerServiceInterface.fsi @@ -15,24 +15,31 @@ type Version = int type FSharpCompilerServiceChecker = new: - hasAnalyzers: bool * typecheckCacheSize: int64 * parallelReferenceResolution: bool * documentSource: DocumentSource -> + hasAnalyzers: bool * typecheckCacheSize: int64 * parallelReferenceResolution: bool -> FSharpCompilerServiceChecker member DisableInMemoryProjectReferences: bool with get, set static member GetDependingProjects: - file: string -> - options: seq -> - (FSharpProjectOptions * FSharpProjectOptions list) option + file : string -> + options: seq + -> option> member GetProjectOptionsFromScript: file: string * source: ISourceText * tfm: FSIRefs.TFM -> Async + member GetProjectSnapshotFromScript: + file: string * source: ISourceTextNew * tfm: FSIRefs.TFM -> Async + member ScriptTypecheckRequirementsChanged: IEvent member RemoveFileFromCache: file: string -> unit + member ClearCache: + snap: seq + -> unit + /// This function is called when the entire environment is known to have changed for reasons not encoded in the ProjectOptions of any project/compilation. member ClearCaches: unit -> unit @@ -82,14 +89,14 @@ type FSharpCompilerServiceChecker = member TryGetLastCheckResultForFile: file: string -> ParseAndCheckResults option member TryGetRecentCheckResultsForFile: - file: string * options: FSharpProjectOptions * source: ISourceText -> Async + file: string * options: FSharpProjectSnapshot * source: ISourceText -> Async member GetUsesOfSymbol: - file: string * options: (string * FSharpProjectOptions) seq * symbol: FSharpSymbol -> + file: string * options: (string * FSharpProjectSnapshot) seq * symbol: FSharpSymbol -> Async member FindReferencesForSymbolInFile: - file: string * project: FSharpProjectOptions * symbol: FSharpSymbol -> Async> + file: string * project: FSharpProjectSnapshot * symbol: FSharpSymbol -> Async> // member GetDeclarations: // fileName: string * source: ISourceText * options: FSharpProjectOptions * version: 'a -> diff --git a/src/FsAutoComplete.Core/FCSPatches.fs b/src/FsAutoComplete.Core/FCSPatches.fs index 83ba575c7..f6f4d5af9 100644 --- a/src/FsAutoComplete.Core/FCSPatches.fs +++ b/src/FsAutoComplete.Core/FCSPatches.fs @@ -282,12 +282,24 @@ module LanguageVersionShim = /// let defaultLanguageVersion = lazy (LanguageVersionShim("latest")) + let internal formOtherOptions (options : string seq) = + options + |> Seq.tryFind (fun x -> x.StartsWith("--langversion:", StringComparison.Ordinal)) + |> Option.map (fun x -> x.Split(":")[1]) + |> Option.map (fun x -> LanguageVersionShim(x)) + |> Option.defaultWith (fun () -> defaultLanguageVersion.Value) + /// Tries to parse out "--langversion:" from OtherOptions if it can't find it, returns defaultLanguageVersion /// The FSharpProjectOptions to use /// A LanguageVersionShim from the parsed "--langversion:" or defaultLanguageVersion let fromFSharpProjectOptions (fpo: FSharpProjectOptions) = fpo.OtherOptions - |> Array.tryFind (fun x -> x.StartsWith("--langversion:", StringComparison.Ordinal)) - |> Option.map (fun x -> x.Split(":")[1]) - |> Option.map (fun x -> LanguageVersionShim(x)) - |> Option.defaultWith (fun () -> defaultLanguageVersion.Value) + |> formOtherOptions + + + /// Tries to parse out "--langversion:" from OtherOptions if it can't find it, returns defaultLanguageVersion + /// The FSharpProjectOptions to use + /// A LanguageVersionShim from the parsed "--langversion:" or defaultLanguageVersion + let fromFSharpProjectSnapshot (fpo: FSharpProjectSnapshot) = + fpo.OtherOptions + |> formOtherOptions diff --git a/src/FsAutoComplete.Core/FCSPatches.fsi b/src/FsAutoComplete.Core/FCSPatches.fsi index 58ef5dc28..3eeebcdf7 100644 --- a/src/FsAutoComplete.Core/FCSPatches.fsi +++ b/src/FsAutoComplete.Core/FCSPatches.fsi @@ -22,6 +22,7 @@ module LanguageVersionShim = /// The FSharpProjectOptions to use /// A LanguageVersionShim from the parsed "--langversion:" or defaultLanguageVersion val fromFSharpProjectOptions: fpo: FSharpProjectOptions -> LanguageVersionShim + val fromFSharpProjectSnapshot: fpo: FSharpProjectSnapshot -> LanguageVersionShim module SyntaxTreeOps = val synExprContainsError: SynExpr -> bool diff --git a/src/FsAutoComplete.Core/SymbolLocation.fs b/src/FsAutoComplete.Core/SymbolLocation.fs index a598afd25..39ad76eb7 100644 --- a/src/FsAutoComplete.Core/SymbolLocation.fs +++ b/src/FsAutoComplete.Core/SymbolLocation.fs @@ -9,15 +9,15 @@ open FsToolkit.ErrorHandling [] type SymbolDeclarationLocation = | CurrentDocument - | Projects of FSharpProjectOptions list * isLocalForProject: bool + | Projects of FSharpProjectSnapshot list * isLocalForProject: bool let getDeclarationLocation ( symbolUse: FSharpSymbolUse, currentDocument: IFSACSourceText, getProjectOptions, - projectsThatContainFile: string -> Async, - getDependentProjectsOfProjects: FSharpProjectOptions list -> Async + projectsThatContainFile: string -> Async, + getDependentProjectsOfProjects: FSharpProjectSnapshot list -> Async ) : Async> = asyncOption { diff --git a/src/FsAutoComplete/CodeFixes.fs b/src/FsAutoComplete/CodeFixes.fs index 481dd9e28..cc6daf732 100644 --- a/src/FsAutoComplete/CodeFixes.fs +++ b/src/FsAutoComplete/CodeFixes.fs @@ -19,6 +19,7 @@ module LspTypes = Ionide.LanguageServerProtocol.Types module Types = open FsAutoComplete.FCSPatches open System.Threading.Tasks + open FSharp.Compiler.CodeAnalysis.ProjectSnapshot type IsEnabled = unit -> bool @@ -34,7 +35,7 @@ module Types = type GetLanguageVersion = string -> Async type GetProjectOptionsForFile = - string -> Async> + string -> Async> [] type FixKind = diff --git a/src/FsAutoComplete/CodeFixes.fsi b/src/FsAutoComplete/CodeFixes.fsi index 05f068a20..cf8b6c009 100644 --- a/src/FsAutoComplete/CodeFixes.fsi +++ b/src/FsAutoComplete/CodeFixes.fsi @@ -28,7 +28,7 @@ module Types = type GetLanguageVersion = string -> Async type GetProjectOptionsForFile = - string -> Async> + string -> Async> [] type FixKind = diff --git a/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs b/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs index 7baa7dfd6..dd0e98024 100644 --- a/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs +++ b/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs @@ -37,6 +37,28 @@ type SynTypeDefn with let title = "Add type alias to signature file" +let codeFixForImplementationFileWithSignature + (getProjectOptionsForFile: GetProjectOptionsForFile) + (codeFix: CodeFix) + (codeActionParams: CodeActionParams) + : Async> = + async { + let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath + let! project = getProjectOptionsForFile fileName + + match project with + | Error _ -> return Ok [] + | Ok projectOptions -> + + let signatureFile = String.Concat(fileName, "i") + let hasSig = projectOptions.SourceFiles |> List.exists(fun s -> s.FileName = signatureFile ) + + if not hasSig then + return Ok [] + else + return! codeFix codeActionParams + } + let fix (getProjectOptionsForFile: GetProjectOptionsForFile) (getParseResultsForFile: GetParseResultsForFile) diff --git a/src/FsAutoComplete/CodeFixes/AddTypeToIndeterminateValue.fs b/src/FsAutoComplete/CodeFixes/AddTypeToIndeterminateValue.fs index b2c63ab7c..33f9b1e12 100644 --- a/src/FsAutoComplete/CodeFixes/AddTypeToIndeterminateValue.fs +++ b/src/FsAutoComplete/CodeFixes/AddTypeToIndeterminateValue.fs @@ -47,7 +47,7 @@ let fix declRange.Start.Column declText SymbolLookupKind.ByLongIdent - projectOptions.OtherOptions + (Array.ofList projectOptions.OtherOptions) |> Result.ofOption (fun _ -> "No lexer symbol for declaration") let! declSymbolUse = diff --git a/src/FsAutoComplete/CodeFixes/MakeDeclarationMutable.fs b/src/FsAutoComplete/CodeFixes/MakeDeclarationMutable.fs index ff2e2ac36..e7c5a866b 100644 --- a/src/FsAutoComplete/CodeFixes/MakeDeclarationMutable.fs +++ b/src/FsAutoComplete/CodeFixes/MakeDeclarationMutable.fs @@ -22,7 +22,7 @@ let fix let! tyRes, line, _lines = getParseResultsForFile fileName fcsPos let! opts = getProjectOptionsForFile fileName - match Lexer.getSymbol fcsPos.Line fcsPos.Column line SymbolLookupKind.Fuzzy opts.OtherOptions with + match Lexer.getSymbol fcsPos.Line fcsPos.Column line SymbolLookupKind.Fuzzy (Array.ofList opts.OtherOptions) with | Some _symbol -> match! tyRes.TryFindDeclaration fcsPos line with | FindDeclarationResult.Range declRange when declRange.FileName = (UMX.untag fileName) -> diff --git a/src/FsAutoComplete/FsAutoComplete.fsproj b/src/FsAutoComplete/FsAutoComplete.fsproj index 0caf50835..e0763796b 100644 --- a/src/FsAutoComplete/FsAutoComplete.fsproj +++ b/src/FsAutoComplete/FsAutoComplete.fsproj @@ -19,14 +19,8 @@ true - - + + @@ -39,6 +33,7 @@ + @@ -68,17 +63,12 @@ $(TargetsForTfmSpecificBuildOutput);CopyProjectReferencesToPackage - - + - + - - diff --git a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fsi b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fsi index 58f5e62d2..9e1c43034 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fsi +++ b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fsi @@ -12,7 +12,7 @@ type AdaptiveFSharpLspServer = interface IFSharpLspServer - member ScriptFileProjectOptions: IEvent + member ScriptFileProjectOptions: IEvent module AdaptiveFSharpLspServer = open System.Threading.Tasks diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index 5ce218b7a..2bd00ea9d 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -37,6 +37,9 @@ open FsAutoComplete.Lsp open FsAutoComplete.Lsp.Helpers open FSharp.Compiler.Syntax open FSharp.Compiler.CodeAnalysis +open FsAutoComplete.ProjectWorkspace + + [] @@ -50,9 +53,19 @@ type AdaptiveWorkspaceChosen = | NotChosen +[] +module Helpers3 = + open FSharp.Compiler.CodeAnalysis.ProjectSnapshot + type FSharpReferencedProjectSnapshot with + + member x.ProjectFilePath = + match x with + | FSharpReferencedProjectSnapshot.FSharpReference(snapshot = snapshot) -> snapshot.ProjectFileName |> Some + | _ -> None + [] type LoadedProject = - { FSharpProjectOptions: FSharpProjectOptions + { FSharpProjectOptions: FSharpProjectSnapshot LanguageVersion: LanguageVersionShim } interface IEquatable with @@ -66,9 +79,7 @@ type LoadedProject = | :? LoadedProject as other -> (x :> IEquatable<_>).Equals other | _ -> false - member x.GetSnapshot(documentSource) = FSharpProjectSnapshot.FromOptions(x.FSharpProjectOptions, documentSource) - - member x.SourceFiles = x.FSharpProjectOptions.SourceFiles + member x.SourceFiles = x.FSharpProjectOptions.SourceFiles |> List.map(fun f -> f.FileName) |> List.toArray member x.ProjectFileName = x.FSharpProjectOptions.ProjectFileName static member op_Implicit(x: LoadedProject) = x.FSharpProjectOptions @@ -107,7 +118,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let checker = (config, documentSource) - ||> AVal.map2 (fun c ds -> c.EnableAnalyzers, c.Fsac.CachedTypeCheckCount, c.Fsac.ParallelReferenceResolution, ds) + ||> AVal.map2 (fun c _ds -> c.EnableAnalyzers, c.Fsac.CachedTypeCheckCount, c.Fsac.ParallelReferenceResolution) |> AVal.map (FSharpCompilerServiceChecker) let configChanges = @@ -268,7 +279,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let notifications = Event() - let scriptFileProjectOptions = Event() + let scriptFileProjectOptions = Event() let fileParsed = Event() @@ -822,175 +833,93 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac |> Option.map (fun v -> v.Split(';', StringSplitOptions.RemoveEmptyEntries)) + let loadProjects (loader : IWorkspaceLoader) binlogConfig projects = + projects + |> AMap.mapWithAdditionalDependencies (fun projects -> - let loadedProjectOptions = - asyncAVal { - let! loader = - loader - |> addAValLogging (fun () -> logger.info (Log.setMessage "Loading projects because WorkspaceLoader change")) - - and! wsp = - adaptiveWorkspacePaths - |> addAValLogging (fun () -> - logger.info (Log.setMessage "Loading projects because adaptiveWorkspacePaths change")) - - and! binlogConfig = - binlogConfig - |> addAValLogging (fun () -> logger.info (Log.setMessage "Loading projects because binlogConfig change")) - - match wsp with - | AdaptiveWorkspaceChosen.NotChosen -> return [] - | AdaptiveWorkspaceChosen.Projs projects -> - let! projectOptions = - projects - |> AMap.mapWithAdditionalDependencies (fun projects -> - - projects - |> Seq.iter (fun (proj: string, _) -> - let not = - UMX.untag proj |> ProjectResponse.ProjectLoading |> NotificationEvent.Workspace - - notifications.Trigger(not, CancellationToken.None)) - - use progressReport = new ServerProgressReport(lspClient) - - progressReport.Begin ($"Loading {projects.Count} Projects") (CancellationToken.None) - |> ignore> - - let projectOptions = - loader.LoadProjects(projects |> Seq.map (fst >> UMX.untag) |> Seq.toList, [], binlogConfig) - |> Seq.toList - - for p in projectOptions do - logger.info ( - Log.setMessage "Found BaseIntermediateOutputPath of {path}" - >> Log.addContextDestructured "path" ((|BaseIntermediateOutputPath|_|) p.Properties) - ) - - // Collect other files that should trigger a reload of a project - let additionalDependencies (p: Types.ProjectOptions) = - [ let projectFileChanges = projectFileChanges p.ProjectFileName - - match p.Properties with - | ProjectAssetsFile v -> yield projectFileChanges (UMX.tag v) - | _ -> () - - let objPath = (|BaseIntermediateOutputPath|_|) p.Properties - - let isWithinObjFolder (file: string) = - match objPath with - | None -> true // if no obj folder provided assume we should track this file - | Some objPath -> file.Contains(objPath) - - match p.Properties with - | MSBuildAllProjects v -> - yield! - v - |> Array.filter (fun x -> x.EndsWith(".props", StringComparison.Ordinal) && isWithinObjFolder x) - |> Array.map (UMX.tag >> projectFileChanges) - | _ -> () ] - - HashMap.ofList - [ for p in projectOptions do - UMX.tag p.ProjectFileName, (p, additionalDependencies p) ] - - ) - |> AMap.toAVal - |> AVal.map HashMap.toValueList - - - and! checker = checker - checker.ClearCaches() // if we got new projects assume we're gonna need to clear caches - - let options = - let fsharpOptions = projectOptions |> FCS.mapManyOptions |> Seq.toList - - List.zip projectOptions fsharpOptions - |> List.map (fun (projectOption, fso) -> - - let langversion = LanguageVersionShim.fromFSharpProjectOptions fso + projects + |> Seq.iter (fun (proj: string, _) -> + let not = + UMX.untag proj |> ProjectResponse.ProjectLoading |> NotificationEvent.Workspace - // Set some default values as FCS uses these for identification/caching purposes - let fso = - { fso with - SourceFiles = fso.SourceFiles |> Array.map (Utils.normalizePath >> UMX.untag) - Stamp = fso.Stamp |> Option.orElse (Some DateTime.UtcNow.Ticks) - ProjectId = fso.ProjectId |> Option.orElse (Some(Guid.NewGuid().ToString())) } + notifications.Trigger(not, CancellationToken.None)) - { FSharpProjectOptions = fso - LanguageVersion = langversion }, - projectOption) + use progressReport = new ServerProgressReport(lspClient) - options - |> List.iter (fun (loadedProject, projectOption) -> - let projectFileName = loadedProject.ProjectFileName - let projViewerItemsNormalized = ProjectViewer.render projectOption + progressReport.Begin ($"Loading {projects.Count} Projects") (CancellationToken.None) + |> ignore> - let responseFiles = - projViewerItemsNormalized.Items - |> List.map (function - | ProjectViewerItem.Compile(p, c) -> ProjectViewerItem.Compile(Helpers.fullPathNormalized p, c)) - |> List.choose (function - | ProjectViewerItem.Compile(p, _) -> Some p) + let projectOptions = + loader.LoadProjects(projects |> Seq.map (fst >> UMX.untag) |> Seq.toList, [], binlogConfig) + |> Seq.toList - let references = - FscArguments.references (loadedProject.FSharpProjectOptions.OtherOptions |> List.ofArray) + for p in projectOptions do + logger.info ( + Log.setMessage "Found BaseIntermediateOutputPath of {path}" + >> Log.addContextDestructured "path" ((|BaseIntermediateOutputPath|_|) p.Properties) + ) + let projectFileName = p.ProjectFileName + let projViewerItemsNormalized = ProjectViewer.render p - logger.info ( - Log.setMessage "ProjectLoaded {file}" - >> Log.addContextDestructured "file" projectFileName - ) + let responseFiles = + projViewerItemsNormalized.Items + |> List.map (function + | ProjectViewerItem.Compile(p, c) -> ProjectViewerItem.Compile(Helpers.fullPathNormalized p, c)) + |> List.choose (function + | ProjectViewerItem.Compile(p, _) -> Some p) - let ws = - { ProjectFileName = projectFileName - ProjectFiles = responseFiles - OutFileOpt = Option.ofObj projectOption.TargetPath - References = references - Extra = projectOption - ProjectItems = projViewerItemsNormalized.Items - Additionals = Map.empty } + let references = + FscArguments.references (p.OtherOptions) - let not = ProjectResponse.Project(ws, false) |> NotificationEvent.Workspace - notifications.Trigger(not, CancellationToken.None)) + logger.info ( + Log.setMessage "ProjectLoaded {file}" + >> Log.addContextDestructured "file" projectFileName + ) - let not = ProjectResponse.WorkspaceLoad true |> NotificationEvent.Workspace + let ws = + { ProjectFileName = projectFileName + ProjectFiles = responseFiles + OutFileOpt = Option.ofObj p.TargetPath + References = references + Extra = p + ProjectItems = projViewerItemsNormalized.Items + Additionals = Map.empty } + let not = ProjectResponse.Project(ws, false) |> NotificationEvent.Workspace notifications.Trigger(not, CancellationToken.None) - return options |> List.map fst - } + let not = ProjectResponse.WorkspaceLoad true |> NotificationEvent.Workspace - /// - /// Evaluates the adaptive value and returns its current value. - /// This should not be used inside the adaptive evaluation of other AdaptiveObjects since it does not track dependencies. - /// - /// A list of FSharpProjectOptions - let forceLoadProjects () = loadedProjectOptions |> AsyncAVal.forceAsync + notifications.Trigger(not, CancellationToken.None) - do - // Reload Projects with some debouncing if `loadedProjectOptions` is out of date. - AVal.Observable.onOutOfDateWeak loadedProjectOptions - |> Observable.throttleOn Concurrency.NewThreadScheduler.Default (TimeSpan.FromMilliseconds(200.)) - |> Observable.observeOn Concurrency.NewThreadScheduler.Default - |> Observable.subscribe (fun _ -> forceLoadProjects () |> Async.Ignore> |> Async.Start) - |> disposables.Add + // Collect other files that should trigger a reload of a project + let additionalDependencies (p: Types.ProjectOptions) = + [ let projectFileChanges = projectFileChanges p.ProjectFileName + match p.Properties with + | ProjectAssetsFile v -> yield projectFileChanges (UMX.tag v) + | _ -> () - let sourceFileToProjectOptions = - asyncAVal { - let! options = loadedProjectOptions + let objPath = (|BaseIntermediateOutputPath|_|) p.Properties - return - options - |> List.collect (fun proj -> - proj.SourceFiles - |> Array.map (fun source -> Utils.normalizePath source, proj) - |> Array.toList) - |> List.groupByFst + let isWithinObjFolder (file: string) = + match objPath with + | None -> true // if no obj folder provided assume we should track this file + | Some objPath -> file.Contains(objPath) - |> AMap.ofList + match p.Properties with + | MSBuildAllProjects v -> + yield! + v + |> Array.filter (fun x -> x.EndsWith(".props", StringComparison.Ordinal) && isWithinObjFolder x) + |> Array.map (UMX.tag >> projectFileChanges) + | _ -> () ] + + HashMap.ofList + [ for p in projectOptions do + UMX.tag p.ProjectFileName, (p, additionalDependencies p) ] + ) - } let openFilesTokens = @@ -1082,6 +1011,191 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac return file }) + let snapshots = + amap { + let! (loader, wsp, binlogConfig) = aval { + let! loader = + loader + |> addAValLogging (fun () -> logger.info (Log.setMessage "Loading projects because WorkspaceLoader change")) + + and! wsp = + adaptiveWorkspacePaths + |> addAValLogging (fun () -> + logger.info (Log.setMessage "Loading projects because adaptiveWorkspacePaths change")) + + and! binlogConfig = + // AVal.constant Ionide.ProjInfo.BinaryLogGeneration.Off + binlogConfig + |> addAValLogging (fun () -> logger.info (Log.setMessage "Loading projects because binlogConfig change")) + + return loader, wsp, binlogConfig + } + + match wsp with + | AdaptiveWorkspaceChosen.NotChosen -> () + | AdaptiveWorkspaceChosen.Projs projects -> + let projects = + loadProjects loader binlogConfig projects + // |> AMap.map' AVal.constant + yield! Snapshots.createSnapshots openFilesWithChanges (AVal.constant sourceTextFactory) projects + } + + let loadedProjectSnapshots2 = + snapshots + |> AMap.map(fun _ (proj, snap) -> + + + proj, + aval { + let! snap = snap + // and! _checker = checker + // checker.ClearCache([snap]) + let langversion = LanguageVersionShim.fromFSharpProjectSnapshot snap + return + { FSharpProjectOptions = snap + LanguageVersion = langversion } + } + ) + + let loadedProjectSnapshots = + loadedProjectSnapshots2 + |> AMap.mapA (fun _ (_,v) -> v) + |> AMap.toAVal + |> AVal.map HashMap.toValueList + + + // let loadedProjectOptions = + // aval { + // let! loader = + // loader + // |> addAValLogging (fun () -> logger.info (Log.setMessage "Loading projects because WorkspaceLoader change")) + + // and! wsp = + // adaptiveWorkspacePaths + // |> addAValLogging (fun () -> + // logger.info (Log.setMessage "Loading projects because adaptiveWorkspacePaths change")) + + // and! binlogConfig = + // binlogConfig + // |> addAValLogging (fun () -> logger.info (Log.setMessage "Loading projects because binlogConfig change")) + + // match wsp with + // | AdaptiveWorkspaceChosen.NotChosen -> return [] + // | AdaptiveWorkspaceChosen.Projs projects -> + // let! projectOptions = + // loadProjects loader binlogConfig projects + // |> AMap.toAVal + // |> AVal.map HashMap.toValueList + + // and! checker = checker + // checker.ClearCaches() // if we got new projects assume we're gonna need to clear caches + + // let options = + // let fsharpOptions = projectOptions |> FCS.mapManyOptions |> Seq.toList + + // List.zip projectOptions fsharpOptions + // |> List.map (fun (projectOption, fso) -> + + // let langversion = LanguageVersionShim.fromFSharpProjectOptions fso + + // // Set some default values as FCS uses these for identification/caching purposes + // let fso = + // { fso with + // SourceFiles = fso.SourceFiles |> Array.map (Utils.normalizePath >> UMX.untag) + // Stamp = fso.Stamp |> Option.orElse (Some DateTime.UtcNow.Ticks) + // ProjectId = fso.ProjectId |> Option.orElse (Some(Guid.NewGuid().ToString())) } + + // { FSharpProjectOptions = fso + // LanguageVersion = langversion }, + // projectOption) + + // options + // |> List.iter (fun (loadedProject, projectOption) -> + // let projectFileName = loadedProject.ProjectFileName + // let projViewerItemsNormalized = ProjectViewer.render projectOption + + // let responseFiles = + // projViewerItemsNormalized.Items + // |> List.map (function + // | ProjectViewerItem.Compile(p, c) -> ProjectViewerItem.Compile(Helpers.fullPathNormalized p, c)) + // |> List.choose (function + // | ProjectViewerItem.Compile(p, _) -> Some p) + + // let references = + // FscArguments.references (loadedProject.FSharpProjectOptions.OtherOptions |> List.ofArray) + + // logger.info ( + // Log.setMessage "ProjectLoaded {file}" + // >> Log.addContextDestructured "file" projectFileName + // ) + + // let ws = + // { ProjectFileName = projectFileName + // ProjectFiles = responseFiles + // OutFileOpt = Option.ofObj projectOption.TargetPath + // References = references + // Extra = projectOption + // ProjectItems = projViewerItemsNormalized.Items + // Additionals = Map.empty } + + // let not = ProjectResponse.Project(ws, false) |> NotificationEvent.Workspace + // notifications.Trigger(not, CancellationToken.None)) + + // let not = ProjectResponse.WorkspaceLoad true |> NotificationEvent.Workspace + + // notifications.Trigger(not, CancellationToken.None) + + // return options |> List.map fst + // } + + /// + /// Evaluates the adaptive value and returns its current value. + /// This should not be used inside the adaptive evaluation of other AdaptiveObjects since it does not track dependencies. + /// + /// A list of FSharpProjectOptions + let forceLoadProjects () = loadedProjectSnapshots |> AVal.force + + do + // Reload Projects with some debouncing if `loadedProjectOptions` is out of date. + AVal.Observable.onOutOfDateWeak loadedProjectSnapshots + |> Observable.throttleOn Concurrency.NewThreadScheduler.Default (TimeSpan.FromMilliseconds(200.)) + |> Observable.observeOn Concurrency.NewThreadScheduler.Default + |> Observable.subscribe (fun _ -> forceLoadProjects () |> ignore )//|> Async.Ignore> |> Async.Start) + |> disposables.Add + + + let sourceFileToProjectOptions = + + + amap { + let! snaps = loadedProjectSnapshots + yield! + snaps + |> List.collect (fun proj -> + proj.SourceFiles + |> Array.toList + |> List.map (fun source -> Utils.normalizePath source, proj) + ) + |> List.groupByFst + + } + + let _sourceFileToProjectOptions2 = + + amap { + let! snaps = loadedProjectSnapshots2 |> AMap.toAVal + yield! + snaps + |> HashMap.toList + |> List.collect (fun (_, (proj,v)) -> + proj.SourceFiles + |> List.map (fun source -> Utils.normalizePath source, (proj,v) + ) + |> List.groupByFst) + } + + + let cancelToken filePath version (cts: CancellationTokenSource) = logger.info ( @@ -1169,7 +1283,6 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac return file, projs else - let! sourceFileToProjectOptions = sourceFileToProjectOptions let! projs = sourceFileToProjectOptions @@ -1186,7 +1299,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac openFilesToChangesAndProjectOptions |> AMap.map (fun _k v -> v |> AsyncAVal.mapSync (fun (file, projects) _ -> file, projects)) - let! sourceFileToProjectOptions = sourceFileToProjectOptions + // let! sourceFileToProjectOptions = sourceFileToProjectOptions2 let loses = sourceFileToProjectOptions @@ -1214,27 +1327,27 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac } |> Async.map (Result.ofOption (fun () -> $"Could not read file: {file}")) - let documentSourceLookup (filePath: string) = - asyncOption { - let! file = forceFindOpenFileOrRead (Utils.normalizePath filePath) - let! file = Result.toOption file - return file.Source :> ISourceText - } + // let documentSourceLookup (filePath: string) = + // asyncOption { + // let! file = forceFindOpenFileOrRead (Utils.normalizePath filePath) + // let! file = Result.toOption file + // return file.Source :> ISourceText + // } - do transact (fun () -> documentSource.Value <- DocumentSource.Custom documentSourceLookup) - let fileShimChanges = openFilesWithChanges |> AMap.mapA (fun _ v -> v) - // let cachedFileContents = cachedFileContents |> cmap.mapA (fun _ v -> v) + // do transact (fun () -> documentSource.Value <- DocumentSource.Custom documentSourceLookup) + // let fileShimChanges = openFilesWithChanges |> AMap.mapA (fun _ v -> v) + // // let cachedFileContents = cachedFileContents |> cmap.mapA (fun _ v -> v) - let filesystemShim file = - // GetLastWriteTimeShim gets called _a lot_ and when we do checks on save we use Async.Parallel for type checking. - // Adaptive uses lots of locks under the covers, so many threads can get blocked waiting for data. - // flattening openFilesWithChanges makes this check a lot quicker as it's not needing to recalculate each value. + // let filesystemShim file = + // // GetLastWriteTimeShim gets called _a lot_ and when we do checks on save we use Async.Parallel for type checking. + // // Adaptive uses lots of locks under the covers, so many threads can get blocked waiting for data. + // // flattening openFilesWithChanges makes this check a lot quicker as it's not needing to recalculate each value. - fileShimChanges |> AMap.force |> HashMap.tryFind file + // fileShimChanges |> AMap.force |> HashMap.tryFind file - do - FSharp.Compiler.IO.FileSystemAutoOpens.FileSystem <- - FileSystem(FSharp.Compiler.IO.FileSystemAutoOpens.FileSystem, filesystemShim) + // do + // FSharp.Compiler.IO.FileSystemAutoOpens.FileSystem <- + // FileSystem(FSharp.Compiler.IO.FileSystemAutoOpens.FileSystem, filesystemShim) /// Parses a source code for a file and caches the results. Returns an AST that can be traversed for various features. /// The FSharpCompilerServiceChecker. @@ -1244,10 +1357,11 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac /// let parseFile (checker: FSharpCompilerServiceChecker) (source: VolatileFile) options snap = async { + let _options = options let! result = checker.ParseFile(source.FileName, source.Source, snap) - let! ct = Async.CancellationToken - fileParsed.Trigger(result, options, ct) + // let! ct = Async.CancellationToken + // fileParsed.Trigger(result, options, ct) return result } @@ -1256,26 +1370,24 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac /// Parses all files in the workspace. This is mostly used to trigger finding tests. let parseAllFiles () = asyncAVal { - let! projects = loadedProjectOptions + let! projects = loadedProjectSnapshots and! (checker: FSharpCompilerServiceChecker) = checker - and! documentSource = documentSource - let fromOpts opts = checker.FromOptions(opts, documentSource) - let! projects = + let projects = projects |> List.map (fun p -> p.FSharpProjectOptions) |> List.toArray - |> fromOpts + // |> fromOpts return projects - |> Array.collect (fun (p, snap) -> p.SourceFiles |> Array.map (fun s -> p, snap, s)) - |> Array.map (fun (opts, snap, fileName) -> - let fileName = UMX.tag fileName + |> Array.collect (fun (snap) -> snap.SourceFiles |> List.toArray |> Array.map (fun s -> snap, s)) + |> Array.map (fun (snap, fileName) -> + let fileName = UMX.tag fileName.FileName asyncResult { let! file = forceFindOpenFileOrRead fileName - return! parseFile checker file opts snap + return! parseFile checker file () snap } |> Async.map Result.toOption) |> Async.parallel75 @@ -1304,15 +1416,14 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac asyncAVal { let! (checker: FSharpCompilerServiceChecker) = checker and! selectProject = projectSelector - and! documentSource = documentSource return! asyncResult { let! options = options let! project = selectProject.FindProject(file.FileName, options) - let! snap = checker.FromOption(project.FSharpProjectOptions, documentSource) - let options = project.FSharpProjectOptions - return! parseFile checker file options snap + // let! snap = checker.FromOption(project.FSharpProjectOptions, documentSource) + // let options = project.FSharpProjectOptions + return! parseFile checker file options project } }) @@ -1401,12 +1512,20 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let getAutoCompleteNamespacesByDeclName name = autoCompleteNamespaces |> AMap.tryFind name + let checkLocks = ConcurrentDictionary, SemaphoreSlim>() + + let getCheckLock filePath = + match checkLocks.TryGetValue(filePath) with + | (true, v) -> v + | _ -> checkLocks.GetOrAdd(filePath, new SemaphoreSlim(1,1)) + /// Gets Parse and Check results of a given file while also handling other concerns like Progress, Logging, Eventing. /// The FSharpCompilerServiceChecker. /// The name of the file in the project whose source to find a typecheck. /// The options for the project or script. /// Determines if the typecheck should be cached for autocompletions. /// The cache to use for autocompletions. + /// cancellationtoken /// let parseAndCheckFile (checker: FSharpCompilerServiceChecker) @@ -1415,79 +1534,88 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac shouldCache snapshotCache = - asyncEx { - let tags = - [ SemanticConventions.fsac_sourceCodePath, box (UMX.untag file.Source.FileName) - SemanticConventions.projectFilePath, box (options.ProjectFileName) ] - - use _ = fsacActivitySource.StartActivityForType(thisType, tags = tags) - - - logger.info ( - Log.setMessage "Getting typecheck results for {file} - {hash} - {date}" - >> Log.addContextDestructured "file" file.Source.FileName - >> Log.addContextDestructured "hash" (file.Source.GetHashCode()) - >> Log.addContextDestructured "date" (file.LastTouched) - ) - - let! ct = Async.CancellationToken - - use progressReport = new ServerProgressReport(lspClient) - - let simpleName = Path.GetFileName(UMX.untag file.Source.FileName) - do! progressReport.Begin($"Typechecking {simpleName}", message = $"{file.Source.FileName}") - - let! result = - checker.ParseAndCheckFileInProject( - file.Source.FileName, - (file.Source.GetHashCode()), - file.Source, - options, - shouldCache = shouldCache, - ?snapshotAccumulator = snapshotCache - ) - |> Debug.measureAsync $"checker.ParseAndCheckFileInProject - {file.Source.FileName}" + fun ctok -> + task { + // use upperTimeout = new CancellationTokenSource() + // upperTimeout.CancelAfter(TimeSpan.FromSeconds(60.)) - do! progressReport.End($"Typechecked {file.Source.FileName}") + // use upperlimitCt = CancellationTokenSource.CreateLinkedTokenSource(ctok, upperTimeout.Token) + // let ctok = upperlimitCt.Token + // use! _lock = getCheckLock(file.FileName).LockAsync(ctok) + let tags = + [ SemanticConventions.fsac_sourceCodePath, box (UMX.untag file.Source.FileName) + SemanticConventions.projectFilePath, box (options.ProjectFileName) ] - notifications.Trigger(NotificationEvent.FileParsed(file.Source.FileName), ct) - - match result with - | Error e -> - logger.error ( - Log.setMessage "Typecheck failed for {file} with {error}" - >> Log.addContextDestructured "file" file.FileName - >> Log.addContextDestructured "error" e - ) + use _ = fsacActivitySource.StartActivityForType(thisType, tags = tags) - return Error e - | Ok parseAndCheck -> logger.info ( - Log.setMessage "Typecheck completed successfully for {file}" + Log.setMessage "Getting typecheck results for {file} - {version} - {date} - {project} - {stamp}" >> Log.addContextDestructured "file" file.Source.FileName + >> Log.addContextDestructured "version" (file.Version) + >> Log.addContextDestructured "date" (file.LastTouched) + >> Log.addContextDestructured "project" options.ProjectFileName + >> Log.addContextDestructured "stamp" options.Stamp ) - Async.Start( - async { + use progressReport = new ServerProgressReport(lspClient) + try + let simpleName = Path.GetFileName(UMX.untag file.Source.FileName) + do! progressReport.Begin($"Typechecking {simpleName}", message = $"{file.Source.FileName}") CancellationToken.None + + let! result = Async.StartAsTask( + checker.ParseAndCheckFileInProject( + file.Source.FileName, + file.Version, + file.Source, + options, + shouldCache = shouldCache, + ?snapshotAccumulator = snapshotCache + ) + ,cancellationToken = ctok) + // |> Debug.measureAsync $"checker.ParseAndCheckFileInProject - {file.Source.FileName}" + + // do! progressReport.End($"Typechecked {file.Source.FileName}") ctok + + notifications.Trigger(NotificationEvent.FileParsed(file.Source.FileName), ctok) + + match result with + | Error e -> + logger.error ( + Log.setMessage "Typecheck failed for {file} with {error}" + >> Log.addContextDestructured "file" file.FileName + >> Log.addContextDestructured "error" e + ) + + return Error e + | Ok parseAndCheck -> + logger.info ( + Log.setMessage "Typecheck completed successfully for {file}" + >> Log.addContextDestructured "file" file.Source.FileName + ) - // fileParsed.Trigger(parseAndCheck.GetParseResults, options.To, ct) - fileChecked.Trigger(parseAndCheck, file, ct) - let checkErrors = parseAndCheck.GetParseResults.Diagnostics - let parseErrors = parseAndCheck.GetCheckResults.Diagnostics + Async.Start( + async { - let errors = - Array.append checkErrors parseErrors - |> Array.distinctBy (fun e -> - e.Severity, e.ErrorNumber, e.StartLine, e.StartColumn, e.EndLine, e.EndColumn, e.Message) + // fileParsed.Trigger(parseAndCheck.GetParseResults, options.To, ct) + fileChecked.Trigger(parseAndCheck, file, ctok) + let checkErrors = parseAndCheck.GetParseResults.Diagnostics + let parseErrors = parseAndCheck.GetCheckResults.Diagnostics - notifications.Trigger(NotificationEvent.ParseError(errors, file.Source.FileName, file.Version), ct) - }, - ct - ) + let errors = + Array.append checkErrors parseErrors + |> Array.distinctBy (fun e -> + e.Severity, e.ErrorNumber, e.StartLine, e.StartColumn, e.EndLine, e.EndColumn, e.Message) + notifications.Trigger(NotificationEvent.ParseError(errors, file.Source.FileName, file.Version), ctok) + }, + ctok + ) - return Ok parseAndCheck - } + + return Ok parseAndCheck + finally + progressReport |> dispose + } /// Bypass Adaptive checking and tell the checker to check a file let bypassAdaptiveTypeCheck (filePath: string) opts snapshotCache = @@ -1501,8 +1629,9 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let checker = checker |> AVal.force let! fileInfo = forceFindOpenFileOrRead filePath + let! ct = Async.CancellationToken // Don't cache for autocompletions as we really only want to cache "Opened" files. - return! parseAndCheckFile checker fileInfo opts false snapshotCache + return! parseAndCheckFile checker fileInfo opts false snapshotCache ct with e -> @@ -1542,7 +1671,6 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let file = info.FileName let! checker = checker and! selectProject = projectSelector - and! documentSource = documentSource return! asyncResult { @@ -1550,12 +1678,8 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let! opts = selectProject.FindProject(file, projectOptions) let cts = getOpenFileTokenOrDefault file use linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ctok, cts) - let opts = opts.FSharpProjectOptions - let! sn = checker.FromOption(opts, documentSource) - return! - parseAndCheckFile checker info sn true None - |> Async.withCancellation linkedCts.Token + parseAndCheckFile checker info opts true None linkedCts.Token } }) @@ -1616,10 +1740,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac | Error _ -> match! forceGetFSharpProjectOptions file with | Ok opts -> - let checker = checker |> AVal.force - let documentSource = documentSource |> AVal.force - let! sn = checker.FromOption(opts, documentSource) - return! bypassAdaptiveTypeCheck file sn None + return! bypassAdaptiveTypeCheck file opts None | Error e -> return Error e } @@ -1743,11 +1864,11 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac member x.ParseFileInProject(file) = forceGetParseResults file |> Async.map (Option.ofResult) } - let getDependentProjectsOfProjects ps = + let getDependentProjectsOfProjects (ps : FSharpProjectSnapshot list) = asyncEx { - let! projectSnapshot = forceLoadProjects () + let projectSnapshot = forceLoadProjects () - let allDependents = System.Collections.Generic.HashSet() + let allDependents = System.Collections.Generic.HashSet<_>() let currentPass = ResizeArray() currentPass.AddRange(ps |> List.map (fun p -> p.ProjectFileName)) @@ -2098,10 +2219,10 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac Log.setMessage "Source Files: {sourceFiles}" >> Log.addContextDestructured "sourceFiles" proj.SourceFiles ) + let sourceFiles = proj.SourceFiles + let idx = sourceFiles |> Array.findIndex (fun x -> x = UMX.untag file) - let idx = proj.SourceFiles |> Array.findIndex (fun x -> x = UMX.untag file) - - proj.SourceFiles + sourceFiles |> Array.splitAt idx |> snd |> Array.map (fun sourceFile -> proj.FSharpProjectOptions, sourceFile)) @@ -2114,7 +2235,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac // let snapshotCache = System.Collections.Generic.Dictionary<_, _>() let tags = [ SemanticConventions.fsac_sourceCodePath, box (UMX.untag filePath) ] use _ = fsacActivitySource.StartActivityForType(thisType, tags = tags) - let! _dependentFiles = getDependentFilesForFile filePath + let! dependentFiles = getDependentFilesForFile filePath let! projs = getProjectOptionsForFile filePath |> AsyncAVal.forceAsync @@ -2126,14 +2247,15 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let! dependentProjects = projs |> getDependentProjectsOfProjects - let checker = checker |> AVal.force + // let checker = checker |> AVal.force - let docSource = documentSource |> AVal.force - let! optsAndSnaps = checker.FromOptions(Array.ofList (List.append projs dependentProjects), docSource) + // let docSource = documentSource |> AVal.force + // let! optsAndSnaps = checker.FromOptions(Array.ofList (List.append projs dependentProjects), docSource) let dependentProjectsAndSourceFiles = - optsAndSnaps - |> Array.collect (fun (proj, snap) -> proj.SourceFiles |> Array.map (fun sourceFile -> snap, proj, sourceFile)) + dependentProjects + |> List.collect (fun (snap) -> snap.SourceFiles |> List.map (fun sourceFile -> snap, sourceFile.FileName)) + |> List.toArray let mutable checksCompleted = 0 @@ -2147,15 +2269,20 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let checksToPerform = let innerChecks = - Array.concat [| dependentProjectsAndSourceFiles |] - |> Array.filter (fun (_, _, file) -> + Array.concat [| dependentFiles; dependentProjectsAndSourceFiles |] + |> Array.filter (fun (_, file) -> file.Contains "AssemblyInfo.fs" |> not && file.Contains "AssemblyAttributes.fs" |> not) + // innerChecks + // |> Array.map fst + // |> Array.distinctBy (fun snap -> snap.ProjectFileName) + // |> checker.ClearCache + let checksToPerformLength = innerChecks.Length innerChecks - |> Array.map (fun (snap, _, file) -> + |> Array.map (fun (snap, file) -> let file = UMX.tag file let token = getOpenFileTokenOrDefault filePath @@ -2288,10 +2415,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac member x.GetTypeCheckResultsForFile(filePath) = asyncResult { let! opts = forceGetProjectOptions filePath - let checker = checker |> AVal.force - let documentSource = documentSource |> AVal.force - let! sn = checker.FromOption(opts, documentSource) - return! x.GetTypeCheckResultsForFile(filePath, sn) + return! x.GetTypeCheckResultsForFile(filePath, opts) } member x.GetFilesToProject() = getAllFilesToProjectOptionsSelected () diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi b/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi index 28d75ed52..e253a7046 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi @@ -30,7 +30,7 @@ type AdaptiveWorkspaceChosen = [] type LoadedProject = - { FSharpProjectOptions: FSharpProjectOptions + { FSharpProjectOptions: FSharpProjectSnapshot LanguageVersion: LanguageVersionShim } interface IEquatable @@ -38,7 +38,7 @@ type LoadedProject = override Equals: other: obj -> bool member SourceFiles: string array member ProjectFileName: string - static member op_Implicit: x: LoadedProject -> FSharpProjectOptions + static member op_Implicit: x: LoadedProject -> FSharpProjectSnapshot type AdaptiveState = new: @@ -51,7 +51,7 @@ type AdaptiveState = member ClientCapabilities: ClientCapabilities option with get, set member WorkspacePaths: WorkspaceChosen with get, set member DiagnosticCollections: DiagnosticCollection - member ScriptFileProjectOptions: Event + member ScriptFileProjectOptions: Event member OpenDocument: filePath: string * text: string * version: int -> CancellableTask @@ -64,7 +64,7 @@ type AdaptiveState = member GetParseResults: filePath: string -> Async> member GetOpenFileTypeCheckResults: file: string -> Async> member GetOpenFileTypeCheckResultsCached: filePath: string -> Async> - member GetProjectOptionsForFile: filePath: string -> Async> + member GetProjectOptionsForFile: filePath: string -> Async> member GetTypeCheckResultsForFile: filePath: string * opts: FSharpProjectSnapshot -> Async> @@ -74,7 +74,7 @@ type AdaptiveState = member GetUsesOfSymbol: filePath: string * - opts: (string * FSharpProjectOptions) seq * + opts: (string * FSharpProjectSnapshot) seq * symbol: FSharp.Compiler.Symbols.FSharpSymbol -> Async diff --git a/src/FsAutoComplete/LspServers/FSharpLspClient.fs b/src/FsAutoComplete/LspServers/FSharpLspClient.fs index a2a7ad1b9..fa4a4710b 100644 --- a/src/FsAutoComplete/LspServers/FSharpLspClient.fs +++ b/src/FsAutoComplete/LspServers/FSharpLspClient.fs @@ -149,16 +149,19 @@ open System.Diagnostics.Tracing open System.Collections.Concurrent open System.Diagnostics open Ionide.ProjInfo.Logging +open System.Text.RegularExpressions /// listener for the the events generated from the fsc ActivitySource type ProgressListener(lspClient: FSharpLspClient, traceNamespace: string array) = + let traceNamespace = traceNamespace |> Array.map(fun x -> Regex(x, RegexOptions.Compiled)) + let isOneOf list string = list |> Array.exists (fun f -> f string) let strEquals (other: string) (this: string) = this.Equals(other, StringComparison.InvariantCultureIgnoreCase) - let strContains (substring: string) (str: string) = str.Contains(substring) + let strContains (substring: Regex) (str: string) = substring.IsMatch str let interestingActivities = traceNamespace |> Array.map strContains diff --git a/src/FsAutoComplete/LspServers/ProjectWorkspace.fs b/src/FsAutoComplete/LspServers/ProjectWorkspace.fs new file mode 100644 index 000000000..10f1669ac --- /dev/null +++ b/src/FsAutoComplete/LspServers/ProjectWorkspace.fs @@ -0,0 +1,330 @@ +namespace FsAutoComplete.ProjectWorkspace + + + +module AMap = + open FSharp.Data.Adaptive + let rec findAllDependenciesOfAndIncluding key findNextKeys items = + amap { + // printfn "findAllDependenciesOfAndIncluding %A" key + let! item = AMap.tryFind key items + match item with + | None -> + () + | Some item -> + yield key, item + let! dependencies = + item + |> AVal.map( + findNextKeys + >> Seq.map (fun newKey -> findAllDependenciesOfAndIncluding newKey findNextKeys items) + >> Seq.fold(fun s v -> AMap.union v s) AMap.empty + ) + yield! dependencies + } + + let findAllDependenciesOfAndIncluding2 key (findNextKeys : _ -> seq<_>) items = + let rec inner key = + aset { + // printfn "findAllDependenciesOfAndIncluding %A" key + let! item = AMap.tryFind key items + match item with + | None -> + () + | Some item -> + yield key, item + let! itemV = item + for newKey in findNextKeys itemV do + yield! inner newKey + } + inner key + |> AMap.ofASet + |> AMap.choose' Seq.tryHead + + + let rec findAllDependentsOfAndIncluding key findNextKeys items = + amap { + // printfn "findAllDependentsOfAndIncluding %A" key + let immediateDependents = + items + |> AMap.filterA (fun _ v -> v |> AVal.map (findNextKeys >> Seq.exists ((=) key))) + yield! immediateDependents + let! dependentOfDependents = + immediateDependents + |> AMap.map (fun nextKey _ -> findAllDependentsOfAndIncluding nextKey findNextKeys items) + |> AMap.fold(fun acc _ x -> AMap.union acc x) AMap.empty + yield! dependentOfDependents + } + + +module Snapshots = + open System + open FsAutoComplete + open System.Threading + open FSharp.UMX + open System.Threading.Tasks + open Ionide.ProjInfo.Types + open FSharp.Compiler.CodeAnalysis.ProjectSnapshot + open System.IO + open FSharp.Compiler.CodeAnalysis + open FSharp.Data.Adaptive + open FSharp.Compiler.Text + open FsAutoComplete.Adaptive + open Ionide.ProjInfo.Logging + open System.Collections.Generic + + let rec logger = LogProvider.getLoggerByQuotation <@ logger @> + + let private loadFromDotnetDll (p: ProjectOptions) : FSharpReferencedProjectSnapshot = + /// because only a successful compilation will be written to a DLL, we can rely on + /// the file metadata for things like write times + let projectFile = FileInfo p.TargetPath + + let getStamp () = + projectFile.Refresh () + projectFile.LastWriteTimeUtc + + let getStream (_ctok: System.Threading.CancellationToken) = + try + projectFile.OpenRead() :> Stream |> Some + with _ -> + None + + let delayedReader = DelayedILModuleReader(p.TargetPath, getStream) + + ProjectSnapshot.FSharpReferencedProjectSnapshot.PEReference(getStamp, delayedReader) + + let makeAdaptiveFCSSnapshot + projectFileName + projectId + sourceFiles + referencePaths + otherOptions + referencedProjects + isIncompleteTypeCheckEnvironment + useScriptResolutionRules + loadTime + unresolvedReferences + originalLoadReferences + = + aval { + let! projectFileName = projectFileName + and! projectId = projectId + and! sourceFiles = sourceFiles + and! referencePaths = referencePaths + and! otherOptions = otherOptions + and! referencedProjects = referencedProjects + and! isIncompleteTypeCheckEnvironment = isIncompleteTypeCheckEnvironment + and! useScriptResolutionRules = useScriptResolutionRules + and! loadTime = loadTime + and! unresolvedReferences = unresolvedReferences + and! originalLoadReferences = originalLoadReferences + let stamp = DateTime.UtcNow.Ticks + logger.info( + Log.setMessage "Creating FCS snapshot {projectFileName} {stamp}" + >> Log.addContextDestructured "projectFileName" projectFileName + >> Log.addContextDestructured "stamp" stamp + ) + + // printfn "Snapshot %A" projectFileName + return FSharpProjectSnapshot.Create( + projectFileName, + projectId, + sourceFiles, + referencePaths, + otherOptions, + referencedProjects , + isIncompleteTypeCheckEnvironment, + useScriptResolutionRules, + loadTime, + unresolvedReferences, + originalLoadReferences, + Some stamp + ) + } + + let makeAdaptiveFCSSnapshot2 + projectFileName + projectId + (sourceFiles: alist>) + (referencePaths: aset>) + (otherOptions: aset>) + (referencedProjects: aset>) + isIncompleteTypeCheckEnvironment + useScriptResolutionRules + loadTime + unresolvedReferences + originalLoadReferences + = + let flattenASet (s: aset>) = s |> ASet.mapA id |> ASet.toAVal |> AVal.map HashSet.toList + let flattenAList (s: alist>) = s |> AList.mapA id |> AList.toAVal |> AVal.map IndexList.toList + makeAdaptiveFCSSnapshot + projectFileName + projectId + (flattenAList sourceFiles) + (flattenASet referencePaths) + (flattenASet otherOptions) + (flattenASet referencedProjects) + isIncompleteTypeCheckEnvironment + useScriptResolutionRules + loadTime + unresolvedReferences + originalLoadReferences + + + let private createFSharpFileSnapshotOnDisk (sourceTextFactory : aval) fileName = + aval { + let! writeTime = AdaptiveFile.GetLastWriteTimeUtc fileName + and! sourceTextFactory = sourceTextFactory + let getSource () = task { + + let file = Utils.normalizePath fileName + // use s = File.openFileStreamForReadingAsync file + // let! source = sourceTextFactory.Create(file, s) CancellationToken.None + let! text = File.ReadAllTextAsync fileName + let source = sourceTextFactory.Create(file, text) + return source :> ISourceTextNew + } + // printfn "Creating source text for %s" fileName + return ProjectSnapshot.FSharpFileSnapshot.Create(fileName, string writeTime.Ticks, getSource) + } + let private createFSharpFileSnapshotInMemory (v : VolatileFile) = + let file = UMX.untag v.FileName + let version = v.LastTouched.Ticks + let getSource () = + v.Source + :> ISourceTextNew + |> Task.FromResult + ProjectSnapshot.FSharpFileSnapshot.Create(file, string version, getSource) + + let private createReferenceOnDisk path : aval = + aval { + let! lastModified = AdaptiveFile.GetLastWriteTimeUtc path + return { LastModified = lastModified; Path = path } + } + + let private createReferencedProjectsFSharpReference projectOutputFile (snapshot: aval) = + aval { + let! projectOutputFile = projectOutputFile + and! snapshot = snapshot + return FSharpReferencedProjectSnapshot.FSharpReference(projectOutputFile, snapshot) + } + + let rec private createReferences + (cachedSnapshots) + (inMemorySourceFiles : amap, aval>) + (sourceTextFactory: aval) + (loadedProjectsA: amap,ProjectOptions>) + (p : ProjectOptions) = + logger.info( + Log.setMessage "Creating references for {projectFileName}" + >> Log.addContextDestructured "projectFileName" p.ProjectFileName + ) + let normPath = Utils.normalizePath p.ProjectFileName + // let deps = + // loadedProjectsA + // |> AMap.findAllDependenciesOfAndIncluding2 + // normPath + // (fun p -> p.ReferencedProjects |> Seq.map(_.ProjectFileName >> Utils.normalizePath)) + let deps = + loadedProjectsA + |> AMap.filter(fun k _ -> p.ReferencedProjects |> List.exists(fun x -> x.ProjectFileName = UMX.untag k)) + deps + |> AMap.filter(fun k _ -> k <> normPath) + |> AMap.map(fun _ p -> aval { + if p.ProjectFileName.EndsWith ".fsproj" then + let snapshot = optionsToSnapshot cachedSnapshots inMemorySourceFiles sourceTextFactory (createReferences cachedSnapshots inMemorySourceFiles sourceTextFactory loadedProjectsA) p + return! createReferencedProjectsFSharpReference (AVal.constant p.ResolvedTargetPath) snapshot + else + // TODO: Find if this needs to be adaptive or if `getStamp` in a PEReference will be enough to break thru the caching in FCS + return loadFromDotnetDll p + }) + |> AMap.toASetValues + + and optionsToSnapshot + (cachedSnapshots : Dictionary<_,_>) + (inMemorySourceFiles : amap<_, aval>) + (sourceTextFactory: aval) + (mapReferences: ProjectOptions -> aset>) + (p : ProjectOptions) = + + // printfn "optionsToSnapshot - enter %A" p.ProjectFileName + aval { + let normPath = Utils.normalizePath p.ProjectFileName + match cachedSnapshots.TryGetValue normPath with + | (true, x) -> + logger.info( + Log.setMessage "optionsToSnapshot - Cache hit - {projectFileName}" + >> Log.addContextDestructured "projectFileName" p.ProjectFileName + ) + // printfn "optionsToSnapshot - Cache hit %A" p.ProjectFileName + return! x + | _ -> + logger.info( + Log.setMessage "optionsToSnapshot - Cache miss - {projectFileName}" + >> Log.addContextDestructured "projectFileName" p.ProjectFileName + ) + // printfn "optionsToSnapshot - Cache miss %A" p.ProjectFileName + let projectName = p.ProjectFileName + let projectId = p.ProjectId |> AVal.constant + + let sourceFiles = + p.SourceFiles + |> AList.ofList + |> AList.map(fun sourcePath -> + let normPath = Utils.normalizePath sourcePath + aval { + match! inMemorySourceFiles |> AMap.tryFind normPath with + | Some volatileFile -> + return! volatileFile |> AVal.map createFSharpFileSnapshotInMemory + | None -> return! createFSharpFileSnapshotOnDisk sourceTextFactory sourcePath + } + + ) + + let references, otherOptions = p.OtherOptions |> List.partition (fun x -> x.StartsWith("-r:")) + let otherOptions = otherOptions |> ASet.ofList |> ASet.map(AVal.constant) + let referencePaths = + references + |> ASet.ofList + |> ASet.map(fun referencePath -> + referencePath.Substring(3) // remove "-r:" + |> createReferenceOnDisk + ) + let referencedProjects = mapReferences p + let isIncompleteTypeCheckEnvironment = AVal.constant false + let useScriptResolutionRules = AVal.constant false + let loadTime = AVal.constant p.LoadTime + let unresolvedReferences = AVal.constant None + let originalLoadReferences = AVal.constant [] + + let snap = + makeAdaptiveFCSSnapshot2 + (AVal.constant projectName) + projectId + sourceFiles + referencePaths + otherOptions + referencedProjects + isIncompleteTypeCheckEnvironment + useScriptResolutionRules + loadTime + unresolvedReferences + originalLoadReferences + + cachedSnapshots.Add(normPath, snap) + + return! snap + } + + let createSnapshots + (inMemorySourceFiles: amap,aval>) + (sourceTextFactory: aval) + (loadedProjectsA: amap,ProjectOptions>) = + let cachedSnapshots = Dictionary<_,_>() + let mapReferences = createReferences cachedSnapshots inMemorySourceFiles sourceTextFactory loadedProjectsA + let optionsToSnapshot = optionsToSnapshot cachedSnapshots inMemorySourceFiles sourceTextFactory mapReferences + + loadedProjectsA + |> AMap.filter(fun k _ -> (UMX.untag k).EndsWith ".fsproj") + |> AMap.map (fun _ v -> v, optionsToSnapshot v ) diff --git a/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs index 6fcc5a409..5b6e1f291 100644 --- a/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs @@ -4,7 +4,8 @@ open System.IO open Ionide.ProjInfo open FsAutoComplete.Utils open FSharp.Data.Adaptive -open FsAutoComplete.Adaptive +open FsAutoComplete.Adaptive +open FsAutoComplete.ProjectWorkspace open System open FSharp.Compiler.Text open Ionide.ProjInfo.ProjectLoader @@ -12,43 +13,13 @@ open Ionide.ProjInfo.Types open FSharp.Compiler.CodeAnalysis.ProjectSnapshot open FSharp.Compiler.CodeAnalysis open IcedTasks +open FSharp.UMX module FcsRange = FSharp.Compiler.Text.Range type FcsRange = FSharp.Compiler.Text.Range type FcsPos = FSharp.Compiler.Text.Position -let rec findAllDependenciesOfAndIncluding key findNextKeys items = - amap { - // printfn "findAllDependenciesOfAndIncluding %A" key - let! item = AMap.tryFind key items - match item with - | None -> - () - | Some item -> - yield key, item - let! dependencies = - item - |> AVal.map( - findNextKeys - >> Seq.map (fun newKey -> findAllDependenciesOfAndIncluding newKey findNextKeys items) - >> Seq.fold(AMap.union) AMap.empty - ) - yield! dependencies - } - -let rec findAllDependentsOfAndIncluding key findNextKeys items = - amap { - // printfn "findAllDependentsOfAndIncluding %A" key - let immediateDependents = - items - |> AMap.filterA (fun _ v -> v |> AVal.map (findNextKeys >> Seq.exists ((=) key))) - yield! immediateDependents - let! dependentOfDependents = - immediateDependents - |> AMap.map (fun nextKey _ -> findAllDependentsOfAndIncluding nextKey findNextKeys items) - |> AMap.fold(fun acc _ x -> AMap.union acc x) AMap.empty - yield! dependentOfDependents - } + module Dotnet = let restore (projectPath : FileInfo) = async { @@ -119,7 +90,7 @@ let createProjectA (projects : FileInfo seq) (loader : IWorkspaceLoader) onLoadC let loaded = projects |> List.map (fun p -> p.FullName) |> loader.LoadProjects |> Seq.cache onLoadCallback () loaded - |> Seq.map(fun l -> l.ProjectFileName, AVal.constant l) + |> Seq.map(fun l -> normalizePath l.ProjectFileName, l) // projects // |> List.map(fun p -> p.FullName, AVal.constant(p, loaded |> Seq.find(fun l -> l.ProjectFileName = p.FullName))) ) @@ -129,220 +100,6 @@ let createProjectA (projects : FileInfo seq) (loader : IWorkspaceLoader) onLoadC -module Snapshots = - open FsAutoComplete - open System.Threading - - let loadFromDotnetDll (p: Types.ProjectOptions) : ProjectSnapshot.FSharpReferencedProjectSnapshot = - /// because only a successful compilation will be written to a DLL, we can rely on - /// the file metadata for things like write times - let projectFile = FileInfo p.TargetPath - - let getStamp () = - projectFile.Refresh () - projectFile.LastWriteTimeUtc - - let getStream (_ctok: System.Threading.CancellationToken) = - try - projectFile.OpenRead() :> Stream |> Some - with _ -> - None - - let delayedReader = DelayedILModuleReader(p.TargetPath, getStream) - - ProjectSnapshot.FSharpReferencedProjectSnapshot.PEReference(getStamp, delayedReader) - - let makeAdaptiveFCSSnapshot - projectFileName - projectId - sourceFiles - referencePaths - otherOptions - referencedProjects - isIncompleteTypeCheckEnvironment - useScriptResolutionRules - loadTime - unresolvedReferences - originalLoadReferences - = - aval { - let! projectFileName = projectFileName - and! projectId = projectId - and! sourceFiles = sourceFiles - and! referencePaths = referencePaths - and! otherOptions = otherOptions - and! referencedProjects = referencedProjects - and! isIncompleteTypeCheckEnvironment = isIncompleteTypeCheckEnvironment - and! useScriptResolutionRules = useScriptResolutionRules - and! loadTime = loadTime - and! unresolvedReferences = unresolvedReferences - and! originalLoadReferences = originalLoadReferences - - // printfn "Snapshot %A" projectFileName - let snap = FSharpProjectSnapshot.Create( - projectFileName, - projectId, - sourceFiles, - referencePaths, - otherOptions, - referencedProjects , - isIncompleteTypeCheckEnvironment, - useScriptResolutionRules, - loadTime, - unresolvedReferences, - originalLoadReferences, - Some (DateTime.UtcNow.Ticks) - ) - - return snap - } - - let makeAdaptiveFCSSnapshot2 - projectFileName - projectId - (sourceFiles: aset>) - (referencePaths: aset>) - (otherOptions: aset>) - (referencedProjects: aset>) - isIncompleteTypeCheckEnvironment - useScriptResolutionRules - loadTime - unresolvedReferences - originalLoadReferences - = - let flattenASet (s: aset>) = s |> ASet.mapA id |> ASet.toAVal |> AVal.map HashSet.toList - makeAdaptiveFCSSnapshot - projectFileName - projectId - (flattenASet sourceFiles) - (flattenASet referencePaths) - (flattenASet otherOptions) - (flattenASet referencedProjects) - isIncompleteTypeCheckEnvironment - useScriptResolutionRules - loadTime - unresolvedReferences - originalLoadReferences - - - let private createFSharpFileSnapshotOnDisk (sourceTextFactory : aval) fileName = - aval { - let! writeTime = AdaptiveFile.GetLastWriteTimeUtc fileName - and! sourceTextFactory = sourceTextFactory - let getSource () = task { - // let! text = File.ReadAllTextAsync fileName - let fileName = normalizePath fileName - use s = File.openFileStreamForReadingAsync fileName - - let! source = sourceTextFactory.Create(fileName, s) CancellationToken.None - return source :> ISourceTextNew - } - // printfn "Creating source text for %s" fileName - return ProjectSnapshot.FSharpFileSnapshot.Create(fileName, string writeTime.Ticks, getSource) - } - - let private createReferenceOnDisk path : aval = - aval { - let! lastModified = AdaptiveFile.GetLastWriteTimeUtc path - return { LastModified = lastModified; Path = path } - } - - let private createReferencedProjectsFSharpReference projectOutputFile (snapshot: aval) = - aval { - let! projectOutputFile = projectOutputFile - and! snapshot = snapshot - return FSharpReferencedProjectSnapshot.FSharpReference(projectOutputFile, snapshot) - } - - let rec private createReferences - (cachedSnapshots: ChangeableHashMap>) - (sourceTextFactory: aval) - (loadedProjectsA: amap>) - (p : ProjectOptions) = - let deps = - loadedProjectsA - |> findAllDependenciesOfAndIncluding - p.ProjectFileName - (fun p -> p.ReferencedProjects |> List.map(_.ProjectFileName)) - deps - |> AMap.filter(fun k _ -> k <> p.ProjectFileName) - |> AMap.mapAVal(fun _ p -> aval { - if p.ProjectFileName.EndsWith ".fsproj" then - let snapshot = optionsToSnapshot cachedSnapshots sourceTextFactory (createReferences cachedSnapshots sourceTextFactory deps) p - return! createReferencedProjectsFSharpReference (AVal.constant p.ResolvedTargetPath) snapshot - else - // TODO: Find if this needs to be adaptive or if `getStamp` in a PEReference will be enough - return loadFromDotnetDll p - }) - |> AMap.toASetValues - - and optionsToSnapshot - (cachedSnapshots : ChangeableHashMap<_,_>) - (sourceTextFactory: aval) - (mapReferences: ProjectOptions -> aset>) - (p : ProjectOptions) = - - // printfn "optionsToSnapshot - enter %A" p.ProjectFileName - aval { - match! cachedSnapshots |> AMap.tryFind p.ProjectFileName with - | Some x -> - // printfn "optionsToSnapshot - Cache hit %A" p.ProjectFileName - return! x - | None -> - // printfn "optionsToSnapshot - Cache miss %A" p.ProjectFileName - let projectName = p.ProjectFileName - let projectId = p.ProjectId |> AVal.constant - - let sourceFiles = - // TODO Make it use "open files" and file system files - p.SourceFiles - |> ASet.ofList - |> ASet.map(createFSharpFileSnapshotOnDisk sourceTextFactory) - - let references, otherOptions = p.OtherOptions |> List.partition (fun x -> x.StartsWith("-r:")) - let otherOptions = otherOptions |> ASet.ofList |> ASet.map(AVal.constant) - let referencePaths = - references - |> ASet.ofList - |> ASet.map(fun referencePath -> - let path = referencePath.Substring(3) // remove "-r:" - createReferenceOnDisk path - ) - let referencedProjects = mapReferences p - let isIncompleteTypeCheckEnvironment = AVal.constant false - let useScriptResolutionRules = AVal.constant false - let loadTime = AVal.constant p.LoadTime - let unresolvedReferences = AVal.constant None - let originalLoadReferences = AVal.constant [] - - let snap = - makeAdaptiveFCSSnapshot2 - (AVal.constant projectName) - projectId - sourceFiles - referencePaths - otherOptions - referencedProjects - isIncompleteTypeCheckEnvironment - useScriptResolutionRules - loadTime - unresolvedReferences - originalLoadReferences - - transact <| fun () -> - cachedSnapshots.Add(projectName, snap) |> ignore<_> - - return! snap - } - - let createSnapshot - (cachedSnapshots: ChangeableHashMap>) - (sourceTextFactory: aval) - (loadedProjectsA: amap>) - (projectOptions: ProjectOptions) = - let mapReferences = createReferences cachedSnapshots sourceTextFactory loadedProjectsA - optionsToSnapshot cachedSnapshots sourceTextFactory mapReferences projectOptions - let snapshotTests loaders toolsPath = @@ -403,15 +160,14 @@ let snapshotTests loaders toolsPath = let snaps = - let cache = ChangeableHashMap() - loadedProjectsA |> AMap.mapAVal (fun _ v -> Snapshots.createSnapshot cache (AVal.constant sourceTextFactory) loadedProjectsA v) + Snapshots.createSnapshots AMap.empty (AVal.constant sourceTextFactory) loadedProjectsA let snapshots = snaps |> AMap.force - let (project, snapshotA) = snapshots |> Seq.head + let (project, (_, snapshotA)) = snapshots |> Seq.head let snapshot = snapshotA |> AVal.force Expect.equal 1 loadedCalls "Loaded Projects should only get called 1 time" - Expect.equal snapshot.ProjectFileName project "Snapshot should have the same project file name as the project" + Expect.equal snapshot.ProjectFileName (UMX.untag project) "Snapshot should have the same project file name as the project" Expect.equal (Seq.length snapshot.SourceFiles) 3 "Snapshot should have the same number of source files as the project" Expect.equal (Seq.length snapshot.ReferencedProjects) 0 "Snapshot should have the same number of referenced projects as the project" } @@ -428,23 +184,20 @@ let snapshotTests loaders toolsPath = let loadedProjectsA = createProjectA projects loader (fun () -> loadedCalls <- loadedCalls + 1) - let cache = ChangeableHashMap() - let snapsA = - loadedProjectsA - |> AMap.mapAVal (fun _ v -> Snapshots.createSnapshot cache (AVal.constant sourceTextFactory) loadedProjectsA v) + Snapshots.createSnapshots AMap.empty (AVal.constant sourceTextFactory) loadedProjectsA - let snapshots = snapsA |> AMap.mapA (fun _ v -> v) |> AMap.force + let snapshots = snapsA |> AMap.mapA (fun _ (_,v) -> v) |> AMap.force - let snapshots2 = snapsA |> AMap.mapA (fun _ v -> v) |> AMap.force + let snapshots2 = snapsA |> AMap.mapA (fun _ (_,v) -> v) |> AMap.force - let ls1 = snapshots |> HashMap.find ((Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) - let ls2 = snapshots2 |> HashMap.find ((Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) + let ls1 = snapshots |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) + let ls2 = snapshots2 |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) Expect.equal ls1 ls2 "library should be the same" - let cs1 = snapshots |> HashMap.find ((Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) - let cs2 = snapshots2 |> HashMap.find ((Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) + let cs1 = snapshots |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) + let cs2 = snapshots2 |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) Expect.equal cs1 cs2 "console should be the same" } @@ -460,13 +213,9 @@ let snapshotTests loaders toolsPath = let loadedProjectsA = createProjectA projects loader (fun () -> loadedCalls <- loadedCalls + 1) - let cache = ChangeableHashMap() - let snapsA = - loadedProjectsA - |> AMap.mapAVal (fun _ v -> Snapshots.createSnapshot cache (AVal.constant sourceTextFactory) loadedProjectsA v) - - let snapshots = snapsA |> AMap.mapA (fun _ v -> v) |> AMap.force + Snapshots.createSnapshots AMap.empty (AVal.constant sourceTextFactory) loadedProjectsA + let snapshots = snapsA |> AMap.mapA (fun _ (_,v) -> v) |> AMap.force let libraryFile = Projects.MultiProjectScenario1.Console1.programFileIn dDir.DirectoryInfo // printfn "Setting last write time for %s %A" libraryFile.FullName libraryFile.LastWriteTime @@ -476,15 +225,15 @@ let snapshotTests loaders toolsPath = // printfn "last write time for %s %A" libraryFile.FullName libraryFile.LastWriteTime - let snapshots2 = snapsA |> AMap.mapA (fun _ v -> v) |> AMap.force + let snapshots2 = snapsA |> AMap.mapA (fun _ (_,v) -> v) |> AMap.force - let ls1 = snapshots |> HashMap.find ((Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) - let ls2 = snapshots2 |> HashMap.find ((Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) + let ls1 = snapshots |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) + let ls2 = snapshots2 |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) Expect.equal ls1 ls2 "library should be the same" - let cs1 = snapshots |> HashMap.find ((Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) - let cs2 = snapshots2 |> HashMap.find ((Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) + let cs1 = snapshots |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) + let cs2 = snapshots2 |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) Expect.equal cs1.ProjectFileName cs2.ProjectFileName "Project file name should be the same" Expect.equal cs1.ProjectId cs2.ProjectId "Project Id name should be the same" @@ -498,7 +247,6 @@ let snapshotTests loaders toolsPath = Expect.equal refLib2 ls2 "Referenced library should be the same as library snapshot" Expect.equal refLib1 refLib2 "Referenced library in both snapshots should be the same as library did not change in this test" Expect.notEqual cs1.Stamp cs2.Stamp "Stamp should not be the same" - } testCaseAsync "Cached Adaptive Snapshot - MultiProject - Updating Source file in Library recreates Library and Console snapshot" <| asyncEx { @@ -512,13 +260,11 @@ let snapshotTests loaders toolsPath = let loadedProjectsA = createProjectA projects loader (fun () -> loadedCalls <- loadedCalls + 1) - let cache = ChangeableHashMap() let snapsA = - loadedProjectsA - |> AMap.mapAVal (fun _ v -> Snapshots.createSnapshot cache (AVal.constant sourceTextFactory) loadedProjectsA v) + Snapshots.createSnapshots AMap.empty (AVal.constant sourceTextFactory) loadedProjectsA - let snapshots = snapsA |> AMap.mapA (fun _ v -> v) |> AMap.force + let snapshots = snapsA |> AMap.mapA (fun _ (_,v) -> v) |> AMap.force let libraryFile = Projects.MultiProjectScenario1.Library1.libraryFileIn dDir.DirectoryInfo // printfn "Setting last write time for %s %A" libraryFile.FullName libraryFile.LastWriteTime @@ -527,10 +273,10 @@ let snapshotTests loaders toolsPath = libraryFile.Refresh() // printfn "last write time for %s %A" libraryFile.FullName libraryFile.LastWriteTime - let snapshots2 = snapsA |> AMap.mapA (fun _ v -> v) |> AMap.force + let snapshots2 = snapsA |> AMap.mapA (fun _ (_,v) -> v) |> AMap.force - let ls1 = snapshots |> HashMap.find ((Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) - let ls2 = snapshots2 |> HashMap.find ((Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) + let ls1 = snapshots |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) + let ls2 = snapshots2 |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) Expect.notEqual ls1 ls2 "library should not be the same" Expect.equal ls1.ProjectFileName ls2.ProjectFileName "Project file name should be the same" @@ -544,8 +290,8 @@ let snapshotTests loaders toolsPath = Expect.equal ls1.ReferencedProjects.Length 0 "Referenced projects length should be 0" Expect.notEqual ls1.Stamp ls2.Stamp "Stamp should not be the same" - let cs1 = snapshots |> HashMap.find ((Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) - let cs2 = snapshots2 |> HashMap.find ((Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) + let cs1 = snapshots |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) + let cs2 = snapshots2 |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) Expect.equal cs1.ProjectFileName cs2.ProjectFileName "Project file name should be the same" Expect.equal cs1.ProjectId cs2.ProjectId "Project Id name should be the same" @@ -555,7 +301,6 @@ let snapshotTests loaders toolsPath = let cs1File = cs1.SourceFiles |> Seq.find (fun x -> x.FileName = consoleFile.FullName) let cs2File = cs2.SourceFiles |> Seq.find (fun x -> x.FileName = consoleFile.FullName) Expect.equal cs1File.Version cs2File.Version "Console source file version should be the same" - Expect.equal cs1.ReferencedProjects.Length cs2.ReferencedProjects.Length "Referenced projects length should be the same" Expect.equal cs1.ReferencedProjects.Length 1 "Referenced projects length should be 1" let refLib1 = cs1.ReferencedProjects |> Seq.tryPick (fun x -> match x with | FSharpReferencedProjectSnapshot.FSharpReference(_, x) -> Some x | _ -> None) |> Option.get From 316d3714382faa4c61dd74774bdd1e7d906906d2 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Thu, 21 Mar 2024 00:25:22 -0400 Subject: [PATCH 11/60] Make source to snapshot lookup adaptive --- src/FsAutoComplete.Core/FCSPatches.fs | 6 +- src/FsAutoComplete.Core/FCSPatches.fsi | 2 + src/FsAutoComplete.Core/Utils.fs | 3 +- .../LspServers/AdaptiveFSharpLspServer.fs | 2 +- .../LspServers/AdaptiveServerState.fs | 275 +++++++++--------- .../LspServers/AdaptiveServerState.fsi | 6 +- 6 files changed, 155 insertions(+), 139 deletions(-) diff --git a/src/FsAutoComplete.Core/FCSPatches.fs b/src/FsAutoComplete.Core/FCSPatches.fs index f6f4d5af9..0b67b32d2 100644 --- a/src/FsAutoComplete.Core/FCSPatches.fs +++ b/src/FsAutoComplete.Core/FCSPatches.fs @@ -282,7 +282,7 @@ module LanguageVersionShim = /// let defaultLanguageVersion = lazy (LanguageVersionShim("latest")) - let internal formOtherOptions (options : string seq) = + let fromOtherOptions (options : string seq) = options |> Seq.tryFind (fun x -> x.StartsWith("--langversion:", StringComparison.Ordinal)) |> Option.map (fun x -> x.Split(":")[1]) @@ -294,7 +294,7 @@ module LanguageVersionShim = /// A LanguageVersionShim from the parsed "--langversion:" or defaultLanguageVersion let fromFSharpProjectOptions (fpo: FSharpProjectOptions) = fpo.OtherOptions - |> formOtherOptions + |> fromOtherOptions /// Tries to parse out "--langversion:" from OtherOptions if it can't find it, returns defaultLanguageVersion @@ -302,4 +302,4 @@ module LanguageVersionShim = /// A LanguageVersionShim from the parsed "--langversion:" or defaultLanguageVersion let fromFSharpProjectSnapshot (fpo: FSharpProjectSnapshot) = fpo.OtherOptions - |> formOtherOptions + |> fromOtherOptions diff --git a/src/FsAutoComplete.Core/FCSPatches.fsi b/src/FsAutoComplete.Core/FCSPatches.fsi index 3eeebcdf7..0168fbc49 100644 --- a/src/FsAutoComplete.Core/FCSPatches.fsi +++ b/src/FsAutoComplete.Core/FCSPatches.fsi @@ -18,6 +18,8 @@ type LanguageVersionShim = module LanguageVersionShim = val defaultLanguageVersion: Lazy + + val fromOtherOptions: options: seq -> LanguageVersionShim /// Tries to parse out "--langversion:" from OtherOptions if it can't find it, returns defaultLanguageVersion /// The FSharpProjectOptions to use /// A LanguageVersionShim from the parsed "--langversion:" or defaultLanguageVersion diff --git a/src/FsAutoComplete.Core/Utils.fs b/src/FsAutoComplete.Core/Utils.fs index 0fad06fb8..6c5c8def2 100644 --- a/src/FsAutoComplete.Core/Utils.fs +++ b/src/FsAutoComplete.Core/Utils.fs @@ -238,7 +238,8 @@ module Async = /// A sequence of distinct computations to be parallelized. let parallel75 computations = let maxConcurrency = - Math.Max(1.0, Math.Floor((float System.Environment.ProcessorCount) * 0.75)) + 2 + // Math.Max(1.0, Math.Floor((float System.Environment.ProcessorCount) * 0.75)) Async.Parallel(computations, int maxConcurrency) diff --git a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs index ed1c9f2de..2e939211b 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs @@ -1202,7 +1202,7 @@ type AdaptiveFSharpLspServer let getAllProjects () = state.GetFilesToProject() |> Async.map ( - Array.map (fun (file, proj) -> UMX.untag file, proj.FSharpProjectOptions) + Array.map (fun (file, proj) -> UMX.untag file, AVal.force proj.FSharpProjectSnapshot) >> Array.toList ) diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index 2bd00ea9d..10dd9dc58 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -65,13 +65,15 @@ module Helpers3 = [] type LoadedProject = - { FSharpProjectOptions: FSharpProjectSnapshot + { + ProjectOptions : Types.ProjectOptions + FSharpProjectSnapshot: aval LanguageVersion: LanguageVersionShim } interface IEquatable with - member x.Equals(other) = x.FSharpProjectOptions = other.FSharpProjectOptions + member x.Equals(other) = x.FSharpProjectSnapshot = other.FSharpProjectSnapshot - override x.GetHashCode() = x.FSharpProjectOptions.GetHashCode() + override x.GetHashCode() = x.FSharpProjectSnapshot.GetHashCode() override x.Equals(other: obj) = @@ -79,9 +81,9 @@ type LoadedProject = | :? LoadedProject as other -> (x :> IEquatable<_>).Equals other | _ -> false - member x.SourceFiles = x.FSharpProjectOptions.SourceFiles |> List.map(fun f -> f.FileName) |> List.toArray - member x.ProjectFileName = x.FSharpProjectOptions.ProjectFileName - static member op_Implicit(x: LoadedProject) = x.FSharpProjectOptions + member x.SourceFiles = x.ProjectOptions.SourceFiles |> List.map(fun f -> f) |> List.toArray + member x.ProjectFileName = x.ProjectOptions.ProjectFileName + // static member op_Implicit(x: LoadedProject) = x.FSharpProjectSnapshot @@ -1043,23 +1045,16 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let loadedProjectSnapshots2 = snapshots |> AMap.map(fun _ (proj, snap) -> - - - proj, - aval { - let! snap = snap - // and! _checker = checker - // checker.ClearCache([snap]) - let langversion = LanguageVersionShim.fromFSharpProjectSnapshot snap - return - { FSharpProjectOptions = snap - LanguageVersion = langversion } + { + ProjectOptions = proj + FSharpProjectSnapshot = snap + LanguageVersion = LanguageVersionShim.fromOtherOptions proj.OtherOptions } ) let loadedProjectSnapshots = loadedProjectSnapshots2 - |> AMap.mapA (fun _ (_,v) -> v) + |> AMap.mapA (fun _ v -> v.FSharpProjectSnapshot |> AVal.map(fun _ -> v)) |> AMap.toAVal |> AVal.map HashMap.toValueList @@ -1155,41 +1150,42 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac /// A list of FSharpProjectOptions let forceLoadProjects () = loadedProjectSnapshots |> AVal.force - do - // Reload Projects with some debouncing if `loadedProjectOptions` is out of date. - AVal.Observable.onOutOfDateWeak loadedProjectSnapshots - |> Observable.throttleOn Concurrency.NewThreadScheduler.Default (TimeSpan.FromMilliseconds(200.)) - |> Observable.observeOn Concurrency.NewThreadScheduler.Default - |> Observable.subscribe (fun _ -> forceLoadProjects () |> ignore )//|> Async.Ignore> |> Async.Start) - |> disposables.Add + // do + // // Reload Projects with some debouncing if `loadedProjectOptions` is out of date. + // AVal.Observable.onOutOfDateWeak loadedProjectSnapshots + // |> Observable.throttleOn Concurrency.NewThreadScheduler.Default (TimeSpan.FromMilliseconds(200.)) + // |> Observable.observeOn Concurrency.NewThreadScheduler.Default + // |> Observable.subscribe (fun _ -> forceLoadProjects () |> ignore )//|> Async.Ignore> |> Async.Start) + // |> disposables.Add - let sourceFileToProjectOptions = + // let sourceFileToProjectOptions = - amap { - let! snaps = loadedProjectSnapshots - yield! - snaps - |> List.collect (fun proj -> - proj.SourceFiles - |> Array.toList - |> List.map (fun source -> Utils.normalizePath source, proj) - ) - |> List.groupByFst + // amap { + // let! snaps = loadedProjectSnapshots + // yield! + // snaps + // |> List.collect (fun proj -> + // proj.SourceFiles + // |> Array.toList + // |> List.map (fun source -> Utils.normalizePath source, proj) + // ) + // |> List.groupByFst - } + // } - let _sourceFileToProjectOptions2 = + let sourceFileToProjectOptions = amap { let! snaps = loadedProjectSnapshots2 |> AMap.toAVal yield! snaps |> HashMap.toList - |> List.collect (fun (_, (proj,v)) -> + |> List.collect (fun (_, proj) -> proj.SourceFiles - |> List.map (fun source -> Utils.normalizePath source, (proj,v) + |> Array.toList + |> List.map (fun source -> Utils.normalizePath source, (proj) ) |> List.groupByFst) } @@ -1294,7 +1290,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac }) let allFSharpFilesAndProjectOptions = - asyncAVal { + // asyncAVal { let wins = openFilesToChangesAndProjectOptions |> AMap.map (fun _k v -> v |> AsyncAVal.mapSync (fun (file, projects) _ -> file, projects)) @@ -1309,14 +1305,15 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac return (file, Ok v) }) - return AMap.union loses wins - } + // return + AMap.union loses wins + // } let forceFindOpenFileOrRead (file: string) : Async> = asyncOption { - let! allFSharpFilesAndProjectOptions = allFSharpFilesAndProjectOptions |> AsyncAVal.forceAsync + // let allFSharpFilesAndProjectOptions = allFSharpFilesAndProjectOptions let! (file, _) = allFSharpFilesAndProjectOptions @@ -1373,14 +1370,18 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let! projects = loadedProjectSnapshots and! (checker: FSharpCompilerServiceChecker) = checker - let projects = + let! projects = projects - |> List.map (fun p -> p.FSharpProjectOptions) - |> List.toArray + |> List.map (fun p -> p.FSharpProjectSnapshot) + // |> List.toArray + |> ASet.ofList + |> ASet.mapA id + |> ASet.toAVal // |> fromOpts return projects + |> HashSet.toArray |> Array.collect (fun (snap) -> snap.SourceFiles |> List.toArray |> Array.map (fun s -> snap, s)) |> Array.map (fun (snap, fileName) -> let fileName = UMX.tag fileName.FileName @@ -1398,7 +1399,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let allFilesToFSharpProjectOptions = asyncAVal { - let! allFSharpFilesAndProjectOptions = allFSharpFilesAndProjectOptions + // let! allFSharpFilesAndProjectOptions = allFSharpFilesAndProjectOptions return allFSharpFilesAndProjectOptions @@ -1408,7 +1409,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let allFilesParsed = asyncAVal { - let! allFSharpFilesAndProjectOptions = allFSharpFilesAndProjectOptions + // let! allFSharpFilesAndProjectOptions = allFSharpFilesAndProjectOptions return allFSharpFilesAndProjectOptions @@ -1417,15 +1418,23 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let! (checker: FSharpCompilerServiceChecker) = checker and! selectProject = projectSelector - return! - asyncResult { + let loadedProject = + result { let! options = options let! project = selectProject.FindProject(file.FileName, options) - // let! snap = checker.FromOption(project.FSharpProjectOptions, documentSource) - // let options = project.FSharpProjectOptions - return! parseFile checker file options project + return project } + match loadedProject with + | Ok x -> + let! snap = x.FSharpProjectSnapshot + let! r = parseFile checker file options snap + return Ok r + + | Error e -> + return Error e + + }) } @@ -1482,7 +1491,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let getAllFSharpProjectOptions () = getAllProjectOptions () - |> Async.map (Array.map (fun x -> x.FSharpProjectOptions)) + |> Async.map (Array.map (fun x -> AVal.force x.FSharpProjectSnapshot)) let getProjectOptionsForFile (filePath: string) = asyncAVal { @@ -1525,7 +1534,6 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac /// The options for the project or script. /// Determines if the typecheck should be cached for autocompletions. /// The cache to use for autocompletions. - /// cancellationtoken /// let parseAndCheckFile (checker: FSharpCompilerServiceChecker) @@ -1534,14 +1542,14 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac shouldCache snapshotCache = - fun ctok -> - task { + cancellableTask { // use upperTimeout = new CancellationTokenSource() // upperTimeout.CancelAfter(TimeSpan.FromSeconds(60.)) // use upperlimitCt = CancellationTokenSource.CreateLinkedTokenSource(ctok, upperTimeout.Token) // let ctok = upperlimitCt.Token - // use! _lock = getCheckLock(file.FileName).LockAsync(ctok) + // use! _lock = getCheckLock(file.FileName).LockAsync(ctok)' + let! ctok = CancellableTask.getCancellationToken() let tags = [ SemanticConventions.fsac_sourceCodePath, box (UMX.untag file.Source.FileName) SemanticConventions.projectFilePath, box (options.ProjectFileName) ] @@ -1558,63 +1566,56 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac ) use progressReport = new ServerProgressReport(lspClient) - try - let simpleName = Path.GetFileName(UMX.untag file.Source.FileName) - do! progressReport.Begin($"Typechecking {simpleName}", message = $"{file.Source.FileName}") CancellationToken.None - - let! result = Async.StartAsTask( - checker.ParseAndCheckFileInProject( - file.Source.FileName, - file.Version, - file.Source, - options, - shouldCache = shouldCache, - ?snapshotAccumulator = snapshotCache - ) - ,cancellationToken = ctok) - // |> Debug.measureAsync $"checker.ParseAndCheckFileInProject - {file.Source.FileName}" - - // do! progressReport.End($"Typechecked {file.Source.FileName}") ctok - - notifications.Trigger(NotificationEvent.FileParsed(file.Source.FileName), ctok) - - match result with - | Error e -> - logger.error ( - Log.setMessage "Typecheck failed for {file} with {error}" - >> Log.addContextDestructured "file" file.FileName - >> Log.addContextDestructured "error" e - ) - - return Error e - | Ok parseAndCheck -> - logger.info ( - Log.setMessage "Typecheck completed successfully for {file}" - >> Log.addContextDestructured "file" file.Source.FileName - ) - Async.Start( - async { + let simpleName = Path.GetFileName(UMX.untag file.Source.FileName) + do! progressReport.Begin($"Typechecking {simpleName}", message = $"{file.Source.FileName}") CancellationToken.None + + let! result = + checker.ParseAndCheckFileInProject( + file.Source.FileName, + file.Version, + file.Source, + options, + shouldCache = shouldCache, + ?snapshotAccumulator = snapshotCache + ) + // |> Debug.measureAsync $"checker.ParseAndCheckFileInProject - {file.Source.FileName}" + + // do! progressReport.End($"Typechecked {file.Source.FileName}") ctok + + notifications.Trigger(NotificationEvent.FileParsed(file.Source.FileName), ctok) + + match result with + | Error e -> + logger.error ( + Log.setMessage "Typecheck failed for {file} with {error}" + >> Log.addContextDestructured "file" file.FileName + >> Log.addContextDestructured "error" e + ) + + return Error e + | Ok parseAndCheck -> + logger.info ( + Log.setMessage "Typecheck completed successfully for {file}" + >> Log.addContextDestructured "file" file.Source.FileName + ) - // fileParsed.Trigger(parseAndCheck.GetParseResults, options.To, ct) - fileChecked.Trigger(parseAndCheck, file, ctok) - let checkErrors = parseAndCheck.GetParseResults.Diagnostics - let parseErrors = parseAndCheck.GetCheckResults.Diagnostics - let errors = - Array.append checkErrors parseErrors - |> Array.distinctBy (fun e -> - e.Severity, e.ErrorNumber, e.StartLine, e.StartColumn, e.EndLine, e.EndColumn, e.Message) + // fileParsed.Trigger(parseAndCheck.GetParseResults, options.To, ct) + fileChecked.Trigger(parseAndCheck, file, ctok) + let checkErrors = parseAndCheck.GetParseResults.Diagnostics + let parseErrors = parseAndCheck.GetCheckResults.Diagnostics - notifications.Trigger(NotificationEvent.ParseError(errors, file.Source.FileName, file.Version), ctok) - }, - ctok - ) + let errors = + Array.append checkErrors parseErrors + |> Array.distinctBy (fun e -> + e.Severity, e.ErrorNumber, e.StartLine, e.StartColumn, e.EndLine, e.EndColumn, e.Message) + notifications.Trigger(NotificationEvent.ParseError(errors, file.Source.FileName, file.Version), ctok) + + + return Ok parseAndCheck - return Ok parseAndCheck - finally - progressReport |> dispose } /// Bypass Adaptive checking and tell the checker to check a file @@ -1652,16 +1653,20 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let! checker = checker and! selectProject = projectSelector - return! - asyncResult { + let options = + result { let! projectOptions = projectOptions let! opts = selectProject.FindProject(file, projectOptions) - + return opts + } + match options with + | Ok x -> + let! snap = x.FSharpProjectSnapshot return! - checker.TryGetRecentCheckResultsForFile(file, opts.FSharpProjectOptions, info.Source) + checker.TryGetRecentCheckResultsForFile(file, snap, info.Source) |> AsyncResult.ofOption (fun () -> $"No recent typecheck results for {file}. This may be ok if the file has not been checked yet.") - } + | Error e -> return Error e }) let openFilesToCheckedFilesResults = @@ -1671,17 +1676,21 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let file = info.FileName let! checker = checker and! selectProject = projectSelector - - return! - asyncResult { + let options = + result { let! projectOptions = projectOptions let! opts = selectProject.FindProject(file, projectOptions) - let cts = getOpenFileTokenOrDefault file - use linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ctok, cts) - return! - parseAndCheckFile checker info opts true None linkedCts.Token + return opts } + match options with + | Error e -> return Error e + | Ok x -> + let! snap = x.FSharpProjectSnapshot + let cts = getOpenFileTokenOrDefault file + // use linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ctok, cts) + return! + parseAndCheckFile checker info snap true None cts }) let getParseResults filePath = @@ -1730,7 +1739,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let forceGetFSharpProjectOptions filePath = forceGetProjectOptions filePath - |> Async.map (Result.map (fun p -> p.FSharpProjectOptions)) + |> Async.map (Result.map (fun p -> AVal.force p.FSharpProjectSnapshot)) let forceGetOpenFileTypeCheckResultsOrCheck file = @@ -1879,7 +1888,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let dependents = projectSnapshot |> Seq.filter (fun p -> - p.FSharpProjectOptions.ReferencedProjects + (AVal.force p.FSharpProjectSnapshot).ReferencedProjects |> Seq.exists (fun r -> match r.ProjectFilePath with | None -> false @@ -1890,7 +1899,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac currentPass.Clear() else for d in dependents do - allDependents.Add d.FSharpProjectOptions |> ignore + allDependents.Add (AVal.force d.FSharpProjectSnapshot) |> ignore currentPass.Clear() currentPass.AddRange(dependents |> Seq.map (fun p -> p.ProjectFileName)) @@ -1910,7 +1919,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac projects |> Result.bind (fun p -> selectProject.FindProject(file, p)) |> Result.toOption - |> Option.map (fun project -> project.FSharpProjectOptions) + |> Option.map (fun project -> AVal.force project.FSharpProjectSnapshot) } @@ -1918,7 +1927,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac async { let! projects = getProjectOptionsForFile file |> AsyncAVal.forceAsync let projects = projects |> Result.toOption |> Option.defaultValue [] - return projects |> List.map (fun p -> p.FSharpProjectOptions) + return projects |> List.map (fun p -> AVal.force p.FSharpProjectSnapshot) } SymbolLocation.getDeclarationLocation ( @@ -2225,7 +2234,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac sourceFiles |> Array.splitAt idx |> snd - |> Array.map (fun sourceFile -> proj.FSharpProjectOptions, sourceFile)) + |> Array.map (fun sourceFile -> AVal.force proj.FSharpProjectSnapshot, sourceFile)) |> Array.distinct } @@ -2243,11 +2252,11 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac projs |> Result.toOption |> Option.defaultValue [] - |> List.map (fun x -> x.FSharpProjectOptions) + |> List.map (fun x -> AVal.force x.FSharpProjectSnapshot) let! dependentProjects = projs |> getDependentProjectsOfProjects - // let checker = checker |> AVal.force + let checker = checker |> AVal.force // let docSource = documentSource |> AVal.force // let! optsAndSnaps = checker.FromOptions(Array.ofList (List.append projs dependentProjects), docSource) @@ -2415,14 +2424,16 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac member x.GetTypeCheckResultsForFile(filePath) = asyncResult { let! opts = forceGetProjectOptions filePath - return! x.GetTypeCheckResultsForFile(filePath, opts) + let snap = opts.FSharpProjectSnapshot |> AVal.force + return! x.GetTypeCheckResultsForFile(filePath, snap) } member x.GetFilesToProject() = getAllFilesToProjectOptionsSelected () member x.GetUsesOfSymbol(filePath, opts, symbol) = (AVal.force checker).GetUsesOfSymbol(filePath, opts, symbol) - member x.Codefixes = codefixes |> AVal.force + member x.Codefixes = + codefixes |> AVal.force member x.GlyphToCompletionKind = glyphToCompletionKind |> AVal.force diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi b/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi index e253a7046..4738b2835 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi @@ -30,7 +30,9 @@ type AdaptiveWorkspaceChosen = [] type LoadedProject = - { FSharpProjectOptions: FSharpProjectSnapshot + { + ProjectOptions : Types.ProjectOptions + FSharpProjectSnapshot: aval LanguageVersion: LanguageVersionShim } interface IEquatable @@ -38,7 +40,7 @@ type LoadedProject = override Equals: other: obj -> bool member SourceFiles: string array member ProjectFileName: string - static member op_Implicit: x: LoadedProject -> FSharpProjectSnapshot + // static member op_Implicit: x: LoadedProject -> FSharpProjectSnapshot type AdaptiveState = new: From e5c28ef76d43b8b6444ce8eec4555172dde7bc96 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Sat, 23 Mar 2024 14:02:25 -0400 Subject: [PATCH 12/60] workspace cleanups --- src/FsAutoComplete.Core/Utils.fs | 3 +- .../LspServers/AdaptiveServerState.fs | 178 ++++----- .../LspServers/ProjectWorkspace.fs | 339 ++++++++---------- .../FsAutoComplete.Tests.Lsp/SnapshotTests.fs | 52 ++- 4 files changed, 286 insertions(+), 286 deletions(-) diff --git a/src/FsAutoComplete.Core/Utils.fs b/src/FsAutoComplete.Core/Utils.fs index 6c5c8def2..0fad06fb8 100644 --- a/src/FsAutoComplete.Core/Utils.fs +++ b/src/FsAutoComplete.Core/Utils.fs @@ -238,8 +238,7 @@ module Async = /// A sequence of distinct computations to be parallelized. let parallel75 computations = let maxConcurrency = - 2 - // Math.Max(1.0, Math.Floor((float System.Environment.ProcessorCount) * 0.75)) + Math.Max(1.0, Math.Floor((float System.Environment.ProcessorCount) * 0.75)) Async.Parallel(computations, int maxConcurrency) diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index 10dd9dc58..ffcab80b2 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -41,7 +41,6 @@ open FsAutoComplete.ProjectWorkspace - [] type WorkspaceChosen = | Projs of HashSet> @@ -71,9 +70,9 @@ type LoadedProject = LanguageVersion: LanguageVersionShim } interface IEquatable with - member x.Equals(other) = x.FSharpProjectSnapshot = other.FSharpProjectSnapshot + member x.Equals(other) = x.ProjectOptions = other.ProjectOptions - override x.GetHashCode() = x.FSharpProjectSnapshot.GetHashCode() + override x.GetHashCode() = x.ProjectOptions.GetHashCode() override x.Equals(other: obj) = @@ -284,11 +283,11 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let scriptFileProjectOptions = Event() let fileParsed = - Event() + Event() let fileChecked = Event() - let detectTests (parseResults: FSharpParseFileResults) (proj: FSharpProjectOptions) ct = + let detectTests (parseResults: FSharpParseFileResults) (proj: FSharpProjectSnapshot) ct = try logger.info (Log.setMessageI $"Test Detection of {parseResults.FileName:file} started") @@ -1200,6 +1199,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac >> Log.addContextDestructured "version" version ) + cts.TryCancel() cts.TryDispose() @@ -1212,7 +1212,6 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac new CancellationTokenSource() openFilesTokens.AddOrUpdate(filePath, adder, updater) - |> ignore let updateOpenFiles (file: VolatileFile) = @@ -1220,7 +1219,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let updater _ (v: cval<_>) = v.Value <- file - resetCancellationToken file.FileName file.Version + resetCancellationToken file.FileName file.Version |> ignore transact (fun () -> openFiles.AddOrElse(file.Source.FileName, adder, updater)) let updateTextChanges filePath ((changes: DidChangeTextDocumentParams, _) as p) = @@ -1228,7 +1227,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let adder _ = cset<_> [ p ] let updater _ (v: cset<_>) = v.Add p |> ignore - resetCancellationToken filePath changes.TextDocument.Version + resetCancellationToken filePath changes.TextDocument.Version |> ignore transact (fun () -> textChanges.AddOrElse(filePath, adder, updater)) let isFileOpen file = openFiles |> AMap.tryFindA file |> AVal.map (Option.isSome) @@ -1349,16 +1348,14 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac /// Parses a source code for a file and caches the results. Returns an AST that can be traversed for various features. /// The FSharpCompilerServiceChecker. /// The source to be parsed. - /// The options for the project or script. /// /// - let parseFile (checker: FSharpCompilerServiceChecker) (source: VolatileFile) options snap = + let parseFile (checker: FSharpCompilerServiceChecker) (source: VolatileFile) snap = async { - let _options = options let! result = checker.ParseFile(source.FileName, source.Source, snap) - // let! ct = Async.CancellationToken - // fileParsed.Trigger(result, options, ct) + let! ct = Async.CancellationToken + fileParsed.Trigger(result, snap, ct) return result } @@ -1388,7 +1385,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac asyncResult { let! file = forceFindOpenFileOrRead fileName - return! parseFile checker file () snap + return! parseFile checker file snap } |> Async.map Result.toOption) |> Async.parallel75 @@ -1398,20 +1395,20 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let allFilesToFSharpProjectOptions = - asyncAVal { + // asyncAVal { // let! allFSharpFilesAndProjectOptions = allFSharpFilesAndProjectOptions - return + // return allFSharpFilesAndProjectOptions |> AMapAsync.mapAsyncAVal (fun _filePath (_file, options) _ctok -> AsyncAVal.constant options) - } + // } let allFilesParsed = - asyncAVal { + // asyncAVal { // let! allFSharpFilesAndProjectOptions = allFSharpFilesAndProjectOptions - return + // return allFSharpFilesAndProjectOptions |> AMapAsync.mapAsyncAVal (fun _filePath (file, options: Result) _ctok -> asyncAVal { @@ -1419,29 +1416,23 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac and! selectProject = projectSelector let loadedProject = - result { - let! options = options - let! project = selectProject.FindProject(file.FileName, options) - return project - } + options + |> Result.bind (fun p -> selectProject.FindProject(file.FileName, p)) match loadedProject with | Ok x -> let! snap = x.FSharpProjectSnapshot - let! r = parseFile checker file options snap + let! r = parseFile checker file snap return Ok r - | Error e -> return Error e - - }) - } + // } let getAllFilesToProjectOptions () = asyncEx { - let! allFilesToFSharpProjectOptions = allFilesToFSharpProjectOptions |> AsyncAVal.forceAsync + // let! allFilesToFSharpProjectOptions = allFilesToFSharpProjectOptions |> AsyncAVal.forceAsync return! allFilesToFSharpProjectOptions @@ -1473,7 +1464,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let getAllProjectOptions () = async { - let! allFilesToFSharpProjectOptions = allFilesToFSharpProjectOptions |> AsyncAVal.forceAsync + // let! allFilesToFSharpProjectOptions = allFilesToFSharpProjectOptions |> AsyncAVal.forceAsync let! set = allFilesToFSharpProjectOptions @@ -1495,7 +1486,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let getProjectOptionsForFile (filePath: string) = asyncAVal { - let! allFilesToFSharpProjectOptions = allFilesToFSharpProjectOptions + // let! allFilesToFSharpProjectOptions = allFilesToFSharpProjectOptions match! allFilesToFSharpProjectOptions |> AMapAsync.tryFindA filePath with | Some projs -> return projs @@ -1521,13 +1512,6 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let getAutoCompleteNamespacesByDeclName name = autoCompleteNamespaces |> AMap.tryFind name - let checkLocks = ConcurrentDictionary, SemaphoreSlim>() - - let getCheckLock filePath = - match checkLocks.TryGetValue(filePath) with - | (true, v) -> v - | _ -> checkLocks.GetOrAdd(filePath, new SemaphoreSlim(1,1)) - /// Gets Parse and Check results of a given file while also handling other concerns like Progress, Logging, Eventing. /// The FSharpCompilerServiceChecker. /// The name of the file in the project whose source to find a typecheck. @@ -1543,13 +1527,6 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac snapshotCache = cancellableTask { - // use upperTimeout = new CancellationTokenSource() - // upperTimeout.CancelAfter(TimeSpan.FromSeconds(60.)) - - // use upperlimitCt = CancellationTokenSource.CreateLinkedTokenSource(ctok, upperTimeout.Token) - // let ctok = upperlimitCt.Token - // use! _lock = getCheckLock(file.FileName).LockAsync(ctok)' - let! ctok = CancellableTask.getCancellationToken() let tags = [ SemanticConventions.fsac_sourceCodePath, box (UMX.untag file.Source.FileName) SemanticConventions.projectFilePath, box (options.ProjectFileName) ] @@ -1568,7 +1545,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac use progressReport = new ServerProgressReport(lspClient) let simpleName = Path.GetFileName(UMX.untag file.Source.FileName) - do! progressReport.Begin($"Typechecking {simpleName}", message = $"{file.Source.FileName}") CancellationToken.None + do! progressReport.Begin($"Typechecking {simpleName}", message = $"{file.Source.FileName}") let! result = checker.ParseAndCheckFileInProject( @@ -1583,6 +1560,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac // do! progressReport.End($"Typechecked {file.Source.FileName}") ctok + let! ctok = CancellableTask.getCancellationToken() notifications.Trigger(NotificationEvent.FileParsed(file.Source.FileName), ctok) match result with @@ -1600,8 +1578,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac >> Log.addContextDestructured "file" file.Source.FileName ) - - // fileParsed.Trigger(parseAndCheck.GetParseResults, options.To, ct) + fileParsed.Trigger(parseAndCheck.GetParseResults, options, ctok) fileChecked.Trigger(parseAndCheck, file, ctok) let checkErrors = parseAndCheck.GetParseResults.Diagnostics let parseErrors = parseAndCheck.GetCheckResults.Diagnostics @@ -1613,7 +1590,6 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac notifications.Trigger(NotificationEvent.ParseError(errors, file.Source.FileName, file.Version), ctok) - return Ok parseAndCheck } @@ -1677,30 +1653,32 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let! checker = checker and! selectProject = projectSelector let options = - result { - let! projectOptions = projectOptions - let! opts = selectProject.FindProject(file, projectOptions) - return opts - } + projectOptions + |> Result.bind (fun p -> selectProject.FindProject(file, p)) + match options with | Error e -> return Error e | Ok x -> let! snap = x.FSharpProjectSnapshot - let cts = getOpenFileTokenOrDefault file - // use linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ctok, cts) return! - parseAndCheckFile checker info snap true None cts + asyncResult { + let cts = getOpenFileTokenOrDefault file + use linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ctok, cts) + return! + parseAndCheckFile checker info snap true None linkedCts.Token + } + }) let getParseResults filePath = - asyncAVal { - let! allFilesParsed = allFilesParsed + // asyncAVal { + // let! allFilesParsed = allFilesParsed - return! + // return! allFilesParsed |> AMapAsync.tryFindAndFlattenR $"No parse results found for {filePath}" filePath - } + // } let getOpenFileTypeCheckResults filePath = openFilesToCheckedFilesResults @@ -1797,17 +1775,17 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac |> AsyncAVal.forceAsync let allFilesToDeclarations = - asyncAVal { - let! allFilesParsed = allFilesParsed + // asyncAVal { + // let! allFilesParsed = allFilesParsed - return + // return allFilesParsed |> AMap.map (fun _k v -> v |> AsyncAVal.mapResult (fun p _ -> p.GetNavigationItems().Declarations)) - } + // } let getAllDeclarations () = async { - let! allFilesToDeclarations = allFilesToDeclarations |> AsyncAVal.forceAsync + // let! allFilesToDeclarations = allFilesToDeclarations //|> AsyncAVal.forceAsync let! results = allFilesToDeclarations @@ -1826,13 +1804,13 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac } let getDeclarations filename = - asyncAVal { - let! allFilesToDeclarations = allFilesToDeclarations + // asyncAVal { + // let! allFilesToDeclarations = allFilesToDeclarations - return! + // return! allFilesToDeclarations |> AMapAsync.tryFindAndFlattenR $"Could not find getDeclarations for {filename}" filename - } + // } let codeGenServer = { new ICodeGenerationService with @@ -2239,14 +2217,15 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac } - let bypassAdaptiveAndCheckDependenciesForFile (filePath: string) = + let bypassAdaptiveAndCheckDependenciesForFile (sourceFilePath: string) = async { - // let snapshotCache = System.Collections.Generic.Dictionary<_, _>() - let tags = [ SemanticConventions.fsac_sourceCodePath, box (UMX.untag filePath) ] + let tags = [ SemanticConventions.fsac_sourceCodePath, box (UMX.untag sourceFilePath) ] use _ = fsacActivitySource.StartActivityForType(thisType, tags = tags) - let! dependentFiles = getDependentFilesForFile filePath + let! dependentFiles = getDependentFilesForFile sourceFilePath - let! projs = getProjectOptionsForFile filePath |> AsyncAVal.forceAsync + let! projs = getProjectOptionsForFile sourceFilePath |> AsyncAVal.forceAsync + + let rootToken = sourceFilePath |> getOpenFileTokenOrDefault let projs = projs @@ -2256,11 +2235,6 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let! dependentProjects = projs |> getDependentProjectsOfProjects - let checker = checker |> AVal.force - - // let docSource = documentSource |> AVal.force - // let! optsAndSnaps = checker.FromOptions(Array.ofList (List.append projs dependentProjects), docSource) - let dependentProjectsAndSourceFiles = dependentProjects |> List.collect (fun (snap) -> snap.SourceFiles |> List.map (fun sourceFile -> snap, sourceFile.FileName)) @@ -2283,32 +2257,36 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac file.Contains "AssemblyInfo.fs" |> not && file.Contains "AssemblyAttributes.fs" |> not) - // innerChecks - // |> Array.map fst - // |> Array.distinctBy (fun snap -> snap.ProjectFileName) - // |> checker.ClearCache - let checksToPerformLength = innerChecks.Length innerChecks |> Array.map (fun (snap, file) -> - let file = UMX.tag file + async { + let file = UMX.tag file - let token = getOpenFileTokenOrDefault filePath + use joinedToken = + if file = sourceFilePath then + // dont reset the token for the incoming file as it would cancel the whole operation + CancellationTokenSource.CreateLinkedTokenSource(rootToken) + else + // only cancel other files + // If we have multiple saves from separate root files we want only one to be running + let token = resetCancellationToken file None // Dont dispose, we're a renter not an owner + // and join with the root token as well since we want to cancel the whole operation if the root files changes + CancellationTokenSource.CreateLinkedTokenSource(rootToken, token.Token) - bypassAdaptiveTypeCheck (file) (snap) (None) - |> Async.withCancellation token - |> Async.Ignore - |> Async.bind (fun _ -> - async { - let checksCompleted = Interlocked.Increment(&checksCompleted) + let! _ = + bypassAdaptiveTypeCheck (file) (snap) (None) + |> Async.withCancellation joinedToken.Token + + let checksCompleted = Interlocked.Increment(&checksCompleted) + do! + progressReporter.Report( + message = $"{checksCompleted}/{checksToPerformLength} remaining", + percentage = percentage checksCompleted checksToPerformLength + ) + }) - do! - progressReporter.Report( - message = $"{checksCompleted}/{checksToPerformLength} remaining", - percentage = percentage checksCompleted checksToPerformLength - ) - })) do! diff --git a/src/FsAutoComplete/LspServers/ProjectWorkspace.fs b/src/FsAutoComplete/LspServers/ProjectWorkspace.fs index 10f1669ac..53646f533 100644 --- a/src/FsAutoComplete/LspServers/ProjectWorkspace.fs +++ b/src/FsAutoComplete/LspServers/ProjectWorkspace.fs @@ -1,61 +1,8 @@ namespace FsAutoComplete.ProjectWorkspace +open System -module AMap = - open FSharp.Data.Adaptive - let rec findAllDependenciesOfAndIncluding key findNextKeys items = - amap { - // printfn "findAllDependenciesOfAndIncluding %A" key - let! item = AMap.tryFind key items - match item with - | None -> - () - | Some item -> - yield key, item - let! dependencies = - item - |> AVal.map( - findNextKeys - >> Seq.map (fun newKey -> findAllDependenciesOfAndIncluding newKey findNextKeys items) - >> Seq.fold(fun s v -> AMap.union v s) AMap.empty - ) - yield! dependencies - } - - let findAllDependenciesOfAndIncluding2 key (findNextKeys : _ -> seq<_>) items = - let rec inner key = - aset { - // printfn "findAllDependenciesOfAndIncluding %A" key - let! item = AMap.tryFind key items - match item with - | None -> - () - | Some item -> - yield key, item - let! itemV = item - for newKey in findNextKeys itemV do - yield! inner newKey - } - inner key - |> AMap.ofASet - |> AMap.choose' Seq.tryHead - - - let rec findAllDependentsOfAndIncluding key findNextKeys items = - amap { - // printfn "findAllDependentsOfAndIncluding %A" key - let immediateDependents = - items - |> AMap.filterA (fun _ v -> v |> AVal.map (findNextKeys >> Seq.exists ((=) key))) - yield! immediateDependents - let! dependentOfDependents = - immediateDependents - |> AMap.map (fun nextKey _ -> findAllDependentsOfAndIncluding nextKey findNextKeys items) - |> AMap.fold(fun acc _ x -> AMap.union acc x) AMap.empty - yield! dependentOfDependents - } - module Snapshots = open System @@ -81,7 +28,7 @@ module Snapshots = let projectFile = FileInfo p.TargetPath let getStamp () = - projectFile.Refresh () + projectFile.Refresh() projectFile.LastWriteTimeUtc let getStream (_ctok: System.Threading.CancellationToken) = @@ -106,34 +53,36 @@ module Snapshots = loadTime unresolvedReferences originalLoadReferences - = - aval { - let! projectFileName = projectFileName - and! projectId = projectId - and! sourceFiles = sourceFiles - and! referencePaths = referencePaths - and! otherOptions = otherOptions - and! referencedProjects = referencedProjects - and! isIncompleteTypeCheckEnvironment = isIncompleteTypeCheckEnvironment - and! useScriptResolutionRules = useScriptResolutionRules - and! loadTime = loadTime - and! unresolvedReferences = unresolvedReferences - and! originalLoadReferences = originalLoadReferences - let stamp = DateTime.UtcNow.Ticks - logger.info( - Log.setMessage "Creating FCS snapshot {projectFileName} {stamp}" - >> Log.addContextDestructured "projectFileName" projectFileName - >> Log.addContextDestructured "stamp" stamp - ) - - // printfn "Snapshot %A" projectFileName - return FSharpProjectSnapshot.Create( + = + aval { + let! projectFileName = projectFileName + and! projectId = projectId + and! sourceFiles = sourceFiles + and! referencePaths = referencePaths + and! otherOptions = otherOptions + and! referencedProjects = referencedProjects + and! isIncompleteTypeCheckEnvironment = isIncompleteTypeCheckEnvironment + and! useScriptResolutionRules = useScriptResolutionRules + and! loadTime = loadTime + and! unresolvedReferences = unresolvedReferences + and! originalLoadReferences = originalLoadReferences + // Always use a new stamp for a new snapshot + let stamp = DateTime.UtcNow.Ticks + + logger.debug ( + Log.setMessage "Creating FCS snapshot {projectFileName} {stamp}" + >> Log.addContextDestructured "projectFileName" projectFileName + >> Log.addContextDestructured "stamp" stamp + ) + + return + FSharpProjectSnapshot.Create( projectFileName, projectId, sourceFiles, referencePaths, otherOptions, - referencedProjects , + referencedProjects, isIncompleteTypeCheckEnvironment, useScriptResolutionRules, loadTime, @@ -141,66 +90,84 @@ module Snapshots = originalLoadReferences, Some stamp ) - } + } let makeAdaptiveFCSSnapshot2 + projectFileName + projectId + (sourceFiles: alist>) + (referencePaths: aset>) + (otherOptions: aset>) + (referencedProjects: aset>) + isIncompleteTypeCheckEnvironment + useScriptResolutionRules + loadTime + unresolvedReferences + originalLoadReferences + = + let flattenASet (s: aset>) = s |> ASet.mapA id |> ASet.toAVal |> AVal.map HashSet.toList + let flattenAList (s: alist>) = s |> AList.mapA id |> AList.toAVal |> AVal.map IndexList.toList + + makeAdaptiveFCSSnapshot projectFileName projectId - (sourceFiles: alist>) - (referencePaths: aset>) - (otherOptions: aset>) - (referencedProjects: aset>) + (flattenAList sourceFiles) + (flattenASet referencePaths) + (flattenASet otherOptions) + (flattenASet referencedProjects) isIncompleteTypeCheckEnvironment useScriptResolutionRules loadTime unresolvedReferences originalLoadReferences - = - let flattenASet (s: aset>) = s |> ASet.mapA id |> ASet.toAVal |> AVal.map HashSet.toList - let flattenAList (s: alist>) = s |> AList.mapA id |> AList.toAVal |> AVal.map IndexList.toList - makeAdaptiveFCSSnapshot - projectFileName - projectId - (flattenAList sourceFiles) - (flattenASet referencePaths) - (flattenASet otherOptions) - (flattenASet referencedProjects) - isIncompleteTypeCheckEnvironment - useScriptResolutionRules - loadTime - unresolvedReferences - originalLoadReferences - - - let private createFSharpFileSnapshotOnDisk (sourceTextFactory : aval) fileName = + + // Could be configurable but this is a good default + // https://learn.microsoft.com/en-us/dotnet/core/runtime-config/garbage-collector#large-object-heap-threshold + let [] LargeObjectHeapThreshold = 85000 + + let private createFSharpFileSnapshotOnDisk (sourceTextFactory: aval) fileName = aval { let! writeTime = AdaptiveFile.GetLastWriteTimeUtc fileName and! sourceTextFactory = sourceTextFactory - let getSource () = task { - - let file = Utils.normalizePath fileName - // use s = File.openFileStreamForReadingAsync file - // let! source = sourceTextFactory.Create(file, s) CancellationToken.None - let! text = File.ReadAllTextAsync fileName - let source = sourceTextFactory.Create(file, text) - return source :> ISourceTextNew - } + + let getSource () = + task { + + let file = Utils.normalizePath fileName + + // use large object heap hits or threadpool hits? Which is worse? Choose your foot gun. + + if FileInfo(fileName).Length >= LargeObjectHeapThreshold then + // Roslyn SourceText doesn't actually support async streaming reads but avoids the large object heap hit + // so we have to block a thread. + use s = File.openFileStreamForReadingAsync file + let! source = sourceTextFactory.Create (file, s) CancellationToken.None + return source :> ISourceTextNew + else + // otherwise it'll be under the LOH threshold and the current thread isn't blocked + let! text = File.ReadAllTextAsync fileName + let source = sourceTextFactory.Create(file, text) + return source :> ISourceTextNew + } // printfn "Creating source text for %s" fileName return ProjectSnapshot.FSharpFileSnapshot.Create(fileName, string writeTime.Ticks, getSource) } - let private createFSharpFileSnapshotInMemory (v : VolatileFile) = + + let private createFSharpFileSnapshotInMemory (v: VolatileFile) = let file = UMX.untag v.FileName + // Use LastTouched instead of Version because we're using that in the onDisk version + // it's useful for keeping the cache consistent in FCS so when someone opens a file we don't need to re-issue type-checks let version = v.LastTouched.Ticks - let getSource () = - v.Source - :> ISourceTextNew - |> Task.FromResult + let getSource () = v.Source :> ISourceTextNew |> Task.FromResult ProjectSnapshot.FSharpFileSnapshot.Create(file, string version, getSource) let private createReferenceOnDisk path : aval = aval { let! lastModified = AdaptiveFile.GetLastWriteTimeUtc path - return { LastModified = lastModified; Path = path } + + return + { LastModified = lastModified + Path = path } } let private createReferencedProjectsFSharpReference projectOutputFile (snapshot: aval) = @@ -212,85 +179,90 @@ module Snapshots = let rec private createReferences (cachedSnapshots) - (inMemorySourceFiles : amap, aval>) + (inMemorySourceFiles: amap, aval>) (sourceTextFactory: aval) - (loadedProjectsA: amap,ProjectOptions>) - (p : ProjectOptions) = - logger.info( + (loadedProjectsA: amap, ProjectOptions>) + (p: ProjectOptions) + = + logger.debug ( Log.setMessage "Creating references for {projectFileName}" >> Log.addContextDestructured "projectFileName" p.ProjectFileName ) - let normPath = Utils.normalizePath p.ProjectFileName - // let deps = - // loadedProjectsA - // |> AMap.findAllDependenciesOfAndIncluding2 - // normPath - // (fun p -> p.ReferencedProjects |> Seq.map(_.ProjectFileName >> Utils.normalizePath)) - let deps = - loadedProjectsA - |> AMap.filter(fun k _ -> p.ReferencedProjects |> List.exists(fun x -> x.ProjectFileName = UMX.untag k)) - deps - |> AMap.filter(fun k _ -> k <> normPath) - |> AMap.map(fun _ p -> aval { - if p.ProjectFileName.EndsWith ".fsproj" then - let snapshot = optionsToSnapshot cachedSnapshots inMemorySourceFiles sourceTextFactory (createReferences cachedSnapshots inMemorySourceFiles sourceTextFactory loadedProjectsA) p - return! createReferencedProjectsFSharpReference (AVal.constant p.ResolvedTargetPath) snapshot + + loadedProjectsA + |> AMap.filter (fun k _ -> p.ReferencedProjects |> List.exists (fun x -> normalizePath x.ProjectFileName = k)) + |> AMap.map (fun _ proj -> + if proj.ProjectFileName.EndsWith ".fsproj" then + + let resolvedTargetPath = + aval { + // TODO: Find if this needs to be adaptive, unsure if we need to check if the file has changed on disk if we need a new snapshot + let! _ = AdaptiveFile.GetLastWriteTimeUtc proj.ResolvedTargetPath + return proj.ResolvedTargetPath + } + + proj + |> optionsToSnapshot + cachedSnapshots + inMemorySourceFiles + sourceTextFactory + (createReferences cachedSnapshots inMemorySourceFiles sourceTextFactory loadedProjectsA) + |> createReferencedProjectsFSharpReference resolvedTargetPath else // TODO: Find if this needs to be adaptive or if `getStamp` in a PEReference will be enough to break thru the caching in FCS - return loadFromDotnetDll p - }) + loadFromDotnetDll proj |> AVal.constant) |> AMap.toASetValues and optionsToSnapshot - (cachedSnapshots : Dictionary<_,_>) - (inMemorySourceFiles : amap<_, aval>) + (cachedSnapshots: Dictionary<_, _>) + (inMemorySourceFiles: amap<_, aval>) (sourceTextFactory: aval) (mapReferences: ProjectOptions -> aset>) - (p : ProjectOptions) = + (p: ProjectOptions) + = + let normPath = Utils.normalizePath p.ProjectFileName - // printfn "optionsToSnapshot - enter %A" p.ProjectFileName - aval { - let normPath = Utils.normalizePath p.ProjectFileName - match cachedSnapshots.TryGetValue normPath with - | (true, x) -> - logger.info( - Log.setMessage "optionsToSnapshot - Cache hit - {projectFileName}" - >> Log.addContextDestructured "projectFileName" p.ProjectFileName - ) - // printfn "optionsToSnapshot - Cache hit %A" p.ProjectFileName - return! x - | _ -> - logger.info( + match cachedSnapshots.TryGetValue normPath with + | true, snapshot -> + logger.debug ( + Log.setMessage "optionsToSnapshot - Cache hit - {projectFileName}" + >> Log.addContextDestructured "projectFileName" p.ProjectFileName + ) + snapshot + | _ -> + aval { + logger.debug ( Log.setMessage "optionsToSnapshot - Cache miss - {projectFileName}" >> Log.addContextDestructured "projectFileName" p.ProjectFileName ) - // printfn "optionsToSnapshot - Cache miss %A" p.ProjectFileName - let projectName = p.ProjectFileName + let projectName = AVal.constant p.ProjectFileName let projectId = p.ProjectId |> AVal.constant - let sourceFiles = + + let sourceFiles = // alist because order matters for the F# Compiler p.SourceFiles |> AList.ofList - |> AList.map(fun sourcePath -> - let normPath = Utils.normalizePath sourcePath - aval { - match! inMemorySourceFiles |> AMap.tryFind normPath with - | Some volatileFile -> - return! volatileFile |> AVal.map createFSharpFileSnapshotInMemory - | None -> return! createFSharpFileSnapshotOnDisk sourceTextFactory sourcePath - } - - ) - - let references, otherOptions = p.OtherOptions |> List.partition (fun x -> x.StartsWith("-r:")) - let otherOptions = otherOptions |> ASet.ofList |> ASet.map(AVal.constant) + |> AList.map (fun sourcePath -> + let normPath = Utils.normalizePath sourcePath + + aval { + match! inMemorySourceFiles |> AMap.tryFind normPath with + | Some volatileFile -> return! volatileFile |> AVal.map createFSharpFileSnapshotInMemory + | None -> return! createFSharpFileSnapshotOnDisk sourceTextFactory sourcePath + }) + + let references, otherOptions = + p.OtherOptions |> List.partition (fun x -> x.StartsWith("-r:")) + + let otherOptions = otherOptions |> ASet.ofList |> ASet.map (AVal.constant) + let referencePaths = references |> ASet.ofList - |> ASet.map(fun referencePath -> + |> ASet.map (fun referencePath -> referencePath.Substring(3) // remove "-r:" - |> createReferenceOnDisk - ) + |> createReferenceOnDisk) + let referencedProjects = mapReferences p let isIncompleteTypeCheckEnvironment = AVal.constant false let useScriptResolutionRules = AVal.constant false @@ -300,7 +272,7 @@ module Snapshots = let snap = makeAdaptiveFCSSnapshot2 - (AVal.constant projectName) + projectName projectId sourceFiles referencePaths @@ -315,16 +287,21 @@ module Snapshots = cachedSnapshots.Add(normPath, snap) return! snap - } + } let createSnapshots - (inMemorySourceFiles: amap,aval>) + (inMemorySourceFiles: amap, aval>) (sourceTextFactory: aval) - (loadedProjectsA: amap,ProjectOptions>) = - let cachedSnapshots = Dictionary<_,_>() - let mapReferences = createReferences cachedSnapshots inMemorySourceFiles sourceTextFactory loadedProjectsA - let optionsToSnapshot = optionsToSnapshot cachedSnapshots inMemorySourceFiles sourceTextFactory mapReferences + (loadedProjectsA: amap, ProjectOptions>) + = + let cachedSnapshots = Dictionary<_, _>() + + let mapReferences = + createReferences cachedSnapshots inMemorySourceFiles sourceTextFactory loadedProjectsA + + let optionsToSnapshot = + optionsToSnapshot cachedSnapshots inMemorySourceFiles sourceTextFactory mapReferences loadedProjectsA - |> AMap.filter(fun k _ -> (UMX.untag k).EndsWith ".fsproj") - |> AMap.map (fun _ v -> v, optionsToSnapshot v ) + |> AMap.filter (fun k _ -> (UMX.untag k).EndsWith ".fsproj") + |> AMap.map (fun _ v -> v, optionsToSnapshot v) diff --git a/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs index 5b6e1f291..86aeca94d 100644 --- a/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs @@ -217,11 +217,11 @@ let snapshotTests loaders toolsPath = Snapshots.createSnapshots AMap.empty (AVal.constant sourceTextFactory) loadedProjectsA let snapshots = snapsA |> AMap.mapA (fun _ (_,v) -> v) |> AMap.force - let libraryFile = Projects.MultiProjectScenario1.Console1.programFileIn dDir.DirectoryInfo + let consoleFile = Projects.MultiProjectScenario1.Console1.programFileIn dDir.DirectoryInfo // printfn "Setting last write time for %s %A" libraryFile.FullName libraryFile.LastWriteTime - do! File.WriteAllTextAsync(libraryFile.FullName, "let x = 1") - libraryFile.Refresh() + do! File.WriteAllTextAsync(consoleFile.FullName, "let x = 1") + consoleFile.Refresh() // printfn "last write time for %s %A" libraryFile.FullName libraryFile.LastWriteTime @@ -311,5 +311,51 @@ let snapshotTests loaders toolsPath = Expect.notEqual cs1.Stamp cs2.Stamp "Stamp should not be the same" } + + (* + Depending on the tree structure of the project, certain things will cause a reload of the project + and certain certain things will only cause a snapshot to be updated. + + Also depending on the structure and update only subgraphs should change and not the entire graph. + We need to reason about each scenario below and create multiple tests for them based on different structures. + + Also performance of bigger project graphs should be tested. + *) + + // Add Project + // - Should cause a project reload + // Delete project + // - Should cause a project reload + // Rename Project + // - Should practically be "Delete" then "Add" + // - Unsure how this works with regards to updating all references to the project in other projects or solution files + // Move project + // - Should practically be "Delete" then "Add" + // - Unsure how this works with regards to updating all references to the project in other projects or solution files + + // Add file + // - Should cause a project reload as this is a project file change + // Delete file + // - Should cause a project reload as this is a project file change + // Rename file + // - Should practically be "Delete" then "Add" + // Move file order + // - Should practically be "Delete" then "Add" + // Update file + // - Should cause a snapshot update and all depending snapshots but not a project reload + + // Add package + // - Should cause a project reload + // Remove package + // - Should cause a project reload + // Update package + // - Should cause a project reload + + // Add reference + // - Should cause a project reload + // Remove reference + // - Should cause a project reload + // Build referenced project that isn't fsproj + // - Probably should only cause a snapshot update but might depend on what was done the csproj as well ] ] From 5c5f108281e1c1143c7026e0bd2c1d869b612c87 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Mon, 25 Mar 2024 16:29:03 -0400 Subject: [PATCH 13/60] Fixup much stuff --- src/FsAutoComplete.Core/AdaptiveExtensions.fs | 29 +- .../CompilerServiceInterface.fs | 225 ++--- .../CompilerServiceInterface.fsi | 36 +- src/FsAutoComplete.Core/FCSPatches.fs | 13 +- src/FsAutoComplete.Core/FileSystem.fs | 3 +- src/FsAutoComplete.Core/SymbolLocation.fs | 4 +- src/FsAutoComplete/CodeFixes.fs | 9 +- src/FsAutoComplete/CodeFixes.fsi | 7 +- .../CodeFixes/AddTypeAliasToSignatureFile.fs | 4 +- .../LspServers/AdaptiveServerState.fs | 837 ++++++++---------- .../LspServers/AdaptiveServerState.fsi | 5 +- .../LspServers/FSharpLspClient.fs | 3 +- .../LspServers/ProjectWorkspace.fs | 15 +- 13 files changed, 499 insertions(+), 691 deletions(-) diff --git a/src/FsAutoComplete.Core/AdaptiveExtensions.fs b/src/FsAutoComplete.Core/AdaptiveExtensions.fs index db856c6c2..29202f06d 100644 --- a/src/FsAutoComplete.Core/AdaptiveExtensions.fs +++ b/src/FsAutoComplete.Core/AdaptiveExtensions.fs @@ -616,6 +616,13 @@ module AsyncAVal = AdaptiveCancellableTask(cancel, real) } :> asyncaval<_> + // Getting values from AVals can block the current thread. We do a lot of conversions between AVals and AsyncAvals + // so we need to make sure we don't block the threads with too many of these conversions otherwise we might start + // having threadpool exhaustion issues. + let avalSyncLock = + // Should have enough parallelism to allow others to finish but not too much to cause threadpool exhaustion + new SemaphoreSlim(Environment.ProcessorCount, Environment.ProcessorCount) + /// /// Creates an async adaptive value evaluation the given value. /// @@ -623,12 +630,28 @@ module AsyncAVal = if value.IsConstant then ConstantVal(Task.FromResult(AVal.force value)) :> asyncaval<_> else + { new AbstractVal<'a>() with member x.Compute t = + let cts = new CancellationTokenSource() + + let cancel () = + cts.TryCancel() + cts.TryDispose() let real = - // if out of date, assume it needs to run on the threadpool and not the current thread - Task.Run(fun () -> value.GetValue t) - AdaptiveCancellableTask(id, real) } + task { + do! avalSyncLock.WaitAsync(cts.Token) + try + // Start this work on the threadpool so we can return AdaptiveCancellableTask and let the system cancel if needed + // We do this because tasks will stay on the current thread unless there is an yield or await in them. + return! Task.Run((fun () -> cts.Token.ThrowIfCancellationRequested(); value.GetValue t), cts.Token) + finally + avalSyncLock.Release() |> ignore + cts.TryDispose() + } + + AdaptiveCancellableTask(cancel, real) + } :> asyncaval<_> diff --git a/src/FsAutoComplete.Core/CompilerServiceInterface.fs b/src/FsAutoComplete.Core/CompilerServiceInterface.fs index bd377fd77..3e72a9bb3 100644 --- a/src/FsAutoComplete.Core/CompilerServiceInterface.fs +++ b/src/FsAutoComplete.Core/CompilerServiceInterface.fs @@ -13,10 +13,6 @@ open Microsoft.Extensions.Caching.Memory open System open FsToolkit.ErrorHandling open FSharp.Compiler.CodeAnalysis.ProjectSnapshot -open System.Collections.Generic -open System.Threading -open IcedTasks - type Version = int @@ -56,7 +52,7 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe /// additional arguments that are added to typechecking of scripts let mutable fsiAdditionalArguments = Array.empty - let mutable fsiAdditionalFiles = Array.empty + let mutable fsiAdditionalFiles = List.empty /// This event is raised when any data that impacts script typechecking /// is changed. This can potentially invalidate existing project options @@ -65,19 +61,31 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe let mutable disableInMemoryProjectReferences = false - let fixupFsharpCoreAndFSIPaths (p: FSharpProjectOptions) = + let fixupFsharpCoreAndFSIPaths (snapshot: FSharpProjectSnapshot) = match sdkFsharpCore, sdkFsiAuxLib with | None, _ - | _, None -> p + | _, None -> snapshot | Some fsc, Some fsi -> let _toReplace, otherOpts = - p.OtherOptions - |> Array.partition (fun opt -> + snapshot.OtherOptions + |> List.partition (fun opt -> opt.EndsWith("FSharp.Core.dll", StringComparison.Ordinal) || opt.EndsWith("FSharp.Compiler.Interactive.Settings.dll", StringComparison.Ordinal)) - { p with - OtherOptions = Array.append otherOpts [| $"-r:%s{fsc.FullName}"; $"-r:%s{fsi.FullName}" |] } + FSharpProjectSnapshot.Create( + snapshot.ProjectFileName, + snapshot.ProjectId, + snapshot.SourceFiles, + snapshot.ReferencesOnDisk, + List.append otherOpts [ $"-r:%s{fsc.FullName}"; $"-r:%s{fsi.FullName}" ], + snapshot.ReferencedProjects, + snapshot.IsIncompleteTypeCheckEnvironment, + snapshot.UseScriptResolutionRules, + snapshot.LoadTime, + snapshot.UnresolvedReferences, + snapshot.OriginalLoadReferences, + snapshot.Stamp + ) let (|StartsWith|_|) (prefix: string) (s: string) = if s.StartsWith(prefix, StringComparison.Ordinal) then @@ -95,16 +103,15 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe /// ensures that any user-configured include/load files are added to the typechecking context - let addLoadedFiles (projectOptions: FSharpProjectOptions) = - let files = Array.append fsiAdditionalFiles projectOptions.SourceFiles + let addLoadedFiles (snapshot: FSharpProjectSnapshot) = + let files = List.append fsiAdditionalFiles snapshot.SourceFiles optsLogger.info ( Log.setMessage "Source file list is {files}" >> Log.addContextDestructured "files" files ) - { projectOptions with - SourceFiles = files } + snapshot.Replace(files) let (|Reference|_|) (opt: string) = if opt.StartsWith("-r:", StringComparison.Ordinal) then @@ -112,93 +119,20 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe else None - /// ensures that all file paths are absolute before being sent to the compiler, because compilation of scripts fails with relative paths - let resolveRelativeFilePaths (projectOptions: FSharpProjectOptions) = - { projectOptions with - SourceFiles = projectOptions.SourceFiles |> Array.map Path.GetFullPath - OtherOptions = - projectOptions.OtherOptions - |> Array.map (fun opt -> - match opt with - | Reference r -> $"-r:{Path.GetFullPath r}" - | opt -> opt) } - - let snapshotAccumulatorDef = - Dictionary() - - let optionsLocker = new SemaphoreSlim(1, 1) - - member _.FromOptions(options: FSharpProjectOptions array, documentSource: DocumentSource) = - asyncEx { - - let! ct = Async.CancellationToken - use! _lock = optionsLocker.LockAsync(ct) - snapshotAccumulatorDef.Clear() - // TODO: Figure out why we can't just recreate the subgraph of snapshots - // for opt in options do - // snapshotAccumulatorDef.Remove(opt) |> ignore - // checker.ClearCache(options) - return! - options - |> Array.map (fun opt -> - async { - let! sn = - FSharpProjectSnapshot.FromOptions( - opt, - (fun _ fileName -> - - FSharpFileSnapshot.CreateFromDocumentSource(fileName, documentSource) - |> async.Return), - snapshotAccumulator = snapshotAccumulatorDef - ) - - return opt, sn - }) - |> Async.Sequential - } - - member _.FromOption - ( - options: FSharpProjectOptions, - documentSource: DocumentSource, - ?snapshotAccumulator: Dictionary - ) = - asyncEx { - let! ct = Async.CancellationToken - use! _lock = optionsLocker.LockAsync(ct) - let useDefaultCache = snapshotAccumulator.IsNone - let snapshotAccumulator = defaultArg snapshotAccumulator snapshotAccumulatorDef - - if useDefaultCache then - snapshotAccumulator.Remove options |> ignore // We need to recreate the snapshot when files change - - let! sn = - FSharpProjectSnapshot.FromOptions( - options, - (fun _ fileName -> - - FSharpFileSnapshot.CreateFromDocumentSource(fileName, documentSource) - |> async.Return), - snapshotAccumulator = snapshotAccumulator - ) - - return sn - } - member __.DisableInMemoryProjectReferences with get () = disableInMemoryProjectReferences and set (value) = disableInMemoryProjectReferences <- value - static member GetDependingProjects (file: string) (options: seq) = + static member GetDependingProjects (file: string) (snapshots: seq) = let project = - options + snapshots |> Seq.tryFind (fun (k, _) -> (UMX.untag k).ToUpperInvariant() = (UMX.untag file).ToUpperInvariant()) project |> Option.map (fun (_, option) -> option, [ yield! - options + snapshots |> Seq.map snd |> Seq.distinctBy (fun o -> o.ProjectFileName) |> Seq.filter (fun o -> @@ -216,7 +150,7 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe let allFlags = Array.append [| "--targetprofile:mscorlib" |] fsiAdditionalArguments let! (opts, errors) = - checker.GetProjectOptionsFromScript( + checker.GetProjectSnapshotFromScript( UMX.untag file, source, assumeDotNetFramework = true, @@ -225,7 +159,7 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe userOpName = "getNetFrameworkScriptOptions" ) - let allModifications = addLoadedFiles >> resolveRelativeFilePaths + let allModifications = addLoadedFiles return allModifications opts, errors } @@ -240,8 +174,8 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe let allFlags = Array.append [| "--targetprofile:netstandard" |] fsiAdditionalArguments - let! (opts, errors) = - checker.GetProjectOptionsFromScript( + let! (snapshot, errors) = + checker.GetProjectSnapshotFromScript( UMX.untag file, source, assumeDotNetFramework = false, @@ -252,17 +186,17 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe ) optsLogger.trace ( - Log.setMessage "Got NetCore options {opts} for file {file} with errors {errors}" + Log.setMessage "Got NetCore snapshot {snapshot} for file {file} with errors {errors}" >> Log.addContextDestructured "file" file - >> Log.addContextDestructured "opts" opts + >> Log.addContextDestructured "snapshot" snapshot >> Log.addContextDestructured "errors" errors ) let allModifications = // filterBadRuntimeRefs >> - addLoadedFiles >> resolveRelativeFilePaths >> fixupFsharpCoreAndFSIPaths + addLoadedFiles >> fixupFsharpCoreAndFSIPaths - let modified = allModifications opts + let modified = allModifications snapshot optsLogger.trace ( Log.setMessage "Replaced options to {opts}" @@ -272,37 +206,10 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe return modified, errors } - member self.GetProjectOptionsFromScript(file: string, source, tfm) = - match tfm with - | FSIRefs.TFM.NetFx -> self.GetNetFxScriptOptions(file, source) - | FSIRefs.TFM.NetCore -> self.GetNetCoreScriptOptions(file, source) - - - member self.GetProjectSnapshotFromScript(file: string, source, tfm : FSIRefs.TFM) = async { - let _tfm = tfm - let allFlags = - Array.append [| "--targetprofile:netstandard" |] fsiAdditionalArguments - let! (snap, errors) = checker. GetProjectSnapshotFromScript( - UMX.untag file, - source, - assumeDotNetFramework = false, - useSdkRefs = true, - useFsiAuxLib = true, - otherFlags = allFlags, - userOpName = "getNetCoreScriptOptions" - ) - match errors with - | [] -> () - | errs -> - optsLogger.info ( - Log.setLogLevel LogLevel.Error - >> Log.setMessage "Resolved {opts} with {errors}" - >> Log.addContextDestructured "opts" snap - >> Log.addContextDestructured "errors" errs - ) - - return snap - } + member self.GetProjectOptionsFromScript(file: string, source, tfm: FSIRefs.TFM) = + match tfm with + | FSIRefs.TFM.NetFx -> self.GetNetFxScriptOptions(file, source) + | FSIRefs.TFM.NetCore -> self.GetNetCoreScriptOptions(file, source) member __.ScriptTypecheckRequirementsChanged = @@ -312,57 +219,51 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe member _.ClearCache(snap : FSharpProjectSnapshot seq) = snap - |> Seq.map(_.Identifier) + |> Seq.map(fun x -> x.Identifier) |> checker.ClearCache /// This function is called when the entire environment is known to have changed for reasons not encoded in the ProjectOptions of any project/compilation. member _.ClearCaches() = lastCheckResults.Dispose() lastCheckResults <- memoryCache () - snapshotAccumulatorDef.Clear() checker.InvalidateAll() checker.ClearLanguageServiceRootCachesAndCollectAndFinalizeAllTransients() /// Parses a source code for a file and caches the results. Returns an AST that can be traversed for various features. /// The path for the file. The file name is used as a module name for implicit top level modules (e.g. in scripts). /// The source to be parsed. - /// Parsing options for the project or script. + /// Parsing options for the project or script. /// - member x.ParseFile(filePath: string, source: ISourceText, options: FSharpProjectSnapshot) = + member x.ParseFile(filePath: string, source: ISourceText, snapshot: FSharpProjectSnapshot) = async { let _source = source - checkerLogger.info ( Log.setMessage "ParseFile - {file}" >> Log.addContextDestructured "file" filePath ) let path = UMX.untag filePath - // let! snapshot = x.FromOption(options, documentSource) - return! checker.ParseFile(path, options) + return! checker.ParseFile(path, snapshot) } /// Parse and check a source code file, returning a handle to the results /// The name of the file in the project whose source is being checked. /// An integer that can be used to indicate the version of the file. This will be returned by TryGetRecentCheckResultsForFile when looking up the file /// The source for the file. - /// The options for the project or script. + /// The snapshot for the project or script. /// Determines if the typecheck should be cached for autocompletions. - /// A dictionary to store the snapshots for the project options /// Note: all files except the one being checked are read from the FileSystem API /// Result of ParseAndCheckResults - member x.ParseAndCheckFileInProject + member _.ParseAndCheckFileInProject ( filePath: string, version: int, source: ISourceText, - options: FSharpProjectSnapshot, - ?shouldCache: bool, - ?snapshotAccumulator: Dictionary + snapshot: FSharpProjectSnapshot, + ?shouldCache: bool ) = asyncResult { let _source = source - let _snapshotAccumulator = snapshotAccumulator let _version = version let shouldCache = defaultArg shouldCache false let opName = sprintf "ParseAndCheckFileInProject - %A" filePath @@ -372,8 +273,7 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe let path = UMX.untag filePath try - // let! snapshot = x.FromOptions(options, documentSource, ?snapshotAccumulator = snapshotAccumulator) - let! (p, c) = checker.ParseAndCheckFileInProject(path, options, userOpName = opName) + let! (p, c) = checker.ParseAndCheckFileInProject(path, snapshot, userOpName = opName) let parseErrors = p.Diagnostics |> Array.map (fun p -> p.Message) @@ -429,10 +329,10 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe | (true, v) -> Some v | _ -> None - member x.TryGetRecentCheckResultsForFile + member _.TryGetRecentCheckResultsForFile ( file: string, - options: FSharpProjectSnapshot, + snapshot: FSharpProjectSnapshot, source: ISourceText ) = async { @@ -447,7 +347,7 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe return - checker.TryGetRecentCheckResultsForFile(UMX.untag file, options, opName) + checker.TryGetRecentCheckResultsForFile(UMX.untag file, snapshot, opName) |> Option.map (fun (pr, cr) -> checkerLogger.info ( Log.setMessage "{opName} - got results - {version}" @@ -457,10 +357,10 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe ParseAndCheckResults(pr, cr, entityCache)) } - member x.GetUsesOfSymbol + member _.GetUsesOfSymbol ( file: string, - options: (string * FSharpProjectSnapshot) seq, + snapshots: (string * FSharpProjectSnapshot) seq, symbol: FSharpSymbol ) = async { @@ -469,10 +369,9 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe >> Log.addContextDestructured "file" file ) - match FSharpCompilerServiceChecker.GetDependingProjects file options with + match FSharpCompilerServiceChecker.GetDependingProjects file snapshots with | None -> return [||] | Some(opts, []) -> - // let! snapshot = x.FromOption(opts, documentSource) let! res = checker.ParseAndCheckProject(opts) return res.GetUsesOfSymbol symbol | Some(opts, dependentProjects) -> @@ -480,8 +379,6 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe opts :: dependentProjects |> List.map (fun (opts) -> async { - - // let! snapshot = x.FromOption(opts, documentSource) let! res = checker.ParseAndCheckProject(opts) return res.GetUsesOfSymbol symbol }) @@ -490,27 +387,16 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe return res |> Array.concat } - member x.FindReferencesForSymbolInFile(file, project: FSharpProjectSnapshot, symbol) = + member _.FindReferencesForSymbolInFile(file, project: FSharpProjectSnapshot, symbol) = async { checkerLogger.info ( Log.setMessage "FindReferencesForSymbolInFile - {file}" >> Log.addContextDestructured "file" file ) - // let! snapshot = x.FromOption(project, documentSource) return! checker.FindBackgroundReferencesInFile(file, project, symbol, userOpName = "find references") } - // member this.GetDeclarations(fileName: string, source: ISourceText, options: FSharpProjectOptions, _) = - // async { - // checkerLogger.info ( - // Log.setMessage "GetDeclarations - {file}" - // >> Log.addContextDestructured "file" fileName - // ) - - // let! parseResult = this.ParseFile(fileName, source, options) - // return parseResult.GetNavigationItems().Declarations - // } member __.SetDotnetRoot(dotnetBinary: FileInfo, cwd: DirectoryInfo) = match Ionide.ProjInfo.SdkDiscovery.versionAt cwd dotnetBinary with @@ -549,5 +435,8 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe else let additionalArgs, files = processFSIArgs args fsiAdditionalArguments <- additionalArgs - fsiAdditionalFiles <- files + fsiAdditionalFiles <- + files + |> Array.map (fun f -> FSharpFileSnapshot.CreateFromFileSystem(System.IO.Path.GetFullPath f)) + |> Array.toList scriptTypecheckRequirementsChanged.Trigger() diff --git a/src/FsAutoComplete.Core/CompilerServiceInterface.fsi b/src/FsAutoComplete.Core/CompilerServiceInterface.fsi index 8e454b16a..9ac41fcee 100644 --- a/src/FsAutoComplete.Core/CompilerServiceInterface.fsi +++ b/src/FsAutoComplete.Core/CompilerServiceInterface.fsi @@ -5,6 +5,7 @@ open System.Collections.Generic open FSharp.Compiler.CodeAnalysis open Utils open FSharp.Compiler.Text +open FsAutoComplete.Logging open Ionide.ProjInfo.ProjectSystem open FSharp.UMX open FSharp.Compiler.EditorServices @@ -22,15 +23,11 @@ type FSharpCompilerServiceChecker = static member GetDependingProjects: file : string -> - options: seq + snapshots: seq -> option> member GetProjectOptionsFromScript: - file: string * source: ISourceText * tfm: FSIRefs.TFM -> - Async - - member GetProjectSnapshotFromScript: - file: string * source: ISourceTextNew * tfm: FSIRefs.TFM -> Async + file: string * source: ISourceTextNew * tfm: FSIRefs.TFM -> Async member ScriptTypecheckRequirementsChanged: IEvent @@ -43,40 +40,29 @@ type FSharpCompilerServiceChecker = /// This function is called when the entire environment is known to have changed for reasons not encoded in the ProjectOptions of any project/compilation. member ClearCaches: unit -> unit - member FromOptions: - options: FSharpProjectOptions array * documentSource: DocumentSource -> - Async<(FSharpProjectOptions * FSharpProjectSnapshot) array> - - member FromOption: - options: FSharpProjectOptions * - documentSource: DocumentSource * - ?snapshotAccumulator: Dictionary -> - Async /// Parses a source code for a file and caches the results. Returns an AST that can be traversed for various features. /// The path for the file. The file name is used as a module name for implicit top level modules (e.g. in scripts). /// The source to be parsed. - /// Parsing options for the project or script. + /// Parsing options for the project or script. /// member ParseFile: - filePath: string * source: ISourceText * options: FSharpProjectSnapshot -> Async + filePath: string * source: ISourceText * snapshot: FSharpProjectSnapshot -> Async /// Parse and check a source code file, returning a handle to the results /// The name of the file in the project whose source is being checked. /// An integer that can be used to indicate the version of the file. This will be returned by TryGetRecentCheckResultsForFile when looking up the file /// The source for the file. - /// The options for the project or script. + /// The options for the project or script. /// Determines if the typecheck should be cached for autocompletions. - /// A dictionary of FSharpProjectOptions to FSharpProjectSnapshot that will be used to accumulate snapshots for the project. This is used to avoid re-reading the project file from disk for every file in the project. /// Note: all files except the one being checked are read from the FileSystem API /// Result of ParseAndCheckResults member ParseAndCheckFileInProject: filePath: string * version: int * source: ISourceText * - options: FSharpProjectSnapshot * - ?shouldCache: bool * - ?snapshotAccumulator: Dictionary -> + snapshot: FSharpProjectSnapshot * + ?shouldCache: bool -> Async> /// @@ -89,17 +75,17 @@ type FSharpCompilerServiceChecker = member TryGetLastCheckResultForFile: file: string -> ParseAndCheckResults option member TryGetRecentCheckResultsForFile: - file: string * options: FSharpProjectSnapshot * source: ISourceText -> Async + file: string * snapshot: FSharpProjectSnapshot * source: ISourceText -> Async member GetUsesOfSymbol: - file: string * options: (string * FSharpProjectSnapshot) seq * symbol: FSharpSymbol -> + file: string * snapshots: (string * FSharpProjectSnapshot) seq * symbol: FSharpSymbol -> Async member FindReferencesForSymbolInFile: file: string * project: FSharpProjectSnapshot * symbol: FSharpSymbol -> Async> // member GetDeclarations: - // fileName: string * source: ISourceText * options: FSharpProjectOptions * version: 'a -> + // fileName: string * source: ISourceText * snapshot: FSharpProjectOptions * version: 'a -> // Async member SetDotnetRoot: dotnetBinary: FileInfo * cwd: DirectoryInfo -> unit diff --git a/src/FsAutoComplete.Core/FCSPatches.fs b/src/FsAutoComplete.Core/FCSPatches.fs index 0b67b32d2..91ccb15d5 100644 --- a/src/FsAutoComplete.Core/FCSPatches.fs +++ b/src/FsAutoComplete.Core/FCSPatches.fs @@ -282,7 +282,10 @@ module LanguageVersionShim = /// let defaultLanguageVersion = lazy (LanguageVersionShim("latest")) - let fromOtherOptions (options : string seq) = + /// Tries to parse out "--langversion:" from OtherOptions if it can't find it, returns defaultLanguageVersion + /// The OtherOptions to use + /// A LanguageVersionShim from the parsed "--langversion:" or defaultLanguageVersion + let fromOtherOptions (options: string seq) = options |> Seq.tryFind (fun x -> x.StartsWith("--langversion:", StringComparison.Ordinal)) |> Option.map (fun x -> x.Split(":")[1]) @@ -292,14 +295,10 @@ module LanguageVersionShim = /// Tries to parse out "--langversion:" from OtherOptions if it can't find it, returns defaultLanguageVersion /// The FSharpProjectOptions to use /// A LanguageVersionShim from the parsed "--langversion:" or defaultLanguageVersion - let fromFSharpProjectOptions (fpo: FSharpProjectOptions) = - fpo.OtherOptions - |> fromOtherOptions + let fromFSharpProjectOptions (fpo: FSharpProjectOptions) = fpo.OtherOptions |> fromOtherOptions /// Tries to parse out "--langversion:" from OtherOptions if it can't find it, returns defaultLanguageVersion /// The FSharpProjectOptions to use /// A LanguageVersionShim from the parsed "--langversion:" or defaultLanguageVersion - let fromFSharpProjectSnapshot (fpo: FSharpProjectSnapshot) = - fpo.OtherOptions - |> fromOtherOptions + let fromFSharpProjectSnapshot (fpo: FSharpProjectSnapshot) = fpo.OtherOptions |> fromOtherOptions diff --git a/src/FsAutoComplete.Core/FileSystem.fs b/src/FsAutoComplete.Core/FileSystem.fs index f58b44b0d..e6ddef021 100644 --- a/src/FsAutoComplete.Core/FileSystem.fs +++ b/src/FsAutoComplete.Core/FileSystem.fs @@ -385,8 +385,9 @@ module RoslynSourceText = member _.CopyTo(sourceIndex, destination, destinationIndex, count) = sourceText.CopyTo(sourceIndex, destination, destinationIndex, count) + interface ISourceTextNew with - member this.GetChecksum() = sourceText.GetChecksum() + member this.GetChecksum() = sourceText.GetChecksum() type ISourceTextFactory = abstract member Create: fileName: string * text: string -> IFSACSourceText diff --git a/src/FsAutoComplete.Core/SymbolLocation.fs b/src/FsAutoComplete.Core/SymbolLocation.fs index 39ad76eb7..e45ad01e7 100644 --- a/src/FsAutoComplete.Core/SymbolLocation.fs +++ b/src/FsAutoComplete.Core/SymbolLocation.fs @@ -17,7 +17,7 @@ let getDeclarationLocation currentDocument: IFSACSourceText, getProjectOptions, projectsThatContainFile: string -> Async, - getDependentProjectsOfProjects: FSharpProjectSnapshot list -> Async + getDependentProjectsOfProjects: FSharpProjectSnapshot list -> FSharpProjectSnapshot list ) : Async> = asyncOption { @@ -60,7 +60,7 @@ let getDeclarationLocation match! projectsThatContainFile (taggedFilePath) with | [] -> return! None | projectsThatContainFile -> - let! projectsThatDependOnContainingProjects = getDependentProjectsOfProjects projectsThatContainFile + let projectsThatDependOnContainingProjects = getDependentProjectsOfProjects projectsThatContainFile match projectsThatDependOnContainingProjects with | [] -> return (SymbolDeclarationLocation.Projects(projectsThatContainFile, isSymbolLocalForProject)) diff --git a/src/FsAutoComplete/CodeFixes.fs b/src/FsAutoComplete/CodeFixes.fs index cc6daf732..04fa17760 100644 --- a/src/FsAutoComplete/CodeFixes.fs +++ b/src/FsAutoComplete/CodeFixes.fs @@ -34,8 +34,7 @@ module Types = type GetLanguageVersion = string -> Async - type GetProjectOptionsForFile = - string -> Async> + type GetProjectOptionsForFile = string -> Async> [] type FixKind = @@ -355,7 +354,11 @@ module Run = | Ok projectOptions -> let signatureFile = System.String.Concat(fileName, "i") - let hasSig = projectOptions.SourceFiles |> Array.contains signatureFile + + let hasSig = + projectOptions.SourceFiles + |> List.map (fun x -> x.FileName) + |> List.contains signatureFile if not hasSig then return Ok [] diff --git a/src/FsAutoComplete/CodeFixes.fsi b/src/FsAutoComplete/CodeFixes.fsi index cf8b6c009..a3389d7bf 100644 --- a/src/FsAutoComplete/CodeFixes.fsi +++ b/src/FsAutoComplete/CodeFixes.fsi @@ -1,13 +1,15 @@ namespace FsAutoComplete.CodeFix +#nowarn "57" + open FsAutoComplete open FsAutoComplete.LspHelpers open Ionide.LanguageServerProtocol.Types open FsAutoComplete.Logging open FSharp.UMX -open FsToolkit.ErrorHandling open FSharp.Compiler.Text open FsAutoComplete.FCSPatches +open FSharp.Compiler.CodeAnalysis.ProjectSnapshot module FcsRange = FSharp.Compiler.Text.Range type FcsRange = FSharp.Compiler.Text.Range @@ -27,8 +29,7 @@ module Types = type GetLanguageVersion = string -> Async - type GetProjectOptionsForFile = - string -> Async> + type GetProjectOptionsForFile = string -> Async> [] type FixKind = diff --git a/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs b/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs index dd0e98024..6036f5c39 100644 --- a/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs +++ b/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs @@ -51,7 +51,9 @@ let codeFixForImplementationFileWithSignature | Ok projectOptions -> let signatureFile = String.Concat(fileName, "i") - let hasSig = projectOptions.SourceFiles |> List.exists(fun s -> s.FileName = signatureFile ) + + let hasSig = + projectOptions.SourceFiles |> List.exists (fun s -> s.FileName = signatureFile) if not hasSig then return Ok [] diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index ffcab80b2..02cde1f8b 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -36,11 +36,9 @@ open FsAutoComplete.FCSPatches open FsAutoComplete.Lsp open FsAutoComplete.Lsp.Helpers open FSharp.Compiler.Syntax -open FSharp.Compiler.CodeAnalysis open FsAutoComplete.ProjectWorkspace - [] type WorkspaceChosen = | Projs of HashSet> @@ -52,9 +50,11 @@ type AdaptiveWorkspaceChosen = | NotChosen + [] module Helpers3 = open FSharp.Compiler.CodeAnalysis.ProjectSnapshot + type FSharpReferencedProjectSnapshot with member x.ProjectFilePath = @@ -64,8 +64,7 @@ module Helpers3 = [] type LoadedProject = - { - ProjectOptions : Types.ProjectOptions + { ProjectOptions: Types.ProjectOptions FSharpProjectSnapshot: aval LanguageVersion: LanguageVersionShim } @@ -80,11 +79,10 @@ type LoadedProject = | :? LoadedProject as other -> (x :> IEquatable<_>).Equals other | _ -> false - member x.SourceFiles = x.ProjectOptions.SourceFiles |> List.map(fun f -> f) |> List.toArray - member x.ProjectFileName = x.ProjectOptions.ProjectFileName - // static member op_Implicit(x: LoadedProject) = x.FSharpProjectSnapshot - + member x.SourceFiles = + x.ProjectOptions.SourceFiles |> List.map (fun f -> f) |> List.toArray + member x.ProjectFileName = x.ProjectOptions.ProjectFileName /// The reality is a file can be in multiple projects /// This is extracted to make it easier to do some type of customized select in the future @@ -114,12 +112,10 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let rootPath = cval None let config = cval FSharpConfig.Default - let documentSource = cval DocumentSource.FileSystem let checker = - - (config, documentSource) - ||> AVal.map2 (fun c _ds -> c.EnableAnalyzers, c.Fsac.CachedTypeCheckCount, c.Fsac.ParallelReferenceResolution) + config + |> AVal.map (fun c -> c.EnableAnalyzers, c.Fsac.CachedTypeCheckCount, c.Fsac.ParallelReferenceResolution) |> AVal.map (FSharpCompilerServiceChecker) let configChanges = @@ -427,7 +423,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac checkUnnecessaryParentheses ] async { - do! analyzers |> Async.Sequential |> Async.Ignore + do! analyzers |> Async.parallel75 |> Async.Ignore do! lspClient.NotifyDocumentAnalyzed @@ -479,18 +475,14 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac Loggers.analyzers.error (Log.setMessageI $"Run failed for {file:file}" >> Log.addExn ex) } - let analyzerLock = new SemaphoreSlim(1, 1) - do disposables.Add <| fileChecked.Publish.Subscribe(fun (parseAndCheck, volatileFile, ct) -> if volatileFile.Source.Length = 0 then () // Don't analyze and error on an empty file else - asyncEx { + async { let config = config |> AVal.force - let! ct = Async.CancellationToken - use! _lock = analyzerLock.LockAsync(ct) do! builtInCompilerAnalyzers config volatileFile parseAndCheck do! runAnalyzers config parseAndCheck volatileFile @@ -731,11 +723,11 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac |> AVal.map (fun writeTime -> filePath, writeTime) let readFileFromDisk lastTouched (file: string) = - cancellableValueTask { + async { if File.Exists(UMX.untag file) then use s = File.openFileStreamForReadingAsync file - let! source = sourceTextFactory.Create(file, s) + let! source = sourceTextFactory.Create(file, s) |> Async.AwaitCancellableValueTask return { LastTouched = lastTouched @@ -788,7 +780,6 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac { new IDisposable with member this.Dispose() : unit = () } - let adaptiveWorkspacePaths = workspacePaths |> AVal.map (fun wsp -> @@ -834,10 +825,16 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac |> Option.map (fun v -> v.Split(';', StringSplitOptions.RemoveEmptyEntries)) - let loadProjects (loader : IWorkspaceLoader) binlogConfig projects = + + + let loadProjects (loader: IWorkspaceLoader) binlogConfig projects = + logger.info (Log.setMessageI $"Enter loading projects") + projects |> AMap.mapWithAdditionalDependencies (fun projects -> + logger.info (Log.setMessageI $"Enter loading projects mapWithAdditionalDependencies") + projects |> Seq.iter (fun (proj: string, _) -> let not = @@ -859,6 +856,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac Log.setMessage "Found BaseIntermediateOutputPath of {path}" >> Log.addContextDestructured "path" ((|BaseIntermediateOutputPath|_|) p.Properties) ) + let projectFileName = p.ProjectFileName let projViewerItemsNormalized = ProjectViewer.render p @@ -869,8 +867,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac |> List.choose (function | ProjectViewerItem.Compile(p, _) -> Some p) - let references = - FscArguments.references (p.OtherOptions) + let references = FscArguments.references (p.OtherOptions) logger.info ( Log.setMessage "ProjectLoaded {file}" @@ -918,8 +915,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac HashMap.ofList [ for p in projectOptions do - UMX.tag p.ProjectFileName, (p, additionalDependencies p) ] - ) + UMX.tag p.ProjectFileName, (p, additionalDependencies p) ]) @@ -1012,197 +1008,97 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac return file }) - let snapshots = + let snapshots: amap, (Types.ProjectOptions * aval)> = amap { - let! (loader, wsp, binlogConfig) = aval { - let! loader = - loader - |> addAValLogging (fun () -> logger.info (Log.setMessage "Loading projects because WorkspaceLoader change")) - - and! wsp = - adaptiveWorkspacePaths - |> addAValLogging (fun () -> - logger.info (Log.setMessage "Loading projects because adaptiveWorkspacePaths change")) - - and! binlogConfig = - // AVal.constant Ionide.ProjInfo.BinaryLogGeneration.Off - binlogConfig - |> addAValLogging (fun () -> logger.info (Log.setMessage "Loading projects because binlogConfig change")) - - return loader, wsp, binlogConfig - } + let! (loader, wsp, binlogConfig) = + aval { + let! loader = + loader + |> addAValLogging (fun () -> logger.info (Log.setMessage "Loading projects because WorkspaceLoader change")) + + and! wsp = + adaptiveWorkspacePaths + |> addAValLogging (fun () -> + logger.info (Log.setMessage "Loading projects because adaptiveWorkspacePaths change")) + + and! binlogConfig = + // AVal.constant Ionide.ProjInfo.BinaryLogGeneration.Off + binlogConfig + |> addAValLogging (fun () -> logger.info (Log.setMessage "Loading projects because binlogConfig change")) + + return loader, wsp, binlogConfig + } match wsp with | AdaptiveWorkspaceChosen.NotChosen -> () | AdaptiveWorkspaceChosen.Projs projects -> - let projects = - loadProjects loader binlogConfig projects - // |> AMap.map' AVal.constant + let projects = loadProjects loader binlogConfig projects + + logger.info (Log.setMessageI $"After loading projects and before creating snapshots") yield! Snapshots.createSnapshots openFilesWithChanges (AVal.constant sourceTextFactory) projects } - let loadedProjectSnapshots2 = + let loadedProjects = snapshots - |> AMap.map(fun _ (proj, snap) -> - { - ProjectOptions = proj - FSharpProjectSnapshot = snap - LanguageVersion = LanguageVersionShim.fromOtherOptions proj.OtherOptions - } - ) + |> AMap.map (fun _ (proj, snap) -> + { ProjectOptions = proj + FSharpProjectSnapshot = snap + LanguageVersion = LanguageVersionShim.fromOtherOptions proj.OtherOptions }) + + let getAllLoadedProjects = + loadedProjects + |> AMap.mapA (fun _ v -> v.FSharpProjectSnapshot |> AVal.map (fun _ -> v)) + |> AMap.toAVal + |> AVal.map HashMap.toValueList - let loadedProjectSnapshots = - loadedProjectSnapshots2 - |> AMap.mapA (fun _ v -> v.FSharpProjectSnapshot |> AVal.map(fun _ -> v)) - |> AMap.toAVal - |> AVal.map HashMap.toValueList - - - // let loadedProjectOptions = - // aval { - // let! loader = - // loader - // |> addAValLogging (fun () -> logger.info (Log.setMessage "Loading projects because WorkspaceLoader change")) - - // and! wsp = - // adaptiveWorkspacePaths - // |> addAValLogging (fun () -> - // logger.info (Log.setMessage "Loading projects because adaptiveWorkspacePaths change")) - - // and! binlogConfig = - // binlogConfig - // |> addAValLogging (fun () -> logger.info (Log.setMessage "Loading projects because binlogConfig change")) - - // match wsp with - // | AdaptiveWorkspaceChosen.NotChosen -> return [] - // | AdaptiveWorkspaceChosen.Projs projects -> - // let! projectOptions = - // loadProjects loader binlogConfig projects - // |> AMap.toAVal - // |> AVal.map HashMap.toValueList - - // and! checker = checker - // checker.ClearCaches() // if we got new projects assume we're gonna need to clear caches - - // let options = - // let fsharpOptions = projectOptions |> FCS.mapManyOptions |> Seq.toList - - // List.zip projectOptions fsharpOptions - // |> List.map (fun (projectOption, fso) -> - - // let langversion = LanguageVersionShim.fromFSharpProjectOptions fso - - // // Set some default values as FCS uses these for identification/caching purposes - // let fso = - // { fso with - // SourceFiles = fso.SourceFiles |> Array.map (Utils.normalizePath >> UMX.untag) - // Stamp = fso.Stamp |> Option.orElse (Some DateTime.UtcNow.Ticks) - // ProjectId = fso.ProjectId |> Option.orElse (Some(Guid.NewGuid().ToString())) } - - // { FSharpProjectOptions = fso - // LanguageVersion = langversion }, - // projectOption) - - // options - // |> List.iter (fun (loadedProject, projectOption) -> - // let projectFileName = loadedProject.ProjectFileName - // let projViewerItemsNormalized = ProjectViewer.render projectOption - - // let responseFiles = - // projViewerItemsNormalized.Items - // |> List.map (function - // | ProjectViewerItem.Compile(p, c) -> ProjectViewerItem.Compile(Helpers.fullPathNormalized p, c)) - // |> List.choose (function - // | ProjectViewerItem.Compile(p, _) -> Some p) - - // let references = - // FscArguments.references (loadedProject.FSharpProjectOptions.OtherOptions |> List.ofArray) - - // logger.info ( - // Log.setMessage "ProjectLoaded {file}" - // >> Log.addContextDestructured "file" projectFileName - // ) - - // let ws = - // { ProjectFileName = projectFileName - // ProjectFiles = responseFiles - // OutFileOpt = Option.ofObj projectOption.TargetPath - // References = references - // Extra = projectOption - // ProjectItems = projViewerItemsNormalized.Items - // Additionals = Map.empty } - - // let not = ProjectResponse.Project(ws, false) |> NotificationEvent.Workspace - // notifications.Trigger(not, CancellationToken.None)) - - // let not = ProjectResponse.WorkspaceLoad true |> NotificationEvent.Workspace - - // notifications.Trigger(not, CancellationToken.None) - - // return options |> List.map fst - // } /// - /// Evaluates the adaptive value and returns its current value. + /// Evaluates the adaptive value and returns its current value. /// This should not be used inside the adaptive evaluation of other AdaptiveObjects since it does not track dependencies. /// /// A list of FSharpProjectOptions - let forceLoadProjects () = loadedProjectSnapshots |> AVal.force + let forceLoadProjects () = getAllLoadedProjects |> AVal.force // do // // Reload Projects with some debouncing if `loadedProjectOptions` is out of date. - // AVal.Observable.onOutOfDateWeak loadedProjectSnapshots + // AVal.Observable.onOutOfDateWeak loadedProjectOptions // |> Observable.throttleOn Concurrency.NewThreadScheduler.Default (TimeSpan.FromMilliseconds(200.)) // |> Observable.observeOn Concurrency.NewThreadScheduler.Default - // |> Observable.subscribe (fun _ -> forceLoadProjects () |> ignore )//|> Async.Ignore> |> Async.Start) + // |> Observable.subscribe (fun _ -> forceLoadProjects () |> ignore>) // |> disposables.Add - // let sourceFileToProjectOptions = - - - // amap { - // let! snaps = loadedProjectSnapshots - // yield! - // snaps - // |> List.collect (fun proj -> - // proj.SourceFiles - // |> Array.toList - // |> List.map (fun source -> Utils.normalizePath source, proj) - // ) - // |> List.groupByFst - - // } - let sourceFileToProjectOptions = amap { - let! snaps = loadedProjectSnapshots2 |> AMap.toAVal + let! snaps = loadedProjects |> AMap.toAVal + yield! snaps |> HashMap.toList |> List.collect (fun (_, proj) -> proj.SourceFiles |> Array.toList - |> List.map (fun source -> Utils.normalizePath source, (proj) - ) - |> List.groupByFst) + |> List.map (fun source -> Utils.normalizePath source, (proj)) + |> List.groupByFst) } - - - let cancelToken filePath version (cts: CancellationTokenSource) = - logger.info ( - Log.setMessage "Cancelling {filePath} - {version}" - >> Log.addContextDestructured "filePath" filePath - >> Log.addContextDestructured "version" version - ) + try + logger.info ( + Log.setMessage "Cancelling {filePath} - {version}" + >> Log.addContextDestructured "filePath" filePath + >> Log.addContextDestructured "version" version + ) - cts.TryCancel() - cts.TryDispose() - + cts.Cancel() + cts.Dispose() + with + | :? OperationCanceledException + | :? ObjectDisposedException as e when e.Message.Contains("CancellationTokenSource has been disposed") -> + // ignore if already cancelled + () let resetCancellationToken (filePath: string) version = let adder _ = new CancellationTokenSource() @@ -1211,7 +1107,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac cancelToken filePath version value new CancellationTokenSource() - openFilesTokens.AddOrUpdate(filePath, adder, updater) + openFilesTokens.AddOrUpdate(filePath, adder, updater).Token let updateOpenFiles (file: VolatileFile) = @@ -1219,7 +1115,8 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let updater _ (v: cval<_>) = v.Value <- file - resetCancellationToken file.FileName file.Version |> ignore + resetCancellationToken file.FileName file.Version |> ignore + transact (fun () -> openFiles.AddOrElse(file.Source.FileName, adder, updater)) let updateTextChanges filePath ((changes: DidChangeTextDocumentParams, _) as p) = @@ -1227,123 +1124,59 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let adder _ = cset<_> [ p ] let updater _ (v: cset<_>) = v.Add p |> ignore - resetCancellationToken filePath changes.TextDocument.Version |> ignore + resetCancellationToken filePath changes.TextDocument.Version + |> ignore + transact (fun () -> textChanges.AddOrElse(filePath, adder, updater)) let isFileOpen file = openFiles |> AMap.tryFindA file |> AVal.map (Option.isSome) + let findFileInOpenFiles file = openFilesWithChanges |> AMap.tryFindA file + let forceFindOpenFile filePath = findFileInOpenFiles filePath |> AVal.force - let openFilesToChangesAndProjectOptions = - openFilesWithChanges - |> AMapAsync.mapAVal (fun filePath file ctok -> - asyncAVal { - if Utils.isAScript (UMX.untag filePath) then - let! (checker: FSharpCompilerServiceChecker) = checker - and! tfmConfig = tfmConfig - - let! projs = - asyncResult { - let cts = getOpenFileTokenOrDefault filePath - use linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ctok, cts) - - try - let! (opts, errors) = - checker.GetProjectOptionsFromScript(filePath, file.Source, tfmConfig) - |> Async.withCancellation linkedCts.Token - opts |> scriptFileProjectOptions.Trigger - let diags = errors |> Array.ofList |> Array.map fcsErrorToDiagnostic - - diagnosticCollections.SetFor( - Path.LocalPathToUri filePath, - "F# Script Project Options", - file.Version, - diags - ) - - return - { FSharpProjectOptions = opts - LanguageVersion = LanguageVersionShim.fromFSharpProjectOptions opts } - |> List.singleton - with e -> - logger.error ( - Log.setMessage "Error getting project options for {filePath}" - >> Log.addContextDestructured "filePath" filePath - >> Log.addExn e - ) - - return! Error $"Error getting project options for {filePath} - {e.Message}" - } - - return file, projs - else - - let! projs = - sourceFileToProjectOptions - |> AMap.tryFindR - $"Couldn't find {filePath} in LoadedProjects. Have the projects loaded yet or have you tried restoring your project/solution?" - filePath - - return file, projs - }) - - let allFSharpFilesAndProjectOptions = - // asyncAVal { - let wins = - openFilesToChangesAndProjectOptions - |> AMap.map (fun _k v -> v |> AsyncAVal.mapSync (fun (file, projects) _ -> file, projects)) - - // let! sourceFileToProjectOptions = sourceFileToProjectOptions2 - - let loses = - sourceFileToProjectOptions - |> AMap.map (fun filePath v -> - asyncAVal { - let! file = getLatestFileChange filePath - return (file, Ok v) - }) - - // return - AMap.union loses wins - // } + let forceFindOpenFileOrRead file = + asyncOption { + match findFileInOpenFiles file |> AVal.force with + | Some s -> return s + | None -> + // TODO: Log how many times this kind area gets hit and possibly if this should be rethought + try + logger.debug ( + Log.setMessage "forceFindOpenFileOrRead else - {file}" + >> Log.addContextDestructured "file" file + ) + let lastTouched = File.getLastWriteTimeOrDefaultNow file - let forceFindOpenFileOrRead (file: string) : Async> = - asyncOption { - // let allFSharpFilesAndProjectOptions = allFSharpFilesAndProjectOptions + return! readFileFromDisk lastTouched file - let! (file, _) = - allFSharpFilesAndProjectOptions - |> AMapAsync.tryFindA file - |> AsyncAVal.forceAsync + with e -> + logger.warn ( + Log.setMessage "Could not read file {file}" + >> Log.addContextDestructured "file" file + >> Log.addExn e + ) - return file + return! None } |> Async.map (Result.ofOption (fun () -> $"Could not read file: {file}")) - // let documentSourceLookup (filePath: string) = - // asyncOption { - // let! file = forceFindOpenFileOrRead (Utils.normalizePath filePath) - // let! file = Result.toOption file - // return file.Source :> ISourceText - // } - - // do transact (fun () -> documentSource.Value <- DocumentSource.Custom documentSourceLookup) - // let fileShimChanges = openFilesWithChanges |> AMap.mapA (fun _ v -> v) - // // let cachedFileContents = cachedFileContents |> cmap.mapA (fun _ v -> v) + do + let fileShimChanges = openFilesWithChanges |> AMap.mapA (fun _ v -> v) + // let cachedFileContents = cachedFileContents |> cmap.mapA (fun _ v -> v) - // let filesystemShim file = - // // GetLastWriteTimeShim gets called _a lot_ and when we do checks on save we use Async.Parallel for type checking. - // // Adaptive uses lots of locks under the covers, so many threads can get blocked waiting for data. - // // flattening openFilesWithChanges makes this check a lot quicker as it's not needing to recalculate each value. + let filesystemShim file = + // GetLastWriteTimeShim gets called _a lot_ and when we do checks on save we use Async.Parallel for type checking. + // Adaptive uses lots of locks under the covers, so many threads can get blocked waiting for data. + // flattening openFilesWithChanges makes this check a lot quicker as it's not needing to recalculate each value. - // fileShimChanges |> AMap.force |> HashMap.tryFind file + fileShimChanges |> AMap.force |> HashMap.tryFind file - // do - // FSharp.Compiler.IO.FileSystemAutoOpens.FileSystem <- - // FileSystem(FSharp.Compiler.IO.FileSystemAutoOpens.FileSystem, filesystemShim) + FSharp.Compiler.IO.FileSystemAutoOpens.FileSystem <- + FileSystem(FSharp.Compiler.IO.FileSystemAutoOpens.FileSystem, filesystemShim) /// Parses a source code for a file and caches the results. Returns an AST that can be traversed for various features. /// The FSharpCompilerServiceChecker. @@ -1360,26 +1193,24 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac } - /// Parses all files in the workspace. This is mostly used to trigger finding tests. let parseAllFiles () = asyncAVal { - let! projects = loadedProjectSnapshots + let! projects = getAllLoadedProjects and! (checker: FSharpCompilerServiceChecker) = checker + let! projects = projects |> List.map (fun p -> p.FSharpProjectSnapshot) - // |> List.toArray |> ASet.ofList |> ASet.mapA id |> ASet.toAVal - // |> fromOpts return projects |> HashSet.toArray - |> Array.collect (fun (snap) -> snap.SourceFiles |> List.toArray |> Array.map (fun s -> snap, s)) + |> Array.collect (fun (snap) -> snap.SourceFiles |> List.toArray |> Array.map (fun s -> snap, s)) |> Array.map (fun (snap, fileName) -> let fileName = UMX.tag fileName.FileName @@ -1394,58 +1225,146 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let forceFindSourceText filePath = forceFindOpenFileOrRead filePath |> AsyncResult.map (fun f -> f.Source) - let allFilesToFSharpProjectOptions = - // asyncAVal { - // let! allFSharpFilesAndProjectOptions = allFSharpFilesAndProjectOptions + let openFilesToChangesAndProjectOptions = + openFilesWithChanges + |> AMapAsync.mapAVal (fun filePath file ctok -> + asyncAVal { + if Utils.isAScript (UMX.untag filePath) then + let! (checker: FSharpCompilerServiceChecker) = checker + and! tfmConfig = tfmConfig - // return - allFSharpFilesAndProjectOptions - |> AMapAsync.mapAsyncAVal (fun _filePath (_file, options) _ctok -> AsyncAVal.constant options) - // } + let! projs = + asyncResult { + let cts = getOpenFileTokenOrDefault filePath + use linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ctok, cts) + try + let! (opts, errors) = + checker.GetProjectOptionsFromScript(filePath, file.Source, tfmConfig) + |> Async.withCancellation linkedCts.Token - let allFilesParsed = - // asyncAVal { - // let! allFSharpFilesAndProjectOptions = allFSharpFilesAndProjectOptions - - // return - allFSharpFilesAndProjectOptions - |> AMapAsync.mapAsyncAVal (fun _filePath (file, options: Result) _ctok -> - asyncAVal { - let! (checker: FSharpCompilerServiceChecker) = checker - and! selectProject = projectSelector - - let loadedProject = - options - |> Result.bind (fun p -> selectProject.FindProject(file.FileName, p)) - - match loadedProject with - | Ok x -> - let! snap = x.FSharpProjectSnapshot - let! r = parseFile checker file snap - return Ok r - | Error e -> - return Error e - }) - // } + opts |> scriptFileProjectOptions.Trigger + let diags = errors |> Array.ofList |> Array.map fcsErrorToDiagnostic + + diagnosticCollections.SetFor( + Path.LocalPathToUri filePath, + "F# Script Project Options", + file.Version, + diags + ) + let projectOptions: Types.ProjectOptions = + let projectSdkInfo: Types.ProjectSdkInfo = + { IsTestProject = false + Configuration = "" + IsPackable = false + TargetFramework = "" + TargetFrameworkIdentifier = "" + TargetFrameworkVersion = "" + MSBuildAllProjects = [] + MSBuildToolsVersion = "" + ProjectAssetsFile = "" + RestoreSuccess = true + Configurations = [] + TargetFrameworks = [] + RunArguments = None + RunCommand = None + IsPublishable = None } + + { ProjectId = opts.ProjectId + ProjectFileName = opts.ProjectFileName + TargetFramework = "" + SourceFiles = opts.SourceFiles |> List.map (fun x -> x.FileName) + OtherOptions = opts.OtherOptions + ReferencedProjects = [] + PackageReferences = [] + LoadTime = opts.LoadTime + TargetPath = "" + TargetRefPath = None + ProjectOutputType = Types.ProjectOutputType.Exe + ProjectSdkInfo = projectSdkInfo + Items = [] + Properties = [] + CustomProperties = [] } - let getAllFilesToProjectOptions () = - asyncEx { - // let! allFilesToFSharpProjectOptions = allFilesToFSharpProjectOptions |> AsyncAVal.forceAsync - return! - allFilesToFSharpProjectOptions - |> AMap.force - |> HashMap.toArray - |> Array.map (fun (sourceTextPath, projects) -> - async { - let! projs = AsyncAVal.forceAsync projects - return sourceTextPath, projs - }) - |> Async.parallel75 - } + return + { FSharpProjectSnapshot = AVal.constant opts + LanguageVersion = LanguageVersionShim.fromFSharpProjectSnapshot opts + ProjectOptions = projectOptions } + |> List.singleton + + with e -> + logger.error ( + Log.setMessage "Error getting project options for {filePath}" + >> Log.addContextDestructured "filePath" filePath + >> Log.addExn e + ) + + return! Error $"Error getting project options for {filePath} - {e.Message}" + } + + return file, projs + else + let! projs = + sourceFileToProjectOptions + |> AMap.tryFindR + $"Couldn't find {filePath} in LoadedProjects. Have the projects loaded yet or have you tried restoring your project/solution?" + filePath + + + return file, projs + }) + + let allFSharpFilesAndProjectOptions = + let wins = + openFilesToChangesAndProjectOptions + |> AMap.map (fun _k v -> v |> AsyncAVal.mapSync (fun (file, projects) _ -> file, projects)) + + let loses = + sourceFileToProjectOptions + |> AMap.map (fun filePath v -> + asyncAVal { + let! file = getLatestFileChange filePath + return (file, Ok v) + }) + + AMap.union loses wins + + let allFilesToFSharpProjectOptions = + allFSharpFilesAndProjectOptions + |> AMapAsync.mapAsyncAVal (fun _filePath (_file, options) _ctok -> AsyncAVal.constant options) + + let allFilesParsed = + allFSharpFilesAndProjectOptions + |> AMapAsync.mapAsyncAVal (fun _filePath (file, options: Result) _ctok -> + asyncAVal { + let! (checker: FSharpCompilerServiceChecker) = checker + and! selectProject = projectSelector + + let loadedProject = + options |> Result.bind (fun p -> selectProject.FindProject(file.FileName, p)) + + match loadedProject with + | Ok x -> + let! snap = x.FSharpProjectSnapshot + let! r = parseFile checker file snap + return Ok r + | Error e -> return Error e + }) + + let getAllFilesToProjectOptions () = + allFilesToFSharpProjectOptions + // |> AMap.toASetValues + |> AMap.force + |> HashMap.toArray + |> Array.map (fun (sourceTextPath, projects) -> + async { + let! projs = AsyncAVal.forceAsync projects + return sourceTextPath, projs + }) + |> Async.parallel75 let getAllFilesToProjectOptionsSelected () = async { @@ -1464,8 +1383,6 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let getAllProjectOptions () = async { - // let! allFilesToFSharpProjectOptions = allFilesToFSharpProjectOptions |> AsyncAVal.forceAsync - let! set = allFilesToFSharpProjectOptions |> AMap.toASetValues @@ -1479,15 +1396,13 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac return set |> Array.collect (List.toArray) } - let getAllFSharpProjectOptions () = getAllProjectOptions () |> Async.map (Array.map (fun x -> AVal.force x.FSharpProjectSnapshot)) + let getProjectOptionsForFile (filePath: string) = asyncAVal { - // let! allFilesToFSharpProjectOptions = allFilesToFSharpProjectOptions - match! allFilesToFSharpProjectOptions |> AMapAsync.tryFindA filePath with | Some projs -> return projs | None -> return Error $"Couldn't find project for {filePath}. Have you tried restoring your project/solution?" @@ -1517,85 +1432,83 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac /// The name of the file in the project whose source to find a typecheck. /// The options for the project or script. /// Determines if the typecheck should be cached for autocompletions. - /// The cache to use for autocompletions. /// let parseAndCheckFile (checker: FSharpCompilerServiceChecker) (file: VolatileFile) (options: FSharpProjectSnapshot) shouldCache - snapshotCache = - cancellableTask { - let tags = - [ SemanticConventions.fsac_sourceCodePath, box (UMX.untag file.Source.FileName) - SemanticConventions.projectFilePath, box (options.ProjectFileName) ] + async { + let tags = + [ SemanticConventions.fsac_sourceCodePath, box (UMX.untag file.Source.FileName) + SemanticConventions.projectFilePath, box (options.ProjectFileName) ] + + use _ = fsacActivitySource.StartActivityForType(thisType, tags = tags) - use _ = fsacActivitySource.StartActivityForType(thisType, tags = tags) - logger.info ( - Log.setMessage "Getting typecheck results for {file} - {version} - {date} - {project} - {stamp}" - >> Log.addContextDestructured "file" file.Source.FileName - >> Log.addContextDestructured "version" (file.Version) - >> Log.addContextDestructured "date" (file.LastTouched) - >> Log.addContextDestructured "project" options.ProjectFileName - >> Log.addContextDestructured "stamp" options.Stamp - ) + logger.info ( + Log.setMessage "Getting typecheck results for {file} - {hash} - {date}" + >> Log.addContextDestructured "file" file.Source.FileName + >> Log.addContextDestructured "hash" (file.Source.GetHashCode()) + >> Log.addContextDestructured "date" (file.LastTouched) + ) - use progressReport = new ServerProgressReport(lspClient) + let! ct = Async.CancellationToken - let simpleName = Path.GetFileName(UMX.untag file.Source.FileName) - do! progressReport.Begin($"Typechecking {simpleName}", message = $"{file.Source.FileName}") + use progressReport = new ServerProgressReport(lspClient) - let! result = - checker.ParseAndCheckFileInProject( - file.Source.FileName, - file.Version, - file.Source, - options, - shouldCache = shouldCache, - ?snapshotAccumulator = snapshotCache - ) - // |> Debug.measureAsync $"checker.ParseAndCheckFileInProject - {file.Source.FileName}" + let simpleName = Path.GetFileName(UMX.untag file.Source.FileName) + do! progressReport.Begin($"Typechecking {simpleName}", message = $"{file.Source.FileName}") - // do! progressReport.End($"Typechecked {file.Source.FileName}") ctok + let! result = + checker.ParseAndCheckFileInProject( + file.Source.FileName, + (file.Source.GetHashCode()), + file.Source, + options, + shouldCache = shouldCache + ) + |> Debug.measureAsync $"checker.ParseAndCheckFileInProject - {file.Source.FileName}" - let! ctok = CancellableTask.getCancellationToken() - notifications.Trigger(NotificationEvent.FileParsed(file.Source.FileName), ctok) + do! progressReport.End($"Typechecked {file.Source.FileName}") - match result with - | Error e -> - logger.error ( - Log.setMessage "Typecheck failed for {file} with {error}" - >> Log.addContextDestructured "file" file.FileName - >> Log.addContextDestructured "error" e - ) + notifications.Trigger(NotificationEvent.FileParsed(file.Source.FileName), ct) - return Error e - | Ok parseAndCheck -> - logger.info ( - Log.setMessage "Typecheck completed successfully for {file}" - >> Log.addContextDestructured "file" file.Source.FileName - ) + match result with + | Error e -> + logger.error ( + Log.setMessage "Typecheck failed for {file} with {error}" + >> Log.addContextDestructured "file" file.FileName + >> Log.addContextDestructured "error" e + ) - fileParsed.Trigger(parseAndCheck.GetParseResults, options, ctok) - fileChecked.Trigger(parseAndCheck, file, ctok) - let checkErrors = parseAndCheck.GetParseResults.Diagnostics - let parseErrors = parseAndCheck.GetCheckResults.Diagnostics + return Error e + | Ok parseAndCheck -> + logger.info ( + Log.setMessage "Typecheck completed successfully for {file}" + >> Log.addContextDestructured "file" file.Source.FileName + ) - let errors = - Array.append checkErrors parseErrors - |> Array.distinctBy (fun e -> - e.Severity, e.ErrorNumber, e.StartLine, e.StartColumn, e.EndLine, e.EndColumn, e.Message) - notifications.Trigger(NotificationEvent.ParseError(errors, file.Source.FileName, file.Version), ctok) + fileParsed.Trigger(parseAndCheck.GetParseResults, options, ct) + fileChecked.Trigger(parseAndCheck, file, ct) + let checkErrors = parseAndCheck.GetParseResults.Diagnostics + let parseErrors = parseAndCheck.GetCheckResults.Diagnostics - return Ok parseAndCheck + let errors = + Array.append checkErrors parseErrors + |> Array.distinctBy (fun e -> + e.Severity, e.ErrorNumber, e.StartLine, e.StartColumn, e.EndLine, e.EndColumn, e.Message) - } + notifications.Trigger(NotificationEvent.ParseError(errors, file.Source.FileName, file.Version), ct) + + + return Ok parseAndCheck + } /// Bypass Adaptive checking and tell the checker to check a file - let bypassAdaptiveTypeCheck (filePath: string) opts snapshotCache = + let bypassAdaptiveTypeCheck (filePath: string) opts = asyncResult { try logger.info ( @@ -1606,9 +1519,8 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let checker = checker |> AVal.force let! fileInfo = forceFindOpenFileOrRead filePath - let! ct = Async.CancellationToken // Don't cache for autocompletions as we really only want to cache "Opened" files. - return! parseAndCheckFile checker fileInfo opts false snapshotCache ct + return! parseAndCheckFile checker fileInfo opts false with e -> @@ -1635,13 +1547,15 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let! opts = selectProject.FindProject(file, projectOptions) return opts } + match options with | Ok x -> - let! snap = x.FSharpProjectSnapshot - return! - checker.TryGetRecentCheckResultsForFile(file, snap, info.Source) - |> AsyncResult.ofOption (fun () -> - $"No recent typecheck results for {file}. This may be ok if the file has not been checked yet.") + let! snap = x.FSharpProjectSnapshot + + return! + checker.TryGetRecentCheckResultsForFile(file, snap, info.Source) + |> AsyncResult.ofOption (fun () -> + $"No recent typecheck results for {file}. This may be ok if the file has not been checked yet.") | Error e -> return Error e }) @@ -1652,33 +1566,31 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let file = info.FileName let! checker = checker and! selectProject = projectSelector + let options = - projectOptions - |> Result.bind (fun p -> selectProject.FindProject(file, p)) + projectOptions |> Result.bind (fun p -> selectProject.FindProject(file, p)) match options with | Error e -> return Error e | Ok x -> let! snap = x.FSharpProjectSnapshot + return! asyncResult { let cts = getOpenFileTokenOrDefault file use linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ctok, cts) + return! - parseAndCheckFile checker info snap true None linkedCts.Token + parseAndCheckFile checker info snap true + |> Async.withCancellation linkedCts.Token } }) let getParseResults filePath = - // asyncAVal { - // let! allFilesParsed = allFilesParsed - - // return! - allFilesParsed - |> AMapAsync.tryFindAndFlattenR $"No parse results found for {filePath}" filePath - // } + allFilesParsed + |> AMapAsync.tryFindAndFlattenR $"No parse results found for {filePath}" filePath let getOpenFileTypeCheckResults filePath = openFilesToCheckedFilesResults @@ -1726,8 +1638,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac | Ok x -> return Ok x | Error _ -> match! forceGetFSharpProjectOptions file with - | Ok opts -> - return! bypassAdaptiveTypeCheck file opts None + | Ok opts -> return! bypassAdaptiveTypeCheck file opts | Error e -> return Error e } @@ -1775,18 +1686,11 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac |> AsyncAVal.forceAsync let allFilesToDeclarations = - // asyncAVal { - // let! allFilesParsed = allFilesParsed - - // return - allFilesParsed - |> AMap.map (fun _k v -> v |> AsyncAVal.mapResult (fun p _ -> p.GetNavigationItems().Declarations)) - // } + allFilesParsed + |> AMap.map (fun _k v -> v |> AsyncAVal.mapResult (fun p _ -> p.GetNavigationItems().Declarations)) let getAllDeclarations () = async { - // let! allFilesToDeclarations = allFilesToDeclarations //|> AsyncAVal.forceAsync - let! results = allFilesToDeclarations |> AMap.force @@ -1804,13 +1708,8 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac } let getDeclarations filename = - // asyncAVal { - // let! allFilesToDeclarations = allFilesToDeclarations - - // return! - allFilesToDeclarations - |> AMapAsync.tryFindAndFlattenR $"Could not find getDeclarations for {filename}" filename - // } + allFilesToDeclarations + |> AMapAsync.tryFindAndFlattenR $"Could not find getDeclarations for {filename}" filename let codeGenServer = { new ICodeGenerationService with @@ -1851,41 +1750,38 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac member x.ParseFileInProject(file) = forceGetParseResults file |> Async.map (Option.ofResult) } - let getDependentProjectsOfProjects (ps : FSharpProjectSnapshot list) = - asyncEx { - let projectSnapshot = forceLoadProjects () + let getDependentProjectsOfProjects (ps: FSharpProjectSnapshot list) = + let projectSnapshot = forceLoadProjects () - let allDependents = System.Collections.Generic.HashSet<_>() + let allDependents = System.Collections.Generic.HashSet<_>() - let currentPass = ResizeArray() - currentPass.AddRange(ps |> List.map (fun p -> p.ProjectFileName)) + let currentPass = ResizeArray() + currentPass.AddRange(ps |> List.map (fun p -> p.ProjectFileName)) - let mutable continueAlong = true + let mutable continueAlong = true - while continueAlong do - let dependents = - projectSnapshot - |> Seq.filter (fun p -> - (AVal.force p.FSharpProjectSnapshot).ReferencedProjects - |> Seq.exists (fun r -> - match r.ProjectFilePath with - | None -> false - | Some p -> currentPass.Contains(p))) + while continueAlong do + let dependents = + projectSnapshot + |> Seq.filter (fun p -> + (AVal.force p.FSharpProjectSnapshot).ReferencedProjects + |> Seq.exists (fun r -> + match r.ProjectFilePath with + | None -> false + | Some p -> currentPass.Contains(p))) - if Seq.isEmpty dependents then - continueAlong <- false - currentPass.Clear() - else - for d in dependents do - allDependents.Add (AVal.force d.FSharpProjectSnapshot) |> ignore + if Seq.isEmpty dependents then + continueAlong <- false + currentPass.Clear() + else + for d in dependents do + allDependents.Add(AVal.force d.FSharpProjectSnapshot) |> ignore - currentPass.Clear() - currentPass.AddRange(dependents |> Seq.map (fun p -> p.ProjectFileName)) + currentPass.Clear() + currentPass.AddRange(dependents |> Seq.map (fun p -> p.ProjectFileName)) - return - Seq.toList allDependents - |> List.filter (fun p -> p.ProjectFileName.EndsWith(".fsproj")) - } + Seq.toList allDependents + |> List.filter (fun p -> p.ProjectFileName.EndsWith(".fsproj")) let getDeclarationLocation (symbolUse, text) = let getProjectOptions file = @@ -2167,7 +2063,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac openFiles.Remove filePath |> ignore match openFilesTokens.TryRemove(filePath) with - | (true, cts) -> cancelToken filePath 0 cts + | (true, cts) -> cancelToken filePath None cts | _ -> () textChanges.Remove filePath |> ignore) @@ -2192,7 +2088,6 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac ) } - let getDependentFilesForFile file = async { let! projects = getProjectOptionsForFile file |> AsyncAVal.forceAsync @@ -2206,6 +2101,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac Log.setMessage "Source Files: {sourceFiles}" >> Log.addContextDestructured "sourceFiles" proj.SourceFiles ) + let sourceFiles = proj.SourceFiles let idx = sourceFiles |> Array.findIndex (fun x -> x = UMX.untag file) @@ -2217,9 +2113,12 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac } + let bypassAdaptiveAndCheckDependenciesForFile (sourceFilePath: string) = async { - let tags = [ SemanticConventions.fsac_sourceCodePath, box (UMX.untag sourceFilePath) ] + let tags = + [ SemanticConventions.fsac_sourceCodePath, box (UMX.untag sourceFilePath) ] + use _ = fsacActivitySource.StartActivityForType(thisType, tags = tags) let! dependentFiles = getDependentFilesForFile sourceFilePath @@ -2233,7 +2132,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac |> Option.defaultValue [] |> List.map (fun x -> AVal.force x.FSharpProjectSnapshot) - let! dependentProjects = projs |> getDependentProjectsOfProjects + let dependentProjects = projs |> getDependentProjectsOfProjects let dependentProjectsAndSourceFiles = dependentProjects @@ -2273,13 +2172,14 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac // If we have multiple saves from separate root files we want only one to be running let token = resetCancellationToken file None // Dont dispose, we're a renter not an owner // and join with the root token as well since we want to cancel the whole operation if the root files changes - CancellationTokenSource.CreateLinkedTokenSource(rootToken, token.Token) + CancellationTokenSource.CreateLinkedTokenSource(rootToken, token) let! _ = - bypassAdaptiveTypeCheck (file) (snap) (None) + bypassAdaptiveTypeCheck (file) (snap) |> Async.withCancellation joinedToken.Token let checksCompleted = Interlocked.Increment(&checksCompleted) + do! progressReporter.Report( message = $"{checksCompleted}/{checksToPerformLength} remaining", @@ -2355,9 +2255,9 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac member x.SaveDocument(filePath: string, text: string option) = cancellableTask { - let! file = - asyncResult { - let! oldFile = forceFindOpenFileOrRead filePath + let file = + option { + let! oldFile = forceFindOpenFile filePath let oldFile = text @@ -2367,7 +2267,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac return oldFile.UpdateTouched() } - |> AsyncResult.defaultWith (fun _ -> + |> Option.defaultWith (fun () -> // Very unlikely to get here VolatileFile.Create(sourceTextFactory.Create(filePath, text.Value), 0)) @@ -2385,6 +2285,8 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac member x.ParseAllFiles() = parseAllFiles () |> AsyncAVal.forceAsync + member x.GetOpenFile(filePath) = forceFindOpenFile filePath + member x.GetOpenFileSource(filePath) = forceFindSourceText filePath member x.GetOpenFileOrRead(filePath) = forceFindOpenFileOrRead filePath @@ -2397,7 +2299,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac member x.GetProjectOptionsForFile(filePath) = forceGetFSharpProjectOptions filePath - member x.GetTypeCheckResultsForFile(filePath, opts) = bypassAdaptiveTypeCheck filePath opts None + member x.GetTypeCheckResultsForFile(filePath, opts) = bypassAdaptiveTypeCheck filePath opts member x.GetTypeCheckResultsForFile(filePath) = asyncResult { @@ -2410,8 +2312,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac member x.GetUsesOfSymbol(filePath, opts, symbol) = (AVal.force checker).GetUsesOfSymbol(filePath, opts, symbol) - member x.Codefixes = - codefixes |> AVal.force + member x.Codefixes = codefixes |> AVal.force member x.GlyphToCompletionKind = glyphToCompletionKind |> AVal.force diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi b/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi index 4738b2835..47cbc2eda 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi @@ -30,8 +30,7 @@ type AdaptiveWorkspaceChosen = [] type LoadedProject = - { - ProjectOptions : Types.ProjectOptions + { ProjectOptions: Types.ProjectOptions FSharpProjectSnapshot: aval LanguageVersion: LanguageVersionShim } @@ -40,7 +39,7 @@ type LoadedProject = override Equals: other: obj -> bool member SourceFiles: string array member ProjectFileName: string - // static member op_Implicit: x: LoadedProject -> FSharpProjectSnapshot +// static member op_Implicit: x: LoadedProject -> FSharpProjectSnapshot type AdaptiveState = new: diff --git a/src/FsAutoComplete/LspServers/FSharpLspClient.fs b/src/FsAutoComplete/LspServers/FSharpLspClient.fs index fa4a4710b..e1526feb8 100644 --- a/src/FsAutoComplete/LspServers/FSharpLspClient.fs +++ b/src/FsAutoComplete/LspServers/FSharpLspClient.fs @@ -155,7 +155,8 @@ open System.Text.RegularExpressions /// listener for the the events generated from the fsc ActivitySource type ProgressListener(lspClient: FSharpLspClient, traceNamespace: string array) = - let traceNamespace = traceNamespace |> Array.map(fun x -> Regex(x, RegexOptions.Compiled)) + let traceNamespace = + traceNamespace |> Array.map (fun x -> Regex(x, RegexOptions.Compiled)) let isOneOf list string = list |> Array.exists (fun f -> f string) diff --git a/src/FsAutoComplete/LspServers/ProjectWorkspace.fs b/src/FsAutoComplete/LspServers/ProjectWorkspace.fs index 53646f533..5a3a0b76a 100644 --- a/src/FsAutoComplete/LspServers/ProjectWorkspace.fs +++ b/src/FsAutoComplete/LspServers/ProjectWorkspace.fs @@ -1,8 +1,6 @@ namespace FsAutoComplete.ProjectWorkspace -open System - - +open System module Snapshots = open System @@ -123,7 +121,8 @@ module Snapshots = // Could be configurable but this is a good default // https://learn.microsoft.com/en-us/dotnet/core/runtime-config/garbage-collector#large-object-heap-threshold - let [] LargeObjectHeapThreshold = 85000 + [] + let LargeObjectHeapThreshold = 85000 let private createFSharpFileSnapshotOnDisk (sourceTextFactory: aval) fileName = aval { @@ -190,7 +189,9 @@ module Snapshots = ) loadedProjectsA - |> AMap.filter (fun k _ -> p.ReferencedProjects |> List.exists (fun x -> normalizePath x.ProjectFileName = k)) + |> AMap.filter (fun k _ -> + p.ReferencedProjects + |> List.exists (fun x -> normalizePath x.ProjectFileName = k)) |> AMap.map (fun _ proj -> if proj.ProjectFileName.EndsWith ".fsproj" then @@ -228,6 +229,7 @@ module Snapshots = Log.setMessage "optionsToSnapshot - Cache hit - {projectFileName}" >> Log.addContextDestructured "projectFileName" p.ProjectFileName ) + snapshot | _ -> aval { @@ -235,11 +237,12 @@ module Snapshots = Log.setMessage "optionsToSnapshot - Cache miss - {projectFileName}" >> Log.addContextDestructured "projectFileName" p.ProjectFileName ) + let projectName = AVal.constant p.ProjectFileName let projectId = p.ProjectId |> AVal.constant - let sourceFiles = // alist because order matters for the F# Compiler + let sourceFiles = // alist because order matters for the F# Compiler p.SourceFiles |> AList.ofList |> AList.map (fun sourcePath -> From eee5675819423567a92ea9fe4a40ad316477a27e Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Mon, 25 Mar 2024 23:05:25 -0400 Subject: [PATCH 14/60] More cleanups --- src/FsAutoComplete.Core/AdaptiveExtensions.fs | 13 +- .../CompilerServiceInterface.fs | 25 +- .../CompilerServiceInterface.fsi | 31 +-- src/FsAutoComplete.Core/FileSystem.fs | 25 ++ src/FsAutoComplete.Core/FileSystem.fsi | 5 + src/FsAutoComplete.Core/SymbolLocation.fs | 3 +- .../CodeFixes/AddTypeAliasToSignatureFile.fs | 46 ++-- .../LspServers/AdaptiveServerState.fs | 220 ++++++------------ .../LspServers/AdaptiveServerState.fsi | 2 +- .../LspServers/ProjectWorkspace.fs | 32 +-- 10 files changed, 169 insertions(+), 233 deletions(-) diff --git a/src/FsAutoComplete.Core/AdaptiveExtensions.fs b/src/FsAutoComplete.Core/AdaptiveExtensions.fs index 29202f06d..080dd46ba 100644 --- a/src/FsAutoComplete.Core/AdaptiveExtensions.fs +++ b/src/FsAutoComplete.Core/AdaptiveExtensions.fs @@ -638,20 +638,27 @@ module AsyncAVal = let cancel () = cts.TryCancel() cts.TryDispose() + let real = task { do! avalSyncLock.WaitAsync(cts.Token) + try // Start this work on the threadpool so we can return AdaptiveCancellableTask and let the system cancel if needed // We do this because tasks will stay on the current thread unless there is an yield or await in them. - return! Task.Run((fun () -> cts.Token.ThrowIfCancellationRequested(); value.GetValue t), cts.Token) + return! + Task.Run( + (fun () -> + cts.Token.ThrowIfCancellationRequested() + value.GetValue t), + cts.Token + ) finally avalSyncLock.Release() |> ignore cts.TryDispose() } - AdaptiveCancellableTask(cancel, real) - } + AdaptiveCancellableTask(cancel, real) } :> asyncaval<_> diff --git a/src/FsAutoComplete.Core/CompilerServiceInterface.fs b/src/FsAutoComplete.Core/CompilerServiceInterface.fs index 3e72a9bb3..ac3101be9 100644 --- a/src/FsAutoComplete.Core/CompilerServiceInterface.fs +++ b/src/FsAutoComplete.Core/CompilerServiceInterface.fs @@ -194,7 +194,7 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe let allModifications = // filterBadRuntimeRefs >> - addLoadedFiles >> fixupFsharpCoreAndFSIPaths + addLoadedFiles >> fixupFsharpCoreAndFSIPaths let modified = allModifications snapshot @@ -207,9 +207,9 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe } member self.GetProjectOptionsFromScript(file: string, source, tfm: FSIRefs.TFM) = - match tfm with - | FSIRefs.TFM.NetFx -> self.GetNetFxScriptOptions(file, source) - | FSIRefs.TFM.NetCore -> self.GetNetCoreScriptOptions(file, source) + match tfm with + | FSIRefs.TFM.NetFx -> self.GetNetFxScriptOptions(file, source) + | FSIRefs.TFM.NetCore -> self.GetNetCoreScriptOptions(file, source) member __.ScriptTypecheckRequirementsChanged = @@ -217,10 +217,7 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe member _.RemoveFileFromCache(file: string) = lastCheckResults.Remove(file) - member _.ClearCache(snap : FSharpProjectSnapshot seq) = - snap - |> Seq.map(fun x -> x.Identifier) - |> checker.ClearCache + member _.ClearCache(snap: FSharpProjectSnapshot seq) = snap |> Seq.map (fun x -> x.Identifier) |> checker.ClearCache /// This function is called when the entire environment is known to have changed for reasons not encoded in the ProjectOptions of any project/compilation. member _.ClearCaches() = @@ -231,12 +228,10 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe /// Parses a source code for a file and caches the results. Returns an AST that can be traversed for various features. /// The path for the file. The file name is used as a module name for implicit top level modules (e.g. in scripts). - /// The source to be parsed. /// Parsing options for the project or script. /// - member x.ParseFile(filePath: string, source: ISourceText, snapshot: FSharpProjectSnapshot) = + member x.ParseFile(filePath: string, snapshot: FSharpProjectSnapshot) = async { - let _source = source checkerLogger.info ( Log.setMessage "ParseFile - {file}" >> Log.addContextDestructured "file" filePath @@ -248,8 +243,6 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe /// Parse and check a source code file, returning a handle to the results /// The name of the file in the project whose source is being checked. - /// An integer that can be used to indicate the version of the file. This will be returned by TryGetRecentCheckResultsForFile when looking up the file - /// The source for the file. /// The snapshot for the project or script. /// Determines if the typecheck should be cached for autocompletions. /// Note: all files except the one being checked are read from the FileSystem API @@ -257,14 +250,10 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe member _.ParseAndCheckFileInProject ( filePath: string, - version: int, - source: ISourceText, snapshot: FSharpProjectSnapshot, ?shouldCache: bool ) = asyncResult { - let _source = source - let _version = version let shouldCache = defaultArg shouldCache false let opName = sprintf "ParseAndCheckFileInProject - %A" filePath @@ -435,8 +424,10 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe else let additionalArgs, files = processFSIArgs args fsiAdditionalArguments <- additionalArgs + fsiAdditionalFiles <- files |> Array.map (fun f -> FSharpFileSnapshot.CreateFromFileSystem(System.IO.Path.GetFullPath f)) |> Array.toList + scriptTypecheckRequirementsChanged.Trigger() diff --git a/src/FsAutoComplete.Core/CompilerServiceInterface.fsi b/src/FsAutoComplete.Core/CompilerServiceInterface.fsi index 9ac41fcee..86c71f884 100644 --- a/src/FsAutoComplete.Core/CompilerServiceInterface.fsi +++ b/src/FsAutoComplete.Core/CompilerServiceInterface.fsi @@ -16,26 +16,24 @@ type Version = int type FSharpCompilerServiceChecker = new: - hasAnalyzers: bool * typecheckCacheSize: int64 * parallelReferenceResolution: bool -> - FSharpCompilerServiceChecker + hasAnalyzers: bool * typecheckCacheSize: int64 * parallelReferenceResolution: bool -> FSharpCompilerServiceChecker member DisableInMemoryProjectReferences: bool with get, set static member GetDependingProjects: - file : string -> - snapshots: seq - -> option> + file: string -> + snapshots: seq -> + option> member GetProjectOptionsFromScript: - file: string * source: ISourceTextNew * tfm: FSIRefs.TFM -> Async + file: string * source: ISourceTextNew * tfm: FSIRefs.TFM -> + Async member ScriptTypecheckRequirementsChanged: IEvent member RemoveFileFromCache: file: string -> unit - member ClearCache: - snap: seq - -> unit + member ClearCache: snap: seq -> unit /// This function is called when the entire environment is known to have changed for reasons not encoded in the ProjectOptions of any project/compilation. member ClearCaches: unit -> unit @@ -43,26 +41,18 @@ type FSharpCompilerServiceChecker = /// Parses a source code for a file and caches the results. Returns an AST that can be traversed for various features. /// The path for the file. The file name is used as a module name for implicit top level modules (e.g. in scripts). - /// The source to be parsed. /// Parsing options for the project or script. /// - member ParseFile: - filePath: string * source: ISourceText * snapshot: FSharpProjectSnapshot -> Async + member ParseFile: filePath: string * snapshot: FSharpProjectSnapshot -> Async /// Parse and check a source code file, returning a handle to the results /// The name of the file in the project whose source is being checked. - /// An integer that can be used to indicate the version of the file. This will be returned by TryGetRecentCheckResultsForFile when looking up the file - /// The source for the file. /// The options for the project or script. /// Determines if the typecheck should be cached for autocompletions. /// Note: all files except the one being checked are read from the FileSystem API /// Result of ParseAndCheckResults member ParseAndCheckFileInProject: - filePath: string * - version: int * - source: ISourceText * - snapshot: FSharpProjectSnapshot * - ?shouldCache: bool -> + filePath: string * snapshot: FSharpProjectSnapshot * ?shouldCache: bool -> Async> /// @@ -75,7 +65,8 @@ type FSharpCompilerServiceChecker = member TryGetLastCheckResultForFile: file: string -> ParseAndCheckResults option member TryGetRecentCheckResultsForFile: - file: string * snapshot: FSharpProjectSnapshot * source: ISourceText -> Async + file: string * snapshot: FSharpProjectSnapshot * source: ISourceText -> + Async member GetUsesOfSymbol: file: string * snapshots: (string * FSharpProjectSnapshot) seq * symbol: FSharpSymbol -> diff --git a/src/FsAutoComplete.Core/FileSystem.fs b/src/FsAutoComplete.Core/FileSystem.fs index e6ddef021..dca06661a 100644 --- a/src/FsAutoComplete.Core/FileSystem.fs +++ b/src/FsAutoComplete.Core/FileSystem.fs @@ -393,6 +393,31 @@ type ISourceTextFactory = abstract member Create: fileName: string * text: string -> IFSACSourceText abstract member Create: fileName: string * stream: Stream -> CancellableValueTask +module SourceTextFactory = + // Could be configurable but using the default for now + // https://learn.microsoft.com/en-us/dotnet/core/runtime-config/garbage-collector#large-object-heap-threshold + [] + let LargeObjectHeapThreshold = 85000 + + let readFile (fileName: string) (sourceTextFactory : ISourceTextFactory) = + cancellableValueTask { + let file = UMX.untag fileName + + // use large object heap hits or threadpool hits? Which is worse? Choose your foot gun. + + if FileInfo(file).Length >= LargeObjectHeapThreshold then + // Roslyn SourceText doesn't actually support async streaming reads but avoids the large object heap hit + // so we have to block a thread. + use s = File.openFileStreamForReadingAsync fileName + let! source = sourceTextFactory.Create (fileName, s) + return source + else + // otherwise it'll be under the LOH threshold and the current thread isn't blocked + let! text = fun ct -> File.ReadAllTextAsync(file, ct) + let source = sourceTextFactory.Create(fileName, text) + return source + } + type RoslynSourceTextFactory() = interface ISourceTextFactory with member this.Create(fileName: string, text: string) : IFSACSourceText = diff --git a/src/FsAutoComplete.Core/FileSystem.fsi b/src/FsAutoComplete.Core/FileSystem.fsi index 1ca968dfa..6829c439d 100644 --- a/src/FsAutoComplete.Core/FileSystem.fsi +++ b/src/FsAutoComplete.Core/FileSystem.fsi @@ -104,6 +104,11 @@ type ISourceTextFactory = abstract member Create: fileName: string * text: string -> IFSACSourceText abstract member Create: fileName: string * stream: Stream -> CancellableValueTask + +module SourceTextFactory = + val readFile: + fileName: string -> sourceTextFactory: ISourceTextFactory -> CancellableValueTask + type RoslynSourceTextFactory = new: unit -> RoslynSourceTextFactory interface ISourceTextFactory diff --git a/src/FsAutoComplete.Core/SymbolLocation.fs b/src/FsAutoComplete.Core/SymbolLocation.fs index e45ad01e7..5f0edace5 100644 --- a/src/FsAutoComplete.Core/SymbolLocation.fs +++ b/src/FsAutoComplete.Core/SymbolLocation.fs @@ -60,7 +60,8 @@ let getDeclarationLocation match! projectsThatContainFile (taggedFilePath) with | [] -> return! None | projectsThatContainFile -> - let projectsThatDependOnContainingProjects = getDependentProjectsOfProjects projectsThatContainFile + let projectsThatDependOnContainingProjects = + getDependentProjectsOfProjects projectsThatContainFile match projectsThatDependOnContainingProjects with | [] -> return (SymbolDeclarationLocation.Projects(projectsThatContainFile, isSymbolLocalForProject)) diff --git a/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs b/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs index 6036f5c39..5cdda6e43 100644 --- a/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs +++ b/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs @@ -37,29 +37,29 @@ type SynTypeDefn with let title = "Add type alias to signature file" -let codeFixForImplementationFileWithSignature - (getProjectOptionsForFile: GetProjectOptionsForFile) - (codeFix: CodeFix) - (codeActionParams: CodeActionParams) - : Async> = - async { - let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath - let! project = getProjectOptionsForFile fileName - - match project with - | Error _ -> return Ok [] - | Ok projectOptions -> - - let signatureFile = String.Concat(fileName, "i") - - let hasSig = - projectOptions.SourceFiles |> List.exists (fun s -> s.FileName = signatureFile) - - if not hasSig then - return Ok [] - else - return! codeFix codeActionParams - } +// let codeFixForImplementationFileWithSignature +// (getProjectOptionsForFile: GetProjectOptionsForFile) +// (codeFix: CodeFix) +// (codeActionParams: CodeActionParams) +// : Async> = +// async { +// let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath +// let project = getProjectOptionsForFile fileName + +// match project with +// | Error _ -> return Ok [] +// | Ok projectOptions -> + +// let signatureFile = String.Concat(fileName, "i") + +// let hasSig = +// projectOptions.SourceFiles |> List.exists (fun s -> s.FileName = signatureFile) + +// if not hasSig then +// return Ok [] +// else +// return! codeFix codeActionParams +// } let fix (getProjectOptionsForFile: GetProjectOptionsForFile) diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index 02cde1f8b..ef509c572 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -38,7 +38,6 @@ open FsAutoComplete.Lsp.Helpers open FSharp.Compiler.Syntax open FsAutoComplete.ProjectWorkspace - [] type WorkspaceChosen = | Projs of HashSet> @@ -49,8 +48,6 @@ type AdaptiveWorkspaceChosen = | Projs of amap, DateTime> | NotChosen - - [] module Helpers3 = open FSharp.Compiler.CodeAnalysis.ProjectSnapshot @@ -62,6 +59,10 @@ module Helpers3 = | FSharpReferencedProjectSnapshot.FSharpReference(snapshot = snapshot) -> snapshot.ProjectFileName |> Some | _ -> None + type FSharpProjectSnapshot with + + member x.SourcePaths = x.SourceFiles |> List.map (fun f -> normalizePath f.FileName) + [] type LoadedProject = { ProjectOptions: Types.ProjectOptions @@ -722,12 +723,10 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac AdaptiveFile.GetLastWriteTimeUtc(UMX.untag filePath) |> AVal.map (fun writeTime -> filePath, writeTime) - let readFileFromDisk lastTouched (file: string) = + let createVolatileFileFromDisk lastTouched (file: string) = async { if File.Exists(UMX.untag file) then - use s = File.openFileStreamForReadingAsync file - - let! source = sourceTextFactory.Create(file, s) |> Async.AwaitCancellableValueTask + let! source = SourceTextFactory.readFile file sourceTextFactory return { LastTouched = lastTouched @@ -741,12 +740,6 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac Version = 0 } } - let getLatestFileChange (filePath: string) = - asyncAVal { - let! (_, lastTouched) = getLastUTCChangeForFile filePath - return! readFileFromDisk lastTouched filePath - } - let addAValLogging cb (aval: aval<_>) = let cb = aval.AddWeakMarkingCallback(cb) aval |> AVal.mapDisposableTuple (fun x -> x, cb) @@ -824,9 +817,6 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac tryFindProp "MSBuildAllProjects" props |> Option.map (fun v -> v.Split(';', StringSplitOptions.RemoveEmptyEntries)) - - - let loadProjects (loader: IWorkspaceLoader) binlogConfig projects = logger.info (Log.setMessageI $"Enter loading projects") @@ -1068,7 +1058,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac // |> disposables.Add - let sourceFileToProjectOptions = + let sourceFileToLoadedProjects = amap { let! snaps = loadedProjects |> AMap.toAVal @@ -1083,6 +1073,12 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac |> List.groupByFst) } + let sourceFileToLoadedProject = + sourceFileToLoadedProjects + |> AMap.mapA (fun sourceFile loadedProjects -> + projectSelector + |> AVal.map (fun selectProject -> selectProject.FindProject(sourceFile, loadedProjects))) + let cancelToken filePath version (cts: CancellationTokenSource) = try @@ -1151,7 +1147,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let lastTouched = File.getLastWriteTimeOrDefaultNow file - return! readFileFromDisk lastTouched file + return! createVolatileFileFromDisk lastTouched file with e -> logger.warn ( @@ -1183,9 +1179,9 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac /// The source to be parsed. /// /// - let parseFile (checker: FSharpCompilerServiceChecker) (source: VolatileFile) snap = + let parseFile (checker: FSharpCompilerServiceChecker) (source) snap = async { - let! result = checker.ParseFile(source.FileName, source.Source, snap) + let! result = checker.ParseFile(source, snap) let! ct = Async.CancellationToken fileParsed.Trigger(result, snap, ct) @@ -1210,22 +1206,17 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac return projects |> HashSet.toArray - |> Array.collect (fun (snap) -> snap.SourceFiles |> List.toArray |> Array.map (fun s -> snap, s)) + |> Array.collect (fun (snap) -> snap.SourcePaths |> List.toArray |> Array.map (fun s -> snap, s)) |> Array.map (fun (snap, fileName) -> - let fileName = UMX.tag fileName.FileName - asyncResult { - let! file = forceFindOpenFileOrRead fileName - return! parseFile checker file snap - } - |> Async.map Result.toOption) + parseFile checker fileName snap) |> Async.parallel75 } let forceFindSourceText filePath = forceFindOpenFileOrRead filePath |> AsyncResult.map (fun f -> f.Source) - let openFilesToChangesAndProjectOptions = + let openFilesToChangesAndLoadedProject = openFilesWithChanges |> AMapAsync.mapAVal (fun filePath file ctok -> asyncAVal { @@ -1233,7 +1224,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let! (checker: FSharpCompilerServiceChecker) = checker and! tfmConfig = tfmConfig - let! projs = + let! proj = asyncResult { let cts = getOpenFileTokenOrDefault filePath use linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ctok, cts) @@ -1293,7 +1284,6 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac { FSharpProjectSnapshot = AVal.constant opts LanguageVersion = LanguageVersionShim.fromFSharpProjectSnapshot opts ProjectOptions = projectOptions } - |> List.singleton with e -> logger.error ( @@ -1305,57 +1295,45 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac return! Error $"Error getting project options for {filePath} - {e.Message}" } - return file, projs + return file, proj else - let! projs = - sourceFileToProjectOptions + let! proj = + sourceFileToLoadedProject |> AMap.tryFindR $"Couldn't find {filePath} in LoadedProjects. Have the projects loaded yet or have you tried restoring your project/solution?" filePath + |> AVal.map (Result.bind id) - return file, projs + return file, proj }) let allFSharpFilesAndProjectOptions = let wins = - openFilesToChangesAndProjectOptions - |> AMap.map (fun _k v -> v |> AsyncAVal.mapSync (fun (file, projects) _ -> file, projects)) + openFilesToChangesAndLoadedProject + |> AMap.map (fun _ v -> v |> AsyncAVal.mapSync (fun (_, projects) _ -> projects)) - let loses = - sourceFileToProjectOptions - |> AMap.map (fun filePath v -> - asyncAVal { - let! file = getLatestFileChange filePath - return (file, Ok v) - }) + let loses = sourceFileToLoadedProject |> AMap.map (fun _ v -> AsyncAVal.constant v) AMap.union loses wins - let allFilesToFSharpProjectOptions = - allFSharpFilesAndProjectOptions - |> AMapAsync.mapAsyncAVal (fun _filePath (_file, options) _ctok -> AsyncAVal.constant options) let allFilesParsed = allFSharpFilesAndProjectOptions - |> AMapAsync.mapAsyncAVal (fun _filePath (file, options: Result) _ctok -> + |> AMapAsync.mapAsyncAVal (fun filePath (loadedProject: Result) _ctok -> asyncAVal { let! (checker: FSharpCompilerServiceChecker) = checker - and! selectProject = projectSelector - - let loadedProject = - options |> Result.bind (fun p -> selectProject.FindProject(file.FileName, p)) match loadedProject with | Ok x -> let! snap = x.FSharpProjectSnapshot - let! r = parseFile checker file snap + let! r = parseFile checker filePath snap return Ok r | Error e -> return Error e }) - let getAllFilesToProjectOptions () = - allFilesToFSharpProjectOptions + let getAllFilesToLoadedProject () = + allFSharpFilesAndProjectOptions // |> AMap.toASetValues |> AMap.force |> HashMap.toArray @@ -1368,23 +1346,17 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let getAllFilesToProjectOptionsSelected () = async { - let! set = getAllFilesToProjectOptions () - let selectProject = projectSelector |> AVal.force - let findProject file projects = selectProject.FindProject(file, projects) + let! set = getAllFilesToLoadedProject () return set - |> Array.choose (fun (k, v) -> - v - |> Result.bind (findProject k) - |> Result.toOption - |> Option.map (fun v -> k, v)) + |> Array.choose (fun (k, v) -> v |> Result.toOption |> Option.map (fun v -> k, v)) } let getAllProjectOptions () = async { let! set = - allFilesToFSharpProjectOptions + allFSharpFilesAndProjectOptions |> AMap.toASetValues |> ASet.force |> HashSet.toArray @@ -1393,7 +1365,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let set = set |> Array.choose (Result.toOption) - return set |> Array.collect (List.toArray) + return set } let getAllFSharpProjectOptions () = @@ -1401,9 +1373,9 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac |> Async.map (Array.map (fun x -> AVal.force x.FSharpProjectSnapshot)) - let getProjectOptionsForFile (filePath: string) = + let getLoadedProjectForFile (filePath: string) = asyncAVal { - match! allFilesToFSharpProjectOptions |> AMapAsync.tryFindA filePath with + match! allFSharpFilesAndProjectOptions |> AMapAsync.tryFindA filePath with | Some projs -> return projs | None -> return Error $"Couldn't find project for {filePath}. Have you tried restoring your project/solution?" } @@ -1430,19 +1402,19 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac /// Gets Parse and Check results of a given file while also handling other concerns like Progress, Logging, Eventing. /// The FSharpCompilerServiceChecker. /// The name of the file in the project whose source to find a typecheck. - /// The options for the project or script. + /// The options for the project or script. /// Determines if the typecheck should be cached for autocompletions. /// let parseAndCheckFile (checker: FSharpCompilerServiceChecker) (file: VolatileFile) - (options: FSharpProjectSnapshot) + (snapshot: FSharpProjectSnapshot) shouldCache = async { let tags = [ SemanticConventions.fsac_sourceCodePath, box (UMX.untag file.Source.FileName) - SemanticConventions.projectFilePath, box (options.ProjectFileName) ] + SemanticConventions.projectFilePath, box (snapshot.ProjectFileName) ] use _ = fsacActivitySource.StartActivityForType(thisType, tags = tags) @@ -1462,13 +1434,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac do! progressReport.Begin($"Typechecking {simpleName}", message = $"{file.Source.FileName}") let! result = - checker.ParseAndCheckFileInProject( - file.Source.FileName, - (file.Source.GetHashCode()), - file.Source, - options, - shouldCache = shouldCache - ) + checker.ParseAndCheckFileInProject(file.Source.FileName, snapshot, shouldCache = shouldCache) |> Debug.measureAsync $"checker.ParseAndCheckFileInProject - {file.Source.FileName}" do! progressReport.End($"Typechecked {file.Source.FileName}") @@ -1491,7 +1457,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac ) - fileParsed.Trigger(parseAndCheck.GetParseResults, options, ct) + fileParsed.Trigger(parseAndCheck.GetParseResults, snapshot, ct) fileChecked.Trigger(parseAndCheck, file, ct) let checkErrors = parseAndCheck.GetParseResults.Diagnostics let parseErrors = parseAndCheck.GetCheckResults.Diagnostics @@ -1534,21 +1500,13 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac } let openFilesToRecentCheckedFilesResults = - openFilesToChangesAndProjectOptions - |> AMapAsync.mapAsyncAVal (fun _ (info, projectOptions) _ -> + openFilesToChangesAndLoadedProject + |> AMapAsync.mapAsyncAVal (fun _ (info, loadedProject) _ -> asyncAVal { let file = info.Source.FileName let! checker = checker - and! selectProject = projectSelector - - let options = - result { - let! projectOptions = projectOptions - let! opts = selectProject.FindProject(file, projectOptions) - return opts - } - match options with + match loadedProject with | Ok x -> let! snap = x.FSharpProjectSnapshot @@ -1560,18 +1518,13 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac }) let openFilesToCheckedFilesResults = - openFilesToChangesAndProjectOptions - |> AMapAsync.mapAsyncAVal (fun _ (info, projectOptions) ctok -> + openFilesToChangesAndLoadedProject + |> AMapAsync.mapAsyncAVal (fun _ (info, loadedProject) ctok -> asyncAVal { let file = info.FileName let! checker = checker - and! selectProject = projectSelector - - let options = - projectOptions |> Result.bind (fun p -> selectProject.FindProject(file, p)) - - match options with + match loadedProject with | Error e -> return Error e | Ok x -> let! snap = x.FSharpProjectSnapshot @@ -1616,16 +1569,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac - let forceGetProjectOptions filePath = - asyncAVal { - let! projects = getProjectOptionsForFile filePath - and! selectProject = projectSelector - - match projects with - | Ok projects -> return selectProject.FindProject(filePath, projects) - | Error e -> return Error e - } - |> AsyncAVal.forceAsync + let forceGetProjectOptions filePath = getLoadedProjectForFile filePath |> AsyncAVal.forceAsync let forceGetFSharpProjectOptions filePath = forceGetProjectOptions filePath @@ -1786,12 +1730,10 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let getDeclarationLocation (symbolUse, text) = let getProjectOptions file = async { - let! projects = getProjectOptionsForFile file |> AsyncAVal.forceAsync - let selectProject = projectSelector |> AVal.force + let! projects = getLoadedProjectForFile file |> AsyncAVal.forceAsync return projects - |> Result.bind (fun p -> selectProject.FindProject(file, p)) |> Result.toOption |> Option.map (fun project -> AVal.force project.FSharpProjectSnapshot) @@ -1799,9 +1741,13 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let projectsThatContainFile file = async { - let! projects = getProjectOptionsForFile file |> AsyncAVal.forceAsync - let projects = projects |> Result.toOption |> Option.defaultValue [] - return projects |> List.map (fun p -> AVal.force p.FSharpProjectSnapshot) + let! project = getLoadedProjectForFile file |> AsyncAVal.forceAsync + + let projects = + project + |> Result.map (fun p -> AVal.force p.FSharpProjectSnapshot |> List.singleton) + + return projects |> Result.defaultWith (fun _ -> []) } SymbolLocation.getDeclarationLocation ( @@ -2038,12 +1984,9 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac if isInside (rootDir, fileDir) then return false else - let! projectOptions = getProjectOptionsForFile file + let! projectOptions = getLoadedProjectForFile file - match - projectOptions - |> Result.bind (fun projs -> selectProject.FindProject(file, projs)) - with + match projectOptions with | Error _ -> return true | Ok projectOptions -> if doesNotExist (UMX.tag projectOptions.ProjectFileName) then @@ -2090,49 +2033,40 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let getDependentFilesForFile file = async { - let! projects = getProjectOptionsForFile file |> AsyncAVal.forceAsync - let projects = projects |> Result.toOption |> Option.defaultValue [] - - return - projects - |> List.toArray - |> Array.collect (fun proj -> - logger.info ( - Log.setMessage "Source Files: {sourceFiles}" - >> Log.addContextDestructured "sourceFiles" proj.SourceFiles - ) + let! project = getLoadedProjectForFile file |> AsyncAVal.forceAsync - let sourceFiles = proj.SourceFiles - let idx = sourceFiles |> Array.findIndex (fun x -> x = UMX.untag file) + match project with + | Ok project -> + let sourceFiles = project.SourceFiles + let idx = sourceFiles |> Array.findIndex (fun x -> x = UMX.untag file) + return sourceFiles |> Array.splitAt idx |> snd - |> Array.map (fun sourceFile -> AVal.force proj.FSharpProjectSnapshot, sourceFile)) - |> Array.distinct + |> Array.map (fun sourceFile -> AVal.force project.FSharpProjectSnapshot, sourceFile) + + | Error _ -> return Array.empty + } let bypassAdaptiveAndCheckDependenciesForFile (sourceFilePath: string) = - async { + asyncResult { let tags = [ SemanticConventions.fsac_sourceCodePath, box (UMX.untag sourceFilePath) ] use _ = fsacActivitySource.StartActivityForType(thisType, tags = tags) let! dependentFiles = getDependentFilesForFile sourceFilePath - let! projs = getProjectOptionsForFile sourceFilePath |> AsyncAVal.forceAsync + let! loadedProject = getLoadedProjectForFile sourceFilePath |> AsyncAVal.forceAsync let rootToken = sourceFilePath |> getOpenFileTokenOrDefault - let projs = - projs - |> Result.toOption - |> Option.defaultValue [] - |> List.map (fun x -> AVal.force x.FSharpProjectSnapshot) + let snap = AVal.force loadedProject.FSharpProjectSnapshot - let dependentProjects = projs |> getDependentProjectsOfProjects + let dependentProjects = [ snap ] |> getDependentProjectsOfProjects let dependentProjectsAndSourceFiles = dependentProjects @@ -2195,6 +2129,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac message = $"0/{checksToPerform.Length} remaining", percentage = percentage 0 checksToPerform.Length ) + |> Async.AwaitCancellableTask do! checksToPerform |> Async.parallel75 |> Async.Ignore @@ -2276,7 +2211,8 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac textChanges.Remove filePath |> ignore) let! _ = forceGetOpenFileTypeCheckResults filePath - do! bypassAdaptiveAndCheckDependenciesForFile filePath + let! _ = bypassAdaptiveAndCheckDependenciesForFile filePath + return () } @@ -2285,8 +2221,6 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac member x.ParseAllFiles() = parseAllFiles () |> AsyncAVal.forceAsync - member x.GetOpenFile(filePath) = forceFindOpenFile filePath - member x.GetOpenFileSource(filePath) = forceFindSourceText filePath member x.GetOpenFileOrRead(filePath) = forceFindOpenFileOrRead filePath diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi b/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi index 47cbc2eda..72bceb95b 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi @@ -59,7 +59,7 @@ type AdaptiveState = member ChangeDocument: filePath: string * p: DidChangeTextDocumentParams -> CancellableTask member SaveDocument: filePath: string * text: string option -> CancellableTask member ForgetDocument: filePath: DocumentUri -> Async - member ParseAllFiles: unit -> Async + member ParseAllFiles: unit -> Async member GetOpenFileSource: filePath: string -> Async> member GetOpenFileOrRead: filePath: string -> Async> member GetParseResults: filePath: string -> Async> diff --git a/src/FsAutoComplete/LspServers/ProjectWorkspace.fs b/src/FsAutoComplete/LspServers/ProjectWorkspace.fs index 5a3a0b76a..66e70aec7 100644 --- a/src/FsAutoComplete/LspServers/ProjectWorkspace.fs +++ b/src/FsAutoComplete/LspServers/ProjectWorkspace.fs @@ -119,36 +119,18 @@ module Snapshots = unresolvedReferences originalLoadReferences - // Could be configurable but this is a good default - // https://learn.microsoft.com/en-us/dotnet/core/runtime-config/garbage-collector#large-object-heap-threshold - [] - let LargeObjectHeapThreshold = 85000 - let private createFSharpFileSnapshotOnDisk (sourceTextFactory: aval) fileName = aval { let! writeTime = AdaptiveFile.GetLastWriteTimeUtc fileName and! sourceTextFactory = sourceTextFactory - let getSource () = - task { - - let file = Utils.normalizePath fileName - - // use large object heap hits or threadpool hits? Which is worse? Choose your foot gun. - - if FileInfo(fileName).Length >= LargeObjectHeapThreshold then - // Roslyn SourceText doesn't actually support async streaming reads but avoids the large object heap hit - // so we have to block a thread. - use s = File.openFileStreamForReadingAsync file - let! source = sourceTextFactory.Create (file, s) CancellationToken.None - return source :> ISourceTextNew - else - // otherwise it'll be under the LOH threshold and the current thread isn't blocked - let! text = File.ReadAllTextAsync fileName - let source = sourceTextFactory.Create(file, text) - return source :> ISourceTextNew - } - // printfn "Creating source text for %s" fileName + let fileNorm = normalizePath fileName + + let getSource () = task { + let! sourceText = SourceTextFactory.readFile fileNorm sourceTextFactory CancellationToken.None + return sourceText :> ISourceTextNew + } + return ProjectSnapshot.FSharpFileSnapshot.Create(fileName, string writeTime.Ticks, getSource) } From 076dceafd944ba24252da1d04f8fed9fa021f766 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Tue, 26 Mar 2024 00:01:17 -0400 Subject: [PATCH 15/60] undo some project selection and fix onsave checks --- .../CompilerServiceInterface.fs | 29 +-- .../CompilerServiceInterface.fsi | 3 +- src/FsAutoComplete.Core/FileSystem.fs | 4 +- .../LspServers/AdaptiveServerState.fs | 206 +++++++++++------- .../LspServers/ProjectWorkspace.fs | 9 +- 5 files changed, 143 insertions(+), 108 deletions(-) diff --git a/src/FsAutoComplete.Core/CompilerServiceInterface.fs b/src/FsAutoComplete.Core/CompilerServiceInterface.fs index ac3101be9..7e516fb69 100644 --- a/src/FsAutoComplete.Core/CompilerServiceInterface.fs +++ b/src/FsAutoComplete.Core/CompilerServiceInterface.fs @@ -318,33 +318,20 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe | (true, v) -> Some v | _ -> None - member _.TryGetRecentCheckResultsForFile - ( - file: string, - snapshot: FSharpProjectSnapshot, - source: ISourceText - ) = - async { - let opName = sprintf "TryGetRecentCheckResultsForFile - %A" file + member _.TryGetRecentCheckResultsForFile(file: string, snapshot: FSharpProjectSnapshot) = + let opName = sprintf "TryGetRecentCheckResultsForFile - %A" file + + checkerLogger.info (Log.setMessage "{opName} - {hash}" >> Log.addContextDestructured "opName" opName) + checker.TryGetRecentCheckResultsForFile(UMX.untag file, snapshot, opName) + |> Option.map (fun (pr, cr) -> checkerLogger.info ( - Log.setMessage "{opName} - {hash}" + Log.setMessage "{opName} - got results - {version}" >> Log.addContextDestructured "opName" opName - >> Log.addContextDestructured "hash" (source.GetHashCode() |> int) - ) + ParseAndCheckResults(pr, cr, entityCache)) - return - checker.TryGetRecentCheckResultsForFile(UMX.untag file, snapshot, opName) - |> Option.map (fun (pr, cr) -> - checkerLogger.info ( - Log.setMessage "{opName} - got results - {version}" - >> Log.addContextDestructured "opName" opName - ) - - ParseAndCheckResults(pr, cr, entityCache)) - } member _.GetUsesOfSymbol ( diff --git a/src/FsAutoComplete.Core/CompilerServiceInterface.fsi b/src/FsAutoComplete.Core/CompilerServiceInterface.fsi index 86c71f884..ab1dcdc52 100644 --- a/src/FsAutoComplete.Core/CompilerServiceInterface.fsi +++ b/src/FsAutoComplete.Core/CompilerServiceInterface.fsi @@ -65,8 +65,7 @@ type FSharpCompilerServiceChecker = member TryGetLastCheckResultForFile: file: string -> ParseAndCheckResults option member TryGetRecentCheckResultsForFile: - file: string * snapshot: FSharpProjectSnapshot * source: ISourceText -> - Async + file: string * snapshot: FSharpProjectSnapshot -> ParseAndCheckResults option member GetUsesOfSymbol: file: string * snapshots: (string * FSharpProjectSnapshot) seq * symbol: FSharpSymbol -> diff --git a/src/FsAutoComplete.Core/FileSystem.fs b/src/FsAutoComplete.Core/FileSystem.fs index dca06661a..1bb220150 100644 --- a/src/FsAutoComplete.Core/FileSystem.fs +++ b/src/FsAutoComplete.Core/FileSystem.fs @@ -399,7 +399,7 @@ module SourceTextFactory = [] let LargeObjectHeapThreshold = 85000 - let readFile (fileName: string) (sourceTextFactory : ISourceTextFactory) = + let readFile (fileName: string) (sourceTextFactory: ISourceTextFactory) = cancellableValueTask { let file = UMX.untag fileName @@ -409,7 +409,7 @@ module SourceTextFactory = // Roslyn SourceText doesn't actually support async streaming reads but avoids the large object heap hit // so we have to block a thread. use s = File.openFileStreamForReadingAsync fileName - let! source = sourceTextFactory.Create (fileName, s) + let! source = sourceTextFactory.Create(fileName, s) return source else // otherwise it'll be under the LOH threshold and the current thread isn't blocked diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index ef509c572..d85b141fe 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -38,6 +38,7 @@ open FsAutoComplete.Lsp.Helpers open FSharp.Compiler.Syntax open FsAutoComplete.ProjectWorkspace + [] type WorkspaceChosen = | Projs of HashSet> @@ -48,6 +49,8 @@ type AdaptiveWorkspaceChosen = | Projs of amap, DateTime> | NotChosen + + [] module Helpers3 = open FSharp.Compiler.CodeAnalysis.ProjectSnapshot @@ -59,10 +62,6 @@ module Helpers3 = | FSharpReferencedProjectSnapshot.FSharpReference(snapshot = snapshot) -> snapshot.ProjectFileName |> Some | _ -> None - type FSharpProjectSnapshot with - - member x.SourcePaths = x.SourceFiles |> List.map (fun f -> normalizePath f.FileName) - [] type LoadedProject = { ProjectOptions: Types.ProjectOptions @@ -723,7 +722,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac AdaptiveFile.GetLastWriteTimeUtc(UMX.untag filePath) |> AVal.map (fun writeTime -> filePath, writeTime) - let createVolatileFileFromDisk lastTouched (file: string) = + let readFileFromDisk lastTouched (file: string) = async { if File.Exists(UMX.untag file) then let! source = SourceTextFactory.readFile file sourceTextFactory @@ -817,6 +816,9 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac tryFindProp "MSBuildAllProjects" props |> Option.map (fun v -> v.Split(';', StringSplitOptions.RemoveEmptyEntries)) + + + let loadProjects (loader: IWorkspaceLoader) binlogConfig projects = logger.info (Log.setMessageI $"Enter loading projects") @@ -1058,7 +1060,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac // |> disposables.Add - let sourceFileToLoadedProjects = + let sourceFileToProjectOptions = amap { let! snaps = loadedProjects |> AMap.toAVal @@ -1073,12 +1075,6 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac |> List.groupByFst) } - let sourceFileToLoadedProject = - sourceFileToLoadedProjects - |> AMap.mapA (fun sourceFile loadedProjects -> - projectSelector - |> AVal.map (fun selectProject -> selectProject.FindProject(sourceFile, loadedProjects))) - let cancelToken filePath version (cts: CancellationTokenSource) = try @@ -1147,7 +1143,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let lastTouched = File.getLastWriteTimeOrDefaultNow file - return! createVolatileFileFromDisk lastTouched file + return! readFileFromDisk lastTouched file with e -> logger.warn ( @@ -1206,17 +1202,18 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac return projects |> HashSet.toArray - |> Array.collect (fun (snap) -> snap.SourcePaths |> List.toArray |> Array.map (fun s -> snap, s)) + |> Array.collect (fun (snap) -> snap.SourceFiles |> List.toArray |> Array.map (fun s -> snap, s)) |> Array.map (fun (snap, fileName) -> + let filePath = UMX.tag fileName.FileName - parseFile checker fileName snap) + parseFile checker filePath snap) |> Async.parallel75 } let forceFindSourceText filePath = forceFindOpenFileOrRead filePath |> AsyncResult.map (fun f -> f.Source) - let openFilesToChangesAndLoadedProject = + let openFilesToChangesAndProjectOptions = openFilesWithChanges |> AMapAsync.mapAVal (fun filePath file ctok -> asyncAVal { @@ -1224,7 +1221,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let! (checker: FSharpCompilerServiceChecker) = checker and! tfmConfig = tfmConfig - let! proj = + let! projs = asyncResult { let cts = getOpenFileTokenOrDefault filePath use linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ctok, cts) @@ -1284,6 +1281,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac { FSharpProjectSnapshot = AVal.constant opts LanguageVersion = LanguageVersionShim.fromFSharpProjectSnapshot opts ProjectOptions = projectOptions } + |> List.singleton with e -> logger.error ( @@ -1295,34 +1293,41 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac return! Error $"Error getting project options for {filePath} - {e.Message}" } - return file, proj + return file, projs else - let! proj = - sourceFileToLoadedProject + let! projs = + sourceFileToProjectOptions |> AMap.tryFindR $"Couldn't find {filePath} in LoadedProjects. Have the projects loaded yet or have you tried restoring your project/solution?" filePath - |> AVal.map (Result.bind id) - return file, proj + return file, projs }) let allFSharpFilesAndProjectOptions = let wins = - openFilesToChangesAndLoadedProject - |> AMap.map (fun _ v -> v |> AsyncAVal.mapSync (fun (_, projects) _ -> projects)) + openFilesToChangesAndProjectOptions + |> AMap.map (fun _k v -> v |> AsyncAVal.mapSync (fun (_, projects) _ -> projects)) - let loses = sourceFileToLoadedProject |> AMap.map (fun _ v -> AsyncAVal.constant v) + let loses = + sourceFileToProjectOptions |> AMap.map (fun _ v -> AsyncAVal.constant (Ok v)) AMap.union loses wins + let allFilesToFSharpProjectOptions = + allFSharpFilesAndProjectOptions + |> AMapAsync.mapAsyncAVal (fun _filePath (options) _ctok -> AsyncAVal.constant options) let allFilesParsed = allFSharpFilesAndProjectOptions - |> AMapAsync.mapAsyncAVal (fun filePath (loadedProject: Result) _ctok -> + |> AMapAsync.mapAsyncAVal (fun filePath (options: Result) _ctok -> asyncAVal { let! (checker: FSharpCompilerServiceChecker) = checker + and! selectProject = projectSelector + + let loadedProject = + options |> Result.bind (fun p -> selectProject.FindProject(filePath, p)) match loadedProject with | Ok x -> @@ -1332,8 +1337,8 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac | Error e -> return Error e }) - let getAllFilesToLoadedProject () = - allFSharpFilesAndProjectOptions + let getAllFilesToProjectOptions () = + allFilesToFSharpProjectOptions // |> AMap.toASetValues |> AMap.force |> HashMap.toArray @@ -1346,17 +1351,23 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let getAllFilesToProjectOptionsSelected () = async { - let! set = getAllFilesToLoadedProject () + let! set = getAllFilesToProjectOptions () + let selectProject = projectSelector |> AVal.force + let findProject file projects = selectProject.FindProject(file, projects) return set - |> Array.choose (fun (k, v) -> v |> Result.toOption |> Option.map (fun v -> k, v)) + |> Array.choose (fun (k, v) -> + v + |> Result.bind (findProject k) + |> Result.toOption + |> Option.map (fun v -> k, v)) } let getAllProjectOptions () = async { let! set = - allFSharpFilesAndProjectOptions + allFilesToFSharpProjectOptions |> AMap.toASetValues |> ASet.force |> HashSet.toArray @@ -1365,7 +1376,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let set = set |> Array.choose (Result.toOption) - return set + return set |> Array.collect (List.toArray) } let getAllFSharpProjectOptions () = @@ -1373,9 +1384,9 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac |> Async.map (Array.map (fun x -> AVal.force x.FSharpProjectSnapshot)) - let getLoadedProjectForFile (filePath: string) = + let getProjectOptionsForFile (filePath: string) = asyncAVal { - match! allFSharpFilesAndProjectOptions |> AMapAsync.tryFindA filePath with + match! allFilesToFSharpProjectOptions |> AMapAsync.tryFindA filePath with | Some projs -> return projs | None -> return Error $"Couldn't find project for {filePath}. Have you tried restoring your project/solution?" } @@ -1402,19 +1413,19 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac /// Gets Parse and Check results of a given file while also handling other concerns like Progress, Logging, Eventing. /// The FSharpCompilerServiceChecker. /// The name of the file in the project whose source to find a typecheck. - /// The options for the project or script. + /// The options for the project or script. /// Determines if the typecheck should be cached for autocompletions. /// let parseAndCheckFile (checker: FSharpCompilerServiceChecker) (file: VolatileFile) - (snapshot: FSharpProjectSnapshot) + (options: FSharpProjectSnapshot) shouldCache = async { let tags = [ SemanticConventions.fsac_sourceCodePath, box (UMX.untag file.Source.FileName) - SemanticConventions.projectFilePath, box (snapshot.ProjectFileName) ] + SemanticConventions.projectFilePath, box (options.ProjectFileName) ] use _ = fsacActivitySource.StartActivityForType(thisType, tags = tags) @@ -1434,7 +1445,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac do! progressReport.Begin($"Typechecking {simpleName}", message = $"{file.Source.FileName}") let! result = - checker.ParseAndCheckFileInProject(file.Source.FileName, snapshot, shouldCache = shouldCache) + checker.ParseAndCheckFileInProject(file.Source.FileName, options, shouldCache = shouldCache) |> Debug.measureAsync $"checker.ParseAndCheckFileInProject - {file.Source.FileName}" do! progressReport.End($"Typechecked {file.Source.FileName}") @@ -1457,7 +1468,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac ) - fileParsed.Trigger(parseAndCheck.GetParseResults, snapshot, ct) + fileParsed.Trigger(parseAndCheck.GetParseResults, options, ct) fileChecked.Trigger(parseAndCheck, file, ct) let checkErrors = parseAndCheck.GetParseResults.Diagnostics let parseErrors = parseAndCheck.GetCheckResults.Diagnostics @@ -1500,31 +1511,44 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac } let openFilesToRecentCheckedFilesResults = - openFilesToChangesAndLoadedProject - |> AMapAsync.mapAsyncAVal (fun _ (info, loadedProject) _ -> + openFilesToChangesAndProjectOptions + |> AMapAsync.mapAsyncAVal (fun _ (info, projectOptions) _ -> asyncAVal { let file = info.Source.FileName let! checker = checker + and! selectProject = projectSelector - match loadedProject with + let options = + result { + let! projectOptions = projectOptions + let! opts = selectProject.FindProject(file, projectOptions) + return opts + } + + match options with | Ok x -> let! snap = x.FSharpProjectSnapshot - return! - checker.TryGetRecentCheckResultsForFile(file, snap, info.Source) - |> AsyncResult.ofOption (fun () -> + return + checker.TryGetRecentCheckResultsForFile(file, snap) + |> Result.ofOption (fun () -> $"No recent typecheck results for {file}. This may be ok if the file has not been checked yet.") | Error e -> return Error e }) let openFilesToCheckedFilesResults = - openFilesToChangesAndLoadedProject - |> AMapAsync.mapAsyncAVal (fun _ (info, loadedProject) ctok -> + openFilesToChangesAndProjectOptions + |> AMapAsync.mapAsyncAVal (fun _ (info, projectOptions) ctok -> asyncAVal { let file = info.FileName let! checker = checker + and! selectProject = projectSelector - match loadedProject with + let options = + projectOptions |> Result.bind (fun p -> selectProject.FindProject(file, p)) + + + match options with | Error e -> return Error e | Ok x -> let! snap = x.FSharpProjectSnapshot @@ -1569,7 +1593,16 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac - let forceGetProjectOptions filePath = getLoadedProjectForFile filePath |> AsyncAVal.forceAsync + let forceGetProjectOptions filePath = + asyncAVal { + let! projects = getProjectOptionsForFile filePath + and! selectProject = projectSelector + + match projects with + | Ok projects -> return selectProject.FindProject(filePath, projects) + | Error e -> return Error e + } + |> AsyncAVal.forceAsync let forceGetFSharpProjectOptions filePath = forceGetProjectOptions filePath @@ -1730,10 +1763,12 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let getDeclarationLocation (symbolUse, text) = let getProjectOptions file = async { - let! projects = getLoadedProjectForFile file |> AsyncAVal.forceAsync + let! projects = getProjectOptionsForFile file |> AsyncAVal.forceAsync + let selectProject = projectSelector |> AVal.force return projects + |> Result.bind (fun p -> selectProject.FindProject(file, p)) |> Result.toOption |> Option.map (fun project -> AVal.force project.FSharpProjectSnapshot) @@ -1741,13 +1776,9 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let projectsThatContainFile file = async { - let! project = getLoadedProjectForFile file |> AsyncAVal.forceAsync - - let projects = - project - |> Result.map (fun p -> AVal.force p.FSharpProjectSnapshot |> List.singleton) - - return projects |> Result.defaultWith (fun _ -> []) + let! projects = getProjectOptionsForFile file |> AsyncAVal.forceAsync + let projects = projects |> Result.toOption |> Option.defaultValue [] + return projects |> List.map (fun p -> AVal.force p.FSharpProjectSnapshot) } SymbolLocation.getDeclarationLocation ( @@ -1984,9 +2015,12 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac if isInside (rootDir, fileDir) then return false else - let! projectOptions = getLoadedProjectForFile file + let! projectOptions = getProjectOptionsForFile file - match projectOptions with + match + projectOptions + |> Result.bind (fun projs -> selectProject.FindProject(file, projs)) + with | Error _ -> return true | Ok projectOptions -> if doesNotExist (UMX.tag projectOptions.ProjectFileName) then @@ -2033,40 +2067,49 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let getDependentFilesForFile file = async { - let! project = getLoadedProjectForFile file |> AsyncAVal.forceAsync + let! projects = getProjectOptionsForFile file |> AsyncAVal.forceAsync + let projects = projects |> Result.toOption |> Option.defaultValue [] - match project with - | Ok project -> - let sourceFiles = project.SourceFiles - let idx = sourceFiles |> Array.findIndex (fun x -> x = UMX.untag file) + return + projects + |> List.toArray + |> Array.collect (fun proj -> + logger.info ( + Log.setMessage "Source Files: {sourceFiles}" + >> Log.addContextDestructured "sourceFiles" proj.SourceFiles + ) + + let sourceFiles = proj.SourceFiles + let idx = sourceFiles |> Array.findIndex (fun x -> x = UMX.untag file) - return sourceFiles |> Array.splitAt idx |> snd - |> Array.map (fun sourceFile -> AVal.force project.FSharpProjectSnapshot, sourceFile) - - | Error _ -> return Array.empty - + |> Array.map (fun sourceFile -> AVal.force proj.FSharpProjectSnapshot, sourceFile)) + |> Array.distinct } let bypassAdaptiveAndCheckDependenciesForFile (sourceFilePath: string) = - asyncResult { + async { let tags = [ SemanticConventions.fsac_sourceCodePath, box (UMX.untag sourceFilePath) ] use _ = fsacActivitySource.StartActivityForType(thisType, tags = tags) let! dependentFiles = getDependentFilesForFile sourceFilePath - let! loadedProject = getLoadedProjectForFile sourceFilePath |> AsyncAVal.forceAsync + let! projs = getProjectOptionsForFile sourceFilePath |> AsyncAVal.forceAsync let rootToken = sourceFilePath |> getOpenFileTokenOrDefault - let snap = AVal.force loadedProject.FSharpProjectSnapshot + let projs = + projs + |> Result.toOption + |> Option.defaultValue [] + |> List.map (fun x -> AVal.force x.FSharpProjectSnapshot) - let dependentProjects = [ snap ] |> getDependentProjectsOfProjects + let dependentProjects = projs |> getDependentProjectsOfProjects let dependentProjectsAndSourceFiles = dependentProjects @@ -2107,10 +2150,17 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let token = resetCancellationToken file None // Dont dispose, we're a renter not an owner // and join with the root token as well since we want to cancel the whole operation if the root files changes CancellationTokenSource.CreateLinkedTokenSource(rootToken, token) + // CancellationTokenSource.CreateLinkedTokenSource(rootToken) + + try + let! _ = + bypassAdaptiveTypeCheck (file) (snap) + |> Async.withCancellation joinedToken.Token - let! _ = - bypassAdaptiveTypeCheck (file) (snap) - |> Async.withCancellation joinedToken.Token + () + with :? OperationCanceledException -> + // if a file shows up multiple times in the list such as Microsoft.NET.Test.Sdk.Program.fs we may cancel it but we don't want to stop the whole operation for it + () let checksCompleted = Interlocked.Increment(&checksCompleted) @@ -2129,7 +2179,6 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac message = $"0/{checksToPerform.Length} remaining", percentage = percentage 0 checksToPerform.Length ) - |> Async.AwaitCancellableTask do! checksToPerform |> Async.parallel75 |> Async.Ignore @@ -2211,8 +2260,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac textChanges.Remove filePath |> ignore) let! _ = forceGetOpenFileTypeCheckResults filePath - let! _ = bypassAdaptiveAndCheckDependenciesForFile filePath - return () + do! bypassAdaptiveAndCheckDependenciesForFile filePath } diff --git a/src/FsAutoComplete/LspServers/ProjectWorkspace.fs b/src/FsAutoComplete/LspServers/ProjectWorkspace.fs index 66e70aec7..5d78c855d 100644 --- a/src/FsAutoComplete/LspServers/ProjectWorkspace.fs +++ b/src/FsAutoComplete/LspServers/ProjectWorkspace.fs @@ -126,10 +126,11 @@ module Snapshots = let fileNorm = normalizePath fileName - let getSource () = task { - let! sourceText = SourceTextFactory.readFile fileNorm sourceTextFactory CancellationToken.None - return sourceText :> ISourceTextNew - } + let getSource () = + task { + let! sourceText = SourceTextFactory.readFile fileNorm sourceTextFactory CancellationToken.None + return sourceText :> ISourceTextNew + } return ProjectSnapshot.FSharpFileSnapshot.Create(fileName, string writeTime.Ticks, getSource) } From 01a710007944d55c93976a19175b12c9bb052abb Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Sun, 31 Mar 2024 14:08:40 -0400 Subject: [PATCH 16/60] Ensure project reloads happen correctly --- src/FsAutoComplete.Core/AdaptiveExtensions.fs | 11 +- .../FsAutoComplete.Core.fsproj | 2 +- src/FsAutoComplete.Core/SymbolLocation.fs | 4 +- src/FsAutoComplete/FsAutoComplete.fsproj | 3 +- .../LspServers/AdaptiveServerState.fs | 223 +++++++++++------- .../LspServers/ProjectWorkspace.fs | 12 +- .../LspServers/ProjectWorkspace.fsi | 25 ++ 7 files changed, 169 insertions(+), 111 deletions(-) create mode 100644 src/FsAutoComplete/LspServers/ProjectWorkspace.fsi diff --git a/src/FsAutoComplete.Core/AdaptiveExtensions.fs b/src/FsAutoComplete.Core/AdaptiveExtensions.fs index 080dd46ba..9521b0026 100644 --- a/src/FsAutoComplete.Core/AdaptiveExtensions.fs +++ b/src/FsAutoComplete.Core/AdaptiveExtensions.fs @@ -6,6 +6,7 @@ open FSharp.Data.Traceable open System.Threading.Tasks open IcedTasks open System.Threading +open FsAutoComplete [] @@ -616,13 +617,6 @@ module AsyncAVal = AdaptiveCancellableTask(cancel, real) } :> asyncaval<_> - // Getting values from AVals can block the current thread. We do a lot of conversions between AVals and AsyncAvals - // so we need to make sure we don't block the threads with too many of these conversions otherwise we might start - // having threadpool exhaustion issues. - let avalSyncLock = - // Should have enough parallelism to allow others to finish but not too much to cause threadpool exhaustion - new SemaphoreSlim(Environment.ProcessorCount, Environment.ProcessorCount) - /// /// Creates an async adaptive value evaluation the given value. /// @@ -641,8 +635,6 @@ module AsyncAVal = let real = task { - do! avalSyncLock.WaitAsync(cts.Token) - try // Start this work on the threadpool so we can return AdaptiveCancellableTask and let the system cancel if needed // We do this because tasks will stay on the current thread unless there is an yield or await in them. @@ -654,7 +646,6 @@ module AsyncAVal = cts.Token ) finally - avalSyncLock.Release() |> ignore cts.TryDispose() } diff --git a/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj b/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj index 7c17d65c9..6292da422 100644 --- a/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj +++ b/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj @@ -9,7 +9,7 @@ - + diff --git a/src/FsAutoComplete.Core/SymbolLocation.fs b/src/FsAutoComplete.Core/SymbolLocation.fs index 5f0edace5..0de74a2e1 100644 --- a/src/FsAutoComplete.Core/SymbolLocation.fs +++ b/src/FsAutoComplete.Core/SymbolLocation.fs @@ -17,7 +17,7 @@ let getDeclarationLocation currentDocument: IFSACSourceText, getProjectOptions, projectsThatContainFile: string -> Async, - getDependentProjectsOfProjects: FSharpProjectSnapshot list -> FSharpProjectSnapshot list + getDependentProjectsOfProjects: FSharpProjectSnapshot list -> Async ) : Async> = asyncOption { @@ -60,7 +60,7 @@ let getDeclarationLocation match! projectsThatContainFile (taggedFilePath) with | [] -> return! None | projectsThatContainFile -> - let projectsThatDependOnContainingProjects = + let! projectsThatDependOnContainingProjects = getDependentProjectsOfProjects projectsThatContainFile match projectsThatDependOnContainingProjects with diff --git a/src/FsAutoComplete/FsAutoComplete.fsproj b/src/FsAutoComplete/FsAutoComplete.fsproj index e0763796b..6ab3a229e 100644 --- a/src/FsAutoComplete/FsAutoComplete.fsproj +++ b/src/FsAutoComplete/FsAutoComplete.fsproj @@ -33,6 +33,7 @@ + @@ -49,7 +50,7 @@ - + fsautocomplete fsautocomplete diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index d85b141fe..b9ea62e49 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -722,7 +722,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac AdaptiveFile.GetLastWriteTimeUtc(UMX.untag filePath) |> AVal.map (fun writeTime -> filePath, writeTime) - let readFileFromDisk lastTouched (file: string) = + let createVolatileFileFromDisk lastTouched (file: string) = async { if File.Exists(UMX.untag file) then let! source = SourceTextFactory.readFile file sourceTextFactory @@ -909,8 +909,6 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac [ for p in projectOptions do UMX.tag p.ProjectFileName, (p, additionalDependencies p) ]) - - let openFilesTokens = ConcurrentDictionary, CancellationTokenSource>() @@ -1000,48 +998,66 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac return file }) - let snapshots: amap, (Types.ProjectOptions * aval)> = - amap { - let! (loader, wsp, binlogConfig) = - aval { - let! loader = - loader - |> addAValLogging (fun () -> logger.info (Log.setMessage "Loading projects because WorkspaceLoader change")) + let snapshots = + asyncAVal { + let! wsp = - and! wsp = adaptiveWorkspacePaths |> addAValLogging (fun () -> logger.info (Log.setMessage "Loading projects because adaptiveWorkspacePaths change")) + + match wsp with + | AdaptiveWorkspaceChosen.NotChosen -> return AMap.empty + | AdaptiveWorkspaceChosen.Projs projects -> + let! projects = asyncAVal { + let! loader = + loader + |> addAValLogging (fun () -> logger.info (Log.setMessage "Loading projects because WorkspaceLoader change")) and! binlogConfig = // AVal.constant Ionide.ProjInfo.BinaryLogGeneration.Off binlogConfig |> addAValLogging (fun () -> logger.info (Log.setMessage "Loading projects because binlogConfig change")) - return loader, wsp, binlogConfig + let! projects = + loadProjects loader binlogConfig projects + |> AMap.toAVal // need to convert to AVal to re-evaluate all snapshots when projects change + |> AsyncAVal.ofAVal // need to go async to allow awaiting on a single value which will keep the threadpool from being exhausted + + + + and! checker = checker + checker.ClearCaches() + return projects + } - match wsp with - | AdaptiveWorkspaceChosen.NotChosen -> () - | AdaptiveWorkspaceChosen.Projs projects -> - let projects = loadProjects loader binlogConfig projects + logger.info (Log.setMessageI $"After loading projects and before creating snapshots") - yield! Snapshots.createSnapshots openFilesWithChanges (AVal.constant sourceTextFactory) projects + return Snapshots.createSnapshots openFilesWithChanges (AVal.constant sourceTextFactory) (AMap.ofHashMap projects) } - let loadedProjects = - snapshots - |> AMap.map (fun _ (proj, snap) -> - { ProjectOptions = proj - FSharpProjectSnapshot = snap - LanguageVersion = LanguageVersionShim.fromOtherOptions proj.OtherOptions }) + let loadedProjects = asyncAVal { + let! snapshots = snapshots + return + snapshots + |> AMap.map (fun _ (proj, snap) -> + { ProjectOptions = proj + FSharpProjectSnapshot = snap + LanguageVersion = LanguageVersionShim.fromOtherOptions proj.OtherOptions }) + + } + + let getAllLoadedProjects = asyncAVal { + let! loadedProjects = loadedProjects + return! + loadedProjects + |> AMap.mapA (fun _ v -> v.FSharpProjectSnapshot |> AVal.map (fun _ -> v)) + |> AMap.toAVal + |> AVal.map HashMap.toValueList - let getAllLoadedProjects = - loadedProjects - |> AMap.mapA (fun _ v -> v.FSharpProjectSnapshot |> AVal.map (fun _ -> v)) - |> AMap.toAVal - |> AVal.map HashMap.toValueList + } /// @@ -1049,7 +1065,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac /// This should not be used inside the adaptive evaluation of other AdaptiveObjects since it does not track dependencies. /// /// A list of FSharpProjectOptions - let forceLoadProjects () = getAllLoadedProjects |> AVal.force + let forceLoadProjects () = getAllLoadedProjects |> AsyncAVal.forceAsync // do // // Reload Projects with some debouncing if `loadedProjectOptions` is out of date. @@ -1060,20 +1076,22 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac // |> disposables.Add - let sourceFileToProjectOptions = - - amap { - let! snaps = loadedProjects |> AMap.toAVal - - yield! - snaps - |> HashMap.toList - |> List.collect (fun (_, proj) -> - proj.SourceFiles - |> Array.toList - |> List.map (fun source -> Utils.normalizePath source, (proj)) - |> List.groupByFst) - } + let sourceFileToProjectOptions = asyncAVal { + let! loadedProjects = loadedProjects + return + amap { + let! snaps = loadedProjects |> AMap.toAVal + + yield! + snaps + |> HashMap.toList + |> List.collect (fun (_, proj) -> + proj.SourceFiles + |> Array.toList + |> List.map (fun source -> Utils.normalizePath source, (proj)) + |> List.groupByFst) + } + } let cancelToken filePath version (cts: CancellationTokenSource) = @@ -1143,7 +1161,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let lastTouched = File.getLastWriteTimeOrDefaultNow file - return! readFileFromDisk lastTouched file + return! createVolatileFileFromDisk lastTouched file with e -> logger.warn ( @@ -1295,6 +1313,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac return file, projs else + let! sourceFileToProjectOptions = sourceFileToProjectOptions let! projs = sourceFileToProjectOptions |> AMap.tryFindR @@ -1305,49 +1324,62 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac return file, projs }) - let allFSharpFilesAndProjectOptions = + let allFSharpFilesAndProjectOptions = asyncAVal { let wins = openFilesToChangesAndProjectOptions |> AMap.map (fun _k v -> v |> AsyncAVal.mapSync (fun (_, projects) _ -> projects)) + let! sourceFileToProjectOptions = sourceFileToProjectOptions + let loses = sourceFileToProjectOptions |> AMap.map (fun _ v -> AsyncAVal.constant (Ok v)) - AMap.union loses wins - - let allFilesToFSharpProjectOptions = - allFSharpFilesAndProjectOptions - |> AMapAsync.mapAsyncAVal (fun _filePath (options) _ctok -> AsyncAVal.constant options) - - let allFilesParsed = - allFSharpFilesAndProjectOptions - |> AMapAsync.mapAsyncAVal (fun filePath (options: Result) _ctok -> - asyncAVal { - let! (checker: FSharpCompilerServiceChecker) = checker - and! selectProject = projectSelector - - let loadedProject = - options |> Result.bind (fun p -> selectProject.FindProject(filePath, p)) + return AMap.union loses wins + } + + let allFilesToFSharpProjectOptions = asyncAVal { + let! allFSharpFilesAndProjectOptions = allFSharpFilesAndProjectOptions + return + allFSharpFilesAndProjectOptions + |> AMapAsync.mapAsyncAVal (fun _filePath (options) _ctok -> AsyncAVal.constant options) + } + + let allFilesParsed = asyncAVal { + let! allFSharpFilesAndProjectOptions = allFSharpFilesAndProjectOptions + return + allFSharpFilesAndProjectOptions + |> AMapAsync.mapAsyncAVal (fun filePath (options: Result) _ctok -> + asyncAVal { + let! (checker: FSharpCompilerServiceChecker) = checker + and! selectProject = projectSelector - match loadedProject with - | Ok x -> - let! snap = x.FSharpProjectSnapshot - let! r = parseFile checker filePath snap - return Ok r - | Error e -> return Error e - }) + let loadedProject = + options |> Result.bind (fun p -> selectProject.FindProject(filePath, p)) + + match loadedProject with + | Ok x -> + let! snap = x.FSharpProjectSnapshot + let! r = parseFile checker filePath snap + return Ok r + | Error e -> return Error e + }) + } + + let getAllFilesToProjectOptions () = async { + let! allFilesToFSharpProjectOptions = allFilesToFSharpProjectOptions |> AsyncAVal.forceAsync + return! + allFilesToFSharpProjectOptions + // |> AMap.toASetValues + |> AMap.force + |> HashMap.toArray + |> Array.map (fun (sourceTextPath, projects) -> + async { + let! projs = AsyncAVal.forceAsync projects + return sourceTextPath, projs + }) + |> Async.parallel75 + } - let getAllFilesToProjectOptions () = - allFilesToFSharpProjectOptions - // |> AMap.toASetValues - |> AMap.force - |> HashMap.toArray - |> Array.map (fun (sourceTextPath, projects) -> - async { - let! projs = AsyncAVal.forceAsync projects - return sourceTextPath, projs - }) - |> Async.parallel75 let getAllFilesToProjectOptionsSelected () = async { @@ -1366,6 +1398,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let getAllProjectOptions () = async { + let! allFilesToFSharpProjectOptions = allFilesToFSharpProjectOptions |> AsyncAVal.forceAsync let! set = allFilesToFSharpProjectOptions |> AMap.toASetValues @@ -1386,6 +1419,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let getProjectOptionsForFile (filePath: string) = asyncAVal { + let! allFilesToFSharpProjectOptions = allFilesToFSharpProjectOptions match! allFilesToFSharpProjectOptions |> AMapAsync.tryFindA filePath with | Some projs -> return projs | None -> return Error $"Couldn't find project for {filePath}. Have you tried restoring your project/solution?" @@ -1565,9 +1599,12 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac }) - let getParseResults filePath = - allFilesParsed - |> AMapAsync.tryFindAndFlattenR $"No parse results found for {filePath}" filePath + let getParseResults filePath = asyncAVal { + let! allFilesParsed = allFilesParsed + return! + allFilesParsed + |> AMapAsync.tryFindAndFlattenR $"No parse results found for {filePath}" filePath + } let getOpenFileTypeCheckResults filePath = openFilesToCheckedFilesResults @@ -1662,12 +1699,16 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac } |> AsyncAVal.forceAsync - let allFilesToDeclarations = - allFilesParsed - |> AMap.map (fun _k v -> v |> AsyncAVal.mapResult (fun p _ -> p.GetNavigationItems().Declarations)) + let allFilesToDeclarations = asyncAVal { + let! allFilesParsed = allFilesParsed + return + allFilesParsed + |> AMap.map (fun _k v -> v |> AsyncAVal.mapResult (fun p _ -> p.GetNavigationItems().Declarations)) + } let getAllDeclarations () = async { + let! allFilesToDeclarations = allFilesToDeclarations |> AsyncAVal.forceAsync let! results = allFilesToDeclarations |> AMap.force @@ -1686,7 +1727,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let getDeclarations filename = allFilesToDeclarations - |> AMapAsync.tryFindAndFlattenR $"Could not find getDeclarations for {filename}" filename + |> AsyncAVal.bind(fun a _ -> AMapAsync.tryFindAndFlattenR $"Could not find getDeclarations for {filename}" filename a) let codeGenServer = { new ICodeGenerationService with @@ -1727,8 +1768,8 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac member x.ParseFileInProject(file) = forceGetParseResults file |> Async.map (Option.ofResult) } - let getDependentProjectsOfProjects (ps: FSharpProjectSnapshot list) = - let projectSnapshot = forceLoadProjects () + let getDependentProjectsOfProjects (ps: FSharpProjectSnapshot list) = async { + let! projectSnapshot = forceLoadProjects () let allDependents = System.Collections.Generic.HashSet<_>() @@ -1757,8 +1798,10 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac currentPass.Clear() currentPass.AddRange(dependents |> Seq.map (fun p -> p.ProjectFileName)) - Seq.toList allDependents - |> List.filter (fun p -> p.ProjectFileName.EndsWith(".fsproj")) + return + Seq.toList allDependents + |> List.filter (fun p -> p.ProjectFileName.EndsWith(".fsproj")) + } let getDeclarationLocation (symbolUse, text) = let getProjectOptions file = @@ -2109,7 +2152,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac |> Option.defaultValue [] |> List.map (fun x -> AVal.force x.FSharpProjectSnapshot) - let dependentProjects = projs |> getDependentProjectsOfProjects + let! dependentProjects = projs |> getDependentProjectsOfProjects let dependentProjectsAndSourceFiles = dependentProjects diff --git a/src/FsAutoComplete/LspServers/ProjectWorkspace.fs b/src/FsAutoComplete/LspServers/ProjectWorkspace.fs index 5d78c855d..6fc8aba8b 100644 --- a/src/FsAutoComplete/LspServers/ProjectWorkspace.fs +++ b/src/FsAutoComplete/LspServers/ProjectWorkspace.fs @@ -119,12 +119,12 @@ module Snapshots = unresolvedReferences originalLoadReferences - let private createFSharpFileSnapshotOnDisk (sourceTextFactory: aval) fileName = + let private createFSharpFileSnapshotOnDisk (sourceTextFactory: aval) sourceFilePath = aval { - let! writeTime = AdaptiveFile.GetLastWriteTimeUtc fileName + let! writeTime = AdaptiveFile.GetLastWriteTimeUtc sourceFilePath and! sourceTextFactory = sourceTextFactory - let fileNorm = normalizePath fileName + let fileNorm = normalizePath sourceFilePath let getSource () = task { @@ -132,7 +132,7 @@ module Snapshots = return sourceText :> ISourceTextNew } - return ProjectSnapshot.FSharpFileSnapshot.Create(fileName, string writeTime.Ticks, getSource) + return ProjectSnapshot.FSharpFileSnapshot.Create(sourceFilePath, string writeTime.Ticks, getSource) } let private createFSharpFileSnapshotInMemory (v: VolatileFile) = @@ -215,7 +215,6 @@ module Snapshots = snapshot | _ -> - aval { logger.debug ( Log.setMessage "optionsToSnapshot - Cache miss - {projectFileName}" >> Log.addContextDestructured "projectFileName" p.ProjectFileName @@ -272,8 +271,7 @@ module Snapshots = cachedSnapshots.Add(normPath, snap) - return! snap - } + snap let createSnapshots (inMemorySourceFiles: amap, aval>) diff --git a/src/FsAutoComplete/LspServers/ProjectWorkspace.fsi b/src/FsAutoComplete/LspServers/ProjectWorkspace.fsi new file mode 100644 index 000000000..4f8863980 --- /dev/null +++ b/src/FsAutoComplete/LspServers/ProjectWorkspace.fsi @@ -0,0 +1,25 @@ +namespace FsAutoComplete.ProjectWorkspace + +open System + +module Snapshots = + open System + open FsAutoComplete + open System.Threading + open FSharp.UMX + open System.Threading.Tasks + open Ionide.ProjInfo.Types + open FSharp.Compiler.CodeAnalysis.ProjectSnapshot + open System.IO + open FSharp.Compiler.CodeAnalysis + open FSharp.Data.Adaptive + open FSharp.Compiler.Text + open FsAutoComplete.Adaptive + open Ionide.ProjInfo.Logging + open System.Collections.Generic + + val createSnapshots: + inMemorySourceFiles: amap, aval> -> + sourceTextFactory: aval -> + loadedProjectsA: amap, ProjectOptions> -> + amap, (ProjectOptions * aval)> From 13a9ddde5a148bc6f8a29d6ba89163bb4b7c95e6 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Sun, 31 Mar 2024 14:20:00 -0400 Subject: [PATCH 17/60] Enable all tests --- test/FsAutoComplete.Tests.Lsp/Program.fs | 217 ++++++++++++----------- 1 file changed, 109 insertions(+), 108 deletions(-) diff --git a/test/FsAutoComplete.Tests.Lsp/Program.fs b/test/FsAutoComplete.Tests.Lsp/Program.fs index 362d160e7..d3643369b 100644 --- a/test/FsAutoComplete.Tests.Lsp/Program.fs +++ b/test/FsAutoComplete.Tests.Lsp/Program.fs @@ -34,10 +34,10 @@ let testTimeout = Environment.SetEnvironmentVariable("FSAC_WORKSPACELOAD_DELAY", "250") let loaders = - [ "Ionide WorkspaceLoader", - (fun toolpath -> WorkspaceLoader.Create(toolpath, FsAutoComplete.Core.ProjectLoader.globalProperties)) - "MSBuild Project Graph WorkspaceLoader", (fun toolpath -> WorkspaceLoaderViaProjectGraph.Create(toolpath, FsAutoComplete.Core.ProjectLoader.globalProperties)) - ] + [ + "Ionide WorkspaceLoader", (fun toolpath -> WorkspaceLoader.Create(toolpath, FsAutoComplete.Core.ProjectLoader.globalProperties)) + // "MSBuild Project Graph WorkspaceLoader", (fun toolpath -> WorkspaceLoaderViaProjectGraph.Create(toolpath, FsAutoComplete.Core.ProjectLoader.globalProperties)) + ] let adaptiveLspServerFactory toolsPath workspaceLoaderFactory sourceTextFactory = @@ -116,120 +116,121 @@ let generalTests = testList "general" [ [] let tests = testList "FSAC" [ - // generalTests; lspTests + generalTests + lspTests SnapshotTests.snapshotTests loaders toolsPath ] [] let main args = - // let outputTemplate = - // "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}" - - // let parseLogLevel (args: string[]) = - // let logMarker = "--log=" - - // let logLevel = - // match - // args - // |> Array.tryFind (fun arg -> arg.StartsWith(logMarker, StringComparison.Ordinal)) - // |> Option.map (fun log -> log.Substring(logMarker.Length)) - // with - // | Some("warn" | "warning") -> Logging.LogLevel.Warn - // | Some "error" -> Logging.LogLevel.Error - // | Some "fatal" -> Logging.LogLevel.Fatal - // | Some "info" -> Logging.LogLevel.Info - // | Some "verbose" -> Logging.LogLevel.Verbose - // | Some "debug" -> Logging.LogLevel.Debug - // | _ -> Logging.LogLevel.Warn - - // let args = - // args - // |> Array.filter (fun arg -> not <| arg.StartsWith(logMarker, StringComparison.Ordinal)) - - // logLevel, args - - // let expectoToSerilogLevel = - // function - // | Logging.LogLevel.Debug -> LogEventLevel.Debug - // | Logging.LogLevel.Verbose -> LogEventLevel.Verbose - // | Logging.LogLevel.Info -> LogEventLevel.Information - // | Logging.LogLevel.Warn -> LogEventLevel.Warning - // | Logging.LogLevel.Error -> LogEventLevel.Error - // | Logging.LogLevel.Fatal -> LogEventLevel.Fatal - - // let parseLogExcludes (args: string[]) = - // let excludeMarker = "--exclude-from-log=" - - // let toExclude = - // args - // |> Array.filter (fun arg -> arg.StartsWith(excludeMarker, StringComparison.Ordinal)) - // |> Array.collect (fun arg -> arg.Substring(excludeMarker.Length).Split(',')) - - // let args = - // args - // |> Array.filter (fun arg -> not <| arg.StartsWith(excludeMarker, StringComparison.Ordinal)) - - // toExclude, args - - // let logLevel, args = parseLogLevel args - // let switch = LoggingLevelSwitch(expectoToSerilogLevel logLevel) - // let logSourcesToExclude, args = parseLogExcludes args - - // let sourcesToExclude = - // Matching.WithProperty( - // Constants.SourceContextPropertyName, - // fun s -> s <> null && logSourcesToExclude |> Array.contains s - // ) - - // let argsToRemove, _loaders = - // args - // |> Array.windowed 2 - // |> Array.tryPick (function - // | [| "--loader"; "ionide" |] as args -> Some(args, [ "Ionide WorkspaceLoader", WorkspaceLoader.Create ]) - // | [| "--loader"; "graph" |] as args -> - // Some(args, [ "MSBuild Project Graph WorkspaceLoader", WorkspaceLoaderViaProjectGraph.Create ]) - // | _ -> None) - // |> Option.defaultValue ([||], loaders) - - // let serilogLogger = - // LoggerConfiguration() - // .Enrich.FromLogContext() - // .MinimumLevel.ControlledBy(switch) - // .Filter.ByExcluding(Matching.FromSource("FileSystem")) - // .Filter.ByExcluding(sourcesToExclude) - - // .Destructure.FSharpTypes() - // .Destructure.ByTransforming(fun r -> - // box - // {| FileName = r.FileName - // Start = r.Start - // End = r.End |}) - // .Destructure.ByTransforming(fun r -> box {| Line = r.Line; Column = r.Column |}) - // .Destructure.ByTransforming(fun tok -> tok.ToString() |> box) - // .Destructure.ByTransforming(fun di -> box di.FullName) - // .WriteTo.Async(fun c -> - // c.Console( - // outputTemplate = outputTemplate, - // standardErrorFromLevel = Nullable<_>(LogEventLevel.Verbose), - // theme = Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme.Code - // ) - // |> ignore) - // .CreateLogger() // make it so that every console log is logged to stderr - - // // uncomment these next two lines if you want verbose output from the LSP server _during_ your tests - // Serilog.Log.Logger <- serilogLogger - // LogProvider.setLoggerProvider (Providers.SerilogProvider.create ()) - - // let fixedUpArgs = args |> Array.except argsToRemove + let outputTemplate = + "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}" + + let parseLogLevel (args: string[]) = + let logMarker = "--log=" + + let logLevel = + match + args + |> Array.tryFind (fun arg -> arg.StartsWith(logMarker, StringComparison.Ordinal)) + |> Option.map (fun log -> log.Substring(logMarker.Length)) + with + | Some("warn" | "warning") -> Logging.LogLevel.Warn + | Some "error" -> Logging.LogLevel.Error + | Some "fatal" -> Logging.LogLevel.Fatal + | Some "info" -> Logging.LogLevel.Info + | Some "verbose" -> Logging.LogLevel.Verbose + | Some "debug" -> Logging.LogLevel.Debug + | _ -> Logging.LogLevel.Warn + + let args = + args + |> Array.filter (fun arg -> not <| arg.StartsWith(logMarker, StringComparison.Ordinal)) + + logLevel, args + + let expectoToSerilogLevel = + function + | Logging.LogLevel.Debug -> LogEventLevel.Debug + | Logging.LogLevel.Verbose -> LogEventLevel.Verbose + | Logging.LogLevel.Info -> LogEventLevel.Information + | Logging.LogLevel.Warn -> LogEventLevel.Warning + | Logging.LogLevel.Error -> LogEventLevel.Error + | Logging.LogLevel.Fatal -> LogEventLevel.Fatal + + let parseLogExcludes (args: string[]) = + let excludeMarker = "--exclude-from-log=" + + let toExclude = + args + |> Array.filter (fun arg -> arg.StartsWith(excludeMarker, StringComparison.Ordinal)) + |> Array.collect (fun arg -> arg.Substring(excludeMarker.Length).Split(',')) + + let args = + args + |> Array.filter (fun arg -> not <| arg.StartsWith(excludeMarker, StringComparison.Ordinal)) + + toExclude, args + + let logLevel, args = parseLogLevel args + let switch = LoggingLevelSwitch(expectoToSerilogLevel logLevel) + let logSourcesToExclude, args = parseLogExcludes args + + let sourcesToExclude = + Matching.WithProperty( + Constants.SourceContextPropertyName, + fun s -> s <> null && logSourcesToExclude |> Array.contains s + ) + + let argsToRemove, _loaders = + args + |> Array.windowed 2 + |> Array.tryPick (function + | [| "--loader"; "ionide" |] as args -> Some(args, [ "Ionide WorkspaceLoader", WorkspaceLoader.Create ]) + | [| "--loader"; "graph" |] as args -> + Some(args, [ "MSBuild Project Graph WorkspaceLoader", WorkspaceLoaderViaProjectGraph.Create ]) + | _ -> None) + |> Option.defaultValue ([||], loaders) + + let serilogLogger = + LoggerConfiguration() + .Enrich.FromLogContext() + .MinimumLevel.ControlledBy(switch) + .Filter.ByExcluding(Matching.FromSource("FileSystem")) + .Filter.ByExcluding(sourcesToExclude) + + .Destructure.FSharpTypes() + .Destructure.ByTransforming(fun r -> + box + {| FileName = r.FileName + Start = r.Start + End = r.End |}) + .Destructure.ByTransforming(fun r -> box {| Line = r.Line; Column = r.Column |}) + .Destructure.ByTransforming(fun tok -> tok.ToString() |> box) + .Destructure.ByTransforming(fun di -> box di.FullName) + .WriteTo.Async(fun c -> + c.Console( + outputTemplate = outputTemplate, + standardErrorFromLevel = Nullable<_>(LogEventLevel.Verbose), + theme = Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme.Code + ) + |> ignore) + .CreateLogger() // make it so that every console log is logged to stderr + + // uncomment these next two lines if you want verbose output from the LSP server _during_ your tests + Serilog.Log.Logger <- serilogLogger + LogProvider.setLoggerProvider (Providers.SerilogProvider.create ()) + + let fixedUpArgs = args |> Array.except argsToRemove let cts = new CancellationTokenSource(testTimeout) let cliArgs = [ - // CLIArguments.Printer(Expecto.Impl.TestPrinters.summaryWithLocationPrinter defaultConfig.printer) + CLIArguments.Printer(Expecto.Impl.TestPrinters.summaryWithLocationPrinter defaultConfig.printer) CLIArguments.Verbosity Expecto.Logging.LogLevel.Info - // // CLIArguments.Parallel - ] + CLIArguments.Parallel + ] - runTestsWithCLIArgsAndCancel cts.Token cliArgs args tests + runTestsWithCLIArgsAndCancel cts.Token cliArgs fixedUpArgs tests From 8a87f44a6e865f4e7c318098162d391cd4154509 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Sun, 31 Mar 2024 14:26:53 -0400 Subject: [PATCH 18/60] formatting --- src/FsAutoComplete.Core/SymbolLocation.fs | 3 +- .../LspServers/AdaptiveServerState.fs | 312 ++++++++++-------- .../LspServers/ProjectWorkspace.fs | 112 +++---- .../LspServers/ProjectWorkspace.fsi | 38 +-- 4 files changed, 246 insertions(+), 219 deletions(-) diff --git a/src/FsAutoComplete.Core/SymbolLocation.fs b/src/FsAutoComplete.Core/SymbolLocation.fs index 0de74a2e1..39ad76eb7 100644 --- a/src/FsAutoComplete.Core/SymbolLocation.fs +++ b/src/FsAutoComplete.Core/SymbolLocation.fs @@ -60,8 +60,7 @@ let getDeclarationLocation match! projectsThatContainFile (taggedFilePath) with | [] -> return! None | projectsThatContainFile -> - let! projectsThatDependOnContainingProjects = - getDependentProjectsOfProjects projectsThatContainFile + let! projectsThatDependOnContainingProjects = getDependentProjectsOfProjects projectsThatContainFile match projectsThatDependOnContainingProjects with | [] -> return (SymbolDeclarationLocation.Projects(projectsThatContainFile, isSymbolLocalForProject)) diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index b9ea62e49..f45a2c75f 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -1002,62 +1002,71 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac asyncAVal { let! wsp = - adaptiveWorkspacePaths - |> addAValLogging (fun () -> - logger.info (Log.setMessage "Loading projects because adaptiveWorkspacePaths change")) + adaptiveWorkspacePaths + |> addAValLogging (fun () -> + logger.info (Log.setMessage "Loading projects because adaptiveWorkspacePaths change")) match wsp with | AdaptiveWorkspaceChosen.NotChosen -> return AMap.empty | AdaptiveWorkspaceChosen.Projs projects -> - let! projects = asyncAVal { - let! loader = - loader - |> addAValLogging (fun () -> logger.info (Log.setMessage "Loading projects because WorkspaceLoader change")) - and! binlogConfig = - // AVal.constant Ionide.ProjInfo.BinaryLogGeneration.Off - binlogConfig - |> addAValLogging (fun () -> logger.info (Log.setMessage "Loading projects because binlogConfig change")) + let! projects = + asyncAVal { + let! loader = + loader + |> addAValLogging (fun () -> + logger.info (Log.setMessage "Loading projects because WorkspaceLoader change")) - let! projects = - loadProjects loader binlogConfig projects - |> AMap.toAVal // need to convert to AVal to re-evaluate all snapshots when projects change - |> AsyncAVal.ofAVal // need to go async to allow awaiting on a single value which will keep the threadpool from being exhausted + and! binlogConfig = + // AVal.constant Ionide.ProjInfo.BinaryLogGeneration.Off + binlogConfig + |> addAValLogging (fun () -> logger.info (Log.setMessage "Loading projects because binlogConfig change")) + let! projects = + loadProjects loader binlogConfig projects + |> AMap.toAVal // need to convert to AVal to re-evaluate all snapshots when projects change + |> AsyncAVal.ofAVal // need to go async to allow awaiting on a single value which will keep the threadpool from being exhausted - and! checker = checker - checker.ClearCaches() - return projects - } + and! checker = checker + checker.ClearCaches() + return projects + + } logger.info (Log.setMessageI $"After loading projects and before creating snapshots") - return Snapshots.createSnapshots openFilesWithChanges (AVal.constant sourceTextFactory) (AMap.ofHashMap projects) + + return + Snapshots.createSnapshots openFilesWithChanges (AVal.constant sourceTextFactory) (AMap.ofHashMap projects) } - let loadedProjects = asyncAVal { - let! snapshots = snapshots - return - snapshots - |> AMap.map (fun _ (proj, snap) -> - { ProjectOptions = proj - FSharpProjectSnapshot = snap - LanguageVersion = LanguageVersionShim.fromOtherOptions proj.OtherOptions }) + let loadedProjects = + asyncAVal { + let! snapshots = snapshots - } + return + snapshots + |> AMap.map (fun _ (proj, snap) -> + { ProjectOptions = proj + FSharpProjectSnapshot = snap + LanguageVersion = LanguageVersionShim.fromOtherOptions proj.OtherOptions }) - let getAllLoadedProjects = asyncAVal { - let! loadedProjects = loadedProjects - return! - loadedProjects - |> AMap.mapA (fun _ v -> v.FSharpProjectSnapshot |> AVal.map (fun _ -> v)) - |> AMap.toAVal - |> AVal.map HashMap.toValueList + } - } + let getAllLoadedProjects = + asyncAVal { + let! loadedProjects = loadedProjects + + return! + loadedProjects + |> AMap.mapA (fun _ v -> v.FSharpProjectSnapshot |> AVal.map (fun _ -> v)) + |> AMap.toAVal + |> AVal.map HashMap.toValueList + + } /// @@ -1076,22 +1085,24 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac // |> disposables.Add - let sourceFileToProjectOptions = asyncAVal { - let! loadedProjects = loadedProjects - return - amap { - let! snaps = loadedProjects |> AMap.toAVal - - yield! - snaps - |> HashMap.toList - |> List.collect (fun (_, proj) -> - proj.SourceFiles - |> Array.toList - |> List.map (fun source -> Utils.normalizePath source, (proj)) - |> List.groupByFst) - } - } + let sourceFileToProjectOptions = + asyncAVal { + let! loadedProjects = loadedProjects + + return + amap { + let! snaps = loadedProjects |> AMap.toAVal + + yield! + snaps + |> HashMap.toList + |> List.collect (fun (_, proj) -> + proj.SourceFiles + |> Array.toList + |> List.map (fun source -> Utils.normalizePath source, (proj)) + |> List.groupByFst) + } + } let cancelToken filePath version (cts: CancellationTokenSource) = @@ -1314,6 +1325,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac return file, projs else let! sourceFileToProjectOptions = sourceFileToProjectOptions + let! projs = sourceFileToProjectOptions |> AMap.tryFindR @@ -1324,61 +1336,68 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac return file, projs }) - let allFSharpFilesAndProjectOptions = asyncAVal { - let wins = - openFilesToChangesAndProjectOptions - |> AMap.map (fun _k v -> v |> AsyncAVal.mapSync (fun (_, projects) _ -> projects)) + let allFSharpFilesAndProjectOptions = + asyncAVal { + let wins = + openFilesToChangesAndProjectOptions + |> AMap.map (fun _k v -> v |> AsyncAVal.mapSync (fun (_, projects) _ -> projects)) - let! sourceFileToProjectOptions = sourceFileToProjectOptions + let! sourceFileToProjectOptions = sourceFileToProjectOptions - let loses = - sourceFileToProjectOptions |> AMap.map (fun _ v -> AsyncAVal.constant (Ok v)) + let loses = + sourceFileToProjectOptions |> AMap.map (fun _ v -> AsyncAVal.constant (Ok v)) - return AMap.union loses wins - } + return AMap.union loses wins + } - let allFilesToFSharpProjectOptions = asyncAVal { - let! allFSharpFilesAndProjectOptions = allFSharpFilesAndProjectOptions - return - allFSharpFilesAndProjectOptions - |> AMapAsync.mapAsyncAVal (fun _filePath (options) _ctok -> AsyncAVal.constant options) - } + let allFilesToFSharpProjectOptions = + asyncAVal { + let! allFSharpFilesAndProjectOptions = allFSharpFilesAndProjectOptions - let allFilesParsed = asyncAVal { - let! allFSharpFilesAndProjectOptions = allFSharpFilesAndProjectOptions - return - allFSharpFilesAndProjectOptions - |> AMapAsync.mapAsyncAVal (fun filePath (options: Result) _ctok -> - asyncAVal { - let! (checker: FSharpCompilerServiceChecker) = checker - and! selectProject = projectSelector + return + allFSharpFilesAndProjectOptions + |> AMapAsync.mapAsyncAVal (fun _filePath (options) _ctok -> AsyncAVal.constant options) + } - let loadedProject = - options |> Result.bind (fun p -> selectProject.FindProject(filePath, p)) - - match loadedProject with - | Ok x -> - let! snap = x.FSharpProjectSnapshot - let! r = parseFile checker filePath snap - return Ok r - | Error e -> return Error e - }) - } - - let getAllFilesToProjectOptions () = async { - let! allFilesToFSharpProjectOptions = allFilesToFSharpProjectOptions |> AsyncAVal.forceAsync - return! - allFilesToFSharpProjectOptions - // |> AMap.toASetValues - |> AMap.force - |> HashMap.toArray - |> Array.map (fun (sourceTextPath, projects) -> - async { - let! projs = AsyncAVal.forceAsync projects - return sourceTextPath, projs - }) - |> Async.parallel75 - } + let allFilesParsed = + asyncAVal { + let! allFSharpFilesAndProjectOptions = allFSharpFilesAndProjectOptions + + return + allFSharpFilesAndProjectOptions + |> AMapAsync.mapAsyncAVal (fun filePath (options: Result) _ctok -> + asyncAVal { + let! (checker: FSharpCompilerServiceChecker) = checker + and! selectProject = projectSelector + + let loadedProject = + options |> Result.bind (fun p -> selectProject.FindProject(filePath, p)) + + match loadedProject with + | Ok x -> + let! snap = x.FSharpProjectSnapshot + let! r = parseFile checker filePath snap + return Ok r + | Error e -> return Error e + }) + } + + let getAllFilesToProjectOptions () = + async { + let! allFilesToFSharpProjectOptions = allFilesToFSharpProjectOptions |> AsyncAVal.forceAsync + + return! + allFilesToFSharpProjectOptions + // |> AMap.toASetValues + |> AMap.force + |> HashMap.toArray + |> Array.map (fun (sourceTextPath, projects) -> + async { + let! projs = AsyncAVal.forceAsync projects + return sourceTextPath, projs + }) + |> Async.parallel75 + } let getAllFilesToProjectOptionsSelected () = @@ -1399,6 +1418,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let getAllProjectOptions () = async { let! allFilesToFSharpProjectOptions = allFilesToFSharpProjectOptions |> AsyncAVal.forceAsync + let! set = allFilesToFSharpProjectOptions |> AMap.toASetValues @@ -1420,6 +1440,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let getProjectOptionsForFile (filePath: string) = asyncAVal { let! allFilesToFSharpProjectOptions = allFilesToFSharpProjectOptions + match! allFilesToFSharpProjectOptions |> AMapAsync.tryFindA filePath with | Some projs -> return projs | None -> return Error $"Couldn't find project for {filePath}. Have you tried restoring your project/solution?" @@ -1599,12 +1620,14 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac }) - let getParseResults filePath = asyncAVal { - let! allFilesParsed = allFilesParsed - return! - allFilesParsed - |> AMapAsync.tryFindAndFlattenR $"No parse results found for {filePath}" filePath - } + let getParseResults filePath = + asyncAVal { + let! allFilesParsed = allFilesParsed + + return! + allFilesParsed + |> AMapAsync.tryFindAndFlattenR $"No parse results found for {filePath}" filePath + } let getOpenFileTypeCheckResults filePath = openFilesToCheckedFilesResults @@ -1699,16 +1722,19 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac } |> AsyncAVal.forceAsync - let allFilesToDeclarations = asyncAVal { - let! allFilesParsed = allFilesParsed - return - allFilesParsed - |> AMap.map (fun _k v -> v |> AsyncAVal.mapResult (fun p _ -> p.GetNavigationItems().Declarations)) - } + let allFilesToDeclarations = + asyncAVal { + let! allFilesParsed = allFilesParsed + + return + allFilesParsed + |> AMap.map (fun _k v -> v |> AsyncAVal.mapResult (fun p _ -> p.GetNavigationItems().Declarations)) + } let getAllDeclarations () = async { let! allFilesToDeclarations = allFilesToDeclarations |> AsyncAVal.forceAsync + let! results = allFilesToDeclarations |> AMap.force @@ -1727,7 +1753,8 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let getDeclarations filename = allFilesToDeclarations - |> AsyncAVal.bind(fun a _ -> AMapAsync.tryFindAndFlattenR $"Could not find getDeclarations for {filename}" filename a) + |> AsyncAVal.bind (fun a _ -> + AMapAsync.tryFindAndFlattenR $"Could not find getDeclarations for {filename}" filename a) let codeGenServer = { new ICodeGenerationService with @@ -1768,40 +1795,41 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac member x.ParseFileInProject(file) = forceGetParseResults file |> Async.map (Option.ofResult) } - let getDependentProjectsOfProjects (ps: FSharpProjectSnapshot list) = async { - let! projectSnapshot = forceLoadProjects () + let getDependentProjectsOfProjects (ps: FSharpProjectSnapshot list) = + async { + let! projectSnapshot = forceLoadProjects () - let allDependents = System.Collections.Generic.HashSet<_>() + let allDependents = System.Collections.Generic.HashSet<_>() - let currentPass = ResizeArray() - currentPass.AddRange(ps |> List.map (fun p -> p.ProjectFileName)) + let currentPass = ResizeArray() + currentPass.AddRange(ps |> List.map (fun p -> p.ProjectFileName)) - let mutable continueAlong = true + let mutable continueAlong = true - while continueAlong do - let dependents = - projectSnapshot - |> Seq.filter (fun p -> - (AVal.force p.FSharpProjectSnapshot).ReferencedProjects - |> Seq.exists (fun r -> - match r.ProjectFilePath with - | None -> false - | Some p -> currentPass.Contains(p))) + while continueAlong do + let dependents = + projectSnapshot + |> Seq.filter (fun p -> + (AVal.force p.FSharpProjectSnapshot).ReferencedProjects + |> Seq.exists (fun r -> + match r.ProjectFilePath with + | None -> false + | Some p -> currentPass.Contains(p))) - if Seq.isEmpty dependents then - continueAlong <- false - currentPass.Clear() - else - for d in dependents do - allDependents.Add(AVal.force d.FSharpProjectSnapshot) |> ignore + if Seq.isEmpty dependents then + continueAlong <- false + currentPass.Clear() + else + for d in dependents do + allDependents.Add(AVal.force d.FSharpProjectSnapshot) |> ignore - currentPass.Clear() - currentPass.AddRange(dependents |> Seq.map (fun p -> p.ProjectFileName)) + currentPass.Clear() + currentPass.AddRange(dependents |> Seq.map (fun p -> p.ProjectFileName)) - return - Seq.toList allDependents - |> List.filter (fun p -> p.ProjectFileName.EndsWith(".fsproj")) - } + return + Seq.toList allDependents + |> List.filter (fun p -> p.ProjectFileName.EndsWith(".fsproj")) + } let getDeclarationLocation (symbolUse, text) = let getProjectOptions file = diff --git a/src/FsAutoComplete/LspServers/ProjectWorkspace.fs b/src/FsAutoComplete/LspServers/ProjectWorkspace.fs index 6fc8aba8b..bccd0ea87 100644 --- a/src/FsAutoComplete/LspServers/ProjectWorkspace.fs +++ b/src/FsAutoComplete/LspServers/ProjectWorkspace.fs @@ -215,63 +215,63 @@ module Snapshots = snapshot | _ -> - logger.debug ( - Log.setMessage "optionsToSnapshot - Cache miss - {projectFileName}" - >> Log.addContextDestructured "projectFileName" p.ProjectFileName - ) + logger.debug ( + Log.setMessage "optionsToSnapshot - Cache miss - {projectFileName}" + >> Log.addContextDestructured "projectFileName" p.ProjectFileName + ) - let projectName = AVal.constant p.ProjectFileName - let projectId = p.ProjectId |> AVal.constant - - - let sourceFiles = // alist because order matters for the F# Compiler - p.SourceFiles - |> AList.ofList - |> AList.map (fun sourcePath -> - let normPath = Utils.normalizePath sourcePath - - aval { - match! inMemorySourceFiles |> AMap.tryFind normPath with - | Some volatileFile -> return! volatileFile |> AVal.map createFSharpFileSnapshotInMemory - | None -> return! createFSharpFileSnapshotOnDisk sourceTextFactory sourcePath - }) - - let references, otherOptions = - p.OtherOptions |> List.partition (fun x -> x.StartsWith("-r:")) - - let otherOptions = otherOptions |> ASet.ofList |> ASet.map (AVal.constant) - - let referencePaths = - references - |> ASet.ofList - |> ASet.map (fun referencePath -> - referencePath.Substring(3) // remove "-r:" - |> createReferenceOnDisk) - - let referencedProjects = mapReferences p - let isIncompleteTypeCheckEnvironment = AVal.constant false - let useScriptResolutionRules = AVal.constant false - let loadTime = AVal.constant p.LoadTime - let unresolvedReferences = AVal.constant None - let originalLoadReferences = AVal.constant [] - - let snap = - makeAdaptiveFCSSnapshot2 - projectName - projectId - sourceFiles - referencePaths - otherOptions - referencedProjects - isIncompleteTypeCheckEnvironment - useScriptResolutionRules - loadTime - unresolvedReferences - originalLoadReferences - - cachedSnapshots.Add(normPath, snap) - - snap + let projectName = AVal.constant p.ProjectFileName + let projectId = p.ProjectId |> AVal.constant + + + let sourceFiles = // alist because order matters for the F# Compiler + p.SourceFiles + |> AList.ofList + |> AList.map (fun sourcePath -> + let normPath = Utils.normalizePath sourcePath + + aval { + match! inMemorySourceFiles |> AMap.tryFind normPath with + | Some volatileFile -> return! volatileFile |> AVal.map createFSharpFileSnapshotInMemory + | None -> return! createFSharpFileSnapshotOnDisk sourceTextFactory sourcePath + }) + + let references, otherOptions = + p.OtherOptions |> List.partition (fun x -> x.StartsWith("-r:")) + + let otherOptions = otherOptions |> ASet.ofList |> ASet.map (AVal.constant) + + let referencePaths = + references + |> ASet.ofList + |> ASet.map (fun referencePath -> + referencePath.Substring(3) // remove "-r:" + |> createReferenceOnDisk) + + let referencedProjects = mapReferences p + let isIncompleteTypeCheckEnvironment = AVal.constant false + let useScriptResolutionRules = AVal.constant false + let loadTime = AVal.constant p.LoadTime + let unresolvedReferences = AVal.constant None + let originalLoadReferences = AVal.constant [] + + let snap = + makeAdaptiveFCSSnapshot2 + projectName + projectId + sourceFiles + referencePaths + otherOptions + referencedProjects + isIncompleteTypeCheckEnvironment + useScriptResolutionRules + loadTime + unresolvedReferences + originalLoadReferences + + cachedSnapshots.Add(normPath, snap) + + snap let createSnapshots (inMemorySourceFiles: amap, aval>) diff --git a/src/FsAutoComplete/LspServers/ProjectWorkspace.fsi b/src/FsAutoComplete/LspServers/ProjectWorkspace.fsi index 4f8863980..5ddc244c8 100644 --- a/src/FsAutoComplete/LspServers/ProjectWorkspace.fsi +++ b/src/FsAutoComplete/LspServers/ProjectWorkspace.fsi @@ -3,23 +3,23 @@ namespace FsAutoComplete.ProjectWorkspace open System module Snapshots = - open System - open FsAutoComplete - open System.Threading - open FSharp.UMX - open System.Threading.Tasks - open Ionide.ProjInfo.Types - open FSharp.Compiler.CodeAnalysis.ProjectSnapshot - open System.IO - open FSharp.Compiler.CodeAnalysis - open FSharp.Data.Adaptive - open FSharp.Compiler.Text - open FsAutoComplete.Adaptive - open Ionide.ProjInfo.Logging - open System.Collections.Generic + open System + open FsAutoComplete + open System.Threading + open FSharp.UMX + open System.Threading.Tasks + open Ionide.ProjInfo.Types + open FSharp.Compiler.CodeAnalysis.ProjectSnapshot + open System.IO + open FSharp.Compiler.CodeAnalysis + open FSharp.Data.Adaptive + open FSharp.Compiler.Text + open FsAutoComplete.Adaptive + open Ionide.ProjInfo.Logging + open System.Collections.Generic - val createSnapshots: - inMemorySourceFiles: amap, aval> -> - sourceTextFactory: aval -> - loadedProjectsA: amap, ProjectOptions> -> - amap, (ProjectOptions * aval)> + val createSnapshots: + inMemorySourceFiles: amap, aval> -> + sourceTextFactory: aval -> + loadedProjectsA: amap, ProjectOptions> -> + amap, (ProjectOptions * aval)> From cfefbd86c65be8ef4ce279b3efbec52fb1fa95f8 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Sun, 31 Mar 2024 14:26:53 -0400 Subject: [PATCH 19/60] formatting --- test/FsAutoComplete.Tests.Lsp/CoreTests.fs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/FsAutoComplete.Tests.Lsp/CoreTests.fs b/test/FsAutoComplete.Tests.Lsp/CoreTests.fs index fbc24265a..bcd0b6994 100644 --- a/test/FsAutoComplete.Tests.Lsp/CoreTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CoreTests.fs @@ -165,7 +165,8 @@ let foldingTests state = } |> Async.Cache - testList + testSequenced + <| testList "folding tests" [ testCaseAsync "can get ranges for sample file" From cd6898f69b3443e544024d72f02ec86805f9a0fb Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Mon, 15 Apr 2024 09:36:47 -0400 Subject: [PATCH 20/60] fix path normalization issues --- .../LspServers/ProjectWorkspace.fs | 45 +++++++++++-------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/src/FsAutoComplete/LspServers/ProjectWorkspace.fs b/src/FsAutoComplete/LspServers/ProjectWorkspace.fs index bccd0ea87..2790051c8 100644 --- a/src/FsAutoComplete/LspServers/ProjectWorkspace.fs +++ b/src/FsAutoComplete/LspServers/ProjectWorkspace.fs @@ -119,20 +119,19 @@ module Snapshots = unresolvedReferences originalLoadReferences - let private createFSharpFileSnapshotOnDisk (sourceTextFactory: aval) sourceFilePath = + let private createFSharpFileSnapshotOnDisk (sourceTextFactory: aval) (sourceFilePath: string) = aval { - let! writeTime = AdaptiveFile.GetLastWriteTimeUtc sourceFilePath + let file = UMX.untag sourceFilePath + let! writeTime = AdaptiveFile.GetLastWriteTimeUtc file and! sourceTextFactory = sourceTextFactory - let fileNorm = normalizePath sourceFilePath - let getSource () = task { - let! sourceText = SourceTextFactory.readFile fileNorm sourceTextFactory CancellationToken.None + let! sourceText = SourceTextFactory.readFile sourceFilePath sourceTextFactory CancellationToken.None return sourceText :> ISourceTextNew } - return ProjectSnapshot.FSharpFileSnapshot.Create(sourceFilePath, string writeTime.Ticks, getSource) + return ProjectSnapshot.FSharpFileSnapshot.Create(file, string writeTime.Ticks, getSource) } let private createFSharpFileSnapshotInMemory (v: VolatileFile) = @@ -141,6 +140,7 @@ module Snapshots = // it's useful for keeping the cache consistent in FCS so when someone opens a file we don't need to re-issue type-checks let version = v.LastTouched.Ticks let getSource () = v.Source :> ISourceTextNew |> Task.FromResult + ProjectSnapshot.FSharpFileSnapshot.Create(file, string version, getSource) let private createReferenceOnDisk path : aval = @@ -221,25 +221,25 @@ module Snapshots = ) let projectName = AVal.constant p.ProjectFileName - let projectId = p.ProjectId |> AVal.constant + let projectId = AVal.constant p.ProjectId let sourceFiles = // alist because order matters for the F# Compiler p.SourceFiles |> AList.ofList + |> AList.map Utils.normalizePath |> AList.map (fun sourcePath -> - let normPath = Utils.normalizePath sourcePath aval { - match! inMemorySourceFiles |> AMap.tryFind normPath with + match! inMemorySourceFiles |> AMap.tryFind sourcePath with | Some volatileFile -> return! volatileFile |> AVal.map createFSharpFileSnapshotInMemory | None -> return! createFSharpFileSnapshotOnDisk sourceTextFactory sourcePath }) - let references, otherOptions = - p.OtherOptions |> List.partition (fun x -> x.StartsWith("-r:")) + let references = + p.OtherOptions |> List.filter (fun x -> x.StartsWith("-r:")) - let otherOptions = otherOptions |> ASet.ofList |> ASet.map (AVal.constant) + let otherOptions = p.OtherOptions |> ASet.ofList |> ASet.map (AVal.constant) let referencePaths = references @@ -278,14 +278,21 @@ module Snapshots = (sourceTextFactory: aval) (loadedProjectsA: amap, ProjectOptions>) = - let cachedSnapshots = Dictionary<_, _>() - let mapReferences = - createReferences cachedSnapshots inMemorySourceFiles sourceTextFactory loadedProjectsA - - let optionsToSnapshot = - optionsToSnapshot cachedSnapshots inMemorySourceFiles sourceTextFactory mapReferences loadedProjectsA |> AMap.filter (fun k _ -> (UMX.untag k).EndsWith ".fsproj") - |> AMap.map (fun _ v -> v, optionsToSnapshot v) + |> AMap.toAVal + |> AVal.map (fun ps -> + let cachedSnapshots = Dictionary<_, _>() + + let mapReferences = + createReferences cachedSnapshots inMemorySourceFiles sourceTextFactory loadedProjectsA + + let optionsToSnapshot = + optionsToSnapshot cachedSnapshots inMemorySourceFiles sourceTextFactory mapReferences + + ps + |> HashMap.map (fun _ v -> (v, optionsToSnapshot v)) + ) + |> AMap.ofAVal From 7657d795deac053def2a28461f19286e147bd68a Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Mon, 15 Apr 2024 09:37:12 -0400 Subject: [PATCH 21/60] fix stacktraces when adaptive has failures --- src/FsAutoComplete.Core/AdaptiveExtensions.fs | 121 +++++++++++------- .../AdaptiveExtensions.fsi | 4 +- .../LspServers/AdaptiveFSharpLspServer.fs | 6 +- .../LspServers/AdaptiveServerState.fs | 99 +++++++------- .../LspServers/AdaptiveServerState.fsi | 1 - src/FsAutoComplete/LspServers/Common.fs | 5 +- .../LspServers/FSharpLspClient.fs | 4 +- 7 files changed, 136 insertions(+), 104 deletions(-) diff --git a/src/FsAutoComplete.Core/AdaptiveExtensions.fs b/src/FsAutoComplete.Core/AdaptiveExtensions.fs index 9521b0026..a50f02374 100644 --- a/src/FsAutoComplete.Core/AdaptiveExtensions.fs +++ b/src/FsAutoComplete.Core/AdaptiveExtensions.fs @@ -7,39 +7,60 @@ open System.Threading.Tasks open IcedTasks open System.Threading open FsAutoComplete - +open FsAutoComplete.Logging +open FsAutoComplete.Logging.Types [] module AdaptiveExtensions = + let rec logger = LogProvider.getLoggerByQuotation <@ logger @> + open System.Runtime.ExceptionServices type CancellationTokenSource with member cts.TryCancel() = try - cts.Cancel() + if not <| isNull cts then + cts.Cancel() with | :? ObjectDisposedException | :? NullReferenceException -> () member cts.TryDispose() = // try - cts.Dispose() - // with _ -> () + if not <| isNull cts then + cts.Dispose() + // with _ -> () type TaskCompletionSource<'a> with /// https://github.com/dotnet/runtime/issues/47998 member tcs.TrySetFromTask(real: Task<'a>) = - task { - try - let! r = real - tcs.TrySetResult r |> ignore - with - | :? OperationCanceledException as x -> tcs.TrySetCanceled(x.CancellationToken) |> ignore - | ex -> tcs.TrySetException ex |> ignore - } - |> ignore> + + real.ContinueWith(fun (task : Task<_>) -> + match task.Status with + | TaskStatus.RanToCompletion -> tcs.TrySetResult task.Result |> ignore + | TaskStatus.Canceled -> tcs.TrySetCanceled(TaskCanceledException(task).CancellationToken) |> ignore + | TaskStatus.Faulted -> + logger.error( + Log.setMessage "Error in TrySetFromTask with {count}" + >> Log.addExn task.Exception.InnerException + >> Log.addContext "count" task.Exception.InnerExceptions.Count + ) + logger.warn( + Log.setMessage "task.Exception.StackTrace {trace}" + >> Log.addContext "trace" task.Exception.StackTrace + ) + logger.warn( + Log.setMessage "task.Exception.StackTrace {trace}" + >> Log.addContext "trace" task.Exception.InnerException.StackTrace + ) + // if task.Exception.StackTrace = null then + tcs.TrySetException(task.Exception.InnerExceptions) |> ignore + // else + // tcs.TrySetException(task.Exception) |> ignore + | _ -> ()) + |> ignore type ChangeableHashMap<'Key, 'Value> with @@ -386,7 +407,8 @@ type internal RefCountingTaskCreator<'a>(create: CancellationToken -> Task<'a>) /// Upon cancellation, it will run the cancel function passed in and set cancellation for the task completion source. /// and AdaptiveCancellableTask<'a>(cancel: unit -> unit, real: Task<'a>) = - let cts = new CancellationTokenSource() + // let cts = new CancellationTokenSource() + let mutable cachedTcs: TaskCompletionSource<'a> = null let mutable cached: Task<'a> = null let getTask () = @@ -394,14 +416,9 @@ and AdaptiveCancellableTask<'a>(cancel: unit -> unit, real: Task<'a>) = if real.IsCompleted then real else - task { - let tcs = new TaskCompletionSource<'a>() - use _s = cts.Token.Register(fun () -> tcs.TrySetCanceled(cts.Token) |> ignore) - - tcs.TrySetFromTask real - - return! tcs.Task - } + cachedTcs <- new TaskCompletionSource<'a>() + cachedTcs.TrySetFromTask real + cachedTcs.Task cached <- match cached with @@ -417,10 +434,12 @@ and AdaptiveCancellableTask<'a>(cancel: unit -> unit, real: Task<'a>) = cached /// Will run the cancel function passed into the constructor and set the output Task to cancelled state. - member x.Cancel() = + member x.Cancel(cancellationToken : CancellationToken) = lock x (fun () -> cancel () - cts.TryCancel()) + if not <| isNull cachedTcs then + cachedTcs.TrySetCanceled(cancellationToken) |> ignore + ) /// The output of the passed in task to the constructor. /// @@ -435,17 +454,17 @@ module CancellableTask = let inline ofAdaptiveCancellableTask (ct: AdaptiveCancellableTask<_>) = fun (ctok: CancellationToken) -> task { - use _ = ctok.Register(fun () -> ct.Cancel()) + use _ = ctok.Register(fun () -> ct.Cancel(ctok)) return! ct.Task } module Async = /// Converts AdaptiveCancellableTask to an Async. let inline ofAdaptiveCancellableTask (ct: AdaptiveCancellableTask<_>) = - async { + asyncEx { let! ctok = Async.CancellationToken - use _ = ctok.Register(fun () -> ct.Cancel()) - return! ct.Task |> Async.AwaitTask + use _ = ctok.Register(fun () -> ct.Cancel(ctok)) + return! ct.Task } [] @@ -479,7 +498,7 @@ module AsyncAVal = /// This follows Async semantics and is not already running. /// let forceAsync (value: asyncaval<_>) = - async { + asyncEx { let ct = value.GetValue(AdaptiveToken.Top) return! Async.ofAdaptiveCancellableTask ct } @@ -666,13 +685,19 @@ module AsyncAVal = if x.OutOfDate || Option.isNone cache then let ref = RefCountingTaskCreator( - cancellableTask { - let! i = input.GetValue t + fun ct -> task { + let v = input.GetValue t + + use _s = + ct.Register(fun () -> + v.Cancel(ct)) + + let! i = v.Task match dataCache with | ValueSome(struct (oa, ob)) when Utils.cheapEqual oa i -> return ob | _ -> - let! b = mapping i + let! b = mapping i ct dataCache <- ValueSome(struct (i, b)) return b } @@ -712,16 +737,14 @@ module AsyncAVal = if x.OutOfDate || Option.isNone cache then let ref = RefCountingTaskCreator( - cancellableTask { + fun ct -> task { let ta = ca.GetValue t let tb = cb.GetValue t - let! ct = CancellableTask.getCancellationToken () - use _s = ct.Register(fun () -> - ta.Cancel() - tb.Cancel()) + ta.Cancel(ct) + tb.Cancel(ct)) let! ia = ta.Task let! ib = tb.Task @@ -768,13 +791,17 @@ module AsyncAVal = if Interlocked.Exchange(&inputChanged, 0) = 1 || Option.isNone cache then let outerTask = RefCountingTaskCreator( - cancellableTask { - let! i = value.GetValue t + fun ct -> task { + let v = value.GetValue t + use _s = + ct.Register(fun () -> + v.Cancel(ct)) + + let! i = v.Task match outerDataCache with | Some(struct (oa, ob)) when Utils.cheapEqual oa i -> return ob | _ -> - let! ct = CancellableTask.getCancellationToken () let inner = mapping i ct outerDataCache <- Some(i, inner) return inner @@ -788,16 +815,22 @@ module AsyncAVal = let ref = RefCountingTaskCreator( - cancellableTask { - let! ct = CancellableTask.getCancellationToken () + fun ct -> task { + + let inner = outerTask.New() + + use _s = + ct.Register(fun () -> + inner.Cancel(ct)) + + let! inner = inner.Task - let! inner = outerTask.New() lock inners (fun () -> inners.Value <- HashSet.add inner inners.Value) let innerTask = inner.GetValue t use _s2 = ct.Register(fun () -> - innerTask.Cancel() + innerTask.Cancel(ct) lock inners (fun () -> inners.Value <- HashSet.remove inner inners.Value) inner.Outputs.Remove x |> ignore) diff --git a/src/FsAutoComplete.Core/AdaptiveExtensions.fsi b/src/FsAutoComplete.Core/AdaptiveExtensions.fsi index 32991c961..f41ee508b 100644 --- a/src/FsAutoComplete.Core/AdaptiveExtensions.fsi +++ b/src/FsAutoComplete.Core/AdaptiveExtensions.fsi @@ -1,5 +1,7 @@ namespace FsAutoComplete.Adaptive +open System.Threading + [] module AdaptiveExtensions = @@ -178,7 +180,7 @@ and [] AdaptiveCancellableTask<'a> = new: cancel: (unit -> unit) * real: System.Threading.Tasks.Task<'a> -> AdaptiveCancellableTask<'a> /// Will run the cancel function passed into the constructor and set the output Task to cancelled state. - member Cancel: unit -> unit + member Cancel: CancellationToken -> unit /// The output of the passed in task to the constructor. /// diff --git a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs index 2e939211b..134ff2cad 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs @@ -166,7 +166,7 @@ type AdaptiveFSharpLspServer do! rootPath |> Option.map (fun rootPath -> - async { + asyncEx { let dotConfig = Path.Combine(rootPath, ".config", "dotnet-tools.json") if not (File.Exists dotConfig) then @@ -177,7 +177,6 @@ type AdaptiveFSharpLspServer .WithWorkingDirectory(rootPath) .ExecuteBufferedAsync() .Task - |> Async.AwaitTask if result.ExitCode <> 0 then fantomasLogger.warn ( @@ -195,7 +194,6 @@ type AdaptiveFSharpLspServer .WithWorkingDirectory(rootPath) .ExecuteBufferedAsync() .Task - |> Async.AwaitTask if result.ExitCode <> 0 then fantomasLogger.warn ( @@ -211,7 +209,6 @@ type AdaptiveFSharpLspServer .WithWorkingDirectory(rootPath) .ExecuteBufferedAsync() .Task - |> Async.AwaitTask if result.ExitCode = 0 then fantomasLogger.info (Log.setMessage (sprintf "fantomas was installed locally at %A" rootPath)) @@ -237,7 +234,6 @@ type AdaptiveFSharpLspServer .WithArguments("tool install -g fantomas") .ExecuteBufferedAsync() .Task - |> Async.AwaitTask if result.ExitCode = 0 then fantomasLogger.info (Log.setMessage "fantomas was installed globally") diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index f45a2c75f..0aca3e75f 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -73,14 +73,13 @@ type LoadedProject = override x.GetHashCode() = x.ProjectOptions.GetHashCode() - override x.Equals(other: obj) = match other with | :? LoadedProject as other -> (x :> IEquatable<_>).Equals other | _ -> false - member x.SourceFiles = - x.ProjectOptions.SourceFiles |> List.map (fun f -> f) |> List.toArray + member x.SourceFilesTagged = + x.ProjectOptions.SourceFiles |> List.map Utils.normalizePath |> List.toArray member x.ProjectFileName = x.ProjectOptions.ProjectFileName @@ -95,9 +94,10 @@ type FindFirstProject() = member x.FindProject(sourceFile, projects) = projects |> Seq.sortBy (fun p -> p.ProjectFileName) - |> Seq.tryFind (fun p -> p.SourceFiles |> Array.exists (fun f -> f = UMX.untag sourceFile)) + |> Seq.tryFind (fun p -> p.SourceFilesTagged |> Array.exists (fun f -> f = sourceFile)) |> Result.ofOption (fun () -> - $"Couldn't find a corresponding project for {sourceFile}. Have the projects loaded yet or have you tried restoring your project/solution?") + let allProjects = String.join ", " (projects |> Seq.map (fun p -> p.ProjectFileName)) + $"Couldn't find a corresponding project for {sourceFile}. \n Projects include {allProjects}. \nHave the projects loaded yet or have you tried restoring your project/solution?") type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFactory, workspaceLoader: IWorkspaceLoader) @@ -287,7 +287,8 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac try logger.info (Log.setMessageI $"Test Detection of {parseResults.FileName:file} started") - let fn = UMX.tag parseResults.FileName + let fn = Utils.normalizePath parseResults.FileName + let res = if proj.OtherOptions |> Seq.exists (fun o -> o.Contains "Expecto.dll") then @@ -887,7 +888,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac [ let projectFileChanges = projectFileChanges p.ProjectFileName match p.Properties with - | ProjectAssetsFile v -> yield projectFileChanges (UMX.tag v) + | ProjectAssetsFile v -> yield projectFileChanges (Utils.normalizePath v) | _ -> () let objPath = (|BaseIntermediateOutputPath|_|) p.Properties @@ -902,12 +903,12 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac yield! v |> Array.filter (fun x -> x.EndsWith(".props", StringComparison.Ordinal) && isWithinObjFolder x) - |> Array.map (UMX.tag >> projectFileChanges) + |> Array.map (Utils.normalizePath >> projectFileChanges) | _ -> () ] HashMap.ofList [ for p in projectOptions do - UMX.tag p.ProjectFileName, (p, additionalDependencies p) ]) + Utils.normalizePath p.ProjectFileName, (p, additionalDependencies p) ]) let openFilesTokens = ConcurrentDictionary, CancellationTokenSource>() @@ -1001,7 +1002,6 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let snapshots = asyncAVal { let! wsp = - adaptiveWorkspacePaths |> addAValLogging (fun () -> logger.info (Log.setMessage "Loading projects because adaptiveWorkspacePaths change")) @@ -1018,25 +1018,20 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac logger.info (Log.setMessage "Loading projects because WorkspaceLoader change")) and! binlogConfig = - // AVal.constant Ionide.ProjInfo.BinaryLogGeneration.Off binlogConfig |> addAValLogging (fun () -> logger.info (Log.setMessage "Loading projects because binlogConfig change")) let! projects = + // need to bind to a single value to keep the threadpool from being exhausted as LoadingProjects can be a long running operation + // and when other adaptive values await on this, the scheduler won't block those other tasks loadProjects loader binlogConfig projects - |> AMap.toAVal // need to convert to AVal to re-evaluate all snapshots when projects change - |> AsyncAVal.ofAVal // need to go async to allow awaiting on a single value which will keep the threadpool from being exhausted - - + |> AMap.toAVal and! checker = checker checker.ClearCaches() return projects - } - - logger.info (Log.setMessageI $"After loading projects and before creating snapshots") return @@ -1084,24 +1079,27 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac // |> Observable.subscribe (fun _ -> forceLoadProjects () |> ignore>) // |> disposables.Add + let AMapReKeyMany f map = + map + |> AMap.toASet + |> ASet.collect f + |> AMap.ofASet let sourceFileToProjectOptions = asyncAVal { let! loadedProjects = loadedProjects - return - amap { - let! snaps = loadedProjects |> AMap.toAVal - - yield! - snaps - |> HashMap.toList - |> List.collect (fun (_, proj) -> - proj.SourceFiles - |> Array.toList - |> List.map (fun source -> Utils.normalizePath source, (proj)) - |> List.groupByFst) - } + let sourceFileToProjectOptions = + loadedProjects + |> AMapReKeyMany(fun (_,v) -> + v.SourceFilesTagged + |> ASet.ofArray + |> ASet.map(fun source -> source, v) + ) + |> AMap.map' HashSet.toList + + return sourceFileToProjectOptions + } let cancelToken filePath version (cts: CancellationTokenSource) = @@ -1205,7 +1203,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac /// /// let parseFile (checker: FSharpCompilerServiceChecker) (source) snap = - async { + task { let! result = checker.ParseFile(source, snap) let! ct = Async.CancellationToken @@ -1228,15 +1226,15 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac |> ASet.mapA id |> ASet.toAVal - return + return! projects |> HashSet.toArray |> Array.collect (fun (snap) -> snap.SourceFiles |> List.toArray |> Array.map (fun s -> snap, s)) |> Array.map (fun (snap, fileName) -> - let filePath = UMX.tag fileName.FileName + let filePath = Utils.normalizePath fileName.FileName parseFile checker filePath snap) - |> Async.parallel75 + |> Task.WhenAll } let forceFindSourceText filePath = forceFindOpenFileOrRead filePath |> AsyncResult.map (fun f -> f.Source) @@ -1477,10 +1475,15 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac (options: FSharpProjectSnapshot) shouldCache = - async { + asyncEx { let tags = - [ SemanticConventions.fsac_sourceCodePath, box (UMX.untag file.Source.FileName) - SemanticConventions.projectFilePath, box (options.ProjectFileName) ] + [ + SemanticConventions.fsac_sourceCodePath, box (UMX.untag file.Source.FileName) + SemanticConventions.projectFilePath, box (options.ProjectFileName) + "source.text", box (file.Source.String) + "source.version", box (file.Version) + + ] use _ = fsacActivitySource.StartActivityForType(thisType, tags = tags) @@ -1503,8 +1506,6 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac checker.ParseAndCheckFileInProject(file.Source.FileName, options, shouldCache = shouldCache) |> Debug.measureAsync $"checker.ParseAndCheckFileInProject - {file.Source.FileName}" - do! progressReport.End($"Typechecked {file.Source.FileName}") - notifications.Trigger(NotificationEvent.FileParsed(file.Source.FileName), ct) match result with @@ -1704,7 +1705,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac return tryGetLastCheckResultForFile filePath |> AsyncResult.orElseWith (fun _ -> forceGetOpenFileRecentTypeCheckResults filePath) - |> AsyncResult.orElseWith (fun _ -> forceGetOpenFileTypeCheckResults filePath) + |> AsyncResult.orElseWith (fun _ -> forceGetOpenFileTypeCheckResultsOrCheck filePath) |> Async.map (fun r -> Async.Start( async { @@ -1786,7 +1787,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac if symbol.Kind = kind then let! (text) = forceFindOpenFileOrRead fileName |> Async.map Option.ofResult let! line = tryGetLineStr pos text.Source |> Option.ofResult - let! tyRes = forceGetOpenFileTypeCheckResults fileName |> Async.map (Option.ofResult) + let! tyRes = forceGetOpenFileTypeCheckResultsOrCheck fileName |> Async.map (Option.ofResult) let symbolUse = tyRes.TryGetSymbolUse pos line return! Some(symbol, symbolUse) else @@ -1876,7 +1877,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac if File.Exists(UMX.untag file) then // `FSharpChecker.FindBackgroundReferencesInFile` only works with existing files - return! checker.FindReferencesForSymbolInFile(UMX.untag file, project, symbol) + return! checker.FindReferencesForSymbolInFile(file, project, symbol) else // untitled script files match! forceGetOpenFileTypeCheckResultsStale file with @@ -2094,7 +2095,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac with | Error _ -> return true | Ok projectOptions -> - if doesNotExist (UMX.tag projectOptions.ProjectFileName) then + if doesNotExist (Utils.normalizePath projectOptions.ProjectFileName) then return true // script file else // issue: fs-file does never get removed from project options (-> requires reload of FSAC to register) @@ -2147,11 +2148,11 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac |> Array.collect (fun proj -> logger.info ( Log.setMessage "Source Files: {sourceFiles}" - >> Log.addContextDestructured "sourceFiles" proj.SourceFiles + >> Log.addContextDestructured "sourceFiles" proj.SourceFilesTagged ) - let sourceFiles = proj.SourceFiles - let idx = sourceFiles |> Array.findIndex (fun x -> x = UMX.untag file) + let sourceFiles = proj.SourceFilesTagged + let idx = sourceFiles |> Array.findIndex (fun x -> x = file) sourceFiles |> Array.splitAt idx @@ -2184,7 +2185,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let dependentProjectsAndSourceFiles = dependentProjects - |> List.collect (fun (snap) -> snap.SourceFiles |> List.map (fun sourceFile -> snap, sourceFile.FileName)) + |> List.collect (fun (snap) -> snap.SourceFiles |> List.map (fun sourceFile -> snap, Utils.normalizePath sourceFile.FileName)) |> List.toArray let mutable checksCompleted = 0 @@ -2201,6 +2202,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let innerChecks = Array.concat [| dependentFiles; dependentProjectsAndSourceFiles |] |> Array.filter (fun (_, file) -> + let file = UMX.untag file file.Contains "AssemblyInfo.fs" |> not && file.Contains "AssemblyAttributes.fs" |> not) @@ -2209,7 +2211,6 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac innerChecks |> Array.map (fun (snap, file) -> async { - let file = UMX.tag file use joinedToken = if file = sourceFilePath then diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi b/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi index 72bceb95b..a9327a5d4 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi @@ -37,7 +37,6 @@ type LoadedProject = interface IEquatable override GetHashCode: unit -> int override Equals: other: obj -> bool - member SourceFiles: string array member ProjectFileName: string // static member op_Implicit: x: LoadedProject -> FSharpProjectSnapshot diff --git a/src/FsAutoComplete/LspServers/Common.fs b/src/FsAutoComplete/LspServers/Common.fs index 318eeda76..2676d3e39 100644 --- a/src/FsAutoComplete/LspServers/Common.fs +++ b/src/FsAutoComplete/LspServers/Common.fs @@ -143,13 +143,14 @@ type DiagnosticCollection(sendDiagnostics: DocumentUri -> Diagnostic[] -> Async< module Async = open System.Threading.Tasks + open IcedTasks let rec logger = LogProvider.getLoggerByQuotation <@ logger @> let inline logCancelled e = logger.trace (Log.setMessage "Operation Cancelled" >> Log.addExn e) let withCancellation (ct: CancellationToken) (a: Async<'a>) : Async<'a> = - async { + asyncEx { let! ct2 = Async.CancellationToken use cts = CancellationTokenSource.CreateLinkedTokenSource(ct, ct2) let tcs = new TaskCompletionSource<'a>() @@ -165,7 +166,7 @@ module Async = } Async.Start(a, cts.Token) - return! tcs.Task |> Async.AwaitTask + return! tcs.Task } let withCancellationSafe ct work = diff --git a/src/FsAutoComplete/LspServers/FSharpLspClient.fs b/src/FsAutoComplete/LspServers/FSharpLspClient.fs index e1526feb8..ea8cc96aa 100644 --- a/src/FsAutoComplete/LspServers/FSharpLspClient.fs +++ b/src/FsAutoComplete/LspServers/FSharpLspClient.fs @@ -288,13 +288,13 @@ type ProgressListener(lspClient: FSharpLspClient, traceNamespace: string array) interface IAsyncDisposable with member this.DisposeAsync() : ValueTask = // was getting a compile error for the state machine in CI to `task` - async { + asyncEx { if not isDisposed then isDisposed <- true dispose listener for (a, p) in inflightEvents.Values do - do! (disposeAsync p).AsTask() |> Async.AwaitTask + do! (disposeAsync p).AsTask() inflightEvents.TryRemove(a.Id) |> ignore } |> Async.StartImmediateAsTask From f53b3393ca4caf4db6e87faae2f89a4ffd6d40a3 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Mon, 15 Apr 2024 09:38:41 -0400 Subject: [PATCH 22/60] fix normalization issues --- src/FsAutoComplete.Core/Commands.fs | 4 +- .../CompilerServiceInterface.fs | 52 ++++++++++++++----- .../CompilerServiceInterface.fsi | 2 +- .../ParseAndCheckResults.fs | 4 +- src/FsAutoComplete.Core/SymbolLocation.fs | 2 +- 5 files changed, 46 insertions(+), 18 deletions(-) diff --git a/src/FsAutoComplete.Core/Commands.fs b/src/FsAutoComplete.Core/Commands.fs index 069e8983a..85f3638be 100644 --- a/src/FsAutoComplete.Core/Commands.fs +++ b/src/FsAutoComplete.Core/Commands.fs @@ -799,7 +799,7 @@ module Commands = yield! project.ReferencedProjects - |> List.map (fun p -> UMX.tag p.OutputFile |> tryGetProjectOptionsForFsproj) ] + |> List.map (fun p -> Utils.normalizePath p.OutputFile |> tryGetProjectOptionsForFsproj) ] |> Async.parallel75 @@ -890,7 +890,7 @@ module Commands = // -> map `Error` to `Some` for `Async.Choice`, afterwards map `Some` back to `Error` [ for project in projects do for file in project.SourceFiles do - let file = UMX.tag file.FileName + let file = Utils.normalizePath file.FileName async { match! tryFindReferencesInFile (file, project) with diff --git a/src/FsAutoComplete.Core/CompilerServiceInterface.fs b/src/FsAutoComplete.Core/CompilerServiceInterface.fs index 7e516fb69..512876b4f 100644 --- a/src/FsAutoComplete.Core/CompilerServiceInterface.fs +++ b/src/FsAutoComplete.Core/CompilerServiceInterface.fs @@ -318,19 +318,26 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe | (true, v) -> Some v | _ -> None - member _.TryGetRecentCheckResultsForFile(file: string, snapshot: FSharpProjectSnapshot) = - let opName = sprintf "TryGetRecentCheckResultsForFile - %A" file - - checkerLogger.info (Log.setMessage "{opName} - {hash}" >> Log.addContextDestructured "opName" opName) + member _.TryGetRecentCheckResultsForFile + ( + file: string, + snapshot: FSharpProjectSnapshot + ) = + let opName = sprintf "TryGetRecentCheckResultsForFile - %A" file - checker.TryGetRecentCheckResultsForFile(UMX.untag file, snapshot, opName) - |> Option.map (fun (pr, cr) -> checkerLogger.info ( - Log.setMessage "{opName} - got results - {version}" + Log.setMessage "{opName} - {hash}" >> Log.addContextDestructured "opName" opName ) - ParseAndCheckResults(pr, cr, entityCache)) + checker.TryGetRecentCheckResultsForFile(UMX.untag file, snapshot, opName) + |> Option.map (fun (pr, cr) -> + checkerLogger.info ( + Log.setMessage "{opName} - got results - {version}" + >> Log.addContextDestructured "opName" opName + ) + + ParseAndCheckResults(pr, cr, entityCache)) member _.GetUsesOfSymbol @@ -363,14 +370,35 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe return res |> Array.concat } - member _.FindReferencesForSymbolInFile(file, project: FSharpProjectSnapshot, symbol) = + member x.FindReferencesForSymbolInFile(file : string, project: FSharpProjectSnapshot, symbol) = async { checkerLogger.info ( - Log.setMessage "FindReferencesForSymbolInFile - {file}" + Log.setMessage "FindReferencesForSymbolInFile - {file} - {projectFile}" >> Log.addContextDestructured "file" file + >> Log.addContextDestructured "projectFile" project.ProjectFileName ) - - return! checker.FindBackgroundReferencesInFile(file, project, symbol, userOpName = "find references") + let file = UMX.untag file + // let file = + // file.Substring(0, 1).ToUpper() + file.Substring(1) + try + // let! _ = checker.ParseAndCheckFileInProject(file, project) + let! results = checker.FindBackgroundReferencesInFile(file, project, symbol, userOpName = "find references") + + checkerLogger.info ( + Log.setMessage "FindReferencesForSymbolInFile - {file} - {projectFile} - {results}" + >> Log.addContextDestructured "file" file + >> Log.addContextDestructured "projectFile" project.ProjectFileName + >> Log.addContextDestructured "results" results + ) + return results + with e -> + checkerLogger.error ( + Log.setMessage "FindReferencesForSymbolInFile - {file} - {projectFile}" + >> Log.addContextDestructured "projectFile" project.ProjectFileName + >> Log.addContextDestructured "file" file + >> Log.addExn e + ) + return [||] } diff --git a/src/FsAutoComplete.Core/CompilerServiceInterface.fsi b/src/FsAutoComplete.Core/CompilerServiceInterface.fsi index ab1dcdc52..8523f2d89 100644 --- a/src/FsAutoComplete.Core/CompilerServiceInterface.fsi +++ b/src/FsAutoComplete.Core/CompilerServiceInterface.fsi @@ -72,7 +72,7 @@ type FSharpCompilerServiceChecker = Async member FindReferencesForSymbolInFile: - file: string * project: FSharpProjectSnapshot * symbol: FSharpSymbol -> Async> + file: string * project: FSharpProjectSnapshot * symbol: FSharpSymbol -> Async> // member GetDeclarations: // fileName: string * source: ISourceText * snapshot: FSharpProjectOptions * version: 'a -> diff --git a/src/FsAutoComplete.Core/ParseAndCheckResults.fs b/src/FsAutoComplete.Core/ParseAndCheckResults.fs index 49149a55d..91fa41d52 100644 --- a/src/FsAutoComplete.Core/ParseAndCheckResults.fs +++ b/src/FsAutoComplete.Core/ParseAndCheckResults.fs @@ -141,7 +141,7 @@ type ParseAndCheckResults ) | Some sym -> match sym.Symbol.Assembly.FileName with - | Some fullFilePath -> Ok(UMX.tag fullFilePath, getFileName rangeInNonexistentFile) + | Some fullFilePath -> Ok(Utils.normalizePath fullFilePath, getFileName rangeInNonexistentFile) | None -> ResultOrString.Error( sprintf @@ -770,4 +770,4 @@ type ParseAndCheckResults member __.GetAST = parseResults.ParseTree member __.GetCheckResults: FSharpCheckFileResults = checkResults member __.GetParseResults: FSharpParseFileResults = parseResults - member __.FileName: string = UMX.tag parseResults.FileName + member __.FileName: string = Utils.normalizePath parseResults.FileName diff --git a/src/FsAutoComplete.Core/SymbolLocation.fs b/src/FsAutoComplete.Core/SymbolLocation.fs index 39ad76eb7..8515bc085 100644 --- a/src/FsAutoComplete.Core/SymbolLocation.fs +++ b/src/FsAutoComplete.Core/SymbolLocation.fs @@ -45,7 +45,7 @@ let getDeclarationLocation else loc.FileName - let taggedFilePath = UMX.tag normalizedPath + let taggedFilePath = Utils.normalizePath normalizedPath if isScript && taggedFilePath = currentDocument.FileName then return SymbolDeclarationLocation.CurrentDocument From d14232a532a4e932873ca21f99890392d29dad02 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Mon, 15 Apr 2024 09:39:39 -0400 Subject: [PATCH 23/60] fixup stacktrackes --- src/FsAutoComplete.Core/Utils.fs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/FsAutoComplete.Core/Utils.fs b/src/FsAutoComplete.Core/Utils.fs index 0fad06fb8..e8573de8c 100644 --- a/src/FsAutoComplete.Core/Utils.fs +++ b/src/FsAutoComplete.Core/Utils.fs @@ -66,8 +66,9 @@ module Seq = } module ProcessHelper = + open IcedTasks let WaitForExitAsync (p: Process) = - async { + asyncEx { let tcs = TaskCompletionSource() p.EnableRaisingEvents <- true p.Exited.Add(fun _args -> tcs.TrySetResult(null) |> ignore) @@ -76,7 +77,7 @@ module ProcessHelper = let _registered = token.Register(fun _ -> tcs.SetCanceled()) - let! _ = tcs.Task |> Async.AwaitTask + let! _ = tcs.Task () } From 1e867f92de57c391d49b64d8fa57660c3cb38507 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Mon, 15 Apr 2024 09:48:50 -0400 Subject: [PATCH 24/60] fixup tests --- .../RenameParamToMatchSignatureTests.fs | 2 +- .../CodeFixTests/Tests.fs | 1 + .../CompletionTests.fs | 10 +++-- .../EmptyFileTests.fs | 3 +- test/FsAutoComplete.Tests.Lsp/Helpers.fs | 37 ++++++++++++++----- test/FsAutoComplete.Tests.Lsp/RenameTests.fs | 1 - .../FsAutoComplete.Tests.Lsp/SnapshotTests.fs | 10 ++--- 7 files changed, 43 insertions(+), 21 deletions(-) diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/RenameParamToMatchSignatureTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/RenameParamToMatchSignatureTests.fs index e4f01db14..4f2adf744 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/RenameParamToMatchSignatureTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/RenameParamToMatchSignatureTests.fs @@ -33,7 +33,7 @@ let tests state = let! (fsiDoc, diags) = server |> Server.openDocumentWithText fsiFile fsiSource use _fsiDoc = fsiDoc - Expect.isEmpty diags "There should be no diagnostics in fsi doc" + Expect.isEmpty diags $"There should be no diagnostics in fsi doc %A{diags}" let! (fsDoc, diags) = server |> Server.openDocumentWithText fsFile fsSource use fsDoc = fsDoc diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs index 456f73bce..2f0e6435a 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs @@ -3384,6 +3384,7 @@ let private removeUnnecessaryParenthesesTests state = """ ]) let tests textFactory state = + testSequenced <| testList "CodeFix-tests" [ HelpersTests.tests textFactory diff --git a/test/FsAutoComplete.Tests.Lsp/CompletionTests.fs b/test/FsAutoComplete.Tests.Lsp/CompletionTests.fs index 5fd468981..4dd5203f3 100644 --- a/test/FsAutoComplete.Tests.Lsp/CompletionTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CompletionTests.fs @@ -716,7 +716,8 @@ let autocompleteTest state = Expect.exists res.Items (fun n -> n.Label = "Baz") "Autocomplete contains given symbol" }) ] - testList + testSequenced + <| testList "Autocomplete Tests" [ testList "Autocomplete within project files" (makeAutocompleteTestList server) testList "Autocomplete within script files" (makeAutocompleteTestList scriptServer) ] @@ -781,7 +782,9 @@ let autoOpenTests state = let (|ContainsOpenAction|_|) (codeActions: CodeAction[]) = codeActions - |> Array.tryFind (fun ca -> ca.Kind = Some "quickfix" && ca.Title.StartsWith("open ", StringComparison.Ordinal)) + |> Array.tryFind (fun ca -> + ca.Kind = Some "quickfix" + && ca.Title.StartsWith("open ", StringComparison.Ordinal)) match! server.TextDocumentCodeAction p with | Error e -> return failtestf "Quick fix Request failed: %A" e @@ -1093,7 +1096,8 @@ let fullNameExternalAutocompleteTest state = Expect.isSome n "Completion doesn't exist" Expect.equal n.Value.InsertText (Some "Result") "Autocomplete contains given symbol") ] - testList + testSequenced + <| testList "fullNameExternalAutocompleteTest Tests" [ testList "fullNameExternalAutocompleteTest within project files" (makeAutocompleteTestList server) testList "fullNameExternalAutocompleteTest within script files" (makeAutocompleteTestList scriptServer) ] diff --git a/test/FsAutoComplete.Tests.Lsp/EmptyFileTests.fs b/test/FsAutoComplete.Tests.Lsp/EmptyFileTests.fs index e5095e16e..1e13a6c35 100644 --- a/test/FsAutoComplete.Tests.Lsp/EmptyFileTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/EmptyFileTests.fs @@ -26,7 +26,8 @@ let tests state = let server1 = createServer() let server2 = createServer() - testList + testSequenced + <| testList "empty file features" [ testList "tests" diff --git a/test/FsAutoComplete.Tests.Lsp/Helpers.fs b/test/FsAutoComplete.Tests.Lsp/Helpers.fs index 9cc715411..84e3d95d9 100644 --- a/test/FsAutoComplete.Tests.Lsp/Helpers.fs +++ b/test/FsAutoComplete.Tests.Lsp/Helpers.fs @@ -57,7 +57,7 @@ module Expecto = let ptestCaseAsync = ptestCaseAsyncWithTimeout DEFAULT_TIMEOUT let ftestCaseAsync = ptestCaseAsyncWithTimeout DEFAULT_TIMEOUT -let rec private copyDirectory (sourceDir : DirectoryInfo) destDir = +let rec private copyDirectory (sourceDir: DirectoryInfo) destDir = // Get the subdirectories for the specified directory. // let dir = DirectoryInfo(sourceDir) @@ -82,18 +82,16 @@ let rec private copyDirectory (sourceDir : DirectoryInfo) destDir = copyDirectory dir tempPath) type DisposableDirectory(directory: string, deleteParentDir) = - static member Create(?name : string) = + static member Create(?name: string) = let tempPath, deleteParentDir = match name with - | Some name -> - IO.Path.GetTempPath() Guid.NewGuid().ToString("n") name, true - | None -> - IO.Path.Combine(IO.Path.GetTempPath(), Guid.NewGuid().ToString("n")), false + | Some name -> IO.Path.GetTempPath() Guid.NewGuid().ToString("n") name, true + | None -> IO.Path.Combine(IO.Path.GetTempPath(), Guid.NewGuid().ToString("n")), false // printfn "Creating directory %s" tempPath IO.Directory.CreateDirectory tempPath |> ignore new DisposableDirectory(tempPath, deleteParentDir) - static member From (sourceDir: DirectoryInfo) = + static member From(sourceDir: DirectoryInfo) = let self = DisposableDirectory.Create(sourceDir.Name) copyDirectory sourceDir self.DirectoryInfo.FullName self @@ -108,8 +106,21 @@ type DisposableDirectory(directory: string, deleteParentDir) = else x.DirectoryInfo - // printfn "Deleting directory %s" dirToDelete.FullName - IO.Directory.Delete(dirToDelete.FullName, true) + let mutable attempts = 5 + + while attempts > 0 do + try + // Handle odd cases with windows file locking + IO.Directory.Delete(dirToDelete.FullName, true) + attempts <- 0 + with _ -> + attempts <- attempts - 1 + if attempts = 0 then + reraise () + Thread.Sleep(15) + + + type Async = /// Behaves like AwaitObservable, but calls the specified guarding function @@ -700,7 +711,13 @@ let diagnosticsToResult = let waitForParseResultsForFile file = fileDiagnostics file >> diagnosticsToResult >> Async.AwaitObservable -let waitForDiagnosticErrorForFile file = fileDiagnostics file >> Observable.choose (function | [||] -> None | diags -> Some diags) >> diagnosticsToResult >> Async.AwaitObservable +let waitForDiagnosticErrorForFile file = + fileDiagnostics file + >> Observable.choose (function + | [||] -> None + | diags -> Some diags) + >> diagnosticsToResult + >> Async.AwaitObservable let waitForFsacDiagnosticsForFile file = fsacDiagnostics file >> diagnosticsToResult >> Async.AwaitObservable diff --git a/test/FsAutoComplete.Tests.Lsp/RenameTests.fs b/test/FsAutoComplete.Tests.Lsp/RenameTests.fs index e9ba9e8b7..671adcc2a 100644 --- a/test/FsAutoComplete.Tests.Lsp/RenameTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/RenameTests.fs @@ -291,7 +291,6 @@ let private crossProjectTests state = { TextDocument = { Uri = normalizePathCasing usageFile } Position = { Line = 6; Character = 28 } NewName = "sup" } - let! res = server.TextDocumentRename(renameHelloUsageInUsageFile) match res with diff --git a/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs index 86aeca94d..63fd20e9e 100644 --- a/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs @@ -98,7 +98,7 @@ let createProjectA (projects : FileInfo seq) (loader : IWorkspaceLoader) onLoadC loadedProjectsA - +let normalizeUntag = normalizePath >> UMX.untag let snapshotTests loaders toolsPath = @@ -283,8 +283,8 @@ let snapshotTests loaders toolsPath = Expect.equal ls1.ProjectId ls2.ProjectId "Project Id name should be the same" Expect.equal ls1.SourceFiles.Length 3 "Source files length should be 3" Expect.equal ls1.SourceFiles.Length ls2.SourceFiles.Length "Source files length should be the same" - let ls1File = ls1.SourceFiles |> Seq.find (fun x -> x.FileName = libraryFile.FullName) - let ls2File = ls2.SourceFiles |> Seq.find (fun x -> x.FileName = libraryFile.FullName) + let ls1File = ls1.SourceFiles |> Seq.find (fun x -> x.FileName = normalizeUntag libraryFile.FullName) + let ls2File = ls2.SourceFiles |> Seq.find (fun x -> x.FileName = normalizeUntag libraryFile.FullName) Expect.notEqual ls1File.Version ls2File.Version "Library source file version should not be the same" Expect.equal ls1.ReferencedProjects.Length ls2.ReferencedProjects.Length "Referenced projects length should be the same" Expect.equal ls1.ReferencedProjects.Length 0 "Referenced projects length should be 0" @@ -298,8 +298,8 @@ let snapshotTests loaders toolsPath = Expect.equal cs1.SourceFiles.Length 3 "Source files length should be 3" Expect.equal cs1.SourceFiles.Length cs2.SourceFiles.Length "Source files length should be the same" let consoleFile = Projects.MultiProjectScenario1.Console1.programFileIn dDir.DirectoryInfo - let cs1File = cs1.SourceFiles |> Seq.find (fun x -> x.FileName = consoleFile.FullName) - let cs2File = cs2.SourceFiles |> Seq.find (fun x -> x.FileName = consoleFile.FullName) + let cs1File = cs1.SourceFiles |> Seq.find (fun x -> x.FileName = normalizeUntag consoleFile.FullName) + let cs2File = cs2.SourceFiles |> Seq.find (fun x -> x.FileName = normalizeUntag consoleFile.FullName) Expect.equal cs1File.Version cs2File.Version "Console source file version should be the same" Expect.equal cs1.ReferencedProjects.Length cs2.ReferencedProjects.Length "Referenced projects length should be the same" Expect.equal cs1.ReferencedProjects.Length 1 "Referenced projects length should be 1" From e3ed5516209d163f4f1f6bc1fdcd485b73682ffd Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Mon, 15 Apr 2024 09:49:29 -0400 Subject: [PATCH 25/60] Add OTel to tests --- test/FsAutoComplete.Tests.Lsp/Program.fs | 26 ++++++++++++++++++- .../FsAutoComplete.Tests.Lsp/paket.references | 1 + 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/test/FsAutoComplete.Tests.Lsp/Program.fs b/test/FsAutoComplete.Tests.Lsp/Program.fs index d3643369b..a1ec4694f 100644 --- a/test/FsAutoComplete.Tests.Lsp/Program.fs +++ b/test/FsAutoComplete.Tests.Lsp/Program.fs @@ -121,9 +121,29 @@ let tests = testList "FSAC" [ SnapshotTests.snapshotTests loaders toolsPath ] +open OpenTelemetry +open OpenTelemetry.Resources +open OpenTelemetry.Trace +open OpenTelemetry.Logs +open OpenTelemetry.Metrics +open System.Diagnostics +open FsAutoComplete.Telemetry [] let main args = + let serviceName = "FsAutoComplete.Tests.Lsp" + use traceProvider = + let version = FsAutoComplete.Utils.Version.info().Version + Sdk + .CreateTracerProviderBuilder() + .AddSource(FsAutoComplete.Utils.Tracing.serviceName, Tracing.fscServiceName, serviceName) + .SetResourceBuilder( + ResourceBuilder + .CreateDefault() + .AddService(serviceName = serviceName, serviceVersion = version) + ) + .AddOtlpExporter() + .Build() let outputTemplate = "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}" @@ -225,12 +245,16 @@ let main args = let fixedUpArgs = args |> Array.except argsToRemove let cts = new CancellationTokenSource(testTimeout) + use activitySource = new ActivitySource(serviceName) let cliArgs = [ CLIArguments.Printer(Expecto.Impl.TestPrinters.summaryWithLocationPrinter defaultConfig.printer) CLIArguments.Verbosity Expecto.Logging.LogLevel.Info CLIArguments.Parallel + CLIArguments.ActivitySource activitySource ] - + // let trace = traceProvider.GetTracer("FsAutoComplete.Tests.Lsp") + // use span = trace.StartActiveSpan("runTests", SpanKind.Internal) + use span = activitySource.StartActivity("runTests") runTestsWithCLIArgsAndCancel cts.Token cliArgs fixedUpArgs tests diff --git a/test/FsAutoComplete.Tests.Lsp/paket.references b/test/FsAutoComplete.Tests.Lsp/paket.references index d01b7b2dc..adb2e8e05 100644 --- a/test/FsAutoComplete.Tests.Lsp/paket.references +++ b/test/FsAutoComplete.Tests.Lsp/paket.references @@ -13,6 +13,7 @@ Serilog Destructurama.FSharp Serilog.Sinks.Async Serilog.Sinks.Console +OpenTelemetry.Exporter.OpenTelemetryProtocol Microsoft.Build copy_local: false Microsoft.Build.Framework copy_local: false From 690e7220a5bf409e1c0c839e2fca15d6da6cb6ec Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Mon, 15 Apr 2024 10:08:39 -0400 Subject: [PATCH 26/60] formatting --- src/FsAutoComplete.Core/AdaptiveExtensions.fs | 84 +++++++------------ .../CompilerServiceInterface.fs | 36 ++++---- src/FsAutoComplete.Core/Utils.fs | 1 + .../LspServers/AdaptiveServerState.fs | 29 +++---- .../LspServers/ProjectWorkspace.fs | 12 +-- 5 files changed, 64 insertions(+), 98 deletions(-) diff --git a/src/FsAutoComplete.Core/AdaptiveExtensions.fs b/src/FsAutoComplete.Core/AdaptiveExtensions.fs index a50f02374..193c02a4e 100644 --- a/src/FsAutoComplete.Core/AdaptiveExtensions.fs +++ b/src/FsAutoComplete.Core/AdaptiveExtensions.fs @@ -27,9 +27,9 @@ module AdaptiveExtensions = member cts.TryDispose() = // try - if not <| isNull cts then - cts.Dispose() - // with _ -> () + if not <| isNull cts then + cts.Dispose() + // with _ -> () type TaskCompletionSource<'a> with @@ -37,28 +37,14 @@ module AdaptiveExtensions = /// https://github.com/dotnet/runtime/issues/47998 member tcs.TrySetFromTask(real: Task<'a>) = - real.ContinueWith(fun (task : Task<_>) -> + real.ContinueWith(fun (task: Task<_>) -> match task.Status with | TaskStatus.RanToCompletion -> tcs.TrySetResult task.Result |> ignore - | TaskStatus.Canceled -> tcs.TrySetCanceled(TaskCanceledException(task).CancellationToken) |> ignore - | TaskStatus.Faulted -> - logger.error( - Log.setMessage "Error in TrySetFromTask with {count}" - >> Log.addExn task.Exception.InnerException - >> Log.addContext "count" task.Exception.InnerExceptions.Count - ) - logger.warn( - Log.setMessage "task.Exception.StackTrace {trace}" - >> Log.addContext "trace" task.Exception.StackTrace - ) - logger.warn( - Log.setMessage "task.Exception.StackTrace {trace}" - >> Log.addContext "trace" task.Exception.InnerException.StackTrace - ) - // if task.Exception.StackTrace = null then - tcs.TrySetException(task.Exception.InnerExceptions) |> ignore - // else - // tcs.TrySetException(task.Exception) |> ignore + | TaskStatus.Canceled -> + tcs.TrySetCanceled(TaskCanceledException(task).CancellationToken) + |> ignore + | TaskStatus.Faulted -> tcs.TrySetException(task.Exception.InnerExceptions) |> ignore + | _ -> ()) |> ignore @@ -416,9 +402,9 @@ and AdaptiveCancellableTask<'a>(cancel: unit -> unit, real: Task<'a>) = if real.IsCompleted then real else - cachedTcs <- new TaskCompletionSource<'a>() - cachedTcs.TrySetFromTask real - cachedTcs.Task + cachedTcs <- new TaskCompletionSource<'a>() + cachedTcs.TrySetFromTask real + cachedTcs.Task cached <- match cached with @@ -434,12 +420,12 @@ and AdaptiveCancellableTask<'a>(cancel: unit -> unit, real: Task<'a>) = cached /// Will run the cancel function passed into the constructor and set the output Task to cancelled state. - member x.Cancel(cancellationToken : CancellationToken) = + member x.Cancel(cancellationToken: CancellationToken) = lock x (fun () -> cancel () + if not <| isNull cachedTcs then - cachedTcs.TrySetCanceled(cancellationToken) |> ignore - ) + cachedTcs.TrySetCanceled(cancellationToken) |> ignore) /// The output of the passed in task to the constructor. /// @@ -684,13 +670,11 @@ module AsyncAVal = member x.Compute t = if x.OutOfDate || Option.isNone cache then let ref = - RefCountingTaskCreator( - fun ct -> task { + RefCountingTaskCreator(fun ct -> + task { let v = input.GetValue t - use _s = - ct.Register(fun () -> - v.Cancel(ct)) + use _s = ct.Register(fun () -> v.Cancel(ct)) let! i = v.Task @@ -700,8 +684,7 @@ module AsyncAVal = let! b = mapping i ct dataCache <- ValueSome(struct (i, b)) return b - } - ) + }) cache <- Some ref ref.New() @@ -736,8 +719,8 @@ module AsyncAVal = member x.Compute t = if x.OutOfDate || Option.isNone cache then let ref = - RefCountingTaskCreator( - fun ct -> task { + RefCountingTaskCreator(fun ct -> + task { let ta = ca.GetValue t let tb = cb.GetValue t @@ -755,8 +738,7 @@ module AsyncAVal = let! vc = mapping ia ib ct dataCache <- ValueSome(struct (ia, ib, vc)) return vc - } - ) + }) cache <- Some ref ref.New() @@ -790,12 +772,10 @@ module AsyncAVal = if x.OutOfDate then if Interlocked.Exchange(&inputChanged, 0) = 1 || Option.isNone cache then let outerTask = - RefCountingTaskCreator( - fun ct -> task { + RefCountingTaskCreator(fun ct -> + task { let v = value.GetValue t - use _s = - ct.Register(fun () -> - v.Cancel(ct)) + use _s = ct.Register(fun () -> v.Cancel(ct)) let! i = v.Task @@ -806,22 +786,19 @@ module AsyncAVal = outerDataCache <- Some(i, inner) return inner - } - ) + }) cache <- Some outerTask let outerTask = cache.Value let ref = - RefCountingTaskCreator( - fun ct -> task { + RefCountingTaskCreator(fun ct -> + task { let inner = outerTask.New() - use _s = - ct.Register(fun () -> - inner.Cancel(ct)) + use _s = ct.Register(fun () -> inner.Cancel(ct)) let! inner = inner.Task @@ -836,8 +813,7 @@ module AsyncAVal = return! innerTask.Task - } - ) + }) innerCache <- Some ref diff --git a/src/FsAutoComplete.Core/CompilerServiceInterface.fs b/src/FsAutoComplete.Core/CompilerServiceInterface.fs index 512876b4f..873265c37 100644 --- a/src/FsAutoComplete.Core/CompilerServiceInterface.fs +++ b/src/FsAutoComplete.Core/CompilerServiceInterface.fs @@ -318,26 +318,19 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe | (true, v) -> Some v | _ -> None - member _.TryGetRecentCheckResultsForFile - ( - file: string, - snapshot: FSharpProjectSnapshot - ) = - let opName = sprintf "TryGetRecentCheckResultsForFile - %A" file + member _.TryGetRecentCheckResultsForFile(file: string, snapshot: FSharpProjectSnapshot) = + let opName = sprintf "TryGetRecentCheckResultsForFile - %A" file + + checkerLogger.info (Log.setMessage "{opName} - {hash}" >> Log.addContextDestructured "opName" opName) + checker.TryGetRecentCheckResultsForFile(UMX.untag file, snapshot, opName) + |> Option.map (fun (pr, cr) -> checkerLogger.info ( - Log.setMessage "{opName} - {hash}" + Log.setMessage "{opName} - got results - {version}" >> Log.addContextDestructured "opName" opName ) - checker.TryGetRecentCheckResultsForFile(UMX.untag file, snapshot, opName) - |> Option.map (fun (pr, cr) -> - checkerLogger.info ( - Log.setMessage "{opName} - got results - {version}" - >> Log.addContextDestructured "opName" opName - ) - - ParseAndCheckResults(pr, cr, entityCache)) + ParseAndCheckResults(pr, cr, entityCache)) member _.GetUsesOfSymbol @@ -370,34 +363,35 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe return res |> Array.concat } - member x.FindReferencesForSymbolInFile(file : string, project: FSharpProjectSnapshot, symbol) = + member x.FindReferencesForSymbolInFile(file: string, project: FSharpProjectSnapshot, symbol) = async { checkerLogger.info ( Log.setMessage "FindReferencesForSymbolInFile - {file} - {projectFile}" >> Log.addContextDestructured "file" file >> Log.addContextDestructured "projectFile" project.ProjectFileName ) + let file = UMX.untag file - // let file = - // file.Substring(0, 1).ToUpper() + file.Substring(1) + try - // let! _ = checker.ParseAndCheckFileInProject(file, project) let! results = checker.FindBackgroundReferencesInFile(file, project, symbol, userOpName = "find references") checkerLogger.info ( Log.setMessage "FindReferencesForSymbolInFile - {file} - {projectFile} - {results}" >> Log.addContextDestructured "file" file - >> Log.addContextDestructured "projectFile" project.ProjectFileName + >> Log.addContextDestructured "projectFile" project.ProjectFileName >> Log.addContextDestructured "results" results ) + return results with e -> checkerLogger.error ( Log.setMessage "FindReferencesForSymbolInFile - {file} - {projectFile}" - >> Log.addContextDestructured "projectFile" project.ProjectFileName + >> Log.addContextDestructured "projectFile" project.ProjectFileName >> Log.addContextDestructured "file" file >> Log.addExn e ) + return [||] } diff --git a/src/FsAutoComplete.Core/Utils.fs b/src/FsAutoComplete.Core/Utils.fs index e8573de8c..70346bcc7 100644 --- a/src/FsAutoComplete.Core/Utils.fs +++ b/src/FsAutoComplete.Core/Utils.fs @@ -67,6 +67,7 @@ module Seq = module ProcessHelper = open IcedTasks + let WaitForExitAsync (p: Process) = asyncEx { let tcs = TaskCompletionSource() diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index 0aca3e75f..04b85334c 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -96,7 +96,9 @@ type FindFirstProject() = |> Seq.sortBy (fun p -> p.ProjectFileName) |> Seq.tryFind (fun p -> p.SourceFilesTagged |> Array.exists (fun f -> f = sourceFile)) |> Result.ofOption (fun () -> - let allProjects = String.join ", " (projects |> Seq.map (fun p -> p.ProjectFileName)) + let allProjects = + String.join ", " (projects |> Seq.map (fun p -> p.ProjectFileName)) + $"Couldn't find a corresponding project for {sourceFile}. \n Projects include {allProjects}. \nHave the projects loaded yet or have you tried restoring your project/solution?") @@ -1024,8 +1026,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let! projects = // need to bind to a single value to keep the threadpool from being exhausted as LoadingProjects can be a long running operation // and when other adaptive values await on this, the scheduler won't block those other tasks - loadProjects loader binlogConfig projects - |> AMap.toAVal + loadProjects loader binlogConfig projects |> AMap.toAVal and! checker = checker checker.ClearCaches() @@ -1079,11 +1080,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac // |> Observable.subscribe (fun _ -> forceLoadProjects () |> ignore>) // |> disposables.Add - let AMapReKeyMany f map = - map - |> AMap.toASet - |> ASet.collect f - |> AMap.ofASet + let AMapReKeyMany f map = map |> AMap.toASet |> ASet.collect f |> AMap.ofASet let sourceFileToProjectOptions = asyncAVal { @@ -1091,11 +1088,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let sourceFileToProjectOptions = loadedProjects - |> AMapReKeyMany(fun (_,v) -> - v.SourceFilesTagged - |> ASet.ofArray - |> ASet.map(fun source -> source, v) - ) + |> AMapReKeyMany(fun (_, v) -> v.SourceFilesTagged |> ASet.ofArray |> ASet.map (fun source -> source, v)) |> AMap.map' HashSet.toList return sourceFileToProjectOptions @@ -1477,13 +1470,12 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac = asyncEx { let tags = - [ - SemanticConventions.fsac_sourceCodePath, box (UMX.untag file.Source.FileName) + [ SemanticConventions.fsac_sourceCodePath, box (UMX.untag file.Source.FileName) SemanticConventions.projectFilePath, box (options.ProjectFileName) "source.text", box (file.Source.String) "source.version", box (file.Version) - ] + ] use _ = fsacActivitySource.StartActivityForType(thisType, tags = tags) @@ -2185,7 +2177,9 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let dependentProjectsAndSourceFiles = dependentProjects - |> List.collect (fun (snap) -> snap.SourceFiles |> List.map (fun sourceFile -> snap, Utils.normalizePath sourceFile.FileName)) + |> List.collect (fun (snap) -> + snap.SourceFiles + |> List.map (fun sourceFile -> snap, Utils.normalizePath sourceFile.FileName)) |> List.toArray let mutable checksCompleted = 0 @@ -2203,6 +2197,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac Array.concat [| dependentFiles; dependentProjectsAndSourceFiles |] |> Array.filter (fun (_, file) -> let file = UMX.untag file + file.Contains "AssemblyInfo.fs" |> not && file.Contains "AssemblyAttributes.fs" |> not) diff --git a/src/FsAutoComplete/LspServers/ProjectWorkspace.fs b/src/FsAutoComplete/LspServers/ProjectWorkspace.fs index 2790051c8..f2c477fb8 100644 --- a/src/FsAutoComplete/LspServers/ProjectWorkspace.fs +++ b/src/FsAutoComplete/LspServers/ProjectWorkspace.fs @@ -119,7 +119,10 @@ module Snapshots = unresolvedReferences originalLoadReferences - let private createFSharpFileSnapshotOnDisk (sourceTextFactory: aval) (sourceFilePath: string) = + let private createFSharpFileSnapshotOnDisk + (sourceTextFactory: aval) + (sourceFilePath: string) + = aval { let file = UMX.untag sourceFilePath let! writeTime = AdaptiveFile.GetLastWriteTimeUtc file @@ -236,8 +239,7 @@ module Snapshots = | None -> return! createFSharpFileSnapshotOnDisk sourceTextFactory sourcePath }) - let references = - p.OtherOptions |> List.filter (fun x -> x.StartsWith("-r:")) + let references = p.OtherOptions |> List.filter (fun x -> x.StartsWith("-r:")) let otherOptions = p.OtherOptions |> ASet.ofList |> ASet.map (AVal.constant) @@ -292,7 +294,5 @@ module Snapshots = let optionsToSnapshot = optionsToSnapshot cachedSnapshots inMemorySourceFiles sourceTextFactory mapReferences - ps - |> HashMap.map (fun _ v -> (v, optionsToSnapshot v)) - ) + ps |> HashMap.map (fun _ v -> (v, optionsToSnapshot v))) |> AMap.ofAVal From 623f2ebdaa012d47961346c6b86a9f6f32a5d12e Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Mon, 15 Apr 2024 10:15:34 -0400 Subject: [PATCH 27/60] remove future expecto features --- test/FsAutoComplete.Tests.Lsp/Program.fs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/FsAutoComplete.Tests.Lsp/Program.fs b/test/FsAutoComplete.Tests.Lsp/Program.fs index a1ec4694f..f52ed762e 100644 --- a/test/FsAutoComplete.Tests.Lsp/Program.fs +++ b/test/FsAutoComplete.Tests.Lsp/Program.fs @@ -252,7 +252,6 @@ let main args = CLIArguments.Printer(Expecto.Impl.TestPrinters.summaryWithLocationPrinter defaultConfig.printer) CLIArguments.Verbosity Expecto.Logging.LogLevel.Info CLIArguments.Parallel - CLIArguments.ActivitySource activitySource ] // let trace = traceProvider.GetTracer("FsAutoComplete.Tests.Lsp") // use span = trace.StartActiveSpan("runTests", SpanKind.Internal) From 64e653e9a10afae4710776bea1875361e37193d3 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Tue, 16 Apr 2024 18:38:21 -0400 Subject: [PATCH 28/60] handle CI failures for file tests --- test/FsAutoComplete.Tests.Lsp/Helpers.fs | 4 +-- .../FsAutoComplete.Tests.Lsp/SnapshotTests.fs | 31 ++++++++++++++----- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/test/FsAutoComplete.Tests.Lsp/Helpers.fs b/test/FsAutoComplete.Tests.Lsp/Helpers.fs index 84e3d95d9..5959762f7 100644 --- a/test/FsAutoComplete.Tests.Lsp/Helpers.fs +++ b/test/FsAutoComplete.Tests.Lsp/Helpers.fs @@ -106,11 +106,11 @@ type DisposableDirectory(directory: string, deleteParentDir) = else x.DirectoryInfo - let mutable attempts = 5 + let mutable attempts = 25 + // Handle odd cases with windows file locking while attempts > 0 do try - // Handle odd cases with windows file locking IO.Directory.Delete(dirToDelete.FullName, true) attempts <- 0 with _ -> diff --git a/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs index 63fd20e9e..1faf1b569 100644 --- a/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs @@ -14,6 +14,7 @@ open FSharp.Compiler.CodeAnalysis.ProjectSnapshot open FSharp.Compiler.CodeAnalysis open IcedTasks open FSharp.UMX +open System.Threading.Tasks module FcsRange = FSharp.Compiler.Text.Range type FcsRange = FSharp.Compiler.Text.Range @@ -100,6 +101,25 @@ let createProjectA (projects : FileInfo seq) (loader : IWorkspaceLoader) onLoadC let normalizeUntag = normalizePath >> UMX.untag +let awaitFileChanged (file : FileInfo) = + // The AdaptiveFile implementation uses FileSystemWatcher under the hood to watch for file changes. + // The problem is on different operating systems the file system watcher behaves differently. + // Our tests may run quicker than the file system watcher can pick up the changes + // So we need to wait for a file system watcher to pick up the changes + // Better than using a sleep is to use a task completion source to signal when the file system watcher has picked up the changes + task { + let tcs = new TaskCompletionSource() + let handleChange _ = + tcs.TrySetResult () |> ignore + use fsi = new FileSystemWatcher(file.Directory.FullName, file.Name) + fsi.NotifyFilter <- NotifyFilters.Attributes ||| NotifyFilters.FileName ||| NotifyFilters.DirectoryName + fsi.Changed.Add handleChange + fsi.Created.Add handleChange + fsi.Deleted.Add handleChange + fsi.Renamed.Add handleChange + fsi.EnableRaisingEvents <- true + do! tcs.Task + } let snapshotTests loaders toolsPath = @@ -218,11 +238,10 @@ let snapshotTests loaders toolsPath = let snapshots = snapsA |> AMap.mapA (fun _ (_,v) -> v) |> AMap.force let consoleFile = Projects.MultiProjectScenario1.Console1.programFileIn dDir.DirectoryInfo - // printfn "Setting last write time for %s %A" libraryFile.FullName libraryFile.LastWriteTime - + let fileChanged = awaitFileChanged consoleFile do! File.WriteAllTextAsync(consoleFile.FullName, "let x = 1") + do! fileChanged consoleFile.Refresh() - // printfn "last write time for %s %A" libraryFile.FullName libraryFile.LastWriteTime let snapshots2 = snapsA |> AMap.mapA (fun _ (_,v) -> v) |> AMap.force @@ -260,18 +279,16 @@ let snapshotTests loaders toolsPath = let loadedProjectsA = createProjectA projects loader (fun () -> loadedCalls <- loadedCalls + 1) - let snapsA = Snapshots.createSnapshots AMap.empty (AVal.constant sourceTextFactory) loadedProjectsA let snapshots = snapsA |> AMap.mapA (fun _ (_,v) -> v) |> AMap.force let libraryFile = Projects.MultiProjectScenario1.Library1.libraryFileIn dDir.DirectoryInfo - // printfn "Setting last write time for %s %A" libraryFile.FullName libraryFile.LastWriteTime - + let fileChanged = awaitFileChanged libraryFile do! File.WriteAllTextAsync(libraryFile.FullName, "let x = 1") + do! fileChanged libraryFile.Refresh() - // printfn "last write time for %s %A" libraryFile.FullName libraryFile.LastWriteTime let snapshots2 = snapsA |> AMap.mapA (fun _ (_,v) -> v) |> AMap.force From a6657d9d9f9fdd1bf7b81a3c6606afd2855f81b0 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Tue, 16 Apr 2024 19:17:50 -0400 Subject: [PATCH 29/60] Maybe make tests work on nix --- .../FsAutoComplete.Tests.Lsp/SnapshotTests.fs | 111 +++++++++--------- 1 file changed, 57 insertions(+), 54 deletions(-) diff --git a/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs index 1faf1b569..db1f713bf 100644 --- a/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs @@ -20,6 +20,7 @@ module FcsRange = FSharp.Compiler.Text.Range type FcsRange = FSharp.Compiler.Text.Range type FcsPos = FSharp.Compiler.Text.Position +open Helpers.Expecto.ShadowedTimeouts module Dotnet = @@ -108,24 +109,26 @@ let awaitFileChanged (file : FileInfo) = // So we need to wait for a file system watcher to pick up the changes // Better than using a sleep is to use a task completion source to signal when the file system watcher has picked up the changes task { - let tcs = new TaskCompletionSource() - let handleChange _ = - tcs.TrySetResult () |> ignore - use fsi = new FileSystemWatcher(file.Directory.FullName, file.Name) - fsi.NotifyFilter <- NotifyFilters.Attributes ||| NotifyFilters.FileName ||| NotifyFilters.DirectoryName - fsi.Changed.Add handleChange - fsi.Created.Add handleChange - fsi.Deleted.Add handleChange - fsi.Renamed.Add handleChange - fsi.EnableRaisingEvents <- true - do! tcs.Task + ignore file + // let tcs = new TaskCompletionSource() + // let handleChange _ = + // tcs.TrySetResult () |> ignore + // use fsi = new FileSystemWatcher(file.Directory.FullName, file.Name) + // // fsi.NotifyFilter <- NotifyFilters.Attributes ||| NotifyFilters.FileName ||| NotifyFilters.DirectoryName + // fsi.Changed.Add handleChange + // fsi.Created.Add handleChange + // fsi.Deleted.Add handleChange + // fsi.Renamed.Add handleChange + // fsi.EnableRaisingEvents <- true + // do! tcs.Task + do! Task.Delay(250) // if this works kill me } let snapshotTests loaders toolsPath = - testList "SnapshotTests" [ + testList "ProjectWorkspace" [ for (loaderName, workspaceLoaderFactory) in loaders do - testSequencedGroup loaderName <| + // testSequencedGroup loaderName <| testList $"{loaderName}" [ testCaseAsync "Simple Project Load" <| async { let (loader : IWorkspaceLoader) = workspaceLoaderFactory toolsPath @@ -207,17 +210,17 @@ let snapshotTests loaders toolsPath = let snapsA = Snapshots.createSnapshots AMap.empty (AVal.constant sourceTextFactory) loadedProjectsA - let snapshots = snapsA |> AMap.mapA (fun _ (_,v) -> v) |> AMap.force + let snapshotBefore = snapsA |> AMap.mapA (fun _ (_,v) -> v) |> AMap.force - let snapshots2 = snapsA |> AMap.mapA (fun _ (_,v) -> v) |> AMap.force + let snapshotAfter = snapsA |> AMap.mapA (fun _ (_,v) -> v) |> AMap.force - let ls1 = snapshots |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) - let ls2 = snapshots2 |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) + let ls1 = snapshotBefore |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) + let ls2 = snapshotAfter |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) Expect.equal ls1 ls2 "library should be the same" - let cs1 = snapshots |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) - let cs2 = snapshots2 |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) + let cs1 = snapshotBefore |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) + let cs2 = snapshotAfter |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) Expect.equal cs1 cs2 "console should be the same" } @@ -235,7 +238,7 @@ let snapshotTests loaders toolsPath = let snapsA = Snapshots.createSnapshots AMap.empty (AVal.constant sourceTextFactory) loadedProjectsA - let snapshots = snapsA |> AMap.mapA (fun _ (_,v) -> v) |> AMap.force + let snapshotsBefore = snapsA |> AMap.mapA (fun _ (_,v) -> v) |> AMap.force let consoleFile = Projects.MultiProjectScenario1.Console1.programFileIn dDir.DirectoryInfo let fileChanged = awaitFileChanged consoleFile @@ -244,15 +247,15 @@ let snapshotTests loaders toolsPath = consoleFile.Refresh() - let snapshots2 = snapsA |> AMap.mapA (fun _ (_,v) -> v) |> AMap.force + let snapshotAfter = snapsA |> AMap.mapA (fun _ (_,v) -> v) |> AMap.force - let ls1 = snapshots |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) - let ls2 = snapshots2 |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) + let ls1 = snapshotsBefore |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) + let ls2 = snapshotAfter |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) Expect.equal ls1 ls2 "library should be the same" - let cs1 = snapshots |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) - let cs2 = snapshots2 |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) + let cs1 = snapshotsBefore |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) + let cs2 = snapshotAfter |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) Expect.equal cs1.ProjectFileName cs2.ProjectFileName "Project file name should be the same" Expect.equal cs1.ProjectId cs2.ProjectId "Project Id name should be the same" @@ -282,7 +285,7 @@ let snapshotTests loaders toolsPath = let snapsA = Snapshots.createSnapshots AMap.empty (AVal.constant sourceTextFactory) loadedProjectsA - let snapshots = snapsA |> AMap.mapA (fun _ (_,v) -> v) |> AMap.force + let snapshotBefore = snapsA |> AMap.mapA (fun _ (_,v) -> v) |> AMap.force let libraryFile = Projects.MultiProjectScenario1.Library1.libraryFileIn dDir.DirectoryInfo let fileChanged = awaitFileChanged libraryFile @@ -290,42 +293,42 @@ let snapshotTests loaders toolsPath = do! fileChanged libraryFile.Refresh() - let snapshots2 = snapsA |> AMap.mapA (fun _ (_,v) -> v) |> AMap.force + let snapshotAfter = snapsA |> AMap.mapA (fun _ (_,v) -> v) |> AMap.force - let ls1 = snapshots |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) - let ls2 = snapshots2 |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) + let libBefore = snapshotBefore |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) + let libAfter = snapshotAfter |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) - Expect.notEqual ls1 ls2 "library should not be the same" - Expect.equal ls1.ProjectFileName ls2.ProjectFileName "Project file name should be the same" - Expect.equal ls1.ProjectId ls2.ProjectId "Project Id name should be the same" - Expect.equal ls1.SourceFiles.Length 3 "Source files length should be 3" - Expect.equal ls1.SourceFiles.Length ls2.SourceFiles.Length "Source files length should be the same" - let ls1File = ls1.SourceFiles |> Seq.find (fun x -> x.FileName = normalizeUntag libraryFile.FullName) - let ls2File = ls2.SourceFiles |> Seq.find (fun x -> x.FileName = normalizeUntag libraryFile.FullName) + Expect.notEqual libBefore libAfter "library should not be the same" + Expect.equal libBefore.ProjectFileName libAfter.ProjectFileName "Project file name should be the same" + Expect.equal libBefore.ProjectId libAfter.ProjectId "Project Id name should be the same" + Expect.equal libBefore.SourceFiles.Length 3 "Source files length should be 3" + Expect.equal libBefore.SourceFiles.Length libAfter.SourceFiles.Length "Source files length should be the same" + let ls1File = libBefore.SourceFiles |> Seq.find (fun x -> x.FileName = normalizeUntag libraryFile.FullName) + let ls2File = libAfter.SourceFiles |> Seq.find (fun x -> x.FileName = normalizeUntag libraryFile.FullName) Expect.notEqual ls1File.Version ls2File.Version "Library source file version should not be the same" - Expect.equal ls1.ReferencedProjects.Length ls2.ReferencedProjects.Length "Referenced projects length should be the same" - Expect.equal ls1.ReferencedProjects.Length 0 "Referenced projects length should be 0" - Expect.notEqual ls1.Stamp ls2.Stamp "Stamp should not be the same" + Expect.equal libBefore.ReferencedProjects.Length libAfter.ReferencedProjects.Length "Referenced projects length should be the same" + Expect.equal libBefore.ReferencedProjects.Length 0 "Referenced projects length should be 0" + Expect.notEqual libBefore.Stamp libAfter.Stamp "Stamp should not be the same" - let cs1 = snapshots |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) - let cs2 = snapshots2 |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) + let consoleBefore = snapshotBefore |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) + let consoleAfter = snapshotAfter |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Console1.projectIn dDir.DirectoryInfo).FullName) - Expect.equal cs1.ProjectFileName cs2.ProjectFileName "Project file name should be the same" - Expect.equal cs1.ProjectId cs2.ProjectId "Project Id name should be the same" - Expect.equal cs1.SourceFiles.Length 3 "Source files length should be 3" - Expect.equal cs1.SourceFiles.Length cs2.SourceFiles.Length "Source files length should be the same" + Expect.equal consoleBefore.ProjectFileName consoleAfter.ProjectFileName "Project file name should be the same" + Expect.equal consoleBefore.ProjectId consoleAfter.ProjectId "Project Id name should be the same" + Expect.equal consoleBefore.SourceFiles.Length 3 "Source files length should be 3" + Expect.equal consoleBefore.SourceFiles.Length consoleAfter.SourceFiles.Length "Source files length should be the same" let consoleFile = Projects.MultiProjectScenario1.Console1.programFileIn dDir.DirectoryInfo - let cs1File = cs1.SourceFiles |> Seq.find (fun x -> x.FileName = normalizeUntag consoleFile.FullName) - let cs2File = cs2.SourceFiles |> Seq.find (fun x -> x.FileName = normalizeUntag consoleFile.FullName) + let cs1File = consoleBefore.SourceFiles |> Seq.find (fun x -> x.FileName = normalizeUntag consoleFile.FullName) + let cs2File = consoleAfter.SourceFiles |> Seq.find (fun x -> x.FileName = normalizeUntag consoleFile.FullName) Expect.equal cs1File.Version cs2File.Version "Console source file version should be the same" - Expect.equal cs1.ReferencedProjects.Length cs2.ReferencedProjects.Length "Referenced projects length should be the same" - Expect.equal cs1.ReferencedProjects.Length 1 "Referenced projects length should be 1" - let refLib1 = cs1.ReferencedProjects |> Seq.tryPick (fun x -> match x with | FSharpReferencedProjectSnapshot.FSharpReference(_, x) -> Some x | _ -> None) |> Option.get - Expect.equal refLib1 ls1 "Referenced library should be the same as library snapshot" - let refLib2 = cs2.ReferencedProjects |> Seq.tryPick (fun x -> match x with | FSharpReferencedProjectSnapshot.FSharpReference(_, x) -> Some x | _ -> None) |> Option.get - Expect.equal refLib2 ls2 "Referenced library should be the same as library snapshot" + Expect.equal consoleBefore.ReferencedProjects.Length consoleAfter.ReferencedProjects.Length "Referenced projects length should be the same" + Expect.equal consoleBefore.ReferencedProjects.Length 1 "Referenced projects length should be 1" + let refLib1 = consoleBefore.ReferencedProjects |> Seq.tryPick (fun x -> match x with | FSharpReferencedProjectSnapshot.FSharpReference(_, x) -> Some x | _ -> None) |> Option.get + Expect.equal refLib1 libBefore "Referenced library should be the same as library snapshot" + let refLib2 = consoleAfter.ReferencedProjects |> Seq.tryPick (fun x -> match x with | FSharpReferencedProjectSnapshot.FSharpReference(_, x) -> Some x | _ -> None) |> Option.get + Expect.equal refLib2 libAfter "Referenced library should be the same as library snapshot" Expect.notEqual refLib1 refLib2 "Referenced library from different snapshot should not be the same as library source file changed" - Expect.notEqual cs1.Stamp cs2.Stamp "Stamp should not be the same" + Expect.notEqual consoleBefore.Stamp consoleAfter.Stamp "Stamp should not be the same" } From 1bb1ce324c270ad9460b46cec09becffc621f6e5 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Tue, 16 Apr 2024 19:40:00 -0400 Subject: [PATCH 30/60] fixing tests part 302 --- .../CodeFixTests/Tests.fs | 4 +-- test/FsAutoComplete.Tests.Lsp/ScriptTests.fs | 2 ++ .../FsAutoComplete.Tests.Lsp/SnapshotTests.fs | 25 ++++++++----------- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs index 2f0e6435a..f8d362aba 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs @@ -2679,11 +2679,11 @@ let private replaceWithSuggestionTests state = let validateDiags (diags: Diagnostic[]) = Diagnostics.expectCode "39" diags - + let messages = diags |> Array.map (fun d -> d.Message) |> String.concat "\n" Expect.exists diags (fun (d: Diagnostic) -> d.Message.Contains "Maybe you want one of the following:") - "Diagnostic with code 39 should suggest name" + $"Diagnostic with code 39 should suggest name: Contained {messages}" testCaseAsync "can change Min to min" <| CodeFix.check diff --git a/test/FsAutoComplete.Tests.Lsp/ScriptTests.fs b/test/FsAutoComplete.Tests.Lsp/ScriptTests.fs index cea2d7ebe..be0553bd5 100644 --- a/test/FsAutoComplete.Tests.Lsp/ScriptTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/ScriptTests.fs @@ -123,6 +123,7 @@ let dependencyManagerTests state = } |> Async.Cache + testSequenced <| testList "dependencyManager integrations" [ testList @@ -178,6 +179,7 @@ let scriptProjectOptionsCacheTests state = return server, events, workingDir, scriptPath, options } + testSequenced <| testList "ScriptProjectOptionsCache" [ testList diff --git a/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs index db1f713bf..f05f7cdae 100644 --- a/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs @@ -103,25 +103,20 @@ let createProjectA (projects : FileInfo seq) (loader : IWorkspaceLoader) onLoadC let normalizeUntag = normalizePath >> UMX.untag let awaitFileChanged (file : FileInfo) = + let originalLastWriteTime = file.LastWriteTimeUtc + let mutable lastWriteTime = file.LastWriteTimeUtc // The AdaptiveFile implementation uses FileSystemWatcher under the hood to watch for file changes. // The problem is on different operating systems the file system watcher behaves differently. // Our tests may run quicker than the file system watcher can pick up the changes - // So we need to wait for a file system watcher to pick up the changes - // Better than using a sleep is to use a task completion source to signal when the file system watcher has picked up the changes + // So we need to wait for a change to happen before we continue. + // FileSystemWatcher doesn't seem to work so we're going to poll the file for changes. + task { - ignore file - // let tcs = new TaskCompletionSource() - // let handleChange _ = - // tcs.TrySetResult () |> ignore - // use fsi = new FileSystemWatcher(file.Directory.FullName, file.Name) - // // fsi.NotifyFilter <- NotifyFilters.Attributes ||| NotifyFilters.FileName ||| NotifyFilters.DirectoryName - // fsi.Changed.Add handleChange - // fsi.Created.Add handleChange - // fsi.Deleted.Add handleChange - // fsi.Renamed.Add handleChange - // fsi.EnableRaisingEvents <- true - // do! tcs.Task - do! Task.Delay(250) // if this works kill me + while lastWriteTime = originalLastWriteTime do + do! Task.Delay(15) + file.Refresh() + lastWriteTime <- file.LastWriteTimeUtc + } let snapshotTests loaders toolsPath = From 2d2f29c7107f43360313b6fee2271c0a67d1ab98 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Tue, 16 Apr 2024 20:12:45 -0400 Subject: [PATCH 31/60] fixing tests part 303 --- test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs index f05f7cdae..6643cd7c5 100644 --- a/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs @@ -104,16 +104,17 @@ let normalizeUntag = normalizePath >> UMX.untag let awaitFileChanged (file : FileInfo) = let originalLastWriteTime = file.LastWriteTimeUtc - let mutable lastWriteTime = file.LastWriteTimeUtc // The AdaptiveFile implementation uses FileSystemWatcher under the hood to watch for file changes. // The problem is on different operating systems the file system watcher behaves differently. // Our tests may run quicker than the file system watcher can pick up the changes // So we need to wait for a change to happen before we continue. // FileSystemWatcher doesn't seem to work so we're going to poll the file for changes. - task { + cancellableTask { + file.Refresh() + let mutable lastWriteTime = file.LastWriteTimeUtc while lastWriteTime = originalLastWriteTime do - do! Task.Delay(15) + do! fun ct -> Task.Delay(15, ct) file.Refresh() lastWriteTime <- file.LastWriteTimeUtc From 54749ee13e1fc8e9b55f5d4b87d67c4b8743d12b Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Tue, 16 Apr 2024 20:49:41 -0400 Subject: [PATCH 32/60] fixing tests part 304 --- .../FsAutoComplete.Tests.Lsp/SnapshotTests.fs | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs index 6643cd7c5..6a903bdd0 100644 --- a/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs @@ -102,7 +102,7 @@ let createProjectA (projects : FileInfo seq) (loader : IWorkspaceLoader) onLoadC let normalizeUntag = normalizePath >> UMX.untag -let awaitFileChanged (file : FileInfo) = +let awaitFileChanged (file : FileInfo) ct = let originalLastWriteTime = file.LastWriteTimeUtc // The AdaptiveFile implementation uses FileSystemWatcher under the hood to watch for file changes. // The problem is on different operating systems the file system watcher behaves differently. @@ -110,14 +110,19 @@ let awaitFileChanged (file : FileInfo) = // So we need to wait for a change to happen before we continue. // FileSystemWatcher doesn't seem to work so we're going to poll the file for changes. - cancellableTask { + task { file.Refresh() let mutable lastWriteTime = file.LastWriteTimeUtc while lastWriteTime = originalLastWriteTime do - do! fun ct -> Task.Delay(15, ct) + do! Task.Delay(17, ct) file.Refresh() lastWriteTime <- file.LastWriteTimeUtc + } +let awaitOutOfDate (o : #IAdaptiveObject) ct = + task { + while not o.OutOfDate do + do! Task.Delay(17, ct) } let snapshotTests loaders toolsPath = @@ -234,16 +239,19 @@ let snapshotTests loaders toolsPath = let snapsA = Snapshots.createSnapshots AMap.empty (AVal.constant sourceTextFactory) loadedProjectsA - let snapshotsBefore = snapsA |> AMap.mapA (fun _ (_,v) -> v) |> AMap.force + let snaps = snapsA |> AMap.mapA (fun _ (_,v) -> v) + let snapshotsBefore = snaps |> AMap.force let consoleFile = Projects.MultiProjectScenario1.Console1.programFileIn dDir.DirectoryInfo - let fileChanged = awaitFileChanged consoleFile + let! ct = Async.CancellationToken + // let fileChanged = awaitFileChanged consoleFile ct do! File.WriteAllTextAsync(consoleFile.FullName, "let x = 1") - do! fileChanged + do! awaitOutOfDate (snaps.Content) ct + // do! fileChanged consoleFile.Refresh() - let snapshotAfter = snapsA |> AMap.mapA (fun _ (_,v) -> v) |> AMap.force + let snapshotAfter = snaps |> AMap.force let ls1 = snapshotsBefore |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) let ls2 = snapshotAfter |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) @@ -280,16 +288,18 @@ let snapshotTests loaders toolsPath = let snapsA = Snapshots.createSnapshots AMap.empty (AVal.constant sourceTextFactory) loadedProjectsA - - let snapshotBefore = snapsA |> AMap.mapA (fun _ (_,v) -> v) |> AMap.force + let snaps = snapsA |> AMap.mapA (fun _ (_,v) -> v) + let snapshotBefore = snaps |> AMap.force let libraryFile = Projects.MultiProjectScenario1.Library1.libraryFileIn dDir.DirectoryInfo - let fileChanged = awaitFileChanged libraryFile + let! ct = Async.CancellationToken + // let fileChanged = awaitFileChanged libraryFile ct do! File.WriteAllTextAsync(libraryFile.FullName, "let x = 1") - do! fileChanged + do! awaitOutOfDate (snaps.Content) ct + // do! fileChanged libraryFile.Refresh() - let snapshotAfter = snapsA |> AMap.mapA (fun _ (_,v) -> v) |> AMap.force + let snapshotAfter = snaps |> AMap.force let libBefore = snapshotBefore |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) let libAfter = snapshotAfter |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) From 1ea103530226ce7507bdeae567c5c5e16c092ebe Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Tue, 16 Apr 2024 21:22:12 -0400 Subject: [PATCH 33/60] fixing tests part 305 --- .../FsAutoComplete.Tests.Lsp/SnapshotTests.fs | 32 ++++++------------- .../FsAutoComplete.Tests.Lsp/paket.references | 2 +- 2 files changed, 10 insertions(+), 24 deletions(-) diff --git a/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs index 6a903bdd0..dd044f24b 100644 --- a/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs @@ -102,27 +102,15 @@ let createProjectA (projects : FileInfo seq) (loader : IWorkspaceLoader) onLoadC let normalizeUntag = normalizePath >> UMX.untag -let awaitFileChanged (file : FileInfo) ct = - let originalLastWriteTime = file.LastWriteTimeUtc +let awaitOutOfDate (o : #IAdaptiveObject) = // The AdaptiveFile implementation uses FileSystemWatcher under the hood to watch for file changes. // The problem is on different operating systems the file system watcher behaves differently. // Our tests may run quicker than the file system watcher can pick up the changes // So we need to wait for a change to happen before we continue. - // FileSystemWatcher doesn't seem to work so we're going to poll the file for changes. - - task { - file.Refresh() - let mutable lastWriteTime = file.LastWriteTimeUtc - while lastWriteTime = originalLastWriteTime do - do! Task.Delay(17, ct) - file.Refresh() - lastWriteTime <- file.LastWriteTimeUtc - } -let awaitOutOfDate (o : #IAdaptiveObject) ct = - task { + async { while not o.OutOfDate do - do! Task.Delay(17, ct) + do! Async.Sleep 15 } let snapshotTests loaders toolsPath = @@ -243,11 +231,10 @@ let snapshotTests loaders toolsPath = let snapshotsBefore = snaps |> AMap.force let consoleFile = Projects.MultiProjectScenario1.Console1.programFileIn dDir.DirectoryInfo - let! ct = Async.CancellationToken - // let fileChanged = awaitFileChanged consoleFile ct + do! File.WriteAllTextAsync(consoleFile.FullName, "let x = 1") - do! awaitOutOfDate (snaps.Content) ct - // do! fileChanged + do! awaitOutOfDate (snaps.Content) + consoleFile.Refresh() @@ -292,11 +279,10 @@ let snapshotTests loaders toolsPath = let snapshotBefore = snaps |> AMap.force let libraryFile = Projects.MultiProjectScenario1.Library1.libraryFileIn dDir.DirectoryInfo - let! ct = Async.CancellationToken - // let fileChanged = awaitFileChanged libraryFile ct + do! File.WriteAllTextAsync(libraryFile.FullName, "let x = 1") - do! awaitOutOfDate (snaps.Content) ct - // do! fileChanged + do! awaitOutOfDate (snaps.Content) + libraryFile.Refresh() let snapshotAfter = snaps |> AMap.force diff --git a/test/FsAutoComplete.Tests.Lsp/paket.references b/test/FsAutoComplete.Tests.Lsp/paket.references index adb2e8e05..ca018d622 100644 --- a/test/FsAutoComplete.Tests.Lsp/paket.references +++ b/test/FsAutoComplete.Tests.Lsp/paket.references @@ -5,7 +5,7 @@ FSharpx.Async Expecto.Diff Microsoft.NET.Test.Sdk YoloDev.Expecto.TestSdk -AltCover +# AltCover GitHubActionsTestLogger CliWrap FSharp.Data.Adaptive From dc39ae3aedc2124ab1d369e0e2e42c968bfb1b15 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Tue, 16 Apr 2024 21:58:22 -0400 Subject: [PATCH 34/60] get more logging in github actions --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8569b493c..4b4afe70e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -102,7 +102,7 @@ jobs: BuildNet8: ${{ matrix.build_net8 }} - name: Run and report tests - run: dotnet test -c Release -f ${{ matrix.test_tfm }} --no-restore --no-build --no-build --logger GitHubActions /p:AltCover=true /p:AltCoverAssemblyExcludeFilter="System.Reactive|FSharp.Compiler.Service|Ionide.ProjInfo|FSharp.Analyzers|Analyzer|Humanizer|FSharp.Core|FSharp.DependencyManager" -- Expecto.fail-on-focused-tests=true --blame-hang --blame-hang-timeout 1m + run: dotnet test -c Release -f ${{ matrix.test_tfm }} --no-restore --no-build --no-build --logger "console;verbosity=normal" --logger GitHubActions /p:AltCover=true /p:AltCoverAssemblyExcludeFilter="System.Reactive|FSharp.Compiler.Service|Ionide.ProjInfo|FSharp.Analyzers|Analyzer|Humanizer|FSharp.Core|FSharp.DependencyManager" -- Expecto.fail-on-focused-tests=true --blame-hang --blame-hang-timeout 1m working-directory: test/FsAutoComplete.Tests.Lsp env: BuildNet7: ${{ matrix.build_net7 }} From 249d1b675101d1516700e945c5e0a7ea2c2d0ce7 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Tue, 16 Apr 2024 22:15:25 -0400 Subject: [PATCH 35/60] fixing tests part 306 --- .github/workflows/build.yml | 4 ++-- test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4b4afe70e..a256f4c5d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,10 +16,10 @@ on: jobs: build: env: - TEST_TIMEOUT_MINUTES: 40 + TEST_TIMEOUT_MINUTES: 19 FSAC_TEST_DEFAULT_TIMEOUT : 120000 #ms, individual test timeouts DOTNET_ROLL_FORWARD_TO_PRERELEASE: 1 # needed to allow .NET 8 RCs to participate in rollforward as expected. - timeout-minutes: 40 # we have a locking issue, so cap the runs at ~20m to account for varying build times, etc + timeout-minutes: 20 # we have a locking issue, so cap the runs at ~20m to account for varying build times, etc strategy: matrix: os: diff --git a/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs index dd044f24b..973f09525 100644 --- a/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs @@ -102,14 +102,14 @@ let createProjectA (projects : FileInfo seq) (loader : IWorkspaceLoader) onLoadC let normalizeUntag = normalizePath >> UMX.untag -let awaitOutOfDate (o : #IAdaptiveObject) = +let awaitOutOfDate (o : amap<_,_>) = // The AdaptiveFile implementation uses FileSystemWatcher under the hood to watch for file changes. // The problem is on different operating systems the file system watcher behaves differently. // Our tests may run quicker than the file system watcher can pick up the changes // So we need to wait for a change to happen before we continue. async { - while not o.OutOfDate do + while not o.Content.OutOfDate && not (o.GetReader().OutOfDate) do do! Async.Sleep 15 } @@ -117,7 +117,7 @@ let snapshotTests loaders toolsPath = testList "ProjectWorkspace" [ for (loaderName, workspaceLoaderFactory) in loaders do - // testSequencedGroup loaderName <| + testSequencedGroup loaderName <| testList $"{loaderName}" [ testCaseAsync "Simple Project Load" <| async { let (loader : IWorkspaceLoader) = workspaceLoaderFactory toolsPath @@ -233,7 +233,7 @@ let snapshotTests loaders toolsPath = let consoleFile = Projects.MultiProjectScenario1.Console1.programFileIn dDir.DirectoryInfo do! File.WriteAllTextAsync(consoleFile.FullName, "let x = 1") - do! awaitOutOfDate (snaps.Content) + do! awaitOutOfDate snaps consoleFile.Refresh() @@ -281,7 +281,7 @@ let snapshotTests loaders toolsPath = let libraryFile = Projects.MultiProjectScenario1.Library1.libraryFileIn dDir.DirectoryInfo do! File.WriteAllTextAsync(libraryFile.FullName, "let x = 1") - do! awaitOutOfDate (snaps.Content) + do! awaitOutOfDate snaps libraryFile.Refresh() From edfa45099bff77427088e424a754821382649472 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Tue, 16 Apr 2024 22:47:27 -0400 Subject: [PATCH 36/60] fixing tests part 307 --- .../FsAutoComplete.Tests.Lsp/SnapshotTests.fs | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs index 973f09525..c922cde76 100644 --- a/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs @@ -108,9 +108,15 @@ let awaitOutOfDate (o : amap<_,_>) = // Our tests may run quicker than the file system watcher can pick up the changes // So we need to wait for a change to happen before we continue. - async { - while not o.Content.OutOfDate && not (o.GetReader().OutOfDate) do - do! Async.Sleep 15 + task { + let tcs = new TaskCompletionSource() + use _ = o.AddCallback(fun _ _ -> tcs.TrySetResult() |> ignore) + return! tcs.Task + // while not o.Content.OutOfDate do + // do! Async.Sleep 15 + + // printfn "o.Content.OutOfDate: %b" o.Content.OutOfDate + // printfn "o.GetReader.OutOfDate: %b" (o.GetReader().OutOfDate) } let snapshotTests loaders toolsPath = @@ -231,9 +237,9 @@ let snapshotTests loaders toolsPath = let snapshotsBefore = snaps |> AMap.force let consoleFile = Projects.MultiProjectScenario1.Console1.programFileIn dDir.DirectoryInfo - + let awaitOutOfDate = awaitOutOfDate snaps do! File.WriteAllTextAsync(consoleFile.FullName, "let x = 1") - do! awaitOutOfDate snaps + do! awaitOutOfDate consoleFile.Refresh() @@ -279,9 +285,9 @@ let snapshotTests loaders toolsPath = let snapshotBefore = snaps |> AMap.force let libraryFile = Projects.MultiProjectScenario1.Library1.libraryFileIn dDir.DirectoryInfo - + let awaitOutOfDate = awaitOutOfDate snaps do! File.WriteAllTextAsync(libraryFile.FullName, "let x = 1") - do! awaitOutOfDate snaps + do! awaitOutOfDate libraryFile.Refresh() From fe45c9cb6151a0bd7d44bde6e0b8e9afeaf06001 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Tue, 16 Apr 2024 23:15:29 -0400 Subject: [PATCH 37/60] fixing tests part 308 --- .../FsAutoComplete.Tests.Lsp/SnapshotTests.fs | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs index c922cde76..83457ba0f 100644 --- a/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs @@ -109,8 +109,25 @@ let awaitOutOfDate (o : amap<_,_>) = // So we need to wait for a change to happen before we continue. task { + // printfn "o.Content.OutOfDate: %b" o.Content.OutOfDate + // printfn "o.GetReader.OutOfDate: %b" (o.GetReader().OutOfDate) let tcs = new TaskCompletionSource() - use _ = o.AddCallback(fun _ _ -> tcs.TrySetResult() |> ignore) + use cts = new System.Threading.CancellationTokenSource() + cts.CancelAfter(5000) + use _ = cts.Token.Register(fun () -> tcs.TrySetCanceled(cts.Token) |> ignore) + use _ = o.AddCallback(fun s _ -> + // printfn "state: %A" s + // printfn "delta: %A" d + // printfn "o.Content.OutOfDate: %b" o.Content.OutOfDate + // printfn "o.GetReader.OutOfDate: %b" (o.GetReader().OutOfDate) + if s.IsEmpty |> not then + tcs.TrySetResult() |> ignore + ) + + // use _ = o.AddCallback(fun s d -> + // printfn "state: %A" s + // printfn "delta: %A" d) + // printfn "Awaiting out of date" return! tcs.Task // while not o.Content.OutOfDate do // do! Async.Sleep 15 @@ -234,17 +251,19 @@ let snapshotTests loaders toolsPath = let snapsA = Snapshots.createSnapshots AMap.empty (AVal.constant sourceTextFactory) loadedProjectsA let snaps = snapsA |> AMap.mapA (fun _ (_,v) -> v) - let snapshotsBefore = snaps |> AMap.force + let consoleFile = Projects.MultiProjectScenario1.Console1.programFileIn dDir.DirectoryInfo + + let snapshotsBefore = snaps |> AMap.force let awaitOutOfDate = awaitOutOfDate snaps do! File.WriteAllTextAsync(consoleFile.FullName, "let x = 1") do! awaitOutOfDate consoleFile.Refresh() + let snapshotAfter = snaps |> AMap.force - let snapshotAfter = snaps |> AMap.force let ls1 = snapshotsBefore |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) let ls2 = snapshotAfter |> HashMap.find (normalizePath (Projects.MultiProjectScenario1.Library1.projectIn dDir.DirectoryInfo).FullName) @@ -282,10 +301,13 @@ let snapshotTests loaders toolsPath = let snapsA = Snapshots.createSnapshots AMap.empty (AVal.constant sourceTextFactory) loadedProjectsA let snaps = snapsA |> AMap.mapA (fun _ (_,v) -> v) - let snapshotBefore = snaps |> AMap.force + let libraryFile = Projects.MultiProjectScenario1.Library1.libraryFileIn dDir.DirectoryInfo + + let snapshotBefore = snaps |> AMap.force let awaitOutOfDate = awaitOutOfDate snaps + printfn "Writing to file: %s" libraryFile.FullName do! File.WriteAllTextAsync(libraryFile.FullName, "let x = 1") do! awaitOutOfDate From 3ac6a9082f5e507128deeb7e0e496e570f59fe5f Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Tue, 16 Apr 2024 23:43:14 -0400 Subject: [PATCH 38/60] cleanup --- .../FsAutoComplete.Tests.Lsp/SnapshotTests.fs | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs index 83457ba0f..01666332b 100644 --- a/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs @@ -109,31 +109,15 @@ let awaitOutOfDate (o : amap<_,_>) = // So we need to wait for a change to happen before we continue. task { - // printfn "o.Content.OutOfDate: %b" o.Content.OutOfDate - // printfn "o.GetReader.OutOfDate: %b" (o.GetReader().OutOfDate) let tcs = new TaskCompletionSource() use cts = new System.Threading.CancellationTokenSource() cts.CancelAfter(5000) use _ = cts.Token.Register(fun () -> tcs.TrySetCanceled(cts.Token) |> ignore) use _ = o.AddCallback(fun s _ -> - // printfn "state: %A" s - // printfn "delta: %A" d - // printfn "o.Content.OutOfDate: %b" o.Content.OutOfDate - // printfn "o.GetReader.OutOfDate: %b" (o.GetReader().OutOfDate) - if s.IsEmpty |> not then + if not <| s.IsEmpty then tcs.TrySetResult() |> ignore ) - - // use _ = o.AddCallback(fun s d -> - // printfn "state: %A" s - // printfn "delta: %A" d) - // printfn "Awaiting out of date" return! tcs.Task - // while not o.Content.OutOfDate do - // do! Async.Sleep 15 - - // printfn "o.Content.OutOfDate: %b" o.Content.OutOfDate - // printfn "o.GetReader.OutOfDate: %b" (o.GetReader().OutOfDate) } let snapshotTests loaders toolsPath = @@ -307,7 +291,6 @@ let snapshotTests loaders toolsPath = let snapshotBefore = snaps |> AMap.force let awaitOutOfDate = awaitOutOfDate snaps - printfn "Writing to file: %s" libraryFile.FullName do! File.WriteAllTextAsync(libraryFile.FullName, "let x = 1") do! awaitOutOfDate From 4caf36662b29f6f11721bd07760ca02cd61ecb00 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Sun, 21 Apr 2024 15:35:59 -0400 Subject: [PATCH 39/60] refactoring --- src/FsAutoComplete.Core/FileSystem.fs | 20 ++--- .../LspServers/AdaptiveServerState.fs | 76 +++++++++---------- .../LspServers/FSharpLspClient.fs | 2 +- 3 files changed, 48 insertions(+), 50 deletions(-) diff --git a/src/FsAutoComplete.Core/FileSystem.fs b/src/FsAutoComplete.Core/FileSystem.fs index 1bb220150..4410dc14e 100644 --- a/src/FsAutoComplete.Core/FileSystem.fs +++ b/src/FsAutoComplete.Core/FileSystem.fs @@ -20,9 +20,13 @@ module File = File.GetLastWriteTimeUtc path else DateTime.UtcNow + /// Buffer size for reading from the stream. + /// 81,920 bytes (80KB) is below the Large Object Heap threshold (85,000 bytes) + /// and is a good size for performance. Dotnet uses this for their defaults. + let [] bufferSize = 81920 let openFileStreamForReadingAsync (path: string) = - new FileStream((UMX.untag path), FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize = 4096, useAsync = true) + new FileStream((UMX.untag path), FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize = bufferSize, useAsync = true) [] module PositionExtensions = @@ -71,7 +75,7 @@ module RangeExtensions = /// utility method to get the tagged filename for use in our state storage /// TODO: should we enforce this/use the Path members for normalization? - member x.TaggedFileName: string = UMX.tag x.FileName + member x.TaggedFileName: string = Utils.normalizePath x.FileName member inline r.With(start, fin) = Range.mkRange r.FileName start fin member inline r.WithStart(start) = Range.mkRange r.FileName start r.End @@ -330,7 +334,6 @@ module RoslynSourceText = Ok(RoslynSourceTextFile(fileName, sourceText.WithChanges(change))) - interface ISourceText with member _.Item @@ -394,22 +397,19 @@ type ISourceTextFactory = abstract member Create: fileName: string * stream: Stream -> CancellableValueTask module SourceTextFactory = - // Could be configurable but using the default for now - // https://learn.microsoft.com/en-us/dotnet/core/runtime-config/garbage-collector#large-object-heap-threshold - [] - let LargeObjectHeapThreshold = 85000 - let readFile (fileName: string) (sourceTextFactory: ISourceTextFactory) = + + let readFile (fileName: string) (sourceTextFactory : ISourceTextFactory) = cancellableValueTask { let file = UMX.untag fileName // use large object heap hits or threadpool hits? Which is worse? Choose your foot gun. - if FileInfo(file).Length >= LargeObjectHeapThreshold then + if FileInfo(file).Length >= File.bufferSize then // Roslyn SourceText doesn't actually support async streaming reads but avoids the large object heap hit // so we have to block a thread. use s = File.openFileStreamForReadingAsync fileName - let! source = sourceTextFactory.Create(fileName, s) + let! source = sourceTextFactory.Create (fileName, s) return source else // otherwise it'll be under the LOH threshold and the current thread isn't blocked diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index 04b85334c..f5a0e0ead 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -326,7 +326,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let inline getSourceLine lineNo = (source: ISourceText).GetLineString(lineNo - 1) let checkUnusedOpens = - async { + asyncEx { try use progress = new ServerProgressReport(lspClient) do! progress.Begin($"Checking unused opens {fileName}...", message = filePathUntag) @@ -340,7 +340,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac } let checkUnusedDeclarations = - async { + asyncEx { try use progress = new ServerProgressReport(lspClient) do! progress.Begin($"Checking unused declarations {fileName}...", message = filePathUntag) @@ -356,7 +356,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac } let checkSimplifiedNames = - async { + asyncEx { try use progress = new ServerProgressReport(lspClient) do! progress.Begin($"Checking simplifying of names {fileName}...", message = filePathUntag) @@ -370,7 +370,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac } let checkUnnecessaryParentheses = - async { + asyncEx { try use progress = new ServerProgressReport(lspClient) do! progress.Begin($"Checking for unnecessary parentheses {fileName}...", message = filePathUntag) @@ -437,7 +437,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let runAnalyzers (config: FSharpConfig) (parseAndCheck: ParseAndCheckResults) (volatileFile: VolatileFile) = - async { + asyncEx { if config.EnableAnalyzers then let file = volatileFile.FileName @@ -823,12 +823,12 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let loadProjects (loader: IWorkspaceLoader) binlogConfig projects = - logger.info (Log.setMessageI $"Enter loading projects") + logger.debug (Log.setMessageI $"Enter loading projects") projects |> AMap.mapWithAdditionalDependencies (fun projects -> - logger.info (Log.setMessageI $"Enter loading projects mapWithAdditionalDependencies") + logger.debug (Log.setMessageI $"Enter loading projects mapWithAdditionalDependencies") projects |> Seq.iter (fun (proj: string, _) -> @@ -1001,7 +1001,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac return file }) - let snapshots = + let projectOptions = asyncAVal { let! wsp = adaptiveWorkspacePaths @@ -1010,33 +1010,31 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac match wsp with - | AdaptiveWorkspaceChosen.NotChosen -> return AMap.empty + | AdaptiveWorkspaceChosen.NotChosen -> return HashMap.empty | AdaptiveWorkspaceChosen.Projs projects -> + let! loader = + loader + |> addAValLogging (fun () -> logger.info (Log.setMessage "Loading projects because WorkspaceLoader change")) + + and! binlogConfig = + binlogConfig + |> addAValLogging (fun () -> logger.info (Log.setMessage "Loading projects because binlogConfig change")) + let! projects = - asyncAVal { - let! loader = - loader - |> addAValLogging (fun () -> - logger.info (Log.setMessage "Loading projects because WorkspaceLoader change")) - - and! binlogConfig = - binlogConfig - |> addAValLogging (fun () -> logger.info (Log.setMessage "Loading projects because binlogConfig change")) - - let! projects = - // need to bind to a single value to keep the threadpool from being exhausted as LoadingProjects can be a long running operation - // and when other adaptive values await on this, the scheduler won't block those other tasks - loadProjects loader binlogConfig projects |> AMap.toAVal - - and! checker = checker - checker.ClearCaches() - return projects - } + // need to bind to a single value to keep the threadpool from being exhausted as LoadingProjects can be a long running operation + // and when other adaptive values await on this, the scheduler won't block those other tasks + loadProjects loader binlogConfig projects |> AMap.toAVal - logger.info (Log.setMessageI $"After loading projects and before creating snapshots") + and! checker = checker + checker.ClearCaches() + return projects + } - return - Snapshots.createSnapshots openFilesWithChanges (AVal.constant sourceTextFactory) (AMap.ofHashMap projects) + + let snapshots = + asyncAVal { + let! projects = projectOptions + return Snapshots.createSnapshots openFilesWithChanges (AVal.constant sourceTextFactory) (AMap.ofHashMap projects) } let loadedProjects = @@ -1072,13 +1070,13 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac /// A list of FSharpProjectOptions let forceLoadProjects () = getAllLoadedProjects |> AsyncAVal.forceAsync - // do - // // Reload Projects with some debouncing if `loadedProjectOptions` is out of date. - // AVal.Observable.onOutOfDateWeak loadedProjectOptions - // |> Observable.throttleOn Concurrency.NewThreadScheduler.Default (TimeSpan.FromMilliseconds(200.)) - // |> Observable.observeOn Concurrency.NewThreadScheduler.Default - // |> Observable.subscribe (fun _ -> forceLoadProjects () |> ignore>) - // |> disposables.Add + do + // Reload Projects with some debouncing if `loadedProjectOptions` is out of date. + AVal.Observable.onOutOfDateWeak projectOptions + |> Observable.throttleOn Concurrency.NewThreadScheduler.Default (TimeSpan.FromMilliseconds(200.)) + |> Observable.observeOn Concurrency.NewThreadScheduler.Default + |> Observable.subscribe (fun _ -> forceLoadProjects () |> Async.Ignore |> Async.Start) + |> disposables.Add let AMapReKeyMany f map = map |> AMap.toASet |> ASet.collect f |> AMap.ofASet @@ -2156,7 +2154,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let bypassAdaptiveAndCheckDependenciesForFile (sourceFilePath: string) = - async { + asyncEx { let tags = [ SemanticConventions.fsac_sourceCodePath, box (UMX.untag sourceFilePath) ] diff --git a/src/FsAutoComplete/LspServers/FSharpLspClient.fs b/src/FsAutoComplete/LspServers/FSharpLspClient.fs index ea8cc96aa..477e1f6c3 100644 --- a/src/FsAutoComplete/LspServers/FSharpLspClient.fs +++ b/src/FsAutoComplete/LspServers/FSharpLspClient.fs @@ -294,7 +294,7 @@ type ProgressListener(lspClient: FSharpLspClient, traceNamespace: string array) dispose listener for (a, p) in inflightEvents.Values do - do! (disposeAsync p).AsTask() + do! disposeAsync p inflightEvents.TryRemove(a.Id) |> ignore } |> Async.StartImmediateAsTask From e499e3fdcf0b5d643f76219aeec559f6c38767fc Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Sun, 21 Apr 2024 15:37:30 -0400 Subject: [PATCH 40/60] Add OTel to projectworkspace --- .../LspServers/ProjectWorkspace.fs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/FsAutoComplete/LspServers/ProjectWorkspace.fs b/src/FsAutoComplete/LspServers/ProjectWorkspace.fs index f2c477fb8..13400ab37 100644 --- a/src/FsAutoComplete/LspServers/ProjectWorkspace.fs +++ b/src/FsAutoComplete/LspServers/ProjectWorkspace.fs @@ -1,6 +1,8 @@ namespace FsAutoComplete.ProjectWorkspace open System +open FsAutoComplete.Telemetry +open FsAutoComplete.Utils.Tracing module Snapshots = open System @@ -31,7 +33,7 @@ module Snapshots = let getStream (_ctok: System.Threading.CancellationToken) = try - projectFile.OpenRead() :> Stream |> Some + File.openFileStreamForReadingAsync (normalizePath p.TargetPath) :> Stream |> Some with _ -> None @@ -169,6 +171,10 @@ module Snapshots = (loadedProjectsA: amap, ProjectOptions>) (p: ProjectOptions) = + let tags = seq { + "projectFileName", box p.ProjectFileName + } + use _span = fsacActivitySource.StartActivityForFunc(tags = tags) logger.debug ( Log.setMessage "Creating references for {projectFileName}" >> Log.addContextDestructured "projectFileName" p.ProjectFileName @@ -195,12 +201,13 @@ module Snapshots = sourceTextFactory (createReferences cachedSnapshots inMemorySourceFiles sourceTextFactory loadedProjectsA) |> createReferencedProjectsFSharpReference resolvedTargetPath + else // TODO: Find if this needs to be adaptive or if `getStamp` in a PEReference will be enough to break thru the caching in FCS loadFromDotnetDll proj |> AVal.constant) |> AMap.toASetValues - and optionsToSnapshot + and private optionsToSnapshot (cachedSnapshots: Dictionary<_, _>) (inMemorySourceFiles: amap<_, aval>) (sourceTextFactory: aval) @@ -208,9 +215,14 @@ module Snapshots = (p: ProjectOptions) = let normPath = Utils.normalizePath p.ProjectFileName + let tags = seq { + "projectFileName", box p.ProjectFileName + } + use span = fsacActivitySource.StartActivityForFunc(tags = tags) match cachedSnapshots.TryGetValue normPath with | true, snapshot -> + span.SetTagSafe("cachehit", true) |> ignore logger.debug ( Log.setMessage "optionsToSnapshot - Cache hit - {projectFileName}" >> Log.addContextDestructured "projectFileName" p.ProjectFileName @@ -280,8 +292,6 @@ module Snapshots = (sourceTextFactory: aval) (loadedProjectsA: amap, ProjectOptions>) = - - loadedProjectsA |> AMap.filter (fun k _ -> (UMX.untag k).EndsWith ".fsproj") |> AMap.toAVal From bd2153cf6c09f47f21f9d6f2d616e9e47e0c461c Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Sun, 21 Apr 2024 15:37:45 -0400 Subject: [PATCH 41/60] add description to emptyfiletests --- test/FsAutoComplete.Tests.Lsp/EmptyFileTests.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/FsAutoComplete.Tests.Lsp/EmptyFileTests.fs b/test/FsAutoComplete.Tests.Lsp/EmptyFileTests.fs index 1e13a6c35..a2315e7a1 100644 --- a/test/FsAutoComplete.Tests.Lsp/EmptyFileTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/EmptyFileTests.fs @@ -93,7 +93,7 @@ let tests state = | Ok () -> failtest "should get an F# compiler checking error from a 'c' by itself" | Core.Result.Error errors -> Expect.hasLength errors 1 "should have only an error FS0039: identifier not defined" - Expect.exists errors (fun error -> error.Code = Some "39") "should have an error FS0039: identifier not defined" + Expect.exists errors (fun error -> error.Code = Some "39") $"should have an error FS0039: identifier not defined %A{errors}" match! completions with | Ok (Some completions) -> From 41c08aa53e51dd4a326936c0314b620e1c4c8431 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Sun, 21 Apr 2024 15:38:03 -0400 Subject: [PATCH 42/60] add retry to cursorbased tests --- .../Utils/CursorbasedTests.fs | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fs b/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fs index f67bc61fc..597085564 100644 --- a/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fs @@ -9,6 +9,7 @@ open Utils.TextEdit open Ionide.ProjInfo.Logging /// Checks for CodeFixes, CodeActions +open System.Runtime.ExceptionServices /// /// Prefixes: /// * `check`: Check to use inside a `testCaseAsync`. Not a Test itself! @@ -116,20 +117,34 @@ module CodeFix = (expected: unit -> ExpectedResult) = async { - let (range, text) = - beforeWithCursor |> Text.trimTripleQuotation |> Cursor.assertExtractRange - // load text file - let! (doc, diags) = server |> Server.createUntitledDocument text - use doc = doc // ensure doc gets closed (disposed) after test + let mutable attempts = 5 + while attempts > 0 do + try + let (range, text) = + beforeWithCursor |> Text.trimTripleQuotation |> Cursor.assertExtractRange + // load text file + let! (doc, diags) = server |> Server.createUntitledDocument text + use doc = doc // ensure doc gets closed (disposed) after test + + do! + checkFixAt + (doc, diags) + doc.VersionedTextDocumentIdentifier + (text, range) + validateDiagnostics + chooseFix + (expected ()) + attempts <- 0 + with + | ex -> + attempts <- attempts - 1 + if attempts = 0 then + ExceptionDispatchInfo.Capture(ex).Throw() + return failwith "Unreachable" + else + _logger.warn (Log.setMessage "Retrying test after failure" >> Log.addContext "attempts" (5 - attempts)) + do! Async.Sleep 15 - do! - checkFixAt - (doc, diags) - doc.VersionedTextDocumentIdentifier - (text, range) - validateDiagnostics - chooseFix - (expected ()) } /// Checks a CodeFix (CodeAction) for validity. From de76398fb555e81ff29549dfa9955ec4709519a1 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Sun, 21 Apr 2024 15:38:16 -0400 Subject: [PATCH 43/60] fix focustest helpers --- test/FsAutoComplete.Tests.Lsp/Helpers.fs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/FsAutoComplete.Tests.Lsp/Helpers.fs b/test/FsAutoComplete.Tests.Lsp/Helpers.fs index 5959762f7..270160d42 100644 --- a/test/FsAutoComplete.Tests.Lsp/Helpers.fs +++ b/test/FsAutoComplete.Tests.Lsp/Helpers.fs @@ -51,11 +51,11 @@ module Expecto = let testCase = testCaseWithTimeout DEFAULT_TIMEOUT let ptestCase = ptestCaseWithTimeout DEFAULT_TIMEOUT - let ftestCase = ptestCaseWithTimeout DEFAULT_TIMEOUT + let ftestCase = ftestCaseWithTimeout DEFAULT_TIMEOUT let testCaseAsync = testCaseAsyncWithTimeout DEFAULT_TIMEOUT let ptestCaseAsync = ptestCaseAsyncWithTimeout DEFAULT_TIMEOUT - let ftestCaseAsync = ptestCaseAsyncWithTimeout DEFAULT_TIMEOUT + let ftestCaseAsync = ftestCaseAsyncWithTimeout DEFAULT_TIMEOUT let rec private copyDirectory (sourceDir: DirectoryInfo) destDir = // Get the subdirectories for the specified directory. From 36fcc7367f4ee4b4110f32bd5986870ad50ceeb9 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Sun, 21 Apr 2024 15:39:22 -0400 Subject: [PATCH 44/60] add telplin --- .config/dotnet-tools.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index f759710e7..dfb834680 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -25,6 +25,12 @@ "commands": [ "fsharp-analyzers" ] + }, + "telplin": { + "version": "0.9.6", + "commands": [ + "telplin" + ] } } } \ No newline at end of file From 77aca9ae427aa4976848d8a5c9a1d1558be3c543 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Sun, 21 Apr 2024 15:39:40 -0400 Subject: [PATCH 45/60] fix otel readme docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9347f6b34..06d3c8c63 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ To export traces, run [Jaeger](https://www.jaegertracing.io/) ```bash docker run -d --name jaeger \ - -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \ + -e COLLECTOR_ZIPKIN_HOST_PORT=9411 \ -e COLLECTOR_OTLP_ENABLED=true \ -p 6831:6831/udp \ -p 6832:6832/udp \ From ae529486d808250f4befb215dde6d5b46c47fcab Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Sun, 21 Apr 2024 18:07:01 -0400 Subject: [PATCH 46/60] Make background/transparent compiler switchable --- src/FsAutoComplete.Core/Commands.fs | 25 +- .../CompilerServiceInterface.fs | 339 +++++++++++++++- .../CompilerServiceInterface.fsi | 43 +- src/FsAutoComplete.Core/FileSystem.fs | 17 +- src/FsAutoComplete.Core/SymbolLocation.fs | 6 +- src/FsAutoComplete/CodeFixes.fs | 6 +- src/FsAutoComplete/CodeFixes.fsi | 2 +- .../LspServers/AdaptiveFSharpLspServer.fs | 16 +- .../LspServers/AdaptiveFSharpLspServer.fsi | 8 +- .../LspServers/AdaptiveServerState.fs | 367 ++++++++++++------ .../LspServers/AdaptiveServerState.fsi | 16 +- .../LspServers/ProjectWorkspace.fs | 13 +- src/FsAutoComplete/Parser.fs | 26 +- .../ExtensionsTests.fs | 1 + .../FindReferencesTests.fs | 1 + test/FsAutoComplete.Tests.Lsp/Helpers.fs | 4 +- test/FsAutoComplete.Tests.Lsp/Helpers.fsi | 5 +- test/FsAutoComplete.Tests.Lsp/Program.fs | 109 +++--- 18 files changed, 751 insertions(+), 253 deletions(-) diff --git a/src/FsAutoComplete.Core/Commands.fs b/src/FsAutoComplete.Core/Commands.fs index 85f3638be..543b4447b 100644 --- a/src/FsAutoComplete.Core/Commands.fs +++ b/src/FsAutoComplete.Core/Commands.fs @@ -731,10 +731,10 @@ module Commands = let symbolUseWorkspaceAux (getDeclarationLocation: FSharpSymbolUse * IFSACSourceText -> Async) - (findReferencesForSymbolInFile: (string * FSharpProjectSnapshot * FSharpSymbol) -> Async) + (findReferencesForSymbolInFile: (string * CompilerProjectOption * FSharpSymbol) -> Async) (tryGetFileSource: string -> Async>) - (tryGetProjectOptionsForFsproj: string -> Async) - (getAllProjectOptions: unit -> Async) + (tryGetProjectOptionsForFsproj: string -> Async) + (getAllProjectOptions: unit -> Async) (includeDeclarations: bool) (includeBackticks: bool) (errorOnFailureToFixRange: bool) @@ -787,7 +787,7 @@ module Commands = return (symbol, ranges) | scope -> - let projectsToCheck: Async = + let projectsToCheck: Async = async { match scope with | Some(SymbolDeclarationLocation.Projects(projects (*isLocalForProject=*) , true)) -> return projects @@ -798,8 +798,8 @@ module Commands = yield Async.singleton (Some project) yield! - project.ReferencedProjects - |> List.map (fun p -> Utils.normalizePath p.OutputFile |> tryGetProjectOptionsForFsproj) ] + project.ReferencedProjectsPath + |> List.map (fun p -> Utils.normalizePath p |> tryGetProjectOptionsForFsproj) ] |> Async.parallel75 @@ -839,7 +839,7 @@ module Commands = /// Adds References of `symbol` in `file` to `dict` /// /// `Error` iff adjusting ranges failed (including cannot get source) and `errorOnFailureToFixRange`. Otherwise always `Ok` - let tryFindReferencesInFile (file: string, project: FSharpProjectSnapshot) = + let tryFindReferencesInFile (file: string, project: CompilerProjectOption) = async { if dict.ContainsKey file then return Ok() @@ -882,15 +882,14 @@ module Commands = if errorOnFailureToFixRange then Error e else Ok()) - let iterProjects (projects: FSharpProjectSnapshot seq) = + let iterProjects (projects: CompilerProjectOption seq) = // should: // * check files in parallel // * stop when error occurs // -> `Async.Choice`: executes in parallel, returns first `Some` // -> map `Error` to `Some` for `Async.Choice`, afterwards map `Some` back to `Error` [ for project in projects do - for file in project.SourceFiles do - let file = Utils.normalizePath file.FileName + for file in project.SourceFilesTagged do async { match! tryFindReferencesInFile (file, project) with @@ -930,10 +929,10 @@ module Commands = /// -> for "Rename" let symbolUseWorkspace (getDeclarationLocation: FSharpSymbolUse * IFSACSourceText -> Async) - (findReferencesForSymbolInFile: (string * FSharpProjectSnapshot * FSharpSymbol) -> Async) + (findReferencesForSymbolInFile: (string * CompilerProjectOption * FSharpSymbol) -> Async) (tryGetFileSource: string -> Async>) - (tryGetProjectOptionsForFsproj: string -> Async) - (getAllProjectOptions: unit -> Async) + (tryGetProjectOptionsForFsproj: string -> Async) + (getAllProjectOptions: unit -> Async) (includeDeclarations: bool) (includeBackticks: bool) (errorOnFailureToFixRange: bool) diff --git a/src/FsAutoComplete.Core/CompilerServiceInterface.fs b/src/FsAutoComplete.Core/CompilerServiceInterface.fs index 873265c37..327f4c4af 100644 --- a/src/FsAutoComplete.Core/CompilerServiceInterface.fs +++ b/src/FsAutoComplete.Core/CompilerServiceInterface.fs @@ -17,7 +17,59 @@ open FSharp.Compiler.CodeAnalysis.ProjectSnapshot type Version = int -type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelReferenceResolution) = + + +[] +module Helpers3 = + open FSharp.Compiler.CodeAnalysis.ProjectSnapshot + + type FSharpReferencedProjectSnapshot with + + member x.ProjectFilePath = + match x with + | FSharpReferencedProjectSnapshot.FSharpReference(snapshot = snapshot) -> snapshot.ProjectFileName |> Some + | _ -> None + + + type FSharpReferencedProject with + + member x.ProjectFilePath = + match x with + | FSharpReferencedProject.FSharpReference(options = options) -> options.ProjectFileName |> Some + | _ -> None + + +[] +type CompilerProjectOption = + | BackgroundCompiler of FSharpProjectOptions + | TransparentCompiler of FSharpProjectSnapshot + + member x.ReferencedProjectsPath = + match x with + | BackgroundCompiler(options) -> + options.ReferencedProjects + |> Array.choose (fun p -> p.ProjectFilePath) + |> Array.toList + | TransparentCompiler(snapshot) -> snapshot.ReferencedProjects |> List.choose (fun p -> p.ProjectFilePath) + + member x.ProjectFileName = + match x with + | BackgroundCompiler(options) -> options.ProjectFileName + | TransparentCompiler(snapshot) -> snapshot.ProjectFileName + + member x.SourceFilesTagged = + match x with + | BackgroundCompiler(options) -> options.SourceFiles |> Array.toList + | TransparentCompiler(snapshot) -> snapshot.SourceFiles |> List.map (fun f -> f.FileName) + |> List.map Utils.normalizePath + + member x.OtherOptions = + match x with + | BackgroundCompiler(options) -> options.OtherOptions |> Array.toList + | TransparentCompiler(snapshot) -> snapshot.OtherOptions + +type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelReferenceResolution, useTransparentCompiler) + = let checker = FSharpChecker.Create( projectCacheSize = 200, @@ -30,7 +82,7 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe parallelReferenceResolution = parallelReferenceResolution, captureIdentifiersWhenParsing = true, useSyntaxTreeCache = true, - useTransparentCompiler = true + useTransparentCompiler = useTransparentCompiler ) let entityCache = EntityCache() @@ -52,7 +104,7 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe /// additional arguments that are added to typechecking of scripts let mutable fsiAdditionalArguments = Array.empty - let mutable fsiAdditionalFiles = List.empty + let mutable fsiAdditionalFiles: FSharpFileSnapshot list = List.empty /// This event is raised when any data that impacts script typechecking /// is changed. This can potentially invalidate existing project options @@ -61,7 +113,7 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe let mutable disableInMemoryProjectReferences = false - let fixupFsharpCoreAndFSIPaths (snapshot: FSharpProjectSnapshot) = + let fixupFsharpCoreAndFSIPathsForSnapshot (snapshot: FSharpProjectSnapshot) = match sdkFsharpCore, sdkFsiAuxLib with | None, _ | _, None -> snapshot @@ -87,6 +139,21 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe snapshot.Stamp ) + let fixupFsharpCoreAndFSIPathsForOptions (p: FSharpProjectOptions) = + match sdkFsharpCore, sdkFsiAuxLib with + | None, _ + | _, None -> p + | Some fsc, Some fsi -> + let _toReplace, otherOpts = + p.OtherOptions + |> Array.partition (fun opt -> + opt.EndsWith("FSharp.Core.dll", StringComparison.Ordinal) + || opt.EndsWith("FSharp.Compiler.Interactive.Settings.dll", StringComparison.Ordinal)) + + { p with + OtherOptions = Array.append otherOpts [| $"-r:%s{fsc.FullName}"; $"-r:%s{fsi.FullName}" |] } + + let (|StartsWith|_|) (prefix: string) (s: string) = if s.StartsWith(prefix, StringComparison.Ordinal) then Some(s.[prefix.Length ..]) @@ -101,9 +168,27 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe | StartsWith "--load:" file -> args, Array.append files [| file |] | arg -> Array.append args [| arg |], files) + let (|Reference|_|) (opt: string) = + if opt.StartsWith("-r:", StringComparison.Ordinal) then + Some(opt.[3..]) + else + None + + + /// ensures that all file paths are absolute before being sent to the compiler, because compilation of scripts fails with relative paths + let resolveRelativeFilePaths (projectOptions: FSharpProjectOptions) = + { projectOptions with + SourceFiles = projectOptions.SourceFiles |> Array.map Path.GetFullPath + OtherOptions = + projectOptions.OtherOptions + |> Array.map (fun opt -> + match opt with + | Reference r -> $"-r:{Path.GetFullPath r}" + | opt -> opt) } + /// ensures that any user-configured include/load files are added to the typechecking context - let addLoadedFiles (snapshot: FSharpProjectSnapshot) = + let addLoadedFilesToSnapshot (snapshot: FSharpProjectSnapshot) = let files = List.append fsiAdditionalFiles snapshot.SourceFiles optsLogger.info ( @@ -119,11 +204,28 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe else None + + /// ensures that any user-configured include/load files are added to the typechecking context + let addLoadedFilesToProject (projectOptions: FSharpProjectOptions) = + let additionalSourceFiles = + (Array.ofList fsiAdditionalFiles) |> Array.map (fun s -> s.FileName) + + let files = Array.append additionalSourceFiles projectOptions.SourceFiles + + optsLogger.info ( + Log.setMessage "Source file list is {files}" + >> Log.addContextDestructured "files" files + ) + + { projectOptions with + SourceFiles = files } + + member __.DisableInMemoryProjectReferences with get () = disableInMemoryProjectReferences and set (value) = disableInMemoryProjectReferences <- value - static member GetDependingProjects (file: string) (snapshots: seq) = + static member GetDependingProjects (file: string) (snapshots: seq) = let project = snapshots |> Seq.tryFind (fun (k, _) -> (UMX.untag k).ToUpperInvariant() = (UMX.untag file).ToUpperInvariant()) @@ -136,11 +238,11 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe |> Seq.map snd |> Seq.distinctBy (fun o -> o.ProjectFileName) |> Seq.filter (fun o -> - o.ReferencedProjects - |> List.map (fun p -> Path.GetFullPath p.OutputFile) + o.ReferencedProjectsPath + |> List.map (fun p -> Path.GetFullPath p) |> List.contains option.ProjectFileName) ]) - member private __.GetNetFxScriptOptions(file: string, source) = + member private __.GetNetFxScriptSnapshot(file: string, source) = async { optsLogger.info ( Log.setMessage "Getting NetFX options for script file {file}" @@ -159,12 +261,12 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe userOpName = "getNetFrameworkScriptOptions" ) - let allModifications = addLoadedFiles + let allModifications = addLoadedFilesToSnapshot return allModifications opts, errors } - member private __.GetNetCoreScriptOptions(file: string, source) = + member private __.GetNetCoreScriptSnapshot(file: string, source) = async { optsLogger.info ( Log.setMessage "Getting NetCore options for script file {file}" @@ -194,7 +296,7 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe let allModifications = // filterBadRuntimeRefs >> - addLoadedFiles >> fixupFsharpCoreAndFSIPaths + addLoadedFilesToSnapshot >> fixupFsharpCoreAndFSIPathsForSnapshot let modified = allModifications snapshot @@ -206,12 +308,88 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe return modified, errors } - member self.GetProjectOptionsFromScript(file: string, source, tfm: FSIRefs.TFM) = + member self.GetProjectSnapshotsFromScript(file: string, source, tfm: FSIRefs.TFM) = + match tfm with + | FSIRefs.TFM.NetFx -> self.GetNetFxScriptSnapshot(file, source) + | FSIRefs.TFM.NetCore -> self.GetNetCoreScriptSnapshot(file, source) + + + + member private __.GetNetFxScriptOptions(file: string, source) = + async { + optsLogger.info ( + Log.setMessage "Getting NetFX options for script file {file}" + >> Log.addContextDestructured "file" file + ) + + let allFlags = Array.append [| "--targetprofile:mscorlib" |] fsiAdditionalArguments + + let! (opts, errors) = + checker.GetProjectOptionsFromScript( + UMX.untag file, + source, + assumeDotNetFramework = true, + useFsiAuxLib = true, + otherFlags = allFlags, + userOpName = "getNetFrameworkScriptOptions" + ) + + let allModifications = addLoadedFilesToProject >> resolveRelativeFilePaths + + return allModifications opts, errors + } + + member private __.GetNetCoreScriptOptions(file: string, source) = + async { + optsLogger.info ( + Log.setMessage "Getting NetCore options for script file {file}" + >> Log.addContextDestructured "file" file + ) + + let allFlags = + Array.append [| "--targetprofile:netstandard" |] fsiAdditionalArguments + + let! (opts, errors) = + checker.GetProjectOptionsFromScript( + UMX.untag file, + source, + assumeDotNetFramework = false, + useSdkRefs = true, + useFsiAuxLib = true, + otherFlags = allFlags, + userOpName = "getNetCoreScriptOptions" + ) + + optsLogger.trace ( + Log.setMessage "Got NetCore options {opts} for file {file} with errors {errors}" + >> Log.addContextDestructured "file" file + >> Log.addContextDestructured "opts" opts + >> Log.addContextDestructured "errors" errors + ) + + let allModifications = + // filterBadRuntimeRefs >> + addLoadedFilesToProject + >> resolveRelativeFilePaths + >> fixupFsharpCoreAndFSIPathsForOptions + + let modified = allModifications opts + + optsLogger.trace ( + Log.setMessage "Replaced options to {opts}" + >> Log.addContextDestructured "opts" modified + ) + + return modified, errors + } + + member self.GetProjectOptionsFromScript(file: string, source, tfm) = match tfm with | FSIRefs.TFM.NetFx -> self.GetNetFxScriptOptions(file, source) | FSIRefs.TFM.NetCore -> self.GetNetCoreScriptOptions(file, source) + member __.ScriptTypecheckRequirementsChanged = scriptTypecheckRequirementsChanged.Publish @@ -241,6 +419,20 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe return! checker.ParseFile(path, snapshot) } + + member x.ParseFile(filePath: string, sourceText: ISourceText, project: FSharpProjectOptions) = + async { + checkerLogger.info ( + Log.setMessage "ParseFile - {file}" + >> Log.addContextDestructured "file" filePath + ) + + let parseOpts = Utils.projectOptionsToParseOptions project + + let path = UMX.untag filePath + return! checker.ParseFile(path, sourceText, parseOpts) + } + /// Parse and check a source code file, returning a handle to the results /// The name of the file in the project whose source is being checked. /// The snapshot for the project or script. @@ -302,6 +494,64 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe return! ResultOrString.Error(ex.ToString()) } + member __.ParseAndCheckFileInProject + ( + filePath: string, + version, + source: ISourceText, + options, + ?shouldCache: bool + ) = + asyncResult { + let shouldCache = defaultArg shouldCache false + let opName = sprintf "ParseAndCheckFileInProject - %A" filePath + + checkerLogger.info (Log.setMessage "{opName}" >> Log.addContextDestructured "opName" opName) + + // let options = clearProjectReferences options + let path = UMX.untag filePath + + try + let! (p, c) = checker.ParseAndCheckFileInProject(path, version, source, options, userOpName = opName) + + let parseErrors = p.Diagnostics |> Array.map (fun p -> p.Message) + + match c with + | FSharpCheckFileAnswer.Aborted -> + checkerLogger.info ( + Log.setMessage "{opName} completed with errors: {errors}" + >> Log.addContextDestructured "opName" opName + >> Log.addContextDestructured "errors" (List.ofArray p.Diagnostics) + ) + + return! ResultOrString.Error(sprintf "Check aborted (%A). Errors: %A" c parseErrors) + | FSharpCheckFileAnswer.Succeeded(c) -> + checkerLogger.info ( + Log.setMessage "{opName} completed successfully" + >> Log.addContextDestructured "opName" opName + ) + + let r = ParseAndCheckResults(p, c, entityCache) + + if shouldCache then + let ops = + MemoryCacheEntryOptions() + .SetSize(1) + .SetSlidingExpiration(TimeSpan.FromMinutes(5.)) + + return lastCheckResults.Set(filePath, r, ops) + else + return r + with ex -> + checkerLogger.error ( + Log.setMessage "{opName} completed with exception: {ex}" + >> Log.addContextDestructured "opName" opName + >> Log.addExn ex + ) + + return! ResultOrString.Error(ex.ToString()) + } + /// /// This is use primary for Autocompletions. The problem with trying to use TryGetRecentCheckResultsForFile is that it will return None /// if there isn't a GetHashCode that matches the SourceText passed in. This a problem particularly for Autocompletions because we'd have to wait for a typecheck @@ -332,11 +582,47 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe ParseAndCheckResults(pr, cr, entityCache)) + member __.TryGetRecentCheckResultsForFile(file: string, options, source: ISourceText) = + let opName = sprintf "TryGetRecentCheckResultsForFile - %A" file + + checkerLogger.info ( + Log.setMessage "{opName} - {hash}" + >> Log.addContextDestructured "opName" opName + >> Log.addContextDestructured "hash" (source.GetHashCode() |> int) + + ) + + // let options = clearProjectReferences options + + let result = + checker.TryGetRecentCheckResultsForFile(UMX.untag file, options, sourceText = source, userOpName = opName) + |> Option.map (fun (pr, cr, version) -> + checkerLogger.info ( + Log.setMessage "{opName} - got results - {version}" + >> Log.addContextDestructured "opName" opName + >> Log.addContextDestructured "version" version + ) + + ParseAndCheckResults(pr, cr, entityCache)) + + checkerLogger.info ( + Log.setMessage "{opName} - {hash} - cacheHit {cacheHit}" + >> Log.addContextDestructured "opName" opName + >> Log.addContextDestructured "hash" (source.GetHashCode() |> int) + >> Log.addContextDestructured "cacheHit" result.IsSome + ) - member _.GetUsesOfSymbol + result + + member _.ParseAndCheckProject(opts: CompilerProjectOption) = + match opts with + | CompilerProjectOption.BackgroundCompiler opts -> checker.ParseAndCheckProject(opts) + | CompilerProjectOption.TransparentCompiler snapshot -> checker.ParseAndCheckProject(snapshot) + + member x.GetUsesOfSymbol ( file: string, - snapshots: (string * FSharpProjectSnapshot) seq, + snapshots: (string * CompilerProjectOption) seq, symbol: FSharpSymbol ) = async { @@ -348,14 +634,15 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe match FSharpCompilerServiceChecker.GetDependingProjects file snapshots with | None -> return [||] | Some(opts, []) -> - let! res = checker.ParseAndCheckProject(opts) + + let! res = x.ParseAndCheckProject(opts) return res.GetUsesOfSymbol symbol | Some(opts, dependentProjects) -> let! res = opts :: dependentProjects |> List.map (fun (opts) -> async { - let! res = checker.ParseAndCheckProject(opts) + let! res = x.ParseAndCheckProject(opts) return res.GetUsesOfSymbol symbol }) |> Async.parallel75 @@ -395,6 +682,24 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe return [||] } + member _.FindReferencesForSymbolInFile(file: string, project, symbol) = + async { + checkerLogger.info ( + Log.setMessage "FindReferencesForSymbolInFile - {file}" + >> Log.addContextDestructured "file" file + ) + + return! + checker.FindBackgroundReferencesInFile( + UMX.untag file, + project, + symbol, + canInvalidateProject = false, + // fastCheck = true, + userOpName = "find references" + ) + } + member __.SetDotnetRoot(dotnetBinary: FileInfo, cwd: DirectoryInfo) = match Ionide.ProjInfo.SdkDiscovery.versionAt cwd dotnetBinary with diff --git a/src/FsAutoComplete.Core/CompilerServiceInterface.fsi b/src/FsAutoComplete.Core/CompilerServiceInterface.fsi index 8523f2d89..320d64d4e 100644 --- a/src/FsAutoComplete.Core/CompilerServiceInterface.fsi +++ b/src/FsAutoComplete.Core/CompilerServiceInterface.fsi @@ -14,21 +14,36 @@ open FSharp.Compiler.Diagnostics type Version = int + +type CompilerProjectOption = + | BackgroundCompiler of FSharpProjectOptions + | TransparentCompiler of FSharpProjectSnapshot + + member ReferencedProjectsPath: string list + member ProjectFileName: string + member SourceFilesTagged: string list + member OtherOptions: string list + type FSharpCompilerServiceChecker = new: - hasAnalyzers: bool * typecheckCacheSize: int64 * parallelReferenceResolution: bool -> FSharpCompilerServiceChecker + hasAnalyzers: bool * typecheckCacheSize: int64 * parallelReferenceResolution: bool * useTransparentCompiler: bool -> + FSharpCompilerServiceChecker member DisableInMemoryProjectReferences: bool with get, set static member GetDependingProjects: file: string -> - snapshots: seq -> - option> + snapshots: seq -> + option> - member GetProjectOptionsFromScript: + member GetProjectSnapshotsFromScript: file: string * source: ISourceTextNew * tfm: FSIRefs.TFM -> Async + member GetProjectOptionsFromScript: + file: string * source: ISourceText * tfm: FSIRefs.TFM -> + Async> + member ScriptTypecheckRequirementsChanged: IEvent member RemoveFileFromCache: file: string -> unit @@ -45,6 +60,10 @@ type FSharpCompilerServiceChecker = /// member ParseFile: filePath: string * snapshot: FSharpProjectSnapshot -> Async + member ParseFile: + filePath: string * sourceText: ISourceText * project: FSharpProjectOptions -> + Async + /// Parse and check a source code file, returning a handle to the results /// The name of the file in the project whose source is being checked. /// The options for the project or script. @@ -55,6 +74,14 @@ type FSharpCompilerServiceChecker = filePath: string * snapshot: FSharpProjectSnapshot * ?shouldCache: bool -> Async> + member ParseAndCheckFileInProject: + filePath: string * + version: int * + source: ISourceText * + options: FSharpProjectOptions * + ?shouldCache: bool -> + Async> + /// /// This is use primary for Autocompletions. The problem with trying to use TryGetRecentCheckResultsForFile is that it will return None /// if there isn't a GetHashCode that matches the SourceText passed in. This a problem particularly for Autocompletions because we'd have to wait for a typecheck @@ -67,13 +94,19 @@ type FSharpCompilerServiceChecker = member TryGetRecentCheckResultsForFile: file: string * snapshot: FSharpProjectSnapshot -> ParseAndCheckResults option + member TryGetRecentCheckResultsForFile: + file: string * options: FSharpProjectOptions * source: ISourceText -> option + member GetUsesOfSymbol: - file: string * snapshots: (string * FSharpProjectSnapshot) seq * symbol: FSharpSymbol -> + file: string * snapshots: (string * CompilerProjectOption) seq * symbol: FSharpSymbol -> Async member FindReferencesForSymbolInFile: file: string * project: FSharpProjectSnapshot * symbol: FSharpSymbol -> Async> + member FindReferencesForSymbolInFile: + file: string * project: FSharpProjectOptions * symbol: FSharpSymbol -> Async> + // member GetDeclarations: // fileName: string * source: ISourceText * snapshot: FSharpProjectOptions * version: 'a -> // Async diff --git a/src/FsAutoComplete.Core/FileSystem.fs b/src/FsAutoComplete.Core/FileSystem.fs index 4410dc14e..e02d61cf8 100644 --- a/src/FsAutoComplete.Core/FileSystem.fs +++ b/src/FsAutoComplete.Core/FileSystem.fs @@ -20,13 +20,22 @@ module File = File.GetLastWriteTimeUtc path else DateTime.UtcNow + /// Buffer size for reading from the stream. /// 81,920 bytes (80KB) is below the Large Object Heap threshold (85,000 bytes) /// and is a good size for performance. Dotnet uses this for their defaults. - let [] bufferSize = 81920 + [] + let bufferSize = 81920 let openFileStreamForReadingAsync (path: string) = - new FileStream((UMX.untag path), FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize = bufferSize, useAsync = true) + new FileStream( + (UMX.untag path), + FileMode.Open, + FileAccess.Read, + FileShare.Read, + bufferSize = bufferSize, + useAsync = true + ) [] module PositionExtensions = @@ -399,7 +408,7 @@ type ISourceTextFactory = module SourceTextFactory = - let readFile (fileName: string) (sourceTextFactory : ISourceTextFactory) = + let readFile (fileName: string) (sourceTextFactory: ISourceTextFactory) = cancellableValueTask { let file = UMX.untag fileName @@ -409,7 +418,7 @@ module SourceTextFactory = // Roslyn SourceText doesn't actually support async streaming reads but avoids the large object heap hit // so we have to block a thread. use s = File.openFileStreamForReadingAsync fileName - let! source = sourceTextFactory.Create (fileName, s) + let! source = sourceTextFactory.Create(fileName, s) return source else // otherwise it'll be under the LOH threshold and the current thread isn't blocked diff --git a/src/FsAutoComplete.Core/SymbolLocation.fs b/src/FsAutoComplete.Core/SymbolLocation.fs index 8515bc085..80bf2e040 100644 --- a/src/FsAutoComplete.Core/SymbolLocation.fs +++ b/src/FsAutoComplete.Core/SymbolLocation.fs @@ -9,15 +9,15 @@ open FsToolkit.ErrorHandling [] type SymbolDeclarationLocation = | CurrentDocument - | Projects of FSharpProjectSnapshot list * isLocalForProject: bool + | Projects of CompilerProjectOption list * isLocalForProject: bool let getDeclarationLocation ( symbolUse: FSharpSymbolUse, currentDocument: IFSACSourceText, getProjectOptions, - projectsThatContainFile: string -> Async, - getDependentProjectsOfProjects: FSharpProjectSnapshot list -> Async + projectsThatContainFile: string -> Async, + getDependentProjectsOfProjects: CompilerProjectOption list -> Async ) : Async> = asyncOption { diff --git a/src/FsAutoComplete/CodeFixes.fs b/src/FsAutoComplete/CodeFixes.fs index 04fa17760..afad75852 100644 --- a/src/FsAutoComplete/CodeFixes.fs +++ b/src/FsAutoComplete/CodeFixes.fs @@ -34,7 +34,7 @@ module Types = type GetLanguageVersion = string -> Async - type GetProjectOptionsForFile = string -> Async> + type GetProjectOptionsForFile = string -> Async> [] type FixKind = @@ -356,8 +356,8 @@ module Run = let signatureFile = System.String.Concat(fileName, "i") let hasSig = - projectOptions.SourceFiles - |> List.map (fun x -> x.FileName) + projectOptions.SourceFilesTagged + |> List.map (UMX.untag) |> List.contains signatureFile if not hasSig then diff --git a/src/FsAutoComplete/CodeFixes.fsi b/src/FsAutoComplete/CodeFixes.fsi index a3389d7bf..bf57c0030 100644 --- a/src/FsAutoComplete/CodeFixes.fsi +++ b/src/FsAutoComplete/CodeFixes.fsi @@ -29,7 +29,7 @@ module Types = type GetLanguageVersion = string -> Async - type GetProjectOptionsForFile = string -> Async> + type GetProjectOptionsForFile = string -> Async> [] type FixKind = diff --git a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs index 134ff2cad..207c64fd9 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs @@ -44,7 +44,12 @@ open Helpers open System.Runtime.ExceptionServices type AdaptiveFSharpLspServer - (workspaceLoader: IWorkspaceLoader, lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFactory) = + ( + workspaceLoader: IWorkspaceLoader, + lspClient: FSharpLspClient, + sourceTextFactory: ISourceTextFactory, + useTransparentCompiler: bool + ) = let mutable lastFSharpDocumentationTypeCheck: ParseAndCheckResults option = None @@ -58,7 +63,8 @@ type AdaptiveFSharpLspServer let disposables = new Disposables.CompositeDisposable() - let state = new AdaptiveState(lspClient, sourceTextFactory, workspaceLoader) + let state = + new AdaptiveState(lspClient, sourceTextFactory, workspaceLoader, useTransparentCompiler) do disposables.Add(state) @@ -1198,7 +1204,7 @@ type AdaptiveFSharpLspServer let getAllProjects () = state.GetFilesToProject() |> Async.map ( - Array.map (fun (file, proj) -> UMX.untag file, AVal.force proj.FSharpProjectSnapshot) + Array.map (fun (file, proj) -> UMX.untag file, AVal.force proj.FSharpProjectCompilerOptions) >> Array.toList ) @@ -3068,7 +3074,7 @@ module AdaptiveFSharpLspServer = - let startCore toolsPath workspaceLoaderFactory sourceTextFactory = + let startCore toolsPath workspaceLoaderFactory sourceTextFactory useTransparentCompiler = use input = Console.OpenStandardInput() use output = Console.OpenStandardOutput() @@ -3104,7 +3110,7 @@ module AdaptiveFSharpLspServer = let adaptiveServer lspClient = let loader = workspaceLoaderFactory toolsPath - new AdaptiveFSharpLspServer(loader, lspClient, sourceTextFactory) :> IFSharpLspServer + new AdaptiveFSharpLspServer(loader, lspClient, sourceTextFactory, useTransparentCompiler) :> IFSharpLspServer Ionide.LanguageServerProtocol.Server.start requestsHandlings input output FSharpLspClient adaptiveServer createRpc diff --git a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fsi b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fsi index 9e1c43034..c7c396500 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fsi +++ b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fsi @@ -7,12 +7,15 @@ open FSharp.Compiler.CodeAnalysis type AdaptiveFSharpLspServer = new: - workspaceLoader: IWorkspaceLoader * lspClient: FSharpLspClient * sourceTextFactory: ISourceTextFactory -> + workspaceLoader: IWorkspaceLoader * + lspClient: FSharpLspClient * + sourceTextFactory: ISourceTextFactory * + useTransparentCompiler: bool -> AdaptiveFSharpLspServer interface IFSharpLspServer - member ScriptFileProjectOptions: IEvent + member ScriptFileProjectOptions: IEvent module AdaptiveFSharpLspServer = open System.Threading.Tasks @@ -24,6 +27,7 @@ module AdaptiveFSharpLspServer = toolsPath: 'a -> workspaceLoaderFactory: ('a -> #IWorkspaceLoader) -> sourceTextFactory: ISourceTextFactory -> + useTransparentCompiler: bool -> LspCloseReason val start: startCore: (unit -> LspCloseReason) -> int diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index f5a0e0ead..730488eb2 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -51,21 +51,10 @@ type AdaptiveWorkspaceChosen = -[] -module Helpers3 = - open FSharp.Compiler.CodeAnalysis.ProjectSnapshot - - type FSharpReferencedProjectSnapshot with - - member x.ProjectFilePath = - match x with - | FSharpReferencedProjectSnapshot.FSharpReference(snapshot = snapshot) -> snapshot.ProjectFileName |> Some - | _ -> None - [] type LoadedProject = { ProjectOptions: Types.ProjectOptions - FSharpProjectSnapshot: aval + FSharpProjectCompilerOptions: aval LanguageVersion: LanguageVersionShim } interface IEquatable with @@ -102,8 +91,13 @@ type FindFirstProject() = $"Couldn't find a corresponding project for {sourceFile}. \n Projects include {allProjects}. \nHave the projects loaded yet or have you tried restoring your project/solution?") -type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFactory, workspaceLoader: IWorkspaceLoader) - = +type AdaptiveState + ( + lspClient: FSharpLspClient, + sourceTextFactory: ISourceTextFactory, + workspaceLoader: IWorkspaceLoader, + useTransparentCompiler: bool + ) = let logger = LogProvider.getLoggerFor () let thisType = typeof let disposables = new Disposables.CompositeDisposable() @@ -114,10 +108,12 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let rootPath = cval None let config = cval FSharpConfig.Default + // let useTransparentCompiler = cval true let checker = config - |> AVal.map (fun c -> c.EnableAnalyzers, c.Fsac.CachedTypeCheckCount, c.Fsac.ParallelReferenceResolution) + |> AVal.map (fun c -> + c.EnableAnalyzers, c.Fsac.CachedTypeCheckCount, c.Fsac.ParallelReferenceResolution, useTransparentCompiler) |> AVal.map (FSharpCompilerServiceChecker) let configChanges = @@ -278,14 +274,14 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let notifications = Event() - let scriptFileProjectOptions = Event() + let scriptFileProjectOptions = Event() let fileParsed = - Event() + Event() let fileChecked = Event() - let detectTests (parseResults: FSharpParseFileResults) (proj: FSharpProjectSnapshot) ct = + let detectTests (parseResults: FSharpParseFileResults) (proj: CompilerProjectOption) ct = try logger.info (Log.setMessageI $"Test Detection of {parseResults.FileName:file} started") @@ -1031,23 +1027,44 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac } - let snapshots = - asyncAVal { - let! projects = projectOptions - return Snapshots.createSnapshots openFilesWithChanges (AVal.constant sourceTextFactory) (AMap.ofHashMap projects) - } + let createSnapshots projectOptions = + Snapshots.createSnapshots openFilesWithChanges (AVal.constant sourceTextFactory) (AMap.ofHashMap projectOptions) + |> AMap.map (fun _ (proj, snap) -> + { ProjectOptions = proj + FSharpProjectCompilerOptions = snap |> AVal.map CompilerProjectOption.TransparentCompiler + LanguageVersion = LanguageVersionShim.fromOtherOptions proj.OtherOptions }) + + let createOptions projectOptions = + let projectOptions = HashMap.toValueList projectOptions + let fsharpOptions = projectOptions |> FCS.mapManyOptions |> Seq.toList + + List.zip projectOptions fsharpOptions + |> List.map (fun (projectOption, fso) -> + + let langversion = LanguageVersionShim.fromFSharpProjectOptions fso + + // Set some default values as FCS uses these for identification/caching purposes + let fso = + { fso with + SourceFiles = fso.SourceFiles |> Array.map (Utils.normalizePath >> UMX.untag) + Stamp = fso.Stamp |> Option.orElse (Some DateTime.UtcNow.Ticks) + ProjectId = fso.ProjectId |> Option.orElse (Some(Guid.NewGuid().ToString())) } + |> CompilerProjectOption.BackgroundCompiler + + Utils.normalizePath projectOption.ProjectFileName, + { FSharpProjectCompilerOptions = AVal.constant fso + LanguageVersion = langversion + ProjectOptions = projectOption }) + |> AMap.ofList let loadedProjects = asyncAVal { - let! snapshots = snapshots - - return - snapshots - |> AMap.map (fun _ (proj, snap) -> - { ProjectOptions = proj - FSharpProjectSnapshot = snap - LanguageVersion = LanguageVersionShim.fromOtherOptions proj.OtherOptions }) + let! projectOptions = projectOptions + if useTransparentCompiler then + return createSnapshots projectOptions + else + return createOptions projectOptions } let getAllLoadedProjects = @@ -1056,7 +1073,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac return! loadedProjects - |> AMap.mapA (fun _ v -> v.FSharpProjectSnapshot |> AVal.map (fun _ -> v)) + |> AMap.mapA (fun _ v -> v.FSharpProjectCompilerOptions |> AVal.map (fun _ -> v)) |> AMap.toAVal |> AVal.map HashMap.toValueList @@ -1190,15 +1207,27 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac /// Parses a source code for a file and caches the results. Returns an AST that can be traversed for various features. /// The FSharpCompilerServiceChecker. - /// The source to be parsed. - /// + /// The source to be parsed. + /// /// - let parseFile (checker: FSharpCompilerServiceChecker) (source) snap = + + let parseFile (checker: FSharpCompilerServiceChecker) (sourceFilePath) (compilerOptions: CompilerProjectOption) = task { - let! result = checker.ParseFile(source, snap) + let! result = + match compilerOptions with + | CompilerProjectOption.TransparentCompiler snap -> + taskResult { return! checker.ParseFile(sourceFilePath, snap) } + | CompilerProjectOption.BackgroundCompiler opts -> + taskResult { + let! file = forceFindOpenFileOrRead sourceFilePath + return! checker.ParseFile(sourceFilePath, file.Source, opts) + } let! ct = Async.CancellationToken - fileParsed.Trigger(result, snap, ct) + + result + |> Result.iter (fun result -> fileParsed.Trigger(result, compilerOptions, ct)) + return result } @@ -1212,7 +1241,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let! projects = projects - |> List.map (fun p -> p.FSharpProjectSnapshot) + |> List.map (fun p -> p.FSharpProjectCompilerOptions) |> ASet.ofList |> ASet.mapA id |> ASet.toAVal @@ -1220,9 +1249,8 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac return! projects |> HashSet.toArray - |> Array.collect (fun (snap) -> snap.SourceFiles |> List.toArray |> Array.map (fun s -> snap, s)) - |> Array.map (fun (snap, fileName) -> - let filePath = Utils.normalizePath fileName.FileName + |> Array.collect (fun (snap) -> snap.SourceFilesTagged |> List.toArray |> Array.map (fun s -> snap, s)) + |> Array.map (fun (snap, filePath) -> parseFile checker filePath snap) |> Task.WhenAll @@ -1245,61 +1273,121 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac use linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ctok, cts) try - let! (opts, errors) = - checker.GetProjectOptionsFromScript(filePath, file.Source, tfmConfig) - |> Async.withCancellation linkedCts.Token - - opts |> scriptFileProjectOptions.Trigger - let diags = errors |> Array.ofList |> Array.map fcsErrorToDiagnostic - - diagnosticCollections.SetFor( - Path.LocalPathToUri filePath, - "F# Script Project Options", - file.Version, - diags - ) + if useTransparentCompiler then + let! (opts, errors) = + checker.GetProjectSnapshotsFromScript(filePath, file.Source, tfmConfig) + |> Async.withCancellation linkedCts.Token + + let tOpts = opts |> CompilerProjectOption.TransparentCompiler + tOpts |> scriptFileProjectOptions.Trigger + let diags = errors |> Array.ofList |> Array.map fcsErrorToDiagnostic + + diagnosticCollections.SetFor( + Path.LocalPathToUri filePath, + "F# Script Project Options", + file.Version, + diags + ) + + let projectOptions: Types.ProjectOptions = + let projectSdkInfo: Types.ProjectSdkInfo = + { IsTestProject = false + Configuration = "" + IsPackable = false + TargetFramework = "" + TargetFrameworkIdentifier = "" + TargetFrameworkVersion = "" + MSBuildAllProjects = [] + MSBuildToolsVersion = "" + ProjectAssetsFile = "" + RestoreSuccess = true + Configurations = [] + TargetFrameworks = [] + RunArguments = None + RunCommand = None + IsPublishable = None } + + { ProjectId = opts.ProjectId + ProjectFileName = opts.ProjectFileName + TargetFramework = "" + SourceFiles = opts.SourceFiles |> List.map (fun x -> x.FileName) + OtherOptions = opts.OtherOptions + ReferencedProjects = [] + PackageReferences = [] + LoadTime = opts.LoadTime + TargetPath = "" + TargetRefPath = None + ProjectOutputType = Types.ProjectOutputType.Exe + ProjectSdkInfo = projectSdkInfo + Items = [] + Properties = [] + CustomProperties = [] } + + + + return + { FSharpProjectCompilerOptions = tOpts |> AVal.constant + LanguageVersion = LanguageVersionShim.fromFSharpProjectSnapshot opts + ProjectOptions = projectOptions } + |> List.singleton + + else + let! (opts, errors) = + checker.GetProjectOptionsFromScript(filePath, file.Source, tfmConfig) + |> Async.withCancellation linkedCts.Token + + let tOpts = opts |> CompilerProjectOption.BackgroundCompiler + tOpts |> scriptFileProjectOptions.Trigger + let diags = errors |> Array.ofList |> Array.map fcsErrorToDiagnostic + + diagnosticCollections.SetFor( + Path.LocalPathToUri filePath, + "F# Script Project Options", + file.Version, + diags + ) + - let projectOptions: Types.ProjectOptions = - let projectSdkInfo: Types.ProjectSdkInfo = - { IsTestProject = false - Configuration = "" - IsPackable = false + let projectOptions: Types.ProjectOptions = + let projectSdkInfo: Types.ProjectSdkInfo = + { IsTestProject = false + Configuration = "" + IsPackable = false + TargetFramework = "" + TargetFrameworkIdentifier = "" + TargetFrameworkVersion = "" + MSBuildAllProjects = [] + MSBuildToolsVersion = "" + ProjectAssetsFile = "" + RestoreSuccess = true + Configurations = [] + TargetFrameworks = [] + RunArguments = None + RunCommand = None + IsPublishable = None } + + { ProjectId = opts.ProjectId + ProjectFileName = opts.ProjectFileName TargetFramework = "" - TargetFrameworkIdentifier = "" - TargetFrameworkVersion = "" - MSBuildAllProjects = [] - MSBuildToolsVersion = "" - ProjectAssetsFile = "" - RestoreSuccess = true - Configurations = [] - TargetFrameworks = [] - RunArguments = None - RunCommand = None - IsPublishable = None } - - { ProjectId = opts.ProjectId - ProjectFileName = opts.ProjectFileName - TargetFramework = "" - SourceFiles = opts.SourceFiles |> List.map (fun x -> x.FileName) - OtherOptions = opts.OtherOptions - ReferencedProjects = [] - PackageReferences = [] - LoadTime = opts.LoadTime - TargetPath = "" - TargetRefPath = None - ProjectOutputType = Types.ProjectOutputType.Exe - ProjectSdkInfo = projectSdkInfo - Items = [] - Properties = [] - CustomProperties = [] } - - - - return - { FSharpProjectSnapshot = AVal.constant opts - LanguageVersion = LanguageVersionShim.fromFSharpProjectSnapshot opts - ProjectOptions = projectOptions } - |> List.singleton + SourceFiles = opts.SourceFiles |> Array.toList + OtherOptions = opts.OtherOptions |> Array.toList + ReferencedProjects = [] + PackageReferences = [] + LoadTime = opts.LoadTime + TargetPath = "" + TargetRefPath = None + ProjectOutputType = Types.ProjectOutputType.Exe + ProjectSdkInfo = projectSdkInfo + Items = [] + Properties = [] + CustomProperties = [] } + + return + { FSharpProjectCompilerOptions = tOpts |> AVal.constant + LanguageVersion = LanguageVersionShim.fromFSharpProjectOptions opts + + ProjectOptions = projectOptions } + |> List.singleton with e -> logger.error ( @@ -1364,9 +1452,9 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac match loadedProject with | Ok x -> - let! snap = x.FSharpProjectSnapshot + let! snap = x.FSharpProjectCompilerOptions let! r = parseFile checker filePath snap - return Ok r + return r | Error e -> return Error e }) } @@ -1423,7 +1511,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let getAllFSharpProjectOptions () = getAllProjectOptions () - |> Async.map (Array.map (fun x -> AVal.force x.FSharpProjectSnapshot)) + |> Async.map (Array.map (fun x -> AVal.force x.FSharpProjectCompilerOptions)) let getProjectOptionsForFile (filePath: string) = @@ -1463,7 +1551,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let parseAndCheckFile (checker: FSharpCompilerServiceChecker) (file: VolatileFile) - (options: FSharpProjectSnapshot) + (options: CompilerProjectOption) shouldCache = asyncEx { @@ -1492,9 +1580,20 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let simpleName = Path.GetFileName(UMX.untag file.Source.FileName) do! progressReport.Begin($"Typechecking {simpleName}", message = $"{file.Source.FileName}") + let! result = - checker.ParseAndCheckFileInProject(file.Source.FileName, options, shouldCache = shouldCache) - |> Debug.measureAsync $"checker.ParseAndCheckFileInProject - {file.Source.FileName}" + match options with + | CompilerProjectOption.TransparentCompiler snap -> + checker.ParseAndCheckFileInProject(file.Source.FileName, snap, shouldCache = shouldCache) + | CompilerProjectOption.BackgroundCompiler opts -> + checker.ParseAndCheckFileInProject( + file.Source.FileName, + file.Version, + file.Source, + opts, + shouldCache = shouldCache + ) + notifications.Trigger(NotificationEvent.FileParsed(file.Source.FileName), ct) @@ -1558,27 +1657,35 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let openFilesToRecentCheckedFilesResults = openFilesToChangesAndProjectOptions - |> AMapAsync.mapAsyncAVal (fun _ (info, projectOptions) _ -> + |> AMapAsync.mapAsyncAVal (fun _ (file, projectOptions) _ -> asyncAVal { - let file = info.Source.FileName + let sourceFilePath = file.Source.FileName let! checker = checker and! selectProject = projectSelector let options = result { let! projectOptions = projectOptions - let! opts = selectProject.FindProject(file, projectOptions) + let! opts = selectProject.FindProject(sourceFilePath, projectOptions) return opts } match options with | Ok x -> - let! snap = x.FSharpProjectSnapshot - - return - checker.TryGetRecentCheckResultsForFile(file, snap) - |> Result.ofOption (fun () -> - $"No recent typecheck results for {file}. This may be ok if the file has not been checked yet.") + let! compilerOptions = x.FSharpProjectCompilerOptions + + let checkResults = + match compilerOptions with + | CompilerProjectOption.TransparentCompiler snap -> + checker.TryGetRecentCheckResultsForFile(sourceFilePath, snap) + |> Result.ofOption (fun () -> + $"No recent typecheck results for {sourceFilePath}. This may be ok if the file has not been checked yet.") + | CompilerProjectOption.BackgroundCompiler opts -> + checker.TryGetRecentCheckResultsForFile(sourceFilePath, opts, file.Source) + |> Result.ofOption (fun () -> + $"No recent typecheck results for {sourceFilePath}. This may be ok if the file has not been checked yet.") + + return checkResults | Error e -> return Error e }) @@ -1597,7 +1704,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac match options with | Error e -> return Error e | Ok x -> - let! snap = x.FSharpProjectSnapshot + let! snap = x.FSharpProjectCompilerOptions return! asyncResult { @@ -1657,7 +1764,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let forceGetFSharpProjectOptions filePath = forceGetProjectOptions filePath - |> Async.map (Result.map (fun p -> AVal.force p.FSharpProjectSnapshot)) + |> Async.map (Result.map (fun p -> AVal.force p.FSharpProjectCompilerOptions)) let forceGetOpenFileTypeCheckResultsOrCheck file = @@ -1786,7 +1893,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac member x.ParseFileInProject(file) = forceGetParseResults file |> Async.map (Option.ofResult) } - let getDependentProjectsOfProjects (ps: FSharpProjectSnapshot list) = + let getDependentProjectsOfProjects (ps: CompilerProjectOption list) = async { let! projectSnapshot = forceLoadProjects () @@ -1801,18 +1908,15 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let dependents = projectSnapshot |> Seq.filter (fun p -> - (AVal.force p.FSharpProjectSnapshot).ReferencedProjects - |> Seq.exists (fun r -> - match r.ProjectFilePath with - | None -> false - | Some p -> currentPass.Contains(p))) + (AVal.force p.FSharpProjectCompilerOptions).ReferencedProjectsPath + |> Seq.exists currentPass.Contains) if Seq.isEmpty dependents then continueAlong <- false currentPass.Clear() else for d in dependents do - allDependents.Add(AVal.force d.FSharpProjectSnapshot) |> ignore + allDependents.Add(AVal.force d.FSharpProjectCompilerOptions) |> ignore currentPass.Clear() currentPass.AddRange(dependents |> Seq.map (fun p -> p.ProjectFileName)) @@ -1832,7 +1936,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac projects |> Result.bind (fun p -> selectProject.FindProject(file, p)) |> Result.toOption - |> Option.map (fun project -> AVal.force project.FSharpProjectSnapshot) + |> Option.map (fun project -> AVal.force project.FSharpProjectCompilerOptions) } @@ -1840,7 +1944,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac async { let! projects = getProjectOptionsForFile file |> AsyncAVal.forceAsync let projects = projects |> Result.toOption |> Option.defaultValue [] - return projects |> List.map (fun p -> AVal.force p.FSharpProjectSnapshot) + return projects |> List.map (fun p -> AVal.force p.FSharpProjectCompilerOptions) } SymbolLocation.getDeclarationLocation ( @@ -1861,13 +1965,17 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac tyRes = - let findReferencesForSymbolInFile (file: string, project, symbol) = + let findReferencesForSymbolInFile (file: string, project: CompilerProjectOption, symbol) = async { let checker = checker |> AVal.force if File.Exists(UMX.untag file) then + match project with + | CompilerProjectOption.TransparentCompiler snap -> + return! checker.FindReferencesForSymbolInFile(file, snap, symbol) // `FSharpChecker.FindBackgroundReferencesInFile` only works with existing files - return! checker.FindReferencesForSymbolInFile(file, project, symbol) + | CompilerProjectOption.BackgroundCompiler opts -> + return! checker.FindReferencesForSymbolInFile(file, opts, symbol) else // untitled script files match! forceGetOpenFileTypeCheckResultsStale file with @@ -2147,7 +2255,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac sourceFiles |> Array.splitAt idx |> snd - |> Array.map (fun sourceFile -> AVal.force proj.FSharpProjectSnapshot, sourceFile)) + |> Array.map (fun sourceFile -> AVal.force proj.FSharpProjectCompilerOptions, sourceFile)) |> Array.distinct } @@ -2169,15 +2277,13 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac projs |> Result.toOption |> Option.defaultValue [] - |> List.map (fun x -> AVal.force x.FSharpProjectSnapshot) + |> List.map (fun x -> AVal.force x.FSharpProjectCompilerOptions) let! dependentProjects = projs |> getDependentProjectsOfProjects let dependentProjectsAndSourceFiles = dependentProjects - |> List.collect (fun (snap) -> - snap.SourceFiles - |> List.map (fun sourceFile -> snap, Utils.normalizePath sourceFile.FileName)) + |> List.collect (fun (snap) -> snap.SourceFilesTagged |> List.map (fun sourceFile -> snap, sourceFile)) |> List.toArray let mutable checksCompleted = 0 @@ -2332,7 +2438,10 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac member x.ForgetDocument(filePath) = forgetDocument filePath - member x.ParseAllFiles() = parseAllFiles () |> AsyncAVal.forceAsync + member x.ParseAllFiles() = + parseAllFiles () + |> AsyncAVal.forceAsync + |> Async.map (Array.choose Result.toOption) member x.GetOpenFileSource(filePath) = forceFindSourceText filePath @@ -2351,7 +2460,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac member x.GetTypeCheckResultsForFile(filePath) = asyncResult { let! opts = forceGetProjectOptions filePath - let snap = opts.FSharpProjectSnapshot |> AVal.force + let snap = opts.FSharpProjectCompilerOptions |> AVal.force return! x.GetTypeCheckResultsForFile(filePath, snap) } diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi b/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi index a9327a5d4..abb0156b2 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi @@ -31,18 +31,20 @@ type AdaptiveWorkspaceChosen = [] type LoadedProject = { ProjectOptions: Types.ProjectOptions - FSharpProjectSnapshot: aval + FSharpProjectCompilerOptions: aval LanguageVersion: LanguageVersionShim } interface IEquatable override GetHashCode: unit -> int override Equals: other: obj -> bool member ProjectFileName: string -// static member op_Implicit: x: LoadedProject -> FSharpProjectSnapshot type AdaptiveState = new: - lspClient: FSharpLspClient * sourceTextFactory: ISourceTextFactory * workspaceLoader: IWorkspaceLoader -> + lspClient: FSharpLspClient * + sourceTextFactory: ISourceTextFactory * + workspaceLoader: IWorkspaceLoader * + useTransparentCompiler: bool -> AdaptiveState member RootPath: string option with get, set @@ -51,7 +53,7 @@ type AdaptiveState = member ClientCapabilities: ClientCapabilities option with get, set member WorkspacePaths: WorkspaceChosen with get, set member DiagnosticCollections: DiagnosticCollection - member ScriptFileProjectOptions: Event + member ScriptFileProjectOptions: Event member OpenDocument: filePath: string * text: string * version: int -> CancellableTask @@ -64,17 +66,17 @@ type AdaptiveState = member GetParseResults: filePath: string -> Async> member GetOpenFileTypeCheckResults: file: string -> Async> member GetOpenFileTypeCheckResultsCached: filePath: string -> Async> - member GetProjectOptionsForFile: filePath: string -> Async> + member GetProjectOptionsForFile: filePath: string -> Async> member GetTypeCheckResultsForFile: - filePath: string * opts: FSharpProjectSnapshot -> Async> + filePath: string * opts: CompilerProjectOption -> Async> member GetTypeCheckResultsForFile: filePath: string -> Async> member GetFilesToProject: unit -> Async<(string * LoadedProject) array> member GetUsesOfSymbol: filePath: string * - opts: (string * FSharpProjectSnapshot) seq * + opts: (string * CompilerProjectOption) seq * symbol: FSharp.Compiler.Symbols.FSharpSymbol -> Async diff --git a/src/FsAutoComplete/LspServers/ProjectWorkspace.fs b/src/FsAutoComplete/LspServers/ProjectWorkspace.fs index 13400ab37..5ab60617c 100644 --- a/src/FsAutoComplete/LspServers/ProjectWorkspace.fs +++ b/src/FsAutoComplete/LspServers/ProjectWorkspace.fs @@ -33,7 +33,8 @@ module Snapshots = let getStream (_ctok: System.Threading.CancellationToken) = try - File.openFileStreamForReadingAsync (normalizePath p.TargetPath) :> Stream |> Some + File.openFileStreamForReadingAsync (normalizePath p.TargetPath) :> Stream + |> Some with _ -> None @@ -171,10 +172,9 @@ module Snapshots = (loadedProjectsA: amap, ProjectOptions>) (p: ProjectOptions) = - let tags = seq { - "projectFileName", box p.ProjectFileName - } + let tags = seq { "projectFileName", box p.ProjectFileName } use _span = fsacActivitySource.StartActivityForFunc(tags = tags) + logger.debug ( Log.setMessage "Creating references for {projectFileName}" >> Log.addContextDestructured "projectFileName" p.ProjectFileName @@ -215,14 +215,13 @@ module Snapshots = (p: ProjectOptions) = let normPath = Utils.normalizePath p.ProjectFileName - let tags = seq { - "projectFileName", box p.ProjectFileName - } + let tags = seq { "projectFileName", box p.ProjectFileName } use span = fsacActivitySource.StartActivityForFunc(tags = tags) match cachedSnapshots.TryGetValue normPath with | true, snapshot -> span.SetTagSafe("cachehit", true) |> ignore + logger.debug ( Log.setMessage "optionsToSnapshot - Cache hit - {projectFileName}" >> Log.addContextDestructured "projectFileName" p.ProjectFileName diff --git a/src/FsAutoComplete/Parser.fs b/src/FsAutoComplete/Parser.fs index 0ad58284b..94fde1740 100644 --- a/src/FsAutoComplete/Parser.fs +++ b/src/FsAutoComplete/Parser.fs @@ -94,6 +94,12 @@ module Parser = "Enabled OpenTelemetry exporter. See https://opentelemetry.io/docs/reference/specification/protocol/exporter/ for environment variables to configure for the exporter." ) + let useTransparentCompilerOption = + Option( + "--use-fcs-transparent-compiler", + "Enable LSP Server based on FSharp.Data.Adaptive. Should be more stable, but is experimental." + ) + let stateLocationOption = Option( "--state-directory", @@ -115,12 +121,13 @@ module Parser = rootCommand.AddOption logLevelOption rootCommand.AddOption stateLocationOption rootCommand.AddOption otelTracingOption + rootCommand.AddOption useTransparentCompilerOption // for back-compat - we removed some options and this broke some clients. rootCommand.TreatUnmatchedTokensAsErrors <- false rootCommand.SetHandler( - Func<_, _, _, Task>(fun projectGraphEnabled stateDirectory adaptiveLspEnabled -> + Func<_, _, _, _, Task>(fun projectGraphEnabled stateDirectory adaptiveLspEnabled useTransparentCompiler -> let workspaceLoaderFactory = fun toolsPath -> if projectGraphEnabled then @@ -145,16 +152,27 @@ module Parser = let lspFactory = if adaptiveLspEnabled then - fun () -> AdaptiveFSharpLspServer.startCore toolsPath workspaceLoaderFactory sourceTextFactory + fun () -> + AdaptiveFSharpLspServer.startCore + toolsPath + workspaceLoaderFactory + sourceTextFactory + useTransparentCompiler else - fun () -> AdaptiveFSharpLspServer.startCore toolsPath workspaceLoaderFactory sourceTextFactory + fun () -> + AdaptiveFSharpLspServer.startCore + toolsPath + workspaceLoaderFactory + sourceTextFactory + useTransparentCompiler let result = AdaptiveFSharpLspServer.start lspFactory Task.FromResult result), projectGraphOption, stateLocationOption, - adaptiveLspServerOption + adaptiveLspServerOption, + useTransparentCompilerOption ) rootCommand diff --git a/test/FsAutoComplete.Tests.Lsp/ExtensionsTests.fs b/test/FsAutoComplete.Tests.Lsp/ExtensionsTests.fs index a5898be1e..07808ea4c 100644 --- a/test/FsAutoComplete.Tests.Lsp/ExtensionsTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/ExtensionsTests.fs @@ -410,6 +410,7 @@ let signatureTests state = |> Async.Sequential |> Async.map (fun _ -> ())) + testSequenced <| testList "signature evaluation" [ testList diff --git a/test/FsAutoComplete.Tests.Lsp/FindReferencesTests.fs b/test/FsAutoComplete.Tests.Lsp/FindReferencesTests.fs index 3bbf90b3b..8014fb433 100644 --- a/test/FsAutoComplete.Tests.Lsp/FindReferencesTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/FindReferencesTests.fs @@ -628,6 +628,7 @@ let tests state = let tryFixupRangeTests (sourceTextFactory: ISourceTextFactory) = + testSequenced <| testList ($"{nameof Tokenizer.tryFixupRange}") [ let checker = lazy (FSharpChecker.Create()) diff --git a/test/FsAutoComplete.Tests.Lsp/Helpers.fs b/test/FsAutoComplete.Tests.Lsp/Helpers.fs index 270160d42..43b72bd09 100644 --- a/test/FsAutoComplete.Tests.Lsp/Helpers.fs +++ b/test/FsAutoComplete.Tests.Lsp/Helpers.fs @@ -215,7 +215,7 @@ let record (cacher: Cacher<_>) = AsyncLspResult.success Unchecked.defaultof<_> -let createAdaptiveServer workspaceLoader sourceTextFactory = +let createAdaptiveServer workspaceLoader sourceTextFactory useTransparentCompiler = let serverInteractions = new Cacher<_>() let recordNotifications = record serverInteractions @@ -225,7 +225,7 @@ let createAdaptiveServer workspaceLoader sourceTextFactory = let loader = workspaceLoader () let client = FSharpLspClient(recordNotifications, recordRequests) - let server = new AdaptiveFSharpLspServer(loader, client, sourceTextFactory) + let server = new AdaptiveFSharpLspServer(loader, client, sourceTextFactory, useTransparentCompiler) server :> IFSharpLspServer, serverInteractions :> ClientEvents let defaultConfigDto: FSharpConfigDto = diff --git a/test/FsAutoComplete.Tests.Lsp/Helpers.fsi b/test/FsAutoComplete.Tests.Lsp/Helpers.fsi index 208f4d491..2303333d4 100644 --- a/test/FsAutoComplete.Tests.Lsp/Helpers.fsi +++ b/test/FsAutoComplete.Tests.Lsp/Helpers.fsi @@ -67,7 +67,10 @@ module Range = val record: cacher: Cacher<'a * 'b> -> ('a -> 'b -> AsyncLspResult<'c>) val createAdaptiveServer: - workspaceLoader: (unit -> #Ionide.ProjInfo.IWorkspaceLoader) -> sourceTextFactory: ISourceTextFactory -> IFSharpLspServer * ClientEvents + workspaceLoader: (unit -> #Ionide.ProjInfo.IWorkspaceLoader) + -> sourceTextFactory: ISourceTextFactory + -> useTransparentCompiler : bool + -> IFSharpLspServer * ClientEvents val defaultConfigDto: FSharpConfigDto val clientCaps: ClientCapabilities diff --git a/test/FsAutoComplete.Tests.Lsp/Program.fs b/test/FsAutoComplete.Tests.Lsp/Program.fs index f52ed762e..9424c0335 100644 --- a/test/FsAutoComplete.Tests.Lsp/Program.fs +++ b/test/FsAutoComplete.Tests.Lsp/Program.fs @@ -48,6 +48,11 @@ let sourceTextFactory: ISourceTextFactory = RoslynSourceTextFactory() let mutable toolsPath = Ionide.ProjInfo.Init.init (System.IO.DirectoryInfo Environment.CurrentDirectory) None +let compilers = [ + "BackgroundCompiler", false + "TransparentCompiler", true +] + let lspTests = testList "lsp" @@ -56,56 +61,60 @@ let lspTests = testList $"{loaderName}" [ - Templates.tests () - let createServer () = - adaptiveLspServerFactory toolsPath workspaceLoaderFactory sourceTextFactory - - initTests createServer - closeTests createServer - - Utils.Tests.Server.tests createServer - Utils.Tests.CursorbasedTests.tests createServer - - CodeLens.tests createServer - documentSymbolTest createServer - Completion.autocompleteTest createServer - Completion.autoOpenTests createServer - Completion.fullNameExternalAutocompleteTest createServer - foldingTests createServer - tooltipTests createServer - Highlighting.tests createServer - scriptPreviewTests createServer - scriptEvictionTests createServer - scriptProjectOptionsCacheTests createServer - dependencyManagerTests createServer - interactiveDirectivesUnitTests - - // commented out because FSDN is down - //fsdnTest createServer - - //linterTests createServer - uriTests - formattingTests createServer - analyzerTests createServer - signatureTests createServer - SignatureHelp.tests createServer - InlineHints.tests createServer - CodeFixTests.Tests.tests sourceTextFactory createServer - Completion.tests createServer - GoTo.tests createServer - - FindReferences.tests createServer - Rename.tests createServer - - InfoPanelTests.docFormattingTest createServer - DetectUnitTests.tests createServer - XmlDocumentationGeneration.tests createServer - InlayHintTests.tests createServer - DependentFileChecking.tests createServer - UnusedDeclarationsTests.tests createServer - EmptyFileTests.tests createServer - CallHierarchy.tests createServer - ] ] + for (compilerName, useTransparentCompiler) in compilers do + testList + $"{compilerName}" + [ + Templates.tests () + let createServer () = + adaptiveLspServerFactory toolsPath workspaceLoaderFactory sourceTextFactory useTransparentCompiler + + initTests createServer + closeTests createServer + + Utils.Tests.Server.tests createServer + Utils.Tests.CursorbasedTests.tests createServer + + CodeLens.tests createServer + documentSymbolTest createServer + Completion.autocompleteTest createServer + Completion.autoOpenTests createServer + Completion.fullNameExternalAutocompleteTest createServer + foldingTests createServer + tooltipTests createServer + Highlighting.tests createServer + scriptPreviewTests createServer + scriptEvictionTests createServer + scriptProjectOptionsCacheTests createServer + dependencyManagerTests createServer + interactiveDirectivesUnitTests + + // commented out because FSDN is down + //fsdnTest createServer + + //linterTests createServer + uriTests + formattingTests createServer + analyzerTests createServer + signatureTests createServer + SignatureHelp.tests createServer + InlineHints.tests createServer + CodeFixTests.Tests.tests sourceTextFactory createServer + Completion.tests createServer + GoTo.tests createServer + + FindReferences.tests createServer + Rename.tests createServer + + InfoPanelTests.docFormattingTest createServer + DetectUnitTests.tests createServer + XmlDocumentationGeneration.tests createServer + InlayHintTests.tests createServer + DependentFileChecking.tests createServer + UnusedDeclarationsTests.tests createServer + EmptyFileTests.tests createServer + CallHierarchy.tests createServer + ] ] ] /// Tests that do not require a LSP server let generalTests = testList "general" [ From aa4f46eb9b571a1d6002d79eaf0e7f4997914879 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Sun, 21 Apr 2024 18:41:52 -0400 Subject: [PATCH 47/60] Up timeout again --- .github/workflows/build.yml | 4 ++-- test/FsAutoComplete.Tests.Lsp/CodeFixTests/Utils.fs | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a256f4c5d..4b4afe70e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,10 +16,10 @@ on: jobs: build: env: - TEST_TIMEOUT_MINUTES: 19 + TEST_TIMEOUT_MINUTES: 40 FSAC_TEST_DEFAULT_TIMEOUT : 120000 #ms, individual test timeouts DOTNET_ROLL_FORWARD_TO_PRERELEASE: 1 # needed to allow .NET 8 RCs to participate in rollforward as expected. - timeout-minutes: 20 # we have a locking issue, so cap the runs at ~20m to account for varying build times, etc + timeout-minutes: 40 # we have a locking issue, so cap the runs at ~20m to account for varying build times, etc strategy: matrix: os: diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Utils.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Utils.fs index 7ab75a18e..fec4b6fe6 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Utils.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Utils.fs @@ -3,11 +3,16 @@ module private FsAutoComplete.Tests.CodeFixTests.Utils open Ionide.LanguageServerProtocol.Types open FsAutoComplete.Logging - +open FsToolkit.ErrorHandling module Diagnostics = let expectCode code (diags: Diagnostic[]) = + let diagMsgs = + diags + |> Array.choose (fun d -> Option.zip d.Code (Some d.Message)) + |> Array.map(fun (code, msg) -> $"{code}: {msg}") + |> String.concat ", " Expecto.Flip.Expect.exists - $"There should be a Diagnostic with code %s{code}" + $"There should be a Diagnostic with code %s{code} but were: {diagMsgs} " (fun (d: Diagnostic) -> d.Code = Some code) diags From 096cb027c288a7aaf9412a4f5398a78505123f2a Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Sun, 21 Apr 2024 22:02:43 -0400 Subject: [PATCH 48/60] Split up tests --- .github/workflows/build.yml | 2 ++ test/FsAutoComplete.Tests.Lsp/Program.fs | 19 +++++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4b4afe70e..33b9431a0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,6 +27,7 @@ jobs: - macos-13 # using 13 because it's a bigger machine, and latest is still pointing to 12 - ubuntu-latest dotnet-version: ["", "6.0.x", "7.0.x", "8.0.x"] + use-transparent-compiler : [true, false] # these entries will mesh with the above combinations include: # just use what's in the repo @@ -107,6 +108,7 @@ jobs: env: BuildNet7: ${{ matrix.build_net7 }} BuildNet8: ${{ matrix.build_net8 }} + USE_TRANSPARENT_COMPILER: ${{ matrix.use-transparent-compiler }} analyze: runs-on: ubuntu-latest diff --git a/test/FsAutoComplete.Tests.Lsp/Program.fs b/test/FsAutoComplete.Tests.Lsp/Program.fs index 9424c0335..cd7edb9a2 100644 --- a/test/FsAutoComplete.Tests.Lsp/Program.fs +++ b/test/FsAutoComplete.Tests.Lsp/Program.fs @@ -17,6 +17,7 @@ open Serilog.Filters open System.IO open FsAutoComplete open Helpers +open FsToolkit.ErrorHandling Expect.defaultDiffPrinter <- Diff.colourisedDiff @@ -48,10 +49,20 @@ let sourceTextFactory: ISourceTextFactory = RoslynSourceTextFactory() let mutable toolsPath = Ionide.ProjInfo.Init.init (System.IO.DirectoryInfo Environment.CurrentDirectory) None -let compilers = [ - "BackgroundCompiler", false - "TransparentCompiler", true -] +let getEnvVarAsBool name = + Environment.GetEnvironmentVariable(name) + |> Option.ofObj + |> Option.bind (fun s -> s.ToLowerInvariant() |> Boolean.TryParse |> Option.ofPair) + +let compilers = + match getEnvVarAsBool "USE_TRANSPARENT_COMPILER" with + | Some true -> ["TransparentCompiler", true ] + | Some false -> [ "BackgroundCompiler", false ] + | None -> + [ + "BackgroundCompiler", false + "TransparentCompiler", true + ] let lspTests = testList From 88e430f08c54f20ded5e87ef1aea14fb078417d0 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Sun, 21 Apr 2024 22:20:02 -0400 Subject: [PATCH 49/60] Retry workspace loader tests --- .github/workflows/build.yml | 8 +++--- test/FsAutoComplete.Tests.Lsp/Program.fs | 32 +++++++++++++++--------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 33b9431a0..f6de52ca0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,7 +27,8 @@ jobs: - macos-13 # using 13 because it's a bigger machine, and latest is still pointing to 12 - ubuntu-latest dotnet-version: ["", "6.0.x", "7.0.x", "8.0.x"] - use-transparent-compiler : [true, false] + use-transparent-compiler : ["TransparentCompiler", "BackgroundCompiler"] + workspace-loader : ["WorkspaceLoader", "ProjectGraph"] # these entries will mesh with the above combinations include: # just use what's in the repo @@ -62,7 +63,7 @@ jobs: runs-on: ${{ matrix.os }} - name: Build on ${{matrix.os}} for ${{ matrix.label }} + name: Build on ${{matrix.os}} for ${{ matrix.label }} ${{ matrix.workspace-loader }} ${{ matrix.use-transparent-compiler }} steps: - uses: actions/checkout@v3 @@ -103,12 +104,13 @@ jobs: BuildNet8: ${{ matrix.build_net8 }} - name: Run and report tests - run: dotnet test -c Release -f ${{ matrix.test_tfm }} --no-restore --no-build --no-build --logger "console;verbosity=normal" --logger GitHubActions /p:AltCover=true /p:AltCoverAssemblyExcludeFilter="System.Reactive|FSharp.Compiler.Service|Ionide.ProjInfo|FSharp.Analyzers|Analyzer|Humanizer|FSharp.Core|FSharp.DependencyManager" -- Expecto.fail-on-focused-tests=true --blame-hang --blame-hang-timeout 1m + run: dotnet test -c Release -f ${{ matrix.test_tfm }} --no-restore --no-build --logger "console;verbosity=normal" --logger GitHubActions /p:AltCover=true /p:AltCoverAssemblyExcludeFilter="System.Reactive|FSharp.Compiler.Service|Ionide.ProjInfo|FSharp.Analyzers|Analyzer|Humanizer|FSharp.Core|FSharp.DependencyManager" -- Expecto.fail-on-focused-tests=true --blame-hang --blame-hang-timeout 1m working-directory: test/FsAutoComplete.Tests.Lsp env: BuildNet7: ${{ matrix.build_net7 }} BuildNet8: ${{ matrix.build_net8 }} USE_TRANSPARENT_COMPILER: ${{ matrix.use-transparent-compiler }} + USE_WORKSPACE_LOADER: ${{ matrix.workspace-loader }} analyze: runs-on: ubuntu-latest diff --git a/test/FsAutoComplete.Tests.Lsp/Program.fs b/test/FsAutoComplete.Tests.Lsp/Program.fs index cd7edb9a2..e32b16470 100644 --- a/test/FsAutoComplete.Tests.Lsp/Program.fs +++ b/test/FsAutoComplete.Tests.Lsp/Program.fs @@ -34,11 +34,22 @@ let testTimeout = // delay in ms between workspace start + stop notifications because the system goes too fast :-/ Environment.SetEnvironmentVariable("FSAC_WORKSPACELOAD_DELAY", "250") +let getEnvVarAsStr name = + Environment.GetEnvironmentVariable(name) + |> Option.ofObj + +let (|EqIC|_|) (a: string) (b: string) = + if String.Equals(a, b, StringComparison.OrdinalIgnoreCase) then Some () else None + let loaders = - [ - "Ionide WorkspaceLoader", (fun toolpath -> WorkspaceLoader.Create(toolpath, FsAutoComplete.Core.ProjectLoader.globalProperties)) - // "MSBuild Project Graph WorkspaceLoader", (fun toolpath -> WorkspaceLoaderViaProjectGraph.Create(toolpath, FsAutoComplete.Core.ProjectLoader.globalProperties)) - ] + match getEnvVarAsStr "USE_WORKSPACE_LOADER" with + | Some (EqIC "WorkspaceLoader") -> [ "Ionide WorkspaceLoader", (fun toolpath -> WorkspaceLoader.Create(toolpath, FsAutoComplete.Core.ProjectLoader.globalProperties)) ] + | Some (EqIC "ProjectGraph") -> [ "MSBuild Project Graph WorkspaceLoader", (fun toolpath -> WorkspaceLoaderViaProjectGraph.Create(toolpath, FsAutoComplete.Core.ProjectLoader.globalProperties)) ] + | _ -> + [ + "Ionide WorkspaceLoader", (fun toolpath -> WorkspaceLoader.Create(toolpath, FsAutoComplete.Core.ProjectLoader.globalProperties)) + // "MSBuild Project Graph WorkspaceLoader", (fun toolpath -> WorkspaceLoaderViaProjectGraph.Create(toolpath, FsAutoComplete.Core.ProjectLoader.globalProperties)) + ] let adaptiveLspServerFactory toolsPath workspaceLoaderFactory sourceTextFactory = @@ -49,16 +60,13 @@ let sourceTextFactory: ISourceTextFactory = RoslynSourceTextFactory() let mutable toolsPath = Ionide.ProjInfo.Init.init (System.IO.DirectoryInfo Environment.CurrentDirectory) None -let getEnvVarAsBool name = - Environment.GetEnvironmentVariable(name) - |> Option.ofObj - |> Option.bind (fun s -> s.ToLowerInvariant() |> Boolean.TryParse |> Option.ofPair) + let compilers = - match getEnvVarAsBool "USE_TRANSPARENT_COMPILER" with - | Some true -> ["TransparentCompiler", true ] - | Some false -> [ "BackgroundCompiler", false ] - | None -> + match getEnvVarAsStr "USE_TRANSPARENT_COMPILER" with + | Some (EqIC "TransparentCompiler") -> ["TransparentCompiler", true ] + | Some (EqIC "BackgroundCompiler") -> [ "BackgroundCompiler", false ] + | _ -> [ "BackgroundCompiler", false "TransparentCompiler", true From cea27d2ec55f335e9da927684ddbe93fb325c718 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Sun, 21 Apr 2024 22:53:58 -0400 Subject: [PATCH 50/60] skip flakey test --- test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs index f8d362aba..6eedc1e2d 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs @@ -2734,7 +2734,8 @@ let private replaceWithSuggestionTests state = let x: float = 2.0 """ - testCaseAsync "can change namespace in open" + // FCS sometimes doesn't give the correct message so test is flakey + ptestCaseAsync "can change namespace in open" <| CodeFix.check server """ From 53baffece939fcd50a9be6f9fd4e48b5ac4e0f04 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Sun, 21 Apr 2024 22:57:57 -0400 Subject: [PATCH 51/60] disable projectgraph tests for perf reasons --- .github/workflows/build.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f6de52ca0..f5cc6538f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,8 +27,12 @@ jobs: - macos-13 # using 13 because it's a bigger machine, and latest is still pointing to 12 - ubuntu-latest dotnet-version: ["", "6.0.x", "7.0.x", "8.0.x"] - use-transparent-compiler : ["TransparentCompiler", "BackgroundCompiler"] - workspace-loader : ["WorkspaceLoader", "ProjectGraph"] + use-transparent-compiler: + - "TransparentCompiler" + - "BackgroundCompiler" + workspace-loader: + - "WorkspaceLoader" + # - "ProjectGraph" # this is disable because it just adds too much time to the build # these entries will mesh with the above combinations include: # just use what's in the repo From c9e2ae01e5d59630b8856726d58b96e8eb85729c Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Sun, 21 Apr 2024 23:06:08 -0400 Subject: [PATCH 52/60] Fix description of transparent compiler flag --- src/FsAutoComplete/Parser.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FsAutoComplete/Parser.fs b/src/FsAutoComplete/Parser.fs index 94fde1740..dd9332990 100644 --- a/src/FsAutoComplete/Parser.fs +++ b/src/FsAutoComplete/Parser.fs @@ -97,7 +97,7 @@ module Parser = let useTransparentCompilerOption = Option( "--use-fcs-transparent-compiler", - "Enable LSP Server based on FSharp.Data.Adaptive. Should be more stable, but is experimental." + "Use Transparent Compiler in FSharp.Compiler.Services. Should have better performance characteristics, but is experimental. See https://github.com/dotnet/fsharp/pull/15179 for more details." ) let stateLocationOption = From 560a44bc8eec7bf0e32dfa5da2c6ade5e087dcb2 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Sun, 21 Apr 2024 23:11:46 -0400 Subject: [PATCH 53/60] cleanup dead code --- src/FsAutoComplete.Core/AdaptiveExtensions.fs | 40 ++----------------- .../CodeFixes/AddTypeAliasToSignatureFile.fs | 24 ----------- 2 files changed, 4 insertions(+), 60 deletions(-) diff --git a/src/FsAutoComplete.Core/AdaptiveExtensions.fs b/src/FsAutoComplete.Core/AdaptiveExtensions.fs index 193c02a4e..0d38073fe 100644 --- a/src/FsAutoComplete.Core/AdaptiveExtensions.fs +++ b/src/FsAutoComplete.Core/AdaptiveExtensions.fs @@ -26,10 +26,10 @@ module AdaptiveExtensions = | :? NullReferenceException -> () member cts.TryDispose() = - // try - if not <| isNull cts then - cts.Dispose() - // with _ -> () + try + if not <| isNull cts then + cts.Dispose() + with _ -> () type TaskCompletionSource<'a> with @@ -393,7 +393,6 @@ type internal RefCountingTaskCreator<'a>(create: CancellationToken -> Task<'a>) /// Upon cancellation, it will run the cancel function passed in and set cancellation for the task completion source. /// and AdaptiveCancellableTask<'a>(cancel: unit -> unit, real: Task<'a>) = - // let cts = new CancellationTokenSource() let mutable cachedTcs: TaskCompletionSource<'a> = null let mutable cached: Task<'a> = null @@ -578,30 +577,6 @@ module AsyncAVal = AdaptiveCancellableTask(cancel, real) } :> asyncaval<_> - - - let _ofAsyncAValSeq (maxDegreeOfParallelism: int) (input: #seq<#asyncaval<'a>>) = - let mutable cache: option> = None - - { new AbstractVal<_>() with - member x.Compute t = - if x.OutOfDate || Option.isNone cache then - let ref = - RefCountingTaskCreator( - cancellableTask { - return! - input - |> Seq.map (fun v -> cancellableTask { return! v.GetValue t }) - |> CancellableTask.whenAllThrottled maxDegreeOfParallelism - } - ) - - cache <- Some ref - ref.New() - else - cache.Value.New() } - :> asyncaval<_> - let ofAsync (value: Async<'a>) = { new AbstractVal<'a>() with member x.Compute _ = @@ -937,10 +912,3 @@ module AMapAsync = | Some x -> return! x | None -> return Error reason } - - - let _filterValuesByKey (key: 'Key) (map: amap<'Key, #asyncaval<'Value>>) = - asyncAVal { - let! values = map |> AMap.filter (fun k _ -> k = key) |> AMap.toASetValues |> ASet.toAVal - return! AsyncAVal._ofAsyncAValSeq 1 values - } diff --git a/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs b/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs index 5cdda6e43..7baa7dfd6 100644 --- a/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs +++ b/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs @@ -37,30 +37,6 @@ type SynTypeDefn with let title = "Add type alias to signature file" -// let codeFixForImplementationFileWithSignature -// (getProjectOptionsForFile: GetProjectOptionsForFile) -// (codeFix: CodeFix) -// (codeActionParams: CodeActionParams) -// : Async> = -// async { -// let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath -// let project = getProjectOptionsForFile fileName - -// match project with -// | Error _ -> return Ok [] -// | Ok projectOptions -> - -// let signatureFile = String.Concat(fileName, "i") - -// let hasSig = -// projectOptions.SourceFiles |> List.exists (fun s -> s.FileName = signatureFile) - -// if not hasSig then -// return Ok [] -// else -// return! codeFix codeActionParams -// } - let fix (getProjectOptionsForFile: GetProjectOptionsForFile) (getParseResultsForFile: GetParseResultsForFile) From 8696c1194f2e90dddcbd8292f6772c84c57371d7 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Sun, 21 Apr 2024 23:21:55 -0400 Subject: [PATCH 54/60] Comment project workspace more --- src/FsAutoComplete.Core/AdaptiveExtensions.fs | 3 +- .../LspServers/ProjectWorkspace.fs | 39 +++++++++++-------- .../LspServers/ProjectWorkspace.fsi | 10 +++++ 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/src/FsAutoComplete.Core/AdaptiveExtensions.fs b/src/FsAutoComplete.Core/AdaptiveExtensions.fs index 0d38073fe..0ccb5c551 100644 --- a/src/FsAutoComplete.Core/AdaptiveExtensions.fs +++ b/src/FsAutoComplete.Core/AdaptiveExtensions.fs @@ -29,7 +29,8 @@ module AdaptiveExtensions = try if not <| isNull cts then cts.Dispose() - with _ -> () + with _ -> + () type TaskCompletionSource<'a> with diff --git a/src/FsAutoComplete/LspServers/ProjectWorkspace.fs b/src/FsAutoComplete/LspServers/ProjectWorkspace.fs index 5ab60617c..ce83352dc 100644 --- a/src/FsAutoComplete/LspServers/ProjectWorkspace.fs +++ b/src/FsAutoComplete/LspServers/ProjectWorkspace.fs @@ -56,6 +56,8 @@ module Snapshots = originalLoadReferences = aval { + // If any of these change, it will create a new snapshot. + // And if any of the snapshots in the referencedProjects change, it will create a new snapshot for them as well. let! projectFileName = projectFileName and! projectId = projectId and! sourceFiles = sourceFiles @@ -128,6 +130,8 @@ module Snapshots = = aval { let file = UMX.untag sourceFilePath + // Useful as files may change from an external process, like a git pull, code generation or a save from another editor + // So we'll want to do typechecks when the file changes on disk let! writeTime = AdaptiveFile.GetLastWriteTimeUtc file and! sourceTextFactory = sourceTextFactory @@ -170,19 +174,19 @@ module Snapshots = (inMemorySourceFiles: amap, aval>) (sourceTextFactory: aval) (loadedProjectsA: amap, ProjectOptions>) - (p: ProjectOptions) + (project: ProjectOptions) = - let tags = seq { "projectFileName", box p.ProjectFileName } + let tags = seq { "projectFileName", box project.ProjectFileName } use _span = fsacActivitySource.StartActivityForFunc(tags = tags) logger.debug ( Log.setMessage "Creating references for {projectFileName}" - >> Log.addContextDestructured "projectFileName" p.ProjectFileName + >> Log.addContextDestructured "projectFileName" project.ProjectFileName ) loadedProjectsA |> AMap.filter (fun k _ -> - p.ReferencedProjects + project.ReferencedProjects |> List.exists (fun x -> normalizePath x.ProjectFileName = k)) |> AMap.map (fun _ proj -> if proj.ProjectFileName.EndsWith ".fsproj" then @@ -207,15 +211,16 @@ module Snapshots = loadFromDotnetDll proj |> AVal.constant) |> AMap.toASetValues + /// Creates a snapshot from a Project, using the already created snapshots it possible. and private optionsToSnapshot (cachedSnapshots: Dictionary<_, _>) (inMemorySourceFiles: amap<_, aval>) (sourceTextFactory: aval) (mapReferences: ProjectOptions -> aset>) - (p: ProjectOptions) + (project: ProjectOptions) = - let normPath = Utils.normalizePath p.ProjectFileName - let tags = seq { "projectFileName", box p.ProjectFileName } + let normPath = Utils.normalizePath project.ProjectFileName + let tags = seq { "projectFileName", box project.ProjectFileName } use span = fsacActivitySource.StartActivityForFunc(tags = tags) match cachedSnapshots.TryGetValue normPath with @@ -224,35 +229,36 @@ module Snapshots = logger.debug ( Log.setMessage "optionsToSnapshot - Cache hit - {projectFileName}" - >> Log.addContextDestructured "projectFileName" p.ProjectFileName + >> Log.addContextDestructured "projectFileName" project.ProjectFileName ) snapshot | _ -> logger.debug ( Log.setMessage "optionsToSnapshot - Cache miss - {projectFileName}" - >> Log.addContextDestructured "projectFileName" p.ProjectFileName + >> Log.addContextDestructured "projectFileName" project.ProjectFileName ) - let projectName = AVal.constant p.ProjectFileName - let projectId = AVal.constant p.ProjectId + let projectName = AVal.constant project.ProjectFileName + let projectId = AVal.constant project.ProjectId let sourceFiles = // alist because order matters for the F# Compiler - p.SourceFiles + project.SourceFiles |> AList.ofList |> AList.map Utils.normalizePath |> AList.map (fun sourcePath -> aval { + // prefer in-memory files over on-disk files match! inMemorySourceFiles |> AMap.tryFind sourcePath with | Some volatileFile -> return! volatileFile |> AVal.map createFSharpFileSnapshotInMemory | None -> return! createFSharpFileSnapshotOnDisk sourceTextFactory sourcePath }) - let references = p.OtherOptions |> List.filter (fun x -> x.StartsWith("-r:")) + let references = project.OtherOptions |> List.filter (fun x -> x.StartsWith("-r:")) - let otherOptions = p.OtherOptions |> ASet.ofList |> ASet.map (AVal.constant) + let otherOptions = project.OtherOptions |> ASet.ofList |> ASet.map (AVal.constant) let referencePaths = references @@ -261,10 +267,10 @@ module Snapshots = referencePath.Substring(3) // remove "-r:" |> createReferenceOnDisk) - let referencedProjects = mapReferences p + let referencedProjects = mapReferences project let isIncompleteTypeCheckEnvironment = AVal.constant false let useScriptResolutionRules = AVal.constant false - let loadTime = AVal.constant p.LoadTime + let loadTime = AVal.constant project.LoadTime let unresolvedReferences = AVal.constant None let originalLoadReferences = AVal.constant [] @@ -286,6 +292,7 @@ module Snapshots = snap + let createSnapshots (inMemorySourceFiles: amap, aval>) (sourceTextFactory: aval) diff --git a/src/FsAutoComplete/LspServers/ProjectWorkspace.fsi b/src/FsAutoComplete/LspServers/ProjectWorkspace.fsi index 5ddc244c8..aa1149c23 100644 --- a/src/FsAutoComplete/LspServers/ProjectWorkspace.fsi +++ b/src/FsAutoComplete/LspServers/ProjectWorkspace.fsi @@ -18,6 +18,16 @@ module Snapshots = open Ionide.ProjInfo.Logging open System.Collections.Generic + + /// This will create FSharpProjectSnapshots for each ProjectOptions. + /// List of files opened in memory or by the editor + /// Factory for retrieving ISourceText + /// Projects that have been loaded by msbuild + /// + /// This allows us to create the DAG of snapshots. Various changes to parent snapshots (Source file changes, referenced assembly changes) + /// will propagate creating new snapshots to its children. + /// + /// An AMap of Project Options with an Adaptive FSharpProjectSnapshot val createSnapshots: inMemorySourceFiles: amap, aval> -> sourceTextFactory: aval -> From bba20b6235df090afcb9b7c8c4d1835fc7fcee60 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Mon, 22 Apr 2024 09:54:18 -0400 Subject: [PATCH 55/60] revert fsproj change --- src/FsAutoComplete/FsAutoComplete.fsproj | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/FsAutoComplete/FsAutoComplete.fsproj b/src/FsAutoComplete/FsAutoComplete.fsproj index 6ab3a229e..9784db058 100644 --- a/src/FsAutoComplete/FsAutoComplete.fsproj +++ b/src/FsAutoComplete/FsAutoComplete.fsproj @@ -50,7 +50,7 @@ - + fsautocomplete fsautocomplete @@ -68,7 +68,8 @@ https://github.com/nuget/home/issues/3891#issuecomment-377319939 --> - + From 47bc36444ae099306798bd718682e6d3c47af089 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Mon, 22 Apr 2024 13:43:29 -0400 Subject: [PATCH 56/60] Update src/FsAutoComplete.Core/AdaptiveExtensions.fs Co-authored-by: Chet Husk --- src/FsAutoComplete.Core/AdaptiveExtensions.fs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/FsAutoComplete.Core/AdaptiveExtensions.fs b/src/FsAutoComplete.Core/AdaptiveExtensions.fs index 0ccb5c551..fffc49920 100644 --- a/src/FsAutoComplete.Core/AdaptiveExtensions.fs +++ b/src/FsAutoComplete.Core/AdaptiveExtensions.fs @@ -38,6 +38,7 @@ module AdaptiveExtensions = /// https://github.com/dotnet/runtime/issues/47998 member tcs.TrySetFromTask(real: Task<'a>) = + // note: using ContinueWith instead of task CE for better stack traces real.ContinueWith(fun (task: Task<_>) -> match task.Status with | TaskStatus.RanToCompletion -> tcs.TrySetResult task.Result |> ignore From 00b911e6721b24ed5415e0c5fc507dbcb3ca4036 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Mon, 22 Apr 2024 13:43:42 -0400 Subject: [PATCH 57/60] Update Directory.Build.props Co-authored-by: Chet Husk --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index d0fe85bb3..01998466f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -7,7 +7,7 @@ $(NoWarn);3186,0042 $(NoWarn);NU1902 - $(NoWarn);57 + $(NoWarn);57 $(WarnOn);1182 From 48ad332a8ff65b8da5d007d29a23b1aa8ec7f134 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Mon, 22 Apr 2024 13:49:12 -0400 Subject: [PATCH 58/60] Remove nowarn since it's in pojectfile now --- src/FsAutoComplete/CodeFixes.fsi | 2 -- src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fs | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/FsAutoComplete/CodeFixes.fsi b/src/FsAutoComplete/CodeFixes.fsi index bf57c0030..3ab46279a 100644 --- a/src/FsAutoComplete/CodeFixes.fsi +++ b/src/FsAutoComplete/CodeFixes.fsi @@ -1,7 +1,5 @@ namespace FsAutoComplete.CodeFix -#nowarn "57" - open FsAutoComplete open FsAutoComplete.LspHelpers open Ionide.LanguageServerProtocol.Types diff --git a/src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fs b/src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fs index 154c671be..9d6cb51f3 100644 --- a/src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fs +++ b/src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fs @@ -10,8 +10,6 @@ open FsAutoComplete.CodeFix.Types open FsAutoComplete open FsAutoComplete.LspHelpers -#nowarn "57" - let title = "Update val in signature file" let fix (getParseResultsForFile: GetParseResultsForFile) : CodeFix = From a6692eb4387aae4d80cba3aeb6e67a3d105a0576 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Mon, 22 Apr 2024 13:49:31 -0400 Subject: [PATCH 59/60] Remove redundant normalize call --- src/FsAutoComplete.Core/SymbolLocation.fs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/FsAutoComplete.Core/SymbolLocation.fs b/src/FsAutoComplete.Core/SymbolLocation.fs index 80bf2e040..12c303096 100644 --- a/src/FsAutoComplete.Core/SymbolLocation.fs +++ b/src/FsAutoComplete.Core/SymbolLocation.fs @@ -38,14 +38,7 @@ let getDeclarationLocation let! loc = declarationLocation let isScript = isAScript loc.FileName // sometimes the source file locations start with a capital, despite all of our efforts. - let normalizedPath = - if System.Char.IsUpper(loc.FileName[0]) then - string (System.Char.ToLowerInvariant loc.FileName[0]) - + (loc.FileName.Substring(1)) - else - loc.FileName - - let taggedFilePath = Utils.normalizePath normalizedPath + let taggedFilePath = Utils.normalizePath loc.FileName if isScript && taggedFilePath = currentDocument.FileName then return SymbolDeclarationLocation.CurrentDocument From 5223f8091e12dd776375f116dfd550691f6456e3 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Mon, 22 Apr 2024 13:51:48 -0400 Subject: [PATCH 60/60] Add noequality no comparison --- src/FsAutoComplete.Core/SemaphoreSlimLocks.fs | 1 + src/FsAutoComplete.Core/SemaphoreSlimLocks.fsi | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/FsAutoComplete.Core/SemaphoreSlimLocks.fs b/src/FsAutoComplete.Core/SemaphoreSlimLocks.fs index 17c7e3a5e..b59dd0007 100644 --- a/src/FsAutoComplete.Core/SemaphoreSlimLocks.fs +++ b/src/FsAutoComplete.Core/SemaphoreSlimLocks.fs @@ -7,6 +7,7 @@ open System.Threading.Tasks /// An awaitable wrapper around a task whose result is disposable. The wrapper is not disposable, so this prevents usage errors like "use _lock = myAsync()" when the appropriate usage should be "use! _lock = myAsync())". /// [] +[] type AwaitableDisposable<'T when 'T :> IDisposable>(t: Task<'T>) = member x.GetAwaiter() = t.GetAwaiter() member x.AsTask() = t diff --git a/src/FsAutoComplete.Core/SemaphoreSlimLocks.fsi b/src/FsAutoComplete.Core/SemaphoreSlimLocks.fsi index d8b22d3b2..358ac356c 100644 --- a/src/FsAutoComplete.Core/SemaphoreSlimLocks.fsi +++ b/src/FsAutoComplete.Core/SemaphoreSlimLocks.fsi @@ -6,8 +6,8 @@ open System.Threading.Tasks /// /// An awaitable wrapper around a task whose result is disposable. The wrapper is not disposable, so this prevents usage errors like "use _lock = myAsync()" when the appropriate usage should be "use! _lock = myAsync())". /// -[] [] +[] type AwaitableDisposable<'T when 'T :> IDisposable> = new: t: Task<'T> -> AwaitableDisposable<'T> member GetAwaiter: unit -> Runtime.CompilerServices.TaskAwaiter<'T>