Use labelDetails for completion item qualifiers (#278)

* Use labelDetails for completion item qualifiers

Use labelDetails.description instead of detail for qualifier text like
'full syntax' and 'list'. This renders the text inline after the label
in the completion menu, making variants immediately distinguishable
without hovering.

* Fix formatting
This commit is contained in:
eric sciple
2026-01-02 12:18:53 -06:00
committed by GitHub
parent d2ffb50a92
commit e3b56c2416
5 changed files with 32 additions and 26 deletions
+20 -16
View File
@@ -465,7 +465,7 @@ jobs:
]);
// One-of (scalar variant)
const concurrencyScalar = result.find(x => x.label === "concurrency" && x.detail === undefined);
const concurrencyScalar = result.find(x => x.label === "concurrency" && x.labelDetails === undefined);
expect(concurrencyScalar?.textEdit?.newText).toEqual("concurrency: ");
});
@@ -489,7 +489,7 @@ jobs:
]);
// One-of (scalar variant)
const concurrencyScalar = result.find(x => x.label === "concurrency" && x.detail === undefined);
const concurrencyScalar = result.find(x => x.label === "concurrency" && x.labelDetails === undefined);
expect(concurrencyScalar?.textEdit?.newText).toEqual("concurrency: ");
});
});
@@ -530,7 +530,7 @@ jobs:
const result = await complete(...getPositionFromCursor(input));
// Scalar variant inserts "types: "
const scalarVariant = result.find(x => x.label === "types" && x.detail === undefined);
const scalarVariant = result.find(x => x.label === "types" && x.labelDetails === undefined);
expect(scalarVariant?.textEdit?.newText).toEqual("types: ");
});
@@ -586,8 +586,8 @@ jobs:
// 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);
expect(checkRunVariants.some(x => x.labelDetails === undefined)).toBe(true);
expect(checkRunVariants.some(x => x.labelDetails?.description === "full syntax")).toBe(true);
});
it("shows all three variants for scalar+sequence+mapping one-of", async () => {
@@ -602,9 +602,9 @@ jobs:
// 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);
expect(runsOnVariants.some(x => x.labelDetails === undefined)).toBe(true);
expect(runsOnVariants.some(x => x.labelDetails?.description === "list")).toBe(true);
expect(runsOnVariants.some(x => x.labelDetails?.description === "full syntax")).toBe(true);
});
it("generates correct insertText for one-of variants in parent mode", async () => {
@@ -619,13 +619,17 @@ jobs:
const runsOnVariants = result.filter(x => x.label === "runs-on");
// Scalar: just key with colon and space
expect(runsOnVariants.find(x => x.detail === undefined)?.textEdit?.newText).toEqual("runs-on: ");
expect(runsOnVariants.find(x => x.labelDetails === undefined)?.textEdit?.newText).toEqual("runs-on: ");
// Sequence: key with colon, newline, and list item
expect(runsOnVariants.find(x => x.detail === "list")?.textEdit?.newText).toEqual("runs-on:\n - ");
expect(runsOnVariants.find(x => x.labelDetails?.description === "list")?.textEdit?.newText).toEqual(
"runs-on:\n - "
);
// Mapping: key with colon, newline, and indentation for nested keys
expect(runsOnVariants.find(x => x.detail === "full syntax")?.textEdit?.newText).toEqual("runs-on:\n ");
expect(runsOnVariants.find(x => x.labelDetails?.description === "full syntax")?.textEdit?.newText).toEqual(
"runs-on:\n "
);
});
it("generates correct insertText for one-of variants in parent mode", async () => {
@@ -654,11 +658,11 @@ jobs:
const runsOnVariants = result.filter(x => x.label === "runs-on");
// Scalar: no sortText needed (sorts naturally first)
expect(runsOnVariants.find(x => x.detail === undefined)?.sortText).toBeUndefined();
expect(runsOnVariants.find(x => x.labelDetails === 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");
expect(runsOnVariants.find(x => x.labelDetails?.description === "list")?.sortText).toEqual("runs-on 1");
expect(runsOnVariants.find(x => x.labelDetails?.description === "full syntax")?.sortText).toEqual("runs-on 2");
});
it("scalar event completion inserts inline without newline", async () => {
@@ -672,13 +676,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" && x.detail === undefined);
const checkRun = result.find(x => x.label === "check_run" && x.labelDetails === 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" && x.detail === "full syntax")).toBeUndefined();
expect(result.find(x => x.label === "check_run" && x.labelDetails?.description === "full syntax")).toBeUndefined();
});
it("filters to sequence options when user has started a sequence", async () => {
+1 -1
View File
@@ -194,7 +194,7 @@ export async function complete(
const item: CompletionItem = {
label: value.label,
detail: value.detail,
labelDetails: value.labelDetail ? {description: value.labelDetail} : undefined,
filterText: value.filterText,
sortText: value.sortText,
documentation: value.description && {
+3 -1
View File
@@ -22,7 +22,9 @@ describe("end-to-end", () => {
expect(result).not.toBeUndefined();
expect(result.length).toEqual(13);
const labelsWithDetails = result.map(x => (x.detail ? `${x.label} (${x.detail})` : x.label));
const labelsWithDetails = result.map(x =>
x.labelDetails?.description ? `${x.label} (${x.labelDetails.description})` : x.label
);
expect(labelsWithDetails).toEqual([
"concurrency",
"concurrency (full syntax)",
@@ -7,8 +7,8 @@ export interface Value {
/** Optional description to show when auto-completing */
description?: string;
/** Optional detail shown after the label, e.g. type or kind information */
detail?: string;
/** Optional qualifier shown inline after the label, e.g. "full syntax" or "list" */
labelDetail?: string;
/** Whether this value is deprecated */
deprecated?: boolean;
@@ -216,14 +216,14 @@ function oneOfValues(
}
/**
* Deduplicates values by label and detail.
* Values with the same label but different details are preserved as distinct items.
* Deduplicates values by label and labelDetail.
* Values with the same label but different labelDetails are preserved as distinct items.
*/
function distinctValues(values: Value[]): Value[] {
const map = new Map<string, Value>();
for (const value of values) {
// Include detail in the key to preserve variants with different details
const key = value.detail ? `${value.label}\0${value.detail}` : value.label;
// Include labelDetail in the key to preserve variants with different details
const key = value.labelDetail ? `${value.label}\0${value.labelDetail}` : value.label;
map.set(key, value);
}
return Array.from(map.values());
@@ -326,7 +326,7 @@ function expandOneOfToCompletions(
results.push({
label: key,
description,
detail: needsQualifier ? "list" : undefined,
labelDetail: needsQualifier ? "list" : undefined,
insertText,
sortText: needsQualifier ? `${key} 1` : undefined
});
@@ -340,7 +340,7 @@ function expandOneOfToCompletions(
results.push({
label: key,
description,
detail: needsQualifier ? "full syntax" : undefined,
labelDetail: needsQualifier ? "full syntax" : undefined,
insertText,
sortText: needsQualifier ? `${key} 2` : undefined
});