|
|
|
@@ -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 () => {
|
|
|
|
|