Skip to content

Commit 17b5415

Browse files
committed
Merge pull request #8428 from Microsoft/stringLiteralCompletions
Add support for completions inside string literals
2 parents 6b8109a + cc5dd5b commit 17b5415

File tree

8 files changed

+547
-381
lines changed

8 files changed

+547
-381
lines changed

src/services/breakpoints.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -722,5 +722,5 @@ namespace ts.BreakpointResolver {
722722
return spanInNode(node.parent);
723723
}
724724
}
725-
}
725+
}
726726
}

src/services/services.ts

Lines changed: 105 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,10 +1064,10 @@ namespace ts {
10641064
getCancellationToken?(): HostCancellationToken;
10651065
getCurrentDirectory(): string;
10661066
getDefaultLibFileName(options: CompilerOptions): string;
1067-
log? (s: string): void;
1068-
trace? (s: string): void;
1069-
error? (s: string): void;
1070-
useCaseSensitiveFileNames? (): boolean;
1067+
log?(s: string): void;
1068+
trace?(s: string): void;
1069+
error?(s: string): void;
1070+
useCaseSensitiveFileNames?(): boolean;
10711071

10721072
/*
10731073
* LS host can optionally implement this method if it wants to be completely in charge of module name resolution.
@@ -2499,7 +2499,7 @@ namespace ts {
24992499
}
25002500

25012501
// should be start of dependency list
2502-
if (token !== SyntaxKind.OpenBracketToken) {
2502+
if (token !== SyntaxKind.OpenBracketToken) {
25032503
return true;
25042504
}
25052505

@@ -4032,10 +4032,15 @@ namespace ts {
40324032
}
40334033
}
40344034

4035-
40364035
function getCompletionsAtPosition(fileName: string, position: number): CompletionInfo {
40374036
synchronizeHostData();
40384037

4038+
const sourceFile = getValidSourceFile(fileName);
4039+
4040+
if (isInString(sourceFile, position)) {
4041+
return getStringLiteralCompletionEntries(sourceFile, position);
4042+
}
4043+
40394044
const completionData = getCompletionData(fileName, position);
40404045
if (!completionData) {
40414046
return undefined;
@@ -4048,12 +4053,10 @@ namespace ts {
40484053
return { isMemberCompletion: false, isNewIdentifierLocation: false, entries: getAllJsDocCompletionEntries() };
40494054
}
40504055

4051-
const sourceFile = getValidSourceFile(fileName);
4052-
40534056
const entries: CompletionEntry[] = [];
40544057

40554058
if (isSourceFileJavaScript(sourceFile)) {
4056-
const uniqueNames = getCompletionEntriesFromSymbols(symbols, entries);
4059+
const uniqueNames = getCompletionEntriesFromSymbols(symbols, entries, location, /*performCharacterChecks*/ false);
40574060
addRange(entries, getJavaScriptCompletionEntries(sourceFile, location.pos, uniqueNames));
40584061
}
40594062
else {
@@ -4077,7 +4080,7 @@ namespace ts {
40774080
}
40784081
}
40794082

4080-
getCompletionEntriesFromSymbols(symbols, entries);
4083+
getCompletionEntriesFromSymbols(symbols, entries, location, /*performCharacterChecks*/ true);
40814084
}
40824085

40834086
// Add keywords if this is not a member completion list
@@ -4127,11 +4130,11 @@ namespace ts {
41274130
}));
41284131
}
41294132

4130-
function createCompletionEntry(symbol: Symbol, location: Node): CompletionEntry {
4133+
function createCompletionEntry(symbol: Symbol, location: Node, performCharacterChecks: boolean): CompletionEntry {
41314134
// Try to get a valid display name for this symbol, if we could not find one, then ignore it.
41324135
// We would like to only show things that can be added after a dot, so for instance numeric properties can
41334136
// not be accessed with a dot (a.1 <- invalid)
4134-
const displayName = getCompletionEntryDisplayNameForSymbol(symbol, program.getCompilerOptions().target, /*performCharacterChecks*/ true, location);
4137+
const displayName = getCompletionEntryDisplayNameForSymbol(symbol, program.getCompilerOptions().target, performCharacterChecks, location);
41354138
if (!displayName) {
41364139
return undefined;
41374140
}
@@ -4152,12 +4155,12 @@ namespace ts {
41524155
};
41534156
}
41544157

4155-
function getCompletionEntriesFromSymbols(symbols: Symbol[], entries: CompletionEntry[]): Map<string> {
4158+
function getCompletionEntriesFromSymbols(symbols: Symbol[], entries: CompletionEntry[], location: Node, performCharacterChecks: boolean): Map<string> {
41564159
const start = new Date().getTime();
41574160
const uniqueNames: Map<string> = {};
41584161
if (symbols) {
41594162
for (const symbol of symbols) {
4160-
const entry = createCompletionEntry(symbol, location);
4163+
const entry = createCompletionEntry(symbol, location, performCharacterChecks);
41614164
if (entry) {
41624165
const id = escapeIdentifier(entry.name);
41634166
if (!lookUp(uniqueNames, id)) {
@@ -4171,6 +4174,93 @@ namespace ts {
41714174
log("getCompletionsAtPosition: getCompletionEntriesFromSymbols: " + (new Date().getTime() - start));
41724175
return uniqueNames;
41734176
}
4177+
4178+
function getStringLiteralCompletionEntries(sourceFile: SourceFile, position: number) {
4179+
const node = findPrecedingToken(position, sourceFile);
4180+
if (!node || node.kind !== SyntaxKind.StringLiteral) {
4181+
return undefined;
4182+
}
4183+
4184+
const argumentInfo = SignatureHelp.getContainingArgumentInfo(node, position, sourceFile);
4185+
if (argumentInfo) {
4186+
// Get string literal completions from specialized signatures of the target
4187+
return getStringLiteralCompletionEntriesFromCallExpression(argumentInfo);
4188+
}
4189+
else if (isElementAccessExpression(node.parent) && node.parent.argumentExpression === node) {
4190+
// Get all names of properties on the expression
4191+
return getStringLiteralCompletionEntriesFromElementAccess(node.parent);
4192+
}
4193+
else {
4194+
// Otherwise, get the completions from the contextual type if one exists
4195+
return getStringLiteralCompletionEntriesFromContextualType(<StringLiteral>node);
4196+
}
4197+
}
4198+
4199+
function getStringLiteralCompletionEntriesFromCallExpression(argumentInfo: SignatureHelp.ArgumentListInfo) {
4200+
const typeChecker = program.getTypeChecker();
4201+
const candidates: Signature[] = [];
4202+
const entries: CompletionEntry[] = [];
4203+
4204+
typeChecker.getResolvedSignature(argumentInfo.invocation, candidates);
4205+
4206+
for (const candidate of candidates) {
4207+
if (candidate.parameters.length > argumentInfo.argumentIndex) {
4208+
const parameter = candidate.parameters[argumentInfo.argumentIndex];
4209+
addStringLiteralCompletionsFromType(typeChecker.getTypeAtLocation(parameter.valueDeclaration), entries);
4210+
}
4211+
}
4212+
4213+
if (entries.length) {
4214+
return { isMemberCompletion: false, isNewIdentifierLocation: true, entries };
4215+
}
4216+
4217+
return undefined;
4218+
}
4219+
4220+
function getStringLiteralCompletionEntriesFromElementAccess(node: ElementAccessExpression) {
4221+
const typeChecker = program.getTypeChecker();
4222+
const type = typeChecker.getTypeAtLocation(node.expression);
4223+
const entries: CompletionEntry[] = [];
4224+
if (type) {
4225+
getCompletionEntriesFromSymbols(type.getApparentProperties(), entries, node, /*performCharacterChecks*/false);
4226+
if (entries.length) {
4227+
return { isMemberCompletion: true, isNewIdentifierLocation: true, entries };
4228+
}
4229+
}
4230+
return undefined;
4231+
}
4232+
4233+
function getStringLiteralCompletionEntriesFromContextualType(node: StringLiteral) {
4234+
const typeChecker = program.getTypeChecker();
4235+
const type = typeChecker.getContextualType(node);
4236+
if (type) {
4237+
const entries: CompletionEntry[] = [];
4238+
addStringLiteralCompletionsFromType(type, entries);
4239+
if (entries.length) {
4240+
return { isMemberCompletion: false, isNewIdentifierLocation: false, entries };
4241+
}
4242+
}
4243+
return undefined;
4244+
}
4245+
4246+
function addStringLiteralCompletionsFromType(type: Type, result: CompletionEntry[]): void {
4247+
if (!type) {
4248+
return;
4249+
}
4250+
if (type.flags & TypeFlags.Union) {
4251+
forEach((<UnionType>type).types, t => addStringLiteralCompletionsFromType(t, result));
4252+
}
4253+
else {
4254+
if (type.flags & TypeFlags.StringLiteral) {
4255+
result.push({
4256+
name: (<StringLiteralType>type).text,
4257+
kindModifiers: ScriptElementKindModifier.none,
4258+
kind: ScriptElementKind.variableElement,
4259+
sortText: "0"
4260+
});
4261+
}
4262+
}
4263+
}
41744264
}
41754265

41764266
function getCompletionEntryDetails(fileName: string, position: number, entryName: string): CompletionEntryDetails {
@@ -4335,7 +4425,7 @@ namespace ts {
43354425
// try get the call/construct signature from the type if it matches
43364426
let callExpression: CallExpression;
43374427
if (location.kind === SyntaxKind.CallExpression || location.kind === SyntaxKind.NewExpression) {
4338-
callExpression = <CallExpression> location;
4428+
callExpression = <CallExpression>location;
43394429
}
43404430
else if (isCallExpressionTarget(location) || isNewExpressionTarget(location)) {
43414431
callExpression = <CallExpression>location.parent;

0 commit comments

Comments
 (0)