Skip to content
Source
Sign inSign up

keybindings.ts

232 lines6.4 KB

1import * as Schema from "effect/Schema";
2import { ForwardCompatibleArray, TrimmedString } from "./baseSchemas.ts";
3​
4const MAX_KEYBINDING_VALUE_LENGTH = 64;
5const MAX_KEYBINDING_WHEN_LENGTH = 256;
6export const MAX_WHEN_EXPRESSION_DEPTH = 64;
7export const MAX_SCRIPT_ID_LENGTH = 24;
8export const MAX_KEYBINDINGS_COUNT = 256;
9​
10export const THREAD_JUMP_KEYBINDING_COMMANDS = [
11 "thread.jump.1",
12 "thread.jump.2",
13 "thread.jump.3",
14 "thread.jump.4",
15 "thread.jump.5",
16 "thread.jump.6",
17 "thread.jump.7",
18 "thread.jump.8",
19 "thread.jump.9",
20] as const;
21export type ThreadJumpKeybindingCommand = (typeof THREAD_JUMP_KEYBINDING_COMMANDS)[number];
22​
23export const MODEL_PICKER_JUMP_KEYBINDING_COMMANDS = [
24 "modelPicker.jump.1",
25 "modelPicker.jump.2",
26 "modelPicker.jump.3",
27 "modelPicker.jump.4",
28 "modelPicker.jump.5",
29 "modelPicker.jump.6",
30 "modelPicker.jump.7",
31 "modelPicker.jump.8",
32 "modelPicker.jump.9",
33] as const;
34export type ModelPickerJumpKeybindingCommand =
35 (typeof MODEL_PICKER_JUMP_KEYBINDING_COMMANDS)[number];
36​
37const THREAD_KEYBINDING_COMMANDS = [
38 "thread.stop",
39 "thread.steerQueuedMessage",
40 "thread.editQueuedMessage",
41 "thread.previous",
42 "thread.next",
43 "thread.copyReference",
44 "thread.settle",
45 "thread.pin",
46 "thread.regenerateTitle",
47 "thread.undo",
48 ...THREAD_JUMP_KEYBINDING_COMMANDS,
49] as const;
50​
51const MODEL_PICKER_KEYBINDING_COMMANDS = [
52 "modelPicker.toggle",
53 "modelPicker.previousProvider",
54 "modelPicker.nextProvider",
55 ...MODEL_PICKER_JUMP_KEYBINDING_COMMANDS,
56] as const;
57​
58export const STATIC_KEYBINDING_COMMANDS = [
59 "workspace.focusNext",
60 "workspace.focusPrevious",
61 "sidebar.toggle",
62 "navigation.back",
63 "navigation.forward",
64 "terminal.toggle",
65 "terminal.split",
66 "terminal.splitVertical",
67 "terminal.new",
68 "terminal.close",
69 "rightPanel.toggle",
70 "rightPanel.new",
71 "threadPanel.toggle",
72 "rightPanel.toggleMaximized",
73 "rightPanel.close",
74 "view.reopenClosed",
75 "pullRequest.copyNumber",
76 "diff.toggle",
77 "preview.toggle",
78 "preview.refresh",
79 "preview.focusUrl",
80 "preview.zoomIn",
81 "preview.zoomOut",
82 "preview.resetZoom",
83 "preview.find",
84 "commandPalette.toggle",
85 "filePicker.toggle",
86 "projectSearch.toggle",
87 "usage.open",
88 "theme.select",
89 "appearance.cycle",
90 "themeEditor.toggle",
91 "composer.stash",
92 "composer.sendAlternate",
93 "composer.sendBackground",
94 "composer.sendAndNewThread",
95 "composer.dictation",
96 "composer.host",
97 "composer.cycleHost",
98 "composer.effort",
99 "composer.increaseEffort",
100 "composer.decreaseEffort",
101 "composer.mode",
102 "composer.workspace",
103 "composer.previousWorktree",
104 "composer.branch",
105 "chat.new",
106 "chat.newLocal",
107 "chat.newWithoutProject",
108 "chat.find",
109 "editor.openFavorite",
110 "usage.cost",
111 "usage.tokens",
112 "usage.limits",
113 "usage.period.day",
114 "usage.period.week",
115 "usage.period.month",
116 "usage.period.quarter",
117 ...MODEL_PICKER_KEYBINDING_COMMANDS,
118 ...THREAD_KEYBINDING_COMMANDS,
119] as const;
120​
121export const SCRIPT_RUN_COMMAND_PATTERN = Schema.TemplateLiteral([
122 Schema.Literal("script."),
123 Schema.NonEmptyString.check(
124 Schema.isMaxLength(MAX_SCRIPT_ID_LENGTH),
125 Schema.isPattern(/^[a-z0-9][a-z0-9-]*$/),
126 ),
127 Schema.Literal(".run"),
128]);
129​
130export const KeybindingCommand = Schema.Union([
131 Schema.Literals(STATIC_KEYBINDING_COMMANDS),
132 SCRIPT_RUN_COMMAND_PATTERN,
133]);
134export type KeybindingCommand = typeof KeybindingCommand.Type;
135​
136export const KeybindingValue = TrimmedString.check(
137 Schema.isMinLength(1),
138 Schema.isMaxLength(MAX_KEYBINDING_VALUE_LENGTH),
139);
140​
141export const KeybindingWhen = TrimmedString.check(
142 Schema.isMinLength(1),
143 Schema.isMaxLength(MAX_KEYBINDING_WHEN_LENGTH),
144);
145export const KeybindingRule = Schema.Struct({
146 key: KeybindingValue,
147 command: KeybindingCommand,
148 when: Schema.optional(KeybindingWhen),
149});
150export type KeybindingRule = typeof KeybindingRule.Type;
151​
152export const KeybindingsConfig = Schema.Array(KeybindingRule).check(
153 Schema.isMaxLength(MAX_KEYBINDINGS_COUNT),
154);
155export type KeybindingsConfig = typeof KeybindingsConfig.Type;
156​
157// Untrimmed: the `space` token resolves to the key " ".
158const KeybindingShortcutKey = Schema.String.check(
159 Schema.isMinLength(1),
160 Schema.isMaxLength(MAX_KEYBINDING_VALUE_LENGTH),
161);
162​
163export const KeybindingShortcut = Schema.Struct({
164 key: KeybindingShortcutKey,
165 metaKey: Schema.Boolean,
166 ctrlKey: Schema.Boolean,
167 shiftKey: Schema.Boolean,
168 altKey: Schema.Boolean,
169 modKey: Schema.Boolean,
170});
171export type KeybindingShortcut = typeof KeybindingShortcut.Type;
172​
173export const PreviewForwardedShortcut = Schema.Struct({
174 command: KeybindingCommand,
175 shortcut: KeybindingShortcut,
176});
177export type PreviewForwardedShortcut = typeof PreviewForwardedShortcut.Type;
178​
179const KeybindingWhenNodeRef = Schema.suspend(
180 (): Schema.Codec<KeybindingWhenNode> => KeybindingWhenNode,
181);
182export const KeybindingWhenNode = Schema.Union([
183 Schema.Struct({
184 type: Schema.Literal("identifier"),
185 name: Schema.NonEmptyString,
186 }),
187 Schema.Struct({
188 type: Schema.Literal("not"),
189 node: KeybindingWhenNodeRef,
190 }),
191 Schema.Struct({
192 type: Schema.Literal("and"),
193 left: KeybindingWhenNodeRef,
194 right: KeybindingWhenNodeRef,
195 }),
196 Schema.Struct({
197 type: Schema.Literal("or"),
198 left: KeybindingWhenNodeRef,
199 right: KeybindingWhenNodeRef,
200 }),
201]);
202export type KeybindingWhenNode =
203 | { type: "identifier"; name: string }
204 | { type: "not"; node: KeybindingWhenNode }
205 | { type: "and"; left: KeybindingWhenNode; right: KeybindingWhenNode }
206 | { type: "or"; left: KeybindingWhenNode; right: KeybindingWhenNode };
207​
208export const ResolvedKeybindingRule = Schema.Struct({
209 command: KeybindingCommand,
210 shortcut: KeybindingShortcut,
211 whenAst: Schema.optional(KeybindingWhenNode),
212}).annotate({ parseOptions: { onExcessProperty: "ignore" } });
213export type ResolvedKeybindingRule = typeof ResolvedKeybindingRule.Type;
214​
215/** Drops undecodable rules without failing the connection. */
216export const ResolvedKeybindingsConfig = ForwardCompatibleArray(ResolvedKeybindingRule).check(
217 Schema.isMaxLength(MAX_KEYBINDINGS_COUNT),
218);
219export type ResolvedKeybindingsConfig = typeof ResolvedKeybindingsConfig.Type;
220​
221export class KeybindingsConfigError extends Schema.TaggedError<KeybindingsConfigError>()(
222 "KeybindingsConfigParseError",
223 {
224 configPath: Schema.String,
225 detail: Schema.String,
226 cause: Schema.optional(Schema.Defect()),
227 },
228) {
229 override get message(): string {
230 return `Unable to parse keybindings config at ${this.configPath}: ${this.detail}`;
231 }