File: StringIndentation\StringIndentationAdornmentManager.VisibleBlock.cs
Web Access
Project: src\src\EditorFeatures\Core\Microsoft.CodeAnalysis.EditorFeatures_3hxu4syd_wpftmp.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.Immutable;
using System.Diagnostics;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Formatting;
 
namespace Microsoft.CodeAnalysis.Editor.StringIndentation;
 
internal sealed partial class StringIndentationAdornmentManager
{
    /// <summary>
    /// Represents the X position of the vertical line we're drawing, and the chunks of that vertical line if we
    /// need to break it up (for example if we need to jump past interpolation holes).
    /// 
    /// Forked from https://devdiv.visualstudio.com/DevDiv/_git/VS-Platform?path=/src/Editor/Text/Impl/Structure/VisibleBlock.cs
    /// </summary>
    private readonly struct VisibleBlock
    {
        public readonly double X;
        public readonly ImmutableArray<(double start, double end)> YSegments;
 
        private VisibleBlock(double x, ImmutableArray<(double start, double end)> ySegments)
        {
            X = x;
            YSegments = ySegments;
        }
 
        public static VisibleBlock? CreateVisibleBlock(
            SnapshotSpan span, ImmutableArray<SnapshotSpan> orderedHoleSpans, IWpfTextView view)
        {
            // This method assumes that we've already been mapped to the view's snapshot.
            Debug.Assert(span.Snapshot == view.TextSnapshot);
 
            // While editing (for example, deleting all the whitespace before the final quotes) the indentation line
            // may move to the 0 column (temporarily until we compute the updated tags and realize this tag needs to
            // go).  Don't try to draw the line here as moving the line back by one will place it at the end of the
            // previous line (which will cause a very distracting visual glitch).
            if (span.End.GetContainingLine().Start == span.End)
                return null;
 
            // This position could actually be 0 if the entire doc is deleted
            // and we haven't recomputed the updated tags yet. So be resilient
            // for the position being out of bounds.
            if (span.End == 0)
                return null;
 
            // We want to draw the line right before the quote character. So -1 to get that character's position.
            // If we position the adornment right at the end of that character it will visually merge with the
            // text, so we want to back it off a little bit to the left. Half of a virtual space is gonna do the trick.
            // It is important to keep this value independent of what space character is used to avoid visual bugs
            // like https://github.com/dotnet/roslyn/issues/64230
            var bufferPosition = span.End - 1;
            var anchorPointLine = view.GetTextViewLineContainingBufferPosition(bufferPosition);
            var bounds = anchorPointLine.GetCharacterBounds(bufferPosition);
            var x = Math.Floor(bounds.Right - (anchorPointLine.VirtualSpaceWidth / 2));
 
            var firstLine = view.TextViewLines.FirstVisibleLine;
            var lastLine = view.TextViewLines.LastVisibleLine;
 
            // Bug #557472: When performing a layout while scrolling downwards, the editor can occasionally
            // invalidate spans that are not visible. When we ask for the start and end point for adornments,
            // if the TextViewLinesCollection doesn't contain the start or end point of the GuideLineSpan, we
            // usually assume that it's the top of the first visible line, or the bottom of the last visible
            // line, respectively. If the editor invalidates an invisible span, this can result in an erroneous
            // top to bottom adornment.
            var guideLineSpanStart = span.Start;
            var guideLineSpanEnd = span.End;
 
            if ((guideLineSpanStart > lastLine.End) ||
                (guideLineSpanEnd < firstLine.Start))
            {
                return null;
            }
 
            var guideLineTopLine = view.TextViewLines.GetTextViewLineContainingBufferPosition(guideLineSpanStart);
            var guideLineBottomLine = view.TextViewLines.GetTextViewLineContainingBufferPosition(guideLineSpanEnd);
 
            // This is slightly subtle.  First, the line might start on a line that is above/below what the actual
            // view is displaying.  In that case we want to draw up to the boundary of the view to make it look like
            // the line is correctly going past that to wherever it starts/ends at.
            //
            // Second, the span we are working with actually includes the lines containing the delimiters of the
            // string literal (since those are the only lines we're certain we have content we can snap the line
            // to).  But we don't want the vertical line to actually be drawn into those lines.  So if those lines
            // are visible, we draw at the interior border of them so that the vertical-line does not intrude into
            // them.
            var yTop = guideLineTopLine == null ? firstLine.Top : guideLineTopLine.Bottom;
            var yBottom = guideLineBottomLine == null ? lastLine.Bottom : guideLineBottomLine.Top;
 
            // Now that we have the 'x' coordinate of hte vertical line, and the top/bottom points we want to draw
            // it through, actually create line segments to draw.  We have segments in case there are gaps in the
            // line we don't want to draw (for example, for a hole).
            return new VisibleBlock(x, CreateVisibleSegments(view.TextViewLines, span, orderedHoleSpans, x, yTop, yBottom));
        }
 
        /// <summary>
        /// Given the horizontal position <paramref name="x"/> and the <paramref name="extent"/> to draw the
        /// vertical line through, create a set of vertical chunks to actually draw.  Multiple chunks happen when we
        /// have interpolation holes we have to skip over.
        /// </summary>
        private static ImmutableArray<(double start, double end)> CreateVisibleSegments(
            ITextViewLineCollection linesCollection,
            SnapshotSpan extent,
            ImmutableArray<SnapshotSpan> orderedHoleSpans,
            double x,
            double yTop,
            double yBottom)
        {
            using var _ = ArrayBuilder<(double start, double end)>.GetInstance(out var segments);
 
            // MinLineHeight must always be larger than ContinuationPadding so that no segments
            // are created for vertical spans between lines.
            const double MinLineHeight = 2.1;
            const double ContinuationPadding = 2.0;
 
            // Find the lines in Block's extent that are currently visible.
            // TODO: can we eliminate or reuse the allocation for this list?
            var visibleSpanTextViewLinesCollection = linesCollection.GetTextViewLinesIntersectingSpan(extent);
 
            var currentSegmentTop = yTop;
            var currentSegmentBottom = 0.0;
 
            // Iterate the visible lines of the Block's extent.
            for (var i = 0; i < visibleSpanTextViewLinesCollection.Count; i++)
            {
                var line = visibleSpanTextViewLinesCollection[i];
                var intersectingCharSnapshotPoint = line.GetBufferPositionFromXCoordinate(x);
 
                // Three main cases for IntersectsNonWhitespaceChar:
                // A) SV intersects a non-whitespace character. In this case we terminate
                //    the current segment and start the next segment at the top of the following
                //    line so that the segment does not intersect with text.
                // B) Current line is the last visible line and is not a non-whitespace character
                //    so we terminate the current segment at the bottom of the last visible line
                //    to ensure that lines with an end-point that is not visible are still drawn.
                // C) Line is not last line and does not have non-whitespace intersecting the SV
                //    so we continue the current segment.
                //
                // Also, if the line would go through an interpolation hole we want to skip it as well.
                if (IntersectsNonWhitespaceChar(intersectingCharSnapshotPoint) || IsInHole(orderedHoleSpans, line))
                {
                    currentSegmentBottom = line.Top;
 
                    // Only add the structure visualizer adornment line segment if it spans at least
                    // a few pixels in height so we don't have artifacts between lines of intersecting
                    // text.
                    if ((currentSegmentBottom - currentSegmentTop) >= MinLineHeight)
                        segments.Add((currentSegmentTop, currentSegmentBottom));
 
                    currentSegmentTop = line.Bottom + ContinuationPadding;
                }
            }
 
            // Due to mapping between versions of the Snapshots, visibleSpanTextViewLinesCollection
            // may include more lines than are actually in the block, so end at yBottom.
            currentSegmentBottom = yBottom;
 
            // Only add the structure visualizer adornment line segment if it spans at least
            // an entire line in height so we don't have 1 to 3 pixel artifacts between lines
            // of intersecting text.
            if ((currentSegmentBottom - currentSegmentTop) >= MinLineHeight)
                segments.Add((currentSegmentTop, currentSegmentBottom));
 
            return segments.ToImmutableAndClear();
        }
 
        private static bool IsInHole(ImmutableArray<SnapshotSpan> orderedHoleSpans, ITextViewLine line)
            => orderedHoleSpans.BinarySearch(
                line.Start.Position,
                (ss, pos) => pos < ss.Start ? 1 : ss.Span.Contains(pos) ? 0 : -1) >= 0;
 
        private static bool IntersectsNonWhitespaceChar(SnapshotPoint? intersectingCharSnapshotPoint)
            => intersectingCharSnapshotPoint != null &&
               !char.IsWhiteSpace(intersectingCharSnapshotPoint.Value.GetChar());
    }
}