|
// 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 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 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.ToImmutableArray();
}
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);
}
}
}
|