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