File: AutomaticCompletion\AbstractAutomaticLineEnderCommandHandler.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.Threading;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Commanding;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Editor.Commanding.Commands;
using Microsoft.VisualStudio.Text.Operations;
 
namespace Microsoft.CodeAnalysis.AutomaticCompletion;
 
internal abstract class AbstractAutomaticLineEnderCommandHandler :
    IChainedCommandHandler<AutomaticLineEnderCommandArgs>
{
    private readonly ITextUndoHistoryRegistry _undoRegistry;
    private readonly IEditorOperationsFactoryService _editorOperationsFactoryService;
 
    public readonly EditorOptionsService EditorOptionsService;
 
    public string DisplayName => EditorFeaturesResources.Automatic_Line_Ender;
 
    protected AbstractAutomaticLineEnderCommandHandler(
        ITextUndoHistoryRegistry undoRegistry,
        IEditorOperationsFactoryService editorOperationsFactoryService,
        EditorOptionsService editorOptionsService)
    {
        _undoRegistry = undoRegistry;
        _editorOperationsFactoryService = editorOperationsFactoryService;
        EditorOptionsService = editorOptionsService;
    }
 
    /// <summary>
    /// get ending string if there is one
    /// </summary>
    protected abstract string? GetEndingString(ParsedDocument document, int position);
 
    /// <summary>
    /// do next action
    /// </summary>
    protected abstract void NextAction(IEditorOperations editorOperation, Action nextAction);
 
    /// <summary>
    /// format after inserting ending string
    /// </summary>
    protected abstract IList<TextChange> FormatBasedOnEndToken(ParsedDocument document, int position, SyntaxFormattingOptions formattingOptions, CancellationToken cancellationToken);
 
    /// <summary>
    /// special cases where we do not want to do line completion but just fall back to line break and formatting.
    /// </summary>
    protected abstract bool TreatAsReturn(ParsedDocument document, int caretPosition, CancellationToken cancellationToken);
 
    /// <summary>
    /// Add or remove the braces for <param name="selectedNode"/>.
    /// </summary>
    protected abstract void ModifySelectedNode(AutomaticLineEnderCommandArgs args, ParsedDocument document, SyntaxNode selectedNode, bool addBrace, int caretPosition, StructuredAnalyzerConfigOptions fallbackOptions, CancellationToken cancellationToken);
 
    /// <summary>
    /// Get the syntax node needs add/remove braces.
    /// </summary>
    protected abstract (SyntaxNode selectedNode, bool addBrace)? GetValidNodeToModifyBraces(ParsedDocument document, int caretPosition, CancellationToken cancellationToken);
 
    public CommandState GetCommandState(AutomaticLineEnderCommandArgs args, Func<CommandState> nextHandler)
        => CommandState.Available;
 
    public void ExecuteCommand(AutomaticLineEnderCommandArgs args, Action nextHandler, CommandExecutionContext context)
    {
        // get editor operation
        var operations = _editorOperationsFactoryService.GetEditorOperations(args.TextView);
        if (operations == null)
        {
            nextHandler();
            return;
        }
 
        var document = args.SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
        if (document == null)
        {
            NextAction(operations, nextHandler);
            return;
        }
 
        // feature off
        if (!EditorOptionsService.GlobalOptions.GetOption(AutomaticLineEnderOptionsStorage.AutomaticLineEnder))
        {
            NextAction(operations, nextHandler);
            return;
        }
 
        using (context.OperationContext.AddScope(allowCancellation: true, EditorFeaturesResources.Automatically_completing))
        {
            var cancellationToken = context.OperationContext.UserCancellationToken;
            var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken);
 
            // caret is not on the subject buffer. nothing we can do
            var caret = args.TextView.GetCaretPoint(args.SubjectBuffer);
            if (!caret.HasValue)
            {
                NextAction(operations, nextHandler);
                return;
            }
 
            var caretPosition = caret.Value;
            // special cases where we treat this command simply as Return.
            if (TreatAsReturn(parsedDocument, caretPosition, cancellationToken))
            {
                // leave it to the VS editor to handle this command.
                // VS editor's default implementation of SmartBreakLine is simply BreakLine, which inserts
                // a new line and positions the caret with smart indent.
                nextHandler();
                return;
            }
 
            var subjectLineWhereCaretIsOn = caretPosition.GetContainingLine();
 
            // Two possible operations
            // 1. Add/remove the brace for the selected syntax node (only for C#)
            // 2. Append an ending string to the line. (For C#, it is semicolon ';', For VB, it is underline '_')
 
            // Check if the node could be used to add/remove brace.
            var selectNodeAndOperationKind = GetValidNodeToModifyBraces(parsedDocument, caretPosition, cancellationToken);
            if (selectNodeAndOperationKind != null)
            {
                var (selectedNode, addBrace) = selectNodeAndOperationKind.Value;
                using var transaction = args.TextView.CreateEditTransaction(EditorFeaturesResources.Automatic_Line_Ender, _undoRegistry, _editorOperationsFactoryService);
                ModifySelectedNode(args, parsedDocument, selectedNode, addBrace, caretPosition, document.Project.GetFallbackAnalyzerOptions(), cancellationToken);
                NextAction(operations, nextHandler);
                transaction.Complete();
                return;
            }
 
            // Check if we could find the ending position
            var endingInsertionPosition = GetInsertionPositionForEndingString(parsedDocument, subjectLineWhereCaretIsOn);
            if (endingInsertionPosition != null)
            {
                using var transaction = args.TextView.CreateEditTransaction(EditorFeaturesResources.Automatic_Line_Ender, _undoRegistry, _editorOperationsFactoryService);
                var formattingOptions = args.SubjectBuffer.GetSyntaxFormattingOptions(EditorOptionsService, document.Project.GetFallbackAnalyzerOptions(), parsedDocument.LanguageServices, explicitFormat: false);
                InsertEnding(args.TextView, args.SubjectBuffer, parsedDocument, endingInsertionPosition.Value, caretPosition, formattingOptions, cancellationToken);
                NextAction(operations, nextHandler);
                transaction.Complete();
                return;
            }
 
            // Neither of the two operations could be performed
            using var editTransaction = args.TextView.CreateEditTransaction(EditorFeaturesResources.Automatic_Line_Ender, _undoRegistry, _editorOperationsFactoryService);
            NextAction(operations, nextHandler);
            editTransaction.Complete();
        }
    }
 
    /// <summary>
    /// return insertion point for the ending string
    /// </summary>
    private static int? GetInsertionPositionForEndingString(ParsedDocument document, ITextSnapshotLine line)
    {
        // find last token on the line
        var token = document.Root.FindTokenOnLeftOfPosition(line.End);
        if (token.RawKind == 0)
            return null;
 
        // bug # 16770
        // don't do anything if token is multiline token such as verbatim string
        if (line.End < token.Span.End)
            return null;
 
        // if there is only whitespace, token doesn't need to be on same line
        var text = document.Text;
        if (string.IsNullOrWhiteSpace(text.ToString(TextSpan.FromBounds(token.Span.End, line.End))))
            return line.End;
 
        // if token is on different line than caret but caret line is empty, we insert ending point at the end of the line
        if (text.Lines.IndexOf(token.Span.End) != text.Lines.IndexOf(line.End))
            return string.IsNullOrWhiteSpace(line.GetText()) ? line.End : null;
 
        return token.Span.End;
    }
 
    /// <summary>
    /// insert ending string if there is one to insert
    /// </summary>
    private void InsertEnding(
        ITextView textView,
        ITextBuffer buffer,
        ParsedDocument document,
        int insertPosition,
        SnapshotPoint caretPosition,
        SyntaxFormattingOptions formattingOptions,
        CancellationToken cancellationToken)
    {
        // 1. Move the caret to line end.
        textView.TryMoveCaretToAndEnsureVisible(caretPosition.GetContainingLine().End);
 
        // 2. Insert ending to the document.
        var newDocument = document;
        var endingString = GetEndingString(document, caretPosition);
        if (endingString != null)
        {
            var insertChange = new TextChange(new TextSpan(insertPosition, 0), endingString);
            buffer.ApplyChange(insertChange);
            newDocument = document.WithChange(insertChange, cancellationToken);
        }
 
        // 3. format the document and apply the changes to the workspace
        var changes = FormatBasedOnEndToken(newDocument, insertPosition, formattingOptions, cancellationToken);
        buffer.ApplyChanges(changes);
    }
}