Don’t offer import statement completions at from position

This commit is contained in:
Andrew Branch
2021-05-17 11:27:44 -07:00
parent 9eab33463c
commit 34b80a51af
2 changed files with 64 additions and 9 deletions
+38 -9
View File
@@ -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: true,
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.some(k => tokenToString(k) === name) ? 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
@@ -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,
}
});
});