-
Notifications
You must be signed in to change notification settings - Fork 196
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use Roslyn project info in cohost endpoints #9805
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
61e378d
Create snapshots and factories that use Roslyn project info instead o…
davidwengier b2fc07d
Create a DocumentContextFactory for cohosting that uses the new facto…
davidwengier 1c9c4d1
Finish off the CohostDocumentSnapshot by sharing reusing code from Do…
davidwengier 92f1ff3
Fill out RazorConfiguration and ProjectEngine properties
davidwengier 209295e
Fill out tag helpers
davidwengier 2d8218e
Fill out related documents properties
davidwengier 7dffc15
Use the new system for the current cohost endpoints
davidwengier 813b80c
Fake the document version for now
davidwengier 484bf6e
PR Feedback
davidwengier File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
31 changes: 31 additions & 0 deletions
31
...azor/src/Microsoft.AspNetCore.Razor.LanguageServer/Cohost/CohostDocumentContextFactory.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
// 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.Composition; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; | ||
|
||
namespace Microsoft.AspNetCore.Razor.LanguageServer.Cohost; | ||
|
||
// NOTE: This is not a "normal" MEF export (ie, exporting an interface) purely because of a strange desire to keep API in | ||
// RazorCohostRequestContextExtensions looking like the previous code in the non-cohost world. | ||
[ExportRazorStatelessLspService(typeof(CohostDocumentContextFactory))] | ||
[method: ImportingConstructor] | ||
internal class CohostDocumentContextFactory(DocumentSnapshotFactory documentSnapshotFactory, IDocumentVersionCache documentVersionCache) : AbstractRazorLspService | ||
{ | ||
private readonly DocumentSnapshotFactory _documentSnapshotFactory = documentSnapshotFactory; | ||
private readonly IDocumentVersionCache _documentVersionCache = documentVersionCache; | ||
|
||
public VersionedDocumentContext Create(Uri documentUri, TextDocument textDocument) | ||
{ | ||
var documentSnapshot = _documentSnapshotFactory.GetOrCreate(textDocument); | ||
|
||
// HACK: For cohosting, we just grab the "current" version, because we know it will have been updated | ||
// since the change handling is synchronous. In future we can just remove the whole concept of | ||
// document versions because TextDocument is inherently versioned. | ||
var version = _documentVersionCache.GetLatestDocumentVersion(documentSnapshot.FilePath.AssumeNotNull()); | ||
|
||
return new VersionedDocumentContext(documentUri, documentSnapshot, null, version); | ||
} | ||
} |
73 changes: 73 additions & 0 deletions
73
src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Cohost/CohostDocumentSnapshot.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the MIT license. See License.txt in the project root for license information. | ||
|
||
using System.Collections.Immutable; | ||
using System.Diagnostics.CodeAnalysis; | ||
using System.Threading.Tasks; | ||
using Microsoft.AspNetCore.Razor.Language; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.Razor.ProjectSystem; | ||
using Microsoft.CodeAnalysis.Text; | ||
|
||
namespace Microsoft.AspNetCore.Razor.LanguageServer.Cohost; | ||
|
||
internal class CohostDocumentSnapshot(TextDocument textDocument, IProjectSnapshot projectSnapshot) : IDocumentSnapshot | ||
{ | ||
private readonly TextDocument _textDocument = textDocument; | ||
private readonly IProjectSnapshot _projectSnapshot = projectSnapshot; | ||
|
||
private RazorCodeDocument? _codeDocument; | ||
|
||
public string? FileKind => FileKinds.GetFileKindFromFilePath(FilePath); | ||
|
||
public string? FilePath => _textDocument.FilePath; | ||
|
||
public string? TargetPath => _textDocument.FilePath; | ||
|
||
public IProjectSnapshot Project => _projectSnapshot; | ||
|
||
public bool SupportsOutput => true; | ||
|
||
public Task<SourceText> GetTextAsync() => _textDocument.GetTextAsync(); | ||
|
||
public Task<VersionStamp> GetTextVersionAsync() => _textDocument.GetTextVersionAsync(); | ||
|
||
public bool TryGetText([NotNullWhen(true)] out SourceText? result) => _textDocument.TryGetText(out result); | ||
|
||
public bool TryGetTextVersion(out VersionStamp result) => _textDocument.TryGetTextVersion(out result); | ||
|
||
public ImmutableArray<IDocumentSnapshot> GetImports() | ||
{ | ||
return DocumentState.GetImportsCore(Project, FilePath.AssumeNotNull(), FileKind.AssumeNotNull()); | ||
} | ||
|
||
public async Task<RazorCodeDocument> GetGeneratedOutputAsync() | ||
{ | ||
// TODO: We don't need to worry about locking if we get called from the didOpen/didChange LSP requests, as CLaSP | ||
// takes care of that for us, and blocks requests until those are complete. If that doesn't end up happening, | ||
// then a locking mechanism here would prevent concurrent compilations. | ||
if (_codeDocument is not null) | ||
{ | ||
return _codeDocument; | ||
} | ||
|
||
// The non-cohosted DocumentSnapshot implementation uses DocumentState to get the generated output, and we could do that too | ||
// but most of that code is optimized around caching pre-computed results when things change that don't affect the compilation. | ||
// We can't do that here because we are using Roslyn's project snapshots, which don't contain the info that Razor needs. We could | ||
// in future provide a side-car mechanism so we can cache things, but still take advantage of snapshots etc. but the working | ||
// assumption for this code is that the source generator will be used, and it will do all of that, so this implementation is naive | ||
// and simply compiles when asked, and if a new document snapshot comes in, we compile again. This is presumably worse for perf | ||
// but since we don't expect users to ever use cohosting without source generators, it's fine for now. | ||
|
||
var imports = await DocumentState.ComputedStateTracker.GetImportsAsync(this).ConfigureAwait(false); | ||
_codeDocument = await DocumentState.ComputedStateTracker.GenerateCodeDocumentAsync(Project, this, imports).ConfigureAwait(false); | ||
|
||
return _codeDocument; | ||
} | ||
|
||
public bool TryGetGeneratedOutput([NotNullWhen(true)] out RazorCodeDocument? result) | ||
{ | ||
result = _codeDocument; | ||
return result is not null; | ||
} | ||
} |
164 changes: 164 additions & 0 deletions
164
src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Cohost/CohostProjectSnapshot.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
// 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.Generic; | ||
using System.Collections.Immutable; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Threading; | ||
using Microsoft.AspNetCore.Razor.Language; | ||
using Microsoft.AspNetCore.Razor.PooledObjects; | ||
using Microsoft.AspNetCore.Razor.ProjectEngineHost; | ||
using Microsoft.AspNetCore.Razor.ProjectSystem; | ||
using Microsoft.AspNetCore.Razor.Telemetry; | ||
using Microsoft.AspNetCore.Razor.Utilities; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CSharp; | ||
using Microsoft.CodeAnalysis.Razor; | ||
using Microsoft.CodeAnalysis.Razor.ProjectSystem; | ||
using Microsoft.VisualStudio.Threading; | ||
|
||
namespace Microsoft.AspNetCore.Razor.LanguageServer.Cohost; | ||
|
||
internal class CohostProjectSnapshot : IProjectSnapshot | ||
{ | ||
private static readonly EmptyProjectEngineFactory s_fallbackProjectEngineFactory = new(); | ||
private static readonly (IProjectEngineFactory Value, ICustomProjectEngineFactoryMetadata)[] s_projectEngineFactories = ProjectEngineFactories.Factories.Select(f => (f.Item1.Value, f.Item2)).ToArray(); | ||
|
||
private readonly Project _project; | ||
private readonly DocumentSnapshotFactory _documentSnapshotFactory; | ||
private readonly ITelemetryReporter _telemetryReporter; | ||
private readonly ProjectKey _projectKey; | ||
private readonly Lazy<RazorConfiguration> _lazyConfiguration; | ||
private readonly Lazy<RazorProjectEngine> _lazyProjectEngine; | ||
private readonly AsyncLazy<ImmutableArray<TagHelperDescriptor>> _tagHelpersLazy; | ||
private readonly Lazy<ProjectWorkspaceState> _projectWorkspaceStateLazy; | ||
private readonly Lazy<ImmutableDictionary<string, ImmutableArray<string>>> _importsToRelatedDocumentsLazy; | ||
|
||
public CohostProjectSnapshot(Project project, DocumentSnapshotFactory documentSnapshotFactory, ITelemetryReporter telemetryReporter, JoinableTaskContext joinableTaskContext) | ||
{ | ||
_project = project; | ||
_documentSnapshotFactory = documentSnapshotFactory; | ||
_telemetryReporter = telemetryReporter; | ||
_projectKey = ProjectKey.From(_project).AssumeNotNull(); | ||
|
||
_lazyConfiguration = new Lazy<RazorConfiguration>(CreateRazorConfiguration); | ||
_lazyProjectEngine = new Lazy<RazorProjectEngine>(() => DefaultProjectEngineFactory.Create( | ||
Configuration, | ||
fileSystem: RazorProjectFileSystem.Create(Path.GetDirectoryName(FilePath)), | ||
configure: builder => | ||
{ | ||
builder.SetRootNamespace(RootNamespace); | ||
builder.SetCSharpLanguageVersion(CSharpLanguageVersion); | ||
builder.SetSupportLocalizedComponentNames(); | ||
}, | ||
fallback: s_fallbackProjectEngineFactory, | ||
factories: s_projectEngineFactories)); | ||
|
||
_tagHelpersLazy = new AsyncLazy<ImmutableArray<TagHelperDescriptor>>(() => | ||
{ | ||
var resolver = new CompilationTagHelperResolver(_telemetryReporter); | ||
return resolver.GetTagHelpersAsync(_project, GetProjectEngine(), CancellationToken.None).AsTask(); | ||
}, joinableTaskContext.Factory); | ||
|
||
_projectWorkspaceStateLazy = new Lazy<ProjectWorkspaceState>(() => ProjectWorkspaceState.Create(TagHelpers, CSharpLanguageVersion)); | ||
|
||
_importsToRelatedDocumentsLazy = new Lazy<ImmutableDictionary<string, ImmutableArray<string>>>(() => | ||
{ | ||
var importsToRelatedDocuments = ImmutableDictionary.Create<string, ImmutableArray<string>>(FilePathNormalizer.Comparer); | ||
foreach (var document in DocumentFilePaths) | ||
{ | ||
var importTargetPaths = ProjectState.GetImportDocumentTargetPaths(document, FileKinds.GetFileKindFromFilePath(document), GetProjectEngine()); | ||
importsToRelatedDocuments = ProjectState.AddToImportsToRelatedDocuments(importsToRelatedDocuments, document, importTargetPaths); | ||
} | ||
|
||
return importsToRelatedDocuments; | ||
}); | ||
} | ||
|
||
public ProjectKey Key => _projectKey; | ||
|
||
public RazorConfiguration Configuration => _lazyConfiguration.Value; | ||
|
||
public IEnumerable<string> DocumentFilePaths | ||
=> _project.AdditionalDocuments | ||
.Where(d => d.FilePath!.EndsWith(".razor", StringComparison.OrdinalIgnoreCase) || d.FilePath.EndsWith(".cshtml", StringComparison.OrdinalIgnoreCase)) | ||
.Select(d => d.FilePath.AssumeNotNull()); | ||
|
||
public string FilePath => _project.FilePath!; | ||
|
||
public string IntermediateOutputPath => FilePathNormalizer.GetNormalizedDirectoryName(_project.CompilationOutputInfo.AssemblyPath); | ||
|
||
public string? RootNamespace => _project.DefaultNamespace ?? "ASP"; | ||
|
||
public string DisplayName => _project.Name; | ||
|
||
public VersionStamp Version => _project.Version; | ||
|
||
public LanguageVersion CSharpLanguageVersion => ((CSharpParseOptions)_project.ParseOptions!).LanguageVersion; | ||
|
||
public ImmutableArray<TagHelperDescriptor> TagHelpers => _tagHelpersLazy.GetValue(); | ||
|
||
public ProjectWorkspaceState ProjectWorkspaceState => _projectWorkspaceStateLazy.Value; | ||
|
||
public IDocumentSnapshot? GetDocument(string filePath) | ||
{ | ||
var textDocument = _project.AdditionalDocuments.FirstOrDefault(d => d.FilePath == filePath); | ||
if (textDocument is null) | ||
{ | ||
return null; | ||
} | ||
|
||
return _documentSnapshotFactory.GetOrCreate(textDocument); | ||
} | ||
|
||
public RazorProjectEngine GetProjectEngine() => _lazyProjectEngine.Value; | ||
|
||
public ImmutableArray<IDocumentSnapshot> GetRelatedDocuments(IDocumentSnapshot document) | ||
{ | ||
var targetPath = document.TargetPath.AssumeNotNull(); | ||
|
||
if (!_importsToRelatedDocumentsLazy.Value.TryGetValue(targetPath, out var relatedDocuments)) | ||
{ | ||
return ImmutableArray<IDocumentSnapshot>.Empty; | ||
} | ||
|
||
using var _ = ArrayBuilderPool<IDocumentSnapshot>.GetPooledObject(out var builder); | ||
builder.SetCapacityIfLarger(relatedDocuments.Length); | ||
|
||
foreach (var relatedDocumentFilePath in relatedDocuments) | ||
{ | ||
if (GetDocument(relatedDocumentFilePath) is { } relatedDocument) | ||
{ | ||
builder.Add(relatedDocument); | ||
} | ||
} | ||
|
||
return builder.ToImmutableArray(); | ||
} | ||
|
||
public bool IsImportDocument(IDocumentSnapshot document) | ||
{ | ||
return document.TargetPath is { } targetPath && _importsToRelatedDocumentsLazy.Value.ContainsKey(targetPath); | ||
} | ||
|
||
private RazorConfiguration CreateRazorConfiguration() | ||
{ | ||
// See RazorSourceGenerator.RazorProviders.cs | ||
|
||
var globalOptions = _project.AnalyzerOptions.AnalyzerConfigOptionsProvider.GlobalOptions; | ||
|
||
globalOptions.TryGetValue("build_property.RazorConfiguration", out var configurationName); | ||
|
||
configurationName ??= "MVC-3.0"; // TODO: Source generator uses "default" here?? | ||
|
||
if (!globalOptions.TryGetValue("build_property.RazorLangVersion", out var razorLanguageVersionString) || | ||
!RazorLanguageVersion.TryParse(razorLanguageVersionString, out var razorLanguageVersion)) | ||
{ | ||
razorLanguageVersion = RazorLanguageVersion.Latest; | ||
} | ||
|
||
return RazorConfiguration.Create(razorLanguageVersion, configurationName, Enumerable.Empty<RazorExtension>(), useConsolidatedMvcViews: true); | ||
} | ||
} |
31 changes: 31 additions & 0 deletions
31
src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Cohost/DocumentSnapshotFactory.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
// 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.Composition; | ||
using System.Runtime.CompilerServices; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.Razor.ProjectSystem; | ||
|
||
namespace Microsoft.AspNetCore.Razor.LanguageServer.Cohost; | ||
|
||
[Export(typeof(DocumentSnapshotFactory)), Shared] | ||
[method: ImportingConstructor] | ||
internal class DocumentSnapshotFactory(Lazy<ProjectSnapshotFactory> projectSnapshotFactory) | ||
{ | ||
private static readonly ConditionalWeakTable<TextDocument, IDocumentSnapshot> _documentSnapshots = new(); | ||
|
||
private readonly Lazy<ProjectSnapshotFactory> _projectSnapshotFactory = projectSnapshotFactory; | ||
|
||
public IDocumentSnapshot GetOrCreate(TextDocument textDocument) | ||
{ | ||
if (!_documentSnapshots.TryGetValue(textDocument, out var documentSnapshot)) | ||
{ | ||
var projectSnapshot = _projectSnapshotFactory.Value.GetOrCreate(textDocument.Project); | ||
documentSnapshot = new CohostDocumentSnapshot(textDocument, projectSnapshot); | ||
_documentSnapshots.Add(textDocument, documentSnapshot); | ||
} | ||
|
||
return documentSnapshot; | ||
} | ||
} |
33 changes: 33 additions & 0 deletions
33
src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Cohost/ProjectSnapshotFactory.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the MIT license. See License.txt in the project root for license information. | ||
|
||
using System.Composition; | ||
using System.Runtime.CompilerServices; | ||
using Microsoft.AspNetCore.Razor.Telemetry; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.Razor.ProjectSystem; | ||
using Microsoft.VisualStudio.Threading; | ||
|
||
namespace Microsoft.AspNetCore.Razor.LanguageServer.Cohost; | ||
|
||
[Export(typeof(ProjectSnapshotFactory)), Shared] | ||
[method: ImportingConstructor] | ||
internal class ProjectSnapshotFactory(DocumentSnapshotFactory documentSnapshotFactory, ITelemetryReporter telemetryReporter, JoinableTaskContext joinableTaskContext) | ||
{ | ||
private static readonly ConditionalWeakTable<Project, IProjectSnapshot> _projectSnapshots = new(); | ||
|
||
private readonly DocumentSnapshotFactory _documentSnapshotFactory = documentSnapshotFactory; | ||
private readonly ITelemetryReporter _telemetryReporter = telemetryReporter; | ||
private readonly JoinableTaskContext _joinableTaskContext = joinableTaskContext; | ||
|
||
public IProjectSnapshot GetOrCreate(Project project) | ||
{ | ||
if (!_projectSnapshots.TryGetValue(project, out var projectSnapshot)) | ||
{ | ||
projectSnapshot = new CohostProjectSnapshot(project, _documentSnapshotFactory, _telemetryReporter, _joinableTaskContext); | ||
_projectSnapshots.Add(project, projectSnapshot); | ||
} | ||
|
||
return projectSnapshot; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider using a
PooledArrayBuilder
if the result will regularly be larger than 4 items.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean if it will be less than 4 items? If it's more than 4, doesn't the
PooledArrayBuilder
just switch to using theArrayBuilderPool
anyway?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having said that, I just copied this code as is, and it can be improved slightly by specifying a capacity at least
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, sorry - I meant "less than 4 items".
PooledArrayBuilder
can help avoid starvation of theArrayBuilderPool
and is useful when smaller arrays are expected. I'm not 100% sure whatGetRelatedDocuments
means. Based on the code and the name of the map, I would guess it means, "the documents that use this import"? If so,ArrayBuilderPool
is probably the way to go.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, that's my reading too, and I agree. Thanks for confirming