Skip to content

fix(33377): completions for string literals don't follow quoteStyle preference #36720

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 24 additions & 11 deletions src/services/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ namespace ts.Completions {
}

for (const literal of literals) {
entries.push(createCompletionEntryForLiteral(literal));
entries.push(createCompletionEntryForLiteral(literal, preferences));
}

return { isGlobalCompletion: isInSnippetScope, isMemberCompletion: isMemberCompletionKind(completionKind), isNewIdentifierLocation, entries };
Expand Down Expand Up @@ -317,10 +317,13 @@ namespace ts.Completions {
});
}

const completionNameForLiteral = (literal: string | number | PseudoBigInt) =>
typeof literal === "object" ? pseudoBigIntToString(literal) + "n" : JSON.stringify(literal);
function createCompletionEntryForLiteral(literal: string | number | PseudoBigInt): CompletionEntry {
return { name: completionNameForLiteral(literal), kind: ScriptElementKind.string, kindModifiers: ScriptElementKindModifier.none, sortText: SortText.LocationPriority };
function completionNameForLiteral(literal: string | number | PseudoBigInt, preferences: UserPreferences): string {
return typeof literal === "object" ? pseudoBigIntToString(literal) + "n" :
isString(literal) ? quote(literal, preferences) : JSON.stringify(literal);
}

function createCompletionEntryForLiteral(literal: string | number | PseudoBigInt, preferences: UserPreferences): CompletionEntry {
return { name: completionNameForLiteral(literal, preferences), kind: ScriptElementKind.string, kindModifiers: ScriptElementKindModifier.none, sortText: SortText.LocationPriority };
}

function createCompletionEntry(
Expand All @@ -344,13 +347,13 @@ namespace ts.Completions {
const useBraces = origin && originIsSymbolMember(origin) || needsConvertPropertyAccess;
if (origin && originIsThisType(origin)) {
insertText = needsConvertPropertyAccess
? `this${insertQuestionDot ? "?." : ""}[${quote(name, preferences)}]`
? `this${insertQuestionDot ? "?." : ""}[${quotePropertyName(name, preferences)}]`
: `this${insertQuestionDot ? "?." : "."}${name}`;
}
// We should only have needsConvertPropertyAccess if there's a property access to convert. But see #21790.
// Somehow there was a global with a non-identifier name. Hopefully someone will complain about getting a "foo bar" global completion and provide a repro.
else if ((useBraces || insertQuestionDot) && propertyAccessToConvert) {
insertText = useBraces ? needsConvertPropertyAccess ? `[${quote(name, preferences)}]` : `[${name}]` : name;
insertText = useBraces ? needsConvertPropertyAccess ? `[${quotePropertyName(name, preferences)}]` : `[${name}]` : name;
if (insertQuestionDot || propertyAccessToConvert.questionDotToken) {
insertText = `?.${insertText}`;
}
Expand Down Expand Up @@ -410,6 +413,14 @@ namespace ts.Completions {
};
}

function quotePropertyName(name: string, preferences: UserPreferences): string {
if (/^\d+$/.test(name)) {
return name;
}

return quote(name, preferences);
}

function isRecommendedCompletionMatch(localSymbol: Symbol, recommendedCompletion: Symbol | undefined, checker: TypeChecker): boolean {
return localSymbol === recommendedCompletion ||
!!(localSymbol.flags & SymbolFlags.ExportValue) && checker.getExportSymbolOfSymbol(localSymbol) === recommendedCompletion;
Expand Down Expand Up @@ -531,6 +542,7 @@ namespace ts.Completions {
position: number,
entryId: CompletionEntryIdentifier,
host: LanguageServiceHost,
preferences: UserPreferences,
): SymbolCompletion | { type: "request", request: Request } | { type: "literal", literal: string | number | PseudoBigInt } | { type: "none" } {
const compilerOptions = program.getCompilerOptions();
const completionData = getCompletionData(program, log, sourceFile, isUncheckedFile(sourceFile, compilerOptions), position, { includeCompletionsForModuleExports: true, includeCompletionsWithInsertText: true }, entryId, host);
Expand All @@ -543,7 +555,7 @@ namespace ts.Completions {

const { symbols, literals, location, completionKind, symbolToOriginInfoMap, previousToken, isJsxInitializer, isTypeOnlyLocation } = completionData;

const literal = find(literals, l => completionNameForLiteral(l) === entryId.name);
const literal = find(literals, l => completionNameForLiteral(l, preferences) === entryId.name);
if (literal !== undefined) return { type: "literal", literal };

// Find the symbol with the matching entry name.
Expand Down Expand Up @@ -595,7 +607,7 @@ namespace ts.Completions {
}

// Compute all the completion symbols again.
const symbolCompletion = getSymbolCompletionFromEntryId(program, log, sourceFile, position, entryId, host);
const symbolCompletion = getSymbolCompletionFromEntryId(program, log, sourceFile, position, entryId, host, preferences);
switch (symbolCompletion.type) {
case "request": {
const { request } = symbolCompletion;
Expand All @@ -617,7 +629,7 @@ namespace ts.Completions {
}
case "literal": {
const { literal } = symbolCompletion;
return createSimpleDetails(completionNameForLiteral(literal), ScriptElementKind.string, typeof literal === "string" ? SymbolDisplayPartKind.stringLiteral : SymbolDisplayPartKind.numericLiteral);
return createSimpleDetails(completionNameForLiteral(literal, preferences), ScriptElementKind.string, typeof literal === "string" ? SymbolDisplayPartKind.stringLiteral : SymbolDisplayPartKind.numericLiteral);
}
case "none":
// Didn't find a symbol with this name. See if we can find a keyword instead.
Expand Down Expand Up @@ -687,8 +699,9 @@ namespace ts.Completions {
position: number,
entryId: CompletionEntryIdentifier,
host: LanguageServiceHost,
preferences: UserPreferences,
): Symbol | undefined {
const completion = getSymbolCompletionFromEntryId(program, log, sourceFile, position, entryId, host);
const completion = getSymbolCompletionFromEntryId(program, log, sourceFile, position, entryId, host, preferences);
return completion.type === "symbol" ? completion.symbol : undefined;
}

Expand Down
4 changes: 2 additions & 2 deletions src/services/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1490,9 +1490,9 @@ namespace ts {
);
}

function getCompletionEntrySymbol(fileName: string, position: number, name: string, source?: string): Symbol | undefined {
function getCompletionEntrySymbol(fileName: string, position: number, name: string, source?: string, preferences: UserPreferences = emptyOptions): Symbol | undefined {
synchronizeHostData();
return Completions.getCompletionEntrySymbol(program, log, getValidSourceFile(fileName), position, { name, source }, host);
return Completions.getCompletionEntrySymbol(program, log, getValidSourceFile(fileName), position, { name, source }, host, preferences);
}

function getQuickInfoAtPosition(fileName: string, position: number): QuickInfo | undefined {
Expand Down
3 changes: 0 additions & 3 deletions src/services/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2343,9 +2343,6 @@ namespace ts {
}

export function quote(text: string, preferences: UserPreferences): string {
if (/^\d+$/.test(text)) {
return text;
}
// Editors can pass in undefined or empty string - we want to infer the preference in those cases.
const quotePreference = preferences.quotePreference || "auto";
const quoted = JSON.stringify(text);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/// <reference path='fourslash.ts'/>

////enum A {
//// A,
//// B,
//// C
////}
////interface B {
//// a: keyof typeof A;
////}
////const b: B = {
//// a: /**/
////}

verify.completions({
marker: "",
includes: [
{ name: "'A'" },
{ name: "'B'" },
{ name: "'C'" },
],
preferences: {
quotePreference: 'single',
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/// <reference path='fourslash.ts'/>

////enum A {
//// A,
//// B,
//// C
////}
////interface B {
//// a: keyof typeof A;
////}
////const b: B = {
//// a: /**/
////}

verify.completions({
marker: "",
includes: [
{ name: '"A"' },
{ name: '"B"' },
{ name: '"C"' },
],
preferences: { quotePreference: "double" },
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/// <reference path='fourslash.ts'/>

////const a = {
//// '#': 'a'
////};
////a[|./**/|]

verify.completions({
marker: "",
includes: [
{ name: "#", insertText: "['#']", replacementSpan: test.ranges()[0] },
],
preferences: {
includeInsertTextCompletions: true,
quotePreference: "single",
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/// <reference path='fourslash.ts'/>

////const a = {
//// "#": "a"
////};
////a[|./**/|]

verify.completions({
marker: "",
includes: [
{ name: "#", insertText: '["#"]', replacementSpan: test.ranges()[0] },
],
preferences: {
includeInsertTextCompletions: true,
quotePreference: "double"
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/// <reference path='fourslash.ts'/>

////type T = 0 | 1;
////const t: T = /**/

verify.completions({
marker: "",
includes: [
{ name: "0" },
{ name: "1" },
],
isNewIdentifierLocation: true
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/// <reference path='fourslash.ts'/>

////type T = "0" | "1";
////const t: T = /**/

verify.completions({
marker: "",
includes: [
{ name: "'1'" },
{ name: "'0'" },
],
isNewIdentifierLocation: true,
preferences: { quotePreference: "single" }
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/// <reference path='fourslash.ts'/>

////type T = "0" | "1";
////const t: T = /**/

verify.completions({
marker: "",
includes: [
{ name: '"1"' },
{ name: '"0"' },
],
isNewIdentifierLocation: true,
preferences: { quotePreference: "double" }
});