Skip to content

Commit ba18337

Browse files
author
Andy
authored
Simplify string literal completions (#21415)
1 parent c4c9a00 commit ba18337

File tree

2 files changed

+108
-144
lines changed

2 files changed

+108
-144
lines changed

src/services/completions.ts

Lines changed: 86 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@ namespace ts.Completions {
3636
): CompletionInfo | undefined {
3737
if (isInReferenceComment(sourceFile, position)) {
3838
const entries = PathCompletions.getTripleSlashReferenceCompletion(sourceFile, position, compilerOptions, host);
39-
return entries && pathCompletionsInfo(entries);
39+
return entries && convertPathCompletions(entries);
4040
}
4141

4242
const contextToken = findPrecedingToken(position, sourceFile);
4343

4444
if (isInString(sourceFile, position, contextToken)) {
4545
return !contextToken || !isStringLiteral(contextToken) && !isNoSubstitutionTemplateLiteral(contextToken)
4646
? undefined
47-
: getStringLiteralCompletionEntries(sourceFile, contextToken, position, typeChecker, compilerOptions, host, log);
47+
: convertStringLiteralCompletions(getStringLiteralCompletionEntries(sourceFile, contextToken, position, typeChecker, compilerOptions, host), sourceFile, typeChecker, log);
4848
}
4949

5050
if (contextToken && isBreakOrContinueStatement(contextToken.parent)
@@ -73,6 +73,34 @@ namespace ts.Completions {
7373
}
7474
}
7575

76+
function convertStringLiteralCompletions(completion: StringLiteralCompletion | undefined, sourceFile: SourceFile, checker: TypeChecker, log: Log): CompletionInfo | undefined {
77+
if (completion === undefined) {
78+
return undefined;
79+
}
80+
switch (completion.kind) {
81+
case StringLiteralCompletionKind.Paths:
82+
return convertPathCompletions(completion.paths);
83+
case StringLiteralCompletionKind.Properties: {
84+
const entries: CompletionEntry[] = [];
85+
getCompletionEntriesFromSymbols(completion.symbols, entries, sourceFile, sourceFile, checker, ScriptTarget.ESNext, log, CompletionKind.String); // Target will not be used, so arbitrary
86+
return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: true, entries };
87+
}
88+
case StringLiteralCompletionKind.Types: {
89+
const entries = completion.types.map(type => ({ name: type.value, kindModifiers: ScriptElementKindModifier.none, kind: ScriptElementKind.variableElement, sortText: "0" }));
90+
return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: true, entries };
91+
}
92+
default:
93+
return Debug.assertNever(completion);
94+
}
95+
}
96+
97+
function convertPathCompletions(pathCompletions: ReadonlyArray<PathCompletions.PathCompletion>): CompletionInfo {
98+
const isGlobalCompletion = false; // We don't want the editor to offer any other completions, such as snippets, inside a comment.
99+
const isNewIdentifierLocation = true; // The user may type in a path that doesn't yet exist, creating a "new identifier" with respect to the collection of identifiers the server is aware of.
100+
const entries = pathCompletions.map(({ name, kind, span }) => ({ name, kind, kindModifiers: ScriptElementKindModifier.none, sortText: "0", replacementSpan: span }));
101+
return { isGlobalCompletion, isMemberCompletion: false, isNewIdentifierLocation, entries };
102+
}
103+
76104
function jsdocCompletionInfo(entries: CompletionEntry[]): CompletionInfo {
77105
return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, entries };
78106
}
@@ -294,7 +322,38 @@ namespace ts.Completions {
294322
}
295323
}
296324

297-
function getStringLiteralCompletionEntries(sourceFile: SourceFile, node: StringLiteralLike, position: number, typeChecker: TypeChecker, compilerOptions: CompilerOptions, host: LanguageServiceHost, log: Log): CompletionInfo | undefined {
325+
function getLabelStatementCompletions(node: Node): CompletionEntry[] {
326+
const entries: CompletionEntry[] = [];
327+
const uniques = createMap<true>();
328+
let current = node;
329+
330+
while (current) {
331+
if (isFunctionLike(current)) {
332+
break;
333+
}
334+
if (isLabeledStatement(current)) {
335+
const name = current.label.text;
336+
if (!uniques.has(name)) {
337+
uniques.set(name, true);
338+
entries.push({
339+
name,
340+
kindModifiers: ScriptElementKindModifier.none,
341+
kind: ScriptElementKind.label,
342+
sortText: "0"
343+
});
344+
}
345+
}
346+
current = current.parent;
347+
}
348+
return entries;
349+
}
350+
351+
const enum StringLiteralCompletionKind { Paths, Properties, Types }
352+
type StringLiteralCompletion =
353+
| { readonly kind: StringLiteralCompletionKind.Paths, readonly paths: ReadonlyArray<PathCompletions.PathCompletion> }
354+
| { readonly kind: StringLiteralCompletionKind.Properties, readonly symbols: ReadonlyArray<Symbol> }
355+
| { readonly kind: StringLiteralCompletionKind.Types, readonly types: ReadonlyArray<StringLiteralType> };
356+
function getStringLiteralCompletionEntries(sourceFile: SourceFile, node: StringLiteralLike, position: number, typeChecker: TypeChecker, compilerOptions: CompilerOptions, host: LanguageServiceHost): StringLiteralCompletion | undefined {
298357
switch (node.parent.kind) {
299358
case SyntaxKind.LiteralType:
300359
switch (node.parent.parent.kind) {
@@ -308,15 +367,13 @@ namespace ts.Completions {
308367
// bar: string;
309368
// }
310369
// let x: Foo["/*completion position*/"]
311-
const type = typeChecker.getTypeFromTypeNode((node.parent.parent as IndexedAccessTypeNode).objectType);
312-
return getStringLiteralCompletionEntriesFromElementAccessOrIndexedAccess(node, sourceFile, type, typeChecker, compilerOptions.target, log);
370+
return { kind: StringLiteralCompletionKind.Properties, symbols: typeChecker.getTypeFromTypeNode((node.parent.parent as IndexedAccessTypeNode).objectType).getApparentProperties() };
313371
default:
314372
return undefined;
315373
}
316374

317375
case SyntaxKind.PropertyAssignment:
318-
if (node.parent.parent.kind === SyntaxKind.ObjectLiteralExpression &&
319-
(<PropertyAssignment>node.parent).name === node) {
376+
if (isObjectLiteralExpression(node.parent.parent) && (<PropertyAssignment>node.parent).name === node) {
320377
// Get quoted name of properties of the object literal expression
321378
// i.e. interface ConfigFiles {
322379
// 'jspm:dev': string
@@ -329,7 +386,8 @@ namespace ts.Completions {
329386
// foo({
330387
// '/*completion position*/'
331388
// });
332-
return getStringLiteralCompletionEntriesFromPropertyAssignment(<PropertyAssignment>node.parent, sourceFile, typeChecker, compilerOptions.target, log);
389+
const type = typeChecker.getContextualType(node.parent.parent);
390+
return { kind: StringLiteralCompletionKind.Properties, symbols: type && type.getApparentProperties() };
333391
}
334392
return fromContextualType();
335393

@@ -342,10 +400,9 @@ namespace ts.Completions {
342400
// }
343401
// let a: A;
344402
// a['/*completion position*/']
345-
const type = typeChecker.getTypeAtLocation(expression);
346-
return getStringLiteralCompletionEntriesFromElementAccessOrIndexedAccess(node, sourceFile, type, typeChecker, compilerOptions.target, log);
403+
return { kind: StringLiteralCompletionKind.Properties, symbols: typeChecker.getTypeAtLocation(expression).getApparentProperties() };
347404
}
348-
break;
405+
return undefined;
349406
}
350407

351408
case SyntaxKind.CallExpression:
@@ -355,9 +412,15 @@ namespace ts.Completions {
355412
// Get string literal completions from specialized signatures of the target
356413
// i.e. declare function f(a: 'A');
357414
// f("/*completion position*/")
358-
return argumentInfo ? getStringLiteralCompletionEntriesFromCallExpression(argumentInfo, typeChecker) : fromContextualType();
415+
if (argumentInfo) {
416+
const candidates: Signature[] = [];
417+
typeChecker.getResolvedSignature(argumentInfo.invocation, candidates, argumentInfo.argumentCount);
418+
const uniques = createMap<true>();
419+
return { kind: StringLiteralCompletionKind.Types, types: flatMap(candidates, candidate => getStringLiteralTypes(typeChecker.getParameterType(candidate, argumentInfo.argumentIndex), typeChecker, uniques)) };
420+
}
421+
return fromContextualType();
359422
}
360-
// falls through
423+
// falls through (is `require("")` or `import("")`)
361424

362425
case SyntaxKind.ImportDeclaration:
363426
case SyntaxKind.ExportDeclaration:
@@ -368,132 +431,28 @@ namespace ts.Completions {
368431
// import x = require("/*completion position*/");
369432
// var y = require("/*completion position*/");
370433
// export * from "/*completion position*/";
371-
return pathCompletionsInfo(PathCompletions.getStringLiteralCompletionsFromModuleNames(sourceFile, node, compilerOptions, host, typeChecker));
434+
return { kind: StringLiteralCompletionKind.Paths, paths: PathCompletions.getStringLiteralCompletionsFromModuleNames(sourceFile, node, compilerOptions, host, typeChecker) };
372435

373436
default:
374437
return fromContextualType();
375438
}
376439

377-
function fromContextualType(): CompletionInfo {
440+
function fromContextualType(): StringLiteralCompletion {
378441
// Get completion for string literal from string literal type
379442
// i.e. var x: "hi" | "hello" = "/*completion position*/"
380-
return getStringLiteralCompletionEntriesFromType(getContextualTypeFromParent(node, typeChecker), typeChecker);
443+
return { kind: StringLiteralCompletionKind.Types, types: getStringLiteralTypes(getContextualTypeFromParent(node, typeChecker), typeChecker) };
381444
}
382445
}
383446

384-
function pathCompletionsInfo(entries: CompletionEntry[]): CompletionInfo {
385-
return {
386-
// We don't want the editor to offer any other completions, such as snippets, inside a comment.
387-
isGlobalCompletion: false,
388-
isMemberCompletion: false,
389-
// The user may type in a path that doesn't yet exist, creating a "new identifier"
390-
// with respect to the collection of identifiers the server is aware of.
391-
isNewIdentifierLocation: true,
392-
entries,
393-
};
394-
}
395-
396-
function getStringLiteralCompletionEntriesFromPropertyAssignment(element: ObjectLiteralElement, sourceFile: SourceFile, typeChecker: TypeChecker, target: ScriptTarget, log: Log): CompletionInfo | undefined {
397-
const type = typeChecker.getContextualType((<ObjectLiteralExpression>element.parent));
398-
const entries: CompletionEntry[] = [];
399-
if (type) {
400-
getCompletionEntriesFromSymbols(type.getApparentProperties(), entries, element, sourceFile, typeChecker, target, log, CompletionKind.String);
401-
if (entries.length) {
402-
return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: true, entries };
403-
}
404-
}
405-
}
406-
407-
function getStringLiteralCompletionEntriesFromCallExpression(argumentInfo: SignatureHelp.ArgumentListInfo, typeChecker: TypeChecker): CompletionInfo | undefined {
408-
const candidates: Signature[] = [];
409-
const entries: CompletionEntry[] = [];
410-
const uniques = createMap<true>();
411-
412-
typeChecker.getResolvedSignature(argumentInfo.invocation, candidates, argumentInfo.argumentCount);
413-
414-
for (const candidate of candidates) {
415-
addStringLiteralCompletionsFromType(typeChecker.getParameterType(candidate, argumentInfo.argumentIndex), entries, typeChecker, uniques);
416-
}
417-
418-
if (entries.length) {
419-
return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: true, entries };
420-
}
421-
422-
return undefined;
423-
}
424-
425-
function getStringLiteralCompletionEntriesFromElementAccessOrIndexedAccess(stringLiteralNode: StringLiteral | NoSubstitutionTemplateLiteral, sourceFile: SourceFile, type: Type, typeChecker: TypeChecker, target: ScriptTarget, log: Log): CompletionInfo | undefined {
426-
const entries: CompletionEntry[] = [];
427-
if (type) {
428-
getCompletionEntriesFromSymbols(type.getApparentProperties(), entries, stringLiteralNode, sourceFile, typeChecker, target, log, CompletionKind.String);
429-
if (entries.length) {
430-
return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: true, entries };
431-
}
432-
}
433-
return undefined;
434-
}
435-
436-
function getStringLiteralCompletionEntriesFromType(type: Type, typeChecker: TypeChecker): CompletionInfo | undefined {
437-
if (type) {
438-
const entries: CompletionEntry[] = [];
439-
addStringLiteralCompletionsFromType(type, entries, typeChecker);
440-
if (entries.length) {
441-
return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, entries };
442-
}
443-
}
444-
return undefined;
445-
}
446-
447-
function getLabelStatementCompletions(node: Node): CompletionEntry[] {
448-
const entries: CompletionEntry[] = [];
449-
const uniques = createMap<true>();
450-
let current = node;
451-
452-
while (current) {
453-
if (isFunctionLike(current)) {
454-
break;
455-
}
456-
if (isLabeledStatement(current)) {
457-
const name = current.label.text;
458-
if (!uniques.has(name)) {
459-
uniques.set(name, true);
460-
entries.push({
461-
name,
462-
kindModifiers: ScriptElementKindModifier.none,
463-
kind: ScriptElementKind.label,
464-
sortText: "0"
465-
});
466-
}
467-
}
468-
current = current.parent;
469-
}
470-
return entries;
471-
}
472-
473-
function addStringLiteralCompletionsFromType(type: Type, result: Push<CompletionEntry>, typeChecker: TypeChecker, uniques = createMap<true>()): void {
447+
function getStringLiteralTypes(type: Type, typeChecker: TypeChecker, uniques = createMap<true>()): ReadonlyArray<StringLiteralType> {
474448
if (type && type.flags & TypeFlags.TypeParameter) {
475-
type = typeChecker.getBaseConstraintOfType(type);
476-
}
477-
if (!type) {
478-
return;
479-
}
480-
if (type.flags & TypeFlags.Union) {
481-
for (const t of (<UnionType>type).types) {
482-
addStringLiteralCompletionsFromType(t, result, typeChecker, uniques);
483-
}
484-
}
485-
else if (type.flags & TypeFlags.StringLiteral && !(type.flags & TypeFlags.EnumLiteral)) {
486-
const name = (<StringLiteralType>type).value;
487-
if (!uniques.has(name)) {
488-
uniques.set(name, true);
489-
result.push({
490-
name,
491-
kindModifiers: ScriptElementKindModifier.none,
492-
kind: ScriptElementKind.variableElement,
493-
sortText: "0"
494-
});
495-
}
449+
type = type.getConstraint();
496450
}
451+
return type && type.flags & TypeFlags.Union
452+
? flatMap((<UnionType>type).types, t => getStringLiteralTypes(t, typeChecker, uniques))
453+
: type && type.flags & TypeFlags.StringLiteral && !(type.flags & TypeFlags.EnumLiteral) && addToSeen(uniques, (type as StringLiteralType).value)
454+
? [type as StringLiteralType]
455+
: emptyArray;
497456
}
498457

499458
interface SymbolCompletion {

0 commit comments

Comments
 (0)