0
# Command System
1
2
The command system provides type-safe command registration and execution for editor actions. Commands are the primary way to interact with and modify the editor state in Milkdown.
3
4
## Capabilities
5
6
### Command Manager
7
8
The central command management system that handles registration, retrieval, and execution of commands.
9
10
```typescript { .api }
11
/**
12
* The command manager that handles all editor commands
13
*/
14
class CommandManager {
15
/** Register a command with the manager */
16
create<T>(meta: CmdKey<T>, value: Cmd<T>): SliceType<Cmd<T>>;
17
18
/** Get a registered command by its key */
19
get<T extends CmdKey<any>>(slice: string): Cmd<InferParams<T>>;
20
get<T>(slice: CmdKey<T>): Cmd<T>;
21
22
/** Remove a command from the manager */
23
remove<T extends CmdKey<any>>(slice: string): void;
24
remove<T>(slice: CmdKey<T>): void;
25
26
/** Execute a registered command with optional payload */
27
call<T extends CmdKey<any>>(slice: string, payload?: InferParams<T>): boolean;
28
call<T>(slice: CmdKey<T>, payload?: T): boolean;
29
30
/** Execute an inline ProseMirror command directly */
31
inline(command: Command): boolean;
32
33
/** Create a command chain for sequential execution */
34
chain(): CommandChain;
35
36
/** Get the current editor context */
37
readonly ctx: Ctx | null;
38
}
39
```
40
41
**Usage Examples:**
42
43
```typescript
44
import { Editor, commandsCtx, createCmdKey } from "@milkdown/core";
45
46
// Create a command key
47
const toggleBoldKey = createCmdKey<void>('toggleBold');
48
49
// Register the command
50
editor.action((ctx) => {
51
const commands = ctx.get(commandsCtx);
52
53
commands.create(toggleBoldKey, () => (state, dispatch) => {
54
// ProseMirror command implementation
55
const { schema } = state;
56
const markType = schema.marks.strong;
57
return toggleMark(markType)(state, dispatch);
58
});
59
});
60
61
// Execute the command
62
editor.action((ctx) => {
63
const commands = ctx.get(commandsCtx);
64
const success = commands.call(toggleBoldKey);
65
console.log('Command executed:', success);
66
});
67
```
68
69
### Command Chain
70
71
Interface for chaining multiple commands together with sequential execution.
72
73
```typescript { .api }
74
/**
75
* A chainable command helper for executing multiple commands in sequence
76
*/
77
interface CommandChain {
78
/** Execute all commands in the chain */
79
run(): boolean;
80
81
/** Add a ProseMirror command directly to the chain */
82
inline(command: Command): CommandChain;
83
84
/** Add a registered command to the chain with optional payload */
85
pipe<T extends CmdKey<any>>(slice: string, payload?: InferParams<T>): CommandChain;
86
pipe<T>(slice: CmdKey<T>, payload?: T): CommandChain;
87
pipe(slice: string | CmdKey<any>, payload?: any): CommandChain;
88
}
89
```
90
91
**Usage Examples:**
92
93
```typescript
94
import { Editor, commandsCtx } from "@milkdown/core";
95
96
editor.action((ctx) => {
97
const commands = ctx.get(commandsCtx);
98
99
// Chain multiple commands
100
const success = commands.chain()
101
.pipe(toggleBoldKey)
102
.inline((state, dispatch) => {
103
// Inline command
104
return selectAll(state, dispatch);
105
})
106
.pipe(insertTextKey, 'Hello World')
107
.run();
108
109
console.log('Chain executed:', success);
110
});
111
```
112
113
### Command Key Creation
114
115
Function to create type-safe command keys for command registration.
116
117
```typescript { .api }
118
/**
119
* Create a command key for type-safe command registration
120
* @param key - Optional identifier for the command key
121
* @returns A typed command key slice
122
*/
123
function createCmdKey<T = undefined>(key?: string): CmdKey<T>;
124
```
125
126
**Usage Examples:**
127
128
```typescript
129
import { createCmdKey } from "@milkdown/core";
130
131
// Command with no payload
132
const saveDocumentKey = createCmdKey<void>('saveDocument');
133
134
// Command with string payload
135
const insertTextKey = createCmdKey<string>('insertText');
136
137
// Command with complex payload
138
interface FormatOptions {
139
bold?: boolean;
140
italic?: boolean;
141
color?: string;
142
}
143
const formatTextKey = createCmdKey<FormatOptions>('formatText');
144
145
// Usage
146
editor.action((ctx) => {
147
const commands = ctx.get(commandsCtx);
148
149
commands.call(saveDocumentKey);
150
commands.call(insertTextKey, 'Hello World');
151
commands.call(formatTextKey, { bold: true, color: 'red' });
152
});
153
```
154
155
### Command Context Slice
156
157
The context slice that provides access to the command manager.
158
159
```typescript { .api }
160
/**
161
* Context slice containing the command manager instance
162
*/
163
const commandsCtx: SliceType<CommandManager, 'commands'>;
164
```
165
166
## Command Types
167
168
### Core Command Types
169
170
```typescript { .api }
171
/**
172
* Command function type that returns a ProseMirror command
173
* @param payload - Optional payload data for the command
174
* @returns ProseMirror Command function
175
*/
176
type Cmd<T = undefined> = (payload?: T) => Command;
177
178
/**
179
* Command key type for type-safe command registration
180
*/
181
type CmdKey<T = undefined> = SliceType<Cmd<T>>;
182
183
/**
184
* Type helper to infer command payload types from CmdKey
185
*/
186
type InferParams<T> = T extends CmdKey<infer U> ? U : never;
187
```
188
189
## Advanced Usage
190
191
### Custom Command Implementation
192
193
```typescript
194
import { Editor, commandsCtx, createCmdKey } from "@milkdown/core";
195
import { setBlockType } from "@milkdown/prose/commands";
196
197
// Create a custom heading command
198
const setHeadingKey = createCmdKey<{ level: number }>('setHeading');
199
200
editor.action((ctx) => {
201
const commands = ctx.get(commandsCtx);
202
203
commands.create(setHeadingKey, ({ level }) => (state, dispatch) => {
204
const { schema } = state;
205
const headingType = schema.nodes.heading;
206
207
if (!headingType) return false;
208
209
return setBlockType(headingType, { level })(state, dispatch);
210
});
211
});
212
213
// Use the command
214
editor.action((ctx) => {
215
const commands = ctx.get(commandsCtx);
216
commands.call(setHeadingKey, { level: 2 });
217
});
218
```
219
220
### Command Composition
221
222
```typescript
223
import { Editor, commandsCtx, createCmdKey } from "@milkdown/core";
224
225
// Compose multiple operations into a single command
226
const formatParagraphKey = createCmdKey<{
227
bold?: boolean;
228
italic?: boolean;
229
align?: 'left' | 'center' | 'right';
230
}>('formatParagraph');
231
232
editor.action((ctx) => {
233
const commands = ctx.get(commandsCtx);
234
235
commands.create(formatParagraphKey, (options) => (state, dispatch, view) => {
236
// Chain multiple formatting commands
237
return commands.chain()
238
.pipe(toggleBoldKey)
239
.pipe(toggleItalicKey)
240
.pipe(setAlignmentKey, options.align || 'left')
241
.run();
242
});
243
});
244
```
245
246
### Conditional Command Execution
247
248
```typescript
249
import { Editor, commandsCtx } from "@milkdown/core";
250
251
editor.action((ctx) => {
252
const commands = ctx.get(commandsCtx);
253
254
// Execute command only if selection is not empty
255
const success = commands.inline((state, dispatch) => {
256
if (state.selection.empty) {
257
console.log('Cannot format empty selection');
258
return false;
259
}
260
261
return commands.call(formatSelectionKey, { bold: true });
262
});
263
});
264
```
265
266
## Error Handling
267
268
### Command Execution Errors
269
270
```typescript
271
import { Editor, commandsCtx } from "@milkdown/core";
272
import { callCommandBeforeEditorView } from "@milkdown/exception";
273
274
editor.action((ctx) => {
275
const commands = ctx.get(commandsCtx);
276
277
try {
278
const success = commands.call(someCommandKey, payload);
279
if (!success) {
280
console.warn('Command execution failed - command returned false');
281
}
282
} catch (error) {
283
if (error === callCommandBeforeEditorView()) {
284
console.error('Cannot execute command before editor view is ready');
285
} else {
286
console.error('Command execution error:', error);
287
}
288
}
289
});
290
```
291
292
### Command Registration Safety
293
294
```typescript
295
import { Editor, commandsCtx, createCmdKey } from "@milkdown/core";
296
297
const myCommandKey = createCmdKey<string>('myCommand');
298
299
editor.action((ctx) => {
300
const commands = ctx.get(commandsCtx);
301
302
// Check if command already exists before registering
303
try {
304
const existing = commands.get(myCommandKey);
305
console.log('Command already registered');
306
} catch {
307
// Command doesn't exist, safe to register
308
commands.create(myCommandKey, (text) => (state, dispatch) => {
309
// Command implementation
310
return true;
311
});
312
}
313
});
314
```
315
316
## Integration with ProseMirror
317
318
The command system seamlessly integrates with ProseMirror's command system:
319
320
```typescript
321
import { Editor, commandsCtx } from "@milkdown/core";
322
import {
323
toggleMark,
324
wrapIn,
325
setBlockType,
326
chainCommands,
327
exitCode
328
} from "@milkdown/prose/commands";
329
330
editor.action((ctx) => {
331
const commands = ctx.get(commandsCtx);
332
333
// Use ProseMirror commands directly
334
commands.inline(toggleMark(schema.marks.strong));
335
336
// Chain ProseMirror commands
337
commands.inline(chainCommands(
338
exitCode,
339
(state, dispatch) => {
340
// Custom logic
341
return true;
342
}
343
));
344
});
345
```