diff --git a/src/Features/Core/Portable/CodeLens/CodeLensReferencesService.cs b/src/Features/Core/Portable/CodeLens/CodeLensReferencesService.cs index 38354a135dc80..1eea7eaac7c52 100644 --- a/src/Features/Core/Portable/CodeLens/CodeLensReferencesService.cs +++ b/src/Features/Core/Portable/CodeLens/CodeLensReferencesService.cs @@ -14,7 +14,6 @@ using Microsoft.CodeAnalysis.FindSymbols; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.LanguageService; -using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; @@ -224,14 +223,21 @@ public async Task> MapReferenceLocat var descriptor = descriptorAndDocument.Descriptor; var span = new TextSpan(descriptor.SpanStart, descriptor.SpanLength); - var results = await SpanMappingHelper.TryGetMappedSpanResultAsync(document, [span], cancellationToken).ConfigureAwait(false); - if (results is null) + + if (!SpanMappingHelper.CanMapSpans(document)) { // for normal document, just add one as they are list.Add(descriptor); continue; } + var results = await SpanMappingHelper.TryGetMappedSpanResultAsync(document, [span], cancellationToken).ConfigureAwait(false); + if (results is null) + { + // If the document can map spans, but this span wasn't mapped, drop the result + continue; + } + var mappedSpans = results.GetValueOrDefault(); // external component violated contracts. the mapper should preserve input order/count. @@ -246,36 +252,13 @@ public async Task> MapReferenceLocat continue; } - var excerpter = document.DocumentServiceProvider.GetService(); - if (excerpter == null) + if (!DocumentExcerptHelper.CanExcerpt(document)) { - if (document.IsRazorSourceGeneratedDocument()) - { - // HACK: Razor doesn't have has a workspace level excerpt service, but if we just return a simple descriptor here, - // the user at least sees something, can navigate, and Razor can improve this later if necessary. Until - // https://github.com/dotnet/roslyn/issues/79699 is fixed this won't get hit anyway. - list.Add(new ReferenceLocationDescriptor( - descriptor.LongDescription, - descriptor.Language, - descriptor.Glyph, - result.Span.Start, - result.Span.Length, - result.LinePositionSpan.Start.Line, - result.LinePositionSpan.Start.Character, - result.FilePath, - descriptor.ReferenceLineText, - descriptor.ReferenceStart, - descriptor.ReferenceLength, - "", - "", - "", - "")); - } continue; } - var referenceExcerpt = await excerpter.TryExcerptAsync(document, span, ExcerptMode.SingleLine, classificationOptions, cancellationToken).ConfigureAwait(false); - var tooltipExcerpt = await excerpter.TryExcerptAsync(document, span, ExcerptMode.Tooltip, classificationOptions, cancellationToken).ConfigureAwait(false); + var referenceExcerpt = await DocumentExcerptHelper.TryExcerptAsync(document, span, ExcerptMode.SingleLine, classificationOptions, cancellationToken).ConfigureAwait(false); + var tooltipExcerpt = await DocumentExcerptHelper.TryExcerptAsync(document, span, ExcerptMode.Tooltip, classificationOptions, cancellationToken).ConfigureAwait(false); var (text, start, length) = GetReferenceInfo(referenceExcerpt, descriptor); var (before1, before2, after1, after2) = GetReferenceTexts(referenceExcerpt, tooltipExcerpt, descriptor); diff --git a/src/Features/Core/Portable/FindUsages/DefinitionItemFactory.cs b/src/Features/Core/Portable/FindUsages/DefinitionItemFactory.cs index fdf4e96c20070..45383a131ee27 100644 --- a/src/Features/Core/Portable/FindUsages/DefinitionItemFactory.cs +++ b/src/Features/Core/Portable/FindUsages/DefinitionItemFactory.cs @@ -12,7 +12,7 @@ using Microsoft.CodeAnalysis.Features.RQName; using Microsoft.CodeAnalysis.FindSymbols; using Microsoft.CodeAnalysis.FindSymbols.Finders; -using Microsoft.CodeAnalysis.Shared.Collections; +using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; @@ -244,8 +244,8 @@ private static async Task> GetSourceLocationsAsync( foreach (var location in locations) { if (location.IsInSource && - (includeHiddenLocations || location.IsVisibleSourceLocation()) && - solution.GetDocument(location.SourceTree) is { } document) + solution.GetDocument(location.SourceTree) is { } document && + (includeHiddenLocations || document.IsRazorSourceGeneratedDocument() || location.IsVisibleSourceLocation())) { var isGeneratedCode = await document.IsGeneratedCodeAsync(cancellationToken).ConfigureAwait(false); source.Add(new DocumentSpan(document, location.SourceSpan, isGeneratedCode)); @@ -300,6 +300,10 @@ private static ImmutableDictionary GetProperties(ISymbol definit { var location = referenceLocation.Location; + // Razor has a mapping service that can map from hidden locations, or will drop results if it wants to, + // so hidden locations aren't a problem, and are actually desirable. + includeHiddenLocations |= referenceLocation.Document.IsRazorSourceGeneratedDocument(); + Debug.Assert(location.IsInSource); if (!location.IsVisibleSourceLocation() && !includeHiddenLocations) diff --git a/src/LanguageServer/Protocol/Extensions/ProtocolConversions.cs b/src/LanguageServer/Protocol/Extensions/ProtocolConversions.cs index 5fcf94c969ce4..02e14778f96d8 100644 --- a/src/LanguageServer/Protocol/Extensions/ProtocolConversions.cs +++ b/src/LanguageServer/Protocol/Extensions/ProtocolConversions.cs @@ -486,37 +486,40 @@ public static LSP.Range TextSpanToRange(TextSpan textSpan, SourceText text) { Debug.Assert(document.FilePath != null); - var result = document is Document d - ? await SpanMappingHelper.TryGetMappedSpanResultAsync(d, [textSpan], cancellationToken).ConfigureAwait(false) - : null; - if (result == null) - return await ConvertTextSpanToLocationAsync(document, textSpan, isStale, cancellationToken).ConfigureAwait(false); + if (document is Document d && + SpanMappingHelper.CanMapSpans(d)) + { + var result = await SpanMappingHelper.TryGetMappedSpanResultAsync(d, [textSpan], cancellationToken).ConfigureAwait(false); + if (result is not [{ IsDefault: false } mappedSpan]) + { + // Couldn't map the span, but mapping is supported, so the mapper must not want to show include this result + return null; + } - var mappedSpan = result.Value.Single(); - if (mappedSpan.IsDefault) - return await ConvertTextSpanToLocationAsync(document, textSpan, isStale, cancellationToken).ConfigureAwait(false); + DocumentUri? uri = null; + try + { + if (PathUtilities.IsAbsolute(mappedSpan.FilePath)) + uri = CreateAbsoluteDocumentUri(mappedSpan.FilePath); + } + catch (UriFormatException) + { + } - DocumentUri? uri = null; - try - { - if (PathUtilities.IsAbsolute(mappedSpan.FilePath)) - uri = CreateAbsoluteDocumentUri(mappedSpan.FilePath); - } - catch (UriFormatException) - { - } + if (uri == null) + { + context?.TraceWarning($"Could not convert '{mappedSpan.FilePath}' to uri"); + return null; + } - if (uri == null) - { - context?.TraceWarning($"Could not convert '{mappedSpan.FilePath}' to uri"); - return null; + return new LSP.Location + { + DocumentUri = uri, + Range = MappedSpanResultToRange(mappedSpan) + }; } - return new LSP.Location - { - DocumentUri = uri, - Range = MappedSpanResultToRange(mappedSpan) - }; + return await ConvertTextSpanToLocationAsync(document, textSpan, isStale, cancellationToken).ConfigureAwait(false); static async Task ConvertTextSpanToLocationAsync( TextDocument document, diff --git a/src/LanguageServer/ProtocolUnitTests/References/FindAllReferencesHandlerTests.cs b/src/LanguageServer/ProtocolUnitTests/References/FindAllReferencesHandlerTests.cs index d1f96e1811f2a..ab833bf10c4c4 100644 --- a/src/LanguageServer/ProtocolUnitTests/References/FindAllReferencesHandlerTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/References/FindAllReferencesHandlerTests.cs @@ -379,7 +379,45 @@ public void M() var results = await RunFindAllReferencesAsync(testLspServer, testLspServer.GetLocations("caret").First()); Assert.Equal(2, results.Length); - Assert.True(results[1].Location.DocumentUri.GetRequiredParsedUri().LocalPath.EndsWith("generated_file.cs")); + Assert.True(results.Any(r => r.Location.DocumentUri.GetRequiredParsedUri().LocalPath.EndsWith("generated_file.cs"))); + + var service = Assert.IsType(workspace.Services.GetService()); + Assert.True(service.DidMapSpans); + } + + [Theory, CombinatorialData] + public async Task TestFindReferencesAsync_WithRazorSourceGeneratedFile_HiddenSpan(bool mutatingLspWorkspace) + { + var generatedMarkup = """ + public class B + { + #line hidden + public void {|reference:M|}() + { + } + } + """; + await using var testLspServer = await CreateTestLspServerAsync(""" + public class A + { + public void M() + { + new B().{|caret:M|}(); + } + } + """, mutatingLspWorkspace, CapabilitiesWithVSExtensions); + + TestFileMarkupParser.GetSpans(generatedMarkup, out var generatedCode, out ImmutableDictionary> spans); + var generatedSourceText = SourceText.From(generatedCode); + + var razorGenerator = new Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator((c) => c.AddSource("generated_file.cs", generatedCode)); + var workspace = testLspServer.TestWorkspace; + var project = workspace.CurrentSolution.Projects.First().AddAnalyzerReference(new TestGeneratorReference(razorGenerator)); + workspace.TryApplyChanges(project.Solution); + + var results = await RunFindAllReferencesAsync(testLspServer, testLspServer.GetLocations("caret").First()); + Assert.Equal(2, results.Length); + Assert.True(results.Any(r => r.Location.DocumentUri.GetRequiredParsedUri().LocalPath.EndsWith("generated_file.cs"))); var service = Assert.IsType(workspace.Services.GetService()); Assert.True(service.DidMapSpans); diff --git a/src/LanguageServer/ProtocolUnitTests/Rename/RenameTests.cs b/src/LanguageServer/ProtocolUnitTests/Rename/RenameTests.cs index 0c928fe2ed958..d15df0f810553 100644 --- a/src/LanguageServer/ProtocolUnitTests/Rename/RenameTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/Rename/RenameTests.cs @@ -317,7 +317,11 @@ void M2() workspace.TryApplyChanges(project.Solution); var generatedDocument = (await project.GetSourceGeneratedDocumentsAsync()).First(); - var renameLocation = await ProtocolConversions.TextSpanToLocationAsync(generatedDocument, spans["caret"].First(), isStale: false, CancellationToken.None); + var renameLocation = new LSP.Location() + { + DocumentUri = generatedDocument.GetURI(), + Range = ProtocolConversions.TextSpanToRange(spans["caret"].First(), generatedSourceText) + }; var renameValue = "RENAME"; var expectedEdits = testLspServer.GetLocations("renamed").Select(location => new LSP.TextEdit() { NewText = renameValue, Range = location.Range }); var expectedGeneratedEdits = spans["renamed"].Select(span => new LSP.TextEdit() { NewText = renameValue, Range = ProtocolConversions.TextSpanToRange(span, generatedSourceText) }); @@ -360,7 +364,11 @@ public void M() workspace.TryApplyChanges(project.Solution); var generatedDocument = (await project.GetSourceGeneratedDocumentsAsync()).First(); - var renameLocation = await ProtocolConversions.TextSpanToLocationAsync(generatedDocument, spans["caret"].First(), isStale: false, CancellationToken.None); + var renameLocation = new LSP.Location() + { + DocumentUri = generatedDocument.GetURI(), + Range = ProtocolConversions.TextSpanToRange(spans["caret"].First(), generatedSourceText) + }; var renameValue = "RENAME"; var expectedGeneratedEdits = spans["renamed"].Select(span => new LSP.TextEdit() { NewText = renameValue, Range = ProtocolConversions.TextSpanToRange(span, generatedSourceText) }); diff --git a/src/LanguageServer/ProtocolUnitTests/TestSourceGeneratedDocumentSpanMappingService.cs b/src/LanguageServer/ProtocolUnitTests/TestSourceGeneratedDocumentSpanMappingService.cs index d7962b84b14ec..cb9d09885c77d 100644 --- a/src/LanguageServer/ProtocolUnitTests/TestSourceGeneratedDocumentSpanMappingService.cs +++ b/src/LanguageServer/ProtocolUnitTests/TestSourceGeneratedDocumentSpanMappingService.cs @@ -12,11 +12,13 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests; [ExportWorkspaceService(typeof(ISourceGeneratedDocumentSpanMappingService))] [Shared] +[PartNotDiscoverable] [method: ImportingConstructor] [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] internal class TestSourceGeneratedDocumentSpanMappingService() : ISourceGeneratedDocumentSpanMappingService @@ -25,7 +27,7 @@ internal class TestSourceGeneratedDocumentSpanMappingService() : ISourceGenerate public bool CanMapSpans(SourceGeneratedDocument sourceGeneratedDocument) { - throw new NotImplementedException(); + return sourceGeneratedDocument.IsRazorSourceGeneratedDocument(); } public Task> GetMappedTextChangesAsync(SourceGeneratedDocument oldDocument, SourceGeneratedDocument newDocument, CancellationToken cancellationToken) @@ -33,13 +35,15 @@ public Task> GetMappedTextChangesAsync(SourceGe throw new NotImplementedException(); } - public Task> MapSpansAsync(SourceGeneratedDocument document, ImmutableArray spans, CancellationToken cancellationToken) + public async Task> MapSpansAsync(SourceGeneratedDocument document, ImmutableArray spans, CancellationToken cancellationToken) { if (document.IsRazorSourceGeneratedDocument()) { + var sourceText = await document.GetTextAsync(cancellationToken); DidMapSpans = true; + return spans.SelectAsArray(s => new MappedSpanResult(document.FilePath, sourceText.Lines.GetLinePositionSpan(s), s)); } - return Task.FromResult(ImmutableArray.Empty); + return []; } } diff --git a/src/Tools/ExternalAccess/Razor/Features/IRazorSourceGeneratedDocumentExcerptService.cs b/src/Tools/ExternalAccess/Razor/Features/IRazorSourceGeneratedDocumentExcerptService.cs new file mode 100644 index 0000000000000..96145328f4ab3 --- /dev/null +++ b/src/Tools/ExternalAccess/Razor/Features/IRazorSourceGeneratedDocumentExcerptService.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor; + +internal interface IRazorSourceGeneratedDocumentExcerptService +{ + Task TryExcerptAsync(SourceGeneratedDocument document, TextSpan span, RazorExcerptMode razorMode, RazorClassificationOptionsWrapper options, CancellationToken cancellationToken); +} diff --git a/src/Tools/ExternalAccess/Razor/Features/RazorClassificationOptionsWrapper.cs b/src/Tools/ExternalAccess/Razor/Features/RazorClassificationOptionsWrapper.cs index 89452b542d87d..5ded80eaba7a9 100644 --- a/src/Tools/ExternalAccess/Razor/Features/RazorClassificationOptionsWrapper.cs +++ b/src/Tools/ExternalAccess/Razor/Features/RazorClassificationOptionsWrapper.cs @@ -2,14 +2,17 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Runtime.Serialization; using Microsoft.CodeAnalysis.Classification; namespace Microsoft.CodeAnalysis.ExternalAccess.Razor { + [DataContract] internal readonly struct RazorClassificationOptionsWrapper { public static RazorClassificationOptionsWrapper Default = new(ClassificationOptions.Default); + [DataMember(Order = 0)] internal readonly ClassificationOptions UnderlyingObject; public RazorClassificationOptionsWrapper(ClassificationOptions underlyingObject) diff --git a/src/Tools/ExternalAccess/Razor/Features/RazorMappedSpanResult.cs b/src/Tools/ExternalAccess/Razor/Features/RazorMappedSpanResult.cs index 4d4e375b6b847..f77ecfdc8da09 100644 --- a/src/Tools/ExternalAccess/Razor/Features/RazorMappedSpanResult.cs +++ b/src/Tools/ExternalAccess/Razor/Features/RazorMappedSpanResult.cs @@ -22,11 +22,6 @@ internal readonly struct RazorMappedSpanResult public RazorMappedSpanResult(string filePath, LinePositionSpan linePositionSpan, TextSpan span) { - if (string.IsNullOrEmpty(filePath)) - { - throw new ArgumentException(nameof(filePath)); - } - FilePath = filePath; LinePositionSpan = linePositionSpan; Span = span; diff --git a/src/Tools/ExternalAccess/Razor/Features/RazorSourceGeneratedDocumentExcerptServiceWrapper.cs b/src/Tools/ExternalAccess/Razor/Features/RazorSourceGeneratedDocumentExcerptServiceWrapper.cs new file mode 100644 index 0000000000000..257c1f79814be --- /dev/null +++ b/src/Tools/ExternalAccess/Razor/Features/RazorSourceGeneratedDocumentExcerptServiceWrapper.cs @@ -0,0 +1,53 @@ + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Classification; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor; + +[ExportWorkspaceService(typeof(ISourceGeneratedDocumentExcerptService)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class RazorSourceGeneratedDocumentExcerptServiceWrapper( + [Import(AllowDefault = true)] IRazorSourceGeneratedDocumentExcerptService? implementation) : ISourceGeneratedDocumentExcerptService +{ + private readonly IRazorSourceGeneratedDocumentExcerptService? _implementation = implementation; + + public bool CanExcerpt(SourceGeneratedDocument document) + { + return _implementation is not null && document.IsRazorSourceGeneratedDocument(); + } + + public async Task TryExcerptAsync(SourceGeneratedDocument document, TextSpan span, ExcerptMode mode, ClassificationOptions classificationOptions, CancellationToken cancellationToken) + { + if (_implementation is null || !document.IsRazorSourceGeneratedDocument()) + { + return null; + } + + var razorMode = mode switch + { + ExcerptMode.SingleLine => RazorExcerptMode.SingleLine, + ExcerptMode.Tooltip => RazorExcerptMode.Tooltip, + _ => throw ExceptionUtilities.UnexpectedValue(mode), + }; + + var options = new RazorClassificationOptionsWrapper(classificationOptions); + var result = await _implementation.TryExcerptAsync(document, span, razorMode, options, cancellationToken).ConfigureAwait(false); + + if (result is null) + return null; + + var razorExcerpt = result.Value; + return new ExcerptResult(razorExcerpt.Content, razorExcerpt.MappedSpan, razorExcerpt.ClassifiedSpans, razorExcerpt.Document, razorExcerpt.Span); + } +} diff --git a/src/Tools/ExternalAccess/Razor/Features/RazorSourceGeneratedDocumentSpanMappingServiceWrapper.cs b/src/Tools/ExternalAccess/Razor/Features/RazorSourceGeneratedDocumentSpanMappingServiceWrapper.cs index 1b02fc896051c..2b8c72f557b36 100644 --- a/src/Tools/ExternalAccess/Razor/Features/RazorSourceGeneratedDocumentSpanMappingServiceWrapper.cs +++ b/src/Tools/ExternalAccess/Razor/Features/RazorSourceGeneratedDocumentSpanMappingServiceWrapper.cs @@ -69,7 +69,8 @@ public async Task> MapSpansAsync(SourceGenerate } var mappedSpans = await _implementation.MapSpansAsync(document, spans, cancellationToken).ConfigureAwait(false); - if (mappedSpans.Length != spans.Length) + if (mappedSpans.IsDefault || + mappedSpans.Length != spans.Length) { return []; } diff --git a/src/VisualStudio/Core/Def/FindReferences/Contexts/AbstractTableDataSourceFindUsagesContext.cs b/src/VisualStudio/Core/Def/FindReferences/Contexts/AbstractTableDataSourceFindUsagesContext.cs index 85e1b6be99b42..c04271ba7a0ed 100644 --- a/src/VisualStudio/Core/Def/FindReferences/Contexts/AbstractTableDataSourceFindUsagesContext.cs +++ b/src/VisualStudio/Core/Def/FindReferences/Contexts/AbstractTableDataSourceFindUsagesContext.cs @@ -471,15 +471,13 @@ protected async Task AddDocumentSpanEntriesAsync( var document = documentSpan.Document; var sourceSpan = documentSpan.SourceSpan; - var excerptService = document.DocumentServiceProvider.GetService(); - // Fetching options is expensive enough to try to avoid it if we can. So only fetch this if absolutely necessary. ClassificationOptions? options = null; - if (excerptService != null) + if (DocumentExcerptHelper.CanExcerpt(document)) { options ??= _globalOptions.GetClassificationOptions(document.Project.Language); - var result = await excerptService.TryExcerptAsync(document, sourceSpan, ExcerptMode.SingleLine, options.Value, cancellationToken).ConfigureAwait(false); + var result = await DocumentExcerptHelper.TryExcerptAsync(document, sourceSpan, ExcerptMode.SingleLine, options.Value, cancellationToken).ConfigureAwait(false); if (result != null) return (result.Value, AbstractDocumentSpanEntry.GetLineContainingPosition(result.Value.Content, result.Value.MappedSpan.Start)); } diff --git a/src/VisualStudio/Core/Def/FindReferences/Entries/DocumentSpanEntry.cs b/src/VisualStudio/Core/Def/FindReferences/Entries/DocumentSpanEntry.cs index 7b69f24be8945..5adedb534aa3a 100644 --- a/src/VisualStudio/Core/Def/FindReferences/Entries/DocumentSpanEntry.cs +++ b/src/VisualStudio/Core/Def/FindReferences/Entries/DocumentSpanEntry.cs @@ -183,11 +183,10 @@ private DisposableToolTip CreateDisposableToolTip(Document document, TextSpan so var controlService = document.Project.Solution.Services.GetRequiredService(); var sourceText = document.GetTextSynchronously(CancellationToken.None); - var excerptService = document.DocumentServiceProvider.GetService(); - if (excerptService != null) + if (DocumentExcerptHelper.CanExcerpt(document)) { var classificationOptions = Presenter._globalOptions.GetClassificationOptions(document.Project.Language); - var excerpt = this.Presenter.ThreadingContext.JoinableTaskFactory.Run(() => excerptService.TryExcerptAsync(document, sourceSpan, ExcerptMode.Tooltip, classificationOptions, CancellationToken.None)); + var excerpt = this.Presenter.ThreadingContext.JoinableTaskFactory.Run(() => DocumentExcerptHelper.TryExcerptAsync(document, sourceSpan, ExcerptMode.Tooltip, classificationOptions, CancellationToken.None)); if (excerpt != null) { // get tooltip from excerpt service diff --git a/src/Workspaces/Core/Portable/Classification/ClassifiedSpan.cs b/src/Workspaces/Core/Portable/Classification/ClassifiedSpan.cs index 0762b05fcedb5..6f01c1d4588f7 100644 --- a/src/Workspaces/Core/Portable/Classification/ClassifiedSpan.cs +++ b/src/Workspaces/Core/Portable/Classification/ClassifiedSpan.cs @@ -3,14 +3,18 @@ // See the LICENSE file in the project root for more information. using System; +using System.Runtime.Serialization; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Classification; +[DataContract] public readonly struct ClassifiedSpan(TextSpan textSpan, string classificationType) : IEquatable { + [DataMember(Order = 0)] public string ClassificationType { get; } = classificationType; + [DataMember(Order = 1)] public TextSpan TextSpan { get; } = textSpan; public ClassifiedSpan(string classificationType, TextSpan textSpan) diff --git a/src/Workspaces/Core/Portable/Workspace/Host/DocumentService/DocumentExcerptHelper.cs b/src/Workspaces/Core/Portable/Workspace/Host/DocumentService/DocumentExcerptHelper.cs new file mode 100644 index 0000000000000..b409b22a479fc --- /dev/null +++ b/src/Workspaces/Core/Portable/Workspace/Host/DocumentService/DocumentExcerptHelper.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Classification; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Host; + +internal static class DocumentExcerptHelper +{ + public static bool CanExcerpt(Document document) + { + if (document is SourceGeneratedDocument sourceGeneratedDocument && + document.Project.Solution.Services.GetService() is { } sourceGeneratedExcerptService) + { + return sourceGeneratedExcerptService.CanExcerpt(sourceGeneratedDocument); + } + + return document.DocumentServiceProvider.GetService() is not null; + } + + public static Task TryExcerptAsync(Document document, TextSpan span, ExcerptMode mode, ClassificationOptions classificationOptions, CancellationToken cancellationToken) + { + if (document is SourceGeneratedDocument sourceGeneratedDocument && + document.Project.Solution.Services.GetService() is { } sourceGeneratedExcerptService) + { + return sourceGeneratedExcerptService.TryExcerptAsync(sourceGeneratedDocument, span, mode, classificationOptions, cancellationToken); + } + + var excerptService = document.DocumentServiceProvider.GetService(); + if (excerptService == null) + { + return SpecializedTasks.Default(); + } + + return excerptService.TryExcerptAsync(document, span, mode, classificationOptions, cancellationToken); + } +} diff --git a/src/Workspaces/Core/Portable/Workspace/Host/DocumentService/ISourceGeneratedDocumentExcerptService.cs b/src/Workspaces/Core/Portable/Workspace/Host/DocumentService/ISourceGeneratedDocumentExcerptService.cs new file mode 100644 index 0000000000000..1c07638fd7710 --- /dev/null +++ b/src/Workspaces/Core/Portable/Workspace/Host/DocumentService/ISourceGeneratedDocumentExcerptService.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Classification; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.Host; + +internal interface ISourceGeneratedDocumentExcerptService : IWorkspaceService +{ + bool CanExcerpt(SourceGeneratedDocument document); + + Task TryExcerptAsync(SourceGeneratedDocument document, TextSpan span, ExcerptMode mode, ClassificationOptions classificationOptions, CancellationToken cancellationToken); +}