diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/IRazorProjectService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/IRazorProjectService.cs index fad6a15871a..6529e4dd0f9 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/IRazorProjectService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/IRazorProjectService.cs @@ -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); 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); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.cs index a5319903b6f..0c368b4c2a6 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.cs @@ -37,67 +37,46 @@ internal class RazorProjectService( private readonly IDocumentVersionCache _documentVersionCache = documentVersionCache; private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); - 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(); } } @@ -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( diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorFileSynchronizer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorFileSynchronizer.cs index 9365f5a251d..c89c54ef303 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorFileSynchronizer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorFileSynchronizer.cs @@ -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 }; diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DefaultRazorComponentSearchEngineTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DefaultRazorComponentSearchEngineTest.cs index 5f2491651f9..c1f3f7d47f3 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DefaultRazorComponentSearchEngineTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DefaultRazorComponentSearchEngineTest.cs @@ -64,7 +64,7 @@ protected override async Task InitializeAsync() return textLoaderMock.Object; }); - var projectService = new RazorProjectService( + var projectService = new TestRazorProjectService( remoteTextLoaderFactoryMock.Object, snapshotResolver, documentVersionCache, @@ -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( @@ -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); } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorFileSynchronizerTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorFileSynchronizerTest.cs index 4871b27d660..74c9ad85dea 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorFileSynchronizerTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorFileSynchronizerTest.cs @@ -22,7 +22,7 @@ public async Task RazorFileChanged_Added_AddsRazorDocument() var filePath = "/path/to/file.razor"; var projectService = new StrictMock(); projectService - .Setup(service => service.AddDocumentAsync(filePath, It.IsAny())) + .Setup(service => service.AddDocumentToMiscProjectAsync(filePath, It.IsAny())) .Returns(Task.CompletedTask) .Verifiable(); var synchronizer = new RazorFileSynchronizer(projectService.Object); @@ -41,7 +41,7 @@ public async Task RazorFileChanged_Added_AddsCSHTMLDocument() var filePath = "/path/to/file.cshtml"; var projectService = new StrictMock(); projectService - .Setup(service => service.AddDocumentAsync(filePath, It.IsAny())) + .Setup(service => service.AddDocumentToMiscProjectAsync(filePath, It.IsAny())) .Returns(Task.CompletedTask) .Verifiable(); var synchronizer = new RazorFileSynchronizer(projectService.Object); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorProjectServiceTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorProjectServiceTest.cs index 1216ebf0b2e..49ea5dc39a1 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorProjectServiceTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorProjectServiceTest.cs @@ -34,12 +34,14 @@ public class RazorProjectServiceTest(ITestOutputHelper testOutput) : LanguageSer private TestProjectSnapshotManager _projectManager; private SnapshotResolver _snapshotResolver; private DocumentVersionCache _documentVersionCache; - private RazorProjectService _projectService; + private TestRazorProjectService _projectService; #nullable enable protected override async Task InitializeAsync() { - _projectManager = CreateProjectSnapshotManager(); + var optionsMonitor = TestRazorLSPOptionsMonitor.Create(); + var projectEngineFactoryProvider = new LspProjectEngineFactoryProvider(optionsMonitor); + _projectManager = CreateProjectSnapshotManager(projectEngineFactoryProvider); _snapshotResolver = new SnapshotResolver(_projectManager, LoggerFactory); await _snapshotResolver.OnInitializedAsync(StrictMock.Of(), DisposalToken); _documentVersionCache = new DocumentVersionCache(_projectManager); @@ -49,7 +51,7 @@ protected override async Task InitializeAsync() .Setup(x => x.Create(It.IsAny())) .Returns(CreateEmptyTextLoader()); - _projectService = new RazorProjectService( + _projectService = new TestRazorProjectService( remoteTextLoaderFactoryMock.Object, _snapshotResolver, _documentVersionCache, @@ -186,6 +188,44 @@ await _projectService.UpdateProjectAsync( Assert.Empty(miscProject.DocumentFilePaths); } + [Fact] + public async Task UpdateProject_MovesDocumentsFromMisc_ViaService() + { + // Arrange + const string DocumentFilePath = "C:/path/to/file.cshtml"; + const string ProjectFilePath = "C:/path/to/project.csproj"; + const string IntermediateOutputPath = "C:/path/to/obj"; + const string RootNamespace = "TestRootNamespace"; + + var ownerProjectKey = await _projectService.AddProjectAsync( + ProjectFilePath, IntermediateOutputPath, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); + + await _projectService.AddDocumentToMiscProjectAsync(DocumentFilePath, DisposalToken); + + var miscProject = _snapshotResolver.GetMiscellaneousProject(); + + var project = _projectManager.GetLoadedProject(ownerProjectKey); + + var addedDocument = new DocumentSnapshotHandle(DocumentFilePath, DocumentFilePath, FileKinds.Legacy); + + // Act + await _projectService.UpdateProjectAsync( + project.Key, + project.Configuration, + project.RootNamespace, + project.DisplayName, + ProjectWorkspaceState.Default, + [addedDocument], + DisposalToken); + + // Assert + project = _projectManager.GetLoadedProject(ownerProjectKey); + var projectFilePaths = project.DocumentFilePaths.OrderBy(path => path); + Assert.Equal(projectFilePaths, [addedDocument.FilePath]); + miscProject = _projectManager.GetLoadedProject(miscProject.Key); + Assert.Empty(miscProject.DocumentFilePaths); + } + [Fact] public async Task UpdateProject_MovesExistingDocumentToMisc() { @@ -439,7 +479,7 @@ public async Task CloseDocument_ClosesDocumentInOwnerProject() var ownerProjectKey = await _projectService.AddProjectAsync( ProjectFilePath, IntermediateOutputPath, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); - await _projectService.AddDocumentAsync(DocumentFilePath, DisposalToken); + await _projectService.AddDocumentToPotentialProjectsAsync(DocumentFilePath, DisposalToken); await _projectService.OpenDocumentAsync(DocumentFilePath, s_emptyText, version: 42, DisposalToken); var ownerProject = _projectManager.GetLoadedProject(ownerProjectKey); @@ -472,7 +512,7 @@ public async Task CloseDocument_ClosesDocumentInAllOwnerProjects() ProjectFilePath, IntermediateOutputPath1, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); var ownerProjectKey2 = await _projectService.AddProjectAsync( ProjectFilePath, IntermediateOutputPath2, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); - await _projectService.AddDocumentAsync(DocumentFilePath, DisposalToken); + await _projectService.AddDocumentToPotentialProjectsAsync(DocumentFilePath, DisposalToken); await _projectService.OpenDocumentAsync(DocumentFilePath, s_emptyText, version: 42, DisposalToken); var ownerProject1 = _projectManager.GetLoadedProject(ownerProjectKey1); @@ -499,7 +539,7 @@ public async Task CloseDocument_ClosesDocumentInMiscellaneousProject() // Arrange const string DocumentFilePath = "document.cshtml"; - await _projectService.AddDocumentAsync(DocumentFilePath, DisposalToken); + await _projectService.AddDocumentToPotentialProjectsAsync(DocumentFilePath, DisposalToken); await _projectService.OpenDocumentAsync(DocumentFilePath, s_emptyText, version: 42, DisposalToken); var miscProject = _snapshotResolver.GetMiscellaneousProject(); @@ -529,7 +569,7 @@ public async Task OpenDocument_OpensAlreadyAddedDocumentInOwnerProject() var ownerProjectKey = await _projectService.AddProjectAsync( ProjectFilePath, IntermediateOutputPath, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); - await _projectService.AddDocumentAsync(DocumentFilePath, DisposalToken); + await _projectService.AddDocumentToPotentialProjectsAsync(DocumentFilePath, DisposalToken); var ownerProject = _projectManager.GetLoadedProject(ownerProjectKey); @@ -561,7 +601,7 @@ public async Task OpenDocument_OpensAlreadyAddedDocumentInAllOwnerProjects() ProjectFilePath, IntermediateOutputPath1, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); var ownerProjectKey2 = await _projectService.AddProjectAsync( ProjectFilePath, IntermediateOutputPath2, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); - await _projectService.AddDocumentAsync(DocumentFilePath, DisposalToken); + await _projectService.AddDocumentToPotentialProjectsAsync(DocumentFilePath, DisposalToken); var ownerProject1 = _projectManager.GetLoadedProject(ownerProjectKey1); var ownerProject2 = _projectManager.GetLoadedProject(ownerProjectKey2); @@ -587,7 +627,7 @@ public async Task OpenDocument_OpensAlreadyAddedDocumentInMiscellaneousProject() // Arrange const string DocumentFilePath = "document.cshtml"; - await _projectService.AddDocumentAsync(DocumentFilePath, DisposalToken); + await _projectService.AddDocumentToMiscProjectAsync(DocumentFilePath, DisposalToken); var miscProject = _snapshotResolver.GetMiscellaneousProject(); @@ -606,7 +646,7 @@ public async Task OpenDocument_OpensAlreadyAddedDocumentInMiscellaneousProject() } [Fact] - public async Task OpenDocument_OpensAndAddsDocumentToOwnerProject() + public async Task OpenDocument_OpensAndAddsDocumentToMiscellaneousProject() { // Arrange const string ProjectFilePath = "C:/path/to/project.csproj"; @@ -617,7 +657,7 @@ public async Task OpenDocument_OpensAndAddsDocumentToOwnerProject() var ownerProjectKey = await _projectService.AddProjectAsync( ProjectFilePath, IntermediateOutputPath, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); - var ownerProject = _projectManager.GetLoadedProject(ownerProjectKey); + var miscProject = _snapshotResolver.GetMiscellaneousProject(); using var listener = _projectManager.ListenToNotifications(); @@ -626,8 +666,8 @@ public async Task OpenDocument_OpensAndAddsDocumentToOwnerProject() // Assert listener.AssertNotifications( - x => x.DocumentAdded(DocumentFilePath, ownerProject.Key), - x => x.DocumentChanged(DocumentFilePath, ownerProject.Key)); + x => x.DocumentAdded(DocumentFilePath, miscProject.Key), + x => x.DocumentChanged(DocumentFilePath, miscProject.Key)); Assert.True(_projectManager.IsDocumentOpen(DocumentFilePath)); } @@ -638,14 +678,14 @@ public async Task AddDocument_NoopsIfDocumentIsAlreadyAdded() // Arrange const string DocumentFilePath = "document.cshtml"; - await _projectService.AddDocumentAsync(DocumentFilePath, DisposalToken); + await _projectService.AddDocumentToPotentialProjectsAsync(DocumentFilePath, DisposalToken); var miscProject = _snapshotResolver.GetMiscellaneousProject(); using var listener = _projectManager.ListenToNotifications(); // Act - await _projectService.AddDocumentAsync(DocumentFilePath, DisposalToken); + await _projectService.AddDocumentToPotentialProjectsAsync(DocumentFilePath, DisposalToken); // Assert listener.AssertNoNotifications(); @@ -668,7 +708,7 @@ public async Task AddDocument_AddsDocumentToOwnerProject() using var listener = _projectManager.ListenToNotifications(); // Act - await _projectService.AddDocumentAsync(DocumentFilePath, DisposalToken); + await _projectService.AddDocumentToPotentialProjectsAsync(DocumentFilePath, DisposalToken); // Assert listener.AssertNotifications( @@ -678,7 +718,7 @@ public async Task AddDocument_AddsDocumentToOwnerProject() } [Fact] - public async Task AddDocument_AddsDocumentToMiscellaneousProject() + public async Task AddDocumentToMiscProjectAsync_AddsDocumentToMiscellaneousProject() { // Arrange const string DocumentFilePath = "document.cshtml"; @@ -688,7 +728,7 @@ public async Task AddDocument_AddsDocumentToMiscellaneousProject() using var listener = _projectManager.ListenToNotifications(); // Act - await _projectService.AddDocumentAsync(DocumentFilePath, DisposalToken); + await _projectService.AddDocumentToMiscProjectAsync(DocumentFilePath, DisposalToken); // Assert listener.AssertNotifications( @@ -708,7 +748,7 @@ public async Task RemoveDocument_RemovesDocumentFromOwnerProject() var ownerProjectKey = await _projectService.AddProjectAsync( ProjectFilePath, IntermediateOutputPath, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); - await _projectService.AddDocumentAsync(DocumentFilePath, DisposalToken); + await _projectService.AddDocumentToPotentialProjectsAsync(DocumentFilePath, DisposalToken); var ownerProject = _projectManager.GetLoadedProject(ownerProjectKey); @@ -738,7 +778,7 @@ public async Task RemoveDocument_RemovesDocumentFromAllOwnerProjects() ProjectFilePath, IntermediateOutputPath1, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); var ownerProjectKey2 = await _projectService.AddProjectAsync( ProjectFilePath, IntermediateOutputPath2, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); - await _projectService.AddDocumentAsync(DocumentFilePath, DisposalToken); + await _projectService.AddDocumentToPotentialProjectsAsync(DocumentFilePath, DisposalToken); var ownerProject1 = _projectManager.GetLoadedProject(ownerProjectKey1); var ownerProject2 = _projectManager.GetLoadedProject(ownerProjectKey2); @@ -767,7 +807,7 @@ public async Task RemoveOpenDocument_RemovesDocumentFromOwnerProject_MovesToMisc var ownerProjectKey = await _projectService.AddProjectAsync( ProjectFilePath, IntermediateOutputPath, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); - await _projectService.AddDocumentAsync(DocumentFilePath, DisposalToken); + await _projectService.AddDocumentToPotentialProjectsAsync(DocumentFilePath, DisposalToken); await _projectService.OpenDocumentAsync(DocumentFilePath, s_emptyText, version: 42, DisposalToken); var ownerProject = _projectManager.GetLoadedProject(ownerProjectKey); @@ -794,7 +834,7 @@ public async Task RemoveDocument_RemovesDocumentFromMiscellaneousProject() // Arrange const string DocumentFilePath = "document.cshtml"; - await _projectService.AddDocumentAsync(DocumentFilePath, DisposalToken); + await _projectService.AddDocumentToMiscProjectAsync(DocumentFilePath, DisposalToken); var miscProject = _snapshotResolver.GetMiscellaneousProject(); @@ -863,7 +903,7 @@ public async Task UpdateDocument_ChangesDocumentInOwnerProject() var ownerProjectKey = await _projectService.AddProjectAsync( ProjectFilePath, IntermediateOutputPath, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); - await _projectService.AddDocumentAsync(DocumentFilePath, DisposalToken); + await _projectService.AddDocumentToPotentialProjectsAsync(DocumentFilePath, DisposalToken); await _projectService.OpenDocumentAsync(DocumentFilePath, s_emptyText, version: 42, DisposalToken); var ownerProject = _projectManager.GetLoadedProject(ownerProjectKey); @@ -896,7 +936,7 @@ public async Task UpdateDocument_ChangesDocumentInAllOwnerProjects() ProjectFilePath, IntermediateOutputPath1, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); var ownerProjectKey2 = await _projectService.AddProjectAsync( ProjectFilePath, IntermediateOutputPath2, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); - await _projectService.AddDocumentAsync(DocumentFilePath, DisposalToken); + await _projectService.AddDocumentToPotentialProjectsAsync(DocumentFilePath, DisposalToken); await _projectService.OpenDocumentAsync(DocumentFilePath, s_emptyText, version: 42, DisposalToken); var ownerProject1 = _projectManager.GetLoadedProject(ownerProjectKey1); @@ -923,7 +963,7 @@ public async Task UpdateDocument_ChangesDocumentInMiscProject() // Arrange const string DocumentFilePath = "document.cshtml"; - await _projectService.AddDocumentAsync(DocumentFilePath, DisposalToken); + await _projectService.AddDocumentToPotentialProjectsAsync(DocumentFilePath, DisposalToken); await _projectService.OpenDocumentAsync(DocumentFilePath, s_emptyText, version: 42, DisposalToken); var miscProject = _snapshotResolver.GetMiscellaneousProject(); @@ -953,7 +993,7 @@ public async Task UpdateDocument_TracksKnownDocumentVersion() var ownerProjectKey = await _projectService.AddProjectAsync( ProjectFilePath, IntermediateOutputPath, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); - await _projectService.AddDocumentAsync(DocumentFilePath, DisposalToken); + await _projectService.AddDocumentToPotentialProjectsAsync(DocumentFilePath, DisposalToken); var ownerProject = _projectManager.GetLoadedProject(ownerProjectKey); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Refactoring/RenameEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Refactoring/RenameEndpointTest.cs index 23f638c183d..407bf13fbc2 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Refactoring/RenameEndpointTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Refactoring/RenameEndpointTest.cs @@ -631,7 +631,7 @@ public async Task Handle_Rename_SingleServer_DoesNotDelegateForRazor() return textLoaderMock.Object; }); - var projectService = new RazorProjectService( + var projectService = new TestRazorProjectService( remoteTextLoaderFactoryMock.Object, snapshotResolver, documentVersionCache, @@ -646,12 +646,12 @@ await projectManager.UpdateAsync(updater => updater.ProjectWorkspaceStateChanged(projectKey1, ProjectWorkspaceState.Create(tagHelpers)); }); - await projectService.AddDocumentAsync(s_componentFilePath1, DisposalToken); - await projectService.AddDocumentAsync(s_componentFilePath2, DisposalToken); - await projectService.AddDocumentAsync(s_directoryFilePath1, DisposalToken); - await projectService.AddDocumentAsync(s_directoryFilePath2, DisposalToken); - await projectService.AddDocumentAsync(s_componentFilePath1337, DisposalToken); - await projectService.AddDocumentAsync(s_indexFilePath1, DisposalToken); + await projectService.AddDocumentToPotentialProjectsAsync(s_componentFilePath1, DisposalToken); + await projectService.AddDocumentToPotentialProjectsAsync(s_componentFilePath2, DisposalToken); + await projectService.AddDocumentToPotentialProjectsAsync(s_directoryFilePath1, DisposalToken); + await projectService.AddDocumentToPotentialProjectsAsync(s_directoryFilePath2, DisposalToken); + await projectService.AddDocumentToPotentialProjectsAsync(s_componentFilePath1337, DisposalToken); + await projectService.AddDocumentToPotentialProjectsAsync(s_indexFilePath1, DisposalToken); await projectService.UpdateDocumentAsync(s_componentFilePath1, SourceText.From(ComponentText1), version: 42, DisposalToken); await projectService.UpdateDocumentAsync(s_componentFilePath2, SourceText.From(ComponentText2), version: 42, DisposalToken); @@ -668,9 +668,9 @@ await projectManager.UpdateAsync(updater => updater.ProjectWorkspaceStateChanged(projectKey2, ProjectWorkspaceState.Create(tagHelpers)); }); - await projectService.AddDocumentAsync(s_componentFilePath3, DisposalToken); - await projectService.AddDocumentAsync(s_componentFilePath4, DisposalToken); - await projectService.AddDocumentAsync(s_componentWithParamFilePath, DisposalToken); + await projectService.AddDocumentToPotentialProjectsAsync(s_componentFilePath3, DisposalToken); + await projectService.AddDocumentToPotentialProjectsAsync(s_componentFilePath4, DisposalToken); + await projectService.AddDocumentToPotentialProjectsAsync(s_componentWithParamFilePath, DisposalToken); await projectService.UpdateDocumentAsync(s_componentFilePath3, SourceText.From(ComponentText3), version: 42, DisposalToken); await projectService.UpdateDocumentAsync(s_componentFilePath4, SourceText.From(ComponentText4), version: 42, DisposalToken); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/TestRazorProjectService.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/TestRazorProjectService.cs new file mode 100644 index 00000000000..bfefe440cfe --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/TestRazorProjectService.cs @@ -0,0 +1,58 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.LanguageServer.Common; +using Microsoft.AspNetCore.Razor.Serialization; +using Microsoft.AspNetCore.Razor.Utilities; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.Logging; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; + +namespace Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; + +internal class TestRazorProjectService( + RemoteTextLoaderFactory remoteTextLoaderFactory, + ISnapshotResolver snapshotResolver, + IDocumentVersionCache documentVersionCache, + IProjectSnapshotManager projectManager, + ILoggerFactory loggerFactory) + : RazorProjectService(remoteTextLoaderFactory, snapshotResolver, documentVersionCache, projectManager, loggerFactory) +{ + private readonly ISnapshotResolver _snapshotResolver = snapshotResolver; + + public async Task AddDocumentToPotentialProjectsAsync(string textDocumentPath, CancellationToken cancellationToken) + { + foreach (var projectSnapshot in _snapshotResolver.FindPotentialProjects(textDocumentPath)) + { + var normalizedProjectPath = FilePathNormalizer.NormalizeDirectory(projectSnapshot.FilePath); + var documents = ImmutableArray + .CreateRange(projectSnapshot.DocumentFilePaths) + .Add(textDocumentPath) + .Select(d => new DocumentSnapshotHandle(d, d, FileKinds.GetFileKindFromFilePath(d))) + .ToImmutableArray(); + + await this.UpdateProjectAsync(projectSnapshot.Key, projectSnapshot.Configuration, projectSnapshot.RootNamespace, projectSnapshot.DisplayName, projectSnapshot.ProjectWorkspaceState, + documents, cancellationToken).ConfigureAwait(false); + } + } + + private static string GetTargetPath(string documentFilePath, string normalizedProjectPath) + { + var targetFilePath = FilePathNormalizer.Normalize(documentFilePath); + if (targetFilePath.StartsWith(normalizedProjectPath, FilePathComparison.Instance)) + { + // Make relative + targetFilePath = documentFilePath[normalizedProjectPath.Length..]; + } + + // Representing all of our host documents with a re-normalized target path to workaround GetRelatedDocument limitations. + var normalizedTargetFilePath = targetFilePath.Replace('/', '\\').TrimStart('\\'); + + return normalizedTargetFilePath; + } +}