Skip to content

Commit d557b62

Browse files
committed
WIP workspace state management, diagnostics
1 parent 9a1de41 commit d557b62

File tree

5 files changed

+198
-53
lines changed

5 files changed

+198
-53
lines changed

src/Compiler/FSharp.Compiler.Service.fsproj

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,9 @@
7777
<InternalsVisibleTo Include="LanguageServiceProfiling" />
7878
<InternalsVisibleTo Include="FSharp.Compiler.Benchmarks" />
7979
<InternalsVisibleTo Include="HistoricalBenchmark" />
80-
<InternalsVisibleTo Include="FSharp.Test.Utilities" />
81-
<InternalsVisibleTo Include="FSharp.Editor" />
80+
<InternalsVisibleTo Include="FSharp.Test.Utilities" />
81+
<InternalsVisibleTo Include="FSharp.Editor" />
82+
<InternalsVisibleTo Include="FSharp.Compiler.LanguageServer" />
8283
</ItemGroup>
8384

8485
<ItemGroup>

src/Compiler/Facilities/Hashing.fs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,11 @@ module internal Md5Hasher =
4646
let private md5 =
4747
new ThreadLocal<_>(fun () -> System.Security.Cryptography.MD5.Create())
4848

49-
let computeHash (bytes: byte array) = md5.Value.ComputeHash(bytes)
49+
let computeHash (bytes: byte array) =
50+
// md5.Value.ComputeHash(bytes) TODO: the threadlocal is not working in new VS extension
51+
ignore md5
52+
let md5 = System.Security.Cryptography.MD5.Create()
53+
md5.ComputeHash(bytes)
5054

5155
let empty = Array.empty
5256

src/FSharp.Compiler.LanguageServer/FSharpLanguageServer.fs

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
namespace FSharp.Compiler.LanguageServer
22

3+
open System.Runtime.CompilerServices
4+
5+
#nowarn "57"
6+
37
open System
48
open System.Threading.Tasks
59
open System.Threading
@@ -20,29 +24,53 @@ module Stuff =
2024
[<Literal>]
2125
let FSharpLanguageName = "F#"
2226

23-
type FSharpRequestContext(lspServices: ILspServices, logger: ILspLogger, workspace: FSharpWorkspace, checker) =
27+
[<Extension>]
28+
type Extensions =
29+
30+
[<Extension>]
31+
static member Please(this: Async<'t>, ct) =
32+
Async.StartAsTask(this, cancellationToken = ct)
33+
34+
type FSharpRequestContext(lspServices: ILspServices, logger: ILspLogger, workspace: FSharpWorkspace, checker: FSharpChecker) =
2435
member _.LspServices = lspServices
2536
member _.Logger = logger
2637
member _.Workspace = workspace
2738
member _.Checker = checker
2839

40+
// TODO: split to parse and check diagnostics
41+
member _.GetDiagnosticsForFile(file: Uri) =
42+
43+
workspace.GetSnapshotForFile file
44+
|> Option.map (fun snapshot ->
45+
async {
46+
let! parseResult, checkFileAnswer = checker.ParseAndCheckFileInProject(file.LocalPath, snapshot, "LSP Get diagnostics")
47+
48+
return
49+
match checkFileAnswer with
50+
| FSharpCheckFileAnswer.Succeeded result -> result.Diagnostics
51+
| FSharpCheckFileAnswer.Aborted -> parseResult.Diagnostics
52+
})
53+
|> Option.defaultValue (async.Return [||])
54+
2955
type ContextHolder(intialWorkspace, lspServices: ILspServices) =
3056

3157
let logger = lspServices.GetRequiredService<ILspLogger>()
3258

3359
// TODO: We need to get configuration for this somehow. Also make it replaceable when configuration changes.
3460
let checker =
3561
FSharpChecker.Create(
36-
keepAllBackgroundResolutions=true,
37-
keepAllBackgroundSymbolUses=true,
38-
enableBackgroundItemKeyStoreAndSemanticClassification=true,
39-
enablePartialTypeChecking=true,
40-
parallelReferenceResolution=true,
41-
captureIdentifiersWhenParsing=true,
42-
useSyntaxTreeCache=true,
43-
useTransparentCompiler=true)
62+
keepAllBackgroundResolutions = true,
63+
keepAllBackgroundSymbolUses = true,
64+
enableBackgroundItemKeyStoreAndSemanticClassification = true,
65+
enablePartialTypeChecking = true,
66+
parallelReferenceResolution = true,
67+
captureIdentifiersWhenParsing = true,
68+
useSyntaxTreeCache = true,
69+
useTransparentCompiler = true
70+
)
4471

45-
let mutable context = FSharpRequestContext(lspServices, logger, intialWorkspace, checker)
72+
let mutable context =
73+
FSharpRequestContext(lspServices, logger, intialWorkspace, checker)
4674

4775
member _.GetContext() = context
4876

@@ -77,10 +105,7 @@ type DocumentStateHandler() =
77105
) =
78106
let contextHolder = context.LspServices.GetRequiredService<ContextHolder>()
79107

80-
contextHolder.UpdateWorkspace(fun w ->
81-
{ w with
82-
OpenFiles = Map.add request.TextDocument.Uri.AbsolutePath request.TextDocument.Text w.OpenFiles
83-
})
108+
contextHolder.UpdateWorkspace _.OpenFile(request.TextDocument.Uri, request.TextDocument.Text)
84109

85110
Task.FromResult(SemanticTokensDeltaPartialResult())
86111

@@ -94,10 +119,7 @@ type DocumentStateHandler() =
94119
) =
95120
let contextHolder = context.LspServices.GetRequiredService<ContextHolder>()
96121

97-
contextHolder.UpdateWorkspace(fun w ->
98-
{ w with
99-
OpenFiles = Map.add request.TextDocument.Uri.AbsolutePath request.ContentChanges.[0].Text w.OpenFiles
100-
})
122+
contextHolder.UpdateWorkspace _.ChangeFile(request.TextDocument.Uri, request.ContentChanges.[0].Text)
101123

102124
Task.FromResult(SemanticTokensDeltaPartialResult())
103125

@@ -111,10 +133,7 @@ type DocumentStateHandler() =
111133
) =
112134
let contextHolder = context.LspServices.GetRequiredService<ContextHolder>()
113135

114-
contextHolder.UpdateWorkspace(fun w ->
115-
{ w with
116-
OpenFiles = Map.remove request.TextDocument.Uri.AbsolutePath w.OpenFiles
117-
})
136+
contextHolder.UpdateWorkspace _.CloseFile(request.TextDocument.Uri)
118137

119138
Task.CompletedTask
120139

Lines changed: 125 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,134 @@
11
module FSharp.Compiler.LanguageServer.Workspace
22

3+
open FSharp.Compiler.Text
4+
35
#nowarn "57"
46

57
open System
8+
open System.Threading.Tasks
69
open FSharp.Compiler.CodeAnalysis.ProjectSnapshot
710

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()
13121

14122
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+
)

src/FSharp.VisualStudio.Extension/FSharpLanguageServerProvider.cs

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -73,37 +73,41 @@ public ServerCapabilities OverrideServerCapabilities(ServerCapabilities value)
7373
}
7474

7575

76-
internal class VsDiagnosticsHandler
76+
internal class VsDiagnosticsHandler
7777
: IRequestHandler<VSInternalDocumentDiagnosticsParams, VSInternalDiagnosticReport[], FSharpRequestContext>,
7878
IRequestHandler<VSGetProjectContextsParams, VSProjectContextList, FSharpRequestContext>
7979
{
8080
public bool MutatesSolutionState => false;
8181

8282
[LanguageServerEndpoint(VSInternalMethods.DocumentPullDiagnosticName)]
83-
public Task<VSInternalDiagnosticReport[]> HandleRequestAsync(VSInternalDocumentDiagnosticsParams request, FSharpRequestContext context, CancellationToken cancellationToken)
83+
public async Task<VSInternalDiagnosticReport[]> HandleRequestAsync(VSInternalDocumentDiagnosticsParams request, FSharpRequestContext context, CancellationToken cancellationToken)
8484
{
85-
var rep = new VSInternalDiagnosticReport
85+
var result = await context.GetDiagnosticsForFile(request!.TextDocument!.Uri).Please(cancellationToken);
86+
87+
var rep = new VSInternalDiagnosticReport
8688
{
8789
ResultId = "potato1", // Has to be present for diagnostic to show up
8890
//Identifier = 69,
8991
//Version = 1,
9092
Diagnostics =
91-
[
92-
new Diagnostic
93-
{
94-
Range = new Microsoft.VisualStudio.LanguageServer.Protocol.Range
95-
{
96-
Start = new Position { Line = 0, Character = 0 },
97-
End = new Position { Line = 0, Character = 1 }
98-
},
99-
Severity = DiagnosticSeverity.Error,
100-
Message = "This is a test diagnostic",
101-
//Source = "Intellisense",
102-
Code = "1234"
103-
}
104-
]
93+
result.Select(d =>
94+
95+
new Diagnostic
96+
{
97+
Range = new Microsoft.VisualStudio.LanguageServer.Protocol.Range
98+
{
99+
Start = new Position { Line = d.StartLine, Character = d.StartColumn },
100+
End = new Position { Line = d.EndLine, Character = d.EndColumn }
101+
},
102+
Severity = DiagnosticSeverity.Error,
103+
Message = $"LSP: {d.Message}",
104+
//Source = "Intellisense",
105+
Code = d.ErrorNumberText
106+
}
107+
).ToArray()
105108
};
106-
return Task.FromResult(new[] { rep });
109+
110+
return [rep];
107111
}
108112

109113
[LanguageServerEndpoint("textDocument/_vs_getProjectContexts")]
@@ -116,6 +120,7 @@ public Task<VSProjectContextList> HandleRequestAsync(VSGetProjectContextsParams
116120
new() {
117121
Id = "potato",
118122
Label = "Potato",
123+
// PR for F# project kind: https://devdiv.visualstudio.com/DevDiv/_git/VSLanguageServerClient/pullrequest/529882
119124
Kind = VSProjectKind.VisualBasic
120125
},
121126
new () {
@@ -174,7 +179,7 @@ internal class FSharpLanguageServerProvider : LanguageServerProvider
174179
try
175180
{
176181
// Some hardcoded projects before we create them from the ProjectQuery
177-
var projectsRoot = @"D:\code\FS";
182+
var projectsRoot = @"D:\code";
178183
var giraffe = FSharpProjectSnapshot.FromResponseFile(
179184
new FileInfo(Path.Combine(projectsRoot, @"Giraffe\src\Giraffe\Giraffe.rsp")),
180185
Path.Combine(projectsRoot, @"Giraffe\src\Giraffe\Giraffe.fsproj"));

0 commit comments

Comments
 (0)