mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
@@ -799,7 +799,7 @@ describe("RedisKeyBrowser command completion", () => {
|
||||
await setCommandInput("VGE");
|
||||
|
||||
expect(mocks.listRedisCompletionCommandDocs).toHaveBeenCalledWith("connection", "0");
|
||||
expect(commandCompletionLabels()).toContain("VGETReads a vendor key.string");
|
||||
expect(commandCompletionLabels()).toEqual(expect.arrayContaining([expect.stringContaining("VGET")]));
|
||||
|
||||
const input = requiredElement<HTMLInputElement>("[data-redis-command-input]");
|
||||
input.dispatchEvent(new KeyboardEvent("keydown", { key: "Tab", shiftKey: true, bubbles: true }));
|
||||
@@ -808,7 +808,7 @@ describe("RedisKeyBrowser command completion", () => {
|
||||
|
||||
input.dispatchEvent(new KeyboardEvent("keydown", { key: "Tab", bubbles: true }));
|
||||
await settle();
|
||||
expect(input.value).toBe("VGET ");
|
||||
expect(input.value).toBe("VGET arg1");
|
||||
});
|
||||
|
||||
it("completes known keys only at a documented key argument", async () => {
|
||||
@@ -929,8 +929,35 @@ describe("RedisKeyBrowser command completion", () => {
|
||||
input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
|
||||
await settle();
|
||||
|
||||
expect(input.value).toBe("SET ");
|
||||
expect(commandCompletionLabels()).toContain("user:1key");
|
||||
expect(input.value).toBe("SET arg1 arg2");
|
||||
expect(mocks.redisExecuteCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("inserts documented Redis argument examples before executing on Enter", async () => {
|
||||
mocks.listRedisCompletionCommandDocs.mockResolvedValueOnce([
|
||||
{
|
||||
name: "GETBIT",
|
||||
group: "bitmap",
|
||||
arity: 3,
|
||||
keySpecs: [{ beginSearch: { type: "index" as const, index: 1 }, findKeys: { type: "range" as const, lastKey: 0, keyStep: 1, limit: 0 } }],
|
||||
arguments: [
|
||||
{ name: "key", type: "key" },
|
||||
{ name: "offset", type: "integer" },
|
||||
],
|
||||
},
|
||||
]);
|
||||
mountBrowser();
|
||||
await settle();
|
||||
await openCommandPanel();
|
||||
await setCommandInput("GETB");
|
||||
|
||||
const input = requiredElement<HTMLInputElement>("[data-redis-command-input]");
|
||||
input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
|
||||
await settle();
|
||||
|
||||
expect(input.value).toBe("GETBIT key offset");
|
||||
expect(mocks.redisExecuteCommand).not.toHaveBeenCalled();
|
||||
expect(commandCompletionLabels()).toEqual([]);
|
||||
});
|
||||
|
||||
it("waits for command metadata instead of sending a partial command on Enter", async () => {
|
||||
@@ -952,7 +979,7 @@ describe("RedisKeyBrowser command completion", () => {
|
||||
await settle();
|
||||
input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
|
||||
await settle();
|
||||
expect(input.value).toBe("SET ");
|
||||
expect(input.value).toBe("SET arg1 arg2");
|
||||
expect(mocks.redisExecuteCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -1792,7 +1792,9 @@ function commandCompletionInsertion(index = commandCompletionSelectedIndex.value
|
||||
const insert = item.apply ?? item.label;
|
||||
const commandHead = context.mode === "command" || context.mode === "subcommand";
|
||||
const appendSpace = (commandHead || item.appendSpace === true) && !/^\s/.test(text.slice(to));
|
||||
return { text, from, to, insert, replacement: `${insert}${appendSpace ? " " : ""}`, appendSpace, commandHead };
|
||||
const hasCommandExample = commandHead && item.apply !== undefined && item.apply !== item.label;
|
||||
const replacement = `${insert}${appendSpace && !hasCommandExample ? " " : ""}`;
|
||||
return { text, from, to, insert, replacement, appendSpace: appendSpace && !hasCommandExample, commandHead };
|
||||
}
|
||||
|
||||
function selectedCompletionMatchesInput(): boolean {
|
||||
@@ -2332,6 +2334,7 @@ defineExpose({ focusSearch, insertCommand, executeCommand: executeAiCommand });
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="block truncate font-mono">{{ item.label }}</span>
|
||||
<span v-if="item.summary" class="block truncate text-[11px] text-slate-400">{{ item.summary }}</span>
|
||||
<span v-if="item.apply && item.apply !== item.label" class="block truncate text-[11px] text-slate-500">{{ item.apply }}</span>
|
||||
</span>
|
||||
<span v-if="item.detail" class="shrink-0 text-slate-400">{{ item.detail }}</span>
|
||||
</button>
|
||||
|
||||
@@ -141,11 +141,52 @@ function buildSpecDetail(entry: CompletionCommandEntry): string {
|
||||
return entry.group;
|
||||
}
|
||||
|
||||
function argumentPlaceholder(argument: RedisCommandArgument): string {
|
||||
const name = argument.name.trim().replaceAll(/[^a-zA-Z0-9_-]+/g, "_");
|
||||
return name || "value";
|
||||
}
|
||||
|
||||
function exampleArguments(arguments_: readonly RedisCommandArgument[]): string[] {
|
||||
const result: string[] = [];
|
||||
for (const argument of arguments_) {
|
||||
if (argument.optional) continue;
|
||||
if (argument.type === "oneof") {
|
||||
const first = argument.arguments?.find((candidate) => !candidate.optional);
|
||||
if (first) result.push(...exampleArgument(first));
|
||||
continue;
|
||||
}
|
||||
result.push(...exampleArgument(argument));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function exampleArgument(argument: RedisCommandArgument): string[] {
|
||||
if (argument.enum?.length) return [argument.enum[0]!];
|
||||
const nested = argument.arguments?.length ? exampleArguments(argument.arguments) : [];
|
||||
const token = argument.token?.trim().toUpperCase();
|
||||
if (argument.type === "pure-token") return token ? [token] : [];
|
||||
if (token && nested.length) return [token, ...nested];
|
||||
if (nested.length) return nested;
|
||||
if (token) return [token, argumentPlaceholder(argument)];
|
||||
return [argumentPlaceholder(argument)];
|
||||
}
|
||||
|
||||
function commandExample(entry: CompletionCommandEntry): string {
|
||||
let arguments_ = entry.arguments.length ? exampleArguments(entry.arguments) : [];
|
||||
if (arguments_.length === 0 && entry.arity) {
|
||||
const count = entry.arity > 0 ? entry.arity - 1 : -entry.arity - 1;
|
||||
arguments_ = Array.from({ length: Math.min(Math.max(count, 0), 8) }, (_, index) => `arg${index + 1}`);
|
||||
}
|
||||
return [entry.name, ...arguments_].join(" ");
|
||||
}
|
||||
|
||||
function buildSpecInfo(entry: CompletionCommandEntry, label: string): string {
|
||||
const info = [];
|
||||
if (entry.summary) info.push(entry.summary);
|
||||
info.push(`Command: ${label}`, `Group: ${entry.group}`, `Arity: ${describeArity(entry.arity)}`);
|
||||
if (entry.since) info.push(`Since: ${entry.since}`);
|
||||
const example = commandExample(entry);
|
||||
if (example !== entry.name) info.push(`Example: ${example}`);
|
||||
return info.join("\n");
|
||||
}
|
||||
|
||||
@@ -216,6 +257,7 @@ function commandItems(index: CompletionIndex, prefix: string): RedisCompletionIt
|
||||
info: buildSpecInfo(entry, entry.name),
|
||||
summary: entry.summary,
|
||||
since: entry.since,
|
||||
apply: commandExample(entry),
|
||||
boost: boostFor(entry),
|
||||
}));
|
||||
return items.sort((a, b) => b.boost - a.boost);
|
||||
@@ -231,6 +273,7 @@ function subcommandItems(index: CompletionIndex, commandPrefix: string, prefix:
|
||||
info: buildSpecInfo(entry, entry.name),
|
||||
summary: entry.summary,
|
||||
since: entry.since,
|
||||
apply: commandExample(entry).slice(commandPrefix.length + 1),
|
||||
boost: boostFor(entry),
|
||||
}));
|
||||
return items.sort((a, b) => b.boost - a.boost);
|
||||
|
||||
+16
@@ -111,6 +111,22 @@ test("command mode completes documented names case-insensitively", () => {
|
||||
assert.ok(!labels(complete("GET")).some((name) => name.includes(" ")));
|
||||
});
|
||||
|
||||
test("command completion inserts a server-documented argument example", () => {
|
||||
const getbit: RedisCommandDocumentation = {
|
||||
name: "GETBIT",
|
||||
group: "bitmap",
|
||||
arity: 3,
|
||||
arguments: [
|
||||
{ name: "key", type: "key" },
|
||||
{ name: "offset", type: "integer" },
|
||||
],
|
||||
keySpecs: oneKey,
|
||||
};
|
||||
const item = buildRedisCompletionItems("GETB", 4, { commands: [getbit] })[0];
|
||||
assert.equal(item?.apply, "GETBIT key offset");
|
||||
assert.match(item?.info ?? "", /Example: GETBIT key offset/);
|
||||
});
|
||||
|
||||
test("subcommand mode follows the server's command hierarchy", () => {
|
||||
const names = labels(complete("XGROUP "));
|
||||
assert.deepEqual(new Set(names), new Set(["CREATE", "DESTROY", "SETID"]));
|
||||
|
||||
Reference in New Issue
Block a user