Skip to content

Commit 8610a88

Browse files
Merge pull request #2495 from Microsoft/useAppropriateCompletionEdge
Use adjusted completion position when at end of identifier
2 parents 07ea406 + 0437dfb commit 8610a88

7 files changed

+92
-21
lines changed

src/services/services.ts

Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2476,36 +2476,38 @@ module ts {
24762476
return undefined;
24772477
}
24782478

2479-
// The decision to provide completion depends on the previous token, so find it
2480-
// Note: previousToken can be undefined if we are the beginning of the file
24812479
start = new Date().getTime();
24822480
let previousToken = findPrecedingToken(position, sourceFile);
24832481
log("getCompletionData: Get previous token 1: " + (new Date().getTime() - start));
24842482

2485-
// The caret is at the end of an identifier; this is a partial identifier that we want to complete: e.g. a.toS|
2486-
// Skip this partial identifier to the previous token
2487-
if (previousToken && position <= previousToken.end && previousToken.kind === SyntaxKind.Identifier) {
2483+
// The decision to provide completion depends on the contextToken, which is determined through the previousToken.
2484+
// Note: 'previousToken' (and thus 'contextToken') can be undefined if we are the beginning of the file
2485+
let contextToken = previousToken;
2486+
2487+
// Check if the caret is at the end of an identifier; this is a partial identifier that we want to complete: e.g. a.toS|
2488+
// Skip this partial identifier and adjust the contextToken to the token that precedes it.
2489+
if (contextToken && position <= contextToken.end && isWord(contextToken.kind)) {
24882490
let start = new Date().getTime();
2489-
previousToken = findPrecedingToken(previousToken.pos, sourceFile);
2491+
contextToken = findPrecedingToken(contextToken.getFullStart(), sourceFile);
24902492
log("getCompletionData: Get previous token 2: " + (new Date().getTime() - start));
24912493
}
24922494

24932495
// Check if this is a valid completion location
2494-
if (previousToken && isCompletionListBlocker(previousToken)) {
2496+
if (contextToken && isCompletionListBlocker(contextToken)) {
24952497
log("Returning an empty list because completion was requested in an invalid position.");
24962498
return undefined;
24972499
}
24982500

24992501
// Find the node where completion is requested on, in the case of a completion after a dot, it is the member access expression
2500-
// other wise, it is a request for all visible symbols in the scope, and the node is the current location
2502+
// otherwise, it is a request for all visible symbols in the scope, and the node is the current location
25012503
let node = currentToken;
25022504
let isRightOfDot = false;
2503-
if (previousToken && previousToken.kind === SyntaxKind.DotToken && previousToken.parent.kind === SyntaxKind.PropertyAccessExpression) {
2504-
node = (<PropertyAccessExpression>previousToken.parent).expression;
2505+
if (contextToken && contextToken.kind === SyntaxKind.DotToken && contextToken.parent.kind === SyntaxKind.PropertyAccessExpression) {
2506+
node = (<PropertyAccessExpression>contextToken.parent).expression;
25052507
isRightOfDot = true;
25062508
}
2507-
else if (previousToken && previousToken.kind === SyntaxKind.DotToken && previousToken.parent.kind === SyntaxKind.QualifiedName) {
2508-
node = (<QualifiedName>previousToken.parent).left;
2509+
else if (contextToken && contextToken.kind === SyntaxKind.DotToken && contextToken.parent.kind === SyntaxKind.QualifiedName) {
2510+
node = (<QualifiedName>contextToken.parent).left;
25092511
isRightOfDot = true;
25102512
}
25112513

@@ -2552,7 +2554,7 @@ module ts {
25522554
}
25532555
}
25542556
else {
2555-
let containingObjectLiteral = getContainingObjectLiteralApplicableForCompletion(previousToken);
2557+
let containingObjectLiteral = getContainingObjectLiteralApplicableForCompletion(contextToken);
25562558
if (containingObjectLiteral) {
25572559
// Object literal expression, look up possible property names from contextual type
25582560
isMemberCompletion = true;
@@ -2569,27 +2571,59 @@ module ts {
25692571
symbols = filterContextualMembersList(contextualTypeMembers, containingObjectLiteral.properties);
25702572
}
25712573
}
2572-
else if (getAncestor(previousToken, SyntaxKind.ImportClause)) {
2574+
else if (getAncestor(contextToken, SyntaxKind.ImportClause)) {
25732575
// cursor is in import clause
25742576
// try to show exported member for imported module
25752577
isMemberCompletion = true;
25762578
isNewIdentifierLocation = true;
2577-
if (showCompletionsInImportsClause(previousToken)) {
2578-
let importDeclaration = <ImportDeclaration>getAncestor(previousToken, SyntaxKind.ImportDeclaration);
2579+
if (showCompletionsInImportsClause(contextToken)) {
2580+
let importDeclaration = <ImportDeclaration>getAncestor(contextToken, SyntaxKind.ImportDeclaration);
25792581
Debug.assert(importDeclaration !== undefined);
25802582
let exports = typeInfoResolver.getExportsOfExternalModule(importDeclaration);
25812583
symbols = filterModuleExports(exports, importDeclaration);
25822584
}
25832585
}
25842586
else {
2585-
// Get scope members
2587+
// Get all entities in the current scope.
25862588
isMemberCompletion = false;
2587-
isNewIdentifierLocation = isNewIdentifierDefinitionLocation(previousToken);
2589+
isNewIdentifierLocation = isNewIdentifierDefinitionLocation(contextToken);
2590+
2591+
if (previousToken !== contextToken) {
2592+
Debug.assert(!!previousToken, "Expected 'contextToken' to be defined when different from 'previousToken'.");
2593+
}
2594+
// We need to find the node that will give us an appropriate scope to begin
2595+
// aggregating completion candidates. This is achieved in 'getScopeNode'
2596+
// by finding the first node that encompasses a position, accounting for whether a node
2597+
// is "complete" to decide whether a position belongs to the node.
2598+
//
2599+
// However, at the end of an identifier, we are interested in the scope of the identifier
2600+
// itself, but fall outside of the identifier. For instance:
2601+
//
2602+
// xyz => x$
2603+
//
2604+
// the cursor is outside of both the 'x' and the arrow function 'xyz => x',
2605+
// so 'xyz' is not returned in our results.
2606+
//
2607+
// We define 'adjustedPosition' so that we may appropriately account for
2608+
// being at the end of an identifier. The intention is that if requesting completion
2609+
// at the end of an identifier, it should be effectively equivalent to requesting completion
2610+
// anywhere inside/at the beginning of the identifier. So in the previous case, the
2611+
// 'adjustedPosition' will work as if requesting completion in the following:
2612+
//
2613+
// xyz => $x
2614+
//
2615+
// If previousToken !== contextToken, then
2616+
// - 'contextToken' was adjusted to the token prior to 'previousToken'
2617+
// because we were at the end of an identifier.
2618+
// - 'previousToken' is defined.
2619+
let adjustedPosition = previousToken !== contextToken ?
2620+
previousToken.getStart() :
2621+
position;
2622+
2623+
let scopeNode = getScopeNode(contextToken, adjustedPosition, sourceFile);
25882624

25892625
/// TODO filter meaning based on the current context
2590-
let scopeNode = getScopeNode(previousToken, position, sourceFile);
25912626
let symbolMeanings = SymbolFlags.Type | SymbolFlags.Value | SymbolFlags.Namespace | SymbolFlags.Alias;
2592-
25932627
symbols = typeInfoResolver.getSymbolsInScope(scopeNode, symbolMeanings);
25942628
}
25952629
}

src/services/utilities.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@ module ts {
450450
return n.kind >= SyntaxKind.FirstToken && n.kind <= SyntaxKind.LastToken;
451451
}
452452

453-
function isWord(kind: SyntaxKind): boolean {
453+
export function isWord(kind: SyntaxKind): boolean {
454454
return kind === SyntaxKind.Identifier || isKeyword(kind);
455455
}
456456

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////xyz => /*1*/x
4+
5+
goTo.marker("1");
6+
verify.completionListContains("xyz");
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////xyz => x/*1*/
4+
5+
goTo.marker("1");
6+
verify.completionListContains("xyz");
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////(d, defaultIsAnInvalidParameterName) => d/*1*/
4+
5+
goTo.marker("1");
6+
verify.completionListContains("d");
7+
verify.completionListContains("defaultIsAnInvalidParameterName");
8+
9+
// This should probably stop working in the future.
10+
verify.completionListContains("default", "default", /*documentation*/ undefined, "keyword");
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////(d, defaultIsAnInvalidParameterName) => default/*1*/
4+
5+
goTo.marker("1");
6+
verify.completionListContains("defaultIsAnInvalidParameterName");
7+
8+
// This should probably stop working in the future.
9+
verify.completionListContains("default", "default", /*documentation*/ undefined, "keyword");
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////xyz => x/*1*/y
4+
5+
goTo.marker("1");
6+
verify.completionListContains("xyz");

0 commit comments

Comments
 (0)