File: Shared\Extensions\IProjectionBufferFactoryServiceExtensions.cs
Web Access
Project: src\src\EditorFeatures\Core\Microsoft.CodeAnalysis.EditorFeatures.csproj (Microsoft.CodeAnalysis.EditorFeatures)
// 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.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods;
using Microsoft.VisualStudio.Text.Projection;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions;
 
internal static class IProjectionBufferFactoryServiceExtensions
{
    public const string RoslynPreviewContentType = nameof(RoslynPreviewContentType);
 
    /// <summary>
    /// Hack to get view taggers working on our preview surfaces.  We need to define
    /// both projection and text in order for this to work.  Talk to JasonMal for he is the only
    /// one who understands this.
    /// </summary>
    [Export]
    [Name(RoslynPreviewContentType)]
    [BaseDefinition("text")]
    [BaseDefinition("projection")]
    public static readonly ContentTypeDefinition? RoslynPreviewContentTypeDefinition;
 
    public static IProjectionBuffer CreateProjectionBufferWithoutIndentation(
        this IProjectionBufferFactoryService factoryService,
        IEditorOptions editorOptions,
        IContentType? contentType = null,
        params SnapshotSpan[] exposedSpans)
    {
        return factoryService.CreateProjectionBufferWithoutIndentation(
            editorOptions,
            contentType,
            (IEnumerable<SnapshotSpan>)exposedSpans);
    }
 
    public static IProjectionBuffer CreateProjectionBufferWithoutIndentation(
        this IProjectionBufferFactoryService factoryService,
        IEditorOptions editorOptions,
        IContentType? contentType,
        IEnumerable<SnapshotSpan> exposedSpans)
    {
        var spans = new NormalizedSnapshotSpanCollection(exposedSpans);
 
        if (spans.Count > 0)
        {
            // BUG(6335): We have to make sure that the spans refer to the current snapshot of
            // the buffer.
            var buffer = spans.First().Snapshot.TextBuffer;
            var currentSnapshot = buffer.CurrentSnapshot;
            spans = new NormalizedSnapshotSpanCollection(
                spans.Select(s => s.TranslateTo(currentSnapshot, SpanTrackingMode.EdgeExclusive)));
        }
 
        contentType ??= factoryService.ProjectionContentType;
        var projectionBuffer = factoryService.CreateProjectionBuffer(
            projectionEditResolver: null,
            sourceSpans: [],
            options: ProjectionBufferOptions.None,
            contentType: contentType);
 
        if (spans.Count > 0)
        {
            var finalSpans = new List<object>();
 
            // We need to figure out the shorted indentation level of the exposed lines.  We'll
            // then remove that indentation from all lines.
            var indentationColumn = DetermineIndentationColumn(editorOptions, spans);
 
            foreach (var span in spans)
            {
                var snapshot = span.Snapshot;
                var startLineNumber = snapshot.GetLineNumberFromPosition(span.Start);
                var endLineNumber = snapshot.GetLineNumberFromPosition(span.End);
 
                for (var lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++)
                {
                    // Compute the span clamped to this line
                    var line = snapshot.GetLineFromLineNumber(lineNumber);
                    var finalSpanStart = Math.Max(line.Start, span.Start);
                    var finalSpanEnd = Math.Min(line.EndIncludingLineBreak, span.End);
 
                    // We'll only offset if our span doesn't already start at the start of the line. See the similar exclusion in
                    // DetermineIndentationColumn that this matches.
                    if (line.Start == finalSpanStart)
                    {
                        finalSpanStart += line.GetLineOffsetFromColumn(indentationColumn, editorOptions);
 
                        // Paranoia: what if the indentation reversed our ordering?
                        if (finalSpanStart > finalSpanEnd)
                        {
                            finalSpanStart = finalSpanEnd;
                        }
                    }
 
                    // We don't expect edits to happen while this projection buffer is active. We'll choose EdgeExclusive so
                    // if they do we don't end up in any cases where there is overlapping source spans.
                    finalSpans.Add(snapshot.CreateTrackingSpan(Span.FromBounds(finalSpanStart, finalSpanEnd), SpanTrackingMode.EdgeExclusive));
                }
            }
 
            projectionBuffer.InsertSpans(0, finalSpans);
        }
 
        return projectionBuffer;
    }
 
    private static int DetermineIndentationColumn(
        IEditorOptions editorOptions,
        IEnumerable<SnapshotSpan> spans)
    {
        int? indentationColumn = null;
        foreach (var span in spans)
        {
            var snapshot = span.Snapshot;
            var startLineNumber = snapshot.GetLineNumberFromPosition(span.Start);
            var endLineNumber = snapshot.GetLineNumberFromPosition(span.End);
 
            // If the span starts after the first non-whitespace of the first line, we'll
            // exclude that line to avoid throwing off the calculation. Otherwise, the
            // incorrect indentation will be returned for lambda cases like so:
            //
            // void M()
            // {
            //     Func<int> f = () =>
            //         {
            //             return 1;
            //         };
            // }
            //
            // Without throwing out the first line in the example above, the indentation column
            // used will be 4, rather than 8.
            var startLineFirstNonWhitespace = snapshot.GetLineFromLineNumber(startLineNumber).GetFirstNonWhitespacePosition();
            if (startLineFirstNonWhitespace.HasValue && startLineFirstNonWhitespace.Value < span.Start)
            {
                startLineNumber++;
            }
 
            for (var lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++)
            {
                var line = snapshot.GetLineFromLineNumber(lineNumber);
                if (string.IsNullOrWhiteSpace(line.GetText()))
                {
                    continue;
                }
 
                indentationColumn = indentationColumn.HasValue
                    ? Math.Min(indentationColumn.Value, line.GetColumnOfFirstNonWhitespaceCharacterOrEndOfLine(editorOptions))
                    : line.GetColumnOfFirstNonWhitespaceCharacterOrEndOfLine(editorOptions);
            }
        }
 
        return indentationColumn ?? 0;
    }
 
    public static IProjectionBuffer CreateProjectionBuffer(
        this IProjectionBufferFactoryService factoryService,
        IContentTypeRegistryService registryService,
        IEditorOptions editorOptions,
        ITextSnapshot snapshot,
        string separator,
        params LineSpan[] exposedLineSpans)
    {
        return CreateProjectionBuffer(
            factoryService,
            registryService,
            editorOptions,
            snapshot,
            separator,
            suffixOpt: null,
            trim: false,
            exposedLineSpans: exposedLineSpans);
    }
 
    public static IProjectionBuffer CreateProjectionBufferWithoutIndentation(
        this IProjectionBufferFactoryService factoryService,
        IContentTypeRegistryService registryService,
        IEditorOptions editorOptions,
        ITextSnapshot snapshot,
        string separator,
        params LineSpan[] exposedLineSpans)
    {
        return factoryService.CreateProjectionBufferWithoutIndentation(
            registryService,
            editorOptions,
            snapshot,
            separator,
            suffixOpt: null,
            exposedLineSpans: exposedLineSpans);
    }
 
    public static IProjectionBuffer CreateProjectionBufferWithoutIndentation(
        this IProjectionBufferFactoryService factoryService,
        IContentTypeRegistryService registryService,
        IEditorOptions editorOptions,
        ITextSnapshot snapshot,
        string separator,
        object? suffixOpt,
        params LineSpan[] exposedLineSpans)
    {
        return CreateProjectionBuffer(
            factoryService,
            registryService,
            editorOptions,
            snapshot,
            separator,
            suffixOpt,
            trim: true,
            exposedLineSpans: exposedLineSpans);
    }
 
    public static IProjectionBuffer CreatePreviewProjectionBuffer(
        this IProjectionBufferFactoryService factoryService,
        IList<object> sourceSpans,
        IContentTypeRegistryService registryService)
    {
        return factoryService.CreateProjectionBuffer(
            projectionEditResolver: null,
            sourceSpans: sourceSpans,
            options: ProjectionBufferOptions.None,
            contentType: registryService.GetContentType(RoslynPreviewContentType));
    }
 
    private static IProjectionBuffer CreateProjectionBuffer(
        IProjectionBufferFactoryService factoryService,
        IContentTypeRegistryService registryService,
        IEditorOptions editorOptions,
        ITextSnapshot snapshot,
        string separator,
        object? suffixOpt,
        bool trim,
        params LineSpan[] exposedLineSpans)
    {
        var spans = new List<object>();
        if (exposedLineSpans.Length > 0)
        {
            if (exposedLineSpans[0].Start > 0 && !string.IsNullOrEmpty(separator))
            {
                spans.Add(separator);
                spans.Add(editorOptions.GetNewLineCharacter());
            }
 
            var snapshotSpanRanges = CreateSnapshotSpanRanges(snapshot, exposedLineSpans);
            var indentColumn = trim
                ? DetermineIndentationColumn(editorOptions, snapshotSpanRanges.Flatten())
                : 0;
 
            foreach (var snapshotSpanRange in snapshotSpanRanges)
            {
                foreach (var snapshotSpan in snapshotSpanRange)
                {
                    var line = snapshotSpan.Snapshot.GetLineFromPosition(snapshotSpan.Start);
                    var indentPosition = line.GetLineOffsetFromColumn(indentColumn, editorOptions) + line.Start;
                    var mappedSpan = new SnapshotSpan(snapshotSpan.Snapshot,
                        Span.FromBounds(indentPosition, snapshotSpan.End));
 
                    var trackingSpan = mappedSpan.CreateTrackingSpan(SpanTrackingMode.EdgeExclusive);
 
                    spans.Add(trackingSpan);
 
                    // Add a newline between every line.
                    if (snapshotSpan != snapshotSpanRange.Last())
                    {
                        spans.Add(editorOptions.GetNewLineCharacter());
                    }
                }
 
                // Add a separator between every set of lines.
                if (snapshotSpanRange != snapshotSpanRanges.Last())
                {
                    spans.Add(editorOptions.GetNewLineCharacter());
                    spans.Add(separator);
                    spans.Add(editorOptions.GetNewLineCharacter());
                }
            }
 
            if (snapshot.GetLineNumberFromPosition(snapshotSpanRanges.Last().Last().End) < snapshot.LineCount - 1)
            {
                spans.Add(editorOptions.GetNewLineCharacter());
                spans.Add(separator);
            }
        }
 
        if (suffixOpt != null)
        {
            if (spans.Count >= 0)
            {
                if (!separator.Equals(spans.Last()))
                {
                    spans.Add(editorOptions.GetNewLineCharacter());
                    spans.Add(separator);
                }
 
                spans.Add(editorOptions.GetNewLineCharacter());
            }
 
            spans.Add(suffixOpt);
        }
 
        return factoryService.CreateProjectionBuffer(
            projectionEditResolver: null,
            sourceSpans: spans,
            options: ProjectionBufferOptions.None,
            contentType: registryService.GetContentType(RoslynPreviewContentType));
    }
 
    private static IList<IList<SnapshotSpan>> CreateSnapshotSpanRanges(ITextSnapshot snapshot, LineSpan[] exposedLineSpans)
    {
        var result = new List<IList<SnapshotSpan>>();
        foreach (var lineSpan in exposedLineSpans)
        {
            var snapshotSpans = CreateSnapshotSpans(snapshot, lineSpan);
            if (snapshotSpans.Count > 0)
            {
                result.Add(snapshotSpans);
            }
        }
 
        return result;
    }
 
    private static IList<SnapshotSpan> CreateSnapshotSpans(ITextSnapshot snapshot, LineSpan lineSpan)
    {
        var result = new List<SnapshotSpan>();
        for (var i = lineSpan.Start; i < lineSpan.End; i++)
        {
            var line = snapshot.GetLineFromLineNumber(i);
            result.Add(line.Extent);
        }
 
        return result;
    }
}