diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Microsoft.CodeAnalysis.Razor.Compiler.csproj b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Microsoft.CodeAnalysis.Razor.Compiler.csproj
index 3d491fee7b9..6b441266e8c 100644
--- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Microsoft.CodeAnalysis.Razor.Compiler.csproj
+++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Microsoft.CodeAnalysis.Razor.Compiler.csproj
@@ -68,6 +68,7 @@
+
diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.RazorProviders.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.RazorProviders.cs
index 8b802d89a68..eb089d0a7f4 100644
--- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.RazorProviders.cs
+++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.RazorProviders.cs
@@ -77,25 +77,37 @@ private static (SourceGeneratorProjectItem?, Diagnostic?) ComputeProjectItems((A
var (additionalText, globalOptions) = pair;
var options = globalOptions.GetOptions(additionalText);
- string relativePath;
- if (options.TryGetValue("build_metadata.AdditionalFiles.TargetPath", out var encodedRelativePath))
+ string? relativePath = null;
+ var hasTargetPath = options.TryGetValue("build_metadata.AdditionalFiles.TargetPath", out var encodedRelativePath);
+ if (hasTargetPath && !string.IsNullOrWhiteSpace(encodedRelativePath))
{
- // TargetPath is optional, but must have a value if provided.
- if (string.IsNullOrWhiteSpace(encodedRelativePath))
- {
- var diagnostic = Diagnostic.Create(
- RazorDiagnostics.TargetPathNotProvided,
- Location.None,
- additionalText.Path);
- return (null, diagnostic);
- }
-
relativePath = Encoding.UTF8.GetString(Convert.FromBase64String(encodedRelativePath));
}
- else
+ else if (globalOptions.GlobalOptions.TryGetValue("build_property.MSBuildProjectDirectory", out var projectPath) &&
+ projectPath is { Length: > 0 } &&
+ additionalText.Path.StartsWith(projectPath, StringComparison.OrdinalIgnoreCase))
+ {
+ // Fallback, when TargetPath isn't specified but we know about the project directory, we can do our own calulation of
+ // the project relative path, and use that as the target path. This is an easy way for a project that isn't using the
+ // Razor SDK to still get TargetPath functionality without the complexity of specifying metadata on every item.
+ relativePath = additionalText.Path[projectPath.Length..].TrimStart(['/', '\\']);
+ }
+ else if (!hasTargetPath)
{
- // If the TargetPath is not provided, we effectively assume its in the root of the project.
- relativePath = Path.GetFileName(additionalText.Path);
+ // If the TargetPath is not provided, it could be a Misc Files situation, or just a project that isn't using the
+ // Web or Razor SDK. In this case, we just use the physical path.
+ relativePath = additionalText.Path;
+ }
+
+ if (relativePath is null)
+ {
+ // If we had a TargetPath but it was empty or whitespace, and we couldn't fall back to computing it from the project path
+ // that's an error.
+ var diagnostic = Diagnostic.Create(
+ RazorDiagnostics.TargetPathNotProvided,
+ Location.None,
+ additionalText.Path);
+ return (null, diagnostic);
}
options.TryGetValue("build_metadata.AdditionalFiles.CssScope", out var cssScope);
diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/Diagnostics/CohostDocumentPullDiagnosticsEndpointBase.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/Diagnostics/CohostDocumentPullDiagnosticsEndpointBase.cs
index d86516a7fc2..540a58d37df 100644
--- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/Diagnostics/CohostDocumentPullDiagnosticsEndpointBase.cs
+++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/Diagnostics/CohostDocumentPullDiagnosticsEndpointBase.cs
@@ -101,12 +101,7 @@ protected virtual TRequest CreateHtmlParams(Uri uri)
protected static Task TryGetGeneratedDocumentAsync(TextDocument razorDocument, CancellationToken cancellationToken)
{
- if (!razorDocument.TryComputeHintNameFromRazorDocument(out var hintName))
- {
- return SpecializedTasks.Null();
- }
-
- return razorDocument.Project.TryGetSourceGeneratedDocumentFromHintNameAsync(hintName, cancellationToken);
+ return razorDocument.Project.TryGetSourceGeneratedDocumentForRazorDocumentAsync(razorDocument, cancellationToken);
}
private async Task GetCSharpDiagnosticsAsync(TextDocument razorDocument, Guid correletionId, CancellationToken cancellationToken)
diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/ProjectExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/ProjectExtensions.cs
index 1901acc7784..967f55a17d7 100644
--- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/ProjectExtensions.cs
+++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/ProjectExtensions.cs
@@ -8,12 +8,14 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Razor;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.AspNetCore.Razor.Threading;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.Telemetry;
+using Microsoft.NET.Sdk.Razor.SourceGenerators;
namespace Microsoft.CodeAnalysis;
@@ -103,6 +105,76 @@ private static ImmutableArray GetTagHelperDescript
return generatedDocuments.SingleOrDefault(d => d.HintName == hintName);
}
+ ///
+ /// Finds source generated documents by iterating through all of them. In OOP there are better options!
+ ///
+ public static async Task TryGetSourceGeneratedDocumentForRazorDocumentAsync(this Project project, TextDocument razorDocument, CancellationToken cancellationToken)
+ {
+ if (razorDocument.FilePath is null)
+ {
+ return null;
+ }
+
+ var generatedDocuments = await project.GetSourceGeneratedDocumentsAsync(cancellationToken).ConfigureAwait(false);
+
+ // For misc files, and projects that don't have a globalconfig file (eg, non Razor SDK projects), the hint name will be based
+ // on the full path of the file.
+ var fullPathHintName = RazorSourceGenerator.GetIdentifierFromPath(razorDocument.FilePath);
+ // For normal Razor SDK projects, the hint name will be based on the project-relative path of the file.
+ var projectRelativeHintName = GetProjectRelativeHintName(razorDocument);
+
+ SourceGeneratedDocument? candidateDoc = null;
+ foreach (var doc in generatedDocuments)
+ {
+ if (!doc.IsRazorSourceGeneratedDocument())
+ {
+ continue;
+ }
+
+ if (doc.HintName == fullPathHintName)
+ {
+ // If the full path matches, we've found it for sure
+ return doc;
+ }
+ else if (doc.HintName == projectRelativeHintName)
+ {
+ if (candidateDoc is not null)
+ {
+ // Multiple documents with the same hint name found, can't be sure which one to return
+ // This can happen as a result of a bug in the source generator: https://github.com/dotnet/razor/issues/11578
+ candidateDoc = null;
+ break;
+ }
+
+ candidateDoc = doc;
+ }
+ }
+
+ return candidateDoc;
+
+ static string? GetProjectRelativeHintName(TextDocument razorDocument)
+ {
+ var filePath = razorDocument.FilePath.AsSpanOrDefault();
+ if (string.IsNullOrEmpty(razorDocument.Project.FilePath))
+ {
+ // Misc file - no project info to get a relative path
+ return null;
+ }
+
+ var projectFilePath = razorDocument.Project.FilePath.AsSpanOrDefault();
+ var projectBasePath = PathUtilities.GetDirectoryName(projectFilePath);
+ if (filePath.Length <= projectBasePath.Length)
+ {
+ // File must be from outside the project directory
+ return null;
+ }
+
+ var relativeDocumentPath = filePath[projectBasePath.Length..].TrimStart(['/', '\\']);
+
+ return RazorSourceGenerator.GetIdentifierFromPath(relativeDocumentPath);
+ }
+ }
+
///
/// Finds source generated documents by iterating through all of them. In OOP there are better options!
///
diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/TextDocumentExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/TextDocumentExtensions.cs
deleted file mode 100644
index 3d7f24baa6f..00000000000
--- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/TextDocumentExtensions.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System;
-using System.Diagnostics.CodeAnalysis;
-using Microsoft.AspNetCore.Razor;
-using Microsoft.NET.Sdk.Razor.SourceGenerators;
-
-namespace Microsoft.CodeAnalysis;
-
-internal static class TextDocumentExtensions
-{
- ///
- /// This method tries to compute the source generated hint name for a Razor document using only string manipulation
- ///
- ///
- /// This should only be used in the devenv process. In OOP we can look at the actual generated run result to find this
- /// information.
- ///
- public static bool TryComputeHintNameFromRazorDocument(this TextDocument razorDocument, [NotNullWhen(true)] out string? hintName)
- {
- if (razorDocument.FilePath is null)
- {
- hintName = null;
- return false;
- }
-
- var filePath = razorDocument.FilePath.AsSpanOrDefault();
- if (string.IsNullOrEmpty(razorDocument.Project.FilePath))
- {
- var fileName = filePath[filePath.LastIndexOfAny(['/', '\\'])..].TrimStart(['/', '\\']);
- hintName = RazorSourceGenerator.GetIdentifierFromPath(fileName);
- return hintName is not null;
- }
-
- var projectFilePath = razorDocument.Project.FilePath.AsSpanOrDefault();
- var projectBasePath = PathUtilities.GetDirectoryName(projectFilePath);
- if (filePath.Length <= projectBasePath.Length)
- {
- // File must be from outside the project directory
- hintName = null;
- return false;
- }
-
- var relativeDocumentPath = filePath[projectBasePath.Length..].TrimStart(['/', '\\']);
-
- hintName = RazorSourceGenerator.GetIdentifierFromPath(relativeDocumentPath);
-
- return hintName is not null;
- }
-}
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Cohosting/CohostTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Cohosting/CohostTestBase.cs
index 6ca282c248f..6b9c368f408 100644
--- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Cohosting/CohostTestBase.cs
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Cohosting/CohostTestBase.cs
@@ -6,16 +6,14 @@
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
-using System.Text;
using System.Threading.Tasks;
using Basic.Reference.Assemblies;
using Microsoft.AspNetCore.Razor;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Test.Common;
+using Microsoft.AspNetCore.Razor.Test.Common.Mef;
+using Microsoft.AspNetCore.Razor.Test.Common.Workspaces;
using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp;
-using Microsoft.CodeAnalysis.Diagnostics;
-using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Razor.Remote;
using Microsoft.CodeAnalysis.Razor.Workspaces;
@@ -23,7 +21,6 @@
using Microsoft.CodeAnalysis.Remote.Razor;
using Microsoft.CodeAnalysis.Remote.Razor.Logging;
using Microsoft.CodeAnalysis.Text;
-using Microsoft.NET.Sdk.Razor.SourceGenerators;
using Microsoft.VisualStudio.Composition;
using Roslyn.Test.Utilities;
using Xunit;
@@ -37,14 +34,23 @@ public abstract class CohostTestBase(ITestOutputHelper testOutputHelper) : Tooli
private TestIncompatibleProjectService _incompatibleProjectService = null!;
private RemoteClientInitializationOptions _clientInitializationOptions;
private RemoteClientLSPInitializationOptions _clientLSPInitializationOptions;
+ private CodeAnalysis.Workspace? _localWorkspace;
+ private ExportProvider? _localExportProvider;
private protected abstract IRemoteServiceInvoker RemoteServiceInvoker { get; }
private protected abstract IClientSettingsManager ClientSettingsManager { get; }
private protected abstract IFilePathService FilePathService { get; }
+ private protected abstract TestComposition LocalComposition { get; }
private protected TestIncompatibleProjectService IncompatibleProjectService => _incompatibleProjectService.AssumeNotNull();
private protected RemoteLanguageServerFeatureOptions FeatureOptions => OOPExportProvider.GetExportedValue();
private protected RemoteClientCapabilitiesService ClientCapabilitiesService => (RemoteClientCapabilitiesService)OOPExportProvider.GetExportedValue();
+ private protected CodeAnalysis.Workspace LocalWorkspace => _localWorkspace.AssumeNotNull();
+
+ ///
+ /// The export provider for client services (Roslyn)
+ ///
+ private protected ExportProvider LocalExportProvider => _localExportProvider.AssumeNotNull();
///
/// The export provider for Razor OOP services (not Roslyn)
@@ -97,8 +103,30 @@ protected override async Task InitializeAsync()
traceSource.Listeners.Add(new XunitTraceListener(TestOutputHelper));
await RemoteWorkspaceProvider.TestAccessor.InitializeRemoteExportProviderBuilderAsync(Path.GetTempPath(), traceSource, DisposalToken);
_ = RemoteWorkspaceProvider.Instance.GetWorkspace();
+
+ _localWorkspace = CreateLocalWorkspace();
+ }
+
+ private AdhocWorkspace CreateLocalWorkspace()
+ {
+ var composition = ConfigureLocalComposition(LocalComposition);
+
+ // We can't enforce that the composition is entirely valid, because we don't have a full MEF catalog, but we
+ // can assume there should be no errors related to Razor, and having this array makes debugging failures a lot
+ // easier.
+ var errors = composition.GetCompositionErrors().ToArray();
+ Assert.Empty(errors.Where(e => e.Contains("Razor")));
+
+ _localExportProvider = composition.ExportProviderFactory.CreateExportProvider();
+ AddDisposable(_localExportProvider);
+ var workspace = TestWorkspace.CreateWithDiagnosticAnalyzers(_localExportProvider);
+ AddDisposable(workspace);
+ return workspace;
}
+ private protected virtual TestComposition ConfigureLocalComposition(TestComposition composition)
+ => composition;
+
private protected abstract RemoteClientLSPInitializationOptions GetRemoteClientLSPInitializationOptions();
private protected void UpdateClientInitializationOptions(Func mutation)
@@ -155,108 +183,57 @@ protected static TextDocument CreateProjectAndRazorDocument(CodeAnalysis.Workspa
protected static TextDocument AddProjectAndRazorDocument(Solution solution, [DisallowNull] string? projectFilePath, ProjectId projectId, bool miscellaneousFile, DocumentId documentId, string documentFilePath, string contents, (string fileName, string contents)[]? additionalFiles, bool inGlobalNamespace)
{
- // We simulate a miscellaneous file project by not having a project file path.
- projectFilePath = miscellaneousFile ? null : projectFilePath;
- var projectName = miscellaneousFile ? "" : Path.GetFileNameWithoutExtension(projectFilePath).AssumeNotNull();
-
- var sgAssembly = typeof(RazorSourceGenerator).Assembly;
-
- var projectInfo = ProjectInfo
- .Create(
- projectId,
- VersionStamp.Create(),
- name: projectName,
- assemblyName: projectName,
- LanguageNames.CSharp,
- projectFilePath,
- compilationOptions: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
- .WithMetadataReferences(
- miscellaneousFile
- ? Net461.ReferenceInfos.All.Select(r => r.Reference) // This isn't quite what Roslyn does, but its close enough for our tests
- : AspNet80.ReferenceInfos.All.Select(r => r.Reference))
- .WithAnalyzerReferences([new AnalyzerFileReference(sgAssembly.Location, TestAnalyzerAssemblyLoader.LoadFromFile)]);
-
- if (!miscellaneousFile && !inGlobalNamespace)
- {
- projectInfo = projectInfo.WithDefaultNamespace(TestProjectData.SomeProject.RootNamespace);
- }
+ var builder = new RazorProjectBuilder(projectId);
- solution = solution.AddProject(projectInfo);
+ builder.AddReferences(miscellaneousFile
+ ? Net461.ReferenceInfos.All.Select(r => r.Reference) // This isn't quite what Roslyn does, but its close enough for our tests
+ : AspNet80.ReferenceInfos.All.Select(r => r.Reference));
+ builder.GenerateGlobalConfigFile = !miscellaneousFile;
+ builder.RootNamespace = null;
- solution = solution
- .AddAdditionalDocument(
- documentId,
- documentFilePath,
- SourceText.From(contents),
- filePath: documentFilePath);
+ builder.AddAdditionalDocument(documentId, documentFilePath, SourceText.From(contents));
if (!miscellaneousFile)
{
- solution = solution.AddAdditionalDocument(
- DocumentId.CreateNewId(projectId),
- name: TestProjectData.SomeProjectComponentImportFile1.FilePath,
- text: SourceText.From("""
- @using Microsoft.AspNetCore.Components
- @using Microsoft.AspNetCore.Components.Authorization
- @using Microsoft.AspNetCore.Components.Forms
- @using Microsoft.AspNetCore.Components.Routing
- @using Microsoft.AspNetCore.Components.Web
- """),
- filePath: TestProjectData.SomeProjectComponentImportFile1.FilePath)
- .AddAdditionalDocument(
- DocumentId.CreateNewId(projectId),
- name: "_ViewImports.cshtml",
- text: SourceText.From("""
- @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
- """),
- filePath: TestProjectData.SomeProjectImportFile.FilePath);
+ builder.ProjectFilePath = projectFilePath;
- if (additionalFiles is not null)
+ if (!inGlobalNamespace)
{
- foreach (var file in additionalFiles)
- {
- solution = Path.GetExtension(file.fileName) == ".cs"
- ? solution.AddDocument(DocumentId.CreateNewId(projectId), name: file.fileName, text: SourceText.From(file.contents), filePath: file.fileName)
- : solution.AddAdditionalDocument(DocumentId.CreateNewId(projectId), name: file.fileName, text: SourceText.From(file.contents), filePath: file.fileName);
- }
+ builder.RootNamespace = TestProjectData.SomeProject.RootNamespace;
}
- var globalConfigContent = new StringBuilder();
- globalConfigContent.AppendLine($"""
- is_global = true
-
- build_property.RazorLangVersion = {FallbackRazorConfiguration.Latest.LanguageVersion}
- build_property.RazorConfiguration = {FallbackRazorConfiguration.Latest.ConfigurationName}
- build_property.RootNamespace = {TestProjectData.SomeProject.RootNamespace}
-
- # This might suprise you, but by suppressing the source generator here, we're mirroring what happens in the Razor SDK
- build_property.SuppressRazorSourceGenerator = true
- """);
+ builder.AddAdditionalDocument(
+ filePath: TestProjectData.SomeProjectComponentImportFile1.FilePath,
+ text: SourceText.From("""
+ @using Microsoft.AspNetCore.Components
+ @using Microsoft.AspNetCore.Components.Authorization
+ @using Microsoft.AspNetCore.Components.Forms
+ @using Microsoft.AspNetCore.Components.Routing
+ @using Microsoft.AspNetCore.Components.Web
+ """));
+ builder.AddAdditionalDocument(
+ filePath: TestProjectData.SomeProjectImportFile.FilePath,
+ text: SourceText.From("""
+ @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
+ """));
- var projectBasePath = Path.GetDirectoryName(projectFilePath).AssumeNotNull();
- // Normally MS Build targets do this for us, but we're on our own!
- foreach (var razorDocument in solution.GetRequiredProject(projectId).AdditionalDocuments)
+ if (additionalFiles is not null)
{
- if (razorDocument.FilePath is not null &&
- razorDocument.FilePath.StartsWith(projectBasePath))
+ foreach (var file in additionalFiles)
{
- var relativePath = razorDocument.FilePath[(projectBasePath.Length + 1)..];
- globalConfigContent.AppendLine($"""
-
- [{razorDocument.FilePath.AssumeNotNull().Replace('\\', '/')}]
- build_metadata.AdditionalFiles.TargetPath = {Convert.ToBase64String(Encoding.UTF8.GetBytes(relativePath))}
- """);
+ if (Path.GetExtension(file.fileName) == ".cs")
+ {
+ builder.AddDocument(filePath: file.fileName, text: SourceText.From(file.contents));
+ }
+ else
+ {
+ builder.AddAdditionalDocument(filePath: file.fileName, text: SourceText.From(file.contents));
+ }
}
}
-
- solution = solution.AddAnalyzerConfigDocument(
- DocumentId.CreateNewId(projectId),
- name: ".globalconfig",
- text: SourceText.From(globalConfigContent.ToString()),
- filePath: Path.Combine(TestProjectData.SomeProjectPath, ".globalconfig"));
}
- return solution.GetAdditionalDocument(documentId).AssumeNotNull();
+ return builder.Build(solution).GetAdditionalDocument(documentId).AssumeNotNull();
}
protected static Uri FileUri(string projectRelativeFileName)
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Cohosting/RazorProjectBuilder.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Cohosting/RazorProjectBuilder.cs
new file mode 100644
index 00000000000..8bd7ebb30ad
--- /dev/null
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Cohosting/RazorProjectBuilder.cs
@@ -0,0 +1,157 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using Microsoft.AspNetCore.Razor;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Razor.ProjectSystem;
+using Microsoft.CodeAnalysis.Text;
+using Microsoft.NET.Sdk.Razor.SourceGenerators;
+using Roslyn.Test.Utilities;
+
+namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
+
+internal class RazorProjectBuilder(ProjectId? id = null)
+{
+ public ProjectId Id { get; } = id ?? ProjectId.CreateNewId();
+
+ public string? ProjectName { get; set; }
+
+ public string? ProjectFilePath
+ {
+ get;
+ set
+ {
+ ProjectName ??= Path.GetFileNameWithoutExtension(value);
+
+ field = value;
+ }
+ }
+
+ public string? RootNamespace { get; set; } = "ASP";
+
+ public bool ReferenceRazorSourceGenerator { get; set; } = true;
+ public bool GenerateGlobalConfigFile { get; set; } = true;
+ public bool GenerateMSBuildProjectDirectory { get; set; } = true;
+ public bool GenerateAdditionalDocumentMetadata { get; set; } = true;
+
+ private readonly List _references = [];
+ private readonly List<(DocumentId id, string name, SourceText text, string filePath)> _documents = [];
+ private readonly List<(DocumentId id, string name, SourceText text, string filePath)> _additionalDocuments = [];
+
+ internal void AddReferences(IEnumerable enumerable)
+ {
+ _references.AddRange(enumerable);
+ }
+
+ internal DocumentId AddDocument(string filePath, SourceText text)
+ {
+ var name = Path.GetFileName(filePath);
+ var id = DocumentId.CreateNewId(Id, name);
+ _documents.Add((id, name, text, filePath));
+ return id;
+ }
+
+ internal DocumentId AddAdditionalDocument(string filePath, SourceText text)
+ {
+ var name = Path.GetFileName(filePath);
+ var id = DocumentId.CreateNewId(Id, name);
+ AddAdditionalDocument(id, filePath, text);
+ return id;
+ }
+
+ internal void AddAdditionalDocument(DocumentId id, string filePath, SourceText text)
+ {
+ var name = Path.GetFileName(filePath);
+ _additionalDocuments.Add((id, name, text, filePath));
+ }
+
+ public Solution Build(Solution solution)
+ {
+ var sgAssembly = typeof(RazorSourceGenerator).Assembly;
+
+ var projectInfo = ProjectInfo
+ .Create(
+ Id,
+ VersionStamp.Create(),
+ name: ProjectName ?? "",
+ assemblyName: ProjectName ?? "",
+ LanguageNames.CSharp,
+ ProjectFilePath,
+ compilationOptions: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
+ .WithMetadataReferences(_references)
+ .WithDefaultNamespace(RootNamespace);
+
+ if (ReferenceRazorSourceGenerator)
+ {
+ projectInfo = projectInfo.WithAnalyzerReferences([new AnalyzerFileReference(sgAssembly.Location, TestAnalyzerAssemblyLoader.LoadFromFile)]);
+ }
+
+ solution = solution.AddProject(projectInfo);
+
+ foreach (var document in _documents)
+ {
+ solution = solution.AddDocument(document.id, document.name, document.text, filePath: document.filePath);
+ }
+
+ foreach (var additionalDocument in _additionalDocuments)
+ {
+ solution = solution.AddAdditionalDocument(additionalDocument.id, additionalDocument.name, additionalDocument.text, filePath: additionalDocument.filePath);
+ }
+
+ if (GenerateGlobalConfigFile)
+ {
+ var globalConfigContent = new StringBuilder();
+
+ globalConfigContent.AppendLine($"""
+ is_global = true
+
+ build_property.RazorLangVersion = {FallbackRazorConfiguration.Latest.LanguageVersion}
+ build_property.RazorConfiguration = {FallbackRazorConfiguration.Latest.ConfigurationName}
+ build_property.RootNamespace = {RootNamespace}
+
+ # This might suprise you, but by suppressing the source generator here, we're mirroring what happens in the Razor SDK
+ build_property.SuppressRazorSourceGenerator = true
+ """);
+
+ if (GenerateMSBuildProjectDirectory)
+ {
+ globalConfigContent.AppendLine($"""
+ build_property.MSBuildProjectDirectory = {Path.GetDirectoryName(ProjectFilePath).AssumeNotNull()}
+ """);
+ }
+
+ var projectBasePath = Path.GetDirectoryName(ProjectFilePath).AssumeNotNull();
+
+ if (GenerateAdditionalDocumentMetadata)
+ {
+ foreach (var additionalDocument in _additionalDocuments)
+ {
+ if (additionalDocument.filePath is not null &&
+ additionalDocument.filePath.StartsWith(projectBasePath))
+ {
+ var relativePath = additionalDocument.filePath[(projectBasePath.Length + 1)..];
+ globalConfigContent.AppendLine($"""
+
+ [{additionalDocument.filePath.AssumeNotNull().Replace('\\', '/')}]
+ build_metadata.AdditionalFiles.TargetPath = {Convert.ToBase64String(Encoding.UTF8.GetBytes(relativePath))}
+ """);
+ }
+ }
+ }
+
+ solution = solution.AddAnalyzerConfigDocument(
+ DocumentId.CreateNewId(Id),
+ name: ".globalconfig",
+ text: SourceText.From(globalConfigContent.ToString()),
+ filePath: Path.Combine(projectBasePath, ".globalconfig"));
+ }
+
+ return solution;
+ }
+}
diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs
index 439cbf66772..fc2c00f62f5 100644
--- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs
+++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs
@@ -2,22 +2,18 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
-using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.LanguageServer.Semantic;
using Microsoft.AspNetCore.Razor.Test.Common.Mef;
-using Microsoft.AspNetCore.Razor.Test.Common.Workspaces;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor.Remote;
using Microsoft.CodeAnalysis.Razor.SemanticTokens;
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.CodeAnalysis.Razor.Workspaces.Settings;
using Microsoft.CodeAnalysis.Remote.Razor;
-using Microsoft.VisualStudio.Composition;
using Microsoft.VisualStudio.Razor.Settings;
-using Xunit;
using Xunit.Abstractions;
namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
@@ -35,10 +31,7 @@ public abstract class CohostEndpointTestBase(ITestOutputHelper testOutputHelper)
private protected override IFilePathService FilePathService => _filePathService.AssumeNotNull();
private protected ISemanticTokensLegendService SemanticTokensLegendService => _semanticTokensLegendService.AssumeNotNull();
- ///
- /// The export provider for Roslyn "devenv" services, if tests opt-in to using them
- ///
- private protected ExportProvider? RoslynDevenvExportProvider { get; private set; }
+ private protected override TestComposition LocalComposition => TestComposition.Roslyn;
protected override async Task InitializeAsync()
{
@@ -85,9 +78,6 @@ private protected override RemoteClientLSPInitializationOptions GetRemoteClientL
};
}
- private protected virtual TestComposition ConfigureRoslynDevenvComposition(TestComposition composition)
- => composition;
-
protected TextDocument CreateProjectAndRazorDocument(
string contents,
bool remoteOnly)
@@ -137,20 +127,7 @@ private TextDocument CreateLocalProjectAndRazorDocument(
(string fileName, string contents)[]? additionalFiles,
bool inGlobalNamespace)
{
- var composition = ConfigureRoslynDevenvComposition(TestComposition.Roslyn);
-
- // We can't enforce that the composition is entirely valid, because we don't have a full MEF catalog, but we
- // can assume there should be no errors related to Razor, and having this array makes debugging failures a lot
- // easier.
- var errors = composition.GetCompositionErrors().ToArray();
- Assert.Empty(errors.Where(e => e.Contains("Razor")));
-
- RoslynDevenvExportProvider = composition.ExportProviderFactory.CreateExportProvider();
- AddDisposable(RoslynDevenvExportProvider);
- var workspace = TestWorkspace.CreateWithDiagnosticAnalyzers(RoslynDevenvExportProvider);
- AddDisposable(workspace);
-
- var razorDocument = CreateProjectAndRazorDocument(workspace, projectId, miscellaneousFile, documentId, documentFilePath, contents, additionalFiles, inGlobalNamespace);
+ var razorDocument = CreateProjectAndRazorDocument(LocalWorkspace, projectId, miscellaneousFile, documentId, documentFilePath, contents, additionalFiles, inGlobalNamespace);
// If we're creating remote and local workspaces, then we'll return the local document, and have to allow
// the remote service invoker to map from the local solution to the remote one.
diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostInlineCompletionEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostInlineCompletionEndpointTest.cs
index 1af7cf964f5..8994ea6ef06 100644
--- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostInlineCompletionEndpointTest.cs
+++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostInlineCompletionEndpointTest.cs
@@ -115,7 +115,7 @@ private async Task VerifyInlineCompletionAsync(TestCode input, string? output =
AssertEx.EqualOrDiff(output, inputText.ToString());
}
- private protected override TestComposition ConfigureRoslynDevenvComposition(TestComposition composition)
+ private protected override TestComposition ConfigureLocalComposition(TestComposition composition)
{
return composition.AddParts(typeof(TestSnippetInfoService));
}
diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRoslynRenameTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRoslynRenameTest.cs
index 5ae9b5ffa99..ec588261018 100644
--- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRoslynRenameTest.cs
+++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRoslynRenameTest.cs
@@ -148,7 +148,7 @@ The end.
""",
useLsp);
- private protected override TestComposition ConfigureRoslynDevenvComposition(TestComposition composition)
+ private protected override TestComposition ConfigureLocalComposition(TestComposition composition)
{
return composition
.AddParts(typeof(RazorSourceGeneratedDocumentSpanMappingService))
@@ -169,8 +169,7 @@ private async Task VerifyRenamesAsync(
var compilation = await project.GetCompilationAsync(DisposalToken);
- Assert.True(razorDocument.TryComputeHintNameFromRazorDocument(out var hintName));
- var generatedDocument = await project.TryGetSourceGeneratedDocumentFromHintNameAsync(hintName, DisposalToken);
+ var generatedDocument = await project.TryGetSourceGeneratedDocumentForRazorDocumentAsync(razorDocument, DisposalToken);
var node = await GetSyntaxNodeAsync(generatedDocument.AssumeNotNull(), razorFile.Position, razorDocument);
@@ -227,10 +226,9 @@ private async Task VerifyVSRenameAsync(string newName, string expectedCSharpFile
AssertEx.EqualOrDiff(expectedCSharpFile, csharpText.ToString());
// Normally in VS, TryApplyChanges would be called, and that calls into our edit mapping service.
- Assert.True(razorDocument.TryComputeHintNameFromRazorDocument(out var hintName));
- var generatedDoc = await project.TryGetSourceGeneratedDocumentFromHintNameAsync(hintName, DisposalToken);
+ var generatedDoc = await project.TryGetSourceGeneratedDocumentForRazorDocumentAsync(razorDocument, DisposalToken);
Assert.NotNull(generatedDoc);
- var renamedGeneratedDoc = await solution.GetRequiredProject(project.Id).TryGetSourceGeneratedDocumentFromHintNameAsync(hintName, DisposalToken);
+ var renamedGeneratedDoc = await solution.GetRequiredProject(project.Id).TryGetSourceGeneratedDocumentForRazorDocumentAsync(razorDocument, DisposalToken);
Assert.NotNull(renamedGeneratedDoc);
// It could be argued this class is really a RazorSourceGeneratedDocumentSpanMappingService test :)
@@ -253,7 +251,7 @@ private async Task VerifyLspRenameAsync(string newName, string expectedCSharpFil
{
// Normally in cohosting tests we directly construct and invoke the endpoints, but in this scenario Roslyn is going to do it
// using a service in their MEF composition, so we have to jump through an extra hook to hook up our test invoker.
- var invoker = RoslynDevenvExportProvider.AssumeNotNull().GetExportedValue();
+ var invoker = LocalExportProvider.AssumeNotNull().GetExportedValue();
invoker.SetInvoker(RemoteServiceInvoker);
var tree = node.SyntaxTree;
diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/TextDocumentExtensionsTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/TextDocumentExtensionsTest.cs
deleted file mode 100644
index 3c782b4a9fa..00000000000
--- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/TextDocumentExtensionsTest.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System.IO;
-using Microsoft.AspNetCore.Razor.Test.Common;
-using Microsoft.AspNetCore.Razor.Test.Common.VisualStudio;
-using Microsoft.CodeAnalysis;
-using Xunit;
-using Xunit.Abstractions;
-
-namespace Microsoft.VisualStudio.LanguageServices.Razor.Test.Cohost;
-
-public class TextDocumentExtensionsTest(ITestOutputHelper testOutput) : VisualStudioWorkspaceTestBase(testOutput)
-{
- [Theory]
- [InlineData(@"Pages\Index.razor")]
- [InlineData(@"Pages/Index.razor")]
- [InlineData(@"Pages.Index.razor")]
- public void TryComputeHintNameFromRazorDocument(string razorFilePath)
- {
- var projectId = ProjectId.CreateNewId();
- var projectInfo = ProjectInfo
- .Create(
- projectId,
- VersionStamp.Create(),
- name: "Project",
- assemblyName: "Project",
- LanguageNames.CSharp,
- TestProjectData.SomeProject.FilePath);
-
- var documentId = DocumentId.CreateNewId(projectId);
- var solution = Workspace.CurrentSolution
- .AddProject(projectInfo)
- .AddAdditionalDocument(documentId, "File.razor", "", filePath: Path.Combine(TestProjectData.SomeProjectPath, razorFilePath));
-
- var document = solution.GetAdditionalDocument(documentId);
-
- Assert.NotNull(document);
- Assert.True(document.TryComputeHintNameFromRazorDocument(out var hintName));
- // This tests TryComputeHintNameFromRazorDocument and also neatly demonstrates a bug: https://github.com/dotnet/razor/issues/11578
- Assert.Equal("Pages_Index_razor.g.cs", hintName);
- }
-}
diff --git a/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/CohostEndpointTestBase.cs b/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/CohostEndpointTestBase.cs
index 7cf127581ee..9a33c2a7b2d 100644
--- a/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/CohostEndpointTestBase.cs
+++ b/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/CohostEndpointTestBase.cs
@@ -2,13 +2,11 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
-using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.LanguageServer.Test;
using Microsoft.AspNetCore.Razor.Test.Common.Mef;
-using Microsoft.AspNetCore.Razor.Test.Common.Workspaces;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor.Remote;
using Microsoft.CodeAnalysis.Razor.SemanticTokens;
@@ -17,7 +15,6 @@
using Microsoft.CodeAnalysis.Remote.Razor;
using Microsoft.VisualStudioCode.RazorExtension.Configuration;
using Microsoft.VisualStudioCode.RazorExtension.Services;
-using Xunit;
using Xunit.Abstractions;
namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
@@ -28,13 +25,13 @@ public abstract class CohostEndpointTestBase(ITestOutputHelper testOutputHelper)
private VSCodeRemoteServiceInvoker? _remoteServiceInvoker;
private IFilePathService? _filePathService;
private ISemanticTokensLegendService? _semanticTokensLegendService;
- private Workspace? _workspace;
private protected override IRemoteServiceInvoker RemoteServiceInvoker => _remoteServiceInvoker.AssumeNotNull();
private protected override IClientSettingsManager ClientSettingsManager => _clientSettingsManager.AssumeNotNull();
private protected override IFilePathService FilePathService => _filePathService.AssumeNotNull();
private protected ISemanticTokensLegendService SemanticTokensLegendService => _semanticTokensLegendService.AssumeNotNull();
- private protected Workspace Workspace => _workspace.AssumeNotNull();
+
+ private protected override TestComposition LocalComposition => TestComposition.RoslynFeatures;
protected override async Task InitializeAsync()
{
@@ -42,10 +39,8 @@ protected override async Task InitializeAsync()
InProcServiceFactory.TestAccessor.SetExportProvider(OOPExportProvider);
- _workspace = CreateWorkspace();
-
var workspaceProvider = new VSCodeWorkspaceProvider();
- workspaceProvider.SetWorkspace(Workspace);
+ workspaceProvider.SetWorkspace(LocalWorkspace);
_remoteServiceInvoker = new VSCodeRemoteServiceInvoker(workspaceProvider, LoggerFactory);
AddDisposable(_remoteServiceInvoker);
@@ -70,7 +65,7 @@ private protected override RemoteClientLSPInitializationOptions GetRemoteClientL
CompletionItem = new CompletionItemSetting(),
CompletionItemKind = new CompletionItemKindSetting()
{
- ValueSet = (CompletionItemKind[])Enum.GetValues(typeof(CompletionItemKind)),
+ ValueSet = Enum.GetValues(),
},
CompletionListSetting = new CompletionListSetting()
{
@@ -94,23 +89,6 @@ protected override TextDocument CreateProjectAndRazorDocument(
bool inGlobalNamespace = false,
bool miscellaneousFile = false)
{
- return CreateProjectAndRazorDocument(Workspace, contents, fileKind, documentFilePath, additionalFiles, inGlobalNamespace, miscellaneousFile);
- }
-
- private AdhocWorkspace CreateWorkspace()
- {
- var composition = TestComposition.RoslynFeatures;
-
- // We can't enforce that the composition is entirely valid, because we don't have a full MEF catalog, but we
- // can assume there should be no errors related to Razor, and having this array makes debugging failures a lot
- // easier.
- var errors = composition.GetCompositionErrors().ToArray();
- Assert.Empty(errors.Where(e => e.Contains("Razor")));
-
- var roslynExportProvider = composition.ExportProviderFactory.CreateExportProvider();
- AddDisposable(roslynExportProvider);
- var workspace = TestWorkspace.CreateWithDiagnosticAnalyzers(roslynExportProvider);
- AddDisposable(workspace);
- return workspace;
+ return CreateProjectAndRazorDocument(LocalWorkspace, contents, fileKind, documentFilePath, additionalFiles, inGlobalNamespace, miscellaneousFile);
}
}
diff --git a/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/CohostDocumentSymbolEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/CohostDocumentSymbolEndpointTest.cs
index ce7c716b34d..bab97ce3211 100644
--- a/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/CohostDocumentSymbolEndpointTest.cs
+++ b/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/CohostDocumentSymbolEndpointTest.cs
@@ -6,6 +6,7 @@
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor;
+using Microsoft.AspNetCore.Razor.Utilities;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Text;
@@ -46,10 +47,15 @@ class {|SomeProject.File1.C:C|}
[Theory]
[CombinatorialData]
public Task DocumentSymbols_CSharpClassWithMethods_MiscFile(bool hierarchical)
- => VerifyDocumentSymbolsAsync(
- """
+ {
+ // What the source generator would product for TestProjectData.SomeProjectPath
+ var generatedNamespace = PlatformInformation.IsWindows
+ ? "c_.users.example.src.SomeProject"
+ : "home.example.SomeProject";
+ return VerifyDocumentSymbolsAsync(
+ $$"""
@functions {
- class {|ASP.File1.C:C|}
+ class {|ASP.{{generatedNamespace}}.File1.C:C|}
{
private void {|HandleString(string s):HandleString|}(string s)
{
@@ -69,6 +75,7 @@ class {|ASP.File1.C:C|}
}
""", hierarchical, miscellaneousFile: true);
+ }
[Theory]
[CombinatorialData]
diff --git a/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/CohostGoToDefinitionEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/CohostGoToDefinitionEndpointTest.cs
index 47d20ed1028..cf58f19a46e 100644
--- a/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/CohostGoToDefinitionEndpointTest.cs
+++ b/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/CohostGoToDefinitionEndpointTest.cs
@@ -411,7 +411,7 @@ public async Task Html()
},
});
- await VerifyGoToDefinitionAsync(input, htmlResponse: htmlResponse);
+ await VerifyGoToDefinitionAsync(input, htmlResponse: htmlResponse, razorDocument: document);
}
[Fact]
@@ -824,9 +824,10 @@ public override void Process(TagHelperContext context, TagHelperOutput output)
private async Task VerifyGoToDefinitionAsync(
TestCode input,
RazorFileKind? fileKind = null,
- SumType? htmlResponse = null)
+ SumType? htmlResponse = null,
+ TextDocument? razorDocument = null)
{
- var document = CreateProjectAndRazorDocument(input.Text, fileKind);
+ var document = razorDocument ?? CreateProjectAndRazorDocument(input.Text, fileKind);
var result = await GetGoToDefinitionResultCoreAsync(document, input, htmlResponse);
Assumes.NotNull(result);
diff --git a/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/ComputedTargetPathTest.cs b/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/ComputedTargetPathTest.cs
new file mode 100644
index 00000000000..ac4c0b17878
--- /dev/null
+++ b/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/ComputedTargetPathTest.cs
@@ -0,0 +1,111 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Razor;
+using Microsoft.AspNetCore.Razor.Test.Common;
+using Microsoft.AspNetCore.Razor.Utilities;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Text;
+using Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Microsoft.VisualStudioCode.RazorExtension.Test.Endpoints.Shared;
+
+public class ComputedTargetPathTest(ITestOutputHelper testOutputHelper) : CohostEndpointTestBase(testOutputHelper)
+{
+ // What the source generator would produce for TestProjectData.SomeProjectPath
+ private static readonly string s_hintNamePrefix = PlatformInformation.IsWindows
+ ? "c__users_example_src_SomeProject"
+ : "_home_example_SomeProject";
+
+ [Theory]
+ [InlineData(true, false)]
+ [InlineData(true, true)]
+ [InlineData(false, false)]
+ public async Task SingleDocument(bool projectPath, bool generateConfigFile)
+ {
+ var builder = new RazorProjectBuilder
+ {
+ ProjectFilePath = projectPath ? TestProjectData.SomeProject.FilePath : null,
+ GenerateGlobalConfigFile = generateConfigFile,
+ GenerateAdditionalDocumentMetadata = false,
+ GenerateMSBuildProjectDirectory = false
+ };
+
+ var id = builder.AddAdditionalDocument(FilePath("File1.razor"), SourceText.From(""));
+
+ var solution = LocalWorkspace.CurrentSolution;
+ solution = builder.Build(solution);
+
+ var document = solution.GetAdditionalDocument(id).AssumeNotNull();
+
+ _ = await document.Project.GetCompilationAsync(DisposalToken);
+
+ var generatedDocument = await document.Project.TryGetSourceGeneratedDocumentForRazorDocumentAsync(document, DisposalToken);
+ Assert.NotNull(generatedDocument);
+ Assert.Equal($"{s_hintNamePrefix}_File1_razor.g.cs", generatedDocument.HintName);
+ }
+
+ [Theory]
+ [CombinatorialData]
+ public async Task TwoDocumentsWithTheSameBaseFileName(bool generateTargetPath)
+ {
+ // This test just proves the "correct" behaviour, with the Razor SDL
+ var builder = new RazorProjectBuilder
+ {
+ ProjectFilePath = TestProjectData.SomeProject.FilePath,
+ GenerateAdditionalDocumentMetadata = generateTargetPath
+ };
+
+ var doc1Id = builder.AddAdditionalDocument(FilePath(@"Pages\Index.razor"), SourceText.From(""));
+ var doc2Id = builder.AddAdditionalDocument(FilePath(@"Components\Index.razor"), SourceText.From(""));
+
+ var solution = LocalWorkspace.CurrentSolution;
+ solution = builder.Build(solution);
+
+ var doc1 = solution.GetAdditionalDocument(doc1Id).AssumeNotNull();
+ var doc2 = solution.GetAdditionalDocument(doc2Id).AssumeNotNull();
+
+ var generatedDocument = await doc1.Project.TryGetSourceGeneratedDocumentForRazorDocumentAsync(doc1, DisposalToken);
+ Assert.NotNull(generatedDocument);
+ Assert.Equal($"Pages_Index_razor.g.cs", generatedDocument.HintName);
+
+ generatedDocument = await doc2.Project.TryGetSourceGeneratedDocumentForRazorDocumentAsync(doc2, DisposalToken);
+ Assert.NotNull(generatedDocument);
+ Assert.Equal($"Components_Index_razor.g.cs", generatedDocument.HintName);
+ }
+
+ [Theory]
+ [InlineData(true, false)]
+ [InlineData(true, true)]
+ [InlineData(false, false)]
+ public async Task TwoDocumentsWithTheSameBaseFileName_FullPathHintName(bool projectPath, bool generateConfigFile)
+ {
+ var builder = new RazorProjectBuilder
+ {
+ ProjectFilePath = projectPath ? TestProjectData.SomeProject.FilePath : null,
+ GenerateGlobalConfigFile = generateConfigFile,
+ GenerateAdditionalDocumentMetadata = false,
+ GenerateMSBuildProjectDirectory = false
+ };
+
+ var doc1Id = builder.AddAdditionalDocument(FilePath(@"Pages\Index.razor"), SourceText.From(""));
+ var doc2Id = builder.AddAdditionalDocument(FilePath(@"Components\Index.razor"), SourceText.From(""));
+
+ var solution = LocalWorkspace.CurrentSolution;
+ solution = builder.Build(solution);
+
+ var doc1 = solution.GetAdditionalDocument(doc1Id).AssumeNotNull();
+ var doc2 = solution.GetAdditionalDocument(doc2Id).AssumeNotNull();
+
+ var generatedDocument = await doc1.Project.TryGetSourceGeneratedDocumentForRazorDocumentAsync(doc1, DisposalToken);
+ Assert.NotNull(generatedDocument);
+ Assert.Equal($"{s_hintNamePrefix}_Pages_Index_razor.g.cs", generatedDocument.HintName);
+
+ generatedDocument = await doc2.Project.TryGetSourceGeneratedDocumentForRazorDocumentAsync(doc2, DisposalToken);
+ Assert.NotNull(generatedDocument);
+ Assert.Equal($"{s_hintNamePrefix}_Components_Index_razor.g.cs", generatedDocument.HintName);
+ }
+}