File: Shared\Extensions\ITextViewExtensions.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.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Outlining;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions;
 
internal static partial class ITextViewExtensions
{
    /// <summary>
    /// Collects the content types in the view's buffer graph.
    /// </summary>
    public static ISet<IContentType> GetContentTypes(this ITextView textView)
    {
        return new HashSet<IContentType>(
            textView.BufferGraph.GetTextBuffers(_ => true).Select(b => b.ContentType));
    }
 
    public static bool IsReadOnlyOnSurfaceBuffer(this ITextView textView, SnapshotSpan span)
    {
        var spansInView = textView.BufferGraph.MapUpToBuffer(span, SpanTrackingMode.EdgeInclusive, textView.TextBuffer);
        return spansInView.Any(spanInView => textView.TextBuffer.IsReadOnly(spanInView.Span));
    }
 
    public static SnapshotPoint? GetCaretPoint(this ITextView textView, ITextBuffer subjectBuffer)
    {
        var caret = textView.Caret.Position;
        return textView.BufferGraph.MapUpOrDownToBuffer(caret.BufferPosition, subjectBuffer);
    }
 
    public static SnapshotPoint? GetCaretPoint(this ITextView textView, Predicate<ITextSnapshot> match)
    {
        var caret = textView.Caret.Position;
        var span = textView.BufferGraph.MapUpOrDownToFirstMatch(new SnapshotSpan(caret.BufferPosition, 0), match);
        if (span.HasValue)
        {
            return span.Value.Start;
        }
        else
        {
            return null;
        }
    }
 
    public static VirtualSnapshotPoint? GetVirtualCaretPoint(this ITextView textView, ITextBuffer subjectBuffer)
    {
        if (subjectBuffer == textView.TextBuffer)
        {
            return textView.Caret.Position.VirtualBufferPosition;
        }
 
        var mappedPoint = textView.BufferGraph.MapDownToBuffer(
            textView.Caret.Position.VirtualBufferPosition.Position,
            PointTrackingMode.Negative,
            subjectBuffer,
            PositionAffinity.Predecessor);
 
        return mappedPoint.HasValue
            ? new VirtualSnapshotPoint(mappedPoint.Value)
            : default;
    }
 
    public static ITextBuffer? GetBufferContainingCaret(this ITextView textView, string contentType = ContentTypeNames.RoslynContentType)
    {
        var point = GetCaretPoint(textView, s => s.ContentType.IsOfType(contentType));
        return point.HasValue ? point.Value.Snapshot.TextBuffer : null;
    }
 
    public static SnapshotPoint? GetPositionInView(this ITextView textView, SnapshotPoint point)
        => textView.BufferGraph.MapUpToSnapshot(point, PointTrackingMode.Positive, PositionAffinity.Successor, textView.TextSnapshot);
 
    public static NormalizedSnapshotSpanCollection GetSpanInView(this ITextView textView, SnapshotSpan span)
        => textView.BufferGraph.MapUpToSnapshot(span, SpanTrackingMode.EdgeInclusive, textView.TextSnapshot);
 
    public static void SetSelection(
        this ITextView textView, VirtualSnapshotPoint anchorPoint, VirtualSnapshotPoint activePoint)
    {
        var isReversed = activePoint < anchorPoint;
        var start = isReversed ? activePoint : anchorPoint;
        var end = isReversed ? anchorPoint : activePoint;
        SetSelection(textView, new SnapshotSpan(start.Position, end.Position), isReversed);
    }
 
    public static void SetSelection(
        this ITextView textView, SnapshotSpan span, bool isReversed = false)
    {
        var spanInView = textView.GetSpanInView(span).Single();
        textView.Selection.Select(spanInView, isReversed);
        textView.Caret.MoveTo(isReversed ? spanInView.Start : spanInView.End);
    }
 
    /// <summary>
    /// Sets a multi selection with the last span as the primary selection.
    /// Also maps up to the correct span in view before attempting to set the selection.
    /// </summary>
    public static void SetMultiSelection(this ITextView textView, IEnumerable<SnapshotSpan> spans)
    {
        var spansInView = spans.Select(s => new Selection(textView.GetSpanInView(s).Single()));
        textView.GetMultiSelectionBroker().SetSelectionRange(spansInView, spansInView.Last());
    }
 
    public static bool TryMoveCaretToAndEnsureVisible(this ITextView textView, SnapshotPoint point, IOutliningManagerService? outliningManagerService = null, EnsureSpanVisibleOptions ensureSpanVisibleOptions = EnsureSpanVisibleOptions.None)
        => textView.TryMoveCaretToAndEnsureVisible(new VirtualSnapshotPoint(point), outliningManagerService, ensureSpanVisibleOptions);
 
    public static bool TryMoveCaretToAndEnsureVisible(this ITextView textView, VirtualSnapshotPoint point, IOutliningManagerService? outliningManagerService = null, EnsureSpanVisibleOptions ensureSpanVisibleOptions = EnsureSpanVisibleOptions.None)
    {
        if (textView.IsClosed)
        {
            return false;
        }
 
        var pointInView = textView.GetPositionInView(point.Position);
 
        if (!pointInView.HasValue)
        {
            return false;
        }
 
        // If we were given an outlining service, we need to expand any outlines first, or else
        // the Caret.MoveTo won't land in the correct location if our target is inside a
        // collapsed outline.
        if (outliningManagerService != null)
        {
            var outliningManager = outliningManagerService.GetOutliningManager(textView);
 
            outliningManager?.ExpandAll(new SnapshotSpan(pointInView.Value, length: 0), match: _ => true);
        }
 
        var newPosition = textView.Caret.MoveTo(new VirtualSnapshotPoint(pointInView.Value, point.VirtualSpaces));
 
        // We use the caret's position in the view's current snapshot here in case something 
        // changed text in response to a caret move (e.g. line commit)
        var spanInView = new SnapshotSpan(newPosition.BufferPosition, 0);
        textView.ViewScroller.EnsureSpanVisible(spanInView, ensureSpanVisibleOptions);
 
        return true;
    }
 
    /// <summary>
    /// Gets or creates a view property that would go away when view gets closed
    /// </summary>
    public static TProperty GetOrCreateAutoClosingProperty<TProperty, TTextView>(
        this TTextView textView,
        Func<TTextView, TProperty> valueCreator) where TTextView : ITextView
    {
        return textView.GetOrCreateAutoClosingProperty(typeof(TProperty), valueCreator);
    }
 
    /// <summary>
    /// Gets or creates a view property that would go away when view gets closed
    /// </summary>
    public static TProperty GetOrCreateAutoClosingProperty<TProperty, TTextView>(
        this TTextView textView,
        object key,
        Func<TTextView, TProperty> valueCreator) where TTextView : ITextView
    {
        GetOrCreateAutoClosingProperty(textView, key, valueCreator, out var value);
        return value;
    }
 
    /// <summary>
    /// Gets or creates a view property that would go away when view gets closed
    /// </summary>
    public static bool GetOrCreateAutoClosingProperty<TProperty, TTextView>(
        this TTextView textView,
        object key,
        Func<TTextView, TProperty> valueCreator,
        out TProperty value) where TTextView : ITextView
    {
        return AutoClosingViewProperty<TProperty, TTextView>.GetOrCreateValue(textView, key, valueCreator, out value);
    }
 
    /// <summary>
    /// Gets or creates a per subject buffer property.
    /// </summary>
    public static TProperty GetOrCreatePerSubjectBufferProperty<TProperty, TTextView>(
        this TTextView textView,
        ITextBuffer subjectBuffer,
        object key,
        Func<TTextView, ITextBuffer, TProperty> valueCreator) where TTextView : class, ITextView
    {
        GetOrCreatePerSubjectBufferProperty(textView, subjectBuffer, key, valueCreator, out var value);
 
        return value;
    }
 
    /// <summary>
    /// Gets or creates a per subject buffer property, returning true if it needed to create it.
    /// </summary>
    public static bool GetOrCreatePerSubjectBufferProperty<TProperty, TTextView>(
        this TTextView textView,
        ITextBuffer subjectBuffer,
        object key,
        Func<TTextView, ITextBuffer, TProperty> valueCreator,
        out TProperty value) where TTextView : class, ITextView
    {
        Contract.ThrowIfNull(textView);
        Contract.ThrowIfNull(subjectBuffer);
        Contract.ThrowIfNull(valueCreator);
 
        return PerSubjectBufferProperty<TProperty, TTextView>.GetOrCreateValue(textView, subjectBuffer, key, valueCreator, out value);
    }
 
    public static bool TryGetPerSubjectBufferProperty<TProperty, TTextView>(
        this TTextView textView,
        ITextBuffer subjectBuffer,
        object key,
        [MaybeNullWhen(false)] out TProperty value) where TTextView : class, ITextView
    {
        Contract.ThrowIfNull(textView);
        Contract.ThrowIfNull(subjectBuffer);
 
        return PerSubjectBufferProperty<TProperty, TTextView>.TryGetValue(textView, subjectBuffer, key, out value);
    }
 
    public static void AddPerSubjectBufferProperty<TProperty, TTextView>(
        this TTextView textView,
        ITextBuffer subjectBuffer,
        object key,
        TProperty value) where TTextView : class, ITextView
    {
        Contract.ThrowIfNull(textView);
        Contract.ThrowIfNull(subjectBuffer);
 
        PerSubjectBufferProperty<TProperty, TTextView>.AddValue(textView, subjectBuffer, key, value);
    }
 
    public static void RemovePerSubjectBufferProperty<TProperty, TTextView>(
        this TTextView textView,
        ITextBuffer subjectBuffer,
        object key) where TTextView : class, ITextView
    {
        Contract.ThrowIfNull(textView);
        Contract.ThrowIfNull(subjectBuffer);
 
        PerSubjectBufferProperty<TProperty, TTextView>.RemoveValue(textView, subjectBuffer, key);
    }
 
    public static bool TypeCharWasHandledStrangely(
        this ITextView textView,
        ITextBuffer subjectBuffer,
        char ch)
    {
        var finalCaretPositionOpt = textView.GetCaretPoint(subjectBuffer);
        if (finalCaretPositionOpt == null)
        {
            // Caret moved outside of our buffer.  Don't want to handle this typed character.
            return true;
        }
 
        var previousPosition = finalCaretPositionOpt.Value.Position - 1;
        var inRange = previousPosition >= 0 && previousPosition < subjectBuffer.CurrentSnapshot.Length;
        if (!inRange)
        {
            // The character before the caret isn't even in the buffer we care about.  Don't
            // handle this.
            return true;
        }
 
        if (subjectBuffer.CurrentSnapshot[previousPosition] != ch)
        {
            // The character that was typed is not in the buffer at the typed location.  Don't
            // handle this character.
            return true;
        }
 
        return false;
    }
 
    public static int? GetDesiredIndentation(this ITextView textView, ISmartIndentationService smartIndentService, ITextSnapshotLine line)
    {
        var pointInView = textView.BufferGraph.MapUpToSnapshot(
            line.Start, PointTrackingMode.Positive, PositionAffinity.Successor, textView.TextSnapshot);
 
        if (!pointInView.HasValue)
        {
            return null;
        }
 
        var lineInView = textView.TextSnapshot.GetLineFromPosition(pointInView.Value.Position);
        return smartIndentService.GetDesiredIndentation(textView, lineInView);
    }
 
    public static bool TryGetSurfaceBufferSpan(
        this ITextView textView,
        VirtualSnapshotSpan virtualSnapshotSpan,
        out VirtualSnapshotSpan surfaceBufferSpan)
    {
        // If we are already on the surface buffer, then there's no reason to attempt mappings
        // as we'll lose virtualness
        if (virtualSnapshotSpan.Snapshot.TextBuffer == textView.TextBuffer)
        {
            surfaceBufferSpan = virtualSnapshotSpan;
            return true;
        }
 
        // We have to map. We'll lose virtualness in this process because
        // mapping virtual points through projections is poorly defined.
        var targetSpan = textView.BufferGraph.MapUpToSnapshot(
            virtualSnapshotSpan.SnapshotSpan,
            SpanTrackingMode.EdgeExclusive,
            textView.TextSnapshot).FirstOrNull();
 
        if (targetSpan.HasValue)
        {
            surfaceBufferSpan = new VirtualSnapshotSpan(targetSpan.Value);
            return true;
        }
 
        surfaceBufferSpan = default;
        return false;
    }
 
    /// <summary>
    /// Returns the span of the lines in subjectBuffer that is currently visible in the provided
    /// view.  "extraLines" can be provided to get a span that encompasses some number of lines
    /// before and after the actual visible lines.
    /// </summary>
    public static SnapshotSpan? GetVisibleLinesSpan(this ITextView textView, ITextBuffer subjectBuffer, int extraLines = 0)
    {
        // No point in continuing if the text view has been closed.
        if (textView.IsClosed)
        {
            return null;
        }
 
        // If we're being called while the textview is actually in the middle of a layout, then 
        // we can't proceed.  Much of the text view state is unsafe to access (and will throw).
        if (textView.InLayout)
        {
            return null;
        }
 
        // During text view initialization the TextViewLines may be null.  In that case we can't
        // get an appropriate visisble span.
        if (textView.TextViewLines == null)
        {
            return null;
        }
 
        // Determine the range of text that is visible in the view.  Then map this down to the
        // bufffer passed in.  From that, determine the start/end line for the buffer that is in
        // view.
        var visibleSpan = textView.TextViewLines.FormattedSpan;
        var visibleSpansInBuffer = textView.BufferGraph.MapDownToBuffer(visibleSpan, SpanTrackingMode.EdgeInclusive, subjectBuffer);
        if (visibleSpansInBuffer.Count == 0)
        {
            return null;
        }
 
        var visibleStart = visibleSpansInBuffer.First().Start;
        var visibleEnd = visibleSpansInBuffer.Last().End;
 
        var snapshot = subjectBuffer.CurrentSnapshot;
        var startLine = visibleStart.GetContainingLineNumber();
        var endLine = visibleEnd.GetContainingLineNumber();
 
        startLine = Math.Max(startLine - extraLines, 0);
        endLine = Math.Min(endLine + extraLines, snapshot.LineCount - 1);
 
        var start = snapshot.GetLineFromLineNumber(startLine).Start;
        var end = snapshot.GetLineFromLineNumber(endLine).EndIncludingLineBreak;
 
        var span = new SnapshotSpan(snapshot, Span.FromBounds(start, end));
 
        return span;
    }
 
    /// <summary>
    /// Determines if the textbuffer passed in matches the buffer for the textview.
    /// </summary>
    public static bool IsNotSurfaceBufferOfTextView(this ITextView textView, ITextBuffer textBuffer)
        => textBuffer != textView.TextBuffer;
 
    internal static bool IsInLspEditorContext(this ITextView textView)
    {
        // If any of the buffers in the projection graph are in the LSP editor context, then we consider this to be in an LSP context.
        // We cannot be in a partial context where some buffers are LSP and some are not.
        var anyBufferInLspContext = false;
        _ = textView.BufferGraph.GetTextBuffers(textBuffer =>
        {
            // Just set a flag if we found one to avoid creating a collection of all the buffers
            if (textBuffer.IsInLspEditorContext())
            {
                anyBufferInLspContext = true;
            }
 
            return false;
        });
 
        return anyBufferInLspContext;
    }
}