diff --git a/src/Compiler/Facilities/AsyncMemoize.fs b/src/Compiler/Facilities/AsyncMemoize.fs index d22093a2b4f..ce3ff5dd470 100644 --- a/src/Compiler/Facilities/AsyncMemoize.fs +++ b/src/Compiler/Facilities/AsyncMemoize.fs @@ -89,6 +89,7 @@ type internal JobEvent = | Cleared type internal ICacheKey<'TKey, 'TVersion> = + // TODO Key should probably be renamed to Identifier abstract member GetKey: unit -> 'TKey abstract member GetVersion: unit -> 'TVersion abstract member GetLabel: unit -> string diff --git a/src/Compiler/Service/FSharpProjectSnapshot.fs b/src/Compiler/Service/FSharpProjectSnapshot.fs index 2a88fcdee9a..dff2bdc3fb4 100644 --- a/src/Compiler/Service/FSharpProjectSnapshot.fs +++ b/src/Compiler/Service/FSharpProjectSnapshot.fs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. module FSharp.Compiler.CodeAnalysis.ProjectSnapshot @@ -41,10 +41,10 @@ module internal Helpers = let addFileNameAndVersion (file: IFileSnapshot) = addFileName file >> Md5Hasher.addBytes file.Version - let signatureHash projectCoreVersion (sourceFiles: IFileSnapshot seq) = + let signatureHash projectBaseVersion (sourceFiles: IFileSnapshot seq) = let mutable lastFile = "" - ((projectCoreVersion, Set.empty), sourceFiles) + ((projectBaseVersion, Set.empty), sourceFiles) ||> Seq.fold (fun (res, sigs) file -> if file.IsSignatureFile then lastFile <- file.FileName @@ -72,6 +72,13 @@ type FSharpFileSnapshot(FileName: string, Version: string, GetSource: unit -> Ta static member Create(fileName: string, version: string, getSource: unit -> Task) = FSharpFileSnapshot(fileName, version, getSource) + static member CreateFromString(filename: string, content: string) = + FSharpFileSnapshot( + filename, + Md5Hasher.hashString content |> Md5Hasher.toString, + fun () -> Task.FromResult(SourceTextNew.ofString content) + ) + static member CreateFromFileSystem(fileName: string) = FSharpFileSnapshot( fileName, @@ -171,11 +178,27 @@ type ReferenceOnDisk = { Path: string; LastModified: DateTime } /// A snapshot of an F# project. The source file type can differ based on which stage of compilation the snapshot is used for. -type internal ProjectSnapshotBase<'T when 'T :> IFileSnapshot>(projectCore: ProjectCore, sourceFiles: 'T list) = +type internal ProjectSnapshotBase<'T when 'T :> IFileSnapshot> + (projectCore: ProjectCore, referencedProjects: FSharpReferencedProjectSnapshot list, sourceFiles: 'T list) = - let noFileVersionsHash = + // Version of project without source files + let baseVersion = lazy (projectCore.Version + |> Md5Hasher.addBytes' (referencedProjects |> Seq.map _.Version)) + + let baseVersionString = lazy (baseVersion.Value |> Md5Hasher.toString) + + let baseCacheKeyWith (label, version) = + { new ICacheKey<_, _> with + member _.GetLabel() = $"{label} ({projectCore.Label})" + member _.GetKey() = projectCore.Identifier + member _.GetVersion() = baseVersionString.Value, version + } + + let noFileVersionsHash = + lazy + (baseVersion.Value |> Md5Hasher.addStrings (sourceFiles |> Seq.map (fun x -> x.FileName))) let noFileVersionsKey = @@ -191,7 +214,7 @@ type internal ProjectSnapshotBase<'T when 'T :> IFileSnapshot>(projectCore: Proj let fullHash = lazy - (projectCore.Version + (baseVersion.Value |> Md5Hasher.addStrings ( sourceFiles |> Seq.collect (fun x -> @@ -213,10 +236,10 @@ type internal ProjectSnapshotBase<'T when 'T :> IFileSnapshot>(projectCore: Proj hash |> Md5Hasher.addString file.FileName |> Md5Hasher.addBytes file.Version let signatureHash = - lazy (signatureHash projectCore.Version (sourceFiles |> Seq.map (fun x -> x :> IFileSnapshot))) + lazy (signatureHash baseVersion.Value (sourceFiles |> Seq.map (fun x -> x :> IFileSnapshot))) let signatureKey = - lazy (projectCore.CacheKeyWith("Signature", signatureHash.Value |> fst |> Md5Hasher.toString)) + lazy (baseCacheKeyWith ("Signature", signatureHash.Value |> fst |> Md5Hasher.toString)) let lastFileHash = lazy @@ -246,7 +269,7 @@ type internal ProjectSnapshotBase<'T when 'T :> IFileSnapshot>(projectCore: Proj member _.Identifier = projectCore.Identifier member _.ReferencesOnDisk = projectCore.ReferencesOnDisk member _.OtherOptions = projectCore.OtherOptions - member _.ReferencedProjects = projectCore.ReferencedProjects + member _.ReferencedProjects = referencedProjects member _.IsIncompleteTypeCheckEnvironment = projectCore.IsIncompleteTypeCheckEnvironment @@ -275,7 +298,7 @@ type internal ProjectSnapshotBase<'T when 'T :> IFileSnapshot>(projectCore: Proj |> Option.defaultWith (fun () -> failwith (sprintf "Unable to find file %s in project %s" fileName projectCore.ProjectFileName)) member private _.With(sourceFiles: 'T list) = - ProjectSnapshotBase(projectCore, sourceFiles) + ProjectSnapshotBase(projectCore, referencedProjects, sourceFiles) /// Create a new snapshot with given source files replacing files in this snapshot with the same name. Other files remain unchanged. member this.Replace(changedSourceFiles: 'T list) = @@ -352,28 +375,31 @@ type internal ProjectSnapshotBase<'T when 'T :> IFileSnapshot>(projectCore: Proj fileKey.WithExtraVersion(fileSnapshot.Version |> Md5Hasher.toString) + /// Cache key for the project without source files + member this.BaseCacheKeyWith(label, version) = baseCacheKeyWith (label, version) + /// Project snapshot with filenames and versions given as initial input and internal ProjectSnapshot = ProjectSnapshotBase /// Project snapshot with file sources loaded and internal ProjectSnapshotWithSources = ProjectSnapshotBase -/// All required information for compiling a project except the source files. It's kept separate so it can be reused +/// All required information for compiling a project except the source files and referenced projects. It's kept separate so it can be reused /// for different stages of a project snapshot and also between changes to the source files. and internal ProjectCore ( ProjectFileName: string, + OutputFileName: string option, ProjectId: string option, ReferencesOnDisk: ReferenceOnDisk list, OtherOptions: string list, - ReferencedProjects: FSharpReferencedProjectSnapshot list, IsIncompleteTypeCheckEnvironment: bool, UseScriptResolutionRules: bool, LoadTime: DateTime, UnresolvedReferences: FSharpUnresolvedReferencesSet option, OriginalLoadReferences: (range * string * string) list, Stamp: int64 option - ) as self = + ) = let hashForParsing = lazy @@ -387,16 +413,7 @@ and internal ProjectCore lazy (hashForParsing.Value |> Md5Hasher.addStrings (ReferencesOnDisk |> Seq.map (fun r -> r.Path)) - |> Md5Hasher.addDateTimes (ReferencesOnDisk |> Seq.map (fun r -> r.LastModified)) - |> Md5Hasher.addBytes' ( - ReferencedProjects - |> Seq.map (function - | FSharpReference(_name, p) -> p.ProjectSnapshot.SignatureVersion - | PEReference(getStamp, _) -> Md5Hasher.empty |> Md5Hasher.addDateTime (getStamp ()) - | ILModuleReference(_name, getStamp, _) -> Md5Hasher.empty |> Md5Hasher.addDateTime (getStamp ())) - )) - - let fullHashString = lazy (fullHash.Value |> Md5Hasher.toString) + |> Md5Hasher.addDateTimes (ReferencesOnDisk |> Seq.map (fun r -> r.LastModified))) let commandLineOptions = lazy @@ -408,21 +425,17 @@ and internal ProjectCore } |> Seq.toList) - let outputFileName = lazy (OtherOptions |> findOutputFileName) - - let key = lazy (ProjectFileName, outputFileName.Value |> Option.defaultValue "") - - let cacheKey = + let outputFileName = lazy - ({ new ICacheKey<_, _> with - member _.GetLabel() = self.Label - member _.GetKey() = self.Identifier - member _.GetVersion() = fullHashString.Value - }) + (OutputFileName + |> Option.orElseWith (fun () -> OtherOptions |> findOutputFileName)) + + let identifier = + lazy (ProjectFileName, outputFileName.Value |> Option.defaultValue "") member val ProjectDirectory = !! Path.GetDirectoryName(ProjectFileName) member _.OutputFileName = outputFileName.Value - member _.Identifier: ProjectIdentifier = key.Value + member _.Identifier: ProjectIdentifier = identifier.Value member _.Version = fullHash.Value member _.Label = ProjectFileName |> shortPath member _.VersionForParsing = hashForParsing.Value @@ -433,7 +446,7 @@ and internal ProjectCore member _.ProjectId = ProjectId member _.ReferencesOnDisk = ReferencesOnDisk member _.OtherOptions = OtherOptions - member _.ReferencedProjects = ReferencedProjects + member _.IsIncompleteTypeCheckEnvironment = IsIncompleteTypeCheckEnvironment member _.UseScriptResolutionRules = UseScriptResolutionRules member _.LoadTime = LoadTime @@ -441,22 +454,6 @@ and internal ProjectCore member _.OriginalLoadReferences = OriginalLoadReferences member _.Stamp = Stamp - member _.CacheKeyWith(label, version) = - { new ICacheKey<_, _> with - member _.GetLabel() = $"{label} ({self.Label})" - member _.GetKey() = self.Identifier - member _.GetVersion() = fullHashString.Value, version - } - - member _.CacheKeyWith(label, key, version) = - { new ICacheKey<_, _> with - member _.GetLabel() = $"{label} ({self.Label})" - member _.GetKey() = key, self.Identifier - member _.GetVersion() = fullHashString.Value, version - } - - member _.CacheKey = cacheKey.Value - and [] FSharpReferencedProjectSnapshot = /// /// A reference to an F# project. The physical data for it is stored/cached inside of the compiler service. @@ -503,6 +500,12 @@ and [ p.ProjectSnapshot.SignatureVersion + | PEReference(getStamp, _) -> Md5Hasher.empty |> Md5Hasher.addDateTime (getStamp ()) + | ILModuleReference(_name, getStamp, _) -> Md5Hasher.empty |> Md5Hasher.addDateTime (getStamp ()) + override this.Equals(o) = match o with | :? FSharpReferencedProjectSnapshot as o -> @@ -523,6 +526,17 @@ and [] FSharpProjectIdentifier = | FSharpProjectIdentifier of projectFileName: string * outputFileName: string + member this.OutputFileName = + match this with + | FSharpProjectIdentifier(_, outputFileName) -> outputFileName + + member this.ProjectFileName = + match this with + | FSharpProjectIdentifier(projectFileName, _) -> projectFileName + + override this.ToString() = + $"{shortPath this.ProjectFileName} 🡒 {shortPath this.OutputFileName}" + /// A snapshot of an F# project. This type contains all the necessary information for type checking a project. and [] FSharpProjectSnapshot internal (projectSnapshot) = @@ -549,10 +563,12 @@ and [] FSha member _.UnresolvedReferences = projectSnapshot.UnresolvedReferences member _.OriginalLoadReferences = projectSnapshot.OriginalLoadReferences member _.Stamp = projectSnapshot.Stamp + member _.OutputFileName = projectSnapshot.OutputFileName static member Create ( projectFileName: string, + outputFileName: string option, projectId: string option, sourceFiles: FSharpFileSnapshot list, referencesOnDisk: ReferenceOnDisk list, @@ -569,10 +585,10 @@ and [] FSha let projectCore = ProjectCore( projectFileName, + outputFileName, projectId, referencesOnDisk, otherOptions, - referencedProjects, isIncompleteTypeCheckEnvironment, useScriptResolutionRules, loadTime, @@ -581,7 +597,8 @@ and [] FSha stamp ) - ProjectSnapshotBase(projectCore, sourceFiles) |> FSharpProjectSnapshot + ProjectSnapshotBase(projectCore, referencedProjects, sourceFiles) + |> FSharpProjectSnapshot static member FromOptions(options: FSharpProjectOptions, getFileSnapshot, ?snapshotAccumulator) = let snapshotAccumulator = defaultArg snapshotAccumulator (Dictionary()) @@ -629,6 +646,7 @@ and [] FSha let snapshot = FSharpProjectSnapshot.Create( projectFileName = options.ProjectFileName, + outputFileName = None, projectId = options.ProjectId, sourceFiles = (sourceFiles |> List.ofArray), referencesOnDisk = (referencesOnDisk |> List.ofArray), @@ -683,7 +701,7 @@ and [] FSha let compilerArgs = File.ReadAllLines responseFile.FullName - let directoryName : string = + let directoryName: string = match responseFile.DirectoryName with | null -> failwith "Directory name of the response file is null" | str -> str @@ -720,6 +738,7 @@ and [] FSha FSharpProjectSnapshot.Create( projectFileName = projectFileName, + outputFileName = None, projectId = None, sourceFiles = (fsharpFiles |> List.map FSharpFileSnapshot.CreateFromFileSystem), referencesOnDisk = diff --git a/src/Compiler/Service/TransparentCompiler.fs b/src/Compiler/Service/TransparentCompiler.fs index 5158ac7f25c..ff66c1afaf4 100644 --- a/src/Compiler/Service/TransparentCompiler.fs +++ b/src/Compiler/Service/TransparentCompiler.fs @@ -826,8 +826,8 @@ type internal TransparentCompiler let mutable BootstrapInfoIdCounter = 0 /// Bootstrap info that does not depend source files - let ComputeBootstrapInfoStatic (projectSnapshot: ProjectCore, tcConfig: TcConfig, assemblyName: string, loadClosureOpt) = - let cacheKey = projectSnapshot.CacheKeyWith("BootstrapInfoStatic", assemblyName) + let ComputeBootstrapInfoStatic (projectSnapshot: ProjectSnapshotBase<_>, tcConfig: TcConfig, assemblyName: string, loadClosureOpt) = + let cacheKey = projectSnapshot.BaseCacheKeyWith("BootstrapInfoStatic", assemblyName) caches.BootstrapInfoStatic.Get( cacheKey, @@ -957,7 +957,7 @@ type internal TransparentCompiler let outFile, _, assemblyName = tcConfigB.DecideNames sourceFiles let! bootstrapId, tcImports, tcGlobals, initialTcInfo, importsInvalidatedByTypeProvider = - ComputeBootstrapInfoStatic(projectSnapshot.ProjectCore, tcConfig, assemblyName, loadClosureOpt) + ComputeBootstrapInfoStatic(projectSnapshot, tcConfig, assemblyName, loadClosureOpt) // Check for the existence of loaded sources and prepend them to the sources list if present. let loadedSources = @@ -1056,7 +1056,7 @@ type internal TransparentCompiler |> Seq.map (fun f -> LoadSource f isExe (f.FileName = bootstrapInfo.LastFileName)) |> MultipleDiagnosticsLoggers.Parallel - return ProjectSnapshotWithSources(projectSnapshot.ProjectCore, sources |> Array.toList) + return ProjectSnapshotWithSources(projectSnapshot.ProjectCore, projectSnapshot.ReferencedProjects, sources |> Array.toList) } @@ -1467,7 +1467,7 @@ type internal TransparentCompiler |> Seq.map (ComputeParseFile projectSnapshot tcConfig) |> MultipleDiagnosticsLoggers.Parallel - return ProjectSnapshotBase<_>(projectSnapshot.ProjectCore, parsedInputs |> Array.toList) + return ProjectSnapshotBase<_>(projectSnapshot.ProjectCore, projectSnapshot.ReferencedProjects, parsedInputs |> Array.toList) } // Type check file and all its dependencies @@ -2394,6 +2394,7 @@ type internal TransparentCompiler FSharpProjectSnapshot.Create( fileName + ".fsproj", None, + None, sourceFiles, references, otherFlags, diff --git a/src/FSharp.Compiler.LanguageServer/Common/CapabilitiesManager.fs b/src/FSharp.Compiler.LanguageServer/Common/CapabilitiesManager.fs index 0d2b42e7d05..7632a80ce62 100644 --- a/src/FSharp.Compiler.LanguageServer/Common/CapabilitiesManager.fs +++ b/src/FSharp.Compiler.LanguageServer/Common/CapabilitiesManager.fs @@ -15,8 +15,7 @@ type CapabilitiesManager(scOverrides: IServerCapabilitiesOverride seq) = TextDocumentSync = TextDocumentSyncOptions(OpenClose = true, Change = TextDocumentSyncKind.Full), DiagnosticOptions = DiagnosticOptions(WorkDoneProgress = true, InterFileDependencies = true, Identifier = "potato", WorkspaceDiagnostics = true), - CompletionProvider = - CompletionOptions(TriggerCharacters=[|"."; " "|], ResolveProvider=true, WorkDoneProgress=true), + CompletionProvider = CompletionOptions(TriggerCharacters = [| "."; " " |], ResolveProvider = true, WorkDoneProgress = true), HoverProvider = SumType(HoverOptions(WorkDoneProgress = true)) ) @@ -33,4 +32,4 @@ type CapabilitiesManager(scOverrides: IServerCapabilitiesOverride seq) = member this.GetInitializeParams() = match initializeParams with | Some params' -> params' - | None -> failwith "InitializeParams is null" \ No newline at end of file + | None -> failwith "InitializeParams is null" diff --git a/src/FSharp.Compiler.LanguageServer/Common/DependencyGraph.fs b/src/FSharp.Compiler.LanguageServer/Common/DependencyGraph.fs new file mode 100644 index 00000000000..cb528342495 --- /dev/null +++ b/src/FSharp.Compiler.LanguageServer/Common/DependencyGraph.fs @@ -0,0 +1,378 @@ +/// This Dependency Graph provides a way to maintain an up-to-date but lazy set of dependent values. +/// When changes are applied to the graph (either vertices change value or edges change), no computation is performed. +/// Only when a value is requested it is lazily computed and thereafter stored until invalidated by further changes. +module FSharp.Compiler.LanguageServer.Common.DependencyGraph + +open System.Collections.Generic + +type DependencyNode<'Identifier, 'Value> = + { + Id: 'Identifier // TODO: probably not needed + Value: 'Value option + + // TODO: optional if it's root node + Compute: 'Value seq -> 'Value + } + +let insert key value (dict: Dictionary<_, _>) = + match dict.TryGetValue key with + | true, _ -> dict[key] <- value + | false, _ -> dict.Add(key, value) + +type IDependencyGraph<'Id, 'Val when 'Id: equality> = + + abstract member AddOrUpdateNode: id: 'Id * value: 'Val -> IGraphBuilder<'Id, 'Val> + abstract member AddList: nodes: ('Id * 'Val) seq -> IGraphBuilder<'Id, 'Val> + abstract member AddOrUpdateNode: id: 'Id * dependsOn: 'Id seq * compute: ('Val seq -> 'Val) -> IGraphBuilder<'Id, 'Val> + abstract member GetValue: id: 'Id -> 'Val + abstract member GetDependenciesOf: id: 'Id -> 'Id seq + abstract member GetDependentsOf: id: 'Id -> 'Id seq + abstract member AddDependency: node: 'Id * dependsOn: 'Id -> unit + abstract member RemoveDependency: node: 'Id * noLongerDependsOn: 'Id -> unit + abstract member UpdateNode: id: 'Id * update: ('Val -> 'Val) -> unit + abstract member RemoveNode: id: 'Id -> unit + abstract member Debug_GetNodes: ('Id -> bool) -> DependencyNode<'Id, 'Val> seq + abstract member Debug_RenderMermaid : ?mapping: ('Id -> 'Id) -> string + abstract member OnWarning: (string -> unit) -> unit + +and IThreadSafeDependencyGraph<'Id, 'Val when 'Id: equality> = + inherit IDependencyGraph<'Id, 'Val> + + abstract member Transact<'a>: (IDependencyGraph<'Id, 'Val> -> 'a) -> 'a + +and IGraphBuilder<'Id, 'Val when 'Id: equality> = + + abstract member Ids: 'Id seq + abstract member AddDependentNode: 'Id * ('Val seq -> 'Val) -> IGraphBuilder<'Id, 'Val> + abstract member And: IGraphBuilder<'Id, 'Val> -> IGraphBuilder<'Id, 'Val> + +module Internal = + + type DependencyGraph<'Id, 'Val when 'Id: equality and 'Id: not null>(?graphBuilder: 'Id seq -> IGraphBuilder<_, _>) as self = + let nodes = Dictionary<'Id, DependencyNode<'Id, 'Val>>() + let dependencies = Dictionary<'Id, HashSet<'Id>>() + let dependents = Dictionary<'Id, HashSet<'Id>>() + let warningSubscribers = ResizeArray() + + let graphBuilder = defaultArg graphBuilder (fun x -> (GraphBuilder(self, x))) + + let rec invalidateDependents (id: 'Id) = + match dependents.TryGetValue id with + | true, set -> + for dependent in set do + nodes.[dependent] <- { nodes.[dependent] with Value = None } + invalidateDependents dependent + | false, _ -> () + + let invalidateNodeAndDependents id = + nodes[id] <- { nodes[id] with Value = None } + invalidateDependents id + + let addNode node = + nodes |> insert node.Id node + invalidateDependents node.Id + + member _.Debug = + {| + Nodes = nodes + Dependencies = dependencies + Dependents = dependents + |} + + member this.AddOrUpdateNode(id: 'Id, value: 'Val) = + addNode + { + Id = id + Value = Some value + Compute = (fun _ -> value) + } + + graphBuilder [ id ] + + member this.AddList(nodes: ('Id * 'Val) seq) = + nodes + |> Seq.iter (fun (id, value) -> + addNode + { + Id = id + Value = Some value + Compute = (fun _ -> value) + }) + + graphBuilder (nodes |> Seq.map fst) + + member this.AddOrUpdateNode(id: 'Id, dependsOn: 'Id seq, compute: 'Val seq -> 'Val) = + addNode + { + Id = id + Value = None + Compute = compute + } + + match dependencies.TryGetValue id with + | true, oldDependencies -> + for dep in oldDependencies do + match dependents.TryGetValue dep with + | true, set -> set.Remove id |> ignore + | _ -> () + | _ -> () + + dependencies |> insert id (HashSet dependsOn) + + for dep in dependsOn do + match dependents.TryGetValue dep with + | true, set -> set.Add id |> ignore + | false, _ -> dependents.Add(dep, HashSet([| id |])) + + graphBuilder [ id ] + + member this.GetValue(id: 'Id) = + let node = nodes[id] + + match node.Value with + | Some value -> value + | None -> + let dependencies = dependencies.[id] + let values = dependencies |> Seq.map (fun id -> this.GetValue id) + let value = node.Compute values + nodes.[id] <- { node with Value = Some value } + value + + member this.GetDependenciesOf(identifier: 'Id) = + match dependencies.TryGetValue identifier with + | true, set -> set |> Seq.map id + | false, _ -> Seq.empty + + member this.GetDependentsOf(identifier: 'Id) = + match dependents.TryGetValue identifier with + | true, set -> set |> Seq.map id + | false, _ -> Seq.empty + + member this.AddDependency(node: 'Id, dependsOn: 'Id) = + match dependencies.TryGetValue node with + | true, deps -> deps.Add dependsOn |> ignore + | false, _ -> dependencies.Add(node, HashSet([| dependsOn |])) + + match dependents.TryGetValue dependsOn with + | true, deps -> deps.Add node |> ignore + | false, _ -> dependents.Add(dependsOn, HashSet([| node |])) + + invalidateDependents dependsOn + + member this.RemoveDependency(node: 'Id, noLongerDependsOn: 'Id) = + match dependencies.TryGetValue node with + | true, deps -> deps.Remove noLongerDependsOn |> ignore + | false, _ -> () + + match dependents.TryGetValue noLongerDependsOn with + | true, deps -> deps.Remove node |> ignore + | false, _ -> () + + invalidateNodeAndDependents node + + member this.UpdateNode(id: 'Id, update: 'Val -> 'Val) = + this.GetValue id + |> update + |> fun value -> this.AddOrUpdateNode(id, value) |> ignore + + member this.RemoveNode(id: 'Id) = + + match nodes.TryGetValue id with + | true, _ -> + // Invalidate dependents of the removed node + invalidateDependents id + + // Remove the node from the nodes dictionary + nodes.Remove id |> ignore + + // Remove the node from dependencies and update dependents + match dependencies.TryGetValue id with + | true, deps -> + for dep in deps do + match dependents.TryGetValue dep with + | true, set -> set.Remove id |> ignore + | false, _ -> () + dependencies.Remove id |> ignore + | false, _ -> () + + // Remove the node from dependents and update dependencies + match dependents.TryGetValue id with + | true, deps -> + for dep in deps do + match dependencies.TryGetValue dep with + | true, set -> set.Remove id |> ignore + | false, _ -> () + dependents.Remove id |> ignore + | false, _ -> () + | false, _ -> () + + member this.Debug_GetNodes(predicate: 'Id -> bool): DependencyNode<'Id,'Val> seq = + nodes.Values |> Seq.filter (fun node -> predicate node.Id) + + member _.Debug_RenderMermaid(?mapping) = + + let mapping = defaultArg mapping id + + // We need to give each node a number so the graph is easy to render + let nodeNumbersById = Dictionary() + nodes.Keys |> Seq.map mapping |> Seq.distinct |> Seq.indexed |> Seq.iter (fun (x, y) -> nodeNumbersById.Add(y, x)) + + let content = + dependencies + |> Seq.collect (fun kv -> + let node = kv.Key + let nodeNumber = nodeNumbersById[mapping node] + + kv.Value + |> Seq.map (fun dep -> nodeNumbersById[mapping dep], mapping dep) + |> Seq.map (fun (depNumber, dep) -> $"{nodeNumber}[{node}] --> {depNumber}[{dep}]" ) + |> Seq.distinct + ) + |> String.concat "\n" + + $"```mermaid\n\ngraph LR\n\n{content}\n\n```" + + member _.OnWarning(f) = + warningSubscribers.Add f |> ignore + + interface IDependencyGraph<'Id, 'Val> with + + member this.Debug_GetNodes(predicate)= self.Debug_GetNodes(predicate) + + member _.AddOrUpdateNode(id, value) = self.AddOrUpdateNode(id, value) + member _.AddList(nodes) = self.AddList(nodes) + + member _.AddOrUpdateNode(id, dependsOn, compute) = + self.AddOrUpdateNode(id, dependsOn, compute) + + member _.GetValue(id) = self.GetValue(id) + member _.GetDependenciesOf(id) = self.GetDependenciesOf(id) + member _.GetDependentsOf(id) = self.GetDependentsOf(id) + member _.AddDependency(node, dependsOn) = self.AddDependency(node, dependsOn) + + member _.RemoveDependency(node, noLongerDependsOn) = + self.RemoveDependency(node, noLongerDependsOn) + + member _.UpdateNode(id, update) = self.UpdateNode(id, update) + member _.RemoveNode(id) = self.RemoveNode(id) + + member _.OnWarning f = self.OnWarning f + + member _.Debug_RenderMermaid(x) = self.Debug_RenderMermaid(?mapping=x) + + + and GraphBuilder<'Id, 'Val when 'Id: equality> internal (graph: IDependencyGraph<'Id, 'Val>, ids: 'Id seq) = + + interface IGraphBuilder<'Id, 'Val> with + + member _.Ids = ids + + member _.AddDependentNode(id, compute) = graph.AddOrUpdateNode(id, ids, compute) + + member _.And(graphBuilder: IGraphBuilder<'Id, 'Val>) = + GraphBuilder( + graph, + seq { + yield! ids + yield! graphBuilder.Ids + } + ) + +open Internal +open System.Runtime.CompilerServices + +type LockOperatedDependencyGraph<'Id, 'Val when 'Id: equality and 'Id: not null>() as self = + + let lockObj = System.Object() + let graph = DependencyGraph<_, _>(fun x -> GraphBuilder(self, x)) + + interface IThreadSafeDependencyGraph<'Id, 'Val> with + + member _.AddDependency(node, dependsOn) = + lock lockObj (fun () -> graph.AddDependency(node, dependsOn)) + + member _.AddList(nodes)= + lock lockObj (fun () -> graph.AddList(nodes)) + + member _.AddOrUpdateNode(id, value)= + lock lockObj (fun () -> graph.AddOrUpdateNode(id, value)) + + member _.AddOrUpdateNode(id, dependsOn, compute)= + lock lockObj (fun () -> graph.AddOrUpdateNode(id, dependsOn, compute)) + + member _.GetDependenciesOf(id) = + lock lockObj (fun () -> graph.GetDependenciesOf(id)) + + member _.GetDependentsOf(id) = + lock lockObj (fun () -> graph.GetDependentsOf(id)) + + member _.GetValue(id) = + lock lockObj (fun () -> graph.GetValue(id)) + + member _.UpdateNode(id, update) = + lock lockObj (fun () -> graph.UpdateNode(id, update)) + + member _.RemoveNode(id) = + lock lockObj (fun () -> graph.RemoveNode(id)) + + member _.RemoveDependency(node, noLongerDependsOn) = + lock lockObj (fun () -> graph.RemoveDependency(node, noLongerDependsOn)) + + member _.Transact(f) = lock lockObj (fun () -> f graph) + + member _.OnWarning(f) = lock lockObj (fun () -> graph.OnWarning f) + + member _.Debug_GetNodes(predicate) = + lock lockObj (fun () -> graph.Debug_GetNodes(predicate)) + + member _.Debug_RenderMermaid(m) = + lock lockObj (fun () -> graph.Debug_RenderMermaid(?mapping=m)) + + +[] +type GraphExtensions = + + [] + static member Unpack(node: 'NodeValue, unpacker) = + match unpacker node with + | Some value -> value + | None -> failwith $"Expected {unpacker} but got: {node}" + + [] + static member UnpackOne(dependencies: 'NodeValue seq, unpacker: 'NodeValue -> 'UnpackedDependency option) = + dependencies + |> Seq.tryExactlyOne + |> Option.bind unpacker + |> Option.defaultWith (fun () -> + failwith $"Expected exactly one dependency matching {unpacker} but got: %A{dependencies |> Seq.toArray}") + + [] + static member UnpackMany(dependencies: 'NodeValue seq, unpacker) = + let results = + dependencies + |> Seq.choose unpacker + + if dependencies |> Seq.length <> (results |> Seq.length) then + failwith $"Expected all dependencies to match {unpacker} but got: %A{dependencies |> Seq.toArray}" + + results + + [] + static member UnpackOneMany(dependencies: 'NodeValue seq, oneUnpacker, manyUnpacker) = + let mutable oneResult = None + let manyResult = new ResizeArray<_>() + let extras = new ResizeArray<_>() + + for dependency in dependencies do + match oneUnpacker dependency, manyUnpacker dependency with + | Some item, _ -> oneResult <- Some item + | None, Some item -> manyResult.Add item |> ignore + | None, None -> extras.Add dependency |> ignore + + match oneResult with + | None -> failwith $"Expected exactly one dependency matching {oneUnpacker} but didn't find any" + | Some head -> + if extras.Count > 0 then + failwith $"Found extra dependencies: %A{extras.ToArray()}" + + head, seq manyResult diff --git a/src/FSharp.Compiler.LanguageServer/Common/FSharpRequestContext.fs b/src/FSharp.Compiler.LanguageServer/Common/FSharpRequestContext.fs index db26ff46514..ee26b0aad5c 100644 --- a/src/FSharp.Compiler.LanguageServer/Common/FSharpRequestContext.fs +++ b/src/FSharp.Compiler.LanguageServer/Common/FSharpRequestContext.fs @@ -39,7 +39,10 @@ module TokenTypes = FSharpLexer.Tokenize( source, tokenCallback, - flags = (FSharpLexerFlags.Default &&& ~~~FSharpLexerFlags.Compiling &&& ~~~FSharpLexerFlags.UseLexFilter), + flags = + (FSharpLexerFlags.Default + &&& ~~~FSharpLexerFlags.Compiling + &&& ~~~FSharpLexerFlags.UseLexFilter), filePath = fileName ) @@ -71,7 +74,15 @@ module TokenTypes = | SemanticClassificationType.Printf -> SemanticTokenTypes.Keyword | _ -> SemanticTokenTypes.Comment - let toIndex (x: string) = SemanticTokenTypes.AllTypes |> Seq.findIndex (fun y -> y = x) + let toIndex (x: string) = + SemanticTokenTypes.AllTypes |> Seq.findIndex (fun y -> y = x) + +type FSharpDiagnosticReport internal (diagnostics, resultId) = + + member _.Diagnostics = diagnostics + + /// The result ID of the diagnostics. This needs to be unique for each version of the document in order to be able to clear old diagnostics. + member _.ResultId = resultId.ToString() type FSharpRequestContext(lspServices: ILspServices, logger: ILspLogger, workspace: FSharpWorkspace, checker: FSharpChecker) = member _.LspServices = lspServices @@ -81,18 +92,24 @@ type FSharpRequestContext(lspServices: ILspServices, logger: ILspLogger, workspa // TODO: split to parse and check diagnostics member _.GetDiagnosticsForFile(file: Uri) = + async { - workspace.GetSnapshotForFile file - |> Option.map (fun snapshot -> - async { - let! parseResult, checkFileAnswer = checker.ParseAndCheckFileInProject(file.LocalPath, snapshot, "LSP Get diagnostics") + let! diagnostics = + workspace.GetSnapshotForFile file + |> Option.map (fun snapshot -> + async { + let! parseResult, checkFileAnswer = + checker.ParseAndCheckFileInProject(file.LocalPath, snapshot, "LSP Get diagnostics") - return - match checkFileAnswer with - | FSharpCheckFileAnswer.Succeeded result -> result.Diagnostics - | FSharpCheckFileAnswer.Aborted -> parseResult.Diagnostics - }) - |> Option.defaultValue (async.Return [||]) + return + match checkFileAnswer with + | FSharpCheckFileAnswer.Succeeded result -> result.Diagnostics + | FSharpCheckFileAnswer.Aborted -> parseResult.Diagnostics + }) + |> Option.defaultValue (async.Return [||]) + + return FSharpDiagnosticReport(diagnostics, workspace.GetDiagnosticResultId()) + } member _.GetSemanticTokensForFile(file: Uri) = @@ -112,42 +129,66 @@ type FSharpRequestContext(lspServices: ILspServices, logger: ILspLogger, workspa |> _.GetSource() |> Async.AwaitTask - let syntacticClassifications = TokenTypes.GetSyntacticTokenTypes source file.LocalPath + let syntacticClassifications = + TokenTypes.GetSyntacticTokenTypes source file.LocalPath let lspFormatTokens = semanticClassifications |> Array.map (fun item -> (item.Range, item.Type |> TokenTypes.FSharpTokenTypeToLSP |> TokenTypes.toIndex)) - |> Array.append (syntacticClassifications|> List.map (fun (r, t) -> (r, TokenTypes.toIndex t)) |> Array.ofList) + |> Array.append ( + syntacticClassifications + |> List.map (fun (r, t) -> (r, TokenTypes.toIndex t)) + |> Array.ofList + ) |> Array.map (fun (r, tokType) -> let length = r.EndColumn - r.StartColumn // XXX Does not deal with multiline tokens? - {| startLine = r.StartLine - 1; startCol = r.StartColumn; length = length; tokType = tokType; tokMods = 0 |}) - //(startLine, startCol, length, tokType, tokMods)) + + {| + startLine = r.StartLine - 1 + startCol = r.StartColumn + length = length + tokType = tokType + tokMods = 0 + |}) + //(startLine, startCol, length, tokType, tokMods)) |> Array.sortWith (fun x1 x2 -> let c = x1.startLine.CompareTo(x2.startLine) - if c <> 0 then c - else x1.startCol.CompareTo(x2.startCol)) + if c <> 0 then c else x1.startCol.CompareTo(x2.startCol)) let tokensRelative = lspFormatTokens - |> Array.append [| {| startLine = 0; startCol = 0; length = 0; tokType = 0; tokMods = 0 |} |] + |> Array.append + [| + {| + startLine = 0 + startCol = 0 + length = 0 + tokType = 0 + tokMods = 0 + |} + |] |> Array.pairwise |> Array.map (fun (prev, this) -> {| startLine = this.startLine - prev.startLine - startCol = (if prev.startLine = this.startLine then this.startCol - prev.startCol else this.startCol) + startCol = + (if prev.startLine = this.startLine then + this.startCol - prev.startCol + else + this.startCol) length = this.length tokType = this.tokType tokMods = this.tokMods |}) - return tokensRelative - |> Array.map (fun tok -> - [| tok.startLine; tok.startCol; tok.length; tok.tokType; tok.tokMods |]) + return + tokensRelative + |> Array.map (fun tok -> [| tok.startLine; tok.startCol; tok.length; tok.tokType; tok.tokMods |]) |> Array.concat }) |> Option.defaultValue (async { return [||] }) -type ContextHolder(intialWorkspace, lspServices: ILspServices) = +type ContextHolder(workspace, lspServices: ILspServices) = let logger = lspServices.GetRequiredService() @@ -160,12 +201,10 @@ type ContextHolder(intialWorkspace, lspServices: ILspServices) = enablePartialTypeChecking = true, parallelReferenceResolution = true, captureIdentifiersWhenParsing = true, - useSyntaxTreeCache = true, useTransparentCompiler = true ) - let mutable context = - FSharpRequestContext(lspServices, logger, intialWorkspace, checker) + let mutable context = FSharpRequestContext(lspServices, logger, workspace, checker) member _.GetContext() = context @@ -185,4 +224,3 @@ type FShapRequestContextFactory(lspServices: ILspServices) = lspServices.GetRequiredService() |> _.GetContext() |> Task.FromResult - diff --git a/src/FSharp.Compiler.LanguageServer/Common/FSharpWorkspace.fs b/src/FSharp.Compiler.LanguageServer/Common/FSharpWorkspace.fs index 6fda31fc9c5..dcc5c5cd8ce 100644 --- a/src/FSharp.Compiler.LanguageServer/Common/FSharpWorkspace.fs +++ b/src/FSharp.Compiler.LanguageServer/Common/FSharpWorkspace.fs @@ -1,134 +1,354 @@ namespace FSharp.Compiler.LanguageServer.Common open FSharp.Compiler.Text +open System.Collections.Generic +open DependencyGraph +open System.IO +open System.Runtime.CompilerServices +open System.Threading +open System.Collections.Concurrent #nowarn "57" open System open System.Threading.Tasks open FSharp.Compiler.CodeAnalysis.ProjectSnapshot +open Internal.Utilities.Collections -/// Holds a project snapshot and a queue of changes that will be applied to it when it's requested -/// -/// The assumption is that this is faster than actually applying the changes to the snapshot immediately and that -/// we will be doing this on potentially every keystroke. But this should probably be measured at some point. -type SnapshotHolder(snapshot: FSharpProjectSnapshot, changedFiles: Set, openFiles: Map) = - - let applyFileChangesToSnapshot () = - let files = - changedFiles - |> Seq.map (fun filePath -> - match openFiles.TryFind filePath with - | Some content -> - FSharpFileSnapshot.Create( - filePath, - DateTime.Now.Ticks.ToString(), - fun () -> content |> SourceTextNew.ofString |> Task.FromResult - ) - | None -> FSharpFileSnapshot.CreateFromFileSystem(filePath)) - |> Seq.toList +[] +type internal WorkspaceNodeKey = + // TODO: maybe this should be URI + | SourceFile of filePath: string + | ReferenceOnDisk of filePath: string + /// All project information except source files and (in-memory) project references + | ProjectCore of FSharpProjectIdentifier + /// All project information except source files + | ProjectBase of FSharpProjectIdentifier + /// Complete project information + | ProjectSnapshot of FSharpProjectIdentifier - snapshot.Replace files - - // We don't want to mutate the workspace by applying the changes when snapshot is requested because that would force the language - // requests to be processed sequentially. So instead we keep the change application under lazy so it's still only computed if needed - // and only once and workspace doesn't change. - let appliedChanges = - lazy SnapshotHolder(applyFileChangesToSnapshot (), Set.empty, openFiles) - - member private _.snapshot = snapshot - member private _.changedFiles = changedFiles - - member private this.GetMostUpToDateInstance() = - if appliedChanges.IsValueCreated then - appliedChanges.Value - else - this - - member this.WithFileChanged(file, openFiles) = - let previous = this.GetMostUpToDateInstance() - SnapshotHolder(previous.snapshot, previous.changedFiles.Add file, openFiles) - - member this.WithoutFileChanged(file, openFiles) = - let previous = this.GetMostUpToDateInstance() - SnapshotHolder(previous.snapshot, previous.changedFiles.Remove file, openFiles) - - member _.GetSnapshot() = appliedChanges.Value.snapshot - - static member Of(snapshot: FSharpProjectSnapshot) = - SnapshotHolder(snapshot, Set.empty, Map.empty) - -type FSharpWorkspace - private - ( - projects: Map, - openFiles: Map, - fileMap: Map> - ) = - - let updateProjectsWithFile (file: Uri) f (projects: Map) = - fileMap - |> Map.tryFind file.LocalPath - |> Option.map (fun identifier -> - (projects, identifier) - ||> Seq.fold (fun projects identifier -> - let snapshotHolder = projects[identifier] - projects.Add(identifier, f snapshotHolder))) - |> Option.defaultValue projects - - member _.Projects = projects - member _.OpenFiles = openFiles - member _.FileMap = fileMap - - member this.OpenFile(file: Uri, content: string) = this.ChangeFile(file, content) - - member _.CloseFile(file: Uri) = - let openFiles = openFiles.Remove(file.LocalPath) - - FSharpWorkspace( - projects = - (projects - |> updateProjectsWithFile file _.WithoutFileChanged(file.LocalPath, openFiles)), - openFiles = openFiles, - fileMap = fileMap - ) + override this.ToString() = + match this with + | SourceFile path -> $"File {shortPath path}" + | ReferenceOnDisk path -> $"Reference on disk {shortPath path}" + | ProjectCore id -> $"ProjectCore {id}" + | ProjectBase id -> $"ProjectBase {id}" + | ProjectSnapshot id -> $"ProjectSnapshot {id}" + +[] +type internal WorkspaceNodeValue = + | SourceFile of FSharpFileSnapshot + | ReferenceOnDisk of ReferenceOnDisk + /// All project information except source files and (in-memory) project references + | ProjectCore of ProjectCore + /// All project information except source files + | ProjectBase of ProjectCore * FSharpReferencedProjectSnapshot list + /// Complete project information + | ProjectSnapshot of FSharpProjectSnapshot + +module internal WorkspaceNode = + + let projectCore value = + match value with + | WorkspaceNodeValue.ProjectCore p -> Some p + | _ -> None + + let projectSnapshot value = + match value with + | WorkspaceNodeValue.ProjectSnapshot p -> Some p + | _ -> None + + let projectBase value = + match value with + | WorkspaceNodeValue.ProjectBase(p, refs) -> Some(p, refs) + | _ -> None + + let sourceFile value = + match value with + | WorkspaceNodeValue.SourceFile f -> Some f + | _ -> None + + let referenceOnDisk value = + match value with + | WorkspaceNodeValue.ReferenceOnDisk r -> Some r + | _ -> None + + let projectCoreKey value = + match value with + | WorkspaceNodeKey.ProjectCore p -> Some p + | _ -> None + + let projectSnapshotKey value = + match value with + | WorkspaceNodeKey.ProjectSnapshot p -> Some p + | _ -> None + + let projectBaseKey value = + match value with + | WorkspaceNodeKey.ProjectBase x -> Some x + | _ -> None + + let sourceFileKey value = + match value with + | WorkspaceNodeKey.SourceFile f -> Some f + | _ -> None + + let referenceOnDiskKey value = + match value with + | WorkspaceNodeKey.ReferenceOnDisk r -> Some r + | _ -> None + +/// This type holds the current state of an F# workspace (or, solution). It's mutable but thread-safe. It accepts updates to the state and can provide immutable snapshots of contained F# projects. The state can be built up incrementally by adding projects and dependencies between them. +type FSharpWorkspace() = + + let depGraph = LockOperatedDependencyGraph() :> IThreadSafeDependencyGraph<_, _> + + /// A map from project output path to project identifier. + let outputPathMap = ConcurrentDictionary() - member _.ChangeFile(file: Uri, content: string) = + /// A map from reference on disk path to which projects depend on it. It can be used to create in-memory project references based on output paths. + let referenceMap = ConcurrentDictionary>() - // TODO: should we assert that the file is open? + /// Open files in the editor. + let openFiles = ConcurrentDictionary() - let openFiles = openFiles.Add(file.LocalPath, content) + let mutable resultIdCounter = 0 - FSharpWorkspace( - projects = - (projects - |> updateProjectsWithFile file _.WithFileChanged(file.LocalPath, openFiles)), - openFiles = openFiles, - fileMap = fileMap + member internal this.Debug = + {| + Snapshots = + depGraph.Debug_GetNodes (function + | WorkspaceNodeKey.ProjectSnapshot _ -> true + | _ -> false) + |} + + member internal this.Debug_DumpMermaid(path) = + let content = + depGraph.Debug_RenderMermaid(function + | WorkspaceNodeKey.ReferenceOnDisk _ -> WorkspaceNodeKey.ReferenceOnDisk "..." + | x -> x) + File.WriteAllText(__SOURCE_DIRECTORY__ + path, content) + + // TODO: we might need something more sophisticated eventually + // for now it's important that the result id is unique every time + // in order to be able to clear previous diagnostics + member this.GetDiagnosticResultId() = Interlocked.Increment(&resultIdCounter) + + member this.OpenFile(file: Uri, content) = + openFiles.AddOrUpdate(file.LocalPath, content, (fun _ _ -> content)) |> ignore + + // No changes in the dep graph. If we already read the contents from disk we don't want to invalidate it. + this + + member this.CloseFile(file: Uri) = + openFiles.TryRemove(file.LocalPath) |> ignore + + // The file may have had changes that weren't saved to disk and are therefore undone by closing it. + depGraph.AddOrUpdateNode( + WorkspaceNodeKey.SourceFile file.LocalPath, + WorkspaceNodeValue.SourceFile(FSharpFileSnapshot.CreateFromFileSystem(file.LocalPath)) ) + |> ignore - member _.GetSnapshotForFile(file: Uri) = - fileMap - |> Map.tryFind file.LocalPath - - // TODO: eventually we need to deal with choosing the appropriate project here - // Hopefully we will be able to do it through receiving project context from LSP - // Otherwise we have to keep track of which project/configuration is active - |> Option.bind Seq.tryHead - - |> Option.bind projects.TryFind - |> Option.map _.GetSnapshot() - - static member Create(projects: FSharpProjectSnapshot seq) = - FSharpWorkspace( - projects = Map.ofSeq (projects |> Seq.map (fun p -> p.Identifier, SnapshotHolder.Of p)), - openFiles = Map.empty, - fileMap = - (projects - |> Seq.collect (fun p -> - p.ProjectSnapshot.SourceFileNames - |> Seq.map (fun f -> Uri(f).LocalPath, p.Identifier)) - |> Seq.groupBy fst - |> Seq.map (fun (f, ps) -> f, Set.ofSeq (ps |> Seq.map snd)) - |> Map.ofSeq) + this + + member this.ChangeFile(file: Uri, content) = + + depGraph.AddOrUpdateNode( + WorkspaceNodeKey.SourceFile file.LocalPath, + WorkspaceNodeValue.SourceFile(FSharpFileSnapshot.CreateFromString(file.LocalPath, content)) ) + |> ignore + + this.OpenFile(file, content) + + /// Adds an F# project to the workspace. The project is identified path to the .fsproj file and output path. + /// The compiler arguments are used to build the project's snapshot. + /// References are created automatically between known projects based on the compiler arguments and output paths. + member _.AddCommandLineArgs(projectPath, outputPath, compilerArgs) = + + let outputPath = + outputPath + |> Option.ofObj + // TODO: maybe there are cases where it's appropriate to not have output path? + |> Option.defaultWith (fun () -> failwith "Output path can't be null for an F# project") + + let projectIdentifier = FSharpProjectIdentifier(projectPath, outputPath) + + // Add the project identifier to the map + outputPathMap.AddOrUpdate(outputPath, (fun _ -> projectIdentifier), (fun _ _ -> projectIdentifier)) + |> ignore + + let directoryPath = Path.GetDirectoryName(projectPath) + + let fsharpFileExtensions = set [| ".fs"; ".fsi"; ".fsx" |] + + let isFSharpFile (file: string) = + Set.exists (fun (ext: string) -> file.EndsWith(ext, StringComparison.Ordinal)) fsharpFileExtensions + + let isReference: string -> bool = _.StartsWith("-r:") + + let referencesOnDisk = + compilerArgs + |> Seq.filter isReference + |> Seq.map _.Substring(3) + |> Seq.map (fun path -> + + referenceMap.AddOrUpdate( + path, + (fun _ -> Set.singleton projectIdentifier), + (fun _ existing -> Set.add projectIdentifier existing) + ) + |> ignore + + { + Path = path + LastModified = File.GetLastWriteTimeUtc path + }) + |> Seq.toList + + let projectReferences = + referencesOnDisk + |> Seq.choose (fun ref -> + match outputPathMap.TryGetValue ref.Path with + | true, projectIdentifier -> Some projectIdentifier + | _ -> None) + |> Set + + let otherOptions = + compilerArgs + |> Seq.filter (not << isReference) + |> Seq.filter (not << isFSharpFile) + |> Seq.toList + + depGraph.Transact(fun depGraph -> + + let fsharpFiles = + compilerArgs + |> Seq.choose (fun (line: string) -> + if not (isFSharpFile line) then + None + else + let fullPath = Path.Combine(directoryPath, line) + if not (File.Exists fullPath) then None else Some fullPath) + |> Seq.map (fun path -> + WorkspaceNodeKey.SourceFile path, + WorkspaceNodeValue.SourceFile( + match openFiles.TryGetValue(path) with + | true, content -> FSharpFileSnapshot.CreateFromString(path, content) + | false, _ -> FSharpFileSnapshot.CreateFromFileSystem path + )) + |> depGraph.AddList + + let referencesOnDiskNodes = + referencesOnDisk + |> Seq.map (fun r -> WorkspaceNodeKey.ReferenceOnDisk r.Path, WorkspaceNodeValue.ReferenceOnDisk r) + |> depGraph.AddList + + let projectCore = + referencesOnDiskNodes.AddDependentNode( + WorkspaceNodeKey.ProjectCore projectIdentifier, + (fun deps -> + let refsOnDisk = deps.UnpackMany WorkspaceNode.referenceOnDisk |> Seq.toList + + ProjectCore( + ProjectFileName = projectPath, + OutputFileName = Some outputPath, + ProjectId = None, + ReferencesOnDisk = refsOnDisk, + OtherOptions = otherOptions, + IsIncompleteTypeCheckEnvironment = false, + UseScriptResolutionRules = false, + LoadTime = DateTime.Now, + UnresolvedReferences = None, + OriginalLoadReferences = [], + Stamp = None + ) + + |> WorkspaceNodeValue.ProjectCore) + ) + + let projectBase = + projectCore.AddDependentNode( + WorkspaceNodeKey.ProjectBase projectIdentifier, + (fun deps -> + let projectCore, referencedProjects = + deps.UnpackOneMany(WorkspaceNode.projectCore, WorkspaceNode.projectSnapshot) + + let referencedProjects = + referencedProjects + |> Seq.map (fun s -> + FSharpReferencedProjectSnapshot.FSharpReference( + s.OutputFileName + |> Option.defaultWith (fun () -> failwith "project doesn't have output filename"), + s + )) + |> Seq.toList + + WorkspaceNodeValue.ProjectBase(projectCore, referencedProjects)) + ) + + // In case this is an update, we should check for any existing project references that are not contained in the incoming compiler args and remove them + let existingReferences = + depGraph.GetDependenciesOf(WorkspaceNodeKey.ProjectBase projectIdentifier) + |> Seq.choose (function + | WorkspaceNodeKey.ProjectSnapshot projectId -> Some projectId + | _ -> None) + |> Set + + let referencesToRemove = existingReferences - projectReferences + let referencesToAdd = projectReferences - existingReferences + + for projectId in referencesToRemove do + depGraph.RemoveDependency( + WorkspaceNodeKey.ProjectBase projectIdentifier, + noLongerDependsOn = WorkspaceNodeKey.ProjectSnapshot projectId + ) + + for projectId in referencesToAdd do + depGraph.AddDependency( + WorkspaceNodeKey.ProjectBase projectIdentifier, + dependsOn = WorkspaceNodeKey.ProjectSnapshot projectId + ) + + projectBase + .And(fsharpFiles) + .AddDependentNode( + WorkspaceNodeKey.ProjectSnapshot projectIdentifier, + (fun deps -> + + let (projectCore, referencedProjects), sourceFiles = + deps.UnpackOneMany(WorkspaceNode.projectBase, WorkspaceNode.sourceFile) + + ProjectSnapshot(projectCore, referencedProjects, sourceFiles |> Seq.toList) + |> FSharpProjectSnapshot + |> WorkspaceNodeValue.ProjectSnapshot) + ) + |> ignore + + // Check if any projects we know about depend on this project and add the references if they don't already exist + let dependentProjectIds = + depGraph + .GetDependentsOf(WorkspaceNodeKey.ReferenceOnDisk outputPath) + .UnpackMany(WorkspaceNode.projectCoreKey) + + for dependentProjectId in dependentProjectIds do + depGraph.AddDependency( + WorkspaceNodeKey.ProjectBase dependentProjectId, + dependsOn = WorkspaceNodeKey.ProjectSnapshot projectIdentifier + ) + + projectIdentifier) + + member _.GetSnapshotForFile(file: Uri) = + depGraph.Transact(fun depGraph -> + + depGraph.GetDependentsOf(WorkspaceNodeKey.SourceFile file.LocalPath) + + // TODO: eventually we need to deal with choosing the appropriate project here + // Hopefully we will be able to do it through receiving project context from LSP + // Otherwise we have to keep track of which project/configuration is active + |> Seq.tryHead // For now just get the first one + + |> Option.map depGraph.GetValue + |> Option.map _.Unpack(WorkspaceNode.projectSnapshot)) diff --git a/src/FSharp.Compiler.LanguageServer/Common/LifecycleManager.fs b/src/FSharp.Compiler.LanguageServer/Common/LifecycleManager.fs index 14c6c4382ea..cd033fc3d2f 100644 --- a/src/FSharp.Compiler.LanguageServer/Common/LifecycleManager.fs +++ b/src/FSharp.Compiler.LanguageServer/Common/LifecycleManager.fs @@ -27,7 +27,7 @@ type FSharpLspServices(serviceCollection: IServiceCollection) as this = let serviceProvider = serviceCollection.BuildServiceProvider() interface ILspServices with - member this.GetRequiredService<'T>() : 'T = + member this.GetRequiredService<'T when 'T: not null>() : 'T = serviceProvider.GetRequiredService<'T>() member this.TryGetService(t) = serviceProvider.GetService(t) diff --git a/src/FSharp.Compiler.LanguageServer/FSharp.Compiler.LanguageServer.fsproj b/src/FSharp.Compiler.LanguageServer/FSharp.Compiler.LanguageServer.fsproj index 5a714ffd86e..44ce89215eb 100644 --- a/src/FSharp.Compiler.LanguageServer/FSharp.Compiler.LanguageServer.fsproj +++ b/src/FSharp.Compiler.LanguageServer/FSharp.Compiler.LanguageServer.fsproj @@ -3,6 +3,8 @@ Exe net8.0 + true + @@ -13,6 +15,10 @@ + + + + @@ -27,6 +33,7 @@ + diff --git a/src/FSharp.Compiler.LanguageServer/FSharpLanguageServer.fs b/src/FSharp.Compiler.LanguageServer/FSharpLanguageServer.fs index 70770760b13..71e6a148e3d 100644 --- a/src/FSharp.Compiler.LanguageServer/FSharpLanguageServer.fs +++ b/src/FSharp.Compiler.LanguageServer/FSharpLanguageServer.fs @@ -30,7 +30,7 @@ type FSharpLanguageServer (jsonRpc: JsonRpc, logger: ILspLogger, ?initialWorkspace: FSharpWorkspace, ?addExtraHandlers: Action) = inherit AbstractLanguageServer(jsonRpc, logger) - let initialWorkspace = defaultArg initialWorkspace (FSharpWorkspace.Create []) + let initialWorkspace = defaultArg initialWorkspace (FSharpWorkspace()) do // This spins up the queue and ensure the LSP is ready to start receiving requests @@ -67,7 +67,7 @@ type FSharpLanguageServer lspServices :> ILspServices static member Create() = - FSharpLanguageServer.Create(FSharpWorkspace.Create Seq.empty, (fun _ -> ())) + FSharpLanguageServer.Create(FSharpWorkspace(), (fun _ -> ())) static member Create(initialWorkspace, addExtraHandlers: Action) = FSharpLanguageServer.Create(LspLogger System.Diagnostics.Trace.TraceInformation, initialWorkspace, addExtraHandlers) diff --git a/src/FSharp.Compiler.LanguageServer/Handlers/LanguageFeaturesHandler.fs b/src/FSharp.Compiler.LanguageServer/Handlers/LanguageFeaturesHandler.fs index 1609cdc06cc..d900fdfc3b8 100644 --- a/src/FSharp.Compiler.LanguageServer/Handlers/LanguageFeaturesHandler.fs +++ b/src/FSharp.Compiler.LanguageServer/Handlers/LanguageFeaturesHandler.fs @@ -19,4 +19,4 @@ type LanguageFeaturesHandler() = context: FSharpRequestContext, cancellationToken: CancellationToken ) = - Task.FromResult(new RelatedUnchangedDocumentDiagnosticReport()) \ No newline at end of file + Task.FromResult(new RelatedUnchangedDocumentDiagnosticReport()) diff --git a/src/FSharp.VisualStudio.Extension/FSharpLanguageServerProvider.cs b/src/FSharp.VisualStudio.Extension/FSharpLanguageServerProvider.cs index ace7d25343e..170e54fa1e7 100644 --- a/src/FSharp.VisualStudio.Extension/FSharpLanguageServerProvider.cs +++ b/src/FSharp.VisualStudio.Extension/FSharpLanguageServerProvider.cs @@ -7,6 +7,7 @@ namespace FSharp.VisualStudio.Extension; using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.IO.Packaging; using System.IO.Pipelines; using System.Linq; using System.Threading; @@ -82,8 +83,8 @@ public ServerCapabilities OverrideServerCapabilities(ServerCapabilities value) Range = false }, HoverProvider = new HoverOptions() - { - WorkDoneProgress = true + { + WorkDoneProgress = true } }; return capabilities; @@ -117,15 +118,16 @@ internal class VsDiagnosticsHandler [LanguageServerEndpoint(VSInternalMethods.DocumentPullDiagnosticName)] public async Task HandleRequestAsync(VSInternalDocumentDiagnosticsParams request, FSharpRequestContext context, CancellationToken cancellationToken) { - var result = await context.GetDiagnosticsForFile(request!.TextDocument!.Uri).Please(cancellationToken); + var report = await context.GetDiagnosticsForFile(request!.TextDocument!.Uri).Please(cancellationToken); - var rep = new VSInternalDiagnosticReport + var vsReport = new VSInternalDiagnosticReport { - ResultId = "potato1", // Has to be present for diagnostic to show up - //Identifier = 69, + ResultId = report.ResultId, + //Identifier = 1, //Version = 1, + Diagnostics = - result.Select(d => + report.Diagnostics.Select(d => new Diagnostic { @@ -143,7 +145,7 @@ public async Task HandleRequestAsync(VSInternalDoc ).ToArray() }; - return [rep]; + return [vsReport]; } [LanguageServerEndpoint("textDocument/_vs_getProjectContexts")] @@ -171,6 +173,108 @@ public Task HandleRequestAsync(VSGetProjectContextsParams } +internal class SolutionObserver : IObserver> +{ + public void OnCompleted() + { + + } + + public void OnError(Exception error) + { + } + + public void OnNext(IQueryResults value) + { + Trace.TraceInformation("Solution was updated"); + } + +} + +internal class ProjectObserver(FSharpWorkspace workspace) : IObserver> +{ + private readonly FSharpWorkspace workspace = workspace; + + internal void ProcessProject(IProjectSnapshot project) + { + project.Id.TryGetValue("ProjectPath", out var projectPath); + + List<(string?, string)> projectInfos = []; + + if (projectPath != null && projectPath.ToLower().EndsWith(".fsproj")) + { + var configs = project.ActiveConfigurations.ToList(); + + foreach (var config in configs) + { + if (config != null) + { + // Extract bin output path for each active config + var data = config.OutputGroups; + + string? outputPath = null; + foreach (var group in data) + { + if (group.Name == "Built") + { + foreach (var output in group.Outputs) + { + if (output.FinalOutputPath != null && (output.FinalOutputPath.ToLower().EndsWith(".dll") || output.FinalOutputPath.ToLower().EndsWith(".exe"))) + { + outputPath = output.FinalOutputPath; + break; + } + } + if (outputPath != null) + { + break; + } + } + } + + foreach (var ruleResults in config.RuleResults) + { + // XXX Idk why `.Where` does not work with these IAsyncQuerable type + if (ruleResults?.RuleName == "CompilerCommandLineArgs") + { + // XXX Not sure why there would be more than one item for this rule result + // Taking first one, ignoring the rest + var args = ruleResults?.Items?.FirstOrDefault()?.Name; + if (args != null) projectInfos.Add((outputPath, args)); + } + } + } + } + + foreach (var projectInfo in projectInfos) + { + workspace.AddCommandLineArgs(projectPath, projectInfo.Item1, projectInfo.Item2.Split(';')); + } + + workspace.Debug_DumpMermaid("../../../../dep-graph.md"); + + + } + } + + public void OnNext(IQueryResults result) + { + foreach (var project in result) + { + this.ProcessProject(project); + } + } + + public void OnCompleted() + { + } + + public void OnError(Exception error) + { + } +} + + [VisualStudioContribution] internal class FSharpLanguageServerProvider : LanguageServerProvider { @@ -194,76 +298,37 @@ internal class FSharpLanguageServerProvider : LanguageServerProvider { var ws = this.Extensibility.Workspaces(); - IQueryResults? result = await ws.QueryProjectsAsync(project => project + var projectQuery = (IAsyncQueryable project) => project .With(p => p.ActiveConfigurations + .With(c => c.ConfigurationDimensions.With(d => d.Name).With(d => d.Value)) + .With(c => c.Properties.With(p => p.Name).With(p => p.Value)) + .With(c => c.OutputGroups.With(g => g.Name).With(g => g.Outputs.With(o => o.Name).With(o => o.FinalOutputPath).With(o => o.RootRelativeURL))) .With(c => c.RuleResultsByRuleName("CompilerCommandLineArgs") .With(r => r.RuleName) .With(r => r.Items))) - .With(p => new { p.ActiveConfigurations, p.Id, p.Guid }), cancellationToken); + .With(p => p.ProjectReferences + .With(r => r.ReferencedProjectPath) + .With(r => r.CanonicalName) + .With(r => r.Id) + .With(r => r.Name) + .With(r => r.ProjectGuid) + .With(r => r.ReferencedProjectId) + .With(r => r.ReferenceType)); + IQueryResults? result = await ws.QueryProjectsAsync(p => projectQuery(p).With(p => new { p.ActiveConfigurations, p.Id, p.Guid }), cancellationToken); + + var workspace = new FSharpWorkspace(); - List<(string, string)> projectsAndCommandLineArgs = []; foreach (var project in result) { - project.Id.TryGetValue("ProjectPath", out var projectPath); + var observer = new ProjectObserver(workspace); - List commandLineArgs = []; - if (projectPath != null) - { - // There can be multiple Active Configurations, e.g. one for net8.0 and one for net472 - // TODO For now taking any single one of them, but we might actually want to pick specific one - var config = project.ActiveConfigurations.FirstOrDefault(); - if (config != null) - { - foreach (var ruleResults in config.RuleResults) - { - // XXX Idk why `.Where` does not work with these IAsyncQuerable type - if (ruleResults?.RuleName == "CompilerCommandLineArgs") - { - // XXX Not sure why there would be more than one item for this rule result - // Taking first one, ignoring the rest - var args = ruleResults?.Items?.FirstOrDefault()?.Name; - if (args != null) commandLineArgs.Add(args); - } - } - } - if (commandLineArgs.Count > 0) - { - projectsAndCommandLineArgs.Add((projectPath, commandLineArgs[0])); - } - } + await projectQuery(project.AsQueryable()).SubscribeAsync(observer, CancellationToken.None); - try - { - this.ProcessProject(project); - } - catch (Exception ex) - { - Debug.WriteLine(ex); - } + // TODO: should we do this, or are we guaranteed it will get processed? + // observer.ProcessProject(project); } - FSharpWorkspace workspace; - - try - { - List snapshots = []; - foreach(var args in projectsAndCommandLineArgs) - { - var lines = args.Item2.Split(';'); // XXX Probably not robust enough - var path = args.Item1; - - string directoryPath = Path.GetDirectoryName(path) ?? throw new Exception("Directory path should not be null"); - var snapshot = FSharpProjectSnapshot.FromCommandLineArgs( - lines, directoryPath, Path.GetFileName(path)); - snapshots.Add(snapshot); - } - workspace = FSharpWorkspace.Create(snapshots); - } - catch - { - workspace = FSharpWorkspace.Create([]); - } var ((clientStream, serverStream), _server) = FSharpLanguageServer.Create(workspace, (serviceCollection) => { @@ -272,41 +337,25 @@ internal class FSharpLanguageServerProvider : LanguageServerProvider serviceCollection.AddSingleton(); }); - return new DuplexPipe( - PipeReader.Create(clientStream), - PipeWriter.Create(serverStream)); - } + var solutions = await ws.QuerySolutionAsync( + solution => solution.With(solution => solution.FileName), + cancellationToken); - private void ProcessProject(IProjectSnapshot project) - { - List>? files = project.Files.Please(); - var references = project.ProjectReferences.Please(); + var singleSolution = solutions.FirstOrDefault(); - var properties = project.Properties.Please(); - var id = project.Id; - - var configurationDimensions = project.ConfigurationDimensions.Please(); - var configurations = project.Configurations.Please(); - - foreach (var configuration in configurations) + if (singleSolution != null) { - this.ProcessConfiguration(configuration.Value); + var unsubscriber = await singleSolution + .AsQueryable() + .With(p => p.Projects.With(p => p.Files)) + .SubscribeAsync(new SolutionObserver(), CancellationToken.None); } - } - private void ProcessConfiguration(IProjectConfigurationSnapshot configuration) - { - var properties = configuration.Properties.Please(); - var packageReferences = configuration.PackageReferences.Please(); - var assemblyReferences = configuration.AssemblyReferences.Please(); - var refNames = assemblyReferences.Select(r => r.Value.Name).ToList(); - var dimensions = configuration.ConfigurationDimensions.Please(); - var outputGroups = configuration.OutputGroups.Please(); - var buildProperties = configuration.BuildProperties.Please(); - var buildPropDictionary = buildProperties.Select(p => (p.Value.Name, p.Value.Value)).ToList(); - return; - } + return new DuplexPipe( + PipeReader.Create(clientStream), + PipeWriter.Create(serverStream)); + } /// public override Task OnServerInitializationResultAsync(ServerInitializationResult serverInitializationResult, LanguageServerInitializationFailureInfo? initializationFailureInfo, CancellationToken cancellationToken) diff --git a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/TransparentCompiler.fs b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/TransparentCompiler.fs index 0395a421895..d43c3e35034 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/TransparentCompiler.fs +++ b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/TransparentCompiler.fs @@ -29,7 +29,7 @@ open OpenTelemetry.Trace #nowarn "57" -[] +[] let ``Use Transparent Compiler`` () = let size = 20 @@ -59,7 +59,7 @@ let ``Use Transparent Compiler`` () = checkFile last expectSignatureChanged } -[] +[] let ``Parallel processing`` () = let project = SyntheticProject.Create( @@ -77,7 +77,7 @@ let ``Parallel processing`` () = checkFile "E" expectSignatureChanged } -[] +[] let ``Parallel processing with signatures`` () = let project = SyntheticProject.Create( @@ -112,7 +112,7 @@ let makeTestProject () = let testWorkflow () = ProjectWorkflowBuilder(makeTestProject(), useTransparentCompiler = true) -[] +[] let ``Edit file, check it, then check dependent file`` () = testWorkflow() { updateFile "First" breakDependentFiles @@ -120,21 +120,21 @@ let ``Edit file, check it, then check dependent file`` () = checkFile "Second" expectErrors } -[] +[] let ``Edit file, don't check it, check dependent file`` () = testWorkflow() { updateFile "First" breakDependentFiles checkFile "Second" expectErrors } -[] +[] let ``Check transitive dependency`` () = testWorkflow() { updateFile "First" breakDependentFiles checkFile "Last" expectSignatureChanged } -[] +[] let ``Change multiple files at once`` () = testWorkflow() { updateFile "First" (setPublicVersion 2) @@ -143,7 +143,7 @@ let ``Change multiple files at once`` () = checkFile "Last" (expectSignatureContains "val f: x: 'a -> (ModuleFirst.TFirstV_2<'a> * ModuleSecond.TSecondV_2<'a>) * (ModuleFirst.TFirstV_2<'a> * ModuleThird.TThirdV_2<'a>) * TLastV_1<'a>") } -[] +[] let ``Files depend on signature file if present`` () = let project = makeTestProject() |> updateFile "First" addSignatureFile @@ -153,7 +153,7 @@ let ``Files depend on signature file if present`` () = checkFile "Second" expectNoChanges } -[] +[] let ``Project with signatures`` () = let project = SyntheticProject.Create( @@ -168,7 +168,7 @@ let ``Project with signatures`` () = checkFile "Second" expectOk } -[] +[] let ``Signature update`` () = let project = SyntheticProject.Create( @@ -184,7 +184,7 @@ let ``Signature update`` () = checkFile "Second" expectSignatureChanged } -[] +[] let ``Adding a file`` () = testWorkflow() { addFileAbove "Second" (sourceFile "New" []) @@ -192,14 +192,14 @@ let ``Adding a file`` () = checkFile "Last" (expectSignatureContains "val f: x: 'a -> (ModuleFirst.TFirstV_1<'a> * ModuleNew.TNewV_1<'a> * ModuleSecond.TSecondV_1<'a>) * (ModuleFirst.TFirstV_1<'a> * ModuleThird.TThirdV_1<'a>) * TLastV_1<'a>") } -[] +[] let ``Removing a file`` () = testWorkflow() { removeFile "Second" checkFile "Last" expectErrors } -[] +[] let ``Changes in a referenced project`` () = let library = SyntheticProject.Create("library", sourceFile "Library" []) @@ -218,7 +218,7 @@ let ``Changes in a referenced project`` () = } -[] +[] let ``File is not checked twice`` () = let cacheEvents = ConcurrentQueue() @@ -242,7 +242,7 @@ let ``File is not checked twice`` () = Assert.Equal([Weakened; Requested; Started; Finished], intermediateTypeChecks["FileFirst.fs"]) Assert.Equal([Weakened; Requested; Started; Finished], intermediateTypeChecks["FileThird.fs"]) -[] +[] let ``If a file is checked as a dependency it's not re-checked later`` () = let cacheEvents = ConcurrentQueue() @@ -266,7 +266,7 @@ let ``If a file is checked as a dependency it's not re-checked later`` () = Assert.Equal([Weakened; Requested; Started; Finished; Requested], intermediateTypeChecks["FileThird.fs"]) -// [] TODO: differentiate complete and minimal checking requests +// [([Started; Finished], intermediateTypeChecks["FileThird.fs"]) Assert.False (intermediateTypeChecks.ContainsKey "FileSecond.fs") -// [] TODO: differentiate complete and minimal checking requests +// [([], intermediateTypeChecks |> Map.toList) -// [] TODO: differentiate complete and minimal checking requests +// [(["FileE.fs", [Started; Finished]], graphConstructions) Assert.Equal(["FileE.fs", [Started; Finished]], intermediateTypeChecks) -[] +[] let ``Changing impl files doesn't invalidate cache when they have signatures`` () = let project = SyntheticProject.Create( { sourceFile "A" [] with SignatureFile = AutoGenerated }, @@ -406,7 +406,7 @@ let ``Changing impl files doesn't invalidate cache when they have signatures`` ( Assert.Equal([], intermediateTypeChecks) -[] +[] let ``Changing impl file doesn't invalidate an in-memory referenced project`` () = let library = SyntheticProject.Create("library", { sourceFile "A" [] with SignatureFile = AutoGenerated }) @@ -789,7 +789,7 @@ module Stuff = let fileName, snapshot, checker = singleFileChecker source checker.ParseFile(fileName, snapshot) |> Async.RunSynchronously - //[] + //[] let ``Hash stays the same when whitespace changes`` () = //let parseResult = getParseResult source @@ -845,7 +845,7 @@ let ``TypeCheck last file in project with transparent compiler`` useTransparentC checkFile lastFile expectOk } -[] +[] let ``LoadClosure for script is computed once`` () = let project = SyntheticProject.CreateForScript( sourceFile "First" []) @@ -870,7 +870,7 @@ let ``LoadClosure for script is computed once`` () = Assert.Empty(closureComputations) -[] +[] let ``LoadClosure for script is recomputed after changes`` () = let project = SyntheticProject.CreateForScript( sourceFile "First" []) @@ -899,7 +899,7 @@ let ``LoadClosure for script is recomputed after changes`` () = Assert.Equal([Weakened; Requested; Started; Finished; Weakened; Requested; Started; Finished], closureComputations["FileFirst.fs"]) -[] +[] let ``TryGetRecentCheckResultsForFile returns None before first call to ParseAndCheckFileInProject`` () = let project = SyntheticProject.Create( sourceFile "First" []) @@ -909,7 +909,7 @@ let ``TryGetRecentCheckResultsForFile returns None before first call to ParseAnd tryGetRecentCheckResults "First" expectNone } |> ignore -[] +[] let ``TryGetRecentCheckResultsForFile returns result after first call to ParseAndCheckFileInProject`` () = let project = SyntheticProject.Create( sourceFile "First" [] ) @@ -918,7 +918,7 @@ let ``TryGetRecentCheckResultsForFile returns result after first call to ParseAn tryGetRecentCheckResults "First" expectSome } |> ignore -[] +[] let ``TryGetRecentCheckResultsForFile returns no result after edit`` () = let project = SyntheticProject.Create( sourceFile "First" []) @@ -931,7 +931,7 @@ let ``TryGetRecentCheckResultsForFile returns no result after edit`` () = tryGetRecentCheckResults "First" expectSome } |> ignore -[] +[] let ``TryGetRecentCheckResultsForFile returns result after edit of other file`` () = let project = SyntheticProject.Create( sourceFile "First" [], @@ -945,16 +945,18 @@ let ``TryGetRecentCheckResultsForFile returns result after edit of other file`` tryGetRecentCheckResults "Second" expectSome // file didn't change so we still want to get the recent result } |> ignore -[] -let ``Background compiler and Transparent compiler return the same options`` () = +[] +[] +[] +let ``Background compiler and Transparent compiler return the same options`` assumeDotNetFramework = async { let backgroundChecker = FSharpChecker.Create(useTransparentCompiler = false) let transparentChecker = FSharpChecker.Create(useTransparentCompiler = true) let scriptName = Path.Combine(__SOURCE_DIRECTORY__, "script.fsx") let content = SourceTextNew.ofString "" - let! backgroundSnapshot, backgroundDiags = backgroundChecker.GetProjectSnapshotFromScript(scriptName, content) - let! transparentSnapshot, transparentDiags = transparentChecker.GetProjectSnapshotFromScript(scriptName, content) + let! backgroundSnapshot, backgroundDiags = backgroundChecker.GetProjectSnapshotFromScript(scriptName, content, assumeDotNetFramework=assumeDotNetFramework) + let! transparentSnapshot, transparentDiags = transparentChecker.GetProjectSnapshotFromScript(scriptName, content, assumeDotNetFramework=assumeDotNetFramework) Assert.Empty(backgroundDiags) Assert.Empty(transparentDiags) Assert.Equal(backgroundSnapshot.OtherOptions, transparentSnapshot.OtherOptions) @@ -1012,7 +1014,7 @@ printfn "Hello from F#" checkFile "As 01" expectTwoWarnings } -[] +[] let ``Transparent Compiler ScriptClosure cache is populated after GetProjectOptionsFromScript`` () = async { let transparentChecker = FSharpChecker.Create(useTransparentCompiler = true) diff --git a/tests/FSharp.Compiler.LanguageServer.Tests/DependencyGraphTests.fs b/tests/FSharp.Compiler.LanguageServer.Tests/DependencyGraphTests.fs new file mode 100644 index 00000000000..a9402e4d517 --- /dev/null +++ b/tests/FSharp.Compiler.LanguageServer.Tests/DependencyGraphTests.fs @@ -0,0 +1,82 @@ +module DependencyGraphTests + +open FSharp.Compiler.LanguageServer.Common.DependencyGraph.Internal +open Xunit + +[] +let ``Can add a node to the graph`` () = + let graph = DependencyGraph() + graph.AddOrUpdateNode(1, 1) |> ignore + Assert.Equal(1, graph.GetValue(1)) + +[] +let ``Can add a node with dependencies to the graph`` () = + let graph = DependencyGraph() + graph.AddOrUpdateNode(1, 1) + .AddDependentNode(2, fun deps -> deps |> Seq.sum |> (+) 1) + .AddDependentNode(3, fun deps -> deps |> Seq.sum |> (+) 1) |> ignore + graph.AddOrUpdateNode(4, [1; 3], fun deps -> deps |> Seq.sum |> (+) 1) |> ignore + Assert.Equal(2, graph.GetValue(2)) + Assert.Equal(3, graph.GetValue(3)) + Assert.Equal(5, graph.GetValue(4)) + +[] +let ``Can update a value`` () = + let graph = DependencyGraph() + graph.AddOrUpdateNode(1, 1) + .AddDependentNode(2, fun deps -> deps |> Seq.sum |> (+) 1) + .AddDependentNode(3, fun deps -> deps |> Seq.sum |> (+) 1) |> ignore + graph.AddOrUpdateNode(4, [1; 3], fun deps -> deps |> Seq.sum |> (+) 1) |> ignore + graph.AddOrUpdateNode(1, 2) |> ignore + + // Values were invalidated + Assert.Equal(None, graph.Debug.Nodes[2].Value) + Assert.Equal(None, graph.Debug.Nodes[3].Value) + Assert.Equal(None, graph.Debug.Nodes[4].Value) + + Assert.Equal(7, graph.GetValue(4)) + Assert.Equal(Some 3, graph.Debug.Nodes[2].Value) + Assert.Equal(Some 4, graph.Debug.Nodes[3].Value) + Assert.Equal(Some 7, graph.Debug.Nodes[4].Value) + +[] +let ``Dependencies are ordered`` () = + let graph = DependencyGraph() + let input = [1..100] + graph.AddList(seq { for x in input -> (x, [x]) }).AddDependentNode(101, fun deps -> deps |> Seq.collect id |> Seq.toList) |> ignore + Assert.Equal(input, graph.GetValue(101)) + graph.AddOrUpdateNode(35, [42]) |> ignore + let expectedResult = input |> List.map (fun x -> if x = 35 then 42 else x) + Assert.Equal(expectedResult, graph.GetValue(101)) + +[] +let ``We can add a dependency between existing nodes`` () = + let graph = DependencyGraph() + graph.AddOrUpdateNode(1, [1]) + .AddDependentNode(2, fun deps -> deps |> Seq.concat |> Seq.toList) |> ignore + graph.AddOrUpdateNode(3, [3]) |> ignore + Assert.Equal([1], graph.GetValue(2)) + graph.AddDependency(2, 3) + Assert.Equal([1; 3], graph.GetValue(2)) + +[] +let ``Can remove a node and update dependents`` () = + let graph = DependencyGraph() + graph.AddOrUpdateNode(1, 1) + .AddDependentNode(2, fun deps -> + let _break = 0 + deps |> Seq.sum |> (+) 1) + .AddDependentNode(3, fun deps -> deps |> Seq.sum |> (+) 1) |> ignore + graph.AddOrUpdateNode(4, [1; 3], fun deps -> deps |> Seq.sum |> (+) 1) |> ignore + + // Check values before removal + Assert.Equal(2, graph.GetValue(2)) + Assert.Equal(3, graph.GetValue(3)) + Assert.Equal(5, graph.GetValue(4)) + + graph.RemoveNode(1) |> ignore + + // Check new values + Assert.Equal(1, graph.GetValue(2)) + Assert.Equal(2, graph.GetValue(3)) + Assert.Equal(3, graph.GetValue(4)) diff --git a/tests/FSharp.Compiler.LanguageServer.Tests/FSharp.Compiler.LanguageServer.Tests.fsproj b/tests/FSharp.Compiler.LanguageServer.Tests/FSharp.Compiler.LanguageServer.Tests.fsproj index dbffa2deb13..91ef50d62c6 100644 --- a/tests/FSharp.Compiler.LanguageServer.Tests/FSharp.Compiler.LanguageServer.Tests.fsproj +++ b/tests/FSharp.Compiler.LanguageServer.Tests/FSharp.Compiler.LanguageServer.Tests.fsproj @@ -15,6 +15,7 @@ +