File: CommentSelection\AbstractCommentSelectionBase.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.
 
#nullable disable
 
using System;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
using Microsoft.VisualStudio.Commanding;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Operations;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CommentSelection;
 
internal enum Operation
{
    /// <summary>
    /// The operation is a comment action.
    /// </summary>
    Comment,
 
    /// <summary>
    /// The operation is an uncomment action.
    /// </summary>
    Uncomment
}
 
internal abstract class AbstractCommentSelectionBase<TCommand>
{
    protected const string LanguageNameString = "languagename";
    protected const string LengthString = "length";
 
    private readonly ITextUndoHistoryRegistry _undoHistoryRegistry;
    private readonly IEditorOperationsFactoryService _editorOperationsFactoryService;
    private readonly EditorOptionsService _editorOptionsService;
 
    internal AbstractCommentSelectionBase(
        ITextUndoHistoryRegistry undoHistoryRegistry,
        IEditorOperationsFactoryService editorOperationsFactoryService,
        EditorOptionsService editorOptionsService)
    {
        Contract.ThrowIfNull(undoHistoryRegistry);
        Contract.ThrowIfNull(editorOperationsFactoryService);
 
        _undoHistoryRegistry = undoHistoryRegistry;
        _editorOperationsFactoryService = editorOperationsFactoryService;
        _editorOptionsService = editorOptionsService;
    }
 
    public abstract string DisplayName { get; }
 
    protected abstract string GetTitle(TCommand command);
 
    protected abstract string GetMessage(TCommand command);
 
    // Internal as tests currently rely on this method.
    internal abstract CommentSelectionResult CollectEdits(
        Document document, ICommentSelectionService service, ITextBuffer textBuffer, NormalizedSnapshotSpanCollection selectedSpans,
        TCommand command, CancellationToken cancellationToken);
 
    protected static CommandState GetCommandState(ITextBuffer buffer)
    {
        return buffer.CanApplyChangeDocumentToWorkspace()
            ? CommandState.Available
            : CommandState.Unspecified;
    }
 
    protected static void InsertText(ArrayBuilder<TextChange> textChanges, int position, string text)
        => textChanges.Add(new TextChange(new TextSpan(position, 0), text));
 
    protected static void DeleteText(ArrayBuilder<TextChange> textChanges, TextSpan span)
        => textChanges.Add(new TextChange(span, string.Empty));
 
    internal bool ExecuteCommand(ITextView textView, ITextBuffer subjectBuffer, TCommand command, CommandExecutionContext context)
    {
        var title = GetTitle(command);
        var message = GetMessage(command);
 
        using (context.OperationContext.AddScope(allowCancellation: true, message))
        {
            var cancellationToken = context.OperationContext.UserCancellationToken;
 
            var selectedSpans = textView.Selection.GetSnapshotSpansOnBuffer(subjectBuffer);
            if (selectedSpans.IsEmpty())
            {
                return true;
            }
 
            var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
            if (document == null)
            {
                return true;
            }
 
            var service = document.GetLanguageService<ICommentSelectionService>();
            if (service == null)
            {
                return true;
            }
 
            var edits = CollectEdits(document, service, subjectBuffer, selectedSpans, command, cancellationToken);
            ApplyEdits(document, textView, subjectBuffer, title, edits, cancellationToken);
        }
 
        return true;
    }
 
    /// <summary>
    /// Applies the requested edits and sets the selection.
    /// This operation is not cancellable.
    /// </summary>
    private void ApplyEdits(Document document, ITextView textView, ITextBuffer subjectBuffer, string title, CommentSelectionResult edits, CancellationToken cancellationToken)
    {
        var originalSnapshot = subjectBuffer.CurrentSnapshot;
 
        // Apply the text changes.
        using (var transaction = new CaretPreservingEditTransaction(title, textView, _undoHistoryRegistry, _editorOperationsFactoryService))
        {
            subjectBuffer.ApplyChanges(edits.TextChanges);
            transaction.Complete();
        }
 
        if (edits.TrackingSpans.Any())
        {
            // Create tracking spans to track the text changes.
            var trackingSpans = edits.TrackingSpans
                .SelectAsArray(textSpan => (originalSpan: textSpan, trackingSpan: CreateTrackingSpan(edits.ResultOperation, originalSnapshot, textSpan.TrackingTextSpan)));
 
            // Convert the tracking spans into snapshot spans for formatting and selection.
            var trackingSnapshotSpans = trackingSpans.Select(s => CreateSnapshotSpan(subjectBuffer.CurrentSnapshot, s.trackingSpan, s.originalSpan));
 
            if (edits.ResultOperation == Operation.Uncomment && document.SupportsSyntaxTree)
            {
                // Format the document only during uncomment operations.  Use second transaction so it can be undone.
                using var transaction = new CaretPreservingEditTransaction(title, textView, _undoHistoryRegistry, _editorOperationsFactoryService);
 
                var newText = subjectBuffer.CurrentSnapshot.AsText();
                var oldSyntaxTree = document.GetSyntaxTreeSynchronously(cancellationToken);
                var newRoot = oldSyntaxTree.WithChangedText(newText).GetRoot(cancellationToken);
 
                var formattingOptions = subjectBuffer.GetSyntaxFormattingOptions(_editorOptionsService, document.Project.GetFallbackAnalyzerOptions(), document.Project.Services, explicitFormat: false);
                var formattingSpans = trackingSnapshotSpans.Select(change => CommonFormattingHelpers.GetFormattingSpan(newRoot, change.Span.ToTextSpan()));
                var formattedChanges = Formatter.GetFormattedTextChanges(newRoot, formattingSpans, document.Project.Solution.Services, formattingOptions, rules: default, cancellationToken);
 
                subjectBuffer.ApplyChanges(formattedChanges);
 
                transaction.Complete();
            }
 
            // Set the multi selection after edits have been applied.
            textView.SetMultiSelection(trackingSnapshotSpans);
        }
    }
 
    /// <summary>
    /// Creates a tracking span for the operation.
    /// Internal for tests.
    /// </summary>
    internal static ITrackingSpan CreateTrackingSpan(Operation operation, ITextSnapshot snapshot, TextSpan textSpan)
    {
        // If a comment is being added, the tracking span must include changes at the edge.
        var spanTrackingMode = operation == Operation.Comment
            ? SpanTrackingMode.EdgeInclusive
            : SpanTrackingMode.EdgeExclusive;
        return snapshot.CreateTrackingSpan(Span.FromBounds(textSpan.Start, textSpan.End), spanTrackingMode);
    }
 
    /// <summary>
    /// Retrieves the snapshot span from a post edited tracking span.
    /// Additionally applies any extra modifications to the tracking span post edit.
    /// </summary>
    private static SnapshotSpan CreateSnapshotSpan(ITextSnapshot snapshot, ITrackingSpan trackingSpan, CommentTrackingSpan originalSpan)
    {
        var snapshotSpan = trackingSpan.GetSpan(snapshot);
        if (originalSpan.HasPostApplyChanges())
        {
            var updatedStart = snapshotSpan.Start.Position + originalSpan.AmountToAddToTrackingSpanStart;
            var updatedEnd = snapshotSpan.End.Position + originalSpan.AmountToAddToTrackingSpanEnd;
            if (updatedStart >= snapshotSpan.Start.Position && updatedEnd <= snapshotSpan.End.Position)
            {
                snapshotSpan = new SnapshotSpan(snapshot, Span.FromBounds(updatedStart, updatedEnd));
            }
        }
 
        return snapshotSpan;
    }
 
    /// <summary>
    /// Given a set of lines, find the minimum indent of all of the non-blank, non-whitespace lines.
    /// </summary>
    protected static int DetermineSmallestIndent(
        SnapshotSpan span, ITextSnapshotLine firstLine, ITextSnapshotLine lastLine)
    {
        // TODO: This breaks if you have mixed tabs/spaces, and/or tabsize != indentsize.
        var indentToCommentAt = int.MaxValue;
        for (var lineNumber = firstLine.LineNumber; lineNumber <= lastLine.LineNumber; ++lineNumber)
        {
            var line = span.Snapshot.GetLineFromLineNumber(lineNumber);
            var firstNonWhitespacePosition = line.GetFirstNonWhitespacePosition();
            var firstNonWhitespaceOnLine = firstNonWhitespacePosition.HasValue
                ? firstNonWhitespacePosition.Value - line.Start
                : int.MaxValue;
            indentToCommentAt = Math.Min(indentToCommentAt, firstNonWhitespaceOnLine);
        }
 
        return indentToCommentAt;
    }
}