|
// 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);
}
}
|