@silverbulletmd/silverbullet 2.9.0 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +36 -23
  2. package/client/plugos/hooks/syscall.ts +24 -10
  3. package/client/plugos/syscalls/asset.ts +61 -23
  4. package/client/plugos/syscalls/clientStore.ts +32 -6
  5. package/client/plugos/syscalls/client_code_widget.ts +9 -4
  6. package/client/plugos/syscalls/code_widget.ts +26 -11
  7. package/client/plugos/syscalls/config.ts +146 -30
  8. package/client/plugos/syscalls/datastore.ts +146 -40
  9. package/client/plugos/syscalls/editor.ts +1432 -711
  10. package/client/plugos/syscalls/event.ts +46 -13
  11. package/client/plugos/syscalls/fetch.ts +123 -82
  12. package/client/plugos/syscalls/index.ts +242 -116
  13. package/client/plugos/syscalls/jsonschema.ts +107 -8
  14. package/client/plugos/syscalls/language.ts +38 -12
  15. package/client/plugos/syscalls/markdown.ts +166 -50
  16. package/client/plugos/syscalls/mq.ts +93 -20
  17. package/client/plugos/syscalls/schema_introspection.ts +33 -0
  18. package/client/plugos/syscalls/service_registry.ts +74 -20
  19. package/client/plugos/syscalls/shell.ts +51 -20
  20. package/client/plugos/syscalls/space.ts +169 -113
  21. package/client/plugos/syscalls/sync.ts +50 -19
  22. package/client/plugos/syscalls/system.ts +236 -139
  23. package/client/plugos/system.ts +21 -5
  24. package/client/space_lua/api_documentation.ts +107 -0
  25. package/client/space_lua/ast.ts +13 -0
  26. package/client/space_lua/eval.ts +12 -4
  27. package/client/space_lua/parse.ts +243 -108
  28. package/client/space_lua/pretty_print.ts +128 -54
  29. package/client/space_lua/query_collection.ts +4 -4
  30. package/client/space_lua/render_widget.ts +156 -0
  31. package/client/space_lua/runtime.ts +83 -22
  32. package/client/space_lua/stdlib/crypto.ts +8 -3
  33. package/client/space_lua/stdlib/encoding.ts +27 -9
  34. package/client/space_lua/stdlib/js.ts +137 -22
  35. package/client/space_lua/stdlib/load.ts +24 -15
  36. package/client/space_lua/stdlib/math.ts +323 -118
  37. package/client/space_lua/stdlib/net.ts +57 -9
  38. package/client/space_lua/stdlib/os.ts +111 -44
  39. package/client/space_lua/stdlib/space_lua.ts +344 -20
  40. package/client/space_lua/stdlib/string.ts +282 -104
  41. package/client/space_lua/stdlib/string_pack.ts +58 -19
  42. package/client/space_lua/stdlib/table.ts +195 -40
  43. package/client/space_lua/stdlib.ts +335 -93
  44. package/client/space_lua/syscalls.ts +270 -0
  45. package/dist/plug-compile.js +1 -1
  46. package/package.json +3 -3
  47. package/plug-api/lib/ref.ts +96 -0
  48. package/plug-api/lib/shortcut.ts +2 -1
  49. package/plug-api/syscalls/config.ts +5 -3
  50. package/plug-api/syscalls/editor.ts +16 -0
  51. package/plug-api/syscalls/index.ts +17 -1
  52. package/plug-api/syscalls/jsonschema.ts +9 -0
  53. package/plug-api/syscalls/lua.ts +7 -0
  54. package/plug-api/syscalls/shell.ts +0 -1
  55. package/plug-api/syscalls/system.ts +7 -0
  56. package/plug-api/system_mock.ts +2 -2
  57. package/plug-api/types/index.ts +52 -1
  58. package/plug-api/types/manifest.ts +22 -4
  59. package/plug-api/ui/cx.ts +1 -3
  60. package/plug-api/ui/index.ts +3 -0
  61. package/plug-api/ui/panel_styles.ts +4 -3
  62. package/plug-api/ui/slugify.ts +44 -0
  63. package/client/plugos/syscalls/lua.ts +0 -81
@@ -1,11 +1,9 @@
1
- import type { Client } from "../../client.ts";
2
1
  import {
3
- foldAll,
4
- foldCode,
5
- toggleFold,
6
- unfoldAll,
7
- unfoldCode,
8
- } from "@codemirror/language";
2
+ acceptCompletion,
3
+ closeCompletion,
4
+ moveCompletionSelection,
5
+ startCompletion,
6
+ } from "@codemirror/autocomplete";
9
7
  import {
10
8
  cursorCharLeft,
11
9
  cursorCharRight,
@@ -54,773 +52,1496 @@ import {
54
52
  transposeChars,
55
53
  undo,
56
54
  } from "@codemirror/commands";
55
+ import { insertNewlineContinueMarkup } from "@codemirror/lang-markdown";
57
56
  import {
58
- acceptCompletion,
59
- closeCompletion,
60
- moveCompletionSelection,
61
- startCompletion,
62
- } from "@codemirror/autocomplete";
57
+ foldAll,
58
+ foldCode,
59
+ toggleFold,
60
+ unfoldAll,
61
+ unfoldCode,
62
+ } from "@codemirror/language";
63
+ import { forceLinting } from "@codemirror/lint";
64
+ import { openSearchPanel } from "@codemirror/search";
63
65
  import type { Transaction } from "@codemirror/state";
64
66
  import { EditorView } from "@codemirror/view";
65
- import { getVimModule } from "../../vim_loader.ts";
66
- import type { SysCallMapping } from "../system.ts";
67
+ import {
68
+ coerceAndValidateRef,
69
+ type Ref,
70
+ } from "@silverbulletmd/silverbullet/lib/ref";
67
71
  import type {
68
72
  FilterOption,
69
73
  NotificationAction,
70
74
  NotificationType,
71
75
  UploadFile,
72
76
  } from "@silverbulletmd/silverbullet/type/client";
73
- import { openSearchPanel } from "@codemirror/search";
74
- import { forceLinting } from "@codemirror/lint";
75
- import { refreshLintEffect } from "../../codemirror/lint.ts";
76
- import {
77
- isValidPath,
78
- parseToRef,
79
- type Path,
80
- type Ref,
81
- } from "@silverbulletmd/silverbullet/lib/ref";
82
- import { insertNewlineContinueMarkup } from "@codemirror/lang-markdown";
83
77
  import type { VimConfig } from "@silverbulletmd/silverbullet/type/config";
84
78
  import type { PageMeta } from "@silverbulletmd/silverbullet/type/index";
79
+ import { updateBakedSections } from "../../baked_sections/bake.ts";
80
+ import type { Client } from "../../client.ts";
81
+ import { refreshLintEffect } from "../../codemirror/lint.ts";
82
+ import { getVimModule } from "../../vim_loader.ts";
83
+ import type { SysCallMapping } from "../system.ts";
85
84
 
86
85
  export function editorSyscalls(client: Client): SysCallMapping {
87
86
  const syscalls: SysCallMapping = {
88
- "editor.getCurrentPage": (): string => {
89
- return client.currentName();
90
- },
91
- "editor.getCurrentPageMeta": (): Promise<PageMeta | undefined> => {
92
- const name = client.currentName();
93
- return client.objectIndex.getObjectByRef(name, "page", name);
94
- },
95
- "editor.getCurrentPath": (_ctx): string => {
96
- return client.currentPath();
97
- },
98
- "editor.getCurrentEditor": (): string => {
99
- return client.contentManager.documentEditor?.name || "page";
100
- },
101
- "editor.getRecentlyOpenedPages": (): PageMeta[] => {
102
- return client.ui.viewState.allPages.sort(
103
- (a, b) => (b.lastOpened || 0) - (a.lastOpened || 0),
104
- );
105
- },
106
- "editor.getText": () => {
107
- return client.editorView.state.sliceDoc();
108
- },
109
- "editor.getCurrentLine": (): {
110
- from: number;
111
- to: number;
112
- text: string;
113
- textWithCursor: string;
114
- } => {
115
- const pos = client.editorView.state.selection.main.from;
116
- const line = client.editorView.state.doc.lineAt(pos);
117
- return {
118
- ...line,
119
- textWithCursor:
120
- line.text.slice(0, pos - line.from) +
121
- "|^|" +
122
- line.text.slice(pos - line.from),
123
- };
124
- },
125
- "editor.setText": (_ctx, newText: string, shouldIsolateHistory = false) => {
126
- client.contentManager.setEditorText(newText, shouldIsolateHistory);
127
- },
128
- "editor.getCursor": (): number => {
129
- return client.editorView.state.selection.main.from;
130
- },
131
- "editor.getSelection": (): { from: number; to: number; text: string } => {
132
- const selection = client.editorView.state.selection.main;
133
- const text = client.editorView.state.sliceDoc(
134
- selection.from,
135
- selection.to,
136
- );
137
- return {
138
- from: selection.from,
139
- to: selection.to,
140
- text,
141
- };
142
- },
143
- "editor.save": () => {
144
- return client.save(true);
145
- },
146
- "editor.navigate": async (
147
- _ctx,
148
- ref: Ref | string,
149
- replaceState = false,
150
- newWindow = false,
151
- ) => {
152
- if (typeof ref === "string") {
153
- const parsedRef = parseToRef(ref);
154
- if (!parsedRef) {
155
- throw new Error(
156
- "Unable to parse string provided to `editor.navigate` as ref",
157
- );
158
- }
159
- ref = parsedRef;
160
- }
161
-
162
- if (
163
- // @ts-expect-error: Legacy support
164
- ref.page !== undefined
165
- ) {
166
- console.warn(
167
- "You are using legacy navigation syntax with `editor.navigate()`, this will be phased out in the future",
87
+ "editor.getCurrentPage": {
88
+ callback: (): string => {
89
+ return client.currentName();
90
+ },
91
+ description:
92
+ "Returns the name of the page or document currently open in the editor.",
93
+ returns: [{ type: "string", description: "The current page name." }],
94
+ },
95
+ "editor.getCurrentPageMeta": {
96
+ callback: (): Promise<PageMeta | undefined> => {
97
+ const name = client.currentName();
98
+ return client.objectIndex.getObjectByRef(name, "page", name);
99
+ },
100
+ description:
101
+ "Returns metadata for the page or document currently open in the editor.",
102
+ returns: [
103
+ {
104
+ type: "PageMeta | undefined",
105
+ description: "The current page metadata, if indexed.",
106
+ },
107
+ ],
108
+ },
109
+ "editor.getCurrentPath": {
110
+ callback: (_ctx): string => {
111
+ return client.currentPath();
112
+ },
113
+ description:
114
+ "Returns the path of the page or document currently open in the editor.",
115
+ returns: [{ type: "string", description: "The current page path." }],
116
+ },
117
+ "editor.getCurrentEditor": {
118
+ callback: (): string => {
119
+ return client.contentManager.documentEditor?.name || "page";
120
+ },
121
+ description:
122
+ "Returns the name of the currently active editor implementation.",
123
+ returns: [
124
+ {
125
+ type: "string",
126
+ description: "The editor name, or `page` for the page editor.",
127
+ },
128
+ ],
129
+ },
130
+ "editor.getRecentlyOpenedPages": {
131
+ callback: (): PageMeta[] => {
132
+ return client.ui.viewState.allPages.sort(
133
+ (a, b) => (b.lastOpened || 0) - (a.lastOpened || 0),
168
134
  );
169
-
170
- const legacyRef = ref as unknown as {
171
- kind: "page" | "document";
172
- page: string;
173
- pos?: number | { line: number; column: number };
174
- header?: string;
175
- meta?: boolean;
135
+ },
136
+ description:
137
+ "Returns page metadata ordered from most to least recently opened.",
138
+ returns: [{ type: "PageMeta[]", description: "Recently opened pages." }],
139
+ },
140
+ "editor.getText": {
141
+ callback: () => {
142
+ return client.editorView.state.sliceDoc();
143
+ },
144
+ description:
145
+ "Returns the full text of the currently open page or document.",
146
+ returns: [{ type: "string", description: "The editor contents." }],
147
+ },
148
+ "editor.getCurrentLine": {
149
+ callback: (): {
150
+ from: number;
151
+ to: number;
152
+ text: string;
153
+ textWithCursor: string;
154
+ } => {
155
+ const pos = client.editorView.state.selection.main.from;
156
+ const line = client.editorView.state.doc.lineAt(pos);
157
+ return {
158
+ ...line,
159
+ textWithCursor:
160
+ line.text.slice(0, pos - line.from) +
161
+ "|^|" +
162
+ line.text.slice(pos - line.from),
176
163
  };
177
-
178
- legacyRef.kind ??= "page";
179
-
180
- let details: Ref["details"];
181
-
182
- if (typeof legacyRef.pos === "number") {
183
- details = {
184
- type: "position",
185
- pos: legacyRef.pos,
186
- };
187
- } else if (legacyRef.pos) {
188
- details = {
189
- type: "linecolumn",
190
- line: legacyRef.pos.line,
191
- column: legacyRef.pos.column,
192
- };
193
- } else if (legacyRef.header) {
194
- details = {
195
- type: "header",
196
- header: legacyRef.header,
197
- };
198
- }
199
-
200
- ref = {
201
- path: (legacyRef.kind === "page"
202
- ? `${legacyRef.page}.md`
203
- : legacyRef.page) as Path,
204
- details,
205
- meta: legacyRef.meta,
164
+ },
165
+ description:
166
+ "Returns the current line's range and text, including a `|^|` cursor marker variant.",
167
+ returns: [
168
+ {
169
+ type: "{ from: number; to: number; text: string; textWithCursor: string }",
170
+ description: "The line containing the main selection head.",
171
+ },
172
+ ],
173
+ },
174
+ "editor.setText": {
175
+ callback: (_ctx, newText: string, shouldIsolateHistory = false) => {
176
+ client.contentManager.setEditorText(newText, shouldIsolateHistory);
177
+ },
178
+ description:
179
+ "Updates the editor text with a minimal diff while preserving the cursor when possible.",
180
+ parameters: [
181
+ {
182
+ name: "newText",
183
+ type: "string",
184
+ description: "The complete replacement text.",
185
+ },
186
+ {
187
+ name: "shouldIsolateHistory",
188
+ type: "boolean",
189
+ description: "Whether to isolate the change in undo history.",
190
+ optional: true,
191
+ },
192
+ ],
193
+ },
194
+ "editor.getCursor": {
195
+ callback: (): number => {
196
+ return client.editorView.state.selection.main.from;
197
+ },
198
+ description:
199
+ "Returns the cursor position as a character offset from the start of the document.",
200
+ returns: [{ type: "number", description: "The cursor offset." }],
201
+ },
202
+ "editor.getSelection": {
203
+ callback: (): { from: number; to: number; text: string } => {
204
+ const selection = client.editorView.state.selection.main;
205
+ const text = client.editorView.state.sliceDoc(
206
+ selection.from,
207
+ selection.to,
208
+ );
209
+ return {
210
+ from: selection.from,
211
+ to: selection.to,
212
+ text,
206
213
  };
207
- }
208
-
209
- // This validation code should be connected to the Ref type. Ideally using
210
- // some validation library. Didn't want to use jsonschemas here tho.
211
- // Ideally this would be moved into a function too
212
- if (!isValidPath(ref.path) && ref.path !== "") {
213
- throw new Error("Path passed in ref to `editor.navigate` is invalid");
214
- } else if (typeof ref.meta !== "boolean" && ref.meta !== undefined) {
215
- throw new Error("ref.meta has to be of type `boolean`");
216
- } else if (ref.details !== undefined && typeof ref.details !== "object") {
217
- throw new Error(
218
- "ref.details has to be of type `object` or `undefined`",
214
+ },
215
+ description: "Returns the current selection range and selected text.",
216
+ returns: [
217
+ {
218
+ type: "{ from: number; to: number; text: string }",
219
+ description: "The main editor selection.",
220
+ },
221
+ ],
222
+ },
223
+ "editor.save": {
224
+ callback: () => {
225
+ return client.save(true);
226
+ },
227
+ description: "Forces the current page or document to be saved.",
228
+ },
229
+ "editor.navigate": {
230
+ callback: async (
231
+ _ctx,
232
+ ref: Ref | string,
233
+ replaceState = false,
234
+ newWindow = false,
235
+ ) => {
236
+ await client.navigate(
237
+ coerceAndValidateRef(ref),
238
+ replaceState,
239
+ newWindow,
219
240
  );
220
- } else if (
221
- ref.details &&
222
- !["position", "linecolumn", "header", "anchor"].includes(
223
- ref.details.type,
224
- )
225
- ) {
226
- throw new Error(
227
- "ref.details.type has to be 'position', 'linecolumn', 'header' or 'anchor'",
241
+ },
242
+ description:
243
+ "Navigates to a page reference without restoring its remembered cursor and scroll position.",
244
+ parameters: [
245
+ {
246
+ name: "ref",
247
+ type: "Ref | string",
248
+ description: "The page reference to navigate to.",
249
+ },
250
+ {
251
+ name: "replaceState",
252
+ type: "boolean",
253
+ description: "Whether to replace the current browser history state.",
254
+ optional: true,
255
+ },
256
+ {
257
+ name: "newWindow",
258
+ type: "boolean",
259
+ description: "Whether to open the reference in a new window.",
260
+ optional: true,
261
+ },
262
+ ],
263
+ examples: [{ code: 'editor.navigate("CHANGELOG@123")' }],
264
+ },
265
+ "editor.open": {
266
+ callback: async (
267
+ _ctx,
268
+ ref: Ref | string,
269
+ replaceState = false,
270
+ newWindow = false,
271
+ ) => {
272
+ await client.open(coerceAndValidateRef(ref), replaceState, newWindow);
273
+ },
274
+ description:
275
+ "Opens a page reference and restores its remembered cursor and scroll position when possible.",
276
+ parameters: [
277
+ {
278
+ name: "ref",
279
+ type: "Ref | string",
280
+ description: "The page reference to open.",
281
+ },
282
+ {
283
+ name: "replaceState",
284
+ type: "boolean",
285
+ description: "Whether to replace the current browser history state.",
286
+ optional: true,
287
+ },
288
+ {
289
+ name: "newWindow",
290
+ type: "boolean",
291
+ description: "Whether to open the reference in a new window.",
292
+ optional: true,
293
+ },
294
+ ],
295
+ examples: [{ code: 'editor.open("CHANGELOG")' }],
296
+ },
297
+ "editor.reloadPage": {
298
+ callback: async () => {
299
+ await client.reloadEditor();
300
+ },
301
+ description: "Force reloads the current page in the editor.",
302
+ },
303
+ "editor.forceLint": {
304
+ callback: () => {
305
+ // `forceLinting` alone is a no-op on unchanged content after the
306
+ // linter's internal "scheduled" flag has been cleared; the effect
307
+ // re-arms it via `needsRefresh`.
308
+ client.editorView.dispatch({
309
+ effects: refreshLintEffect.of(null),
310
+ });
311
+ forceLinting(client.editorView);
312
+ },
313
+ description:
314
+ "Forces editor linting to run, including when the content has not changed.",
315
+ },
316
+ "editor.reloadUI": {
317
+ callback: () => {
318
+ location.reload();
319
+ },
320
+ description: "Force reloads the browser UI.",
321
+ },
322
+ "editor.rebuildEditorState": {
323
+ callback: () => {
324
+ client.rebuildEditorState();
325
+ },
326
+ description:
327
+ "Rebuilds the CodeMirror editor state from the current client configuration.",
328
+ },
329
+ "editor.reloadConfigAndCommands": {
330
+ callback: async () => {
331
+ await client.clientSystem.system.localSyscall("system.loadScripts", []);
332
+ await client.clientSystem.system.localSyscall(
333
+ "system.loadSpaceStyles",
334
+ [],
228
335
  );
229
- }
230
-
231
- if (
232
- ref.details?.type === "position" &&
233
- typeof ref.details.pos !== "number"
234
- ) {
235
- throw new Error("ref.details.pos has to be of type `number`");
236
- } else if (
237
- ref.details?.type === "header" &&
238
- typeof ref.details.header !== "string"
239
- ) {
240
- throw new Error("ref.details.header has to be of type `string`");
241
- } else if (
242
- ref.details?.type === "linecolumn" &&
243
- typeof ref.details.line !== "number" &&
244
- typeof ref.details.column !== "number"
245
- ) {
246
- throw new Error(
247
- "ref.details.line and ref.details.column has to be of type `number`",
336
+ client.rebuildEditorState();
337
+ },
338
+ description:
339
+ "Reloads space scripts and styles, then rebuilds the editor state.",
340
+ },
341
+ "editor.invokeCommand": {
342
+ callback: (_ctx, name: string, args?: string[]) => {
343
+ return client.runCommandByName(name, args);
344
+ },
345
+ description: "Invokes a client command by name.",
346
+ parameters: [
347
+ { name: "name", type: "string", description: "The command name." },
348
+ {
349
+ name: "args",
350
+ type: "string[]",
351
+ description: "Arguments passed to the command.",
352
+ optional: true,
353
+ },
354
+ ],
355
+ },
356
+ // Re-evaluate every baked section on the current page and rewrite each body
357
+ // with its latest output (same as the "Baked Sections: Update" command),
358
+ // exposed for programmatic use.
359
+ "editor.updateBakedSections": {
360
+ callback: (): Promise<void> => {
361
+ return updateBakedSections(client);
362
+ },
363
+ description:
364
+ "Re-evaluates every baked section on the current page and replaces each body with its latest output.",
365
+ },
366
+ "editor.openUrl": {
367
+ callback: (_ctx, url: string, existingWindow = false) => {
368
+ client.openUrl(url, existingWindow);
369
+ },
370
+ description: "Opens a URL in the browser.",
371
+ parameters: [
372
+ { name: "url", type: "string", description: "The URL to open." },
373
+ {
374
+ name: "existingWindow",
375
+ type: "boolean",
376
+ description: "Whether to reuse an existing window.",
377
+ optional: true,
378
+ },
379
+ ],
380
+ },
381
+ "editor.newWindow": {
382
+ callback: () => {
383
+ globalThis.open(
384
+ location.href,
385
+ `rnd${Math.random()}`,
386
+ `width=${globalThis.innerWidth},heigh=${globalThis.innerHeight}`,
248
387
  );
249
- } else if (
250
- ref.details?.type === "anchor" &&
251
- typeof ref.details.name !== "string"
252
- ) {
253
- throw new Error("ref.details.name has to be of type `string`");
254
- }
388
+ },
389
+ description:
390
+ "Opens the current SilverBullet URL in a new browser window.",
391
+ },
392
+ "editor.goHistory": {
393
+ callback: (_ctx, delta: number) => {
394
+ globalThis.history.go(delta);
395
+ },
396
+ description: "Moves backward or forward through browser history.",
397
+ parameters: [
398
+ {
399
+ name: "delta",
400
+ type: "number",
401
+ description:
402
+ "The relative history offset; negative moves backward and positive moves forward.",
403
+ },
404
+ ],
405
+ },
406
+ "editor.downloadFile": {
407
+ callback: (_ctx, filename: string, dataUrl: string) => {
408
+ const link = document.createElement("a");
409
+ link.href = dataUrl;
410
+ link.download = filename;
411
+ link.click();
412
+ },
413
+ description:
414
+ "Triggers a browser download of a data URL under the given filename.",
415
+ parameters: [
416
+ {
417
+ name: "filename",
418
+ type: "string",
419
+ description: "The downloaded filename.",
420
+ },
421
+ {
422
+ name: "dataUrl",
423
+ type: "string",
424
+ description: "The data URL to download.",
425
+ },
426
+ ],
427
+ examples: [
428
+ {
429
+ code: 'editor.downloadFile("test.txt", "data:text/plain;base64,SGVsbG8=")',
430
+ },
431
+ ],
432
+ },
433
+ "editor.uploadFile": {
434
+ callback: (
435
+ _ctx,
436
+ accept?: string,
437
+ capture?: string,
438
+ ): Promise<UploadFile> => {
439
+ return new Promise<UploadFile>((resolve, reject) => {
440
+ const input = document.createElement("input");
441
+ input.type = "file";
442
+ if (accept) {
443
+ input.accept = accept;
444
+ }
445
+ if (capture) {
446
+ input.capture = capture;
447
+ }
255
448
 
256
- await client.navigate(ref, replaceState, newWindow);
257
- },
258
- "editor.reloadPage": async () => {
259
- await client.reloadEditor();
260
- },
261
- "editor.forceLint": () => {
262
- // `forceLinting` alone is a no-op on unchanged content after the
263
- // linter's internal "scheduled" flag has been cleared; the effect
264
- // re-arms it via `needsRefresh`.
265
- client.editorView.dispatch({
266
- effects: refreshLintEffect.of(null),
267
- });
268
- forceLinting(client.editorView);
269
- },
270
- "editor.reloadUI": () => {
271
- location.reload();
272
- },
273
- "editor.rebuildEditorState": () => {
274
- client.rebuildEditorState();
275
- },
276
- "editor.reloadConfigAndCommands": async () => {
277
- await client.clientSystem.system.localSyscall("system.loadScripts", []);
278
- await client.clientSystem.system.localSyscall(
279
- "system.loadSpaceStyles",
280
- [],
281
- );
282
- client.reconfigureLanguage();
283
- },
284
- "editor.invokeCommand": (_ctx, name: string, args?: string[]) => {
285
- return client.runCommandByName(name, args);
286
- },
287
- "editor.openUrl": (_ctx, url: string, existingWindow = false) => {
288
- client.openUrl(url, existingWindow);
289
- },
290
- "editor.newWindow": () => {
291
- globalThis.open(
292
- location.href,
293
- `rnd${Math.random()}`,
294
- `width=${globalThis.innerWidth},heigh=${globalThis.innerHeight}`,
295
- );
296
- },
297
- "editor.goHistory": (_ctx, delta: number) => {
298
- globalThis.history.go(delta);
299
- },
300
- "editor.downloadFile": (_ctx, filename: string, dataUrl: string) => {
301
- const link = document.createElement("a");
302
- link.href = dataUrl;
303
- link.download = filename;
304
- link.click();
305
- },
306
- "editor.uploadFile": (
307
- _ctx,
308
- accept?: string,
309
- capture?: string,
310
- ): Promise<UploadFile> => {
311
- return new Promise<UploadFile>((resolve, reject) => {
312
- const input = document.createElement("input");
313
- input.type = "file";
314
- if (accept) {
315
- input.accept = accept;
316
- }
317
- if (capture) {
318
- input.capture = capture;
319
- }
449
+ input.onchange = () => {
450
+ const file = input.files?.item(0);
451
+ if (!file) {
452
+ reject(new Error("No file found"));
453
+ } else {
454
+ const reader = new FileReader();
455
+ reader.readAsArrayBuffer(file);
456
+ reader.onloadend = async (evt) => {
457
+ if (evt.target?.readyState === FileReader.DONE) {
458
+ resolve({
459
+ name: file.name,
460
+ contentType: file.type,
461
+ content: new Uint8Array(await file.arrayBuffer()),
462
+ });
463
+ }
464
+ };
465
+ reader.onabort = (e) => {
466
+ reject(e);
467
+ };
468
+ reader.onerror = (e) => {
469
+ reject(e);
470
+ };
471
+ }
472
+ };
473
+ input.onabort = (e) => {
474
+ reject(e);
475
+ };
320
476
 
321
- input.onchange = () => {
322
- const file = input.files?.item(0);
323
- if (!file) {
324
- reject(new Error("No file found"));
477
+ input.style.display = "none";
478
+ document.body.appendChild(input);
479
+ input.click();
480
+ setTimeout(() => document.body.removeChild(input), 1000);
481
+ });
482
+ },
483
+ description:
484
+ "Opens the browser's native file picker and returns the selected file's bytes and metadata.",
485
+ parameters: [
486
+ {
487
+ name: "accept",
488
+ type: "string",
489
+ description: "Accepted file types for the file input.",
490
+ optional: true,
491
+ },
492
+ {
493
+ name: "capture",
494
+ type: "string",
495
+ description: "The media capture mode for the file input.",
496
+ optional: true,
497
+ },
498
+ ],
499
+ returns: [
500
+ {
501
+ type: "UploadFile",
502
+ description: "The selected file's name, content type, and bytes.",
503
+ },
504
+ ],
505
+ examples: [
506
+ {
507
+ code: 'local file = editor.uploadFile(".txt")\nprint(file.name)',
508
+ },
509
+ ],
510
+ },
511
+ "editor.flashNotification": {
512
+ callback: (
513
+ _ctx,
514
+ message: string,
515
+ type: NotificationType = "info",
516
+ options?: { timeout?: number; actions?: NotificationAction[] },
517
+ ) => {
518
+ client.ui.flashNotification(message, type, options);
519
+ },
520
+ description: "Shows a flash notification in the editor UI.",
521
+ parameters: [
522
+ {
523
+ name: "message",
524
+ type: "string",
525
+ description: "The message to display.",
526
+ },
527
+ {
528
+ name: "type",
529
+ type: "NotificationType",
530
+ description:
531
+ 'The notification severity: "info", "error", or "warning".',
532
+ optional: true,
533
+ },
534
+ {
535
+ name: "options",
536
+ type: "{ timeout?: number; actions?: NotificationAction[] }",
537
+ description:
538
+ "Optional timeout and action buttons. A timeout of 0 keeps the notification visible until dismissed.",
539
+ optional: true,
540
+ },
541
+ ],
542
+ examples: [
543
+ {
544
+ code: 'editor.flashNotification("Update available", "warning", {\n timeout = 0,\n actions = {{\n name = "Reload",\n run = function() editor.reloadUI() end\n }}\n})',
545
+ },
546
+ ],
547
+ },
548
+ "editor.filterBox": {
549
+ callback: (
550
+ _ctx,
551
+ label: string,
552
+ options: FilterOption[],
553
+ helpText = "",
554
+ placeHolder = "",
555
+ ): Promise<FilterOption | undefined> => {
556
+ return client.ui.filterBox(label, options, helpText, placeHolder);
557
+ },
558
+ description:
559
+ "Shows a filterable option picker similar to the page navigator.",
560
+ parameters: [
561
+ {
562
+ name: "label",
563
+ type: "string",
564
+ description: "The label shown beside the filter input.",
565
+ },
566
+ {
567
+ name: "options",
568
+ type: "FilterOption[]",
569
+ description: "The available options.",
570
+ },
571
+ {
572
+ name: "helpText",
573
+ type: "string",
574
+ description: "Help text shown below the picker.",
575
+ optional: true,
576
+ },
577
+ {
578
+ name: "placeHolder",
579
+ type: "string",
580
+ description: "Placeholder text for the filter input.",
581
+ optional: true,
582
+ },
583
+ ],
584
+ returns: [
585
+ {
586
+ type: "FilterOption | undefined",
587
+ description: "The selected option, or undefined if dismissed.",
588
+ },
589
+ ],
590
+ examples: [
591
+ {
592
+ code: 'local result = editor.filterBox("Select:", {\n {name = "Option 1", value = "1"},\n {name = "Option 2", value = "2", description = "More details"}\n})',
593
+ },
594
+ ],
595
+ },
596
+ "editor.showPanel": {
597
+ callback: (
598
+ _ctx,
599
+ id: string,
600
+ mode: number,
601
+ html: HTMLElement | HTMLElement[] | string,
602
+ script: string,
603
+ ) => {
604
+ client.ui.viewDispatch({
605
+ type: "show-panel",
606
+ id: id as any,
607
+ config: { html, script, mode },
608
+ });
609
+ setTimeout(() => {
610
+ // Dummy dispatch to rerender the editor and toggle the panel
611
+ client.editorView.dispatch({});
612
+ });
613
+ },
614
+ description: "Shows an HTML panel in a specified editor UI location.",
615
+ parameters: [
616
+ {
617
+ name: "id",
618
+ type: "string",
619
+ description: "The panel location identifier.",
620
+ },
621
+ {
622
+ name: "mode",
623
+ type: "number",
624
+ description: "The panel display mode or size.",
625
+ },
626
+ {
627
+ name: "html",
628
+ type: "HTMLElement | HTMLElement[] | string",
629
+ description: "The panel content.",
630
+ },
631
+ {
632
+ name: "script",
633
+ type: "string",
634
+ description: "A script associated with the panel content.",
635
+ },
636
+ ],
637
+ },
638
+ "editor.focus": {
639
+ callback: () => {
640
+ client.focus();
641
+ },
642
+ description: "Returns focus to the main editor.",
643
+ },
644
+ "editor.hidePanel": {
645
+ callback: (_ctx, id: string) => {
646
+ client.ui.viewDispatch({
647
+ type: "hide-panel",
648
+ id: id as any,
649
+ });
650
+ setTimeout(() => {
651
+ // Dummy dispatch to rerender the editor and toggle the panel
652
+ client.editorView.dispatch({});
653
+ });
654
+ },
655
+ description: "Hides the panel at a specified editor UI location.",
656
+ parameters: [
657
+ {
658
+ name: "id",
659
+ type: "string",
660
+ description: "The panel location identifier.",
661
+ },
662
+ ],
663
+ },
664
+ "editor.showProgress": {
665
+ callback: (
666
+ _ctx,
667
+ progressPercentage?: number,
668
+ progressType?: "sync" | "index",
669
+ ) => {
670
+ client.ui.showProgress(progressPercentage, progressType);
671
+ },
672
+ description:
673
+ "Shows, updates, or hides a sync or indexing progress indicator.",
674
+ parameters: [
675
+ {
676
+ name: "progressPercentage",
677
+ type: "number",
678
+ description:
679
+ "Completion percentage, or undefined to hide the indicator.",
680
+ optional: true,
681
+ },
682
+ {
683
+ name: "progressType",
684
+ type: "sync | index",
685
+ description: "The operation represented by the indicator.",
686
+ optional: true,
687
+ },
688
+ ],
689
+ },
690
+ "editor.insertAtPos": {
691
+ callback: (
692
+ _ctx,
693
+ text: string,
694
+ pos: number,
695
+ cursorPlaceHolder = false,
696
+ ) => {
697
+ let cursorPlaceholderPos = -1;
698
+ if (cursorPlaceHolder) {
699
+ cursorPlaceholderPos = text.indexOf("|^|");
700
+ if (cursorPlaceholderPos !== -1) {
701
+ text =
702
+ text.slice(0, cursorPlaceholderPos) +
703
+ text.slice(cursorPlaceholderPos + 3);
325
704
  } else {
326
- const reader = new FileReader();
327
- reader.readAsArrayBuffer(file);
328
- reader.onloadend = async (evt) => {
329
- if (evt.target?.readyState === FileReader.DONE) {
330
- resolve({
331
- name: file.name,
332
- contentType: file.type,
333
- content: new Uint8Array(await file.arrayBuffer()),
334
- });
335
- }
336
- };
337
- reader.onabort = (e) => {
338
- reject(e);
339
- };
340
- reader.onerror = (e) => {
341
- reject(e);
342
- };
705
+ cursorPlaceHolder = false;
343
706
  }
344
- };
345
- input.onabort = (e) => {
346
- reject(e);
347
- };
348
-
349
- input.style.display = "none";
350
- document.body.appendChild(input);
351
- input.click();
352
- setTimeout(() => document.body.removeChild(input), 1000);
353
- });
354
- },
355
- "editor.flashNotification": (
356
- _ctx,
357
- message: string,
358
- type: NotificationType = "info",
359
- options?: { timeout?: number; actions?: NotificationAction[] },
360
- ) => {
361
- client.ui.flashNotification(message, type, options);
362
- },
363
- "editor.filterBox": (
364
- _ctx,
365
- label: string,
366
- options: FilterOption[],
367
- helpText = "",
368
- placeHolder = "",
369
- ): Promise<FilterOption | undefined> => {
370
- return client.ui.filterBox(label, options, helpText, placeHolder);
371
- },
372
- "editor.showPanel": (
373
- _ctx,
374
- id: string,
375
- mode: number,
376
- html: HTMLElement | HTMLElement[] | string,
377
- script: string,
378
- ) => {
379
- client.ui.viewDispatch({
380
- type: "show-panel",
381
- id: id as any,
382
- config: { html, script, mode },
383
- });
384
- setTimeout(() => {
385
- // Dummy dispatch to rerender the editor and toggle the panel
386
- client.editorView.dispatch({});
387
- });
388
- },
389
- "editor.focus": () => {
390
- client.focus();
391
- },
392
- "editor.hidePanel": (_ctx, id: string) => {
393
- client.ui.viewDispatch({
394
- type: "hide-panel",
395
- id: id as any,
396
- });
397
- setTimeout(() => {
398
- // Dummy dispatch to rerender the editor and toggle the panel
399
- client.editorView.dispatch({});
400
- });
401
- },
402
- "editor.showProgress": (
403
- _ctx,
404
- progressPercentage?: number,
405
- progressType?: "sync" | "index",
406
- ) => {
407
- client.ui.showProgress(progressPercentage, progressType);
408
- },
409
- "editor.insertAtPos": (
410
- _ctx,
411
- text: string,
412
- pos: number,
413
- cursorPlaceHolder = false,
414
- ) => {
415
- let cursorPlaceholderPos = -1;
416
- if (cursorPlaceHolder) {
417
- cursorPlaceholderPos = text.indexOf("|^|");
418
- if (cursorPlaceholderPos !== -1) {
707
+ }
708
+ client.editorView.dispatch({
709
+ changes: {
710
+ insert: text,
711
+ from: pos,
712
+ },
713
+ });
714
+ if (cursorPlaceHolder) {
715
+ const cursorPos = pos + cursorPlaceholderPos;
716
+ client.editorView.dispatch({
717
+ selection: {
718
+ anchor: cursorPos,
719
+ },
720
+ effects: [EditorView.scrollIntoView(cursorPos)],
721
+ });
722
+ }
723
+ },
724
+ description:
725
+ "Inserts text at a character offset, optionally placing the cursor at a `|^|` marker.",
726
+ parameters: [
727
+ { name: "text", type: "string", description: "The text to insert." },
728
+ {
729
+ name: "pos",
730
+ type: "number",
731
+ description: "The character offset at which to insert.",
732
+ },
733
+ {
734
+ name: "cursorPlaceHolder",
735
+ type: "boolean",
736
+ description:
737
+ "Whether to remove `|^|` and move the cursor to its position.",
738
+ optional: true,
739
+ },
740
+ ],
741
+ },
742
+ "editor.replaceRange": {
743
+ callback: (
744
+ _ctx,
745
+ from: number,
746
+ to: number,
747
+ text: string,
748
+ cursorPlaceHolder = false,
749
+ ) => {
750
+ let cursorPlaceholderPos = -1;
751
+ if (cursorPlaceHolder) {
752
+ cursorPlaceholderPos = text.indexOf("|^|");
419
753
  text =
420
754
  text.slice(0, cursorPlaceholderPos) +
421
755
  text.slice(cursorPlaceholderPos + 3);
422
- } else {
423
- cursorPlaceHolder = false;
424
756
  }
425
- }
426
- client.editorView.dispatch({
427
- changes: {
428
- insert: text,
429
- from: pos,
430
- },
431
- });
432
- if (cursorPlaceHolder) {
433
- const cursorPos = pos + cursorPlaceholderPos;
434
757
  client.editorView.dispatch({
435
- selection: {
436
- anchor: cursorPos,
758
+ changes: {
759
+ insert: text,
760
+ from: from,
761
+ to: to,
437
762
  },
438
- effects: [EditorView.scrollIntoView(cursorPos)],
439
763
  });
440
- }
441
- },
442
- "editor.replaceRange": (
443
- _ctx,
444
- from: number,
445
- to: number,
446
- text: string,
447
- cursorPlaceHolder = false,
448
- ) => {
449
- let cursorPlaceholderPos = -1;
450
- if (cursorPlaceHolder) {
451
- cursorPlaceholderPos = text.indexOf("|^|");
452
- text =
453
- text.slice(0, cursorPlaceholderPos) +
454
- text.slice(cursorPlaceholderPos + 3);
455
- }
456
- client.editorView.dispatch({
457
- changes: {
458
- insert: text,
459
- from: from,
460
- to: to,
461
- },
462
- });
463
- if (cursorPlaceHolder) {
464
- const cursorPos = from + cursorPlaceholderPos;
764
+ if (cursorPlaceHolder) {
765
+ const cursorPos = from + cursorPlaceholderPos;
766
+ client.editorView.dispatch({
767
+ selection: {
768
+ anchor: cursorPos,
769
+ },
770
+ effects: [EditorView.scrollIntoView(cursorPos)],
771
+ });
772
+ }
773
+ },
774
+ description:
775
+ "Replaces a text range, optionally placing the cursor at a `|^|` marker in the replacement.",
776
+ parameters: [
777
+ {
778
+ name: "from",
779
+ type: "number",
780
+ description: "The start offset of the range.",
781
+ },
782
+ {
783
+ name: "to",
784
+ type: "number",
785
+ description: "The end offset of the range.",
786
+ },
787
+ {
788
+ name: "text",
789
+ type: "string",
790
+ description: "The replacement text.",
791
+ },
792
+ {
793
+ name: "cursorPlaceHolder",
794
+ type: "boolean",
795
+ description:
796
+ "Whether to remove `|^|` and move the cursor to its position.",
797
+ optional: true,
798
+ },
799
+ ],
800
+ },
801
+ "editor.moveCursor": {
802
+ callback: (_ctx, pos: number, center = false) => {
803
+ // Always scroll into view so callers don't strand the cursor
804
+ // off-screen; `nearest` (default) is a no-op when already visible.
805
+ client.editorView.dispatch({
806
+ selection: { anchor: pos },
807
+ effects: [
808
+ EditorView.scrollIntoView(pos, center ? { y: "center" } : {}),
809
+ ],
810
+ });
811
+ client.editorView.focus();
812
+ },
813
+ description:
814
+ "Moves and focuses the cursor at a character offset, scrolling it into view.",
815
+ parameters: [
816
+ {
817
+ name: "pos",
818
+ type: "number",
819
+ description: "The character offset to move to.",
820
+ },
821
+ {
822
+ name: "center",
823
+ type: "boolean",
824
+ description: "Whether to vertically center the cursor.",
825
+ optional: true,
826
+ },
827
+ ],
828
+ },
829
+ "editor.moveCursorToLine": {
830
+ callback: (_ctx, line: number, column = 1, center = false) => {
831
+ // CodeMirror already keeps information about lines
832
+ const cmLine = client.editorView.state.doc.line(line);
833
+ // How much to move inside the line, column number starts from 1
834
+ const offset = Math.max(0, Math.min(cmLine.length, column - 1));
835
+ // Just reuse the implementation above
836
+ const moveCursor = syscalls["editor.moveCursor"];
837
+ const moveCursorCallback =
838
+ typeof moveCursor === "function" ? moveCursor : moveCursor.callback;
839
+ moveCursorCallback(_ctx, cmLine.from + offset, center);
840
+ },
841
+ description:
842
+ "Moves the cursor to a one-based line and column, clamping the column to the line length.",
843
+ parameters: [
844
+ {
845
+ name: "line",
846
+ type: "number",
847
+ description: "The one-based line number.",
848
+ },
849
+ {
850
+ name: "column",
851
+ type: "number",
852
+ description: "The one-based column number.",
853
+ optional: true,
854
+ },
855
+ {
856
+ name: "center",
857
+ type: "boolean",
858
+ description: "Whether to vertically center the cursor.",
859
+ optional: true,
860
+ },
861
+ ],
862
+ },
863
+ "editor.setSelection": {
864
+ callback: (_ctx, from: number, to: number) => {
465
865
  client.editorView.dispatch({
466
866
  selection: {
467
- anchor: cursorPos,
867
+ anchor: from,
868
+ head: to,
468
869
  },
469
- effects: [EditorView.scrollIntoView(cursorPos)],
470
870
  });
471
- }
472
- },
473
- "editor.moveCursor": (_ctx, pos: number, center = false) => {
474
- // Always scroll into view so callers don't strand the cursor
475
- // off-screen; `nearest` (default) is a no-op when already visible.
476
- client.editorView.dispatch({
477
- selection: { anchor: pos },
478
- effects: [
479
- EditorView.scrollIntoView(pos, center ? { y: "center" } : {}),
480
- ],
481
- });
482
- client.editorView.focus();
483
- },
484
- "editor.moveCursorToLine": (
485
- _ctx,
486
- line: number,
487
- column = 1,
488
- center = false,
489
- ) => {
490
- // CodeMirror already keeps information about lines
491
- const cmLine = client.editorView.state.doc.line(line);
492
- // How much to move inside the line, column number starts from 1
493
- const offset = Math.max(0, Math.min(cmLine.length, column - 1));
494
- // Just reuse the implementation above
495
- syscalls["editor.moveCursor"](_ctx, cmLine.from + offset, center);
496
- },
497
- "editor.setSelection": (_ctx, from: number, to: number) => {
498
- client.editorView.dispatch({
499
- selection: {
500
- anchor: from,
501
- head: to,
502
- },
503
- });
871
+ },
872
+ description: "Sets the main editor selection to a character range.",
873
+ parameters: [
874
+ {
875
+ name: "from",
876
+ type: "number",
877
+ description: "The selection anchor offset.",
878
+ },
879
+ {
880
+ name: "to",
881
+ type: "number",
882
+ description: "The selection head offset.",
883
+ },
884
+ ],
504
885
  },
505
886
 
506
- "editor.insertAtCursor": (
507
- _ctx,
508
- text: string,
509
- scrollIntoView = false,
510
- cursorPlaceHolder = false,
511
- ) => {
512
- const editorView = client.editorView;
513
- const from = editorView.state.selection.main.from;
514
- const cursorPlaceholderPos = text.indexOf("|^|");
515
- if (cursorPlaceHolder && cursorPlaceholderPos !== -1) {
516
- text =
517
- text.slice(0, cursorPlaceholderPos) +
518
- text.slice(cursorPlaceholderPos + 3);
519
- } else {
520
- cursorPlaceHolder = false;
521
- }
522
- editorView.dispatch({
523
- changes: {
524
- insert: text,
525
- from: from,
526
- },
527
- selection: {
528
- anchor: cursorPlaceHolder
529
- ? from + cursorPlaceholderPos
530
- : from + text.length,
531
- },
532
- scrollIntoView,
533
- });
534
- },
535
- "editor.dispatch": (_ctx, change: Transaction) => {
536
- client.editorView.dispatch(change);
537
- },
538
- "editor.prompt": (
539
- _ctx,
540
- message: string,
541
- defaultValue = "",
542
- ): Promise<string | undefined> => {
543
- return client.ui.prompt(message, defaultValue);
544
- },
545
- "editor.confirm": (
546
- _ctx,
547
- message: string,
548
- options?: { destructive?: boolean },
549
- ): Promise<boolean> => {
550
- return client.ui.confirm(message, options);
551
- },
552
- "editor.alert": (_ctx, message: string) => {
553
- alert(message);
554
- },
555
- "editor.getUiOption": (_ctx, key: string): any => {
556
- return (client.ui.viewState.uiOptions as any)[key];
557
- },
558
- "editor.setUiOption": (_ctx, key: string, value: any) => {
559
- client.ui.viewDispatch({
560
- type: "set-ui-option",
561
- key,
562
- value,
563
- });
564
- void client.reloadEditor();
565
- },
566
- "editor.vimEx": (_ctx, exCommand: string) => {
567
- const vimMod = getVimModule();
568
- if (!vimMod) {
569
- throw new Error("Vim module not loaded.");
570
- }
571
- const cm = vimMod.getCM(client.editorView);
572
- if (cm?.state.vim) {
573
- return vimMod.Vim.handleEx(cm as any, exCommand);
574
- } else {
575
- throw new Error("Vim mode not active or not initialized.");
576
- }
577
- },
578
- "editor.configureVimMode": () => {
579
- const vimMod = getVimModule();
580
- if (!vimMod) {
581
- throw new Error("Vim module not loaded.");
582
- }
583
- const { Vim } = vimMod;
584
- // Override the default "o" binding to be more intelligent and follow the markdown editor's behavior
585
- Vim.mapCommand("o", "action", "newline-continue-markup", {}, {});
586
- Vim.mapCommand("O", "action", "back-newline-continue-markup", {}, {});
587
- Vim.unmap("<C-q>", undefined as any);
588
- Vim.defineAction("newline-continue-markup", (cm) => {
589
- // Append at end of line
590
- Vim.handleKey(cm, "A", "+input");
591
- // Insert newline continuing markup where appropriate
592
- insertNewlineContinueMarkup(client.editorView) ||
593
- insertNewlineAndIndent(client.editorView);
594
- });
595
- Vim.defineAction("back-newline-continue-markup", (cm) => {
596
- // Determine current line
597
- const pos = client.editorView.state.selection.main.from;
598
- const line = client.editorView.state.doc.lineAt(pos).number;
599
- if (line === 1) {
600
- // We're on the top line
601
- // Go to 0:0
602
- Vim.handleKey(cm, "0", "+input");
603
- // Insert a newline
604
- insertNewline(client.editorView);
605
- // Go up to the new line
606
- Vim.handleKey(cm, "k", "+input");
607
- // Into insert mode
608
- Vim.handleKey(cm, "i", "+input");
887
+ "editor.insertAtCursor": {
888
+ callback: (
889
+ _ctx,
890
+ text: string,
891
+ scrollIntoView = false,
892
+ cursorPlaceHolder = false,
893
+ ) => {
894
+ const editorView = client.editorView;
895
+ const from = editorView.state.selection.main.from;
896
+ const cursorPlaceholderPos = text.indexOf("|^|");
897
+ if (cursorPlaceHolder && cursorPlaceholderPos !== -1) {
898
+ text =
899
+ text.slice(0, cursorPlaceholderPos) +
900
+ text.slice(cursorPlaceholderPos + 3);
901
+ } else {
902
+ cursorPlaceHolder = false;
903
+ }
904
+ editorView.dispatch({
905
+ changes: {
906
+ insert: text,
907
+ from: from,
908
+ },
909
+ selection: {
910
+ anchor: cursorPlaceHolder
911
+ ? from + cursorPlaceholderPos
912
+ : from + text.length,
913
+ },
914
+ scrollIntoView,
915
+ });
916
+ },
917
+ description:
918
+ "Inserts text at the cursor and moves the cursor after it or to an optional `|^|` marker.",
919
+ parameters: [
920
+ { name: "text", type: "string", description: "The text to insert." },
921
+ {
922
+ name: "scrollIntoView",
923
+ type: "boolean",
924
+ description: "Whether to scroll the new cursor position into view.",
925
+ optional: true,
926
+ },
927
+ {
928
+ name: "cursorPlaceHolder",
929
+ type: "boolean",
930
+ description:
931
+ "Whether to remove `|^|` and move the cursor to its position.",
932
+ optional: true,
933
+ },
934
+ ],
935
+ },
936
+ "editor.dispatch": {
937
+ callback: (_ctx, change: Transaction) => {
938
+ client.editorView.dispatch(change);
939
+ },
940
+ description: "Dispatches a CodeMirror transaction to the editor view.",
941
+ parameters: [
942
+ {
943
+ name: "change",
944
+ type: "Transaction",
945
+ description: "The CodeMirror transaction to dispatch.",
946
+ },
947
+ ],
948
+ },
949
+ "editor.prompt": {
950
+ callback: (
951
+ _ctx,
952
+ message: string,
953
+ defaultValue = "",
954
+ ): Promise<string | undefined> => {
955
+ return client.ui.prompt(message, defaultValue);
956
+ },
957
+ description: "Prompts the user for text input.",
958
+ parameters: [
959
+ {
960
+ name: "message",
961
+ type: "string",
962
+ description: "The prompt message.",
963
+ },
964
+ {
965
+ name: "defaultValue",
966
+ type: "string",
967
+ description: "The initial input value.",
968
+ optional: true,
969
+ },
970
+ ],
971
+ returns: [
972
+ {
973
+ type: "string | undefined",
974
+ description: "The entered text, or undefined if dismissed.",
975
+ },
976
+ ],
977
+ },
978
+ "editor.confirm": {
979
+ callback: (
980
+ _ctx,
981
+ message: string,
982
+ options?: { destructive?: boolean },
983
+ ): Promise<boolean> => {
984
+ return client.ui.confirm(message, options);
985
+ },
986
+ description: "Prompts the user to confirm or cancel an action.",
987
+ parameters: [
988
+ {
989
+ name: "message",
990
+ type: "string",
991
+ description: "The confirmation message.",
992
+ },
993
+ {
994
+ name: "options",
995
+ type: "{ destructive?: boolean }",
996
+ description: "Optional dialog styling settings.",
997
+ optional: true,
998
+ },
999
+ ],
1000
+ returns: [
1001
+ { type: "boolean", description: "Whether the user confirmed." },
1002
+ ],
1003
+ },
1004
+ "editor.alert": {
1005
+ callback: (_ctx, message: string) => {
1006
+ alert(message);
1007
+ },
1008
+ description: "Shows a browser alert dialog.",
1009
+ parameters: [
1010
+ {
1011
+ name: "message",
1012
+ type: "string",
1013
+ description: "The alert message.",
1014
+ },
1015
+ ],
1016
+ },
1017
+ "editor.getUiOption": {
1018
+ callback: (_ctx, key: string): any => {
1019
+ return (client.ui.viewState.uiOptions as any)[key];
1020
+ },
1021
+ description: "Returns the current value of an editor UI option.",
1022
+ parameters: [
1023
+ { name: "key", type: "string", description: "The UI option key." },
1024
+ ],
1025
+ returns: [{ type: "any", description: "The option value." }],
1026
+ },
1027
+ "editor.setUiOption": {
1028
+ callback: (_ctx, key: string, value: any) => {
1029
+ client.ui.viewDispatch({
1030
+ type: "set-ui-option",
1031
+ key,
1032
+ value,
1033
+ });
1034
+ void client.reloadEditor();
1035
+ },
1036
+ description: "Sets an editor UI option and reloads the editor.",
1037
+ parameters: [
1038
+ { name: "key", type: "string", description: "The UI option key." },
1039
+ { name: "value", type: "any", description: "The option value." },
1040
+ ],
1041
+ },
1042
+ "editor.vimEx": {
1043
+ callback: (_ctx, exCommand: string) => {
1044
+ const vimMod = getVimModule();
1045
+ if (!vimMod) {
1046
+ throw new Error("Vim module not loaded.");
1047
+ }
1048
+ const cm = vimMod.getCM(client.editorView);
1049
+ if (cm?.state.vim) {
1050
+ return vimMod.Vim.handleEx(cm as any, exCommand);
609
1051
  } else {
610
- // We're elsewhere in the document
611
- // Go up
612
- Vim.handleKey(cm, "k", "+input");
613
- // Append mode at the end of the line
1052
+ throw new Error("Vim mode not active or not initialized.");
1053
+ }
1054
+ },
1055
+ description: "Executes a Vim Ex command in the active Vim-mode editor.",
1056
+ parameters: [
1057
+ {
1058
+ name: "exCommand",
1059
+ type: "string",
1060
+ description: "The Ex command to execute.",
1061
+ },
1062
+ ],
1063
+ },
1064
+ "editor.configureVimMode": {
1065
+ callback: () => {
1066
+ const vimMod = getVimModule();
1067
+ if (!vimMod) {
1068
+ throw new Error("Vim module not loaded.");
1069
+ }
1070
+ const { Vim } = vimMod;
1071
+ // Override the default "o" binding to be more intelligent and follow the markdown editor's behavior
1072
+ Vim.mapCommand("o", "action", "newline-continue-markup", {}, {});
1073
+ Vim.mapCommand("O", "action", "back-newline-continue-markup", {}, {});
1074
+ Vim.unmap("<C-q>", undefined as any);
1075
+ Vim.defineAction("newline-continue-markup", (cm) => {
1076
+ // Append at end of line
614
1077
  Vim.handleKey(cm, "A", "+input");
615
- // Insert a newline using the continue markup thing
1078
+ // Insert newline continuing markup where appropriate
616
1079
  insertNewlineContinueMarkup(client.editorView) ||
617
1080
  insertNewlineAndIndent(client.editorView);
618
- }
619
- });
1081
+ });
1082
+ Vim.defineAction("back-newline-continue-markup", (cm) => {
1083
+ // Determine current line
1084
+ const pos = client.editorView.state.selection.main.from;
1085
+ const line = client.editorView.state.doc.lineAt(pos).number;
1086
+ if (line === 1) {
1087
+ // We're on the top line
1088
+ // Go to 0:0
1089
+ Vim.handleKey(cm, "0", "+input");
1090
+ // Insert a newline
1091
+ insertNewline(client.editorView);
1092
+ // Go up to the new line
1093
+ Vim.handleKey(cm, "k", "+input");
1094
+ // Into insert mode
1095
+ Vim.handleKey(cm, "i", "+input");
1096
+ } else {
1097
+ // We're elsewhere in the document
1098
+ // Go up
1099
+ Vim.handleKey(cm, "k", "+input");
1100
+ // Append mode at the end of the line
1101
+ Vim.handleKey(cm, "A", "+input");
1102
+ // Insert a newline using the continue markup thing
1103
+ insertNewlineContinueMarkup(client.editorView) ||
1104
+ insertNewlineAndIndent(client.editorView);
1105
+ }
1106
+ });
620
1107
 
621
- // Load the config if any
622
- const config = client.config.get<VimConfig>("vim", {});
623
- if (config) {
624
- config.unmap?.forEach((binding) => {
625
- if (typeof binding === "string") {
626
- console.log(`Unmapping ${binding}`);
627
- // @ts-expect-error: unmap expects a string for the mode, this is problematic with Ex mappings which requires undefined or false
628
- Vim.unmap(binding, undefined);
629
- } else if (binding.key) {
630
- console.log(
631
- `Unmapping ${binding.key} in ${binding.mode ?? "normal"}`,
1108
+ // Load the config if any
1109
+ const config = client.config.get<VimConfig>("vim", {});
1110
+ if (config) {
1111
+ config.unmap?.forEach((binding) => {
1112
+ if (typeof binding === "string") {
1113
+ console.log(`Unmapping ${binding}`);
1114
+ // @ts-expect-error: unmap expects a string for the mode, this is problematic with Ex mappings which requires undefined or false
1115
+ Vim.unmap(binding, undefined);
1116
+ } else if (binding.key) {
1117
+ console.log(
1118
+ `Unmapping ${binding.key} in ${binding.mode ?? "normal"}`,
1119
+ );
1120
+ Vim.unmap(binding.key, binding.mode ?? "normal");
1121
+ }
1122
+ });
1123
+ config.map?.forEach(({ map, to, mode }) => {
1124
+ console.log(`Mapping ${map} to ${to} for ${mode ?? "normal"}`);
1125
+ Vim.map(map, to, mode ?? "normal");
1126
+ });
1127
+ config.noremap?.forEach(({ map, to, mode }) => {
1128
+ console.log(`Noremapping ${map} to ${to} for ${mode ?? "normal"}`);
1129
+ Vim.noremap(map, to, mode ?? "normal");
1130
+ });
1131
+ config.commands?.forEach(({ ex, command }) => {
1132
+ console.log(`Mapping command '${command}' to Ex ${ex}`);
1133
+ Vim.defineEx(ex, "", (_, params) =>
1134
+ client.runCommandByName(command, params.args),
632
1135
  );
633
- Vim.unmap(binding.key, binding.mode ?? "normal");
634
- }
1136
+ });
1137
+ } else {
1138
+ console.log("No vim config found");
1139
+ }
1140
+ },
1141
+ description:
1142
+ "Configures CodeMirror Vim mode from the current SilverBullet Vim settings.",
1143
+ },
1144
+ "editor.openPageNavigator": {
1145
+ callback: (_ctx, mode: "page" | "meta" | "document" | "all" = "page") => {
1146
+ client.startPageNavigate(mode);
1147
+ },
1148
+ description: "Opens the page navigator in the requested browsing mode.",
1149
+ parameters: [
1150
+ {
1151
+ name: "mode",
1152
+ type: "page | meta | document | all",
1153
+ description: "The navigator mode.",
1154
+ optional: true,
1155
+ },
1156
+ ],
1157
+ },
1158
+ "editor.openCommandPalette": {
1159
+ callback: () => {
1160
+ void client.startCommandPalette();
1161
+ },
1162
+ description: "Opens the command palette.",
1163
+ },
1164
+ "editor.deleteLine": {
1165
+ callback: () => {
1166
+ deleteLine(client.editorView);
1167
+ },
1168
+ description:
1169
+ "Deletes the current line or the lines touched by the selection.",
1170
+ },
1171
+ "editor.selectAll": {
1172
+ callback: () => {
1173
+ return selectAll({
1174
+ state: client.editorView.state,
1175
+ dispatch: client.editorView.dispatch,
1176
+ });
1177
+ },
1178
+ description: "Selects the entire editor document.",
1179
+ },
1180
+ "editor.indentMore": {
1181
+ callback: () => {
1182
+ return indentMore({
1183
+ state: client.editorView.state,
1184
+ dispatch: client.editorView.dispatch,
635
1185
  });
636
- config.map?.forEach(({ map, to, mode }) => {
637
- console.log(`Mapping ${map} to ${to} for ${mode ?? "normal"}`);
638
- Vim.map(map, to, mode ?? "normal");
1186
+ },
1187
+ description: "Increases indentation for the current line or selection.",
1188
+ },
1189
+ "editor.indentLess": {
1190
+ callback: () => {
1191
+ return indentLess({
1192
+ state: client.editorView.state,
1193
+ dispatch: client.editorView.dispatch,
639
1194
  });
640
- config.noremap?.forEach(({ map, to, mode }) => {
641
- console.log(`Noremapping ${map} to ${to} for ${mode ?? "normal"}`);
642
- Vim.noremap(map, to, mode ?? "normal");
1195
+ },
1196
+ description: "Decreases indentation for the current line or selection.",
1197
+ },
1198
+ "editor.toggleComment": {
1199
+ callback: () => {
1200
+ return toggleComment({
1201
+ state: client.editorView.state,
1202
+ dispatch: client.editorView.dispatch,
643
1203
  });
644
- config.commands?.forEach(({ ex, command }) => {
645
- console.log(`Mapping command '${command}' to Ex ${ex}`);
646
- Vim.defineEx(ex, "", (_, params) =>
647
- client.runCommandByName(command, params.args),
648
- );
1204
+ },
1205
+ description: "Comments or uncomments the current line or selection.",
1206
+ },
1207
+ "editor.moveLineUp": {
1208
+ callback: () => {
1209
+ return moveLineUp({
1210
+ state: client.editorView.state,
1211
+ dispatch: client.editorView.dispatch,
649
1212
  });
650
- } else {
651
- console.log("No vim config found");
652
- }
653
- },
654
- "editor.openPageNavigator": (
655
- _ctx,
656
- mode: "page" | "meta" | "document" | "all" = "page",
657
- ) => {
658
- client.startPageNavigate(mode);
659
- },
660
- "editor.openCommandPalette": () => {
661
- void client.startCommandPalette();
662
- },
663
- "editor.deleteLine": () => {
664
- deleteLine(client.editorView);
665
- },
666
- "editor.selectAll": () => {
667
- return selectAll({
668
- state: client.editorView.state,
669
- dispatch: client.editorView.dispatch,
670
- });
671
- },
672
- "editor.indentMore": () => {
673
- return indentMore({
674
- state: client.editorView.state,
675
- dispatch: client.editorView.dispatch,
676
- });
677
- },
678
- "editor.indentLess": () => {
679
- return indentLess({
680
- state: client.editorView.state,
681
- dispatch: client.editorView.dispatch,
682
- });
683
- },
684
- "editor.toggleComment": () => {
685
- return toggleComment({
686
- state: client.editorView.state,
687
- dispatch: client.editorView.dispatch,
688
- });
689
- },
690
- "editor.moveLineUp": () => {
691
- return moveLineUp({
692
- state: client.editorView.state,
693
- dispatch: client.editorView.dispatch,
694
- });
695
- },
696
- "editor.moveLineDown": () => {
697
- return moveLineDown({
698
- state: client.editorView.state,
699
- dispatch: client.editorView.dispatch,
700
- });
1213
+ },
1214
+ description: "Moves the current line or selected lines upward.",
1215
+ },
1216
+ "editor.moveLineDown": {
1217
+ callback: () => {
1218
+ return moveLineDown({
1219
+ state: client.editorView.state,
1220
+ dispatch: client.editorView.dispatch,
1221
+ });
1222
+ },
1223
+ description: "Moves the current line or selected lines downward.",
701
1224
  },
702
1225
  // Folding
703
- "editor.fold": () => {
704
- foldCode(client.editorView);
705
- },
706
- "editor.unfold": () => {
707
- unfoldCode(client.editorView);
708
- },
709
- "editor.toggleFold": () => {
710
- toggleFold(client.editorView);
711
- },
712
- "editor.foldAll": () => {
713
- foldAll(client.editorView);
714
- },
715
- "editor.unfoldAll": () => {
716
- unfoldAll(client.editorView);
717
- },
718
- "editor.undo": () => {
719
- return undo(client.editorView);
720
- },
721
- "editor.redo": () => {
722
- return redo(client.editorView);
1226
+ "editor.fold": {
1227
+ callback: () => {
1228
+ foldCode(client.editorView);
1229
+ },
1230
+ description: "Folds the code or markup region at the cursor.",
1231
+ },
1232
+ "editor.unfold": {
1233
+ callback: () => {
1234
+ unfoldCode(client.editorView);
1235
+ },
1236
+ description: "Unfolds the folded region at the cursor.",
1237
+ },
1238
+ "editor.toggleFold": {
1239
+ callback: () => {
1240
+ toggleFold(client.editorView);
1241
+ },
1242
+ description: "Toggles folding for the region at the cursor.",
1243
+ },
1244
+ "editor.foldAll": {
1245
+ callback: () => {
1246
+ foldAll(client.editorView);
1247
+ },
1248
+ description: "Folds all foldable regions in the editor.",
1249
+ },
1250
+ "editor.unfoldAll": {
1251
+ callback: () => {
1252
+ unfoldAll(client.editorView);
1253
+ },
1254
+ description: "Unfolds all folded regions in the editor.",
1255
+ },
1256
+ "editor.undo": {
1257
+ callback: () => {
1258
+ return undo(client.editorView);
1259
+ },
1260
+ description: "Undoes the most recent editor change.",
1261
+ },
1262
+ "editor.redo": {
1263
+ callback: () => {
1264
+ return redo(client.editorView);
1265
+ },
1266
+ description: "Redoes the most recently undone editor change.",
723
1267
  },
724
1268
  // Cursor motion syscalls
725
- "editor.cursorCharLeft": () => cursorCharLeft(client.editorView),
726
- "editor.cursorCharRight": () => cursorCharRight(client.editorView),
727
- "editor.cursorGroupLeft": () => cursorGroupLeft(client.editorView),
728
- "editor.cursorGroupRight": () => cursorGroupRight(client.editorView),
729
- "editor.cursorLineBoundaryLeft": () =>
730
- cursorLineBoundaryLeft(client.editorView),
731
- "editor.cursorLineBoundaryRight": () =>
732
- cursorLineBoundaryRight(client.editorView),
733
- "editor.cursorLineStart": () => cursorLineStart(client.editorView),
734
- "editor.cursorLineEnd": () => cursorLineEnd(client.editorView),
735
- "editor.cursorDocStart": () => cursorDocStart(client.editorView),
736
- "editor.cursorDocEnd": () => cursorDocEnd(client.editorView),
1269
+ "editor.cursorCharLeft": {
1270
+ callback: () => cursorCharLeft(client.editorView),
1271
+ description:
1272
+ "Moves the cursor one character left, respecting bidirectional text.",
1273
+ },
1274
+ "editor.cursorCharRight": {
1275
+ callback: () => cursorCharRight(client.editorView),
1276
+ description:
1277
+ "Moves the cursor one character right, respecting bidirectional text.",
1278
+ },
1279
+ "editor.cursorGroupLeft": {
1280
+ callback: () => cursorGroupLeft(client.editorView),
1281
+ description: "Moves the cursor left by one character group or word.",
1282
+ },
1283
+ "editor.cursorGroupRight": {
1284
+ callback: () => cursorGroupRight(client.editorView),
1285
+ description: "Moves the cursor right by one character group or word.",
1286
+ },
1287
+ "editor.cursorLineBoundaryLeft": {
1288
+ callback: () => cursorLineBoundaryLeft(client.editorView),
1289
+ description:
1290
+ "Moves the cursor to the left visual boundary of the current line.",
1291
+ },
1292
+ "editor.cursorLineBoundaryRight": {
1293
+ callback: () => cursorLineBoundaryRight(client.editorView),
1294
+ description:
1295
+ "Moves the cursor to the right visual boundary of the current line.",
1296
+ },
1297
+ "editor.cursorLineStart": {
1298
+ callback: () => cursorLineStart(client.editorView),
1299
+ description: "Moves the cursor to the start of the current logical line.",
1300
+ },
1301
+ "editor.cursorLineEnd": {
1302
+ callback: () => cursorLineEnd(client.editorView),
1303
+ description: "Moves the cursor to the end of the current logical line.",
1304
+ },
1305
+ "editor.cursorDocStart": {
1306
+ callback: () => cursorDocStart(client.editorView),
1307
+ description: "Moves the cursor to the start of the document.",
1308
+ },
1309
+ "editor.cursorDocEnd": {
1310
+ callback: () => cursorDocEnd(client.editorView),
1311
+ description: "Moves the cursor to the end of the document.",
1312
+ },
737
1313
  // Cursor motions that also navigate the completion popup if it's open
738
- "editor.cursorLineUp": () => {
739
- const view = client.editorView;
740
- if (moveCompletionSelection(false)(view)) return true;
741
- return cursorLineUp(view);
742
- },
743
- "editor.cursorLineDown": () => {
744
- const view = client.editorView;
745
- if (moveCompletionSelection(true)(view)) return true;
746
- return cursorLineDown(view);
747
- },
748
- "editor.cursorPageUp": () => {
749
- const view = client.editorView;
750
- if (moveCompletionSelection(false, "page")(view)) return true;
751
- return cursorPageUp(view);
752
- },
753
- "editor.cursorPageDown": () => {
754
- const view = client.editorView;
755
- if (moveCompletionSelection(true, "page")(view)) return true;
756
- return cursorPageDown(view);
1314
+ "editor.cursorLineUp": {
1315
+ callback: () => {
1316
+ const view = client.editorView;
1317
+ if (moveCompletionSelection(false)(view)) return true;
1318
+ return cursorLineUp(view);
1319
+ },
1320
+ description:
1321
+ "Moves completion selection up when open, otherwise moves the cursor up one visual line.",
1322
+ },
1323
+ "editor.cursorLineDown": {
1324
+ callback: () => {
1325
+ const view = client.editorView;
1326
+ if (moveCompletionSelection(true)(view)) return true;
1327
+ return cursorLineDown(view);
1328
+ },
1329
+ description:
1330
+ "Moves completion selection down when open, otherwise moves the cursor down one visual line.",
1331
+ },
1332
+ "editor.cursorPageUp": {
1333
+ callback: () => {
1334
+ const view = client.editorView;
1335
+ if (moveCompletionSelection(false, "page")(view)) return true;
1336
+ return cursorPageUp(view);
1337
+ },
1338
+ description:
1339
+ "Moves completion selection up one page when open, otherwise moves the cursor up one viewport page.",
1340
+ },
1341
+ "editor.cursorPageDown": {
1342
+ callback: () => {
1343
+ const view = client.editorView;
1344
+ if (moveCompletionSelection(true, "page")(view)) return true;
1345
+ return cursorPageDown(view);
1346
+ },
1347
+ description:
1348
+ "Moves completion selection down one page when open, otherwise moves the cursor down one viewport page.",
757
1349
  },
758
1350
  // Selection-extending motions
759
- "editor.selectCharLeft": () => selectCharLeft(client.editorView),
760
- "editor.selectCharRight": () => selectCharRight(client.editorView),
761
- "editor.selectGroupLeft": () => selectGroupLeft(client.editorView),
762
- "editor.selectGroupRight": () => selectGroupRight(client.editorView),
763
- "editor.selectLineBoundaryLeft": () =>
764
- selectLineBoundaryLeft(client.editorView),
765
- "editor.selectLineBoundaryRight": () =>
766
- selectLineBoundaryRight(client.editorView),
767
- "editor.selectLineStart": () => selectLineStart(client.editorView),
768
- "editor.selectLineEnd": () => selectLineEnd(client.editorView),
769
- "editor.selectDocStart": () => selectDocStart(client.editorView),
770
- "editor.selectDocEnd": () => selectDocEnd(client.editorView),
771
- "editor.selectLineUp": () => selectLineUp(client.editorView),
772
- "editor.selectLineDown": () => selectLineDown(client.editorView),
773
- "editor.selectPageUp": () => selectPageUp(client.editorView),
774
- "editor.selectPageDown": () => selectPageDown(client.editorView),
1351
+ "editor.selectCharLeft": {
1352
+ callback: () => selectCharLeft(client.editorView),
1353
+ description:
1354
+ "Extends the selection one character left, respecting bidirectional text.",
1355
+ },
1356
+ "editor.selectCharRight": {
1357
+ callback: () => selectCharRight(client.editorView),
1358
+ description:
1359
+ "Extends the selection one character right, respecting bidirectional text.",
1360
+ },
1361
+ "editor.selectGroupLeft": {
1362
+ callback: () => selectGroupLeft(client.editorView),
1363
+ description: "Extends the selection left by one character group or word.",
1364
+ },
1365
+ "editor.selectGroupRight": {
1366
+ callback: () => selectGroupRight(client.editorView),
1367
+ description:
1368
+ "Extends the selection right by one character group or word.",
1369
+ },
1370
+ "editor.selectLineBoundaryLeft": {
1371
+ callback: () => selectLineBoundaryLeft(client.editorView),
1372
+ description:
1373
+ "Extends the selection to the left visual boundary of the current line.",
1374
+ },
1375
+ "editor.selectLineBoundaryRight": {
1376
+ callback: () => selectLineBoundaryRight(client.editorView),
1377
+ description:
1378
+ "Extends the selection to the right visual boundary of the current line.",
1379
+ },
1380
+ "editor.selectLineStart": {
1381
+ callback: () => selectLineStart(client.editorView),
1382
+ description:
1383
+ "Extends the selection to the start of the current logical line.",
1384
+ },
1385
+ "editor.selectLineEnd": {
1386
+ callback: () => selectLineEnd(client.editorView),
1387
+ description:
1388
+ "Extends the selection to the end of the current logical line.",
1389
+ },
1390
+ "editor.selectDocStart": {
1391
+ callback: () => selectDocStart(client.editorView),
1392
+ description: "Extends the selection to the start of the document.",
1393
+ },
1394
+ "editor.selectDocEnd": {
1395
+ callback: () => selectDocEnd(client.editorView),
1396
+ description: "Extends the selection to the end of the document.",
1397
+ },
1398
+ "editor.selectLineUp": {
1399
+ callback: () => selectLineUp(client.editorView),
1400
+ description: "Extends the selection upward by one visual line.",
1401
+ },
1402
+ "editor.selectLineDown": {
1403
+ callback: () => selectLineDown(client.editorView),
1404
+ description: "Extends the selection downward by one visual line.",
1405
+ },
1406
+ "editor.selectPageUp": {
1407
+ callback: () => selectPageUp(client.editorView),
1408
+ description: "Extends the selection upward by one viewport page.",
1409
+ },
1410
+ "editor.selectPageDown": {
1411
+ callback: () => selectPageDown(client.editorView),
1412
+ description: "Extends the selection downward by one viewport page.",
1413
+ },
775
1414
  // Delete
776
- "editor.deleteCharBackward": () => deleteCharBackward(client.editorView),
777
- "editor.deleteCharForward": () => deleteCharForward(client.editorView),
778
- "editor.deleteGroupBackward": () => deleteGroupBackward(client.editorView),
779
- "editor.deleteGroupForward": () => deleteGroupForward(client.editorView),
780
- "editor.deleteLineBoundaryBackward": () =>
781
- deleteLineBoundaryBackward(client.editorView),
782
- "editor.deleteLineBoundaryForward": () =>
783
- deleteLineBoundaryForward(client.editorView),
784
- "editor.transposeChars": () => transposeChars(client.editorView),
1415
+ "editor.deleteCharBackward": {
1416
+ callback: () => deleteCharBackward(client.editorView),
1417
+ description: "Deletes the selection or the character before the cursor.",
1418
+ },
1419
+ "editor.deleteCharForward": {
1420
+ callback: () => deleteCharForward(client.editorView),
1421
+ description: "Deletes the selection or the character after the cursor.",
1422
+ },
1423
+ "editor.deleteGroupBackward": {
1424
+ callback: () => deleteGroupBackward(client.editorView),
1425
+ description:
1426
+ "Deletes the selection or the character group before the cursor.",
1427
+ },
1428
+ "editor.deleteGroupForward": {
1429
+ callback: () => deleteGroupForward(client.editorView),
1430
+ description:
1431
+ "Deletes the selection or the character group after the cursor.",
1432
+ },
1433
+ "editor.deleteLineBoundaryBackward": {
1434
+ callback: () => deleteLineBoundaryBackward(client.editorView),
1435
+ description:
1436
+ "Deletes the selection or text back to the current line boundary.",
1437
+ },
1438
+ "editor.deleteLineBoundaryForward": {
1439
+ callback: () => deleteLineBoundaryForward(client.editorView),
1440
+ description:
1441
+ "Deletes the selection or text forward to the current line boundary.",
1442
+ },
1443
+ "editor.transposeChars": {
1444
+ callback: () => transposeChars(client.editorView),
1445
+ description: "Transposes the characters around the cursor.",
1446
+ },
785
1447
  // Enter: accept completion if popup is open, else newline-and-indent
786
- "editor.insertNewline": () => {
787
- const view = client.editorView;
788
- if (acceptCompletion(view)) return true;
789
- return insertNewlineAndIndent(view);
1448
+ "editor.insertNewline": {
1449
+ callback: () => {
1450
+ const view = client.editorView;
1451
+ if (acceptCompletion(view)) return true;
1452
+ return insertNewlineAndIndent(view);
1453
+ },
1454
+ description:
1455
+ "Accepts the active completion, or inserts a newline with appropriate indentation.",
790
1456
  },
791
1457
  // Completion popup control
792
- "editor.acceptCompletion": () => acceptCompletion(client.editorView),
793
- "editor.startCompletion": () => startCompletion(client.editorView),
794
- "editor.closeCompletion": () => closeCompletion(client.editorView),
795
- "editor.openSearchPanel": () => {
796
- openSearchPanel(client.editorView);
797
- },
798
- "editor.copyToClipboard": async (_ctx, data: string | Blob) => {
799
- try {
800
- if (typeof data === "string") {
801
- await navigator.clipboard.writeText(data);
802
- } else {
803
- await navigator.clipboard.write([
804
- new ClipboardItem({ [data.type]: data }),
805
- ]);
1458
+ "editor.acceptCompletion": {
1459
+ callback: () => acceptCompletion(client.editorView),
1460
+ description:
1461
+ "Accepts the currently selected completion when the completion popup is active.",
1462
+ returns: [
1463
+ {
1464
+ type: "boolean",
1465
+ description: "Whether an active completion was accepted.",
1466
+ },
1467
+ ],
1468
+ },
1469
+ "editor.startCompletion": {
1470
+ callback: () => startCompletion(client.editorView),
1471
+ description: "Explicitly starts editor completion at the cursor.",
1472
+ },
1473
+ "editor.closeCompletion": {
1474
+ callback: () => closeCompletion(client.editorView),
1475
+ description: "Closes the active editor completion popup.",
1476
+ },
1477
+ "editor.openSearchPanel": {
1478
+ callback: () => {
1479
+ openSearchPanel(client.editorView);
1480
+ },
1481
+ description: "Opens the editor's native search panel.",
1482
+ },
1483
+ "editor.copyToClipboard": {
1484
+ callback: async (_ctx, data: string | Blob) => {
1485
+ try {
1486
+ if (typeof data === "string") {
1487
+ await navigator.clipboard.writeText(data);
1488
+ } else {
1489
+ await navigator.clipboard.write([
1490
+ new ClipboardItem({ [data.type]: data }),
1491
+ ]);
1492
+ }
1493
+ } catch (e) {
1494
+ console.error(e);
1495
+ client.ui.flashNotification(`Could not copy to clipboard: ${e}`);
806
1496
  }
807
- } catch (e) {
808
- console.error(e);
809
- client.ui.flashNotification(`Could not copy to clipboard: ${e}`);
810
- }
1497
+ },
1498
+ description:
1499
+ "Copies text or binary Blob data to the system clipboard. Clipboard access requires a secure HTTPS context.",
1500
+ parameters: [
1501
+ {
1502
+ name: "data",
1503
+ type: "string | Blob",
1504
+ description: "The text or Blob to copy.",
1505
+ },
1506
+ ],
1507
+ examples: [{ code: 'editor.copyToClipboard("Copied text")' }],
811
1508
  },
812
- "editor.sendMessage": (_ctx, type: string, data: any) => {
813
- if (!client.contentManager.isDocumentEditor()) return;
1509
+ "editor.sendMessage": {
1510
+ callback: (_ctx, type: string, data: any) => {
1511
+ if (!client.contentManager.isDocumentEditor()) return;
814
1512
 
815
- client.contentManager.documentEditor.sendPublicMessage({
816
- type,
817
- data,
818
- });
1513
+ client.contentManager.documentEditor.sendPublicMessage({
1514
+ type,
1515
+ data,
1516
+ });
1517
+ },
1518
+ description:
1519
+ "Sends a public message to the active document editor, if one is open.",
1520
+ parameters: [
1521
+ { name: "type", type: "string", description: "The message type." },
1522
+ {
1523
+ name: "data",
1524
+ type: "any",
1525
+ description: "Data attached to the message.",
1526
+ optional: true,
1527
+ },
1528
+ ],
819
1529
  },
820
1530
 
821
- "editor.isMobile": () => {
822
- const mouseDetected = globalThis.matchMedia("(pointer:fine)").matches;
823
- return !mouseDetected;
1531
+ "editor.isMobile": {
1532
+ callback: () => {
1533
+ const mouseDetected = globalThis.matchMedia("(pointer:fine)").matches;
1534
+ return !mouseDetected;
1535
+ },
1536
+ description:
1537
+ "Checks whether the current device lacks a fine pointer and should be treated as mobile.",
1538
+ returns: [
1539
+ {
1540
+ type: "boolean",
1541
+ description:
1542
+ "Whether the editor is running in a mobile-style pointer environment.",
1543
+ },
1544
+ ],
824
1545
  },
825
1546
  };
826
1547