From badc7418c13f0a834c833f5e2e036e29df18949b Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Mon, 17 May 2021 23:49:29 +0000 Subject: [PATCH] Cherry-pick PR #44125 into release-4.3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Component commits: 34b80a51af Don’t offer import statement completions at `from` position afa4d051a9 Set isGlobalCompletion to false, use indexOf lookup --- src/services/completions.ts | 47 +++++++++++++++---- .../fourslash/importStatementCompletions1.ts | 26 ++++++++++ 2 files changed, 64 insertions(+), 9 deletions(-) diff --git a/src/services/completions.ts b/src/services/completions.ts index 98d5add6fe0a4..5ed1608fbcdcb 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -174,6 +174,8 @@ namespace ts.Completions { return jsdocCompletionInfo(JsDoc.getJSDocTagCompletions()); case CompletionDataKind.JsDocParameterName: return jsdocCompletionInfo(JsDoc.getJSDocParameterNameCompletions(completionData.tag)); + case CompletionDataKind.Keywords: + return specificKeywordCompletionInfo(completionData.keywords); default: return Debug.assertNever(completionData); } @@ -183,6 +185,20 @@ namespace ts.Completions { return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, entries }; } + function specificKeywordCompletionInfo(keywords: readonly SyntaxKind[]): CompletionInfo { + return { + isGlobalCompletion: false, + isMemberCompletion: false, + isNewIdentifierLocation: false, + entries: keywords.map(k => ({ + name: tokenToString(k)!, + kind: ScriptElementKind.keyword, + kindModifiers: ScriptElementKindModifier.none, + sortText: SortText.GlobalsOrKeywords, + })), + }; + } + function getOptionalReplacementSpan(location: Node | undefined) { // StringLiteralLike locations are handled separately in stringCompletions.ts return location?.kind === SyntaxKind.Identifier ? createTextSpanFromNode(location) : undefined; @@ -802,6 +818,8 @@ namespace ts.Completions { return JsDoc.getJSDocTagCompletionDetails(name); case CompletionDataKind.JsDocParameterName: return JsDoc.getJSDocParameterNameCompletionDetails(name); + case CompletionDataKind.Keywords: + return request.keywords.indexOf(stringToToken(name)!) > -1 ? createSimpleDetails(name, ScriptElementKind.keyword, SymbolDisplayPartKind.keyword) : undefined; default: return Debug.assertNever(request); } @@ -893,7 +911,7 @@ namespace ts.Completions { return completion.type === "symbol" ? completion.symbol : undefined; } - const enum CompletionDataKind { Data, JsDocTagName, JsDocTag, JsDocParameterName } + const enum CompletionDataKind { Data, JsDocTagName, JsDocTag, JsDocParameterName, Keywords } /** true: after the `=` sign but no identifier has been typed yet. Else is the Identifier after the initializer. */ type IsJsxInitializer = boolean | Identifier; interface CompletionData { @@ -918,7 +936,10 @@ namespace ts.Completions { readonly isJsxIdentifierExpected: boolean; readonly importCompletionNode?: Node; } - type Request = { readonly kind: CompletionDataKind.JsDocTagName | CompletionDataKind.JsDocTag } | { readonly kind: CompletionDataKind.JsDocParameterName, tag: JSDocParameterTag }; + type Request = + | { readonly kind: CompletionDataKind.JsDocTagName | CompletionDataKind.JsDocTag } + | { readonly kind: CompletionDataKind.JsDocParameterName, tag: JSDocParameterTag } + | { readonly kind: CompletionDataKind.Keywords, keywords: readonly SyntaxKind[] }; export const enum CompletionKind { ObjectPropertyDeclaration, @@ -1101,13 +1122,17 @@ namespace ts.Completions { let location = getTouchingPropertyName(sourceFile, position); if (contextToken) { + const importCompletionCandidate = getImportCompletionNode(contextToken); + if (importCompletionCandidate === SyntaxKind.FromKeyword) { + return { kind: CompletionDataKind.Keywords, keywords: [SyntaxKind.FromKeyword] }; + } // Import statement completions use `insertText`, and also require the `data` property of `CompletionEntryIdentifier` // added in TypeScript 4.3 to be sent back from the client during `getCompletionEntryDetails`. Since this feature // is not backward compatible with older clients, the language service defaults to disabling it, allowing newer clients // to opt in with the `includeCompletionsForImportStatements` user preference. - importCompletionNode = preferences.includeCompletionsForImportStatements && preferences.includeCompletionsWithInsertText - ? getImportCompletionNode(contextToken) - : undefined; + if (importCompletionCandidate && preferences.includeCompletionsForImportStatements && preferences.includeCompletionsWithInsertText) { + importCompletionNode = importCompletionCandidate; + } // Bail out if this is a known invalid completion location if (!importCompletionNode && isCompletionListBlocker(contextToken)) { log("Returning an empty list because completion was requested in an invalid position."); @@ -3041,7 +3066,7 @@ namespace ts.Completions { function getImportCompletionNode(contextToken: Node) { const candidate = getCandidate(); - return candidate && rangeIsOnSingleLine(candidate, candidate.getSourceFile()) ? candidate : undefined; + return candidate === SyntaxKind.FromKeyword || candidate && rangeIsOnSingleLine(candidate, candidate.getSourceFile()) ? candidate : undefined; function getCandidate() { const parent = contextToken.parent; @@ -3049,9 +3074,13 @@ namespace ts.Completions { return isModuleSpecifierMissingOrEmpty(parent.moduleReference) ? parent : undefined; } if (isNamedImports(parent) || isNamespaceImport(parent)) { - return isModuleSpecifierMissingOrEmpty(parent.parent.parent.moduleSpecifier) && (isNamespaceImport(parent) || parent.elements.length < 2) && !parent.parent.name - ? parent.parent.parent - : undefined; + if (isModuleSpecifierMissingOrEmpty(parent.parent.parent.moduleSpecifier) && (isNamespaceImport(parent) || parent.elements.length < 2) && !parent.parent.name) { + // At `import { ... } |` or `import * as Foo |`, the only possible completion is `from` + return contextToken.kind === SyntaxKind.CloseBraceToken || contextToken.kind === SyntaxKind.Identifier + ? SyntaxKind.FromKeyword + : parent.parent.parent; + } + return undefined; } if (isImportKeyword(contextToken) && isSourceFile(parent)) { // A lone import keyword with nothing following it does not parse as a statement at all diff --git a/tests/cases/fourslash/importStatementCompletions1.ts b/tests/cases/fourslash/importStatementCompletions1.ts index c9f991339314d..c28aa81c7922b 100644 --- a/tests/cases/fourslash/importStatementCompletions1.ts +++ b/tests/cases/fourslash/importStatementCompletions1.ts @@ -73,3 +73,29 @@ } }); }); + +// @Filename: /index13.ts +//// import {} /*13*/ + +// @Filename: /index14.ts +//// import {} f/*14*/ + +// @Filename: /index15.ts +//// import * as foo /*15*/ + +// @Filename: /index16.ts +//// import * as foo f/*16*/ + +[13, 14, 15, 16].forEach(marker => { + verify.completions({ + marker: "" + marker, + exact: { + name: "from", + sortText: completion.SortText.GlobalsOrKeywords, + }, + preferences: { + includeCompletionsForImportStatements: true, + includeInsertTextCompletions: true, + } + }); +});