Skip to content
Merged
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f6c3566
fix services' type's isLiteral
gabritto Sep 23, 2022
4467330
update literal completions tests
gabritto Sep 23, 2022
a206fe1
initial prototype
gabritto Sep 7, 2022
73c1eea
use symbol to expression. TODO: filter existing, replace import nodes
gabritto Sep 16, 2022
5648cba
WIP
gabritto Sep 23, 2022
d46c0d2
WIP
gabritto Sep 28, 2022
297f892
remove booleans from literals
gabritto Sep 28, 2022
fd1d6ed
Merge branch 'gabritto/servicesIsLiteral' into gabritto/switchsnippet
gabritto Sep 28, 2022
4c528b3
trigger at case keyword positions
gabritto Sep 29, 2022
1a5cd05
clean up tests
gabritto Nov 8, 2022
ee42732
fix element access expression case
gabritto Nov 9, 2022
1819d0b
refactor dealing with existing values into a tracker
gabritto Nov 9, 2022
b19543e
Merge branch 'main' into gabritto/switchsnippet
gabritto Nov 10, 2022
bd5b817
fix merge errors
gabritto Nov 10, 2022
f02122b
cleanup and more tests
gabritto Nov 10, 2022
a35bc4a
fix lint errors
gabritto Nov 10, 2022
83b88f7
more merge conflict fixes and cleanup
gabritto Nov 11, 2022
599fb30
use appropriate quotes
gabritto Nov 11, 2022
97dcf69
small indentation fix
gabritto Nov 11, 2022
3b92638
refactor case clause tracker
gabritto Nov 14, 2022
89f6f6b
Merge branch 'main' into gabritto/switchsnippet
gabritto Nov 14, 2022
1894d2e
experiment: support tabstops after each case clause
gabritto Nov 22, 2022
90767fc
address small CR comments
gabritto Nov 23, 2022
d1c8968
fix completion entry details; add test case
gabritto Nov 30, 2022
fb15ba1
Merge branch 'main' into gabritto/switchsnippet
gabritto Nov 30, 2022
3980b93
fix lint errors
gabritto Dec 1, 2022
8823108
remove space before tab stops; refactor
gabritto Dec 1, 2022
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
Prev Previous commit
Next Next commit
trigger at case keyword positions
  • Loading branch information
gabritto committed Sep 29, 2022
commit 4c528b3834784c5d582ade5a2646514185146464
299 changes: 144 additions & 155 deletions src/services/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,13 +266,11 @@ namespace ts.Completions {
}

if (triggerCharacter === " ") {
// `isValidTrigger` ensures we are at `import |` or `case |`.
if (previousToken && isImportKeyword(previousToken)) {
if (preferences.includeCompletionsForImportStatements && preferences.includeCompletionsWithInsertText) {
return { isGlobalCompletion: true, isMemberCompletion: false, isNewIdentifierLocation: true, isIncomplete: true, entries: [] };
}
return undefined;
// `isValidTrigger` ensures we are at `import |`
if (preferences.includeCompletionsForImportStatements && preferences.includeCompletionsWithInsertText) {
return { isGlobalCompletion: true, isMemberCompletion: false, isNewIdentifierLocation: true, isIncomplete: true, entries: [] };
}
return undefined;
}

// If the request is a continuation of an earlier `isIncomplete` response,
Expand Down Expand Up @@ -568,8 +566,8 @@ namespace ts.Completions {
}

// >> TODO: trigger on semi-completed case-keyword
if (contextToken && isCaseKeyword(contextToken) && preferences.includeCompletionsWithInsertText) {
const cases = getExhaustiveCaseSnippets(contextToken, sourceFile, preferences, compilerOptions, host, program, formatContext);
if (contextToken && isCaseBlock(contextToken.parent) && preferences.includeCompletionsWithInsertText) {
const cases = getExhaustiveCaseSnippets(contextToken.parent, sourceFile, preferences, compilerOptions, host, program, formatContext);
if (cases) {
entries.push(cases.entry);
}
Expand All @@ -591,168 +589,160 @@ namespace ts.Completions {
}

function getExhaustiveCaseSnippets(
contextToken: Token<SyntaxKind.CaseKeyword>,
// contextToken: Token<SyntaxKind.CaseKeyword>,
caseBlock: CaseBlock,
sourceFile: SourceFile,
preferences: UserPreferences,
options: CompilerOptions,
host: LanguageServiceHost,
program: Program,
formatContext: formatting.FormatContext | undefined): { entry: CompletionEntry, importAdder: codefix.ImportAdder } | undefined {
const caseClause = tryCast(contextToken.parent, isCaseClause);
if (caseClause) {
// Only offer this completion if we're not positioned *after* a default clause
const clauses = caseClause.parent.clauses;
const defaultClauseIndex = findIndex(clauses, isDefaultClause);
const currentIndex = findIndex(clauses, c => c === caseClause);
if (defaultClauseIndex !== -1 && currentIndex > defaultClauseIndex) {
return undefined;
}
const checker = program.getTypeChecker();
const switchType = getSwitchedType(caseClause, checker);
// >> TODO: use `isTypeLiteral` from checker?
// >> that considers unions of literals, all unit types, and also booleans.
if (switchType && switchType.isUnion() && every(switchType.types, type => type.isLiteral())) {// >> TODO: does this work for enum members? aliases?
const printer = createSnippetPrinter({
removeComments: true,
module: options.module,
target: options.target,
newLine: getNewLineKind(getNewLineCharacter(options, maybeBind(host, host.getNewLine))),
});

const clauses = caseBlock.clauses;
// >> TODO: Only offer this completion if we're not positioned *after* a default clause
// const defaultClauseIndex = findIndex(clauses, isDefaultClause);
// const currentIndex = findIndex(clauses, c => c === caseClause);
// if (defaultClauseIndex !== -1 && currentIndex > defaultClauseIndex) {
// return undefined;
// }
const checker = program.getTypeChecker();
// const switchType = getSwitchedType(caseClause, checker);
const switchType = checker.getTypeAtLocation(caseBlock.parent.expression);
// >> TODO: handle unit type case?
if (switchType && switchType.isUnion() && every(switchType.types, type => type.isLiteral())) {// >> TODO: does this work for enum members? aliases?
const printer = createSnippetPrinter({
removeComments: true,
module: options.module,
target: options.target,
newLine: getNewLineKind(getNewLineCharacter(options, maybeBind(host, host.getNewLine))),
});

// >> TODO: not sure if this is fast enough for filtering existing cases
// const existingSymbols = new Map<SymbolId, true>();
// const existingLiterals: (number | string | PseudoBigInt)[] = [];
// existingLiterals;
// const existingValues: (string | number | PseudoBigInt)[] = [];
const existingStrings = new Set<string>();
const existingNumbers = new Set<number>();
const existingBigInts = new Set<string>();
const existingBools = new Set<boolean>();

for (const clause of clauses) {
if (isDefaultClause(clause)) {
continue;
}
if (isLiteralExpression(clause.expression)) {
const expression = clause.expression;
switch (expression.kind) {
case SyntaxKind.NoSubstitutionTemplateLiteral:
case SyntaxKind.StringLiteral:
existingStrings.add(expression.text);
break;
case SyntaxKind.NumericLiteral:
existingNumbers.add(parseInt(expression.text)); // >> do we need to parse it??
break;
case SyntaxKind.BigIntLiteral:
const parsedBigInt = parseBigInt(endsWith(expression.text, "n") ? expression.text.slice(0, -1) : expression.text);
if (parsedBigInt) {
existingBigInts.add(pseudoBigIntToString(parsedBigInt)); // >> does it work? answer: no
}
break;
case SyntaxKind.TrueKeyword:
existingBools.add(true);
break;
case SyntaxKind.FalseKeyword:
existingBools.add(false);
break;
}
}
else if (checker.getSymbolAtLocation(clause.expression)) {
const symbol = checker.getSymbolAtLocation(clause.expression);
if (symbol && symbol.valueDeclaration && isEnumMember(symbol.valueDeclaration)) {
// TODO: check that it's an enum member symbol
const enumValue = checker.getConstantValue(symbol.valueDeclaration);
if (enumValue !== undefined) {
switch (typeof enumValue) {
case "string":
existingStrings.add(enumValue);
break;
case "number":
existingNumbers.add(enumValue);
}
// >> TODO: not sure if this is fast enough for filtering existing cases
// const existingSymbols = new Map<SymbolId, true>();
// const existingLiterals: (number | string | PseudoBigInt)[] = [];
// existingLiterals;
// const existingValues: (string | number | PseudoBigInt)[] = [];
const existingStrings = new Set<string>();
const existingNumbers = new Set<number>();
const existingBigInts = new Set<string>();
const existingBools = new Set<boolean>();

for (const clause of clauses) {
if (isDefaultClause(clause)) {
continue;
}
if (isLiteralExpression(clause.expression)) {
const expression = clause.expression;
switch (expression.kind) {
case SyntaxKind.NoSubstitutionTemplateLiteral:
case SyntaxKind.StringLiteral:
existingStrings.add(expression.text);
break;
case SyntaxKind.NumericLiteral:
existingNumbers.add(parseInt(expression.text)); // >> do we need to parse it??
break;
case SyntaxKind.BigIntLiteral:
const parsedBigInt = parseBigInt(endsWith(expression.text, "n") ? expression.text.slice(0, -1) : expression.text);
if (parsedBigInt) {
existingBigInts.add(pseudoBigIntToString(parsedBigInt)); // >> does it work? answer: no
}
}
break;
case SyntaxKind.TrueKeyword:
existingBools.add(true);
break;
case SyntaxKind.FalseKeyword:
existingBools.add(false);
break;
}
}

const importAdder = codefix.createImportAdder(sourceFile, program, preferences, host);
const elements: Expression[] = [];
for (const type of switchType.types as LiteralType[]) {
if (type.flags & TypeFlags.EnumLiteral) {
Debug.assert(type.symbol, "TODO: should this hold always?");
Debug.assert(type.symbol.parent, "TODO: should this hold always too?");
// >> TODO: see if we need to filter enum by their constant vals
const target = getEmitScriptTarget(options);
// >> TODO: figure out if need an import action
// >> TODO: fix issue when qualified import
const typeNode = codefix.typeToAutoImportableTypeNode(checker, importAdder, type, caseClause.parent, target);
if (!typeNode) {
return undefined;
}
const typeNodeText = printer.printSnippetList(
ListFormat.None,
factory.createNodeArray([typeNode]),
sourceFile);
typeNodeText;

const expr = foo(typeNode, target);
if (!expr) {
return undefined;
else if (checker.getSymbolAtLocation(clause.expression)) {
const symbol = checker.getSymbolAtLocation(clause.expression);
if (symbol && symbol.valueDeclaration && isEnumMember(symbol.valueDeclaration)) {
const enumValue = checker.getConstantValue(symbol.valueDeclaration);
if (enumValue !== undefined) {
switch (typeof enumValue) {
case "string":
existingStrings.add(enumValue);
break;
case "number":
existingNumbers.add(enumValue);
}
}
elements.push(expr);
// >> TODO: what if expression has import node?
// const expr = checker.symbolToExpression(type.symbol, SymbolFlags.EnumMember, caseClause.parent, /*flags*/ undefined);
// if (expr) {
// return expr;
// }
// }
} // >> TODO: else if boolean???
else {
// const text = completionNameForLiteral(sourceFile, preferences, type.value);
// >> TODO: filter by existing
const literal: Expression = typeof type.value === "object"
? factory.createBigIntLiteral(type.value)
: typeof type.value === "number"
? factory.createNumericLiteral(type.value)
: factory.createStringLiteral(type.value);
elements.push(literal);
}
}
}

if (elements.length === 0) {
return undefined;
const importAdder = codefix.createImportAdder(sourceFile, program, preferences, host);
const elements: Expression[] = [];
for (const type of switchType.types as LiteralType[]) {
if (type.flags & TypeFlags.EnumLiteral) {
Debug.assert(type.symbol, "TODO: should this hold always?");
Debug.assert(type.symbol.parent, "TODO: should this hold always too?");
// >> TODO: see if we need to filter enum by their constant vals
const target = getEmitScriptTarget(options);
// >> TODO: figure out if need an import action
// >> TODO: fix issue when qualified import
const typeNode = codefix.typeToAutoImportableTypeNode(checker, importAdder, type, caseBlock, target);
if (!typeNode) {
return undefined;
}
const expr = foo(typeNode, target);
if (!expr) {
return undefined;
}
elements.push(expr);
// >> TODO: what if expression has import node?
// const expr = checker.symbolToExpression(type.symbol, SymbolFlags.EnumMember, caseClause.parent, /*flags*/ undefined);
// if (expr) {
// return expr;
// }
// }
} // >> TODO: else if boolean???
else {
// const text = completionNameForLiteral(sourceFile, preferences, type.value);
// >> TODO: filter by existing
const literal: Expression = typeof type.value === "object"
? factory.createBigIntLiteral(type.value)
: typeof type.value === "number"
? factory.createNumericLiteral(type.value)
: factory.createStringLiteral(type.value);
elements.push(literal);
}
}

const newClauses = mapDefined(elements, element => {
return factory.createCaseClause(element, []);
});
const insertText = formatContext
? printer.printAndFormatSnippetList(
ListFormat.MultiLine | ListFormat.NoTrailingNewLine,
factory.createNodeArray(newClauses),
sourceFile,
formatContext)
: printer.printSnippetList(
ListFormat.MultiLine | ListFormat.NoTrailingNewLine,
factory.createNodeArray(newClauses),
sourceFile);

const firstClause = printer.printSnippetList(ListFormat.SingleLine, factory.createNodeArray([first(clauses)!]), sourceFile);
return {
entry: {
name: `${firstClause} ...`, // >> TODO: what should this be?
// isRecommended: true, // >> I assume that is ok because if there's another recommended, it will be sorted after this one
kind: ScriptElementKind.unknown, // >> TODO: what should this be?
sortText: SortText.LocalDeclarationPriority, // >> TODO: sort *right after* case keyword
insertText,
replacementSpan: getReplacementSpanForContextToken(contextToken),
hasAction: importAdder.hasFixes() || undefined,
source: CompletionSource.SwitchCases,
},
importAdder,
}
if (elements.length === 0) {
return undefined;
}

const newClauses = map(elements, element => {
return factory.createCaseClause(element, []);
});
const insertText = formatContext
? printer.printAndFormatSnippetList(
ListFormat.MultiLine | ListFormat.NoTrailingNewLine,
factory.createNodeArray(newClauses),
sourceFile,
formatContext)
: printer.printSnippetList(
ListFormat.MultiLine | ListFormat.NoTrailingNewLine,
factory.createNodeArray(newClauses),
sourceFile);

const firstClause = printer.printSnippetList(ListFormat.SingleLine, factory.createNodeArray([first(newClauses)!]), sourceFile);
return {
entry: {
name: `${firstClause} ...`, // >> TODO: what should this be?
// isRecommended: true, // >> I assume that is ok because if there's another recommended, it will be sorted after this one
kind: ScriptElementKind.unknown, // >> TODO: what should this be?
sortText: SortText.LocalDeclarationPriority, // >> TODO: sort *right after* case keyword
insertText,
// replacementSpan: getReplacementSpanForContextToken(contextToken),
hasAction: importAdder.hasFixes() || undefined,
source: CompletionSource.SwitchCases,
},
importAdder,
};
}

return undefined;
Expand Down Expand Up @@ -1887,7 +1877,8 @@ namespace ts.Completions {
}
case "cases": {
const { entry, importAdder } = getExhaustiveCaseSnippets(
contextToken as CaseKeyword,
// contextToken as CaseKeyword,
contextToken!.parent as CaseBlock,
sourceFile,
preferences,
program.getCompilerOptions(),
Expand Down Expand Up @@ -4424,9 +4415,7 @@ namespace ts.Completions {
? !!tryGetImportFromModuleSpecifier(contextToken)
: contextToken.kind === SyntaxKind.SlashToken && isJsxClosingElement(contextToken.parent));
case " ":
return !!contextToken && (
isImportKeyword(contextToken) && contextToken.parent.kind === SyntaxKind.SourceFile ||
contextToken.kind === SyntaxKind.CaseKeyword);
return !!contextToken && isImportKeyword(contextToken) && contextToken.parent.kind === SyntaxKind.SourceFile;
default:
return Debug.assertNever(triggerCharacter);
}
Expand Down