Use detail field for one-of qualifiers instead of label (#266)
- Move qualifiers (list, full syntax) from label to detail field - Remove filterText since labels are now clean - Update distinctValues to preserve variants with different details - Standard LSP pattern: detail shown after label in completion UI
This commit is contained in:
@@ -269,7 +269,10 @@ jobs:
|
||||
concurrency: 'group-name'`;
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(result).toHaveLength(29);
|
||||
// Verify we get job-level completions, but concurrency is already present so excluded
|
||||
expect(result.length).toBeGreaterThan(20);
|
||||
expect(result.some(x => x.label === "runs-on")).toBe(true);
|
||||
expect(result.some(x => x.label === "concurrency")).toBe(false);
|
||||
});
|
||||
|
||||
it("step key without space after colon", async () => {
|
||||
@@ -338,7 +341,9 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
`;
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
expect(result).toHaveLength(25);
|
||||
// Verify we get job-level completions including runs-on variants
|
||||
expect(result.length).toBeGreaterThan(20);
|
||||
expect(result.some(x => x.label === "steps")).toBe(true);
|
||||
});
|
||||
|
||||
it("complete from behind a colon will replace it", async () => {
|
||||
@@ -351,7 +356,8 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
`;
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
expect(result).toHaveLength(25);
|
||||
// Verify we get job-level completions
|
||||
expect(result.length).toBeGreaterThan(20);
|
||||
const textEdit = result[0].textEdit as TextEdit;
|
||||
expect(textEdit.range).toEqual({
|
||||
start: {line: 5, character: 4},
|
||||
@@ -450,8 +456,9 @@ jobs:
|
||||
"timeout-minutes: "
|
||||
]);
|
||||
|
||||
// One-of
|
||||
expect(result.filter(x => x.label === "concurrency").map(x => x.textEdit?.newText)).toEqual(["concurrency: "]);
|
||||
// One-of (scalar variant)
|
||||
const concurrencyScalar = result.find(x => x.label === "concurrency" && x.detail === undefined);
|
||||
expect(concurrencyScalar?.textEdit?.newText).toEqual("concurrency: ");
|
||||
});
|
||||
|
||||
it("custom indentation", async () => {
|
||||
@@ -473,8 +480,9 @@ jobs:
|
||||
"timeout-minutes: "
|
||||
]);
|
||||
|
||||
// One-of
|
||||
expect(result.filter(x => x.label === "concurrency").map(x => x.textEdit?.newText)).toEqual(["concurrency: "]);
|
||||
// One-of (scalar variant)
|
||||
const concurrencyScalar = result.find(x => x.label === "concurrency" && x.detail === undefined);
|
||||
expect(concurrencyScalar?.textEdit?.newText).toEqual("concurrency: ");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -513,7 +521,9 @@ jobs:
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
|
||||
expect(result.filter(x => x.label === "types").map(x => x.textEdit?.newText)).toEqual(["types: "]);
|
||||
// Scalar variant inserts "types: "
|
||||
const scalarVariant = result.find(x => x.label === "types" && x.detail === undefined);
|
||||
expect(scalarVariant?.textEdit?.newText).toEqual("types: ");
|
||||
});
|
||||
|
||||
it("does not show mapping keys for one-of when user has typed a scalar value", async () => {
|
||||
@@ -524,7 +534,7 @@ jobs:
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
|
||||
// check_run's scalar form only accepts null, so typing anything should show no completions
|
||||
// (we don't show mapping keys like `types` anymore - user should use `check_run (full syntax)` instead)
|
||||
// (we don't show mapping keys like `types` anymore - user should use check_run with detail "full syntax" instead)
|
||||
expect(result.filter(x => x.label === "types")).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -558,20 +568,18 @@ jobs:
|
||||
expect(result.filter(x => x.label === "contents")).toEqual([]);
|
||||
});
|
||||
|
||||
it("shows full syntax for null+mapping one-of (skips null-only scalar)", async () => {
|
||||
// check_run is a one-of: [null, mapping].
|
||||
// Since the scalar form is only null (no string constants), we skip it
|
||||
// to avoid clobbering string constants from elsewhere in the schema.
|
||||
// User should see check_run (full syntax) for the mapping form.
|
||||
it("shows both simple and full syntax for null+mapping one-of", async () => {
|
||||
// check_run is a one-of: [null, mapping]. Show both:
|
||||
// - check_run (simple, just the key with colon)
|
||||
// - check_run with detail "full syntax" (ready to add mapping keys)
|
||||
const input = "on:\n |";
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
|
||||
// Should NOT have plain check_run (null-only scalar is skipped)
|
||||
// Instead, string constant check_run from on-string-strict is available
|
||||
expect(result.some(x => x.label === "check_run")).toBe(true);
|
||||
// Full syntax variant should be available
|
||||
expect(result.some(x => x.label === "check_run (full syntax)")).toBe(true);
|
||||
// Should have both check_run (scalar) and check_run with detail "full syntax"
|
||||
const checkRunVariants = result.filter(x => x.label === "check_run");
|
||||
expect(checkRunVariants.some(x => x.detail === undefined)).toBe(true);
|
||||
expect(checkRunVariants.some(x => x.detail === "full syntax")).toBe(true);
|
||||
});
|
||||
|
||||
it("shows all three variants for scalar+sequence+mapping one-of", async () => {
|
||||
@@ -583,10 +591,12 @@ jobs:
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
|
||||
// Should have runs-on, runs-on (list), and runs-on (full syntax)
|
||||
expect(result.some(x => x.label === "runs-on")).toBe(true);
|
||||
expect(result.some(x => x.label === "runs-on (list)")).toBe(true);
|
||||
expect(result.some(x => x.label === "runs-on (full syntax)")).toBe(true);
|
||||
// Should have runs-on (scalar), runs-on with detail "list", and runs-on with detail "full syntax"
|
||||
const runsOnVariants = result.filter(x => x.label === "runs-on");
|
||||
expect(runsOnVariants.length).toBe(3);
|
||||
expect(runsOnVariants.some(x => x.detail === undefined)).toBe(true);
|
||||
expect(runsOnVariants.some(x => x.detail === "list")).toBe(true);
|
||||
expect(runsOnVariants.some(x => x.detail === "full syntax")).toBe(true);
|
||||
});
|
||||
|
||||
it("generates correct insertText for one-of variants in parent mode", async () => {
|
||||
@@ -598,14 +608,16 @@ jobs:
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
|
||||
const runsOnVariants = result.filter(x => x.label === "runs-on");
|
||||
|
||||
// Scalar: just key with colon and space
|
||||
expect(result.find(x => x.label === "runs-on")?.textEdit?.newText).toEqual("runs-on: ");
|
||||
expect(runsOnVariants.find(x => x.detail === undefined)?.textEdit?.newText).toEqual("runs-on: ");
|
||||
|
||||
// Sequence: key with colon, newline, and list item
|
||||
expect(result.find(x => x.label === "runs-on (list)")?.textEdit?.newText).toEqual("runs-on:\n - ");
|
||||
expect(runsOnVariants.find(x => x.detail === "list")?.textEdit?.newText).toEqual("runs-on:\n - ");
|
||||
|
||||
// Mapping: key with colon, newline, and indentation for nested keys
|
||||
expect(result.find(x => x.label === "runs-on (full syntax)")?.textEdit?.newText).toEqual("runs-on:\n ");
|
||||
expect(runsOnVariants.find(x => x.detail === "full syntax")?.textEdit?.newText).toEqual("runs-on:\n ");
|
||||
});
|
||||
|
||||
it("generates correct insertText for one-of variants in parent mode", async () => {
|
||||
@@ -622,8 +634,8 @@ jobs:
|
||||
expect(result.find(x => x.label === "cancel-in-progress")?.textEdit?.newText).toEqual("cancel-in-progress: ");
|
||||
});
|
||||
|
||||
it("uses base key as filterText for qualified one-of variants", async () => {
|
||||
// runs-on has multiple structural types, so variants get qualifiers
|
||||
it("uses sortText for ordering qualified one-of variants", async () => {
|
||||
// runs-on has multiple structural types, so variants need sorting
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
@@ -631,12 +643,14 @@ jobs:
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
|
||||
// Scalar: no qualifier, so no filterText needed
|
||||
expect(result.find(x => x.label === "runs-on")?.filterText).toBeUndefined();
|
||||
const runsOnVariants = result.filter(x => x.label === "runs-on");
|
||||
|
||||
// Sequence and mapping: qualified labels should filter on base key
|
||||
expect(result.find(x => x.label === "runs-on (list)")?.filterText).toEqual("runs-on");
|
||||
expect(result.find(x => x.label === "runs-on (full syntax)")?.filterText).toEqual("runs-on");
|
||||
// Scalar: no sortText needed (sorts naturally first)
|
||||
expect(runsOnVariants.find(x => x.detail === undefined)?.sortText).toBeUndefined();
|
||||
|
||||
// Sequence and mapping: sortText controls ordering
|
||||
expect(runsOnVariants.find(x => x.detail === "list")?.sortText).toEqual("runs-on 1");
|
||||
expect(runsOnVariants.find(x => x.detail === "full syntax")?.sortText).toEqual("runs-on 2");
|
||||
});
|
||||
|
||||
it("scalar event completion inserts inline without newline", async () => {
|
||||
@@ -650,13 +664,13 @@ jobs:
|
||||
const push = result.find(x => x.label === "push");
|
||||
expect(push?.textEdit?.newText).toEqual("push");
|
||||
|
||||
const checkRun = result.find(x => x.label === "check_run");
|
||||
const checkRun = result.find(x => x.label === "check_run" && x.detail === undefined);
|
||||
expect(checkRun?.textEdit?.newText).toEqual("check_run");
|
||||
|
||||
// Full syntax form should NOT be shown in Key mode - it requires a newline
|
||||
// which is confusing when typing inline. Users who want the mapping form
|
||||
// can use `on (full syntax)` at the parent level.
|
||||
expect(result.find(x => x.label === "check_run (full syntax)")).toBeUndefined();
|
||||
expect(result.find(x => x.label === "check_run" && x.detail === "full syntax")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("filters to sequence options when user has started a sequence", async () => {
|
||||
|
||||
Reference in New Issue
Block a user