Skip to content

Commit

Permalink
Hook brace-matching up to the 'responsive completion' editor option (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
CyrusNajmabadi authored Jan 14, 2025
2 parents f0b28f4 + a3e40fe commit a494430
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using System;
using System.Collections.Generic;
using System.Composition;
using System.Linq;
using Microsoft.CodeAnalysis.AutomaticCompletion;
using Microsoft.CodeAnalysis.BraceCompletion;
using Microsoft.CodeAnalysis.Host.Mef;
Expand All @@ -15,7 +14,6 @@ namespace Microsoft.CodeAnalysis.Editor.CSharp.AutomaticCompletion;
[ExportLanguageService(typeof(IBraceCompletionServiceFactory), LanguageNames.CSharp), Shared]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal class CSharpBraceCompletionServiceFactory(
[ImportMany] IEnumerable<Lazy<IBraceCompletionService, LanguageMetadata>> braceCompletionServices) : AbstractBraceCompletionServiceFactory(braceCompletionServices, LanguageNames.CSharp)
{
}
internal sealed class CSharpBraceCompletionServiceFactory(
[ImportMany] IEnumerable<Lazy<IBraceCompletionService, LanguageMetadata>> braceCompletionServices)
: AbstractBraceCompletionServiceFactory(braceCompletionServices, LanguageNames.CSharp);
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,25 @@ internal partial class BraceCompletionSessionProvider
// 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(
BraceCompletionSessionProvider provider,
ITextView textView,
ITextBuffer subjectBuffer,
SnapshotPoint openingPoint,
char openingBrace,
char closingBrace,
ITextUndoHistory undoHistory,
IEditorOperationsFactoryService editorOperationsFactoryService,
EditorOptionsService editorOptionsService,
IBraceCompletionService service,
IThreadingContext threadingContext) : IBraceCompletionSession
bool responsiveCompletion) : IBraceCompletionSession
{
private readonly ITextUndoHistory _undoHistory = undoHistory;
private readonly IEditorOperations _editorOperations = editorOperationsFactoryService.GetEditorOperations(textView);
private readonly EditorOptionsService _editorOptionsService = editorOptionsService;
private readonly BraceCompletionSessionProvider _provider = provider;

private readonly IEditorOperations _editorOperations = provider._editorOperationsFactoryService.GetEditorOperations(textView);
private readonly IBraceCompletionService _service = service;
private readonly IThreadingContext _threadingContext = threadingContext;
private readonly ITextUndoHistory _undoHistory = undoHistory;
private readonly bool _responsiveCompletion = responsiveCompletion;

private IThreadingContext ThreadingContext => _provider._threadingContext;
private EditorOptionsService EditorOptionsService => _provider._editorOptionsService;

public char OpeningBrace { get; } = openingBrace;
public char ClosingBrace { get; } = closingBrace;
Expand All @@ -63,17 +66,31 @@ private class BraceCompletionSession(

public void Start()
{
_threadingContext.ThrowIfNotOnUIThread();
ThreadingContext.ThrowIfNotOnUIThread();

// Brace completion is not cancellable.
var success = _threadingContext.JoinableTaskFactory.Run(() => TryStartAsync(CancellationToken.None));
if (!success)
// Brace completion is cancellable if the user has the 'responsive completion' option enabled. 200 ms was
// chosen as the default timeout with the editor as a good balance of having enough time for computation,
// while canceling early enough to not be too disruptive.
var cancellationTokenSource = new CancellationTokenSource();
if (_responsiveCompletion)
cancellationTokenSource.CancelAfter(200);

try
{
var success = ThreadingContext.JoinableTaskFactory.Run(() => TryStartAsync(cancellationTokenSource.Token));
if (!success)
EndSession();
}
catch (OperationCanceledException)
{
EndSession();
}
}

private async Task<bool> TryStartAsync(CancellationToken cancellationToken)
{
_threadingContext.ThrowIfNotOnUIThread();
ThreadingContext.ThrowIfNotOnUIThread();
cancellationToken.ThrowIfCancellationRequested();
var closingSnapshotPoint = ClosingPoint.GetPoint(SubjectBuffer.CurrentSnapshot);

if (closingSnapshotPoint.Position < 1)
Expand Down Expand Up @@ -118,7 +135,7 @@ private async Task<bool> TryStartAsync(CancellationToken cancellationToken)

if (TryGetBraceCompletionContext(out var contextAfterStart, cancellationToken))
{
var indentationOptions = SubjectBuffer.GetIndentationOptions(_editorOptionsService, document.Project.GetFallbackAnalyzerOptions(), contextAfterStart.Document.LanguageServices, explicitFormat: false);
var indentationOptions = SubjectBuffer.GetIndentationOptions(EditorOptionsService, document.Project.GetFallbackAnalyzerOptions(), contextAfterStart.Document.LanguageServices, explicitFormat: false);
var changesAfterStart = _service.GetTextChangesAfterCompletion(contextAfterStart, indentationOptions, cancellationToken);
if (changesAfterStart != null)
{
Expand All @@ -132,7 +149,7 @@ private async Task<bool> TryStartAsync(CancellationToken cancellationToken)

public void PreBackspace(out bool handledCommand)
{
_threadingContext.ThrowIfNotOnUIThread();
ThreadingContext.ThrowIfNotOnUIThread();
handledCommand = false;

var caretPos = this.GetCaretPosition();
Expand Down Expand Up @@ -172,7 +189,7 @@ public void PostBackspace()

public void PreOverType(out bool handledCommand)
{
_threadingContext.ThrowIfNotOnUIThread();
ThreadingContext.ThrowIfNotOnUIThread();
handledCommand = false;
if (ClosingPoint == null)
{
Expand Down Expand Up @@ -240,7 +257,7 @@ public void PostOverType()

public void PreTab(out bool handledCommand)
{
_threadingContext.ThrowIfNotOnUIThread();
ThreadingContext.ThrowIfNotOnUIThread();
handledCommand = false;

if (!HasForwardTyping)
Expand All @@ -264,7 +281,7 @@ public void PreReturn(out bool handledCommand)

public void PostReturn()
{
_threadingContext.ThrowIfNotOnUIThread();
ThreadingContext.ThrowIfNotOnUIThread();
if (this.GetCaretPosition().HasValue)
{
var closingSnapshotPoint = ClosingPoint.GetPoint(SubjectBuffer.CurrentSnapshot);
Expand All @@ -276,7 +293,7 @@ public void PostReturn()
return;
}

var indentationOptions = SubjectBuffer.GetIndentationOptions(_editorOptionsService, context.FallbackOptions, context.Document.LanguageServices, explicitFormat: false);
var indentationOptions = SubjectBuffer.GetIndentationOptions(EditorOptionsService, context.FallbackOptions, context.Document.LanguageServices, explicitFormat: false);
var changesAfterReturn = _service.GetTextChangeAfterReturn(context, indentationOptions, CancellationToken.None);
if (changesAfterReturn != null)
{
Expand Down Expand Up @@ -321,7 +338,7 @@ private bool HasForwardTyping
{
get
{
_threadingContext.ThrowIfNotOnUIThread();
ThreadingContext.ThrowIfNotOnUIThread();
var closingSnapshotPoint = ClosingPoint.GetPoint(SubjectBuffer.CurrentSnapshot);

if (closingSnapshotPoint.Position > 0)
Expand Down Expand Up @@ -366,7 +383,7 @@ internal ITextUndoTransaction CreateUndoTransaction()

private void MoveCaretToClosingPoint()
{
_threadingContext.ThrowIfNotOnUIThread();
ThreadingContext.ThrowIfNotOnUIThread();
var closingSnapshotPoint = ClosingPoint.GetPoint(SubjectBuffer.CurrentSnapshot);

// find the position just after the closing brace in the view's text buffer
Expand Down Expand Up @@ -396,7 +413,7 @@ private bool TryGetBraceCompletionContext(out BraceCompletionContext context, Ca

private BraceCompletionContext GetBraceCompletionContext(ParsedDocument document, StructuredAnalyzerConfigOptions fallbackOptions)
{
_threadingContext.ThrowIfNotOnUIThread();
ThreadingContext.ThrowIfNotOnUIThread();
var snapshot = SubjectBuffer.CurrentSnapshot;

var closingSnapshotPoint = ClosingPoint.GetPosition(snapshot);
Expand All @@ -409,7 +426,7 @@ private BraceCompletionContext GetBraceCompletionContext(ParsedDocument document

private void ApplyBraceCompletionResult(BraceCompletionResult result)
{
_threadingContext.ThrowIfNotOnUIThread();
ThreadingContext.ThrowIfNotOnUIThread();
using var edit = SubjectBuffer.CreateEdit();
foreach (var change in result.TextChanges)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ internal partial class BraceCompletionSessionProvider(
public bool TryCreateSession(ITextView textView, SnapshotPoint openingPoint, char openingBrace, char closingBrace, out IBraceCompletionSession session)
{
_threadingContext.ThrowIfNotOnUIThread();

var responsiveCompletion = textView.Options.GetOptionValue(DefaultOptions.ResponsiveCompletionOptionId);

var textSnapshot = openingPoint.Snapshot;
var document = textSnapshot.GetOpenDocumentInCurrentContextWithChanges();
if (document != null)
Expand All @@ -63,9 +66,9 @@ public bool TryCreateSession(ITextView textView, SnapshotPoint openingPoint, cha
{
var undoHistory = _undoManager.GetTextBufferUndoManager(textView.TextBuffer).TextBufferUndoHistory;
session = new BraceCompletionSession(
textView, openingPoint.Snapshot.TextBuffer, openingPoint, openingBrace, closingBrace,
undoHistory, _editorOperationsFactoryService, _editorOptionsService,
editorSession, _threadingContext);
this, textView, openingPoint.Snapshot.TextBuffer,
openingPoint, openingBrace, closingBrace,
undoHistory, editorSession, responsiveCompletion);
return true;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,22 @@ protected override bool IsValidOpeningBraceToken(SyntaxToken token)
protected override bool IsValidClosingBraceToken(SyntaxToken token)
=> token.IsKind(SyntaxKind.GreaterThanToken);

protected override ValueTask<bool> IsValidOpenBraceTokenAtPositionAsync(Document document, SyntaxToken token, int position, CancellationToken cancellationToken)
protected override async ValueTask<bool> IsValidOpenBraceTokenAtPositionAsync(Document document, SyntaxToken token, int position, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

// check what parser thinks about the newly typed "<" and only proceed if parser thinks it is "<" of
// type argument or parameter list
if (token.CheckParent<TypeParameterListSyntax>(n => n.LessThanToken == token) ||
token.CheckParent<TypeArgumentListSyntax>(n => n.LessThanToken == token) ||
token.CheckParent<FunctionPointerParameterListSyntax>(n => n.LessThanToken == token))
{
return ValueTaskFactory.FromResult(true);
return true;
}

// type argument can be easily ambiguous with normal < operations
if (token.Parent is not BinaryExpressionSyntax(SyntaxKind.LessThanExpression) node || node.OperatorToken != token)
return ValueTaskFactory.FromResult(false);
return false;

// type_argument_list only shows up in the following grammar construct:
//
Expand All @@ -58,15 +60,10 @@ protected override ValueTask<bool> IsValidOpenBraceTokenAtPositionAsync(Document
// So if the prior token is not an identifier, this could not be a type-argument-list.
var previousToken = token.GetPreviousToken();
if (previousToken.Parent is not IdentifierNameSyntax identifier)
return ValueTaskFactory.FromResult(false);

return IsSemanticTypeArgumentAsync(document, node.SpanStart, identifier, cancellationToken);
return false;

static async ValueTask<bool> IsSemanticTypeArgumentAsync(Document document, int position, IdentifierNameSyntax identifier, CancellationToken cancellationToken)
{
var semanticModel = await document.ReuseExistingSpeculativeModelAsync(position, cancellationToken).ConfigureAwait(false);
var info = semanticModel.GetSymbolInfo(identifier, cancellationToken);
return info.CandidateSymbols.Any(static s => s.GetArity() > 0);
}
var semanticModel = await document.ReuseExistingSpeculativeModelAsync(position, cancellationToken).ConfigureAwait(false);
var info = semanticModel.GetSymbolInfo(identifier, cancellationToken);
return info.CandidateSymbols.Any(static s => s.GetArity() > 0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ internal abstract class AbstractBraceCompletionService : IBraceCompletionService

public ValueTask<bool> HasBraceCompletionAsync(BraceCompletionContext context, Document document, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (!context.HasCompletionForOpeningBrace(OpeningBrace))
return ValueTaskFactory.FromResult(false);

Expand Down

0 comments on commit a494430

Please sign in to comment.