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