Skip to content

Commit 90a9ea4

Browse files
aesopovCopilot
andcommitted
feat(terminal): add unit test for TerminalSession capabilities
refactor(ui-focus): consolidate FocusLayer type definition refactor(autocomplete): streamline command registration in AutocompleteInput refactor(nested-popover-menu): simplify command registration in NestedPopoverMenu refactor(panel-group): optimize command registration in PanelGroup refactor(panel-commands): enhance command registration logic in usePanelCommands refactor(commands): update built-in command handling to use executeCommands fix(extension-host): improve error logging for worker runtime errors refactor(extension-lifecycle): clean up useEffect dependencies in useExtensionLifecycleRuntime fix(extension-theme): handle promise rejection in theme application refactor(vscodeShim): update imports for better type management across modules test(commands): enhance command keybinding tests and ensure backward compatibility Co-authored-by: Copilot <copilot@github.com>
1 parent fa92635 commit 90a9ea4

28 files changed

Lines changed: 326 additions & 491 deletions

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
"lint": "eslint . --cache",
1313
"lint:fix": "eslint . --cache --fix",
1414
"fmt": "eslint . --cache --fix",
15-
"fmt:check": "eslint . --cache --fix-dry-run"
15+
"fmt:check": "eslint . --cache --fix-dry-run",
16+
"test": "turbo run test"
1617
},
1718
"dependencies": {
1819
"@dotdirfm/ui": "workspace:*",

packages/commands/src/commands.ts

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ export type KeybindingLayer = "default" | "extension" | "user";
5757
type CommandHandler = (...args: unknown[]) => void | Promise<void>;
5858
type RegisteredCommandHandler = {
5959
handler: CommandHandler;
60-
isActive?: () => boolean;
6160
};
6261
type ContextGetter = () => Record<string, unknown>;
6362
type ResolvedKeybinding = Keybinding & { normalizedKey: string };
@@ -117,9 +116,9 @@ export class CommandRegistry {
117116
};
118117
}
119118

120-
registerCommand(id: string, handler: CommandHandler, options?: { isActive?: () => boolean }): () => void {
119+
registerCommand(id: string, handler: CommandHandler): () => void {
121120
const handlers = this.handlers.get(id);
122-
const entry: RegisteredCommandHandler = { handler, isActive: options?.isActive };
121+
const entry: RegisteredCommandHandler = { handler };
123122
if (handlers) {
124123
handlers.push(entry);
125124
} else {
@@ -175,15 +174,7 @@ export class CommandRegistry {
175174
return;
176175
}
177176

178-
const selected =
179-
[...registrations].reverse().find((entry) => {
180-
if (!entry.isActive) return false;
181-
try {
182-
return entry.isActive();
183-
} catch {
184-
return false;
185-
}
186-
}) ?? registrations[registrations.length - 1];
177+
const selected = registrations[registrations.length - 1];
187178
if (!selected) return;
188179

189180
console.log("[dotdir:command]", id, ...args);
@@ -484,8 +475,40 @@ export class CommandRegistry {
484475
private notifyListeners(): void {
485476
for (const cb of this.listeners) cb();
486477
}
478+
479+
async executeCommands(steps: RunCommandsStep[]): Promise<void> {
480+
for (const step of steps) {
481+
if (typeof step === "string") {
482+
await this.executeCommand(step);
483+
continue;
484+
}
485+
486+
if (Array.isArray(step.args)) {
487+
await this.executeCommand(step.command, ...step.args);
488+
continue;
489+
}
490+
491+
if (step.args !== undefined) {
492+
await this.executeCommand(step.command, step.args);
493+
continue;
494+
}
495+
496+
await this.executeCommand(step.command);
497+
}
498+
}
487499
}
488500

501+
export type RunCommandsStep =
502+
| string
503+
| {
504+
command: string;
505+
args?: unknown;
506+
};
507+
508+
export type RunCommandsArgs = {
509+
commands: RunCommandsStep[];
510+
};
511+
489512
const CommandRegistryReactContext = createContext<CommandRegistry | null>(null);
490513

491514
export function CommandRegistryProvider({ children }: { children: ReactNode }) {
Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,7 @@
1-
import type { CommandRegistry } from "./commands";
2-
3-
export type RunCommandsStep =
4-
| string
5-
| {
6-
command: string;
7-
args?: unknown;
8-
};
9-
10-
export type RunCommandsArgs = {
11-
commands: RunCommandsStep[];
12-
};
1+
import type { CommandRegistry, RunCommandsStep } from "./commands";
132

3+
/** @deprecated Use `registry.executeCommands(steps)` instead. */
144
export async function runCommandSequence(commandRegistry: CommandRegistry, commands: RunCommandsStep[]): Promise<void> {
15-
for (const step of commands) {
16-
if (typeof step === "string") {
17-
await commandRegistry.executeCommand(step);
18-
continue;
19-
}
20-
21-
if (Array.isArray(step.args)) {
22-
await commandRegistry.executeCommand(step.command, ...step.args);
23-
continue;
24-
}
25-
26-
if (step.args !== undefined) {
27-
await commandRegistry.executeCommand(step.command, step.args);
28-
continue;
29-
}
30-
31-
await commandRegistry.executeCommand(step.command);
32-
}
5+
return commandRegistry.executeCommands(commands);
336
}
7+

packages/commands/tests/commands.test.ts

Lines changed: 22 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -76,49 +76,37 @@ describe("CommandRegistry", () => {
7676

7777
it("does not throw when command is not found", async () => {
7878
const registry = createRegistry();
79-
await expect(
80-
registry.executeCommand("nonexistent.command"),
81-
).resolves.toBeUndefined();
79+
await expect(registry.executeCommand("nonexistent.command")).resolves.toBeUndefined();
8280
});
8381

84-
it("prefers active-scoped handlers over inactive ones", async () => {
82+
it("executes the last registered handler", async () => {
8583
const registry = createRegistry();
8684
const results: string[] = [];
8785

88-
registry.registerCommand("test.scope", () => results.push("default"));
89-
90-
registry.registerCommand(
91-
"test.scope",
92-
() => results.push("active"),
93-
{ isActive: () => true },
94-
);
86+
registry.registerCommand("test.override", () => {
87+
results.push("first");
88+
});
9589

96-
registry.registerCommand(
97-
"test.scope",
98-
() => results.push("inactive"),
99-
{ isActive: () => false },
100-
);
90+
registry.registerCommand("test.override", () => {
91+
results.push("second");
92+
});
10193

102-
await registry.executeCommand("test.scope");
103-
expect(results).toEqual(["active"]);
94+
await registry.executeCommand("test.override");
95+
expect(results).toEqual(["second"]);
10496
});
10597

106-
it("falls back to the last registered handler when no active scope matches", async () => {
98+
it("unregister removes the handler", async () => {
10799
const registry = createRegistry();
108-
const results: string[] = [];
100+
let called = false;
109101

110-
registry.registerCommand(
111-
"test.fallback",
112-
() => results.push("inactive"),
113-
{ isActive: () => false },
114-
);
102+
const unregister = registry.registerCommand("test.unreg", () => {
103+
called = true;
104+
});
115105

116-
registry.registerCommand("test.fallback", () =>
117-
results.push("fallback"),
118-
);
106+
unregister();
119107

120-
await registry.executeCommand("test.fallback");
121-
expect(results).toEqual(["fallback"]);
108+
await registry.executeCommand("test.unreg");
109+
expect(called).toBe(false);
122110
});
123111
});
124112

@@ -130,34 +118,22 @@ describe("CommandRegistry", () => {
130118

131119
it("adds and retrieves keybindings for a layer", () => {
132120
const registry = createRegistry();
133-
registry.registerKeybinding(
134-
{ command: "test.cmd", key: "ctrl+a" },
135-
"default",
136-
);
121+
registry.registerKeybinding({ command: "test.cmd", key: "ctrl+a" }, "default");
137122
const bindings = registry.getKeybindings();
138123
expect(bindings).toHaveLength(1);
139124
expect(bindings[0]!.key).toBe("ctrl+a");
140125
});
141126

142127
it("filters out keybindings with empty command", () => {
143128
const registry = createRegistry();
144-
registry.registerKeybinding(
145-
{ command: "", key: "ctrl+a" },
146-
"default",
147-
);
129+
registry.registerKeybinding({ command: "", key: "ctrl+a" }, "default");
148130
expect(registry.getKeybindings()).toHaveLength(0);
149131
});
150132

151133
it("resolves keybindings for a specific layer", () => {
152134
const registry = createRegistry();
153-
registry.registerKeybinding(
154-
{ command: "cmd.a", key: "ctrl+x" },
155-
"default",
156-
);
157-
registry.registerKeybinding(
158-
{ command: "cmd.b", key: "ctrl+y" },
159-
"user",
160-
);
135+
registry.registerKeybinding({ command: "cmd.a", key: "ctrl+x" }, "default");
136+
registry.registerKeybinding({ command: "cmd.b", key: "ctrl+y" }, "user");
161137

162138
const defaultBindings = registry.getKeybindingsForLayer("default");
163139
expect(defaultBindings).toHaveLength(1);

packages/fss/src/layers.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ export class LayeredResolver implements StyleResolver {
133133
const layers = this.getApplicableLayers(node.path);
134134
const cacheKey = this.buildCacheKey(node, layers);
135135

136-
let cached = this.styleCache.get(cacheKey);
136+
const cached = this.styleCache.get(cacheKey);
137137
if (cached !== undefined) return cached;
138138

139139
const merged: ResolvedStyle = {};
@@ -155,7 +155,7 @@ export class LayeredResolver implements StyleResolver {
155155
const layers = this.getApplicableLayers(node.path);
156156
const cacheKey = this.buildCacheKey(node, layers);
157157

158-
let cached = this.sortCache.get(cacheKey);
158+
const cached = this.sortCache.get(cacheKey);
159159
if (cached !== undefined) return cached;
160160

161161
const merged: Record<string, FssValue> = {};
@@ -173,7 +173,7 @@ export class LayeredResolver implements StyleResolver {
173173
* Resolve table config for a given path (uses closest layer).
174174
*/
175175
resolveTableConfig(path: string): Record<string, any> {
176-
let cached = this.tableConfigCache.get(path);
176+
const cached = this.tableConfigCache.get(path);
177177
if (cached !== undefined) return cached;
178178

179179
const layers = this.getApplicableLayers(path);

packages/fss/src/resolver.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,11 @@ function getNodeAttrValue(name: string, node: FsNode): string | boolean | undefi
127127
return node.path;
128128
case 'type':
129129
return node.type;
130-
default:
130+
default: {
131131
const meta = node.meta[name];
132132
if (meta === undefined) return undefined;
133133
return typeof meta === 'string' || typeof meta === 'boolean' ? meta : String(meta);
134+
}
134135
}
135136
}
136137

@@ -207,7 +208,7 @@ function gatherCandidates(sheet: CompiledStylesheet, node: FsNode): CompiledRule
207208
}
208209

209210
const bKey = computeBucketKey(sheet, node);
210-
let cached = sheetCache.get(bKey);
211+
const cached = sheetCache.get(bKey);
211212
if (cached) return cached;
212213

213214
// Use a Set to avoid duplicates (a rule might be indexed under multiple keys)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { describe, expect, it } from "vitest";
2+
import { TerminalSession } from "../src";
3+
4+
describe("TerminalSession", () => {
5+
it("has correct initial status", () => {
6+
const bridge = {} as any;
7+
const session = new TerminalSession(
8+
bridge,
9+
"/home/user",
10+
{ id: "test", shell: "/bin/bash", label: "Test", cwdEscape: "posix", hiddenCdTemplate: " cd {{cwd}}", lineEnding: "\n" },
11+
() => false,
12+
);
13+
const caps = session.getCapabilities();
14+
expect(caps.cwd).toBe("/home/user");
15+
expect(caps.profileId).toBe("test");
16+
});
17+
});

packages/ui-focus/src/index.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,6 @@
11
import { createContext, createElement, useContext, useEffect, useRef, type ReactNode } from "react";
22

3-
export type FocusLayer =
4-
| "panel"
5-
| "autocomplete"
6-
| "searchResults"
7-
| "menu"
8-
| "commandPalette"
9-
| "modal"
10-
| "terminal"
11-
| "editor"
12-
| "viewer";
3+
export type FocusLayer = "panel" | "autocomplete" | "searchResults" | "menu" | "commandPalette" | "modal" | "terminal" | "editor" | "viewer";
134

145
type FocusChangeCallback = (layer: FocusLayer) => void;
156
type FocusStateChangeCallback = (state: FocusState) => void;

0 commit comments

Comments
 (0)