|
// 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.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.BraceCompletion;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.BraceCompletion;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Operations;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.AutomaticCompletion;
internal partial class BraceCompletionSessionProvider
{
// ported and modified from "Platform\Text\Impl\BraceCompletion\BraceCompletionDefaultSession.cs"
//
// we want to provide better context based brace completion but IBraceCompletionContext is too simple for that.
// fortunately, editor provides another extension point where we have more control over brace completion but we do not
// want to re-implement logics base session provider already provides. so I ported editor's default session and
// modified it little bit so that we can use it as base class.
private class BraceCompletionSession(
ITextView textView,
ITextBuffer subjectBuffer,
SnapshotPoint openingPoint,
char openingBrace,
char closingBrace,
ITextUndoHistory undoHistory,
IEditorOperationsFactoryService editorOperationsFactoryService,
EditorOptionsService editorOptionsService,
IBraceCompletionService service,
IThreadingContext threadingContext) : IBraceCompletionSession
{
private readonly ITextUndoHistory _undoHistory = undoHistory;
private readonly IEditorOperations _editorOperations = editorOperationsFactoryService.GetEditorOperations(textView);
private readonly EditorOptionsService _editorOptionsService = editorOptionsService;
private readonly IBraceCompletionService _service = service;
private readonly IThreadingContext _threadingContext = threadingContext;
public char OpeningBrace { get; } = openingBrace;
public char ClosingBrace { get; } = closingBrace;
public ITrackingPoint OpeningPoint { get; private set; }
public ITrackingPoint ClosingPoint { get; private set; } = subjectBuffer.CurrentSnapshot.CreateTrackingPoint(openingPoint.Position, PointTrackingMode.Positive);
public ITextBuffer SubjectBuffer { get; } = subjectBuffer;
public ITextView TextView { get; } = textView;
#region IBraceCompletionSession Methods
public void Start()
{
_threadingContext.ThrowIfNotOnUIThread();
// Brace completion is not cancellable.
var success = _threadingContext.JoinableTaskFactory.Run(() => TryStartAsync(CancellationToken.None));
if (!success)
EndSession();
}
private async Task<bool> TryStartAsync(CancellationToken cancellationToken)
{
_threadingContext.ThrowIfNotOnUIThread();
var closingSnapshotPoint = ClosingPoint.GetPoint(SubjectBuffer.CurrentSnapshot);
if (closingSnapshotPoint.Position < 1)
{
Debug.Fail("The closing point was not found at the expected position.");
return false;
}
var openingSnapshotPoint = closingSnapshotPoint.Subtract(1);
if (openingSnapshotPoint.GetChar() != OpeningBrace)
{
// there is a bug in editor brace completion engine on projection buffer that already fixed in vs_pro. until that is FIed to use
// I will make this not to assert
// Debug.Fail("The opening brace was not found at the expected position.");
return false;
}
OpeningPoint = SubjectBuffer.CurrentSnapshot.CreateTrackingPoint(openingSnapshotPoint, PointTrackingMode.Positive);
var document = SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
if (document == null)
return false;
var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken);
var context = GetBraceCompletionContext(parsedDocument, document.Project.GetFallbackAnalyzerOptions());
// Note: completes synchronously unless Semantic Model is needed to determine the result:
var hasBraceCompletions = await _service.HasBraceCompletionAsync(context, document, cancellationToken).ConfigureAwait(true);
if (!hasBraceCompletions)
return false;
var braceResult = _service.GetBraceCompletion(context);
using var caretPreservingTransaction = new CaretPreservingEditTransaction(EditorFeaturesResources.Brace_Completion, _undoHistory, _editorOperations);
// Apply the change to complete the brace.
ApplyBraceCompletionResult(braceResult);
// switch the closing point from positive to negative tracking so that the closing point stays against the closing brace
ClosingPoint = SubjectBuffer.CurrentSnapshot.CreateTrackingPoint(ClosingPoint.GetPoint(SubjectBuffer.CurrentSnapshot), PointTrackingMode.Negative);
if (TryGetBraceCompletionContext(out var contextAfterStart, cancellationToken))
{
var indentationOptions = SubjectBuffer.GetIndentationOptions(_editorOptionsService, document.Project.GetFallbackAnalyzerOptions(), contextAfterStart.Document.LanguageServices, explicitFormat: false);
var changesAfterStart = _service.GetTextChangesAfterCompletion(contextAfterStart, indentationOptions, cancellationToken);
if (changesAfterStart != null)
{
ApplyBraceCompletionResult(changesAfterStart.Value);
}
}
caretPreservingTransaction.Complete();
return true;
}
public void PreBackspace(out bool handledCommand)
{
_threadingContext.ThrowIfNotOnUIThread();
handledCommand = false;
var caretPos = this.GetCaretPosition();
var snapshot = SubjectBuffer.CurrentSnapshot;
if (caretPos.HasValue && caretPos.Value.Position > 0 && (caretPos.Value.Position - 1) == OpeningPoint.GetPoint(snapshot).Position
&& !HasForwardTyping)
{
using var undo = CreateUndoTransaction();
using var edit = SubjectBuffer.CreateEdit();
var span = new SnapshotSpan(OpeningPoint.GetPoint(snapshot), ClosingPoint.GetPoint(snapshot));
edit.Delete(span);
if (edit.HasFailedChanges)
{
edit.Cancel();
undo.Cancel();
Debug.Fail("Unable to clear braces");
}
else
{
// handle the command so the backspace does
// not go through since we've already cleared the braces
handledCommand = true;
edit.ApplyAndLogExceptions();
undo.Complete();
EndSession();
}
}
}
public void PostBackspace()
{
}
public void PreOverType(out bool handledCommand)
{
_threadingContext.ThrowIfNotOnUIThread();
handledCommand = false;
if (ClosingPoint == null)
{
return;
}
// Brace completion is not cancellable.
var cancellationToken = CancellationToken.None;
var snapshot = this.SubjectBuffer.CurrentSnapshot;
var closingSnapshotPoint = ClosingPoint.GetPoint(snapshot);
if (HasForwardTyping)
{
return;
}
if (!TryGetBraceCompletionContext(out var context, cancellationToken) ||
!_service.AllowOverType(context, cancellationToken))
{
return;
}
var caretPos = this.GetCaretPosition();
Debug.Assert(caretPos.HasValue && caretPos.Value.Position < closingSnapshotPoint.Position);
// ensure that we are within the session before clearing
if (caretPos.HasValue && caretPos.Value.Position < closingSnapshotPoint.Position && closingSnapshotPoint.Position > 0)
{
using var undo = CreateUndoTransaction();
_editorOperations.AddBeforeTextBufferChangePrimitive();
var span = new SnapshotSpan(caretPos.Value, closingSnapshotPoint.Subtract(1));
using var edit = SubjectBuffer.CreateEdit();
edit.Delete(span);
if (edit.HasFailedChanges)
{
Debug.Fail("Unable to clear closing brace");
edit.Cancel();
undo.Cancel();
}
else
{
handledCommand = true;
edit.ApplyAndLogExceptions();
MoveCaretToClosingPoint();
_editorOperations.AddAfterTextBufferChangePrimitive();
undo.Complete();
}
}
}
public void PostOverType()
{
}
public void PreTab(out bool handledCommand)
{
_threadingContext.ThrowIfNotOnUIThread();
handledCommand = false;
if (!HasForwardTyping)
{
handledCommand = true;
using var undo = CreateUndoTransaction();
_editorOperations.AddBeforeTextBufferChangePrimitive();
MoveCaretToClosingPoint();
_editorOperations.AddAfterTextBufferChangePrimitive();
undo.Complete();
}
}
public void PreReturn(out bool handledCommand)
=> handledCommand = false;
public void PostReturn()
{
_threadingContext.ThrowIfNotOnUIThread();
if (this.GetCaretPosition().HasValue)
{
var closingSnapshotPoint = ClosingPoint.GetPoint(SubjectBuffer.CurrentSnapshot);
if (closingSnapshotPoint.Position > 0 && HasNoForwardTyping(this.GetCaretPosition().Value, closingSnapshotPoint.Subtract(1)))
{
if (!TryGetBraceCompletionContext(out var context, CancellationToken.None))
{
return;
}
var indentationOptions = SubjectBuffer.GetIndentationOptions(_editorOptionsService, context.FallbackOptions, context.Document.LanguageServices, explicitFormat: false);
var changesAfterReturn = _service.GetTextChangeAfterReturn(context, indentationOptions, CancellationToken.None);
if (changesAfterReturn != null)
{
using var caretPreservingTransaction = new CaretPreservingEditTransaction(EditorFeaturesResources.Brace_Completion, _undoHistory, _editorOperations);
ApplyBraceCompletionResult(changesAfterReturn.Value);
caretPreservingTransaction.Complete();
}
}
}
}
public void Finish()
{
}
#endregion
#region Unused IBraceCompletionSession Methods
public void PostTab() { }
public void PreDelete(out bool handledCommand)
=> handledCommand = false;
public void PostDelete() { }
#endregion
#region Private Helpers
private void EndSession()
{
// set the points to null to get off the stack
// the stack will determine that the current point
// is not contained within the session if either are null
OpeningPoint = null;
ClosingPoint = null;
}
// check if there any typing between the caret the closing point
private bool HasForwardTyping
{
get
{
_threadingContext.ThrowIfNotOnUIThread();
var closingSnapshotPoint = ClosingPoint.GetPoint(SubjectBuffer.CurrentSnapshot);
if (closingSnapshotPoint.Position > 0)
{
var caretPos = this.GetCaretPosition();
if (caretPos.HasValue && !HasNoForwardTyping(caretPos.Value, closingSnapshotPoint.Subtract(1)))
{
return true;
}
}
return false;
}
}
// verify that there is only whitespace between the two given points
private static bool HasNoForwardTyping(SnapshotPoint caretPoint, SnapshotPoint endPoint)
{
Debug.Assert(caretPoint.Snapshot == endPoint.Snapshot, "snapshots do not match");
if (caretPoint.Snapshot == endPoint.Snapshot)
{
if (caretPoint == endPoint)
{
return true;
}
if (caretPoint.Position < endPoint.Position)
{
var span = new SnapshotSpan(caretPoint, endPoint);
return string.IsNullOrWhiteSpace(span.GetText());
}
}
return false;
}
internal ITextUndoTransaction CreateUndoTransaction()
=> _undoHistory.CreateTransaction(EditorFeaturesResources.Brace_Completion);
private void MoveCaretToClosingPoint()
{
_threadingContext.ThrowIfNotOnUIThread();
var closingSnapshotPoint = ClosingPoint.GetPoint(SubjectBuffer.CurrentSnapshot);
// find the position just after the closing brace in the view's text buffer
var afterBrace = TextView.BufferGraph.MapUpToBuffer(closingSnapshotPoint,
PointTrackingMode.Negative, PositionAffinity.Predecessor, TextView.TextBuffer);
Debug.Assert(afterBrace.HasValue, "Unable to move caret to closing point");
if (afterBrace.HasValue)
{
TextView.Caret.MoveTo(afterBrace.Value);
}
}
private bool TryGetBraceCompletionContext(out BraceCompletionContext context, CancellationToken cancellationToken)
{
var document = SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
if (document == null)
{
context = default;
return false;
}
context = GetBraceCompletionContext(ParsedDocument.CreateSynchronously(document, cancellationToken), document.Project.GetFallbackAnalyzerOptions());
return true;
}
private BraceCompletionContext GetBraceCompletionContext(ParsedDocument document, StructuredAnalyzerConfigOptions fallbackOptions)
{
_threadingContext.ThrowIfNotOnUIThread();
var snapshot = SubjectBuffer.CurrentSnapshot;
var closingSnapshotPoint = ClosingPoint.GetPosition(snapshot);
var openingSnapshotPoint = OpeningPoint.GetPosition(snapshot);
// The user is actively typing so the caret position should not be null.
var caretPosition = this.GetCaretPosition().Value.Position;
return new BraceCompletionContext(document, fallbackOptions, openingSnapshotPoint, closingSnapshotPoint, caretPosition);
}
private void ApplyBraceCompletionResult(BraceCompletionResult result)
{
_threadingContext.ThrowIfNotOnUIThread();
using var edit = SubjectBuffer.CreateEdit();
foreach (var change in result.TextChanges)
{
edit.Replace(change.Span.ToSpan(), change.NewText);
}
edit.ApplyAndLogExceptions();
try
{
Contract.ThrowIfFalse(SubjectBuffer.CurrentSnapshot[OpeningPoint.GetPosition(SubjectBuffer.CurrentSnapshot)] == OpeningBrace,
"The opening point does not match the opening brace character");
Contract.ThrowIfFalse(SubjectBuffer.CurrentSnapshot[ClosingPoint.GetPosition(SubjectBuffer.CurrentSnapshot) - 1] == ClosingBrace,
"The closing point does not match the closing brace character");
}
catch (Exception e) when (FatalError.ReportAndCatch(e))
{
return;
}
var caretLine = SubjectBuffer.CurrentSnapshot.GetLineFromLineNumber(result.CaretLocation.Line);
TextView.TryMoveCaretToAndEnsureVisible(new VirtualSnapshotPoint(caretLine, result.CaretLocation.Character));
}
#endregion
}
}
|