Convert RazorProjectService to an interface (#9819)

Just converting another pure abstract class to an interface.
This commit is contained in:
Dustin Campbell 2024-01-16 08:31:00 -08:00 коммит произвёл GitHub
Родитель 3afb7ffffa acc5d9fb60
Коммит 71bfe681f7
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
18 изменённых файлов: 551 добавлений и 550 удалений

Просмотреть файл

@ -20,14 +20,14 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.DocumentSynchronization;
[LanguageServerEndpoint(Methods.TextDocumentDidChangeName)]
internal class DocumentDidChangeEndpoint(
ProjectSnapshotManagerDispatcher projectSnapshotManagerDispatcher,
RazorProjectService razorProjectService,
IRazorProjectService razorProjectService,
IRazorLoggerFactory loggerFactory)
: IRazorNotificationHandler<DidChangeTextDocumentParams>, ITextDocumentIdentifierHandler<DidChangeTextDocumentParams, TextDocumentIdentifier>, ICapabilitiesProvider
{
public bool MutatesSolutionState => true;
private readonly ProjectSnapshotManagerDispatcher _projectSnapshotManagerDispatcher = projectSnapshotManagerDispatcher;
private readonly RazorProjectService _projectService = razorProjectService;
private readonly IRazorProjectService _projectService = razorProjectService;
private readonly ILogger _logger = loggerFactory.CreateLogger<DocumentDidChangeEndpoint>();
public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, VSInternalClientCapabilities clientCapabilities)

Просмотреть файл

@ -16,13 +16,13 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.DocumentSynchronization;
internal class DocumentDidCloseEndpoint : IRazorNotificationHandler<DidCloseTextDocumentParams>, ITextDocumentIdentifierHandler<DidCloseTextDocumentParams, TextDocumentIdentifier>
{
private readonly ProjectSnapshotManagerDispatcher _projectSnapshotManagerDispatcher;
private readonly RazorProjectService _projectService;
private readonly IRazorProjectService _projectService;
public bool MutatesSolutionState => true;
public DocumentDidCloseEndpoint(
ProjectSnapshotManagerDispatcher projectSnapshotManagerDispatcher,
RazorProjectService projectService)
IRazorProjectService projectService)
{
_projectSnapshotManagerDispatcher = projectSnapshotManagerDispatcher ?? throw new ArgumentNullException(nameof(projectSnapshotManagerDispatcher));
_projectService = projectService ?? throw new ArgumentNullException(nameof(projectService));

Просмотреть файл

@ -18,9 +18,9 @@ internal class DocumentDidOpenEndpoint : IRazorNotificationHandler<DidOpenTextDo
public bool MutatesSolutionState => true;
private readonly ProjectSnapshotManagerDispatcher _projectSnapshotManagerDispatcher;
private readonly RazorProjectService _projectService;
private readonly IRazorProjectService _projectService;
public DocumentDidOpenEndpoint(ProjectSnapshotManagerDispatcher projectSnapshotManagerDispatcher, RazorProjectService razorProjectService)
public DocumentDidOpenEndpoint(ProjectSnapshotManagerDispatcher projectSnapshotManagerDispatcher, IRazorProjectService razorProjectService)
{
_projectSnapshotManagerDispatcher = projectSnapshotManagerDispatcher;
_projectService = razorProjectService;

Просмотреть файл

@ -204,7 +204,7 @@ internal static class IServiceCollectionExtensions
services.AddSingleton<RemoteTextLoaderFactory, DefaultRemoteTextLoaderFactory>();
services.AddSingleton<ISnapshotResolver, SnapshotResolver>();
services.AddSingleton<RazorProjectService, DefaultRazorProjectService>();
services.AddSingleton<IRazorProjectService, RazorProjectService>();
services.AddSingleton<IProjectSnapshotChangeTrigger, OpenDocumentGenerator>();
services.AddSingleton<IRazorDocumentMappingService, RazorDocumentMappingService>();
services.AddSingleton<RazorFileChangeDetectorManager>();

Просмотреть файл

@ -21,14 +21,14 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer;
internal class ProjectConfigurationStateSynchronizer : IProjectConfigurationFileChangeListener
{
private readonly ProjectSnapshotManagerDispatcher _projectSnapshotManagerDispatcher;
private readonly RazorProjectService _projectService;
private readonly IRazorProjectService _projectService;
private readonly ILogger _logger;
private readonly Dictionary<string, ProjectKey> _configurationToProjectMap;
internal readonly Dictionary<ProjectKey, DelayedProjectInfo> ProjectInfoMap;
public ProjectConfigurationStateSynchronizer(
ProjectSnapshotManagerDispatcher projectSnapshotManagerDispatcher,
RazorProjectService projectService,
IRazorProjectService projectService,
IRazorLoggerFactory loggerFactory)
{
_projectSnapshotManagerDispatcher = projectSnapshotManagerDispatcher;

Просмотреть файл

@ -1,504 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System;
using System.Collections.Immutable;
using System.Composition;
using System.Diagnostics;
using System.IO;
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.ProjectSystem;
using Microsoft.AspNetCore.Razor.Serialization;
using Microsoft.AspNetCore.Razor.Utilities;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.CodeAnalysis.Text;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem;
[Export(typeof(RazorProjectService)), Shared]
[method: ImportingConstructor]
internal class DefaultRazorProjectService(
ProjectSnapshotManagerDispatcher projectSnapshotManagerDispatcher,
RemoteTextLoaderFactory remoteTextLoaderFactory,
ISnapshotResolver snapshotResolver,
IDocumentVersionCache documentVersionCache,
ProjectSnapshotManagerAccessor projectSnapshotManagerAccessor,
IRazorLoggerFactory loggerFactory)
: RazorProjectService
{
private readonly ProjectSnapshotManagerAccessor _projectSnapshotManagerAccessor = projectSnapshotManagerAccessor ?? throw new ArgumentNullException(nameof(projectSnapshotManagerAccessor));
private readonly ProjectSnapshotManagerDispatcher _projectSnapshotManagerDispatcher = projectSnapshotManagerDispatcher ?? throw new ArgumentNullException(nameof(projectSnapshotManagerDispatcher));
private readonly RemoteTextLoaderFactory _remoteTextLoaderFactory = remoteTextLoaderFactory ?? throw new ArgumentNullException(nameof(remoteTextLoaderFactory));
private readonly ISnapshotResolver _snapshotResolver = snapshotResolver ?? throw new ArgumentNullException(nameof(snapshotResolver));
private readonly IDocumentVersionCache _documentVersionCache = documentVersionCache ?? throw new ArgumentNullException(nameof(documentVersionCache));
private readonly ILogger _logger = loggerFactory.CreateLogger<DefaultRazorProjectService>();
public override void AddDocument(string filePath)
{
_projectSnapshotManagerDispatcher.AssertDispatcherThread();
var textDocumentPath = FilePathNormalizer.Normalize(filePath);
var added = false;
foreach (var projectSnapshot in _snapshotResolver.FindPotentialProjects(textDocumentPath))
{
added = true;
AddDocumentToProject(projectSnapshot, textDocumentPath);
}
if (!added)
{
AddDocumentToProject(_snapshotResolver.GetMiscellaneousProject(), textDocumentPath);
}
void AddDocumentToProject(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('\\');
var hostDocument = new HostDocument(textDocumentPath, normalizedTargetFilePath);
var textLoader = _remoteTextLoaderFactory.Create(textDocumentPath);
var projectSnapshotManager = _projectSnapshotManagerAccessor.Instance;
_logger.LogInformation("Adding document '{filePath}' to project '{projectKey}'.", filePath, projectSnapshot.Key);
projectSnapshotManager.DocumentAdded(projectSnapshot.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 (projectSnapshotManager.IsDocumentOpen(textDocumentPath) &&
projectSnapshotManager.GetLoadedProject(projectSnapshot.Key) is { } project &&
project.GetDocument(textDocumentPath) is { } document)
{
_ = document.GetGeneratedOutputAsync();
}
}
}
public override void OpenDocument(string filePath, SourceText sourceText, int version)
{
_projectSnapshotManagerDispatcher.AssertDispatcherThread();
var textDocumentPath = FilePathNormalizer.Normalize(filePath);
// We are okay to use the non-project-key overload of TryResolveDocument here because we really are just checking if the document
// has been added to _any_ project. AddDocument will take care of adding to all of the necessary ones, and then below we ensure
// we process them all too
if (!_snapshotResolver.TryResolveDocumentInAnyProject(textDocumentPath, out _))
{
// Document hasn't been added. This usually occurs when VSCode trumps all other initialization
// processes and pre-initializes already open documents.
AddDocument(filePath);
}
ActOnDocumentInMultipleProjects(filePath, (projectSnapshot, textDocumentPath) =>
{
_logger.LogInformation("Opening document '{textDocumentPath}' in project '{projectKey}'.", textDocumentPath, projectSnapshot.Key);
_projectSnapshotManagerAccessor.Instance.DocumentOpened(projectSnapshot.Key, textDocumentPath, sourceText);
});
// Use a separate loop, as the above call modified out projects, so we have to make sure we're operating on the latest snapshot
ActOnDocumentInMultipleProjects(filePath, (projectSnapshot, textDocumentPath) =>
{
TrackDocumentVersion(projectSnapshot, textDocumentPath, version, startGenerating: true);
});
}
public override void CloseDocument(string filePath)
{
_projectSnapshotManagerDispatcher.AssertDispatcherThread();
ActOnDocumentInMultipleProjects(filePath, (projectSnapshot, textDocumentPath) =>
{
var textLoader = _remoteTextLoaderFactory.Create(filePath);
_logger.LogInformation("Closing document '{textDocumentPath}' in project '{projectKey}'.", textDocumentPath, projectSnapshot.Key);
_projectSnapshotManagerAccessor.Instance.DocumentClosed(projectSnapshot.Key, textDocumentPath, textLoader);
});
}
public override void RemoveDocument(string filePath)
{
_projectSnapshotManagerDispatcher.AssertDispatcherThread();
ActOnDocumentInMultipleProjects(filePath, (projectSnapshot, textDocumentPath) =>
{
if (!projectSnapshot.DocumentFilePaths.Contains(textDocumentPath, FilePathComparer.Instance))
{
_logger.LogInformation("Containing project is not tracking document '{filePath}'", textDocumentPath);
return;
}
if (projectSnapshot.GetDocument(textDocumentPath) is not DocumentSnapshot documentSnapshot)
{
_logger.LogError("Containing project does not contain document '{filePath}'", textDocumentPath);
return;
}
// If the document is open, we can't remove it, because we could still get a request for it, and that
// request would fail. Instead we move it to the miscellaneous project, just like if we got notified of
// a remove via the project.razor.bin
if (_projectSnapshotManagerAccessor.Instance.IsDocumentOpen(textDocumentPath))
{
_logger.LogInformation("Moving document '{textDocumentPath}' from project '{projectKey}' to misc files because it is open.", textDocumentPath, projectSnapshot.Key);
var miscellaneousProject = (ProjectSnapshot)_snapshotResolver.GetMiscellaneousProject();
if (projectSnapshot != miscellaneousProject)
{
MoveDocument(textDocumentPath, projectSnapshot, miscellaneousProject);
}
}
else
{
_logger.LogInformation("Removing document '{textDocumentPath}' from project '{projectKey}'.", textDocumentPath, projectSnapshot.Key);
_projectSnapshotManagerAccessor.Instance.DocumentRemoved(projectSnapshot.Key, documentSnapshot.State.HostDocument);
}
});
}
public override void UpdateDocument(string filePath, SourceText sourceText, int version)
{
_projectSnapshotManagerDispatcher.AssertDispatcherThread();
ActOnDocumentInMultipleProjects(filePath, (project, textDocumentPath) =>
{
_logger.LogTrace("Updating document '{textDocumentPath}' in {projectKey}.", textDocumentPath, project.Key);
_projectSnapshotManagerAccessor.Instance.DocumentChanged(project.Key, textDocumentPath, sourceText);
});
// Use a separate loop, as the above call modified out projects, so we have to make sure we're operating on the latest snapshot
ActOnDocumentInMultipleProjects(filePath, (projectSnapshot, textDocumentPath) =>
{
TrackDocumentVersion(projectSnapshot, textDocumentPath, version, startGenerating: false);
});
}
private void ActOnDocumentInMultipleProjects(string filePath, Action<IProjectSnapshot, string> action)
{
var textDocumentPath = FilePathNormalizer.Normalize(filePath);
if (!_snapshotResolver.TryResolveAllProjects(textDocumentPath, out var projectSnapshots))
{
projectSnapshots = new[] { _snapshotResolver.GetMiscellaneousProject() };
}
foreach (var project in projectSnapshots)
{
action(project, textDocumentPath);
}
}
public override ProjectKey AddProject(string filePath, string intermediateOutputPath, RazorConfiguration? configuration, string? rootNamespace, string displayName)
{
_projectSnapshotManagerDispatcher.AssertDispatcherThread();
var normalizedPath = FilePathNormalizer.Normalize(filePath);
var hostProject = new HostProject(normalizedPath, intermediateOutputPath, configuration ?? FallbackRazorConfiguration.Latest, rootNamespace, displayName);
// ProjectAdded will no-op if the project already exists
_projectSnapshotManagerAccessor.Instance.ProjectAdded(hostProject);
_logger.LogInformation("Added project '{filePath}' with key {key} to project system.", filePath, hostProject.Key);
TryMigrateMiscellaneousDocumentsToProject();
return hostProject.Key;
}
public override void UpdateProject(
ProjectKey projectKey,
RazorConfiguration? configuration,
string? rootNamespace,
string displayName,
ProjectWorkspaceState projectWorkspaceState,
ImmutableArray<DocumentSnapshotHandle> documents)
{
_projectSnapshotManagerDispatcher.AssertDispatcherThread();
var project = (ProjectSnapshot?)_projectSnapshotManagerAccessor.Instance.GetLoadedProject(projectKey);
if (project is null)
{
// Never tracked the project to begin with, noop.
_logger.LogInformation("Failed to update untracked project '{projectKey}'.", projectKey);
return;
}
UpdateProjectDocuments(documents, project.Key);
if (!projectWorkspaceState.Equals(ProjectWorkspaceState.Default))
{
_logger.LogInformation("Updating project '{key}' TagHelpers ({projectWorkspaceState.TagHelpers.Count}) and C# Language Version ({projectWorkspaceState.CSharpLanguageVersion}).",
project.Key, projectWorkspaceState.TagHelpers.Length, projectWorkspaceState.CSharpLanguageVersion);
}
_projectSnapshotManagerAccessor.Instance.ProjectWorkspaceStateChanged(project.Key, projectWorkspaceState);
var currentHostProject = project.HostProject;
var currentConfiguration = currentHostProject.Configuration;
if (currentConfiguration.ConfigurationName == configuration?.ConfigurationName &&
currentHostProject.RootNamespace == rootNamespace)
{
_logger.LogTrace("Updating project '{key}'. The project is already using configuration '{configuration.ConfigurationName}' and root namespace '{rootNamespace}'.",
project.Key, configuration.ConfigurationName, rootNamespace);
return;
}
if (configuration is null)
{
configuration = FallbackRazorConfiguration.Latest;
_logger.LogInformation("Updating project '{key}' to use the latest configuration ('{configuration.ConfigurationName}')'.", project.Key, configuration.ConfigurationName);
}
else if (currentConfiguration.ConfigurationName != configuration.ConfigurationName)
{
_logger.LogInformation("Updating project '{key}' to Razor configuration '{configuration.ConfigurationName}' with language version '{configuration.LanguageVersion}'.",
project.Key, configuration.ConfigurationName, configuration.LanguageVersion);
}
if (currentHostProject.RootNamespace != rootNamespace)
{
_logger.LogInformation("Updating project '{key}''s root namespace to '{rootNamespace}'.", project.Key, rootNamespace);
}
var hostProject = new HostProject(project.FilePath, project.IntermediateOutputPath, configuration, rootNamespace, displayName);
_projectSnapshotManagerAccessor.Instance.ProjectConfigurationChanged(hostProject);
}
private void UpdateProjectDocuments(ImmutableArray<DocumentSnapshotHandle> documents, ProjectKey projectKey)
{
_logger.LogDebug("UpdateProjectDocuments for {projectKey} with {documentCount} documents.", projectKey, documents.Length);
var project = (ProjectSnapshot)_projectSnapshotManagerAccessor.Instance.GetLoadedProject(projectKey).AssumeNotNull();
var currentHostProject = project.HostProject;
var projectDirectory = FilePathNormalizer.GetNormalizedDirectoryName(project.FilePath);
var documentMap = documents.ToDictionary(document => EnsureFullPath(document.FilePath, projectDirectory), FilePathComparer.Instance);
var miscellaneousProject = (ProjectSnapshot)_snapshotResolver.GetMiscellaneousProject();
// "Remove" any unnecessary documents by putting them into the misc project
foreach (var documentFilePath in project.DocumentFilePaths)
{
if (documentMap.ContainsKey(documentFilePath))
{
// This document still exists in the updated project
continue;
}
_logger.LogDebug("Document '{documentFilePath}' no longer exists in project '{projectKey}'. Moving to miscellaneous project.", documentFilePath, projectKey);
MoveDocument(documentFilePath, project, miscellaneousProject);
}
project = (ProjectSnapshot)_projectSnapshotManagerAccessor.Instance.GetLoadedProject(projectKey).AssumeNotNull();
// Update existing documents
foreach (var documentFilePath in project.DocumentFilePaths)
{
if (!documentMap.TryGetValue(documentFilePath, out var documentHandle))
{
// Document exists in the project but not in the configured documents. Chances are the project configuration is from a fallback
// configuration case (< 2.1) or the project isn't fully loaded yet.
continue;
}
if (project.GetDocument(documentFilePath) is not DocumentSnapshot documentSnapshot)
{
continue;
}
var currentHostDocument = documentSnapshot.State.HostDocument;
var newFilePath = EnsureFullPath(documentHandle.FilePath, projectDirectory);
var newHostDocument = new HostDocument(newFilePath, documentHandle.TargetPath, documentHandle.FileKind);
if (HostDocumentComparer.Instance.Equals(currentHostDocument, newHostDocument))
{
// Current and "new" host documents are equivalent
continue;
}
_logger.LogTrace("Updating document '{newHostDocument.FilePath}''s file kind to '{newHostDocument.FileKind}' and target path to '{newHostDocument.TargetPath}'.",
newHostDocument.FilePath, newHostDocument.FileKind, newHostDocument.TargetPath);
_projectSnapshotManagerAccessor.Instance.DocumentRemoved(currentHostProject.Key, currentHostDocument);
var remoteTextLoader = _remoteTextLoaderFactory.Create(newFilePath);
_projectSnapshotManagerAccessor.Instance.DocumentAdded(currentHostProject.Key, newHostDocument, remoteTextLoader);
}
project = (ProjectSnapshot)_projectSnapshotManagerAccessor.Instance.GetLoadedProject(project.Key).AssumeNotNull();
miscellaneousProject = (ProjectSnapshot)_snapshotResolver.GetMiscellaneousProject();
// Add (or migrate from misc) any new documents
foreach (var documentKvp in documentMap)
{
var documentFilePath = documentKvp.Key;
if (project.DocumentFilePaths.Contains(documentFilePath, FilePathComparer.Instance))
{
// Already know about this document
continue;
}
if (miscellaneousProject.DocumentFilePaths.Contains(documentFilePath, FilePathComparer.Instance))
{
MoveDocument(documentFilePath, miscellaneousProject, project);
}
else
{
var documentHandle = documentKvp.Value;
var remoteTextLoader = _remoteTextLoaderFactory.Create(documentFilePath);
var newHostDocument = new HostDocument(documentFilePath, documentHandle.TargetPath, documentHandle.FileKind);
_logger.LogInformation("Adding new document '{documentFilePath}' to project '{key}'.", documentFilePath, currentHostProject.Key);
_projectSnapshotManagerAccessor.Instance.DocumentAdded(currentHostProject.Key, newHostDocument, remoteTextLoader);
}
}
}
private void MoveDocument(string documentFilePath, IProjectSnapshot fromProject, ProjectSnapshot toProject)
{
Debug.Assert(fromProject.DocumentFilePaths.Contains(documentFilePath, FilePathComparer.Instance));
Debug.Assert(!toProject.DocumentFilePaths.Contains(documentFilePath, FilePathComparer.Instance));
if (fromProject.GetDocument(documentFilePath) is not DocumentSnapshot documentSnapshot)
{
return;
}
var currentHostDocument = documentSnapshot.State.HostDocument;
var textLoader = new DocumentSnapshotTextLoader(documentSnapshot);
var newHostDocument = new HostDocument(documentSnapshot.FilePath, documentSnapshot.TargetPath, documentSnapshot.FileKind);
_logger.LogInformation("Moving '{documentFilePath}' from the '{fromProject.Key}' project to '{toProject.Key}' project.",
documentFilePath, fromProject.Key, toProject.Key);
_projectSnapshotManagerAccessor.Instance.DocumentRemoved(fromProject.Key, currentHostDocument);
_projectSnapshotManagerAccessor.Instance.DocumentAdded(toProject.Key, newHostDocument, textLoader);
}
private static string EnsureFullPath(string filePath, string projectDirectory)
{
var normalizedFilePath = FilePathNormalizer.Normalize(filePath);
if (!normalizedFilePath.StartsWith(projectDirectory, FilePathComparison.Instance))
{
var absolutePath = Path.Combine(projectDirectory, normalizedFilePath);
normalizedFilePath = FilePathNormalizer.Normalize(absolutePath);
}
return normalizedFilePath;
}
// Internal for testing
internal void TryMigrateDocumentsFromRemovedProject(IProjectSnapshot project)
{
_projectSnapshotManagerDispatcher.AssertDispatcherThread();
var miscellaneousProject = _snapshotResolver.GetMiscellaneousProject();
foreach (var documentFilePath in project.DocumentFilePaths)
{
if (project.GetDocument(documentFilePath) is not DocumentSnapshot documentSnapshot)
{
continue;
}
var toProject = _snapshotResolver.FindPotentialProjects(documentFilePath).FirstOrDefault()
?? miscellaneousProject;
var textLoader = new DocumentSnapshotTextLoader(documentSnapshot);
var defaultToProject = (ProjectSnapshot)toProject;
var newHostDocument = new HostDocument(documentSnapshot.FilePath, documentSnapshot.TargetPath, documentSnapshot.FileKind);
_logger.LogInformation("Migrating '{documentFilePath}' from the '{project.Key}' project to '{toProject.KEy}' project.",
documentFilePath, project.Key, toProject.Key);
_projectSnapshotManagerAccessor.Instance.DocumentAdded(defaultToProject.Key, newHostDocument, textLoader);
}
}
// Internal for testing
internal void TryMigrateMiscellaneousDocumentsToProject()
{
_projectSnapshotManagerDispatcher.AssertDispatcherThread();
var miscellaneousProject = _snapshotResolver.GetMiscellaneousProject();
foreach (var documentFilePath in miscellaneousProject.DocumentFilePaths)
{
var projectSnapshot = _snapshotResolver.FindPotentialProjects(documentFilePath).FirstOrDefault();
if (projectSnapshot is null)
{
continue;
}
if (miscellaneousProject.GetDocument(documentFilePath) is not DocumentSnapshot documentSnapshot)
{
continue;
}
// Remove from miscellaneous project
var defaultMiscProject = (ProjectSnapshot)miscellaneousProject;
_projectSnapshotManagerAccessor.Instance.DocumentRemoved(defaultMiscProject.Key, documentSnapshot.State.HostDocument);
// Add to new project
var textLoader = new DocumentSnapshotTextLoader(documentSnapshot);
var defaultProject = (ProjectSnapshot)projectSnapshot;
var newHostDocument = new HostDocument(documentSnapshot.FilePath, documentSnapshot.TargetPath);
_logger.LogInformation("Migrating '{documentFilePath}' from the '{miscellaneousProject.Key}' project to '{projectSnapshot.Key}' project.",
documentFilePath, miscellaneousProject.Key, projectSnapshot.Key);
_projectSnapshotManagerAccessor.Instance.DocumentAdded(defaultProject.Key, newHostDocument, textLoader);
}
}
private void TrackDocumentVersion(IProjectSnapshot projectSnapshot, string textDocumentPath, int version, bool startGenerating)
{
if (projectSnapshot.GetDocument(FilePathNormalizer.Normalize(textDocumentPath)) is not { } documentSnapshot)
{
return;
}
_documentVersionCache.TrackDocumentVersion(documentSnapshot, version);
if (startGenerating)
{
// Start generating the C# for the document so it can immediately be ready for incoming requests.
_ = documentSnapshot.GetGeneratedOutputAsync();
}
}
private class DelegatingTextLoader : TextLoader
{
private readonly IDocumentSnapshot _fromDocument;
public DelegatingTextLoader(IDocumentSnapshot fromDocument)
{
_fromDocument = fromDocument ?? throw new ArgumentNullException(nameof(fromDocument));
}
public override async Task<TextAndVersion> LoadTextAndVersionAsync(
LoadTextOptions options,
CancellationToken cancellationToken)
{
var sourceText = await _fromDocument.GetTextAsync().ConfigureAwait(false);
var version = await _fromDocument.GetTextVersionAsync().ConfigureAwait(false);
var textAndVersion = TextAndVersion.Create(sourceText, version.GetNewerVersion());
return textAndVersion;
}
}
}

Просмотреть файл

@ -0,0 +1,35 @@
// 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 Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.ProjectSystem;
using Microsoft.AspNetCore.Razor.Serialization;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem;
internal interface IRazorProjectService
{
void AddDocument(string filePath);
void OpenDocument(string filePath, SourceText sourceText, int version);
void UpdateDocument(string filePath, SourceText sourceText, int version);
void CloseDocument(string filePath);
void RemoveDocument(string filePath);
ProjectKey AddProject(
string filePath,
string intermediateOutputPath,
RazorConfiguration? configuration,
string? rootNamespace,
string displayName);
void UpdateProject(
ProjectKey projectKey,
RazorConfiguration? configuration,
string? rootNamespace,
string displayName,
ProjectWorkspaceState projectWorkspaceState,
ImmutableArray<DocumentSnapshotHandle> documents);
}

Просмотреть файл

@ -1,34 +1,504 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System;
using System.Collections.Immutable;
using System.Composition;
using System.Diagnostics;
using System.IO;
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.ProjectSystem;
using Microsoft.AspNetCore.Razor.Serialization;
using Microsoft.AspNetCore.Razor.Utilities;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.CodeAnalysis.Text;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem;
internal abstract class RazorProjectService
[Export(typeof(IRazorProjectService)), Shared]
[method: ImportingConstructor]
internal class RazorProjectService(
ProjectSnapshotManagerDispatcher projectSnapshotManagerDispatcher,
RemoteTextLoaderFactory remoteTextLoaderFactory,
ISnapshotResolver snapshotResolver,
IDocumentVersionCache documentVersionCache,
ProjectSnapshotManagerAccessor projectSnapshotManagerAccessor,
IRazorLoggerFactory loggerFactory)
: IRazorProjectService
{
public abstract void AddDocument(string filePath);
private readonly ProjectSnapshotManagerAccessor _projectSnapshotManagerAccessor = projectSnapshotManagerAccessor ?? throw new ArgumentNullException(nameof(projectSnapshotManagerAccessor));
private readonly ProjectSnapshotManagerDispatcher _projectSnapshotManagerDispatcher = projectSnapshotManagerDispatcher ?? throw new ArgumentNullException(nameof(projectSnapshotManagerDispatcher));
private readonly RemoteTextLoaderFactory _remoteTextLoaderFactory = remoteTextLoaderFactory ?? throw new ArgumentNullException(nameof(remoteTextLoaderFactory));
private readonly ISnapshotResolver _snapshotResolver = snapshotResolver ?? throw new ArgumentNullException(nameof(snapshotResolver));
private readonly IDocumentVersionCache _documentVersionCache = documentVersionCache ?? throw new ArgumentNullException(nameof(documentVersionCache));
private readonly ILogger _logger = loggerFactory.CreateLogger<RazorProjectService>();
public abstract void OpenDocument(string filePath, SourceText sourceText, int version);
public void AddDocument(string filePath)
{
_projectSnapshotManagerDispatcher.AssertDispatcherThread();
public abstract void CloseDocument(string filePath);
var textDocumentPath = FilePathNormalizer.Normalize(filePath);
public abstract void RemoveDocument(string filePath);
var added = false;
foreach (var projectSnapshot in _snapshotResolver.FindPotentialProjects(textDocumentPath))
{
added = true;
AddDocumentToProject(projectSnapshot, textDocumentPath);
}
public abstract void UpdateDocument(string filePath, SourceText sourceText, int version);
if (!added)
{
AddDocumentToProject(_snapshotResolver.GetMiscellaneousProject(), textDocumentPath);
}
public abstract ProjectKey AddProject(string filePath, string intermediateOutputPath, RazorConfiguration? configuration, string? rootNamespace, string displayName);
void AddDocumentToProject(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;
}
public abstract void UpdateProject(
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('\\');
var hostDocument = new HostDocument(textDocumentPath, normalizedTargetFilePath);
var textLoader = _remoteTextLoaderFactory.Create(textDocumentPath);
var projectSnapshotManager = _projectSnapshotManagerAccessor.Instance;
_logger.LogInformation("Adding document '{filePath}' to project '{projectKey}'.", filePath, projectSnapshot.Key);
projectSnapshotManager.DocumentAdded(projectSnapshot.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 (projectSnapshotManager.IsDocumentOpen(textDocumentPath) &&
projectSnapshotManager.GetLoadedProject(projectSnapshot.Key) is { } project &&
project.GetDocument(textDocumentPath) is { } document)
{
_ = document.GetGeneratedOutputAsync();
}
}
}
public void OpenDocument(string filePath, SourceText sourceText, int version)
{
_projectSnapshotManagerDispatcher.AssertDispatcherThread();
var textDocumentPath = FilePathNormalizer.Normalize(filePath);
// We are okay to use the non-project-key overload of TryResolveDocument here because we really are just checking if the document
// has been added to _any_ project. AddDocument will take care of adding to all of the necessary ones, and then below we ensure
// we process them all too
if (!_snapshotResolver.TryResolveDocumentInAnyProject(textDocumentPath, out _))
{
// Document hasn't been added. This usually occurs when VSCode trumps all other initialization
// processes and pre-initializes already open documents.
AddDocument(filePath);
}
ActOnDocumentInMultipleProjects(filePath, (projectSnapshot, textDocumentPath) =>
{
_logger.LogInformation("Opening document '{textDocumentPath}' in project '{projectKey}'.", textDocumentPath, projectSnapshot.Key);
_projectSnapshotManagerAccessor.Instance.DocumentOpened(projectSnapshot.Key, textDocumentPath, sourceText);
});
// Use a separate loop, as the above call modified out projects, so we have to make sure we're operating on the latest snapshot
ActOnDocumentInMultipleProjects(filePath, (projectSnapshot, textDocumentPath) =>
{
TrackDocumentVersion(projectSnapshot, textDocumentPath, version, startGenerating: true);
});
}
public void CloseDocument(string filePath)
{
_projectSnapshotManagerDispatcher.AssertDispatcherThread();
ActOnDocumentInMultipleProjects(filePath, (projectSnapshot, textDocumentPath) =>
{
var textLoader = _remoteTextLoaderFactory.Create(filePath);
_logger.LogInformation("Closing document '{textDocumentPath}' in project '{projectKey}'.", textDocumentPath, projectSnapshot.Key);
_projectSnapshotManagerAccessor.Instance.DocumentClosed(projectSnapshot.Key, textDocumentPath, textLoader);
});
}
public void RemoveDocument(string filePath)
{
_projectSnapshotManagerDispatcher.AssertDispatcherThread();
ActOnDocumentInMultipleProjects(filePath, (projectSnapshot, textDocumentPath) =>
{
if (!projectSnapshot.DocumentFilePaths.Contains(textDocumentPath, FilePathComparer.Instance))
{
_logger.LogInformation("Containing project is not tracking document '{filePath}'", textDocumentPath);
return;
}
if (projectSnapshot.GetDocument(textDocumentPath) is not DocumentSnapshot documentSnapshot)
{
_logger.LogError("Containing project does not contain document '{filePath}'", textDocumentPath);
return;
}
// If the document is open, we can't remove it, because we could still get a request for it, and that
// request would fail. Instead we move it to the miscellaneous project, just like if we got notified of
// a remove via the project.razor.bin
if (_projectSnapshotManagerAccessor.Instance.IsDocumentOpen(textDocumentPath))
{
_logger.LogInformation("Moving document '{textDocumentPath}' from project '{projectKey}' to misc files because it is open.", textDocumentPath, projectSnapshot.Key);
var miscellaneousProject = (ProjectSnapshot)_snapshotResolver.GetMiscellaneousProject();
if (projectSnapshot != miscellaneousProject)
{
MoveDocument(textDocumentPath, projectSnapshot, miscellaneousProject);
}
}
else
{
_logger.LogInformation("Removing document '{textDocumentPath}' from project '{projectKey}'.", textDocumentPath, projectSnapshot.Key);
_projectSnapshotManagerAccessor.Instance.DocumentRemoved(projectSnapshot.Key, documentSnapshot.State.HostDocument);
}
});
}
public void UpdateDocument(string filePath, SourceText sourceText, int version)
{
_projectSnapshotManagerDispatcher.AssertDispatcherThread();
ActOnDocumentInMultipleProjects(filePath, (project, textDocumentPath) =>
{
_logger.LogTrace("Updating document '{textDocumentPath}' in {projectKey}.", textDocumentPath, project.Key);
_projectSnapshotManagerAccessor.Instance.DocumentChanged(project.Key, textDocumentPath, sourceText);
});
// Use a separate loop, as the above call modified out projects, so we have to make sure we're operating on the latest snapshot
ActOnDocumentInMultipleProjects(filePath, (projectSnapshot, textDocumentPath) =>
{
TrackDocumentVersion(projectSnapshot, textDocumentPath, version, startGenerating: false);
});
}
private void ActOnDocumentInMultipleProjects(string filePath, Action<IProjectSnapshot, string> action)
{
var textDocumentPath = FilePathNormalizer.Normalize(filePath);
if (!_snapshotResolver.TryResolveAllProjects(textDocumentPath, out var projectSnapshots))
{
projectSnapshots = new[] { _snapshotResolver.GetMiscellaneousProject() };
}
foreach (var project in projectSnapshots)
{
action(project, textDocumentPath);
}
}
public ProjectKey AddProject(string filePath, string intermediateOutputPath, RazorConfiguration? configuration, string? rootNamespace, string displayName)
{
_projectSnapshotManagerDispatcher.AssertDispatcherThread();
var normalizedPath = FilePathNormalizer.Normalize(filePath);
var hostProject = new HostProject(normalizedPath, intermediateOutputPath, configuration ?? FallbackRazorConfiguration.Latest, rootNamespace, displayName);
// ProjectAdded will no-op if the project already exists
_projectSnapshotManagerAccessor.Instance.ProjectAdded(hostProject);
_logger.LogInformation("Added project '{filePath}' with key {key} to project system.", filePath, hostProject.Key);
TryMigrateMiscellaneousDocumentsToProject();
return hostProject.Key;
}
public void UpdateProject(
ProjectKey projectKey,
RazorConfiguration? configuration,
string? rootNamespace,
string displayName,
ProjectWorkspaceState projectWorkspaceState,
ImmutableArray<DocumentSnapshotHandle> documents);
ImmutableArray<DocumentSnapshotHandle> documents)
{
_projectSnapshotManagerDispatcher.AssertDispatcherThread();
var project = (ProjectSnapshot?)_projectSnapshotManagerAccessor.Instance.GetLoadedProject(projectKey);
if (project is null)
{
// Never tracked the project to begin with, noop.
_logger.LogInformation("Failed to update untracked project '{projectKey}'.", projectKey);
return;
}
UpdateProjectDocuments(documents, project.Key);
if (!projectWorkspaceState.Equals(ProjectWorkspaceState.Default))
{
_logger.LogInformation("Updating project '{key}' TagHelpers ({projectWorkspaceState.TagHelpers.Count}) and C# Language Version ({projectWorkspaceState.CSharpLanguageVersion}).",
project.Key, projectWorkspaceState.TagHelpers.Length, projectWorkspaceState.CSharpLanguageVersion);
}
_projectSnapshotManagerAccessor.Instance.ProjectWorkspaceStateChanged(project.Key, projectWorkspaceState);
var currentHostProject = project.HostProject;
var currentConfiguration = currentHostProject.Configuration;
if (currentConfiguration.ConfigurationName == configuration?.ConfigurationName &&
currentHostProject.RootNamespace == rootNamespace)
{
_logger.LogTrace("Updating project '{key}'. The project is already using configuration '{configuration.ConfigurationName}' and root namespace '{rootNamespace}'.",
project.Key, configuration.ConfigurationName, rootNamespace);
return;
}
if (configuration is null)
{
configuration = FallbackRazorConfiguration.Latest;
_logger.LogInformation("Updating project '{key}' to use the latest configuration ('{configuration.ConfigurationName}')'.", project.Key, configuration.ConfigurationName);
}
else if (currentConfiguration.ConfigurationName != configuration.ConfigurationName)
{
_logger.LogInformation("Updating project '{key}' to Razor configuration '{configuration.ConfigurationName}' with language version '{configuration.LanguageVersion}'.",
project.Key, configuration.ConfigurationName, configuration.LanguageVersion);
}
if (currentHostProject.RootNamespace != rootNamespace)
{
_logger.LogInformation("Updating project '{key}''s root namespace to '{rootNamespace}'.", project.Key, rootNamespace);
}
var hostProject = new HostProject(project.FilePath, project.IntermediateOutputPath, configuration, rootNamespace, displayName);
_projectSnapshotManagerAccessor.Instance.ProjectConfigurationChanged(hostProject);
}
private void UpdateProjectDocuments(ImmutableArray<DocumentSnapshotHandle> documents, ProjectKey projectKey)
{
_logger.LogDebug("UpdateProjectDocuments for {projectKey} with {documentCount} documents.", projectKey, documents.Length);
var project = (ProjectSnapshot)_projectSnapshotManagerAccessor.Instance.GetLoadedProject(projectKey).AssumeNotNull();
var currentHostProject = project.HostProject;
var projectDirectory = FilePathNormalizer.GetNormalizedDirectoryName(project.FilePath);
var documentMap = documents.ToDictionary(document => EnsureFullPath(document.FilePath, projectDirectory), FilePathComparer.Instance);
var miscellaneousProject = (ProjectSnapshot)_snapshotResolver.GetMiscellaneousProject();
// "Remove" any unnecessary documents by putting them into the misc project
foreach (var documentFilePath in project.DocumentFilePaths)
{
if (documentMap.ContainsKey(documentFilePath))
{
// This document still exists in the updated project
continue;
}
_logger.LogDebug("Document '{documentFilePath}' no longer exists in project '{projectKey}'. Moving to miscellaneous project.", documentFilePath, projectKey);
MoveDocument(documentFilePath, project, miscellaneousProject);
}
project = (ProjectSnapshot)_projectSnapshotManagerAccessor.Instance.GetLoadedProject(projectKey).AssumeNotNull();
// Update existing documents
foreach (var documentFilePath in project.DocumentFilePaths)
{
if (!documentMap.TryGetValue(documentFilePath, out var documentHandle))
{
// Document exists in the project but not in the configured documents. Chances are the project configuration is from a fallback
// configuration case (< 2.1) or the project isn't fully loaded yet.
continue;
}
if (project.GetDocument(documentFilePath) is not DocumentSnapshot documentSnapshot)
{
continue;
}
var currentHostDocument = documentSnapshot.State.HostDocument;
var newFilePath = EnsureFullPath(documentHandle.FilePath, projectDirectory);
var newHostDocument = new HostDocument(newFilePath, documentHandle.TargetPath, documentHandle.FileKind);
if (HostDocumentComparer.Instance.Equals(currentHostDocument, newHostDocument))
{
// Current and "new" host documents are equivalent
continue;
}
_logger.LogTrace("Updating document '{newHostDocument.FilePath}''s file kind to '{newHostDocument.FileKind}' and target path to '{newHostDocument.TargetPath}'.",
newHostDocument.FilePath, newHostDocument.FileKind, newHostDocument.TargetPath);
_projectSnapshotManagerAccessor.Instance.DocumentRemoved(currentHostProject.Key, currentHostDocument);
var remoteTextLoader = _remoteTextLoaderFactory.Create(newFilePath);
_projectSnapshotManagerAccessor.Instance.DocumentAdded(currentHostProject.Key, newHostDocument, remoteTextLoader);
}
project = (ProjectSnapshot)_projectSnapshotManagerAccessor.Instance.GetLoadedProject(project.Key).AssumeNotNull();
miscellaneousProject = (ProjectSnapshot)_snapshotResolver.GetMiscellaneousProject();
// Add (or migrate from misc) any new documents
foreach (var documentKvp in documentMap)
{
var documentFilePath = documentKvp.Key;
if (project.DocumentFilePaths.Contains(documentFilePath, FilePathComparer.Instance))
{
// Already know about this document
continue;
}
if (miscellaneousProject.DocumentFilePaths.Contains(documentFilePath, FilePathComparer.Instance))
{
MoveDocument(documentFilePath, miscellaneousProject, project);
}
else
{
var documentHandle = documentKvp.Value;
var remoteTextLoader = _remoteTextLoaderFactory.Create(documentFilePath);
var newHostDocument = new HostDocument(documentFilePath, documentHandle.TargetPath, documentHandle.FileKind);
_logger.LogInformation("Adding new document '{documentFilePath}' to project '{key}'.", documentFilePath, currentHostProject.Key);
_projectSnapshotManagerAccessor.Instance.DocumentAdded(currentHostProject.Key, newHostDocument, remoteTextLoader);
}
}
}
private void MoveDocument(string documentFilePath, IProjectSnapshot fromProject, ProjectSnapshot toProject)
{
Debug.Assert(fromProject.DocumentFilePaths.Contains(documentFilePath, FilePathComparer.Instance));
Debug.Assert(!toProject.DocumentFilePaths.Contains(documentFilePath, FilePathComparer.Instance));
if (fromProject.GetDocument(documentFilePath) is not DocumentSnapshot documentSnapshot)
{
return;
}
var currentHostDocument = documentSnapshot.State.HostDocument;
var textLoader = new DocumentSnapshotTextLoader(documentSnapshot);
var newHostDocument = new HostDocument(documentSnapshot.FilePath, documentSnapshot.TargetPath, documentSnapshot.FileKind);
_logger.LogInformation("Moving '{documentFilePath}' from the '{fromProject.Key}' project to '{toProject.Key}' project.",
documentFilePath, fromProject.Key, toProject.Key);
_projectSnapshotManagerAccessor.Instance.DocumentRemoved(fromProject.Key, currentHostDocument);
_projectSnapshotManagerAccessor.Instance.DocumentAdded(toProject.Key, newHostDocument, textLoader);
}
private static string EnsureFullPath(string filePath, string projectDirectory)
{
var normalizedFilePath = FilePathNormalizer.Normalize(filePath);
if (!normalizedFilePath.StartsWith(projectDirectory, FilePathComparison.Instance))
{
var absolutePath = Path.Combine(projectDirectory, normalizedFilePath);
normalizedFilePath = FilePathNormalizer.Normalize(absolutePath);
}
return normalizedFilePath;
}
// Internal for testing
internal void TryMigrateDocumentsFromRemovedProject(IProjectSnapshot project)
{
_projectSnapshotManagerDispatcher.AssertDispatcherThread();
var miscellaneousProject = _snapshotResolver.GetMiscellaneousProject();
foreach (var documentFilePath in project.DocumentFilePaths)
{
if (project.GetDocument(documentFilePath) is not DocumentSnapshot documentSnapshot)
{
continue;
}
var toProject = _snapshotResolver.FindPotentialProjects(documentFilePath).FirstOrDefault()
?? miscellaneousProject;
var textLoader = new DocumentSnapshotTextLoader(documentSnapshot);
var defaultToProject = (ProjectSnapshot)toProject;
var newHostDocument = new HostDocument(documentSnapshot.FilePath, documentSnapshot.TargetPath, documentSnapshot.FileKind);
_logger.LogInformation("Migrating '{documentFilePath}' from the '{project.Key}' project to '{toProject.KEy}' project.",
documentFilePath, project.Key, toProject.Key);
_projectSnapshotManagerAccessor.Instance.DocumentAdded(defaultToProject.Key, newHostDocument, textLoader);
}
}
// Internal for testing
internal void TryMigrateMiscellaneousDocumentsToProject()
{
_projectSnapshotManagerDispatcher.AssertDispatcherThread();
var miscellaneousProject = _snapshotResolver.GetMiscellaneousProject();
foreach (var documentFilePath in miscellaneousProject.DocumentFilePaths)
{
var projectSnapshot = _snapshotResolver.FindPotentialProjects(documentFilePath).FirstOrDefault();
if (projectSnapshot is null)
{
continue;
}
if (miscellaneousProject.GetDocument(documentFilePath) is not DocumentSnapshot documentSnapshot)
{
continue;
}
// Remove from miscellaneous project
var defaultMiscProject = (ProjectSnapshot)miscellaneousProject;
_projectSnapshotManagerAccessor.Instance.DocumentRemoved(defaultMiscProject.Key, documentSnapshot.State.HostDocument);
// Add to new project
var textLoader = new DocumentSnapshotTextLoader(documentSnapshot);
var defaultProject = (ProjectSnapshot)projectSnapshot;
var newHostDocument = new HostDocument(documentSnapshot.FilePath, documentSnapshot.TargetPath);
_logger.LogInformation("Migrating '{documentFilePath}' from the '{miscellaneousProject.Key}' project to '{projectSnapshot.Key}' project.",
documentFilePath, miscellaneousProject.Key, projectSnapshot.Key);
_projectSnapshotManagerAccessor.Instance.DocumentAdded(defaultProject.Key, newHostDocument, textLoader);
}
}
private void TrackDocumentVersion(IProjectSnapshot projectSnapshot, string textDocumentPath, int version, bool startGenerating)
{
if (projectSnapshot.GetDocument(FilePathNormalizer.Normalize(textDocumentPath)) is not { } documentSnapshot)
{
return;
}
_documentVersionCache.TrackDocumentVersion(documentSnapshot, version);
if (startGenerating)
{
// Start generating the C# for the document so it can immediately be ready for incoming requests.
_ = documentSnapshot.GetGeneratedOutputAsync();
}
}
private class DelegatingTextLoader : TextLoader
{
private readonly IDocumentSnapshot _fromDocument;
public DelegatingTextLoader(IDocumentSnapshot fromDocument)
{
_fromDocument = fromDocument ?? throw new ArgumentNullException(nameof(fromDocument));
}
public override async Task<TextAndVersion> LoadTextAndVersionAsync(
LoadTextOptions options,
CancellationToken cancellationToken)
{
var sourceText = await _fromDocument.GetTextAsync().ConfigureAwait(false);
var version = await _fromDocument.GetTextVersionAsync().ConfigureAwait(false);
var textAndVersion = TextAndVersion.Create(sourceText, version.GetNewerVersion());
return textAndVersion;
}
}
}

Просмотреть файл

@ -11,11 +11,11 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer;
internal class RazorFileSynchronizer : IRazorFileChangeListener
{
private readonly ProjectSnapshotManagerDispatcher _projectSnapshotManagerDispatcher;
private readonly RazorProjectService _projectService;
private readonly IRazorProjectService _projectService;
public RazorFileSynchronizer(
ProjectSnapshotManagerDispatcher projectSnapshotManagerDispatcher,
RazorProjectService projectService)
IRazorProjectService projectService)
{
if (projectSnapshotManagerDispatcher is null)
{

Просмотреть файл

@ -17,11 +17,11 @@ namespace Microsoft.VisualStudio.LanguageServerClient.Razor.Cohost;
[method: ImportingConstructor]
internal class DidChangeHandler(
ProjectSnapshotManagerDispatcher projectSnapshotManagerDispatcher,
RazorProjectService razorProjectService,
IRazorProjectService razorProjectService,
OpenDocumentGenerator openDocumentGenerator) : IRazorCohostDidChangeHandler
{
private readonly ProjectSnapshotManagerDispatcher _projectSnapshotManagerDispatcher = projectSnapshotManagerDispatcher;
private readonly RazorProjectService _razorProjectService = razorProjectService;
private readonly IRazorProjectService _razorProjectService = razorProjectService;
private readonly OpenDocumentGenerator _openDocumentGenerator = openDocumentGenerator;
public async Task HandleAsync(Uri uri, int version, SourceText sourceText, CancellationToken cancellationToken)

Просмотреть файл

@ -13,10 +13,10 @@ namespace Microsoft.VisualStudio.LanguageServerClient.Razor.Cohost;
[Export(typeof(IRazorCohostDidCloseHandler)), Shared]
[method: ImportingConstructor]
internal class DidCloseHandler(ProjectSnapshotManagerDispatcher projectSnapshotManagerDispatcher, RazorProjectService razorProjectService) : IRazorCohostDidCloseHandler
internal class DidCloseHandler(ProjectSnapshotManagerDispatcher projectSnapshotManagerDispatcher, IRazorProjectService razorProjectService) : IRazorCohostDidCloseHandler
{
private readonly ProjectSnapshotManagerDispatcher _projectSnapshotManagerDispatcher = projectSnapshotManagerDispatcher;
private readonly RazorProjectService _razorProjectService = razorProjectService;
private readonly IRazorProjectService _razorProjectService = razorProjectService;
public Task HandleAsync(Uri uri, CancellationToken cancellationToken)
{

Просмотреть файл

@ -17,11 +17,11 @@ namespace Microsoft.VisualStudio.LanguageServerClient.Razor.Cohost;
[method: ImportingConstructor]
internal class DidOpenHandler(
ProjectSnapshotManagerDispatcher projectSnapshotManagerDispatcher,
RazorProjectService razorProjectService,
IRazorProjectService razorProjectService,
OpenDocumentGenerator openDocumentGenerator) : IRazorCohostDidOpenHandler
{
private readonly ProjectSnapshotManagerDispatcher _projectSnapshotManagerDispatcher = projectSnapshotManagerDispatcher;
private readonly RazorProjectService _razorProjectService = razorProjectService;
private readonly IRazorProjectService _razorProjectService = razorProjectService;
private readonly OpenDocumentGenerator _openDocumentGenerator = openDocumentGenerator;
public async Task HandleAsync(Uri uri, int version, SourceText sourceText, CancellationToken cancellationToken)

Просмотреть файл

@ -1243,7 +1243,7 @@ public class DefaultRazorProjectServiceTest(ITestOutputHelper testOutput) : Lang
document => Assert.Equal(documentFilePath2, document.FilePath));
}
private DefaultRazorProjectService CreateProjectService(
private RazorProjectService CreateProjectService(
ISnapshotResolver snapshotResolver,
ProjectSnapshotManagerBase projectSnapshotManager,
IDocumentVersionCache documentVersionCache = null)
@ -1262,7 +1262,7 @@ public class DefaultRazorProjectServiceTest(ITestOutputHelper testOutput) : Lang
}
var remoteTextLoaderFactory = Mock.Of<RemoteTextLoaderFactory>(factory => factory.Create(It.IsAny<string>()) == Mock.Of<TextLoader>(MockBehavior.Strict), MockBehavior.Strict);
var projectService = new DefaultRazorProjectService(
var projectService = new RazorProjectService(
Dispatcher,
remoteTextLoaderFactory,
snapshotResolver,

Просмотреть файл

@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.DocumentSynchronization;
public class DocumentDidChangeEndpointTest(ITestOutputHelper testOutput) : LanguageServerTestBase(testOutput)
{
private readonly RazorProjectService _projectService = Mock.Of<RazorProjectService>(MockBehavior.Strict);
private readonly IRazorProjectService _projectService = Mock.Of<IRazorProjectService>(MockBehavior.Strict);
[Fact]
public void ApplyContentChanges_SingleChange()
@ -107,7 +107,7 @@ public class DocumentDidChangeEndpointTest(ITestOutputHelper testOutput) : Langu
var sourceText = "<p>";
var codeDocument = CreateCodeDocument(sourceText);
var documentContext = CreateDocumentContext(documentPath, codeDocument);
var projectService = new Mock<RazorProjectService>(MockBehavior.Strict);
var projectService = new Mock<IRazorProjectService>(MockBehavior.Strict);
projectService.Setup(service => service.UpdateDocument(It.IsAny<string>(), It.IsAny<SourceText>(), It.IsAny<int>()))
.Callback<string, SourceText, int>((path, text, version) =>
{

Просмотреть файл

@ -20,7 +20,7 @@ public class DocumentDidCloseEndpointTest(ITestOutputHelper testOutput) : Langua
{
// Arrange
var documentPath = "C:/path/to/document.cshtml";
var projectService = new Mock<RazorProjectService>(MockBehavior.Strict);
var projectService = new Mock<IRazorProjectService>(MockBehavior.Strict);
projectService.Setup(service => service.CloseDocument(It.IsAny<string>()))
.Callback<string>((path) => Assert.Equal(documentPath, path));
var endpoint = new DocumentDidCloseEndpoint(Dispatcher, projectService.Object);

Просмотреть файл

@ -21,7 +21,7 @@ public class DocumentDidOpenEndpointTest(ITestOutputHelper testOutput) : Languag
{
// Arrange
var documentPath = "C:/path/to/document.cshtml";
var projectService = new Mock<RazorProjectService>(MockBehavior.Strict);
var projectService = new Mock<IRazorProjectService>(MockBehavior.Strict);
projectService.Setup(service => service.OpenDocument(It.IsAny<string>(), It.IsAny<SourceText>(), It.IsAny<int>()))
.Callback<string, SourceText, int>((path, text, version) =>
{

Просмотреть файл

@ -29,7 +29,7 @@ public class ProjectConfigurationStateSynchronizerTest(ITestOutputHelper testOut
public async Task ProjectConfigurationFileChanged_Removed_UnknownDocumentNoops()
{
// Arrange
var projectService = new Mock<RazorProjectService>(MockBehavior.Strict);
var projectService = new Mock<IRazorProjectService>(MockBehavior.Strict);
var synchronizer = GetSynchronizer(projectService.Object);
var deserializerMock = new Mock<IRazorProjectInfoDeserializer>(MockBehavior.Strict);
var args = new ProjectConfigurationFileChangeEventArgs(
@ -59,7 +59,7 @@ public class ProjectConfigurationStateSynchronizerTest(ITestOutputHelper testOut
ImmutableArray<DocumentSnapshotHandle>.Empty);
var intermediateOutputPath = Path.GetDirectoryName(FilePathNormalizer.Normalize(projectInfo.SerializedFilePath));
var projectKey = TestProjectKey.Create(intermediateOutputPath);
var projectService = new Mock<RazorProjectService>(MockBehavior.Strict);
var projectService = new Mock<IRazorProjectService>(MockBehavior.Strict);
projectService
.Setup(x => x.AddProject(
projectInfo.FilePath,
@ -117,7 +117,7 @@ public class ProjectConfigurationStateSynchronizerTest(ITestOutputHelper testOut
public async Task ProjectConfigurationFileChanged_Added_CantDeserialize_Noops()
{
// Arrange
var projectService = new Mock<RazorProjectService>(MockBehavior.Strict);
var projectService = new Mock<IRazorProjectService>(MockBehavior.Strict);
var synchronizer = GetSynchronizer(projectService.Object);
var deserializerMock = new Mock<IRazorProjectInfoDeserializer>(MockBehavior.Strict);
@ -152,7 +152,7 @@ public class ProjectConfigurationStateSynchronizerTest(ITestOutputHelper testOut
ImmutableArray<DocumentSnapshotHandle>.Empty);
var intermediateOutputPath = Path.GetDirectoryName(FilePathNormalizer.Normalize(projectInfo.SerializedFilePath));
var projectKey = TestProjectKey.Create(intermediateOutputPath);
var projectService = new Mock<RazorProjectService>(MockBehavior.Strict);
var projectService = new Mock<IRazorProjectService>(MockBehavior.Strict);
projectService
.Setup(service => service.AddProject(
projectInfo.FilePath,
@ -200,7 +200,7 @@ public class ProjectConfigurationStateSynchronizerTest(ITestOutputHelper testOut
ImmutableArray<DocumentSnapshotHandle>.Empty);
var intermediateOutputPath = Path.GetDirectoryName(FilePathNormalizer.Normalize(projectInfo.SerializedFilePath));
var projectKey = TestProjectKey.Create(intermediateOutputPath);
var projectService = new Mock<RazorProjectService>(MockBehavior.Strict);
var projectService = new Mock<IRazorProjectService>(MockBehavior.Strict);
projectService
.Setup(service => service.AddProject(
projectInfo.FilePath,
@ -268,7 +268,7 @@ public class ProjectConfigurationStateSynchronizerTest(ITestOutputHelper testOut
ImmutableArray<DocumentSnapshotHandle>.Empty);
var intermediateOutputPath = Path.GetDirectoryName(FilePathNormalizer.Normalize(initialProjectInfo.SerializedFilePath));
var projectKey = TestProjectKey.Create(intermediateOutputPath);
var projectService = new Mock<RazorProjectService>(MockBehavior.Strict);
var projectService = new Mock<IRazorProjectService>(MockBehavior.Strict);
projectService
.Setup(service => service.AddProject(
initialProjectInfo.FilePath,
@ -352,7 +352,7 @@ public class ProjectConfigurationStateSynchronizerTest(ITestOutputHelper testOut
ImmutableArray<DocumentSnapshotHandle>.Empty);
var intermediateOutputPath = Path.GetDirectoryName(FilePathNormalizer.Normalize(initialProjectInfo.SerializedFilePath));
var projectKey = TestProjectKey.Create(intermediateOutputPath);
var projectService = new Mock<RazorProjectService>(MockBehavior.Strict);
var projectService = new Mock<IRazorProjectService>(MockBehavior.Strict);
projectService
.Setup(service => service.AddProject(
initialProjectInfo.FilePath,
@ -428,7 +428,7 @@ public class ProjectConfigurationStateSynchronizerTest(ITestOutputHelper testOut
public async Task ProjectConfigurationFileChanged_Changed_UntrackedProject_Noops()
{
// Arrange
var projectService = new Mock<RazorProjectService>(MockBehavior.Strict);
var projectService = new Mock<IRazorProjectService>(MockBehavior.Strict);
var synchronizer = GetSynchronizer(projectService.Object);
var changedDeserializerMock = new Mock<IRazorProjectInfoDeserializer>(MockBehavior.Strict);
@ -467,7 +467,7 @@ public class ProjectConfigurationStateSynchronizerTest(ITestOutputHelper testOut
ImmutableArray<DocumentSnapshotHandle>.Empty);
var intermediateOutputPath = Path.GetDirectoryName(FilePathNormalizer.Normalize(projectInfo.SerializedFilePath));
var projectKey = TestProjectKey.Create(intermediateOutputPath);
var projectService = new Mock<RazorProjectService>(MockBehavior.Strict);
var projectService = new Mock<IRazorProjectService>(MockBehavior.Strict);
projectService
.Setup(service => service.AddProject(
projectInfo.FilePath,
@ -526,7 +526,7 @@ public class ProjectConfigurationStateSynchronizerTest(ITestOutputHelper testOut
}
}
private ProjectConfigurationStateSynchronizer GetSynchronizer(RazorProjectService razorProjectService)
private ProjectConfigurationStateSynchronizer GetSynchronizer(IRazorProjectService razorProjectService)
{
var synchronizer = new ProjectConfigurationStateSynchronizer(Dispatcher, razorProjectService, LoggerFactory);
synchronizer.EnqueueDelay = 5;

Просмотреть файл

@ -18,7 +18,7 @@ public class RazorFileSynchronizerTest(ITestOutputHelper testOutput) : LanguageS
{
// Arrange
var filePath = "/path/to/file.razor";
var projectService = new Mock<RazorProjectService>(MockBehavior.Strict);
var projectService = new Mock<IRazorProjectService>(MockBehavior.Strict);
projectService.Setup(service => service.AddDocument(filePath)).Verifiable();
var synchronizer = new RazorFileSynchronizer(Dispatcher, projectService.Object);
@ -35,7 +35,7 @@ public class RazorFileSynchronizerTest(ITestOutputHelper testOutput) : LanguageS
{
// Arrange
var filePath = "/path/to/file.cshtml";
var projectService = new Mock<RazorProjectService>(MockBehavior.Strict);
var projectService = new Mock<IRazorProjectService>(MockBehavior.Strict);
projectService.Setup(service => service.AddDocument(filePath)).Verifiable();
var synchronizer = new RazorFileSynchronizer(Dispatcher, projectService.Object);
@ -52,7 +52,7 @@ public class RazorFileSynchronizerTest(ITestOutputHelper testOutput) : LanguageS
{
// Arrange
var filePath = "/path/to/file.razor";
var projectService = new Mock<RazorProjectService>(MockBehavior.Strict);
var projectService = new Mock<IRazorProjectService>(MockBehavior.Strict);
projectService.Setup(service => service.RemoveDocument(filePath)).Verifiable();
var synchronizer = new RazorFileSynchronizer(Dispatcher, projectService.Object);
@ -69,7 +69,7 @@ public class RazorFileSynchronizerTest(ITestOutputHelper testOutput) : LanguageS
{
// Arrange
var filePath = "/path/to/file.cshtml";
var projectService = new Mock<RazorProjectService>(MockBehavior.Strict);
var projectService = new Mock<IRazorProjectService>(MockBehavior.Strict);
projectService.Setup(service => service.RemoveDocument(filePath)).Verifiable();
var synchronizer = new RazorFileSynchronizer(Dispatcher, projectService.Object);