-
Notifications
You must be signed in to change notification settings - Fork 4.2k
soft-select select camelcase matched item if user might be typing an … #80809
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,10 +5,12 @@ | |
| using System; | ||
| using System.Composition; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using Microsoft.CodeAnalysis.Completion; | ||
| using Microsoft.CodeAnalysis.CSharp.Completion.Providers; | ||
| using Microsoft.CodeAnalysis.Host; | ||
| using Microsoft.CodeAnalysis.Host.Mef; | ||
| using Microsoft.CodeAnalysis.Shared.Extensions; | ||
| using Microsoft.CodeAnalysis.Shared.TestHooks; | ||
| using Microsoft.CodeAnalysis.Text; | ||
|
|
||
|
|
@@ -65,4 +67,22 @@ internal override CompletionRules GetRules(CompletionOptions options) | |
|
|
||
| return newRules; | ||
| } | ||
|
|
||
| internal override async Task<bool> IsSpeculativeTypeParameterContextAsync(Document document, int position, CancellationToken cancellationToken) | ||
| { | ||
| var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. doc why 'true' for inMemberDeclarationOnly |
||
|
|
||
| // Because it's less likely the user wants to type a (undeclared) type parameter when they are inside a method body, treating them so | ||
| // might intefere with user intention. For example, while it's fine to provide a speculative `T` item in a statement context, | ||
| // since typing 2 characters would filter it out, but for selection, we don't want to soft-select item `TypeBuilder`after `TB` | ||
| // is typed in the example below (as if user want to add `TBuilder` to method declaration later): | ||
| // | ||
| // class C | ||
| // { | ||
| // void M() | ||
| // { | ||
| // TB$$ | ||
| // } | ||
| return CompletionUtilities.IsSpeculativeTypeParameterContext(syntaxTree, position, semanticModel: null, includeStatementContexts: false, cancellationToken); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,7 +6,9 @@ | |
| using System.Threading; | ||
| using Microsoft.CodeAnalysis.Completion; | ||
| using Microsoft.CodeAnalysis.CSharp.Extensions; | ||
| using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; | ||
| using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
| using Microsoft.CodeAnalysis.CSharp.Utilities; | ||
| using Microsoft.CodeAnalysis.Shared.Extensions; | ||
| using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery; | ||
| using Microsoft.CodeAnalysis.Text; | ||
|
|
@@ -238,4 +240,123 @@ public static TextSpan GetTargetSelectionSpanForInsertedMember(SyntaxNode caretT | |
| throw ExceptionUtilities.Unreachable(); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Determines whether the specified position in the syntax tree is a valid context for speculatively typing | ||
| /// a type parameter (which might be undeclared yet). This handles cases where the user may be in the middle of typing a generic type, tuple | ||
| /// and ref type as well. | ||
| /// | ||
| /// For example, when you typed `public TBuilder$$`, you might want to type `public TBuilder M<TBuilder>(){}`, | ||
| /// so TBuilder is a valid speculative type parameter context. | ||
| /// </summary> | ||
| public static bool IsSpeculativeTypeParameterContext(SyntaxTree syntaxTree, int position, SemanticModel? semanticModel, bool includeStatementContexts, CancellationToken cancellationToken) | ||
| { | ||
| var spanStart = position; | ||
|
|
||
| // We could be in the middle of a ref/generic/tuple type, instead of a simple T case. | ||
| // If we managed to walk out and get a different SpanStart, we treat it as a simple $$T case. | ||
| while (true) | ||
| { | ||
| var oldSpanStart = spanStart; | ||
|
|
||
| spanStart = WalkOutOfGenericType(syntaxTree, spanStart, semanticModel, cancellationToken); | ||
| spanStart = WalkOutOfTupleType(syntaxTree, spanStart, cancellationToken); | ||
| spanStart = WalkOutOfRefType(syntaxTree, spanStart, cancellationToken); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if you make these helpers into local functions, you can just rewrite this as: spanStart = WalkOutOfGeneric(WalkOutOfTuple(WalkOutOfRef(spanStart))) |
||
|
|
||
| if (spanStart == oldSpanStart) | ||
| { | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| var token = syntaxTree.FindTokenOnLeftOfPosition(spanStart, cancellationToken); | ||
|
|
||
| // Always want to allow in member declaration and delegate return type context, for example: | ||
| // class C | ||
| // { | ||
| // public T$$ | ||
| // } | ||
| // | ||
| // delegate T$$ | ||
| if (syntaxTree.IsMemberDeclarationContext(spanStart, context: null, SyntaxKindSet.AllMemberModifiers, SyntaxKindSet.NonEnumTypeDeclarations, canBePartial: true, cancellationToken) || | ||
| syntaxTree.IsGlobalMemberDeclarationContext(spanStart, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || | ||
| syntaxTree.IsDelegateReturnTypeContext(spanStart, token)) | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| // Because it's less likely the user wants to type a (undeclared) type parameter when they are inside a method body, treating them so | ||
| // might intefere with user intention. For example, while it's fine to provide a speculative `T` item in a statement context, | ||
| // since typing 2 characters would filter it out, but for selection, we don't want to soft-select item `TypeBuilder`after `TB` | ||
| // is typed in the example below (as if user want to add `TBuilder` to method declaration later): | ||
| // | ||
| // class C | ||
| // { | ||
| // void M() | ||
| // { | ||
| // TB$$ | ||
| // } | ||
| if (includeStatementContexts) | ||
| { | ||
| return syntaxTree.IsStatementContext(spanStart, token, cancellationToken) || | ||
| syntaxTree.IsGlobalStatementContext(spanStart, cancellationToken); | ||
| } | ||
|
|
||
| return false; | ||
|
|
||
| static int WalkOutOfGenericType(SyntaxTree syntaxTree, int position, SemanticModel? semanticModel, CancellationToken cancellationToken) | ||
| { | ||
| var spanStart = position; | ||
| var token = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken); | ||
|
|
||
| if (syntaxTree.IsGenericTypeArgumentContext(position, token, cancellationToken, semanticModel)) | ||
| { | ||
| if (syntaxTree.IsInPartiallyWrittenGeneric(spanStart, cancellationToken, out var nameToken)) | ||
| { | ||
| spanStart = nameToken.SpanStart; | ||
| } | ||
|
|
||
| // If the user types Goo<T, automatic brace completion will insert the close brace | ||
| // and the generic won't be "partially written". | ||
| if (spanStart == position) | ||
| { | ||
| spanStart = token.GetAncestor<GenericNameSyntax>()?.SpanStart ?? spanStart; | ||
| } | ||
|
|
||
| var tokenLeftOfGenericName = syntaxTree.FindTokenOnLeftOfPosition(spanStart, cancellationToken); | ||
| if (tokenLeftOfGenericName.IsKind(SyntaxKind.DotToken) && tokenLeftOfGenericName.Parent.IsKind(SyntaxKind.QualifiedName)) | ||
| { | ||
| spanStart = tokenLeftOfGenericName.Parent.SpanStart; | ||
| } | ||
| } | ||
|
|
||
| return spanStart; | ||
| } | ||
|
|
||
| static int WalkOutOfRefType(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken) | ||
| { | ||
| var prevToken = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken) | ||
| .GetPreviousTokenIfTouchingWord(position); | ||
|
|
||
| if (prevToken.Kind() is SyntaxKind.RefKeyword or SyntaxKind.ReadOnlyKeyword && prevToken.Parent.IsKind(SyntaxKind.RefType)) | ||
| { | ||
| return prevToken.SpanStart; | ||
| } | ||
|
|
||
| return position; | ||
| } | ||
|
|
||
| static int WalkOutOfTupleType(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken) | ||
| { | ||
| var prevToken = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken) | ||
| .GetPreviousTokenIfTouchingWord(position); | ||
|
|
||
| if (prevToken.IsPossibleTupleOpenParenOrComma()) | ||
| { | ||
| return prevToken.Parent!.SpanStart; | ||
| } | ||
|
|
||
| return position; | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
perhaps mention this is just a weak heuristic. But it helps given general .Net naming patterns.