Exhaustive case completion for switch statements (#50996)

* fix services' type's isLiteral

* update literal completions tests

* initial prototype

* use symbol to expression. TODO: filter existing, replace import nodes

* WIP

* WIP

* remove booleans from literals

* trigger at case keyword positions

* clean up tests

* fix element access expression case

* refactor dealing with existing values into a tracker

* fix merge errors

* cleanup and more tests

* fix lint errors

* more merge conflict fixes and cleanup

* use appropriate quotes

* small indentation fix

* refactor case clause tracker

* experiment: support tabstops after each case clause

* address small CR comments

* fix completion entry details; add test case

* fix lint errors

* remove space before tab stops; refactor
This commit is contained in:
Gabriela Araujo Britto
2022-12-01 21:48:32 -03:00
committed by GitHub
parent 5435efbf37
commit 6a3c9ea125
10 changed files with 925 additions and 42 deletions

View File

@@ -0,0 +1,107 @@
/// <reference path="fourslash.ts" />
// Basic tests
// @newline: LF
//// enum E {
//// A = 0,
//// B = "B",
//// C = "C",
//// }
//// // Mixed union
//// declare const u: E.A | E.B | 1;
//// switch (u) {
//// case/*1*/
//// }
//// // Union enum
//// declare const e: E;
//// switch (e) {
//// case/*2*/
//// }
//// enum F {
//// D = 1 << 0,
//// E = 1 << 1,
//// F = 1 << 2,
//// }
////
//// declare const f: F;
//// switch (f) {
//// case/*3*/
//// }
verify.completions(
{
marker: "1",
isNewIdentifierLocation: false,
includes: [
{
name: "case E.A: ...",
source: completion.CompletionSource.SwitchCases,
sortText: completion.SortText.GlobalsOrKeywords,
insertText:
`case E.A:
case E.B:
case 1:`,
},
],
preferences: {
includeCompletionsWithInsertText: true,
},
},
{
marker: "2",
isNewIdentifierLocation: false,
includes: [
{
name: "case E.A: ...",
source: completion.CompletionSource.SwitchCases,
sortText: completion.SortText.GlobalsOrKeywords,
insertText:
`case E.A:
case E.B:
case E.C:`,
},
],
preferences: {
includeCompletionsWithInsertText: true,
},
},
{
marker: "3",
isNewIdentifierLocation: false,
includes: [
{
name: "case F.D: ...",
source: completion.CompletionSource.SwitchCases,
sortText: completion.SortText.GlobalsOrKeywords,
insertText:
`case F.D:
case F.E:
case F.F:`,
},
],
preferences: {
includeCompletionsWithInsertText: true,
},
},
{
marker: "3",
isNewIdentifierLocation: false,
includes: [
{
name: "case F.D: ...",
source: completion.CompletionSource.SwitchCases,
sortText: completion.SortText.GlobalsOrKeywords,
isSnippet: true,
insertText:
`case F.D:$1
case F.E:$2
case F.F:$3`,
},
],
preferences: {
includeCompletionsWithInsertText: true,
includeCompletionsWithSnippetText: true,
},
},
);

View File

@@ -0,0 +1,77 @@
/// <reference path="fourslash.ts" />
// Import-related cases
// @newline: LF
// @Filename: /dep.ts
//// export enum E {
//// A = 0,
//// B = "B",
//// C = "C",
//// }
//// declare const u: E.A | E.B | 1;
//// export { u };
// @Filename: /main.ts
//// import { u } from "./dep";
//// switch (u) {
//// case/*1*/
//// }
// @Filename: /other.ts
//// import * as d from "./dep";
//// declare const u: d.E;
//// switch (u) {
//// case/*2*/
//// }
verify.completions(
{
marker: "1",
isNewIdentifierLocation: false,
includes: [
{
name: "case E.A: ...",
source: completion.CompletionSource.SwitchCases,
sortText: completion.SortText.GlobalsOrKeywords,
insertText:
`case E.A:
case E.B:
case 1:`,
hasAction: true,
},
],
preferences: {
includeCompletionsWithInsertText: true,
},
},
{
marker: "2",
isNewIdentifierLocation: false,
includes: [
{
name: "case d.E.A: ...",
source: completion.CompletionSource.SwitchCases,
sortText: completion.SortText.GlobalsOrKeywords,
insertText:
`case d.E.A:
case d.E.B:
case d.E.C:`,
},
],
preferences: {
includeCompletionsWithInsertText: true,
},
},
);
verify.applyCodeActionFromCompletion("1", {
name: "case E.A: ...",
source: "SwitchCases/",
description: "Includes imports of types referenced by 'case E.A: ...'",
newFileContent:
`import { E, u } from "./dep";
switch (u) {
case
}`,
});

View File

@@ -0,0 +1,112 @@
/// <reference path="fourslash.ts" />
// Where the exhaustive case completion appears or not.
// @newline: LF
// @Filename: /main.ts
//// enum E {
//// A = 0,
//// B = "B",
//// C = "C",
//// }
//// declare const u: E;
//// switch (u) {
//// case/*1*/
//// }
//// switch (u) {
//// /*2*/
//// }
//// switch (u) {
//// case 1:
//// /*3*/
//// }
//// switch (u) {
//// c/*4*/
//// }
//// switch (u) {
//// case /*5*/
//// }
//// /*6*/
//// switch (u) {
//// /*7*/
////
const exhaustiveCaseCompletion = {
name: "case E.A: ...",
source: completion.CompletionSource.SwitchCases,
sortText: completion.SortText.GlobalsOrKeywords,
insertText:
`case E.A:
case E.B:
case E.C:`,
};
verify.completions(
{
marker: "1",
isNewIdentifierLocation: false,
includes: [
exhaustiveCaseCompletion,
],
preferences: {
includeCompletionsWithInsertText: true,
},
},
{
marker: "2",
includes: [
exhaustiveCaseCompletion,
],
preferences: {
includeCompletionsWithInsertText: true,
}
},
{
marker: "3",
includes: [
exhaustiveCaseCompletion,
],
preferences: {
includeCompletionsWithInsertText: true,
}
},
{
marker: "4",
includes: [
exhaustiveCaseCompletion,
],
preferences: {
includeCompletionsWithInsertText: true,
}
},
{
marker: "5",
includes: [
exhaustiveCaseCompletion,
],
preferences: {
includeCompletionsWithInsertText: true,
}
},
{
marker: "6",
exact: [
"E",
"u",
...completion.globals,
exhaustiveCaseCompletion,
],
preferences: {
includeCompletionsWithInsertText: true,
}
},
{
marker: "7",
includes: [
exhaustiveCaseCompletion,
],
preferences: {
includeCompletionsWithInsertText: true,
}
},
);

View File

@@ -0,0 +1,179 @@
/// <reference path="fourslash.ts" />
// Filter existing values.
// @newline: LF
//// enum E {
//// A = 0,
//// B = "B",
//// C = "C",
//// }
//// // Filtering existing literals
//// declare const u: E.A | E.B | 1 | 1n | "1";
//// switch (u) {
//// case E.A:
//// case 1:
//// case 1n:
//// case 0x1n:
//// case "1":
//// case `1`:
//// case `1${u}`:
//// case/*1*/
//// }
//// declare const v: E.A | "1" | "2";
//// switch (v) {
//// case 0:
//// case `1`:
//// /*2*/
//// }
//// // Filtering repreated enum members
//// enum F {
//// A = "A",
//// B = "B",
//// C = A,
//// }
//// declare const x: F;
//// switch (x) {
//// /*3*/
//// }
//// // Enum with computed elements
//// enum G {
//// C = 0,
//// D = 1 << 1,
//// E = 1 << 2,
//// OtherD = D,
//// DorE = D | E,
//// }
//// declare const y: G;
//// switch (y) {
//// /*4*/
//// }
//// switch (y) {
//// case 0: // same as G.C
//// case 1: // same as G.D, but we don't know it
//// case 3: // same as G.DorE, but we don't know
//// /*5*/
//// }
////
//// // Already exhaustive switch
//// enum H {
//// A = "A",
//// B = "B",
//// C = "C",
//// }
//// declare const z: H;
//// switch (z) {
//// case H.A:
//// case H.B:
//// case H.C:
//// /*6*/
//// }
verify.completions(
{
marker: "1",
isNewIdentifierLocation: false,
includes: [
{
name: "case E.B: ...",
source: completion.CompletionSource.SwitchCases,
sortText: completion.SortText.GlobalsOrKeywords,
insertText:
`case E.B:`,
},
],
preferences: {
includeCompletionsWithInsertText: true,
},
},
{
marker: "2",
isNewIdentifierLocation: false,
includes: [
{
name: `case "2": ...`,
source: completion.CompletionSource.SwitchCases,
sortText: completion.SortText.GlobalsOrKeywords,
insertText:
`case "2":`,
},
],
preferences: {
includeCompletionsWithInsertText: true,
},
},
{
marker: "3",
isNewIdentifierLocation: false,
includes: [
{
name: "case F.A: ...",
source: completion.CompletionSource.SwitchCases,
sortText: completion.SortText.GlobalsOrKeywords,
insertText:
`case F.A:
case F.B:`, // no C because C's value is the same as A's
},
],
preferences: {
includeCompletionsWithInsertText: true,
},
},
{
marker: "4",
isNewIdentifierLocation: false,
includes: [
{
name: "case G.C: ...",
source: completion.CompletionSource.SwitchCases,
sortText: completion.SortText.GlobalsOrKeywords,
insertText:
`case G.C:
case G.D:
case G.E:
case G.DorE:`,
},
],
preferences: {
includeCompletionsWithInsertText: true,
},
},
{
marker: "5",
isNewIdentifierLocation: false,
includes: [
{
name: "case G.D: ...",
source: completion.CompletionSource.SwitchCases,
sortText: completion.SortText.GlobalsOrKeywords,
insertText:
`case G.D:
case G.E:
case G.DorE:`,
},
],
preferences: {
includeCompletionsWithInsertText: true,
},
},
{
marker: "6",
isNewIdentifierLocation: false,
// No exhaustive case completion offered here because the switch is already exhaustive
exact: [
"E",
"F",
"G",
"H",
"u",
"v",
"x",
"y",
"z",
...completion.globals,
],
preferences: {
includeCompletionsWithInsertText: true,
},
},
);

View File

@@ -0,0 +1,35 @@
/// <reference path="fourslash.ts" />
// Filter existing values.
// @newline: LF
//// enum P {
//// " Space",
//// Bar,
//// }
////
//// declare const p: P;
////
//// switch (p) {
//// /*1*/
//// }
verify.completions(
{
marker: "1",
isNewIdentifierLocation: false,
includes: [
{
name: `case P[" Space"]: ...`,
source: completion.CompletionSource.SwitchCases,
sortText: completion.SortText.GlobalsOrKeywords,
insertText:
`case P[" Space"]:
case P.Bar:`,
},
],
preferences: {
includeCompletionsWithInsertText: true,
},
},
);

View File

@@ -884,6 +884,7 @@ declare namespace completion {
ClassMemberSnippet = "ClassMemberSnippet/",
TypeOnlyAlias = "TypeOnlyAlias/",
ObjectLiteralMethodSnippet = "ObjectLiteralMethodSnippet/",
SwitchCases = "SwitchCases/",
}
export const globalThisEntry: Entry;
export const undefinedVarEntry: Entry;