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 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8569b493c..f5cc6538f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,6 +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" # 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 @@ -61,7 +67,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 @@ -102,11 +108,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 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/Directory.Build.props b/Directory.Build.props index ed5de172f..01998466f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -7,8 +7,10 @@ $(NoWarn);3186,0042 $(NoWarn);NU1902 - $(WarnOn);1182 + $(WarnOn);1182 + $(WarnOn);3390 true $(MSBuildThisFileDirectory)CHANGELOG.md 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 \ 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 +} diff --git a/src/FsAutoComplete.Core/AdaptiveExtensions.fs b/src/FsAutoComplete.Core/AdaptiveExtensions.fs index 96b49c766..fffc49920 100644 --- a/src/FsAutoComplete.Core/AdaptiveExtensions.fs +++ b/src/FsAutoComplete.Core/AdaptiveExtensions.fs @@ -6,40 +6,49 @@ open FSharp.Data.Traceable 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 - /// Communicates a request for cancellation. Ignores ObjectDisposedException member cts.TryCancel() = try - cts.Cancel() - with :? ObjectDisposedException -> - () + if not <| isNull cts then + cts.Cancel() + with + | :? ObjectDisposedException + | :? NullReferenceException -> () - /// Releases all resources used by the current instance of the System.Threading.CancellationTokenSource class. member cts.TryDispose() = try - cts.Dispose() + 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> + + // 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 + | TaskStatus.Canceled -> + tcs.TrySetCanceled(TaskCanceledException(task).CancellationToken) + |> ignore + | TaskStatus.Faulted -> tcs.TrySetException(task.Exception.InnerExceptions) |> ignore + + | _ -> ()) + |> ignore type ChangeableHashMap<'Key, 'Value> with @@ -148,7 +157,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)) @@ -386,7 +395,7 @@ 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 let getTask () = @@ -394,14 +403,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 +421,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 +441,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 +485,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 } @@ -531,36 +537,78 @@ 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 + 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<_> + + /// + /// Creates an async adaptive value evaluation the given value. + /// + let ofAVal (value: aval<'a>) = + 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 () = @@ -570,27 +618,20 @@ module AsyncAVal = let real = task { try - return! Async.StartImmediateAsTask(value, cts.Token) + // 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 cts.TryDispose() } - cache <- Some(AdaptiveCancellableTask(cancel, real)) - - cache.Value } - :> asyncaval<_> - - /// - /// Creates an async adaptive value evaluation the given value. - /// - let ofAVal (value: aval<'a>) = - if value.IsConstant then - ConstantVal(Task.FromResult(AVal.force value)) :> asyncaval<_> - else - { new AbstractVal<'a>() with - member x.Compute t = - let real = Task.Run(fun () -> value.GetValue t) - AdaptiveCancellableTask(id, real) } + AdaptiveCancellableTask(cancel, real) } :> asyncaval<_> @@ -600,17 +641,27 @@ 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 = if x.OutOfDate || Option.isNone cache then let ref = - RefCountingTaskCreator( - cancellableTask { - let! i = input.GetValue t - return! mapping i - } - ) + RefCountingTaskCreator(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 ct + dataCache <- ValueSome(struct (i, b)) + return b + }) cache <- Some ref ref.New() @@ -631,13 +682,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.Run(fun () -> mapping a ct)) input /// /// Returns a new async adaptive value that adaptively applies the mapping function to the given @@ -645,28 +690,32 @@ 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 = if x.OutOfDate || Option.isNone cache then let ref = - RefCountingTaskCreator( - cancellableTask { + RefCountingTaskCreator(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! 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 + }) cache <- Some ref ref.New() @@ -680,6 +729,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 @@ -699,39 +749,48 @@ module AsyncAVal = if x.OutOfDate then if Interlocked.Exchange(&inputChanged, 0) = 1 || Option.isNone cache then let outerTask = - RefCountingTaskCreator( - cancellableTask { - let! i = value.GetValue t - let! ct = CancellableTask.getCancellationToken () - let inner = mapping i ct - return inner + RefCountingTaskCreator(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 inner = mapping i ct + outerDataCache <- Some(i, inner) + return inner + + }) cache <- Some outerTask let outerTask = cache.Value let ref = - RefCountingTaskCreator( - cancellableTask { - let! ct = CancellableTask.getCancellationToken () + RefCountingTaskCreator(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) return! innerTask.Task - } - ) + }) innerCache <- Some ref @@ -800,7 +859,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 diff --git a/src/FsAutoComplete.Core/AdaptiveExtensions.fsi b/src/FsAutoComplete.Core/AdaptiveExtensions.fsi index 5a758f0cc..f41ee508b 100644 --- a/src/FsAutoComplete.Core/AdaptiveExtensions.fsi +++ b/src/FsAutoComplete.Core/AdaptiveExtensions.fsi @@ -1,7 +1,17 @@ namespace FsAutoComplete.Adaptive +open System.Threading + [] 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 +73,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. @@ -170,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. /// @@ -268,6 +278,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 +366,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/Commands.fs b/src/FsAutoComplete.Core/Commands.fs index a30caa016..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 * FSharpProjectOptions * 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 - |> Array.map (fun p -> UMX.tag 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: FSharpProjectOptions) = + 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: FSharpProjectOptions 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 = UMX.tag file + 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 * FSharpProjectOptions * 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 d27bddcee..327f4c4af 100644 --- a/src/FsAutoComplete.Core/CompilerServiceInterface.fs +++ b/src/FsAutoComplete.Core/CompilerServiceInterface.fs @@ -12,12 +12,64 @@ open FSharp.Compiler.Symbols open Microsoft.Extensions.Caching.Memory open System open FsToolkit.ErrorHandling - +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, @@ -29,7 +81,8 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe enablePartialTypeChecking = not hasAnalyzers, parallelReferenceResolution = parallelReferenceResolution, captureIdentifiersWhenParsing = true, - useSyntaxTreeCache = true + useSyntaxTreeCache = true, + useTransparentCompiler = useTransparentCompiler ) let entityCache = EntityCache() @@ -51,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 = Array.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 @@ -60,7 +113,33 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe let mutable disableInMemoryProjectReferences = false - let fixupFsharpCoreAndFSIPaths (p: FSharpProjectOptions) = + let fixupFsharpCoreAndFSIPathsForSnapshot (snapshot: FSharpProjectSnapshot) = + match sdkFsharpCore, sdkFsiAuxLib with + | None, _ + | _, None -> snapshot + | Some fsc, Some fsi -> + let _toReplace, otherOpts = + snapshot.OtherOptions + |> List.partition (fun opt -> + opt.EndsWith("FSharp.Core.dll", StringComparison.Ordinal) + || opt.EndsWith("FSharp.Compiler.Interactive.Settings.dll", StringComparison.Ordinal)) + + 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 fixupFsharpCoreAndFSIPathsForOptions (p: FSharpProjectOptions) = match sdkFsharpCore, sdkFsiAuxLib with | None, _ | _, None -> p @@ -74,6 +153,7 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe { 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 ..]) @@ -88,23 +168,35 @@ 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 = [||] } + let (|Reference|_|) (opt: string) = + if opt.StartsWith("-r:", StringComparison.Ordinal) then + Some(opt.[3..]) else - opts + 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 (projectOptions: FSharpProjectOptions) = - let files = Array.append fsiAdditionalFiles projectOptions.SourceFiles + let addLoadedFilesToSnapshot (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,37 +204,116 @@ 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) = + + /// 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 = projectOptions.SourceFiles |> Array.map Path.GetFullPath - OtherOptions = - projectOptions.OtherOptions - |> Array.map (fun opt -> - match opt with - | Reference r -> $"-r:{Path.GetFullPath r}" - | opt -> opt) } + SourceFiles = files } + 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 -> - o.ReferencedProjects - |> Array.map (fun p -> Path.GetFullPath p.OutputFile) - |> Array.contains option.ProjectFileName) ]) + o.ReferencedProjectsPath + |> List.map (fun p -> Path.GetFullPath p) + |> List.contains option.ProjectFileName) ]) + + member private __.GetNetFxScriptSnapshot(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.GetProjectSnapshotFromScript( + UMX.untag file, + source, + assumeDotNetFramework = true, + useFsiAuxLib = true, + otherFlags = allFlags, + userOpName = "getNetFrameworkScriptOptions" + ) + + let allModifications = addLoadedFilesToSnapshot + + return allModifications opts, errors + } + + member private __.GetNetCoreScriptSnapshot(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! (snapshot, errors) = + checker.GetProjectSnapshotFromScript( + UMX.untag file, + source, + assumeDotNetFramework = false, + useSdkRefs = true, + useFsiAuxLib = true, + otherFlags = allFlags, + userOpName = "getNetCoreScriptOptions" + ) + + optsLogger.trace ( + Log.setMessage "Got NetCore snapshot {snapshot} for file {file} with errors {errors}" + >> Log.addContextDestructured "file" file + >> Log.addContextDestructured "snapshot" snapshot + >> Log.addContextDestructured "errors" errors + ) + + let allModifications = + // filterBadRuntimeRefs >> + addLoadedFilesToSnapshot >> fixupFsharpCoreAndFSIPathsForSnapshot + + let modified = allModifications snapshot + + optsLogger.trace ( + Log.setMessage "Replaced options to {opts}" + >> Log.addContextDestructured "opts" modified + ) + + return modified, errors + } + + 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 { @@ -163,7 +334,7 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe userOpName = "getNetFrameworkScriptOptions" ) - let allModifications = addLoadedFiles >> resolveRelativeFilePaths + let allModifications = addLoadedFilesToProject >> resolveRelativeFilePaths return allModifications opts, errors } @@ -198,7 +369,9 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe let allModifications = // filterBadRuntimeRefs >> - addLoadedFiles >> resolveRelativeFilePaths >> fixupFsharpCoreAndFSIPaths + addLoadedFilesToProject + >> resolveRelativeFilePaths + >> fixupFsharpCoreAndFSIPathsForOptions let modified = allModifications opts @@ -216,11 +389,14 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe | FSIRefs.TFM.NetCore -> self.GetNetCoreScriptOptions(file, source) + member __.ScriptTypecheckRequirementsChanged = scriptTypecheckRequirementsChanged.Publish member _.RemoveFileFromCache(file: string) = lastCheckResults.Remove(file) + 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() = lastCheckResults.Dispose() @@ -230,28 +406,94 @@ 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. + /// Parsing options for the project or script. /// - member __.ParseFile(filePath: string, source: ISourceText, options: FSharpParsingOptions) = + member x.ParseFile(filePath: string, snapshot: FSharpProjectSnapshot) = + async { + checkerLogger.info ( + Log.setMessage "ParseFile - {file}" + >> Log.addContextDestructured "file" filePath + ) + + let path = UMX.untag filePath + 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, source, options) + 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. - /// 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. /// Note: all files except the one being checked are read from the FileSystem API /// Result of ParseAndCheckResults + member _.ParseAndCheckFileInProject + ( + filePath: string, + snapshot: FSharpProjectSnapshot, + ?shouldCache: bool + ) = + asyncResult { + let shouldCache = defaultArg shouldCache false + let opName = sprintf "ParseAndCheckFileInProject - %A" filePath + + checkerLogger.info (Log.setMessage "{opName}" >> Log.addContextDestructured "opName" opName) + + let path = UMX.untag filePath + + try + let! (p, c) = checker.ParseAndCheckFileInProject(path, snapshot, 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()) + } + member __.ParseAndCheckFileInProject ( filePath: string, @@ -266,7 +508,7 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe checkerLogger.info (Log.setMessage "{opName}" >> Log.addContextDestructured "opName" opName) - let options = clearProjectReferences options + // let options = clearProjectReferences options let path = UMX.untag filePath try @@ -326,6 +568,20 @@ 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) + + 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 __.TryGetRecentCheckResultsForFile(file: string, options, source: ISourceText) = let opName = sprintf "TryGetRecentCheckResultsForFile - %A" file @@ -336,7 +592,7 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe ) - let options = clearProjectReferences options + // let options = clearProjectReferences options let result = checker.TryGetRecentCheckResultsForFile(UMX.untag file, options, sourceText = source, userOpName = opName) @@ -358,10 +614,15 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe 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, - options: (string * FSharpProjectOptions) seq, + snapshots: (string * CompilerProjectOption) seq, symbol: FSharpSymbol ) = async { @@ -370,19 +631,18 @@ 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 opts = clearProjectReferences 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 opts = clearProjectReferences opts - let! res = checker.ParseAndCheckProject opts + let! res = x.ParseAndCheckProject(opts) return res.GetUsesOfSymbol symbol }) |> Async.parallel75 @@ -390,7 +650,39 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe return res |> Array.concat } - member _.FindReferencesForSymbolInFile(file, project, 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 + + try + 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 [||] + } + + member _.FindReferencesForSymbolInFile(file: string, project, symbol) = async { checkerLogger.info ( Log.setMessage "FindReferencesForSymbolInFile - {file}" @@ -399,7 +691,7 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe return! checker.FindBackgroundReferencesInFile( - file, + UMX.untag file, project, symbol, canInvalidateProject = false, @@ -408,16 +700,6 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe ) } - member __.GetDeclarations(fileName: string, source: ISourceText, options: FSharpParsingOptions, _) = - async { - checkerLogger.info ( - Log.setMessage "GetDeclarations - {file}" - >> Log.addContextDestructured "file" fileName - ) - - let! parseResult = checker.ParseFile(UMX.untag fileName, source, options, userOpName = "getDeclarations") - return parseResult.GetNavigationItems().Declarations - } member __.SetDotnetRoot(dotnetBinary: FileInfo, cwd: DirectoryInfo) = match Ionide.ProjInfo.SdkDiscovery.versionAt cwd dotnetBinary with @@ -456,5 +738,10 @@ 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 f332a9a27..320d64d4e 100644 --- a/src/FsAutoComplete.Core/CompilerServiceInterface.fsi +++ b/src/FsAutoComplete.Core/CompilerServiceInterface.fsi @@ -1,9 +1,11 @@ namespace FsAutoComplete open System.IO +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 @@ -12,44 +14,66 @@ 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 -> - options: seq -> - (FSharpProjectOptions * FSharpProjectOptions list) option + snapshots: seq -> + option> + + member GetProjectSnapshotsFromScript: + file: string * source: ISourceTextNew * tfm: FSIRefs.TFM -> + Async member GetProjectOptionsFromScript: file: string * source: ISourceText * tfm: FSIRefs.TFM -> - Async + 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 + /// 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 * snapshot: FSharpProjectSnapshot -> Async + member ParseFile: - filePath: string * source: ISourceText * options: FSharpParsingOptions -> Async + 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. - /// 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. /// Note: all files except the one being checked are read from the FileSystem API /// Result of ParseAndCheckResults + member ParseAndCheckFileInProject: + filePath: string * snapshot: FSharpProjectSnapshot * ?shouldCache: bool -> + Async> + member ParseAndCheckFileInProject: filePath: string * version: int * @@ -68,18 +92,24 @@ type FSharpCompilerServiceChecker = member TryGetLastCheckResultForFile: file: string -> ParseAndCheckResults option member TryGetRecentCheckResultsForFile: - file: string * options: FSharpProjectOptions * source: ISourceText -> ParseAndCheckResults option + file: string * snapshot: FSharpProjectSnapshot -> ParseAndCheckResults option + + member TryGetRecentCheckResultsForFile: + file: string * options: FSharpProjectOptions * source: ISourceText -> option member GetUsesOfSymbol: - file: string * options: (string * FSharpProjectOptions) seq * symbol: FSharpSymbol -> + file: string * snapshots: (string * CompilerProjectOption) seq * symbol: FSharpSymbol -> Async member FindReferencesForSymbolInFile: - file: string * project: FSharpProjectOptions * symbol: FSharpSymbol -> Async> + file: string * project: FSharpProjectSnapshot * symbol: FSharpSymbol -> Async> + + member FindReferencesForSymbolInFile: + file: string * project: FSharpProjectOptions * symbol: FSharpSymbol -> Async> - member GetDeclarations: - fileName: string * source: ISourceText * options: FSharpParsingOptions * version: 'a -> - Async + // member GetDeclarations: + // fileName: string * source: ISourceText * snapshot: FSharpProjectOptions * version: 'a -> + // Async member SetDotnetRoot: dotnetBinary: FileInfo * cwd: DirectoryInfo -> unit member GetDotnetRoot: unit -> DirectoryInfo option diff --git a/src/FsAutoComplete.Core/FCSPatches.fs b/src/FsAutoComplete.Core/FCSPatches.fs index 83ba575c7..91ccb15d5 100644 --- a/src/FsAutoComplete.Core/FCSPatches.fs +++ b/src/FsAutoComplete.Core/FCSPatches.fs @@ -283,11 +283,22 @@ module LanguageVersionShim = let defaultLanguageVersion = lazy (LanguageVersionShim("latest")) /// Tries to parse out "--langversion:" from OtherOptions if it can't find it, returns defaultLanguageVersion - /// The FSharpProjectOptions to use + /// The OtherOptions 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)) + let fromOtherOptions (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 |> 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 diff --git a/src/FsAutoComplete.Core/FCSPatches.fsi b/src/FsAutoComplete.Core/FCSPatches.fsi index 58ef5dc28..0168fbc49 100644 --- a/src/FsAutoComplete.Core/FCSPatches.fsi +++ b/src/FsAutoComplete.Core/FCSPatches.fsi @@ -18,10 +18,13 @@ 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 val fromFSharpProjectOptions: fpo: FSharpProjectOptions -> LanguageVersionShim + val fromFSharpProjectSnapshot: fpo: FSharpProjectSnapshot -> LanguageVersionShim module SyntaxTreeOps = val synExprContainsError: SynExpr -> bool diff --git a/src/FsAutoComplete.Core/FileSystem.fs b/src/FsAutoComplete.Core/FileSystem.fs index 533589b0a..e02d61cf8 100644 --- a/src/FsAutoComplete.Core/FileSystem.fs +++ b/src/FsAutoComplete.Core/FileSystem.fs @@ -21,8 +21,21 @@ module File = 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 +84,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 @@ -140,6 +153,7 @@ type IFSACSourceText = position: Position * terminal: (char -> bool) * condition: (char -> bool) -> option inherit ISourceText + inherit ISourceTextNew module RoslynSourceText = open Microsoft.CodeAnalysis.Text @@ -329,7 +343,6 @@ module RoslynSourceText = Ok(RoslynSourceTextFile(fileName, sourceText.WithChanges(change))) - interface ISourceText with member _.Item @@ -385,10 +398,35 @@ 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 abstract member Create: fileName: string * stream: Stream -> CancellableValueTask +module SourceTextFactory = + + + 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 >= 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) + 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 3c62dfa57..6829c439d 100644 --- a/src/FsAutoComplete.Core/FileSystem.fsi +++ b/src/FsAutoComplete.Core/FileSystem.fsi @@ -98,10 +98,17 @@ type IFSACSourceText = inherit ISourceText + inherit ISourceTextNew + 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/FsAutoComplete.Core.fsproj b/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj index 115b3ae1e..6292da422 100644 --- a/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj +++ b/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj @@ -9,8 +9,10 @@ - + + + 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/SemaphoreSlimLocks.fs b/src/FsAutoComplete.Core/SemaphoreSlimLocks.fs new file mode 100644 index 000000000..b59dd0007 --- /dev/null +++ b/src/FsAutoComplete.Core/SemaphoreSlimLocks.fs @@ -0,0 +1,39 @@ +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..358ac356c --- /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..12c303096 100644 --- a/src/FsAutoComplete.Core/SymbolLocation.fs +++ b/src/FsAutoComplete.Core/SymbolLocation.fs @@ -9,16 +9,15 @@ open FsToolkit.ErrorHandling [] type SymbolDeclarationLocation = | CurrentDocument - | Projects of FSharpProjectOptions list * isLocalForProject: bool + | Projects of CompilerProjectOption list * isLocalForProject: bool let getDeclarationLocation ( symbolUse: FSharpSymbolUse, currentDocument: IFSACSourceText, getProjectOptions, - projectsThatContainFile: string -> Async, - getDependentProjectsOfProjects - // state: State + projectsThatContainFile: string -> Async, + getDependentProjectsOfProjects: CompilerProjectOption list -> Async ) : Async> = asyncOption { @@ -39,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 = UMX.tag normalizedPath + let taggedFilePath = Utils.normalizePath loc.FileName if isScript && taggedFilePath = currentDocument.FileName then return SymbolDeclarationLocation.CurrentDocument @@ -61,8 +53,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.Core/Utils.fs b/src/FsAutoComplete.Core/Utils.fs index 0fad06fb8..70346bcc7 100644 --- a/src/FsAutoComplete.Core/Utils.fs +++ b/src/FsAutoComplete.Core/Utils.fs @@ -66,8 +66,10 @@ 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 +78,7 @@ module ProcessHelper = let _registered = token.Register(fun _ -> tcs.SetCanceled()) - let! _ = tcs.Task |> Async.AwaitTask + let! _ = tcs.Task () } diff --git a/src/FsAutoComplete/CodeFixes.fs b/src/FsAutoComplete/CodeFixes.fs index 481dd9e28..afad75852 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 @@ -33,8 +34,7 @@ module Types = type GetLanguageVersion = string -> Async - type GetProjectOptionsForFile = - string -> Async> + type GetProjectOptionsForFile = string -> Async> [] type FixKind = @@ -354,7 +354,11 @@ module Run = | Ok projectOptions -> let signatureFile = System.String.Concat(fileName, "i") - let hasSig = projectOptions.SourceFiles |> Array.contains signatureFile + + let hasSig = + projectOptions.SourceFilesTagged + |> List.map (UMX.untag) + |> List.contains signatureFile if not hasSig then return Ok [] diff --git a/src/FsAutoComplete/CodeFixes.fsi b/src/FsAutoComplete/CodeFixes.fsi index 05f068a20..3ab46279a 100644 --- a/src/FsAutoComplete/CodeFixes.fsi +++ b/src/FsAutoComplete/CodeFixes.fsi @@ -5,9 +5,9 @@ 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 +27,7 @@ module Types = type GetLanguageVersion = string -> Async - type GetProjectOptionsForFile = - string -> Async> + type GetProjectOptionsForFile = string -> Async> [] type FixKind = 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/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 = diff --git a/src/FsAutoComplete/FsAutoComplete.fsproj b/src/FsAutoComplete/FsAutoComplete.fsproj index 0caf50835..9784db058 100644 --- a/src/FsAutoComplete/FsAutoComplete.fsproj +++ b/src/FsAutoComplete/FsAutoComplete.fsproj @@ -19,14 +19,8 @@ true - - + + @@ -39,6 +33,8 @@ + + @@ -68,17 +64,13 @@ $(TargetsForTfmSpecificBuildOutput);CopyProjectReferencesToPackage - - + - + - - diff --git a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs index ed1c9f2de..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) @@ -166,7 +172,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 +183,6 @@ type AdaptiveFSharpLspServer .WithWorkingDirectory(rootPath) .ExecuteBufferedAsync() .Task - |> Async.AwaitTask if result.ExitCode <> 0 then fantomasLogger.warn ( @@ -195,7 +200,6 @@ type AdaptiveFSharpLspServer .WithWorkingDirectory(rootPath) .ExecuteBufferedAsync() .Task - |> Async.AwaitTask if result.ExitCode <> 0 then fantomasLogger.warn ( @@ -211,7 +215,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 +240,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") @@ -1202,7 +1204,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.FSharpProjectCompilerOptions) >> Array.toList ) @@ -3072,7 +3074,7 @@ module AdaptiveFSharpLspServer = - let startCore toolsPath workspaceLoaderFactory sourceTextFactory = + let startCore toolsPath workspaceLoaderFactory sourceTextFactory useTransparentCompiler = use input = Console.OpenStandardInput() use output = Console.OpenStandardOutput() @@ -3108,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 58f5e62d2..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 a490792a5..730488eb2 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 FsAutoComplete.ProjectWorkspace [] @@ -49,24 +50,27 @@ type AdaptiveWorkspaceChosen = | NotChosen + [] type LoadedProject = - { FSharpProjectOptions: FSharpProjectOptions + { ProjectOptions: Types.ProjectOptions + FSharpProjectCompilerOptions: aval LanguageVersion: LanguageVersionShim } interface IEquatable with - member x.Equals(other) = x.FSharpProjectOptions = other.FSharpProjectOptions + member x.Equals(other) = x.ProjectOptions = other.ProjectOptions - override x.GetHashCode() = x.FSharpProjectOptions.GetHashCode() + 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.FSharpProjectOptions.SourceFiles - member x.ProjectFileName = x.FSharpProjectOptions.ProjectFileName - static member op_Implicit(x: LoadedProject) = x.FSharpProjectOptions + member x.SourceFilesTagged = + x.ProjectOptions.SourceFiles |> List.map Utils.normalizePath |> 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 @@ -79,13 +83,21 @@ 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) - = + +type AdaptiveState + ( + lspClient: FSharpLspClient, + sourceTextFactory: ISourceTextFactory, + workspaceLoader: IWorkspaceLoader, + useTransparentCompiler: bool + ) = let logger = LogProvider.getLoggerFor () let thisType = typeof let disposables = new Disposables.CompositeDisposable() @@ -96,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 = @@ -260,18 +274,19 @@ 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: FSharpProjectOptions) ct = + let detectTests (parseResults: FSharpParseFileResults) (proj: CompilerProjectOption) ct = 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 @@ -307,7 +322,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) @@ -321,7 +336,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) @@ -337,7 +352,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) @@ -351,7 +366,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) @@ -418,7 +433,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 @@ -706,12 +721,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 @@ -725,12 +738,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) @@ -808,166 +815,98 @@ 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 - match wsp with - | AdaptiveWorkspaceChosen.NotChosen -> return [] - | AdaptiveWorkspaceChosen.Projs projects -> - let! binlogConfig = binlogConfig - - 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 + let loadProjects (loader: IWorkspaceLoader) binlogConfig projects = + logger.debug (Log.setMessageI $"Enter loading projects") - 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 + projects + |> AMap.mapWithAdditionalDependencies (fun projects -> - 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) + logger.debug (Log.setMessageI $"Enter loading projects mapWithAdditionalDependencies") - 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) -> + projects + |> Seq.iter (fun (proj: string, _) -> + let not = + UMX.untag proj |> ProjectResponse.ProjectLoading |> NotificationEvent.Workspace - let langversion = LanguageVersionShim.fromFSharpProjectOptions fso + notifications.Trigger(not, CancellationToken.None)) - // 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())) } + use progressReport = new ServerProgressReport(lspClient) - { FSharpProjectOptions = fso - LanguageVersion = langversion }, - projectOption) + progressReport.Begin ($"Loading {projects.Count} Projects") (CancellationToken.None) + |> ignore> - options - |> List.iter (fun (loadedProject, projectOption) -> - let projectFileName = loadedProject.ProjectFileName - let projViewerItemsNormalized = ProjectViewer.render projectOption + let projectOptions = + loader.LoadProjects(projects |> Seq.map (fst >> UMX.untag) |> Seq.toList, [], binlogConfig) + |> Seq.toList - 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) + for p in projectOptions do + logger.info ( + Log.setMessage "Found BaseIntermediateOutputPath of {path}" + >> Log.addContextDestructured "path" ((|BaseIntermediateOutputPath|_|) p.Properties) + ) - let references = - FscArguments.references (loadedProject.FSharpProjectOptions.OtherOptions |> List.ofArray) + 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 |> AVal.force + 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 () |> ignore>) - |> 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 (Utils.normalizePath v) + | _ -> () - let sourceFileToProjectOptions = - aval { - 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.ofAVal + match p.Properties with + | MSBuildAllProjects v -> + yield! + v + |> Array.filter (fun x -> x.EndsWith(".props", StringComparison.Ordinal) && isWithinObjFolder x) + |> Array.map (Utils.normalizePath >> projectFileChanges) + | _ -> () ] + HashMap.ofList + [ for p in projectOptions do + Utils.normalizePath p.ProjectFileName, (p, additionalDependencies p) ]) let openFilesTokens = ConcurrentDictionary, CancellationTokenSource>() @@ -1058,14 +997,126 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac return file }) + let projectOptions = + asyncAVal { + let! wsp = + adaptiveWorkspacePaths + |> addAValLogging (fun () -> + logger.info (Log.setMessage "Loading projects because adaptiveWorkspacePaths change")) + + + match wsp with + | 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 = + // 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 + } + + + 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 - let cancelToken filePath (cts: CancellationTokenSource) = + // 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! projectOptions = projectOptions + + if useTransparentCompiler then + return createSnapshots projectOptions + else + return createOptions projectOptions + } + + let getAllLoadedProjects = + asyncAVal { + let! loadedProjects = loadedProjects + + return! + loadedProjects + |> AMap.mapA (fun _ v -> v.FSharpProjectCompilerOptions |> AVal.map (fun _ -> v)) + |> AMap.toAVal + |> AVal.map HashMap.toValueList + + } + + + /// + /// 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 () = getAllLoadedProjects |> AsyncAVal.forceAsync + + 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 + + let sourceFileToProjectOptions = + asyncAVal { + let! loadedProjects = loadedProjects + + 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) = try logger.info ( Log.setMessage "Cancelling {filePath} - {version}" >> Log.addContextDestructured "filePath" filePath - // >> Log.addContextDestructured "version" oldFile.Version + >> Log.addContextDestructured "version" version ) cts.Cancel() @@ -1076,16 +1127,14 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac // ignore if already cancelled () - 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) - |> ignore + openFilesTokens.AddOrUpdate(filePath, adder, updater).Token let updateOpenFiles (file: VolatileFile) = @@ -1093,14 +1142,18 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let updater _ (v: cval<_>) = v.Value <- file - resetCancellationToken file.FileName + resetCancellationToken file.FileName file.Version |> ignore + 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 + |> ignore + transact (fun () -> textChanges.AddOrElse(filePath, adder, updater)) let isFileOpen file = openFiles |> AMap.tryFindA file |> AVal.map (Option.isSome) @@ -1125,7 +1178,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let lastTouched = File.getLastWriteTimeOrDefaultNow file - return! readFileFromDisk lastTouched file + return! createVolatileFileFromDisk lastTouched file with e -> logger.warn ( @@ -1154,42 +1207,53 @@ 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. - /// Parsing options for the project or script - /// The options for the project or script. + /// The source to be parsed. + /// /// - let parseFile (checker: FSharpCompilerServiceChecker) (source: VolatileFile) parseOpts options = - async { - let! result = checker.ParseFile(source.FileName, source.Source, parseOpts) + + let parseFile (checker: FSharpCompilerServiceChecker) (sourceFilePath) (compilerOptions: CompilerProjectOption) = + task { + 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, options, ct) + + result + |> Result.iter (fun result -> fileParsed.Trigger(result, compilerOptions, ct)) + return result } - /// Parses all files in the workspace. This is mostly used to trigger finding tests. let parseAllFiles () = asyncAVal { - let! projects = loadedProjectOptions + let! projects = getAllLoadedProjects and! (checker: FSharpCompilerServiceChecker) = checker - return + + let! projects = 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) -> - let fileName = UMX.tag fileName - - asyncResult { - let! file = forceFindOpenFileOrRead fileName - return! parseFile checker file parseOpts opts.FSharpProjectOptions - } - |> Async.map Result.toOption) - |> Async.parallel75 + |> List.map (fun p -> p.FSharpProjectCompilerOptions) + |> ASet.ofList + |> ASet.mapA id + |> ASet.toAVal + + return! + projects + |> HashSet.toArray + |> 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 } let forceFindSourceText filePath = forceFindOpenFileOrRead filePath |> AsyncResult.map (fun f -> f.Source) @@ -1209,24 +1273,122 @@ 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 + 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 |> 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 - return - { FSharpProjectOptions = opts - LanguageVersion = LanguageVersionShim.fromFSharpProjectOptions opts } - |> List.singleton with e -> logger.error ( Log.setMessage "Error getting project options for {filePath}" @@ -1239,6 +1401,8 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac return file, projs else + let! sourceFileToProjectOptions = sourceFileToProjectOptions + let! projs = sourceFileToProjectOptions |> AMap.tryFindR @@ -1250,53 +1414,68 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac }) let allFSharpFilesAndProjectOptions = - let wins = - openFilesToChangesAndProjectOptions - |> AMap.map (fun _k v -> v |> AsyncAVal.mapSync (fun (file, projects) _ -> file, projects)) + asyncAVal { + let wins = + openFilesToChangesAndProjectOptions + |> AMap.map (fun _k 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! sourceFileToProjectOptions = sourceFileToProjectOptions + + let loses = + sourceFileToProjectOptions |> AMap.map (fun _ v -> AsyncAVal.constant (Ok v)) - AMap.union loses wins + return AMap.union loses wins + } let allFilesToFSharpProjectOptions = - allFSharpFilesAndProjectOptions - |> AMapAsync.mapAsyncAVal (fun _filePath (_file, options) _ctok -> AsyncAVal.constant options) + asyncAVal { + let! allFSharpFilesAndProjectOptions = allFSharpFilesAndProjectOptions - let allFilesParsed = - allFSharpFilesAndProjectOptions - |> AMapAsync.mapAsyncAVal (fun _filePath (file, options: Result) _ctok -> - asyncAVal { - let! (checker: FSharpCompilerServiceChecker) = checker - and! selectProject = projectSelector + return + allFSharpFilesAndProjectOptions + |> AMapAsync.mapAsyncAVal (fun _filePath (options) _ctok -> AsyncAVal.constant options) + } - 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 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.FSharpProjectCompilerOptions + let! r = parseFile checker filePath snap + return 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 + 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 () = async { @@ -1315,6 +1494,8 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let getAllProjectOptions () = async { + let! allFilesToFSharpProjectOptions = allFilesToFSharpProjectOptions |> AsyncAVal.forceAsync + let! set = allFilesToFSharpProjectOptions |> AMap.toASetValues @@ -1328,13 +1509,15 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac return set |> Array.collect (List.toArray) } - let getAllFSharpProjectOptions () = getAllProjectOptions () - |> Async.map (Array.map (fun x -> x.FSharpProjectOptions)) + |> Async.map (Array.map (fun x -> AVal.force x.FSharpProjectCompilerOptions)) + 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?" @@ -1365,11 +1548,20 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac /// The options for the project or script. /// Determines if the typecheck should be cached for autocompletions. /// - let parseAndCheckFile (checker: FSharpCompilerServiceChecker) (file: VolatileFile) options shouldCache = - async { + let parseAndCheckFile + (checker: FSharpCompilerServiceChecker) + (file: VolatileFile) + (options: CompilerProjectOption) + shouldCache + = + asyncEx { let tags = [ SemanticConventions.fsac_sourceCodePath, box (UMX.untag file.Source.FileName) - SemanticConventions.projectFilePath, box (options.ProjectFileName) ] + SemanticConventions.projectFilePath, box (options.ProjectFileName) + "source.text", box (file.Source.String) + "source.version", box (file.Version) + + ] use _ = fsacActivitySource.StartActivityForType(thisType, tags = tags) @@ -1388,17 +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, - (file.Source.GetHashCode()), - file.Source, - 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 + ) - do! progressReport.End($"Typechecked {file.Source.FileName}") notifications.Trigger(NotificationEvent.FileParsed(file.Source.FileName), ct) @@ -1417,23 +1612,18 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac >> Log.addContextDestructured "file" file.Source.FileName ) - Async.Start( - async { - fileParsed.Trigger(parseAndCheck.GetParseResults, options, ct) - fileChecked.Trigger(parseAndCheck, file, ct) - let checkErrors = parseAndCheck.GetParseResults.Diagnostics - let parseErrors = parseAndCheck.GetCheckResults.Diagnostics + fileParsed.Trigger(parseAndCheck.GetParseResults, options, ct) + fileChecked.Trigger(parseAndCheck, file, ct) + 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) + 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) - }, - ct - ) + notifications.Trigger(NotificationEvent.ParseError(errors, file.Source.FileName, file.Version), ct) return Ok parseAndCheck @@ -1467,22 +1657,36 @@ 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 - return + 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! compilerOptions = x.FSharpProjectCompilerOptions - return! - checker.TryGetRecentCheckResultsForFile(file, opts.FSharpProjectOptions, info.Source) + let checkResults = + match compilerOptions with + | CompilerProjectOption.TransparentCompiler snap -> + checker.TryGetRecentCheckResultsForFile(sourceFilePath, snap) |> Result.ofOption (fun () -> - $"No recent typecheck results for {file}. This may be ok if the file has not been checked yet.") - } + $"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 }) let openFilesToCheckedFilesResults = @@ -1493,23 +1697,35 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let! checker = checker and! selectProject = projectSelector - return! - asyncResult { - let! projectOptions = projectOptions - let! opts = selectProject.FindProject(file, projectOptions) - let cts = getOpenFileTokenOrDefault file - use linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ctok, cts) + let options = + projectOptions |> Result.bind (fun p -> selectProject.FindProject(file, p)) - return! - parseAndCheckFile checker info opts.FSharpProjectOptions true - |> Async.withCancellation linkedCts.Token - } + + match options with + | Error e -> return Error e + | Ok x -> + let! snap = x.FSharpProjectCompilerOptions + + return! + asyncResult { + let cts = getOpenFileTokenOrDefault file + use linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ctok, cts) + + return! + parseAndCheckFile checker info snap true + |> 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 @@ -1548,7 +1764,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.FSharpProjectCompilerOptions)) let forceGetOpenFileTypeCheckResultsOrCheck file = @@ -1586,7 +1802,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 { @@ -1605,11 +1821,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 @@ -1628,7 +1851,8 @@ 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 @@ -1660,7 +1884,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 @@ -1669,37 +1893,38 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac member x.ParseFileInProject(file) = forceGetParseResults file |> Async.map (Option.ofResult) } - let getDependentProjectsOfProjects ps = - let projectSnapshot = forceLoadProjects () + let getDependentProjectsOfProjects (ps: CompilerProjectOption 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 -> - 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 -> + (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 d.FSharpProjectOptions |> ignore + if Seq.isEmpty dependents then + continueAlong <- false + currentPass.Clear() + else + for d in dependents do + allDependents.Add(AVal.force d.FSharpProjectCompilerOptions) |> 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 = @@ -1711,7 +1936,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.FSharpProjectCompilerOptions) } @@ -1719,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 -> p.FSharpProjectOptions) + return projects |> List.map (fun p -> AVal.force p.FSharpProjectCompilerOptions) } SymbolLocation.getDeclarationLocation ( @@ -1740,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(UMX.untag file, project, symbol) + | CompilerProjectOption.BackgroundCompiler opts -> + return! checker.FindReferencesForSymbolInFile(file, opts, symbol) else // untitled script files match! forceGetOpenFileTypeCheckResultsStale file with @@ -1964,7 +2193,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) @@ -1981,7 +2210,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 None cts | _ -> () textChanges.Remove filePath |> ignore) @@ -2006,7 +2235,6 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac ) } - let getDependentFilesForFile file = async { let! projects = getProjectOptionsForFile file |> AsyncAVal.forceAsync @@ -2018,34 +2246,45 @@ 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 idx = proj.SourceFiles |> Array.findIndex (fun x -> x = UMX.untag file) + let sourceFiles = proj.SourceFilesTagged + let idx = sourceFiles |> Array.findIndex (fun x -> x = file) - proj.SourceFiles + sourceFiles |> Array.splitAt idx |> snd - |> Array.map (fun sourceFile -> proj.FSharpProjectOptions, sourceFile)) + |> Array.map (fun sourceFile -> AVal.force proj.FSharpProjectCompilerOptions, sourceFile)) |> Array.distinct } - let bypassAdaptiveAndCheckDependenciesForFile (filePath: string) = - async { - let tags = [ SemanticConventions.fsac_sourceCodePath, box (UMX.untag filePath) ] + + let bypassAdaptiveAndCheckDependenciesForFile (sourceFilePath: string) = + asyncEx { + 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 = projs |> Result.toOption |> Option.defaultValue [] + let! projs = getProjectOptionsForFile sourceFilePath |> AsyncAVal.forceAsync - let dependentProjects = + let rootToken = sourceFilePath |> getOpenFileTokenOrDefault + + let projs = projs - |> List.map (fun x -> x.FSharpProjectOptions) - |> getDependentProjectsOfProjects + |> Result.toOption + |> Option.defaultValue [] + |> List.map (fun x -> AVal.force x.FSharpProjectCompilerOptions) + + let! dependentProjects = projs |> getDependentProjectsOfProjects + + let dependentProjectsAndSourceFiles = + dependentProjects + |> List.collect (fun (snap) -> snap.SourceFilesTagged |> List.map (fun sourceFile -> snap, sourceFile)) |> List.toArray - |> Array.collect (fun proj -> proj.SourceFiles |> Array.map (fun sourceFile -> proj, sourceFile)) let mutable checksCompleted = 0 @@ -2059,32 +2298,50 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let checksToPerform = let innerChecks = - Array.concat [| dependentFiles; dependentProjects |] + Array.concat [| dependentFiles; dependentProjectsAndSourceFiles |] |> Array.filter (fun (_, file) -> + let file = UMX.untag file + file.Contains "AssemblyInfo.fs" |> not && file.Contains "AssemblyAttributes.fs" |> not) let checksToPerformLength = innerChecks.Length innerChecks - |> Array.map (fun (proj, file) -> - let file = UMX.tag file + |> Array.map (fun (snap, file) -> + async { - 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) + // CancellationTokenSource.CreateLinkedTokenSource(rootToken) - bypassAdaptiveTypeCheck (file) (proj) - |> Async.withCancellation token - |> Async.Ignore - |> Async.bind (fun _ -> - async { - let checksCompleted = Interlocked.Increment(&checksCompleted) + try + 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) + + do! + progressReporter.Report( + message = $"{checksCompleted}/{checksToPerformLength} remaining", + percentage = percentage checksCompleted checksToPerformLength + ) + }) - do! - progressReporter.Report( - message = $"{checksCompleted}/{checksToPerformLength} remaining", - percentage = percentage checksCompleted checksToPerformLength - ) - })) do! @@ -2181,9 +2438,10 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac member x.ForgetDocument(filePath) = forgetDocument filePath - member x.ParseAllFiles() = parseAllFiles () |> AsyncAVal.forceAsync - - member x.GetOpenFile(filePath) = forceFindOpenFile filePath + member x.ParseAllFiles() = + parseAllFiles () + |> AsyncAVal.forceAsync + |> Async.map (Array.choose Result.toOption) member x.GetOpenFileSource(filePath) = forceFindSourceText filePath @@ -2202,7 +2460,8 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac member x.GetTypeCheckResultsForFile(filePath) = asyncResult { let! opts = forceGetProjectOptions filePath - return! x.GetTypeCheckResultsForFile(filePath, opts) + let snap = opts.FSharpProjectCompilerOptions |> AVal.force + return! x.GetTypeCheckResultsForFile(filePath, snap) } member x.GetFilesToProject() = getAllFilesToProjectOptionsSelected () diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi b/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi index 3ed4e9ebf..abb0156b2 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi @@ -30,19 +30,21 @@ type AdaptiveWorkspaceChosen = [] type LoadedProject = - { FSharpProjectOptions: FSharpProjectOptions + { ProjectOptions: Types.ProjectOptions + FSharpProjectCompilerOptions: aval LanguageVersion: LanguageVersionShim } interface IEquatable override GetHashCode: unit -> int override Equals: other: obj -> bool - member SourceFiles: string array member ProjectFileName: string - static member op_Implicit: x: LoadedProject -> FSharpProjectOptions 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,31 +53,30 @@ 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 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 GetOpenFile: filePath: string -> VolatileFile option + member ParseAllFiles: unit -> Async member GetOpenFileSource: filePath: string -> Async> member GetOpenFileOrRead: filePath: string -> Async> 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: FSharpProjectOptions -> Async> + filePath: string * opts: CompilerProjectOption -> Async> member GetTypeCheckResultsForFile: filePath: string -> Async> member GetFilesToProject: unit -> Async<(string * LoadedProject) array> member GetUsesOfSymbol: filePath: string * - opts: (string * FSharpProjectOptions) seq * + opts: (string * CompilerProjectOption) seq * symbol: FSharp.Compiler.Symbols.FSharpSymbol -> Async 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 cbd1e2fcb..477e1f6c3 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 @@ -180,16 +149,20 @@ 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 @@ -315,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 inflightEvents.TryRemove(a.Id) |> ignore } |> Async.StartImmediateAsTask diff --git a/src/FsAutoComplete/LspServers/ProjectWorkspace.fs b/src/FsAutoComplete/LspServers/ProjectWorkspace.fs new file mode 100644 index 000000000..ce83352dc --- /dev/null +++ b/src/FsAutoComplete/LspServers/ProjectWorkspace.fs @@ -0,0 +1,314 @@ +namespace FsAutoComplete.ProjectWorkspace + +open System +open FsAutoComplete.Telemetry +open FsAutoComplete.Utils.Tracing + +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 + File.openFileStreamForReadingAsync (normalizePath p.TargetPath) :> 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 { + // 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 + 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, + 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) + (sourceFilePath: string) + = + 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 + + let getSource () = + task { + let! sourceText = SourceTextFactory.readFile sourceFilePath sourceTextFactory CancellationToken.None + return sourceText :> ISourceTextNew + } + + return ProjectSnapshot.FSharpFileSnapshot.Create(file, string writeTime.Ticks, getSource) + } + + 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 + + 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>) + (project: ProjectOptions) + = + let tags = seq { "projectFileName", box project.ProjectFileName } + use _span = fsacActivitySource.StartActivityForFunc(tags = tags) + + logger.debug ( + Log.setMessage "Creating references for {projectFileName}" + >> Log.addContextDestructured "projectFileName" project.ProjectFileName + ) + + loadedProjectsA + |> AMap.filter (fun k _ -> + project.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 + 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>) + (project: ProjectOptions) + = + let normPath = Utils.normalizePath project.ProjectFileName + let tags = seq { "projectFileName", box project.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" project.ProjectFileName + ) + + snapshot + | _ -> + logger.debug ( + Log.setMessage "optionsToSnapshot - Cache miss - {projectFileName}" + >> Log.addContextDestructured "projectFileName" project.ProjectFileName + ) + + let projectName = AVal.constant project.ProjectFileName + let projectId = AVal.constant project.ProjectId + + + let sourceFiles = // alist because order matters for the F# Compiler + 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 = project.OtherOptions |> List.filter (fun x -> x.StartsWith("-r:")) + + let otherOptions = project.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 project + let isIncompleteTypeCheckEnvironment = AVal.constant false + let useScriptResolutionRules = AVal.constant false + let loadTime = AVal.constant project.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>) + (sourceTextFactory: aval) + (loadedProjectsA: amap, ProjectOptions>) + = + loadedProjectsA + |> AMap.filter (fun k _ -> (UMX.untag k).EndsWith ".fsproj") + |> 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 diff --git a/src/FsAutoComplete/LspServers/ProjectWorkspace.fsi b/src/FsAutoComplete/LspServers/ProjectWorkspace.fsi new file mode 100644 index 000000000..aa1149c23 --- /dev/null +++ b/src/FsAutoComplete/LspServers/ProjectWorkspace.fsi @@ -0,0 +1,35 @@ +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 + + + /// 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 -> + loadedProjectsA: amap, ProjectOptions> -> + amap, (ProjectOptions * aval)> diff --git a/src/FsAutoComplete/Parser.fs b/src/FsAutoComplete/Parser.fs index 0ad58284b..dd9332990 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", + "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 = 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/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..6eedc1e2d 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 @@ -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 """ @@ -3384,6 +3385,7 @@ let private removeUnnecessaryParenthesesTests state = """ ]) let tests textFactory state = + testSequenced <| testList "CodeFix-tests" [ HelpersTests.tests textFactory 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 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/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" diff --git a/test/FsAutoComplete.Tests.Lsp/EmptyFileTests.fs b/test/FsAutoComplete.Tests.Lsp/EmptyFileTests.fs index e5095e16e..a2315e7a1 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" @@ -92,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) -> 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 23a459b20..43b72bd09 100644 --- a/test/FsAutoComplete.Tests.Lsp/Helpers.fs +++ b/test/FsAutoComplete.Tests.Lsp/Helpers.fs @@ -51,26 +51,26 @@ 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 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,20 @@ 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")) - printfn "Creating directory %s" tempPath + 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 +100,27 @@ 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 + + let mutable attempts = 25 + + // Handle odd cases with windows file locking + while attempts > 0 do + try + 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 @@ -193,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 @@ -203,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 = @@ -689,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/Helpers.fsi b/test/FsAutoComplete.Tests.Lsp/Helpers.fsi index 7dc01c165..2303333d4 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 @@ -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 9e09c51e9..e32b16470 100644 --- a/test/FsAutoComplete.Tests.Lsp/Program.fs +++ b/test/FsAutoComplete.Tests.Lsp/Program.fs @@ -16,9 +16,12 @@ open System.Threading open Serilog.Filters open System.IO open FsAutoComplete +open Helpers +open FsToolkit.ErrorHandling Expect.defaultDiffPrinter <- Diff.colourisedDiff + let testTimeout = Environment.GetEnvironmentVariable "TEST_TIMEOUT_MINUTES" |> Int32.TryParse @@ -31,10 +34,21 @@ 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)) ] @@ -46,6 +60,18 @@ let sourceTextFactory: ISourceTextFactory = RoslynSourceTextFactory() let mutable toolsPath = Ionide.ProjInfo.Init.init (System.IO.DirectoryInfo Environment.CurrentDirectory) None + + +let compilers = + match getEnvVarAsStr "USE_TRANSPARENT_COMPILER" with + | Some (EqIC "TransparentCompiler") -> ["TransparentCompiler", true ] + | Some (EqIC "BackgroundCompiler") -> [ "BackgroundCompiler", false ] + | _ -> + [ + "BackgroundCompiler", false + "TransparentCompiler", true + ] + let lspTests = testList "lsp" @@ -54,56 +80,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" [ @@ -113,11 +143,35 @@ let generalTests = testList "general" [ ] [] -let tests = testList "FSAC" [ generalTests; lspTests ] - +let tests = testList "FSAC" [ + generalTests + lspTests + 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}" @@ -219,11 +273,15 @@ let main args = let fixedUpArgs = args |> Array.except argsToRemove let cts = new CancellationTokenSource(testTimeout) + use activitySource = new ActivitySource(serviceName) - let args = - [ CLIArguments.Printer(Expecto.Impl.TestPrinters.summaryWithLocationPrinter defaultConfig.printer) - CLIArguments.Verbosity logLevel - // CLIArguments.Parallel - ] - - runTestsWithCLIArgsAndCancel cts.Token args fixedUpArgs tests + let cliArgs = + [ + CLIArguments.Printer(Expecto.Impl.TestPrinters.summaryWithLocationPrinter defaultConfig.printer) + CLIArguments.Verbosity Expecto.Logging.LogLevel.Info + CLIArguments.Parallel + ] + // 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/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/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 new file mode 100644 index 000000000..01666332b --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/SnapshotTests.fs @@ -0,0 +1,384 @@ +module FsAutoComplete.Tests.SnapshotTests +open Expecto +open System.IO +open Ionide.ProjInfo +open FsAutoComplete.Utils +open FSharp.Data.Adaptive +open FsAutoComplete.Adaptive +open FsAutoComplete.ProjectWorkspace +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 +open FSharp.UMX +open System.Threading.Tasks + +module FcsRange = FSharp.Compiler.Text.Range +type FcsRange = FSharp.Compiler.Text.Range +type FcsPos = FSharp.Compiler.Text.Position + +open Helpers.Expecto.ShadowedTimeouts + + +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 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) = + [ + Console1.projectIn srcDir + Library1.projectIn srcDir + ] + +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 () + loaded + |> 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))) + ) + |> AMap.ofAVal + + loadedProjectsA + +let normalizeUntag = normalizePath >> UMX.untag + +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. + + task { + 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 _ -> + if not <| s.IsEmpty then + tcs.TrySetResult() |> ignore + ) + return! tcs.Task + } + +let snapshotTests loaders toolsPath = + + testList "ProjectWorkspace" [ + for (loaderName, workspaceLoaderFactory) in loaders do + testSequencedGroup loaderName <| + 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 + 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) + + + let snaps = + Snapshots.createSnapshots AMap.empty (AVal.constant sourceTextFactory) loadedProjectsA + + let snapshots = snaps |> 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 (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" + } + + + testCaseAsync "Cached Adaptive Snapshot - MultiProject - Updating nothing shouldn't cause recalculation" <| asyncEx { + let (loader : IWorkspaceLoader) = workspaceLoaderFactory toolsPath + let sourceTextFactory : FsAutoComplete.ISourceTextFactory = FsAutoComplete.RoslynSourceTextFactory() + use dDir = Helpers.DisposableDirectory.From Projects.MultiProjectScenario1.multiProjectScenario1Dir + 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 = + Snapshots.createSnapshots AMap.empty (AVal.constant sourceTextFactory) loadedProjectsA + + let snapshotBefore = snapsA |> AMap.mapA (fun _ (_,v) -> v) |> AMap.force + + let snapshotAfter = snapsA |> AMap.mapA (fun _ (_,v) -> v) |> AMap.force + + 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 = 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" + } + + 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() + use dDir = Helpers.DisposableDirectory.From Projects.MultiProjectScenario1.multiProjectScenario1Dir + 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 = + Snapshots.createSnapshots AMap.empty (AVal.constant sourceTextFactory) loadedProjectsA + let snaps = snapsA |> AMap.mapA (fun _ (_,v) -> v) + + + 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 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 = 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" + 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" + 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" + } + + 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() + use dDir = Helpers.DisposableDirectory.From Projects.MultiProjectScenario1.multiProjectScenario1Dir + 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 = + Snapshots.createSnapshots AMap.empty (AVal.constant sourceTextFactory) loadedProjectsA + let snaps = snapsA |> AMap.mapA (fun _ (_,v) -> v) + + + let libraryFile = Projects.MultiProjectScenario1.Library1.libraryFileIn dDir.DirectoryInfo + + let snapshotBefore = snaps |> AMap.force + let awaitOutOfDate = awaitOutOfDate snaps + do! File.WriteAllTextAsync(libraryFile.FullName, "let x = 1") + do! awaitOutOfDate + + libraryFile.Refresh() + + 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) + + 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 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 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 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 = 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 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 consoleBefore.Stamp consoleAfter.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 + ] +] 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 + + + + + + + + + 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. diff --git a/test/FsAutoComplete.Tests.Lsp/paket.references b/test/FsAutoComplete.Tests.Lsp/paket.references index d01b7b2dc..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 @@ -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