Skip to content
Open
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
63 changes: 34 additions & 29 deletions languageservice/src/complete.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,28 +212,31 @@ jobs:
expect(result[0].label).toEqual("my-custom-label");
});

it("does not show parent mapping sibling keys", async () => {
it("does not show mapping keys or parent sibling keys in Key mode", async () => {
// At `container: |`, the scalar form is a string with no constants.
// Mapping keys should NOT be shown - users should use `container (full syntax)`.
const input = `on: push
jobs:
build:
container: |
runs-on: ubuntu-latest`;
const result = await complete(...getPositionFromCursor(input));
expect(result).not.toBeUndefined();
expect(result.length).toEqual(6);
// Should not contain other top-level job keys like `if` and `runs-on`
expect(result.map(x => x.label)).not.toContain("if");
expect(result.map(x => x.label)).not.toContain("runs-on");
// No completions because: scalar has no constants, mapping variant skipped in Key mode
expect(result.length).toEqual(0);
});

it("shows mapping keys within a new map ", async () => {
it("does not show mapping keys in Key mode when structure is uncommitted", async () => {
// At `concurrency: |`, user is in Key mode but hasn't committed to a structure.
// The scalar form is a string with no constants, so no completions.
// Mapping keys are NOT shown - users should use `concurrency (full syntax)` at parent level.
const input = `on: push
jobs:
build:
concurrency: |`;
const result = await complete(...getPositionFromCursor(input));
expect(result).not.toBeUndefined();
expect(result.map(x => x.label).sort()).toEqual(["cancel-in-progress", "group"]);
expect(result.map(x => x.label)).toEqual([]);
});

it("job key", async () => {
Expand Down Expand Up @@ -475,15 +478,15 @@ jobs:
});
});

it("adds a new line and indentation for mapping keys when the key is given", async () => {
it("does not show mapping keys in Key mode for one-of with mapping variant", async () => {
// At `concurrency: |`, mapping keys should NOT be shown.
// Users who want the mapping form should use `concurrency (full syntax)` at parent level.
const input = "concurrency: |";

const result = await complete(...getPositionFromCursor(input));

expect(result.filter(x => x.label === "cancel-in-progress").map(x => x.textEdit?.newText)).toEqual([
"\n cancel-in-progress: "
]);
expect(result.filter(x => x.label === "group").map(x => x.textEdit?.newText)).toEqual(["\n group: "]);
expect(result.filter(x => x.label === "cancel-in-progress")).toEqual([]);
expect(result.filter(x => x.label === "group")).toEqual([]);
});

it("does not add new line if no key in line", async () => {
Expand Down Expand Up @@ -525,8 +528,10 @@ jobs:
expect(result.filter(x => x.label === "types")).toEqual([]);
});

it("shows all options for one-of when user hasn't committed to a type yet", async () => {
// At `permissions: |` user hasn't typed anything yet - show all options
it("shows only scalar options for one-of in Key mode when user hasn't committed to a type", async () => {
// At `permissions: |` user hasn't typed anything yet - show only scalar options
// Mapping keys are NOT shown because they would require a newline
// Users who want the mapping form can use `permissions (full syntax)` at the parent level
const input = "on: push\npermissions: |";

const result = await complete(...getPositionFromCursor(input));
Expand All @@ -535,9 +540,9 @@ jobs:
expect(result.filter(x => x.label === "read-all").map(x => x.textEdit?.newText)).toEqual(["read-all"]);
expect(result.filter(x => x.label === "write-all").map(x => x.textEdit?.newText)).toEqual(["write-all"]);

// Mapping keys should also be available (user hasn't committed yet)
expect(result.filter(x => x.label === "actions").map(x => x.textEdit?.newText)).toEqual(["\n actions: "]);
expect(result.filter(x => x.label === "contents").map(x => x.textEdit?.newText)).toEqual(["\n contents: "]);
// Mapping keys should NOT be shown - they require a newline which is confusing inline
expect(result.filter(x => x.label === "actions")).toEqual([]);
expect(result.filter(x => x.label === "contents")).toEqual([]);
});

it("filters to scalar options when user has started typing a scalar", async () => {
Expand Down Expand Up @@ -603,17 +608,18 @@ jobs:
expect(result.find(x => x.label === "runs-on (full syntax)")?.textEdit?.newText).toEqual("runs-on:\n ");
});

it("generates correct insertText for one-of variants in key mode", async () => {
// concurrency is a one-of: [string, mapping] - testing key mode (after colon on same line)
const input = "concurrency: |";
it("generates correct insertText for one-of variants in parent mode", async () => {
// concurrency is a one-of: [string, mapping] - testing parent mode (inside mapping)
// At `concurrency:\n |`, user HAS committed to mapping structure, so mapping keys are shown
const input = "concurrency:\n |";

const result = await complete(...getPositionFromCursor(input));

// Scalar in key mode: newline + indented key + colon + space
expect(result.find(x => x.label === "group")?.textEdit?.newText).toEqual("\n group: ");
// In parent mode: just key + colon + space (no leading newline)
expect(result.find(x => x.label === "group")?.textEdit?.newText).toEqual("group: ");

// Boolean in key mode (cancel-in-progress): newline + indented key + colon + space
expect(result.find(x => x.label === "cancel-in-progress")?.textEdit?.newText).toEqual("\n cancel-in-progress: ");
// Boolean in parent mode (cancel-in-progress): key + colon + space
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 () => {
Expand Down Expand Up @@ -647,11 +653,10 @@ jobs:
const checkRun = result.find(x => x.label === "check_run");
expect(checkRun?.textEdit?.newText).toEqual("check_run");

// Full syntax form inserts as a mapping key (with newline in Key mode)
// This is expected behavior - it starts the mapping form
const checkRunFull = result.find(x => x.label === "check_run (full syntax)");
// In Key mode: \n + indent + key + : + \n + indent + indent (for nested content)
expect(checkRunFull?.textEdit?.newText).toEqual("\n check_run:\n ");
// 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();
});

it("filters to sequence options when user has started a sequence", async () => {
Expand Down
11 changes: 11 additions & 0 deletions languageservice/src/value-providers/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,17 @@ function oneOfValues(
}
}

// In Key mode (after colon, e.g., `on: |`), only include scalar variants when
// completing an empty value. Mapping/sequence forms require newlines which is
// confusing when typing inline. Users who want those forms can use completions
// like `(full syntax)` or `(list)` at the parent level.
if (!tokenStructure && mode === DefinitionValueMode.Key) {
const variantBucket = getStructuralBucket(variantDef.definitionType);
if (variantBucket !== "scalar") {
continue;
}
}

values.push(...definitionValues(variantDef, indentation, mode, tokenStructure));
}
return distinctValues(values);
Expand Down
Loading