/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/VisualStudio/Core/Def/Venus/ContainedDocument.DocumentServiceProvider.cs
321 строка
15 KB
Cyrus Najmabadi
Make sealed
26 мар 2025, 22:21
26 мар 2025, 22:21
d99b771
Код
Авторство
О чём код?
// 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. #nullable disable using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Classification; using Microsoft.CodeAnalysis.Editor.Shared.Extensions; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Text.Shared.Extensions; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Projection; using Roslyn.Utilities; namespace Microsoft.VisualStudio.LanguageServices.Implementation.Venus; internal sealed partial class ContainedDocument { // this is to support old venus/razor case before dev16. // all new razor (asp.NET core after dev16) should use their own implementation not ours public sealed class DocumentServiceProvider : IDocumentServiceProvider { private readonly SpanMapper _spanMapper; private readonly DocumentExcerpter _excerpter; public DocumentServiceProvider(ITextBuffer primaryBuffer) { _spanMapper = new SpanMapper(primaryBuffer); _excerpter = new DocumentExcerpter(primaryBuffer); } public TService GetService<TService>() where TService : class, IDocumentService { if (_spanMapper is TService spanMapper) { return spanMapper; } if (_excerpter is TService excerpter) { return excerpter; } // ask the default document service provider return DefaultTextDocumentServiceProvider.Instance.GetService<TService>(); } private static ITextSnapshot GetRoslynSnapshot(SourceText sourceText) => sourceText.FindCorrespondingEditorTextSnapshot(); private sealed class SpanMapper : AbstractSpanMappingService { private readonly ITextBuffer _primaryBuffer; public SpanMapper(ITextBuffer primaryBuffer) => _primaryBuffer = primaryBuffer; /// <summary> /// Legacy venus does not support us adding import directives and them mapping them to their own concepts. /// </summary> public override bool SupportsMappingImportDirectives => false; public override async Task<ImmutableArray<(string mappedFilePath, TextChange mappedTextChange)>> GetMappedTextChangesAsync( Document oldDocument, Document newDocument, CancellationToken cancellationToken) { var textChanges = (await newDocument.GetTextChangesAsync(oldDocument, cancellationToken).ConfigureAwait(false)).ToImmutableArray(); var mappedSpanResults = await MapSpansAsync(oldDocument, textChanges.Select(tc => tc.Span), CancellationToken.None).ConfigureAwait(false); var mappedTextChanges = MatchMappedSpansToTextChanges(textChanges, mappedSpanResults); return mappedTextChanges; } public override async Task<ImmutableArray<MappedSpanResult>> MapSpansAsync( Document document, IEnumerable<TextSpan> spans, CancellationToken cancellationToken) { // REVIEW: for now, we keep document here due to open file case, otherwise, we need to create new SpanMappingService for every char user types. var sourceText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); // _primary buffer (in this case razor html files) is not in roslyn snapshot, so mapping from roslyn snapshot to razor document // always just map to current snapshot which have potential to have a race since content could have changed while we are doing this. // but for ones that uses this implementation, it always had that possiblity. so this doesn't change that aspect due to this. var primarySnapshot = (IProjectionSnapshot)_primaryBuffer.CurrentSnapshot; var roslynSnapshot = GetRoslynSnapshot(sourceText); if (roslynSnapshot == null) { return default; } var builder = ArrayBuilder<MappedSpanResult>.GetInstance(); foreach (var span in spans) { var result = (MappedSpanResult?)null; foreach (var primarySpan in primarySnapshot.MapFromSourceSnapshot(span.ToSnapshotSpan(roslynSnapshot))) { // this is from http://index/?query=MapSecondaryToPrimarySpan&rightProject=Microsoft.VisualStudio.Editor.Implementation&file=VsTextBufferCoordinatorAdapter.cs&line=177 // make sure we only consider one that's not split if (primarySpan.Length != span.Length) { continue; } // take the first one. // contained document file path points cshtml this secondary buffer belong to var primarySnapshotSpan = new SnapshotSpan(primarySnapshot, primarySpan); result = new MappedSpanResult(document.FilePath, primarySnapshotSpan.ToLinePositionSpan(), primarySpan.ToTextSpan()); break; } builder.Add(result ?? default); } return builder.ToImmutableAndFree(); } } private sealed class DocumentExcerpter : IDocumentExcerptService { private readonly ITextBuffer _primaryBuffer; public DocumentExcerpter(ITextBuffer primaryBuffer) => _primaryBuffer = primaryBuffer; public async Task<ExcerptResult?> TryExcerptAsync(Document document, TextSpan span, ExcerptMode mode, ClassificationOptions classificationOptions, CancellationToken cancellationToken) { // REVIEW: for now, we keep document here due to open file case, otherwise, we need to create new DocumentExcerpter for every char user types. var sourceText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); var primarySnapshot = (IProjectionSnapshot)_primaryBuffer.CurrentSnapshot; var roslynSnapshot = GetRoslynSnapshot(sourceText); if (roslynSnapshot == null) { return null; } var spanOnPrimarySnapshot = MapRoslynSpanToPrimarySpan(primarySnapshot, roslynSnapshot, span); if (spanOnPrimarySnapshot == null) { return null; } var contentSpanOnPrimarySnapshot = GetContentSpanFromPrimarySpan(mode, spanOnPrimarySnapshot.Value); if (contentSpanOnPrimarySnapshot == null) { // can't figure out span to extract content from return null; } var (content, spanOnContent) = GetContentAndMappedSpan(mode, spanOnPrimarySnapshot.Value, contentSpanOnPrimarySnapshot.Value); if (content == null) { return null; } var classifiedSpansOnContent = await GetClassifiedSpansOnContentAsync(document, roslynSnapshot, contentSpanOnPrimarySnapshot.Value, classificationOptions, cancellationToken).ConfigureAwait(false); // the default implementation has no idea how to classify the primary snapshot return new ExcerptResult(content, spanOnContent, classifiedSpansOnContent, document, span); } private static async Task<ImmutableArray<ClassifiedSpan>> GetClassifiedSpansOnContentAsync( Document document, ITextSnapshot roslynSnapshot, SnapshotSpan contentSpanOnPrimarySnapshot, ClassificationOptions options, CancellationToken cancellationToken) { var primarySnapshot = (IProjectionSnapshot)contentSpanOnPrimarySnapshot.Snapshot; // map content span on the primary buffer to second buffer and for ones that can be mapped, // get classification for those portion on secondary buffer and convert span on those to // span on the content and create ClassifiedSpan var contentSpan = contentSpanOnPrimarySnapshot.Span.ToTextSpan(); // anything based on content is starting from 0 var startPositionOnContentSpan = GetNonWhitespaceStartPositionOnContent(contentSpanOnPrimarySnapshot); using var _1 = Classifier.GetPooledList(out var list); foreach (var roslynSpan in primarySnapshot.MapToSourceSnapshots(contentSpanOnPrimarySnapshot.Span)) { if (roslynSnapshot.TextBuffer != roslynSpan.Snapshot.TextBuffer) { // not mapped to right buffer. ignore continue; } // we don't have guarantee that primary snapshot is from same snapshot as roslyn snapshot. make // sure we map it to right snapshot var fixedUpSpan = roslynSpan.TranslateTo(roslynSnapshot, SpanTrackingMode.EdgeExclusive); var classifiedSpans = await ClassifierHelper.GetClassifiedSpansAsync( document, fixedUpSpan.Span.ToTextSpan(), options, includeAdditiveSpans: false, cancellationToken).ConfigureAwait(false); if (classifiedSpans.IsDefault) { continue; } foreach (var classifiedSpan in classifiedSpans) { var mappedSpan = MapRoslynSpanToPrimarySpan(primarySnapshot, roslynSnapshot, classifiedSpan.TextSpan); if (mappedSpan == null) { continue; } var spanOnContentSpan = GetSpanOnContent(mappedSpan.Value.Span.ToTextSpan(), contentSpan); if (spanOnContentSpan.Start < startPositionOnContentSpan) { // skip span before start position. continue; } list.Add(new ClassifiedSpan(spanOnContentSpan, classifiedSpan.ClassificationType)); } } // classifier expects there is no gap between classification spans. any empty space // from the above classification call will be filled with "text" // // the EditorClassifier call above fills all the gaps for the span it is called with, but we are combining // multiple spans with html code, so we need to fill those gaps using var _2 = Classifier.GetPooledList(out var builder); ClassifierHelper.FillInClassifiedSpanGaps(startPositionOnContentSpan, list, builder); // add html after roslyn content if there is any if (builder.Count == 0) { // no roslyn code. add all as html code builder.Add(new ClassifiedSpan(new TextSpan(0, contentSpan.Length), ClassificationTypeNames.Text)); } else { var lastSpan = builder[^1].TextSpan; if (lastSpan.End < contentSpan.Length) { builder.Add(new ClassifiedSpan(new TextSpan(lastSpan.End, contentSpan.Length - lastSpan.End), ClassificationTypeNames.Text)); } } return [.. builder]; } private static int GetNonWhitespaceStartPositionOnContent(SnapshotSpan spanOnPrimarySnapshot) { for (var i = spanOnPrimarySnapshot.Start.Position; i < spanOnPrimarySnapshot.End.Position; i++) { if (!char.IsWhiteSpace(spanOnPrimarySnapshot.Snapshot[i])) { return i - spanOnPrimarySnapshot.Start.Position; } } return spanOnPrimarySnapshot.Length; } private static SnapshotSpan? MapRoslynSpanToPrimarySpan(IProjectionSnapshot primarySnapshot, ITextSnapshot roslynSnapshot, TextSpan span) { var primarySpans = primarySnapshot.MapFromSourceSnapshot(span.ToSnapshotSpan(roslynSnapshot)); if (primarySpans.Count != 1) { // default version doesn't support where span mapped multiple primary buffer spans return null; } return new SnapshotSpan(primarySnapshot, primarySpans[0]); } private static (SourceText, TextSpan) GetContentAndMappedSpan(ExcerptMode mode, SnapshotSpan primarySpan, SnapshotSpan contentSpan) { var line = primarySpan.Start.GetContainingLine(); if (mode == ExcerptMode.SingleLine) { return (line.Snapshot.AsText().GetSubText(contentSpan.Span.ToTextSpan()), GetSpanOnContent(primarySpan.Span.ToTextSpan(), contentSpan.Span.ToTextSpan())); } if (mode == ExcerptMode.Tooltip) { return (line.Snapshot.AsText().GetSubText(contentSpan.Span.ToTextSpan()), GetSpanOnContent(primarySpan.Span.ToTextSpan(), contentSpan.Span.ToTextSpan())); } return (null, default); } private static SnapshotSpan? GetContentSpanFromPrimarySpan(ExcerptMode mode, SnapshotSpan primarySpan) { var line = primarySpan.Start.GetContainingLine(); if (mode == ExcerptMode.SingleLine) { // the line where primary span is on return line.Extent; } if (mode == ExcerptMode.Tooltip) { // +-3 line of the line where primary span is on const int AdditionalLineCountPerSide = 3; var startLine = line.Snapshot.GetLineFromLineNumber(Math.Max(0, line.LineNumber - AdditionalLineCountPerSide)); var endLine = line.Snapshot.GetLineFromLineNumber(Math.Min(line.Snapshot.LineCount - 1, line.LineNumber + AdditionalLineCountPerSide)); return new SnapshotSpan(line.Snapshot, Span.FromBounds(startLine.Extent.Start.Position, endLine.Extent.End.Position)); } return null; } private static TextSpan GetSpanOnContent(TextSpan targetSpan, TextSpan excerptSpan) => new(targetSpan.Start - excerptSpan.Start, targetSpan.Length); } } }