|
1 | 1 | module FSharp.Compiler.LanguageServer.Workspace |
2 | 2 |
|
| 3 | +open FSharp.Compiler.Text |
| 4 | + |
3 | 5 | #nowarn "57" |
4 | 6 |
|
5 | 7 | open System |
| 8 | +open System.Threading.Tasks |
6 | 9 | open FSharp.Compiler.CodeAnalysis.ProjectSnapshot |
7 | 10 |
|
8 | | -type FSharpWorkspace = |
9 | | - { |
10 | | - Projects: Map<FSharpProjectIdentifier, FSharpProjectSnapshot> |
11 | | - OpenFiles: Map<string, string> |
12 | | - } |
| 11 | +/// Holds a project snapshot and a queue of changes that will be applied to it when it's requested |
| 12 | +/// |
| 13 | +/// The assumption is that this is faster than actually applying the changes to the snapshot immediately and that |
| 14 | +/// we will be doing this on potentially every keystroke. But this should probably be measured at some point. |
| 15 | +type SnapshotHolder(snapshot: FSharpProjectSnapshot, changedFiles: Set<string>, openFiles: Map<string, string>) = |
| 16 | + |
| 17 | + let applyFileChangesToSnapshot () = |
| 18 | + let files = |
| 19 | + changedFiles |
| 20 | + |> Seq.map (fun filePath -> |
| 21 | + match openFiles.TryFind filePath with |
| 22 | + | Some content -> |
| 23 | + FSharpFileSnapshot.Create( |
| 24 | + filePath, |
| 25 | + DateTime.Now.Ticks.ToString(), |
| 26 | + fun () -> content |> SourceTextNew.ofString |> Task.FromResult |
| 27 | + ) |
| 28 | + | None -> FSharpFileSnapshot.CreateFromFileSystem(filePath)) |
| 29 | + |> Seq.toList |
| 30 | + |
| 31 | + snapshot.Replace files |
| 32 | + |
| 33 | + // We don't want to mutate the workspace by applying the changes when snapshot is requested because that would force the language |
| 34 | + // requests to be processed sequentially. So instead we keep the change application under lazy so it's still only computed if needed |
| 35 | + // and only once and workspace doesn't change. |
| 36 | + let appliedChanges = |
| 37 | + lazy SnapshotHolder(applyFileChangesToSnapshot (), Set.empty, openFiles) |
| 38 | + |
| 39 | + member private _.snapshot = snapshot |
| 40 | + member private _.changedFiles = changedFiles |
| 41 | + |
| 42 | + member private this.GetMostUpToDateInstance() = |
| 43 | + if appliedChanges.IsValueCreated then |
| 44 | + appliedChanges.Value |
| 45 | + else |
| 46 | + this |
| 47 | + |
| 48 | + member this.WithFileChanged(file, openFiles) = |
| 49 | + let previous = this.GetMostUpToDateInstance() |
| 50 | + SnapshotHolder(previous.snapshot, previous.changedFiles.Add file, openFiles) |
| 51 | + |
| 52 | + member this.WithoutFileChanged(file, openFiles) = |
| 53 | + let previous = this.GetMostUpToDateInstance() |
| 54 | + SnapshotHolder(previous.snapshot, previous.changedFiles.Remove file, openFiles) |
| 55 | + |
| 56 | + member _.GetSnapshot() = appliedChanges.Value.snapshot |
| 57 | + |
| 58 | + static member Of(snapshot: FSharpProjectSnapshot) = |
| 59 | + SnapshotHolder(snapshot, Set.empty, Map.empty) |
| 60 | + |
| 61 | +type FSharpWorkspace |
| 62 | + private |
| 63 | + ( |
| 64 | + projects: Map<FSharpProjectIdentifier, SnapshotHolder>, |
| 65 | + openFiles: Map<string, string>, |
| 66 | + fileMap: Map<string, Set<FSharpProjectIdentifier>> |
| 67 | + ) = |
| 68 | + |
| 69 | + let updateProjectsWithFile (file: Uri) f (projects: Map<FSharpProjectIdentifier, SnapshotHolder>) = |
| 70 | + fileMap |
| 71 | + |> Map.tryFind file.LocalPath |
| 72 | + |> Option.map (fun identifier -> |
| 73 | + (projects, identifier) |
| 74 | + ||> Seq.fold (fun projects identifier -> |
| 75 | + let snapshotHolder = projects[identifier] |
| 76 | + projects.Add(identifier, f snapshotHolder))) |
| 77 | + |> Option.defaultValue projects |
| 78 | + |
| 79 | + member _.Projects = projects |
| 80 | + member _.OpenFiles = openFiles |
| 81 | + member _.FileMap = fileMap |
| 82 | + |
| 83 | + member this.OpenFile(file: Uri, content: string) = this.ChangeFile(file, content) |
| 84 | + |
| 85 | + member _.CloseFile(file: Uri) = |
| 86 | + let openFiles = openFiles.Remove(file.LocalPath) |
| 87 | + |
| 88 | + FSharpWorkspace( |
| 89 | + projects = |
| 90 | + (projects |
| 91 | + |> updateProjectsWithFile file _.WithoutFileChanged(file.LocalPath, openFiles)), |
| 92 | + openFiles = openFiles, |
| 93 | + fileMap = fileMap |
| 94 | + ) |
| 95 | + |
| 96 | + member _.ChangeFile(file: Uri, content: string) = |
| 97 | + |
| 98 | + // TODO: should we assert that the file is open? |
| 99 | + |
| 100 | + let openFiles = openFiles.Add(file.LocalPath, content) |
| 101 | + |
| 102 | + FSharpWorkspace( |
| 103 | + projects = |
| 104 | + (projects |
| 105 | + |> updateProjectsWithFile file _.WithFileChanged(file.LocalPath, openFiles)), |
| 106 | + openFiles = openFiles, |
| 107 | + fileMap = fileMap |
| 108 | + ) |
| 109 | + |
| 110 | + member _.GetSnapshotForFile(file: Uri) = |
| 111 | + fileMap |
| 112 | + |> Map.tryFind file.LocalPath |
| 113 | + |
| 114 | + // TODO: eventually we need to deal with choosing the appropriate project here |
| 115 | + // Hopefully we will be able to do it through receiving project context from LSP |
| 116 | + // Otherwise we have to keep track of which project/configuration is active |
| 117 | + |> Option.bind Seq.tryHead |
| 118 | + |
| 119 | + |> Option.bind projects.TryFind |
| 120 | + |> Option.map _.GetSnapshot() |
13 | 121 |
|
14 | 122 | static member Create(projects: FSharpProjectSnapshot seq) = |
15 | | - { |
16 | | - Projects = Map.ofSeq (projects |> Seq.map (fun p -> p.Identifier, p)) |
17 | | - OpenFiles = Map.empty |
18 | | - } |
| 123 | + FSharpWorkspace( |
| 124 | + projects = Map.ofSeq (projects |> Seq.map (fun p -> p.Identifier, SnapshotHolder.Of p)), |
| 125 | + openFiles = Map.empty, |
| 126 | + fileMap = |
| 127 | + (projects |
| 128 | + |> Seq.collect (fun p -> |
| 129 | + p.ProjectSnapshot.SourceFileNames |
| 130 | + |> Seq.map (fun f -> Uri(f).LocalPath, p.Identifier)) |
| 131 | + |> Seq.groupBy fst |
| 132 | + |> Seq.map (fun (f, ps) -> f, Set.ofSeq (ps |> Seq.map snd)) |
| 133 | + |> Map.ofSeq) |
| 134 | + ) |
0 commit comments