Skip to content

Commit badc741

Browse files
andrewbranchtypescript-bot
authored andcommitted
Cherry-pick PR microsoft#44125 into release-4.3
Component commits: 34b80a5 Don’t offer import statement completions at `from` position afa4d05 Set isGlobalCompletion to false, use indexOf lookup
1 parent 1d850c0 commit badc741

File tree

2 files changed

+64
-9
lines changed

2 files changed

+64
-9
lines changed

src/services/completions.ts

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ namespace ts.Completions {
174174
return jsdocCompletionInfo(JsDoc.getJSDocTagCompletions());
175175
case CompletionDataKind.JsDocParameterName:
176176
return jsdocCompletionInfo(JsDoc.getJSDocParameterNameCompletions(completionData.tag));
177+
case CompletionDataKind.Keywords:
178+
return specificKeywordCompletionInfo(completionData.keywords);
177179
default:
178180
return Debug.assertNever(completionData);
179181
}
@@ -183,6 +185,20 @@ namespace ts.Completions {
183185
return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, entries };
184186
}
185187

188+
function specificKeywordCompletionInfo(keywords: readonly SyntaxKind[]): CompletionInfo {
189+
return {
190+
isGlobalCompletion: false,
191+
isMemberCompletion: false,
192+
isNewIdentifierLocation: false,
193+
entries: keywords.map(k => ({
194+
name: tokenToString(k)!,
195+
kind: ScriptElementKind.keyword,
196+
kindModifiers: ScriptElementKindModifier.none,
197+
sortText: SortText.GlobalsOrKeywords,
198+
})),
199+
};
200+
}
201+
186202
function getOptionalReplacementSpan(location: Node | undefined) {
187203
// StringLiteralLike locations are handled separately in stringCompletions.ts
188204
return location?.kind === SyntaxKind.Identifier ? createTextSpanFromNode(location) : undefined;
@@ -802,6 +818,8 @@ namespace ts.Completions {
802818
return JsDoc.getJSDocTagCompletionDetails(name);
803819
case CompletionDataKind.JsDocParameterName:
804820
return JsDoc.getJSDocParameterNameCompletionDetails(name);
821+
case CompletionDataKind.Keywords:
822+
return request.keywords.indexOf(stringToToken(name)!) > -1 ? createSimpleDetails(name, ScriptElementKind.keyword, SymbolDisplayPartKind.keyword) : undefined;
805823
default:
806824
return Debug.assertNever(request);
807825
}
@@ -893,7 +911,7 @@ namespace ts.Completions {
893911
return completion.type === "symbol" ? completion.symbol : undefined;
894912
}
895913

896-
const enum CompletionDataKind { Data, JsDocTagName, JsDocTag, JsDocParameterName }
914+
const enum CompletionDataKind { Data, JsDocTagName, JsDocTag, JsDocParameterName, Keywords }
897915
/** true: after the `=` sign but no identifier has been typed yet. Else is the Identifier after the initializer. */
898916
type IsJsxInitializer = boolean | Identifier;
899917
interface CompletionData {
@@ -918,7 +936,10 @@ namespace ts.Completions {
918936
readonly isJsxIdentifierExpected: boolean;
919937
readonly importCompletionNode?: Node;
920938
}
921-
type Request = { readonly kind: CompletionDataKind.JsDocTagName | CompletionDataKind.JsDocTag } | { readonly kind: CompletionDataKind.JsDocParameterName, tag: JSDocParameterTag };
939+
type Request =
940+
| { readonly kind: CompletionDataKind.JsDocTagName | CompletionDataKind.JsDocTag }
941+
| { readonly kind: CompletionDataKind.JsDocParameterName, tag: JSDocParameterTag }
942+
| { readonly kind: CompletionDataKind.Keywords, keywords: readonly SyntaxKind[] };
922943

923944
export const enum CompletionKind {
924945
ObjectPropertyDeclaration,
@@ -1101,13 +1122,17 @@ namespace ts.Completions {
11011122
let location = getTouchingPropertyName(sourceFile, position);
11021123

11031124
if (contextToken) {
1125+
const importCompletionCandidate = getImportCompletionNode(contextToken);
1126+
if (importCompletionCandidate === SyntaxKind.FromKeyword) {
1127+
return { kind: CompletionDataKind.Keywords, keywords: [SyntaxKind.FromKeyword] };
1128+
}
11041129
// Import statement completions use `insertText`, and also require the `data` property of `CompletionEntryIdentifier`
11051130
// added in TypeScript 4.3 to be sent back from the client during `getCompletionEntryDetails`. Since this feature
11061131
// is not backward compatible with older clients, the language service defaults to disabling it, allowing newer clients
11071132
// to opt in with the `includeCompletionsForImportStatements` user preference.
1108-
importCompletionNode = preferences.includeCompletionsForImportStatements && preferences.includeCompletionsWithInsertText
1109-
? getImportCompletionNode(contextToken)
1110-
: undefined;
1133+
if (importCompletionCandidate && preferences.includeCompletionsForImportStatements && preferences.includeCompletionsWithInsertText) {
1134+
importCompletionNode = importCompletionCandidate;
1135+
}
11111136
// Bail out if this is a known invalid completion location
11121137
if (!importCompletionNode && isCompletionListBlocker(contextToken)) {
11131138
log("Returning an empty list because completion was requested in an invalid position.");
@@ -3041,17 +3066,21 @@ namespace ts.Completions {
30413066

30423067
function getImportCompletionNode(contextToken: Node) {
30433068
const candidate = getCandidate();
3044-
return candidate && rangeIsOnSingleLine(candidate, candidate.getSourceFile()) ? candidate : undefined;
3069+
return candidate === SyntaxKind.FromKeyword || candidate && rangeIsOnSingleLine(candidate, candidate.getSourceFile()) ? candidate : undefined;
30453070

30463071
function getCandidate() {
30473072
const parent = contextToken.parent;
30483073
if (isImportEqualsDeclaration(parent)) {
30493074
return isModuleSpecifierMissingOrEmpty(parent.moduleReference) ? parent : undefined;
30503075
}
30513076
if (isNamedImports(parent) || isNamespaceImport(parent)) {
3052-
return isModuleSpecifierMissingOrEmpty(parent.parent.parent.moduleSpecifier) && (isNamespaceImport(parent) || parent.elements.length < 2) && !parent.parent.name
3053-
? parent.parent.parent
3054-
: undefined;
3077+
if (isModuleSpecifierMissingOrEmpty(parent.parent.parent.moduleSpecifier) && (isNamespaceImport(parent) || parent.elements.length < 2) && !parent.parent.name) {
3078+
// At `import { ... } |` or `import * as Foo |`, the only possible completion is `from`
3079+
return contextToken.kind === SyntaxKind.CloseBraceToken || contextToken.kind === SyntaxKind.Identifier
3080+
? SyntaxKind.FromKeyword
3081+
: parent.parent.parent;
3082+
}
3083+
return undefined;
30553084
}
30563085
if (isImportKeyword(contextToken) && isSourceFile(parent)) {
30573086
// A lone import keyword with nothing following it does not parse as a statement at all

tests/cases/fourslash/importStatementCompletions1.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,29 @@
7373
}
7474
});
7575
});
76+
77+
// @Filename: /index13.ts
78+
//// import {} /*13*/
79+
80+
// @Filename: /index14.ts
81+
//// import {} f/*14*/
82+
83+
// @Filename: /index15.ts
84+
//// import * as foo /*15*/
85+
86+
// @Filename: /index16.ts
87+
//// import * as foo f/*16*/
88+
89+
[13, 14, 15, 16].forEach(marker => {
90+
verify.completions({
91+
marker: "" + marker,
92+
exact: {
93+
name: "from",
94+
sortText: completion.SortText.GlobalsOrKeywords,
95+
},
96+
preferences: {
97+
includeCompletionsForImportStatements: true,
98+
includeInsertTextCompletions: true,
99+
}
100+
});
101+
});

0 commit comments

Comments
 (0)