Skip to content

Commit 1141376

Browse files
authored
YAPCSSF: Automatically add projects if they don't exist when updating (#10335)
Fixes #10306 This change takes a different approach than #10308 and #10330 and #10332 because life was meant to be interesting. The original problem we were trying to solve is that when a Reset then Add came in, we would migrate all of the documents out of the project, then migrate them all back in again. The various solutions in the other PRs you can read about at your leisure, this one takes a rather simple approach: 1. Ignore everything but the most recent update for a specific project 2. If we need to update a project, but it doesn't exist, add it In the long run we should remove `IRazorProjectService.AddProjectAsync` entirely, and rename `IRazorProjectService.UpdateProjectAsync` to `ResetProjectAsync`, but doing that now would just conflict with other open PRs.
2 parents 17ab875 + 809ebcd commit 1141376

File tree

9 files changed

+536
-281
lines changed

9 files changed

+536
-281
lines changed

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/OpenDocumentGenerator.cs

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Collections.Immutable;
77
using System.Threading;
88
using System.Threading.Tasks;
9+
using Microsoft.CodeAnalysis.Razor.Logging;
910
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
1011
using Microsoft.CodeAnalysis.Razor.Utilities;
1112
using Microsoft.CodeAnalysis.Razor.Workspaces;
@@ -29,14 +30,16 @@ internal partial class OpenDocumentGenerator : IRazorStartupService, IDisposable
2930
private readonly ImmutableArray<IDocumentProcessedListener> _listeners;
3031
private readonly IProjectSnapshotManager _projectManager;
3132
private readonly LanguageServerFeatureOptions _options;
33+
private readonly ILogger _logger;
3234

3335
private readonly AsyncBatchingWorkQueue<IDocumentSnapshot> _workQueue;
3436
private readonly CancellationTokenSource _disposeTokenSource;
3537

3638
public OpenDocumentGenerator(
3739
IEnumerable<IDocumentProcessedListener> listeners,
3840
IProjectSnapshotManager projectManager,
39-
LanguageServerFeatureOptions options)
41+
LanguageServerFeatureOptions options,
42+
ILoggerFactory loggerFactory)
4043
{
4144
_listeners = listeners.ToImmutableArray();
4245
_projectManager = projectManager;
@@ -49,6 +52,7 @@ public OpenDocumentGenerator(
4952
_disposeTokenSource.Token);
5053

5154
_projectManager.Changed += ProjectManager_Changed;
55+
_logger = loggerFactory.GetOrCreateLogger<OpenDocumentGenerator>();
5256
}
5357

5458
public void Dispose()
@@ -88,6 +92,8 @@ private void ProjectManager_Changed(object? sender, ProjectChangeEventArgs args)
8892
return;
8993
}
9094

95+
_logger.LogDebug($"Got a project change of type {args.Kind} for {args.ProjectKey.Id}");
96+
9197
switch (args.Kind)
9298
{
9399
case ProjectChangeKind.ProjectChanged:
@@ -106,25 +112,11 @@ private void ProjectManager_Changed(object? sender, ProjectChangeEventArgs args)
106112
}
107113

108114
case ProjectChangeKind.DocumentAdded:
109-
{
110-
var newProject = args.Newer.AssumeNotNull();
111-
var documentFilePath = args.DocumentFilePath.AssumeNotNull();
112-
113-
if (newProject.TryGetDocument(documentFilePath, out var document))
114-
{
115-
// We don't enqueue the current document because added documents are initially closed.
116-
117-
foreach (var relatedDocument in newProject.GetRelatedDocuments(document))
118-
{
119-
EnqueueIfNecessary(relatedDocument);
120-
}
121-
}
122-
123-
break;
124-
}
125-
126115
case ProjectChangeKind.DocumentChanged:
127116
{
117+
// Most of the time Add will be called on closed files, but when migrating files to/from the misc files
118+
// project they could already be open, but with a different generated C# path
119+
128120
var newProject = args.Newer.AssumeNotNull();
129121
var documentFilePath = args.DocumentFilePath.AssumeNotNull();
130122

@@ -178,6 +170,8 @@ void EnqueueIfNecessary(IDocumentSnapshot document)
178170
return;
179171
}
180172

173+
_logger.LogDebug($"Enqueuing generation of {document.FilePath} in {document.Project.Key.Id}");
174+
181175
_workQueue.AddWork(document);
182176
}
183177
}

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectConfigurationFileChangeEventArgs.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Microsoft.AspNetCore.Razor.ProjectSystem;
99
using Microsoft.AspNetCore.Razor.Utilities;
1010
using Microsoft.CodeAnalysis.Razor;
11+
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
1112
using Microsoft.CodeAnalysis.Razor.Workspaces;
1213

1314
namespace Microsoft.AspNetCore.Razor.LanguageServer;
@@ -77,4 +78,10 @@ public bool TryDeserialize(LanguageServerFeatureOptions options, [NotNullWhen(tr
7778

7879
return projectInfo is not null;
7980
}
81+
82+
internal ProjectKey GetProjectKey()
83+
{
84+
var intermediateOutputPath = FilePathNormalizer.GetNormalizedDirectoryName(ConfigurationFilePath);
85+
return ProjectKey.FromString(intermediateOutputPath);
86+
}
8087
}

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectConfigurationStateSynchronizer.Comparer.cs

Lines changed: 8 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Collections.Generic;
55
using Microsoft.CodeAnalysis.Razor;
6+
using Microsoft.Extensions.Internal;
67

78
namespace Microsoft.AspNetCore.Razor.LanguageServer;
89

@@ -17,42 +18,15 @@ private Comparer()
1718
}
1819

1920
public bool Equals(Work? x, Work? y)
20-
{
21-
if (x is null)
22-
{
23-
return y is null;
24-
}
25-
else if (y is null)
26-
{
27-
return x is null;
28-
}
29-
30-
// For purposes of removing duplicates from batches, two Work instances
31-
// are equal only if their identifying properties are equal. So, only
32-
// configuration file paths and project keys.
33-
34-
if (!FilePathComparer.Instance.Equals(x.ConfigurationFilePath, y.ConfigurationFilePath))
35-
{
36-
return false;
37-
}
38-
39-
return (x, y) switch
40-
{
41-
(AddProject, AddProject) => true,
21+
=> (x, y) switch
22+
{
23+
(Work(var keyX), Work(var keyY)) => keyX == keyY,
24+
(null, null) => true,
4225

43-
(ResetProject { ProjectKey: var keyX },
44-
ResetProject { ProjectKey: var keyY })
45-
=> keyX == keyY,
46-
47-
(UpdateProject { ProjectKey: var keyX },
48-
UpdateProject { ProjectKey: var keyY })
49-
=> keyX == keyY,
50-
51-
_ => false,
52-
};
53-
}
26+
_ => false
27+
};
5428

5529
public int GetHashCode(Work obj)
56-
=> obj.GetHashCode();
30+
=> obj.ProjectKey.GetHashCode();
5731
}
5832
}

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectConfigurationStateSynchronizer.cs

Lines changed: 35 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,26 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Collections.Immutable;
7-
using System.IO;
7+
using System.Diagnostics;
88
using System.Threading;
99
using System.Threading.Tasks;
1010
using Microsoft.AspNetCore.Razor.LanguageServer.Common;
1111
using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem;
12+
using Microsoft.AspNetCore.Razor.PooledObjects;
1213
using Microsoft.AspNetCore.Razor.ProjectSystem;
1314
using Microsoft.AspNetCore.Razor.Utilities;
14-
using Microsoft.CodeAnalysis;
15-
using Microsoft.CodeAnalysis.Razor;
1615
using Microsoft.CodeAnalysis.Razor.Logging;
1716
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
1817
using Microsoft.CodeAnalysis.Razor.Utilities;
1918
using Microsoft.CodeAnalysis.Razor.Workspaces;
20-
using Microsoft.VisualStudio.Threading;
2119

2220
namespace Microsoft.AspNetCore.Razor.LanguageServer;
2321

2422
internal partial class ProjectConfigurationStateSynchronizer : IProjectConfigurationFileChangeListener, IDisposable
2523
{
26-
private abstract record Work(string ConfigurationFilePath);
27-
private sealed record AddProject(string ConfigurationFilePath, RazorProjectInfo ProjectInfo) : Work(ConfigurationFilePath);
28-
private sealed record ResetProject(string ConfigurationFilePath, ProjectKey ProjectKey) : Work(ConfigurationFilePath);
29-
private sealed record UpdateProject(string ConfigurationFilePath, ProjectKey ProjectKey, RazorProjectInfo ProjectInfo) : Work(ConfigurationFilePath);
24+
private abstract record Work(ProjectKey ProjectKey);
25+
private sealed record ResetProject(ProjectKey ProjectKey) : Work(ProjectKey);
26+
private sealed record UpdateProject(ProjectKey ProjectKey, RazorProjectInfo ProjectInfo) : Work(ProjectKey);
3027

3128
private static readonly TimeSpan s_delay = TimeSpan.FromMilliseconds(250);
3229

@@ -37,8 +34,7 @@ private sealed record UpdateProject(string ConfigurationFilePath, ProjectKey Pro
3734
private readonly CancellationTokenSource _disposeTokenSource;
3835
private readonly AsyncBatchingWorkQueue<Work> _workQueue;
3936

40-
private ImmutableDictionary<string, ProjectKey> _filePathToProjectKeyMap =
41-
ImmutableDictionary<string, ProjectKey>.Empty.WithComparers(keyComparer: FilePathComparer.Instance);
37+
private readonly Dictionary<ProjectKey, ResetProject> _resetProjectMap = new();
4238

4339
public ProjectConfigurationStateSynchronizer(
4440
IRazorProjectService projectService,
@@ -67,44 +63,26 @@ public void Dispose()
6763
_disposeTokenSource.Cancel();
6864
_disposeTokenSource.Dispose();
6965
}
66+
7067
private async ValueTask ProcessBatchAsync(ImmutableArray<Work> items, CancellationToken token)
7168
{
7269
foreach (var item in items.GetMostRecentUniqueItems(Comparer.Instance))
7370
{
71+
if (token.IsCancellationRequested)
72+
{
73+
return;
74+
}
75+
7476
var itemTask = item switch
7577
{
76-
AddProject(var configurationFilePath, var projectInfo) => AddProjectAsync(configurationFilePath, projectInfo, token),
77-
ResetProject(_, var projectKey) => ResetProjectAsync(projectKey, token),
78-
UpdateProject(_, var projectKey, var projectInfo) => UpdateProjectAsync(projectKey, projectInfo, token),
78+
ResetProject(var projectKey) => ResetProjectAsync(projectKey, token),
79+
UpdateProject(var projectKey, var projectInfo) => UpdateProjectAsync(projectKey, projectInfo, token),
7980
_ => Assumed.Unreachable<Task>()
8081
};
8182

8283
await itemTask.ConfigureAwait(false);
8384
}
8485

85-
async Task AddProjectAsync(string configurationFilePath, RazorProjectInfo projectInfo, CancellationToken token)
86-
{
87-
var projectFilePath = FilePathNormalizer.Normalize(projectInfo.FilePath);
88-
var intermediateOutputPath = FilePathNormalizer.GetNormalizedDirectoryName(configurationFilePath);
89-
90-
var projectKey = await _projectService
91-
.AddProjectAsync(
92-
projectFilePath,
93-
intermediateOutputPath,
94-
projectInfo.Configuration,
95-
projectInfo.RootNamespace,
96-
projectInfo.DisplayName,
97-
token)
98-
.ConfigureAwait(false);
99-
100-
_logger.LogInformation($"Added {projectKey.Id}.");
101-
102-
ImmutableInterlocked.AddOrUpdate(ref _filePathToProjectKeyMap, configurationFilePath, projectKey, static (k, v) => v);
103-
_logger.LogInformation($"Project configuration file added for project '{projectFilePath}': '{configurationFilePath}'");
104-
105-
await UpdateProjectAsync(projectKey, projectInfo, token).ConfigureAwait(false);
106-
}
107-
10886
Task ResetProjectAsync(ProjectKey projectKey, CancellationToken token)
10987
{
11088
_logger.LogInformation($"Resetting {projectKey.Id}.");
@@ -125,8 +103,9 @@ Task UpdateProjectAsync(ProjectKey projectKey, RazorProjectInfo projectInfo, Can
125103
_logger.LogInformation($"Updating {projectKey.Id}.");
126104

127105
return _projectService
128-
.UpdateProjectAsync(
106+
.AddOrUpdateProjectAsync(
129107
projectKey,
108+
projectInfo.FilePath,
130109
projectInfo.Configuration,
131110
projectInfo.RootNamespace,
132111
projectInfo.DisplayName,
@@ -138,55 +117,25 @@ Task UpdateProjectAsync(ProjectKey projectKey, RazorProjectInfo projectInfo, Can
138117

139118
public void ProjectConfigurationFileChanged(ProjectConfigurationFileChangeEventArgs args)
140119
{
141-
var configurationFilePath = FilePathNormalizer.Normalize(args.ConfigurationFilePath);
142-
143120
switch (args.Kind)
144121
{
145122
case RazorFileChangeKind.Changed:
146123
{
147124
if (args.TryDeserialize(_options, out var projectInfo))
148125
{
149-
if (_filePathToProjectKeyMap.TryGetValue(configurationFilePath, out var projectKey))
150-
{
151-
_logger.LogInformation($"""
152-
Configuration file changed for project '{projectKey.Id}'.
153-
Configuration file path: '{configurationFilePath}'
154-
""");
155-
156-
_workQueue.AddWork(new UpdateProject(configurationFilePath, projectKey, projectInfo));
157-
}
158-
else
159-
{
160-
_logger.LogWarning($"""
161-
Adding project for previously unseen configuration file.
162-
Configuration file path: '{configurationFilePath}'
163-
""");
164-
165-
_workQueue.AddWork(new AddProject(configurationFilePath, projectInfo));
166-
}
126+
var projectKey = ProjectKey.From(projectInfo);
127+
_logger.LogInformation($"Configuration file changed for project '{projectKey.Id}'.");
128+
129+
_workQueue.AddWork(new UpdateProject(projectKey, projectInfo));
167130
}
168131
else
169132
{
170-
if (_filePathToProjectKeyMap.TryGetValue(configurationFilePath, out var projectKey))
171-
{
172-
_logger.LogWarning($"""
173-
Failed to deserialize after change to configuration file for project '{projectKey.Id}'.
174-
Configuration file path: '{configurationFilePath}'
175-
""");
176-
177-
// We found the last associated project file for the configuration file. Reset the project since we can't
178-
// accurately determine its configurations.
179-
180-
_workQueue.AddWork(new ResetProject(configurationFilePath, projectKey));
181-
}
182-
else
183-
{
184-
// Could not resolve an associated project file.
185-
_logger.LogWarning($"""
186-
Failed to deserialize after change to previously unseen configuration file.
187-
Configuration file path: '{configurationFilePath}'
188-
""");
189-
}
133+
var projectKey = args.GetProjectKey();
134+
_logger.LogWarning($"Failed to deserialize after change to configuration file for project '{projectKey.Id}'.");
135+
136+
// We found the last associated project file for the configuration file. Reset the project since we can't
137+
// accurately determine its configurations.
138+
_workQueue.AddWork(new ResetProject(projectKey));
190139
}
191140
}
192141

@@ -196,39 +145,28 @@ Failed to deserialize after change to previously unseen configuration file.
196145
{
197146
if (args.TryDeserialize(_options, out var projectInfo))
198147
{
199-
_workQueue.AddWork(new AddProject(configurationFilePath, projectInfo));
148+
var projectKey = ProjectKey.From(projectInfo);
149+
_logger.LogInformation($"Configuration file added for project '{projectKey.Id}'.");
150+
151+
// Update will add the project if it doesn't exist
152+
_workQueue.AddWork(new UpdateProject(projectKey, projectInfo));
200153
}
201154
else
202155
{
203156
// This is the first time we've seen this configuration file, but we can't deserialize it.
204157
// The only thing we can really do is issue a warning.
205-
_logger.LogWarning($"""
206-
Failed to deserialize previously unseen configuration file.
207-
Configuration file path: '{configurationFilePath}'
208-
""");
158+
_logger.LogWarning($"Failed to deserialize previously unseen configuration file '{args.ConfigurationFilePath}'");
209159
}
210160
}
211161

212162
break;
213163

214164
case RazorFileChangeKind.Removed:
215165
{
216-
if (ImmutableInterlocked.TryRemove(ref _filePathToProjectKeyMap, configurationFilePath, out var projectKey))
217-
{
218-
_logger.LogInformation($"""
219-
Configuration file removed for project '{projectKey}'.
220-
Configuration file path: '{configurationFilePath}'
221-
""");
166+
var projectKey = args.GetProjectKey();
167+
_logger.LogInformation($"Configuration file removed for project '{projectKey}'.");
222168

223-
_workQueue.AddWork(new ResetProject(configurationFilePath, projectKey));
224-
}
225-
else
226-
{
227-
_logger.LogWarning($"""
228-
Failed to resolve associated project on configuration removed event.
229-
Configuration file path: '{configurationFilePath}'
230-
""");
231-
}
169+
_workQueue.AddWork(new ResetProject(projectKey));
232170
}
233171

234172
break;

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/IRazorProjectService.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,14 @@ Task UpdateProjectAsync(
3636
ProjectWorkspaceState projectWorkspaceState,
3737
ImmutableArray<DocumentSnapshotHandle> documents,
3838
CancellationToken cancellationToken);
39+
40+
Task AddOrUpdateProjectAsync(
41+
ProjectKey projectKey,
42+
string filePath,
43+
RazorConfiguration? configuration,
44+
string? rootNamespace,
45+
string displayName,
46+
ProjectWorkspaceState projectWorkspaceState,
47+
ImmutableArray<DocumentSnapshotHandle> documents,
48+
CancellationToken cancellationToken);
3949
}

0 commit comments

Comments
 (0)