From 4ecb563aa4b436cd92fe82b61f4e4bbd1d5200d7 Mon Sep 17 00:00:00 2001 From: Xu Zhuo Date: Fri, 30 Apr 2021 02:16:51 +0800 Subject: [PATCH] Complete `constructor` keyword after property declaration (#43654) * Complete `constructor` keyword after property declaration. * Fix logical errors. * Fix for more universal situations. * Only provide completions if property declaration is terminated. * Simplify many logical conditions. * Make the fix more reliable. * Narrowing the fix. --- src/services/completions.ts | 42 +++++++- ...structorKeywordAfterPropertyDeclaration.ts | 98 +++++++++++++++++++ 2 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 tests/cases/fourslash/completionConstructorKeywordAfterPropertyDeclaration.ts diff --git a/src/services/completions.ts b/src/services/completions.ts index 098f0025808..98d5add6fe0 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -2360,7 +2360,7 @@ namespace ts.Completions { return isFunctionLike(contextToken.parent) && !isMethodDeclaration(contextToken.parent); } - // If the previous token is keyword correspoding to class member completion keyword + // If the previous token is keyword corresponding to class member completion keyword // there will be completion available here if (isClassMemberCompletionKeyword(keywordForNode(contextToken)) && isFromObjectTypeDeclaration(contextToken)) { return false; @@ -2398,6 +2398,33 @@ namespace ts.Completions { return isPropertyDeclaration(contextToken.parent); } + // If we are inside a class declaration, and `constructor` is totally not present, + // but we request a completion manually at a whitespace... + const ancestorClassLike = findAncestor(contextToken.parent, isClassLike); + if (ancestorClassLike && contextToken === previousToken && isPreviousPropertyDeclarationTerminated(contextToken, position)) { + return false; // Don't block completions. + } + + const ancestorPropertyDeclaraion = getAncestor(contextToken.parent, SyntaxKind.PropertyDeclaration); + // If we are inside a class declaration and typing `constructor` after property declaration... + if (ancestorPropertyDeclaraion + && contextToken !== previousToken + && isClassLike(previousToken.parent.parent) + // And the cursor is at the token... + && position <= previousToken.end) { + // If we are sure that the previous property declaration is terminated according to newline or semicolon... + if (isPreviousPropertyDeclarationTerminated(contextToken, previousToken.end)) { + return false; // Don't block completions. + } + else if (contextToken.kind !== SyntaxKind.EqualsToken + // Should not block: `class C { blah = c/**/ }` + // But should block: `class C { blah = somewhat c/**/ }` and `class C { blah: SomeType c/**/ }` + && (isInitializedProperty(ancestorPropertyDeclaraion as PropertyDeclaration) + || hasType(ancestorPropertyDeclaraion))) { + return true; + } + } + return isDeclarationName(contextToken) && !isShorthandPropertyAssignment(contextToken.parent) && !isJsxAttribute(contextToken.parent) @@ -2406,6 +2433,12 @@ namespace ts.Completions { && !(isClassLike(contextToken.parent) && (contextToken !== previousToken || position > previousToken.end)); } + function isPreviousPropertyDeclarationTerminated(contextToken: Node, position: number) { + return contextToken.kind !== SyntaxKind.EqualsToken && + (contextToken.kind === SyntaxKind.SemicolonToken + || !positionsAreOnSameLine(contextToken.end, position, sourceFile)); + } + function isFunctionLikeButNotConstructor(kind: SyntaxKind) { return isFunctionLikeKind(kind) && kind !== SyntaxKind.Constructor; } @@ -2859,6 +2892,13 @@ namespace ts.Completions { if (!contextToken) return undefined; + // class C { blah; constructor/**/ } and so on + if (location.kind === SyntaxKind.ConstructorKeyword + // class C { blah \n constructor/**/ } + || (isIdentifier(contextToken) && isPropertyDeclaration(contextToken.parent) && isClassLike(location))) { + return findAncestor(contextToken, isClassLike) as ObjectTypeDeclaration; + } + switch (contextToken.kind) { case SyntaxKind.EqualsToken: // class c { public prop = | /* global completions */ } return undefined; diff --git a/tests/cases/fourslash/completionConstructorKeywordAfterPropertyDeclaration.ts b/tests/cases/fourslash/completionConstructorKeywordAfterPropertyDeclaration.ts new file mode 100644 index 00000000000..922da3c3cef --- /dev/null +++ b/tests/cases/fourslash/completionConstructorKeywordAfterPropertyDeclaration.ts @@ -0,0 +1,98 @@ +/// + +//// // situations that `constructor` is partly present +//// class A { +//// blah; con/*1*/ +//// } +//// class B { +//// blah +//// con/*2*/ +//// } +//// class C { +//// blah: number +//// con/*3*/ +//// } +//// class D { +//// blah = 123 +//// con/*4*/ +//// } +//// class E { +//// blah = [123] +//// con/*5*/ +//// } +//// class F { +//// blah = {key: 123} +//// con/*6*/ +//// } +//// // situations that `constructor` is fully present +//// class G { +//// blah; constructor/*7*/ +//// } +//// class H { +//// blah +//// constructor/*8*/ +//// } +//// class I { +//// blah: number +//// constructor/*9*/ +//// } +//// class J { +//// blah = 123 +//// constructor/*10*/ +//// } +//// class K { +//// blah = [123] +//// constructor/*11*/ +//// } +//// class L { +//// blah = {key: 123} +//// constructor/*12*/ +//// } +//// // situations that `constructor` isn't present, but we should offer it +//// class M { +//// blah; /*13*/ +//// } +//// class N { +//// blah +//// /*14*/ +//// } +//// // situations that `constructor` should not be suggested +//// class O { +//// blah /*15*/ +//// } +//// class P { +//// blah con/*16*/ +//// } +//// class Q { +//// blah: number con/*17*/ +//// } +//// class R { +//// blah = 123 con/*18*/ +//// } +//// class S { +//// blah = {key: 123} con/*19*/ +//// } +//// type SomeType = number +//// class T { +//// blah: SomeType con/*20*/ +//// } +//// const SomeValue = 123 +//// class U { +//// blah = SomeValue con/*21*/ +//// } + +function generateRange(l: number, r: number) { + return Array.from(Array(r - l + 1), (_, i) => String(i + l)); // [l, r] +} + +verify.completions({ + marker: generateRange(1, 14), + includes: { name: "constructor", sortText: completion.SortText.GlobalsOrKeywords }, + isNewIdentifierLocation: true, +}); + +verify.completions({ + marker: generateRange(15, 21), + exact: [], + isNewIdentifierLocation: true, +});