Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem;

internal interface IRazorProjectService
{
Task AddDocumentAsync(string filePath, CancellationToken cancellationToken);
Task AddDocumentToMiscProjectAsync(string filePath, CancellationToken cancellationToken);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand the reason for the name change, and I like its specificity, but it makes me a little sad that the symmetry is lost.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Somewhere in this PR, I'd love to see comments to explain why this is the way it is. Your PR description was good, and I think it'd be good to capture it in the code.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it makes me a little sad that the symmetry is lost

RemoveDocument is in a similar position as AddDocument was: It's only called by the file watcher, and in lots and lots of tests. Given that it's removing documents though, its not having to guess so its a little more reasonable, though I'd still be a fan of having the client be the source of truth (via project.razor.vs.bin), I didn't want to break into that jail right now. Plus it would mean RemoveDocument would become MoveDocumentsToMiscProject so we'd lose symmetry anyway

Somewhere in this PR, I'd love to see comments to explain why this is the way it is. Your PR description was good, and I think it'd be good to capture it in the code.

That's why I renamed the method, but I'll add some comments around explaining why we don't want to try to do anything smarter, lest someone is tempted to repeat history :)

Task OpenDocumentAsync(string filePath, SourceText sourceText, int version, CancellationToken cancellationToken);
Task UpdateDocumentAsync(string filePath, SourceText sourceText, int version, CancellationToken cancellationToken);
Task CloseDocumentAsync(string filePath, CancellationToken cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,67 +37,46 @@ internal class RazorProjectService(
private readonly IDocumentVersionCache _documentVersionCache = documentVersionCache;
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<RazorProjectService>();

public Task AddDocumentAsync(string filePath, CancellationToken cancellationToken)
public Task AddDocumentToMiscProjectAsync(string filePath, CancellationToken cancellationToken)
{
return _projectManager.UpdateAsync(
updater: AddDocumentCore,
updater: AddDocumentToMiscProjectCore,
state: filePath,
cancellationToken);
}

private void AddDocumentCore(ProjectSnapshotManager.Updater updater, string filePath)
private void AddDocumentToMiscProjectCore(ProjectSnapshotManager.Updater updater, string filePath)
{
var textDocumentPath = FilePathNormalizer.Normalize(filePath);

var added = false;
foreach (var projectSnapshot in _snapshotResolver.FindPotentialProjects(textDocumentPath))
{
added = true;
AddDocumentToProject(updater, projectSnapshot, textDocumentPath);
}
_logger.LogDebug($"Adding {filePath} to the miscellaneous files project, because we don't have project info (yet?)");
var miscFilesProject = _snapshotResolver.GetMiscellaneousProject();

if (!added)
if (miscFilesProject.GetDocument(FilePathNormalizer.Normalize(textDocumentPath)) is not null)
{
var miscFilesProject = _snapshotResolver.GetMiscellaneousProject();
AddDocumentToProject(updater, miscFilesProject, textDocumentPath);
// Document already added. This usually occurs when VSCode has already pre-initialized
// open documents and then we try to manually add all known razor documents.
return;
}

void AddDocumentToProject(ProjectSnapshotManager.Updater updater, IProjectSnapshot projectSnapshot, string textDocumentPath)
{
if (projectSnapshot.GetDocument(FilePathNormalizer.Normalize(textDocumentPath)) is not null)
{
// Document already added. This usually occurs when VSCode has already pre-initialized
// open documents and then we try to manually add all known razor documents.
return;
}

var targetFilePath = textDocumentPath;
var projectDirectory = FilePathNormalizer.GetNormalizedDirectoryName(projectSnapshot.FilePath);
if (targetFilePath.StartsWith(projectDirectory, FilePathComparison.Instance))
{
// Make relative
targetFilePath = textDocumentPath[projectDirectory.Length..];
}

// Representing all of our host documents with a re-normalized target path to workaround GetRelatedDocument limitations.
var normalizedTargetFilePath = targetFilePath.Replace('/', '\\').TrimStart('\\');
// Representing all of our host documents with a re-normalized target path to workaround GetRelatedDocument limitations.
var normalizedTargetFilePath = textDocumentPath.Replace('/', '\\').TrimStart('\\');

var hostDocument = new HostDocument(textDocumentPath, normalizedTargetFilePath);
var textLoader = _remoteTextLoaderFactory.Create(textDocumentPath);
var hostDocument = new HostDocument(textDocumentPath, normalizedTargetFilePath);
var textLoader = _remoteTextLoaderFactory.Create(textDocumentPath);

_logger.LogInformation($"Adding document '{filePath}' to project '{projectSnapshot.Key}'.");
_logger.LogInformation($"Adding document '{filePath}' to project '{miscFilesProject.Key}'.");

updater.DocumentAdded(projectSnapshot.Key, hostDocument, textLoader);
updater.DocumentAdded(miscFilesProject.Key, hostDocument, textLoader);

// Adding a document to a project could also happen because a target was added to a project, or we're moving a document
// from Misc Project to a real one, and means the newly added document could actually already be open.
// If it is, we need to make sure we start generating it so we're ready to handle requests that could start coming in.
if (_projectManager.IsDocumentOpen(textDocumentPath) &&
_projectManager.TryGetLoadedProject(projectSnapshot.Key, out var project) &&
project.GetDocument(textDocumentPath) is { } document)
{
document.GetGeneratedOutputAsync().Forget();
}
// Adding a document to a project could also happen because a target was added to a project, or we're moving a document
// from Misc Project to a real one, and means the newly added document could actually already be open.
// If it is, we need to make sure we start generating it so we're ready to handle requests that could start coming in.
if (_projectManager.IsDocumentOpen(textDocumentPath) &&
_projectManager.TryGetLoadedProject(miscFilesProject.Key, out var project) &&
project.GetDocument(textDocumentPath) is { } document)
{
document.GetGeneratedOutputAsync().Forget();
}
}

Expand All @@ -114,8 +93,9 @@ public Task OpenDocumentAsync(string filePath, SourceText sourceText, int versio
if (!_snapshotResolver.TryResolveDocumentInAnyProject(textDocumentPath, out var document))
{
// Document hasn't been added. This usually occurs when VSCode trumps all other initialization
// processes and pre-initializes already open documents.
AddDocumentCore(updater, filePath);
// processes and pre-initializes already open documents. We add this to the misc project, and
// if/when we get project info from the client, it will be migrated to a real project.
AddDocumentToMiscProjectCore(updater, filePath);
}

ActOnDocumentInMultipleProjects(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ internal class RazorFileSynchronizer(IRazorProjectService projectService) : IRaz
public Task RazorFileChangedAsync(string filePath, RazorFileChangeKind kind, CancellationToken cancellationToken)
=> kind switch
{
RazorFileChangeKind.Added => _projectService.AddDocumentAsync(filePath, cancellationToken),
// We put the new file in the misc files project, so we don't confuse the client by sending updates for
// a razor file that we guess is going to be in a project, when the client might not have received that
// info yet. When the client does find out, it will tell us by updating the project info, and we'll
// migrate the file as necessary.
RazorFileChangeKind.Added => _projectService.AddDocumentToMiscProjectAsync(filePath, cancellationToken),
RazorFileChangeKind.Removed => _projectService.RemoveDocumentAsync(filePath, cancellationToken),
_ => Task.CompletedTask
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ protected override async Task InitializeAsync()
return textLoaderMock.Object;
});

var projectService = new RazorProjectService(
var projectService = new TestRazorProjectService(
remoteTextLoaderFactoryMock.Object,
snapshotResolver,
documentVersionCache,
Expand All @@ -79,10 +79,10 @@ await projectService.AddProjectAsync(
displayName: "",
DisposalToken);

await projectService.AddDocumentAsync(s_componentFilePath1, DisposalToken);
await projectService.AddDocumentToPotentialProjectsAsync(s_componentFilePath1, DisposalToken);
await projectService.UpdateDocumentAsync(s_componentFilePath1, SourceText.From(""), version: 1, DisposalToken);

await projectService.AddDocumentAsync(s_componentFilePath2, DisposalToken);
await projectService.AddDocumentToPotentialProjectsAsync(s_componentFilePath2, DisposalToken);
await projectService.UpdateDocumentAsync(s_componentFilePath2, SourceText.From("@namespace Test"), version: 1, DisposalToken);

await projectService.AddProjectAsync(
Expand All @@ -93,7 +93,7 @@ await projectService.AddProjectAsync(
displayName: "",
DisposalToken);

await projectService.AddDocumentAsync(s_componentFilePath3, DisposalToken);
await projectService.AddDocumentToPotentialProjectsAsync(s_componentFilePath3, DisposalToken);
await projectService.UpdateDocumentAsync(s_componentFilePath3, SourceText.From(""), version: 1, DisposalToken);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public async Task RazorFileChanged_Added_AddsRazorDocument()
var filePath = "/path/to/file.razor";
var projectService = new StrictMock<IRazorProjectService>();
projectService
.Setup(service => service.AddDocumentAsync(filePath, It.IsAny<CancellationToken>()))
.Setup(service => service.AddDocumentToMiscProjectAsync(filePath, It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask)
.Verifiable();
var synchronizer = new RazorFileSynchronizer(projectService.Object);
Expand All @@ -41,7 +41,7 @@ public async Task RazorFileChanged_Added_AddsCSHTMLDocument()
var filePath = "/path/to/file.cshtml";
var projectService = new StrictMock<IRazorProjectService>();
projectService
.Setup(service => service.AddDocumentAsync(filePath, It.IsAny<CancellationToken>()))
.Setup(service => service.AddDocumentToMiscProjectAsync(filePath, It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask)
.Verifiable();
var synchronizer = new RazorFileSynchronizer(projectService.Object);
Expand Down
Loading