@silverbulletmd/silverbullet 2.10.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 (100) hide show
  1. package/README.md +4 -4
  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 +0 -4
  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/editor.ts +128 -53
  15. package/client/plugos/syscalls/event.ts +0 -2
  16. package/client/plugos/syscalls/fetch.ts +0 -4
  17. package/client/plugos/syscalls/icon.ts +46 -0
  18. package/client/plugos/syscalls/index.ts +33 -13
  19. package/client/plugos/syscalls/jsonschema.ts +0 -1
  20. package/client/plugos/syscalls/mq.ts +0 -1
  21. package/client/plugos/syscalls/navigator.ts +143 -0
  22. package/client/plugos/syscalls/search.ts +44 -0
  23. package/client/plugos/syscalls/shell.ts +0 -1
  24. package/client/plugos/syscalls/space.ts +250 -7
  25. package/client/plugos/syscalls/sync.ts +56 -4
  26. package/client/plugos/syscalls/system.ts +38 -0
  27. package/client/plugos/system.ts +7 -5
  28. package/client/plugos/worker_runtime.ts +0 -2
  29. package/client/space_lua/aggregates.ts +0 -16
  30. package/client/space_lua/ast.ts +0 -5
  31. package/client/space_lua/ast_narrow.ts +0 -5
  32. package/client/space_lua/budget.ts +113 -0
  33. package/client/space_lua/budget_ui.ts +49 -0
  34. package/client/space_lua/eval.ts +113 -88
  35. package/client/space_lua/labels.ts +0 -4
  36. package/client/space_lua/numeric.ts +0 -5
  37. package/client/space_lua/parse.ts +0 -17
  38. package/client/space_lua/quarantine.ts +127 -0
  39. package/client/space_lua/query_collection.ts +3 -47
  40. package/client/space_lua/query_env.ts +0 -2
  41. package/client/space_lua/render_lua_markdown.ts +2 -7
  42. package/client/space_lua/render_widget.ts +18 -1
  43. package/client/space_lua/runtime.ts +50 -70
  44. package/client/space_lua/stdlib/format.ts +0 -12
  45. package/client/space_lua/stdlib/js.ts +0 -2
  46. package/client/space_lua/stdlib/load.ts +0 -1
  47. package/client/space_lua/stdlib/math.ts +0 -7
  48. package/client/space_lua/stdlib/net.ts +0 -4
  49. package/client/space_lua/stdlib/os.ts +0 -10
  50. package/client/space_lua/stdlib/pattern.ts +10 -15
  51. package/client/space_lua/stdlib/space_lua.ts +1 -2
  52. package/client/space_lua/stdlib/string_pack.ts +0 -3
  53. package/client/space_lua/stdlib/table.ts +0 -5
  54. package/client/space_lua/stdlib.ts +18 -31
  55. package/client/space_lua/tonumber.ts +0 -9
  56. package/dist/plug-compile.js +2 -3
  57. package/package.json +13 -3
  58. package/plug-api/lib/async.ts +1 -6
  59. package/plug-api/lib/collation.ts +22 -0
  60. package/plug-api/lib/crypto.ts +16 -15
  61. package/plug-api/lib/dates.ts +33 -0
  62. package/plug-api/lib/fuzzy.ts +282 -0
  63. package/plug-api/lib/json.ts +0 -6
  64. package/plug-api/lib/limited_map.ts +0 -2
  65. package/plug-api/lib/link_write.ts +27 -0
  66. package/plug-api/lib/ref.ts +28 -5
  67. package/plug-api/lib/resolve.ts +0 -1
  68. package/plug-api/lib/resolve_path.ts +290 -0
  69. package/plug-api/lib/shortcut.ts +15 -0
  70. package/plug-api/lib/tags.ts +0 -3
  71. package/plug-api/lib/transclusion.ts +28 -1
  72. package/plug-api/lib/tree.ts +0 -5
  73. package/plug-api/lib/yaml.ts +3 -42
  74. package/plug-api/syscall.ts +1 -5
  75. package/plug-api/syscalls/config.ts +2 -2
  76. package/plug-api/syscalls/editor.ts +59 -213
  77. package/plug-api/syscalls/icon.ts +18 -0
  78. package/plug-api/syscalls/index.ts +22 -4
  79. package/plug-api/syscalls/search.ts +11 -0
  80. package/plug-api/syscalls/space.ts +122 -5
  81. package/plug-api/syscalls/sync.ts +23 -2
  82. package/plug-api/syscalls/system.ts +46 -0
  83. package/plug-api/syscalls.ts +2 -0
  84. package/plug-api/system_mock.ts +13 -0
  85. package/plug-api/types/client.ts +4 -3
  86. package/plug-api/types/datastore.ts +11 -2
  87. package/plug-api/types/index.ts +10 -0
  88. package/plug-api/types/profile.ts +13 -0
  89. package/plug-api/types/revisions.ts +67 -0
  90. package/plug-api/ui/description.ts +64 -0
  91. package/plug-api/ui/hover.ts +69 -0
  92. package/plug-api/ui/index.ts +58 -2
  93. package/plug-api/ui/scroll.ts +30 -0
  94. package/plug-api/ui/tree_model.ts +263 -0
  95. package/plug-api/ui/tree_types.ts +66 -0
  96. package/plug-api/ui/use_fit_collapse.ts +67 -0
  97. package/plugs/builtin_plugs.ts +0 -1
  98. package/plugs/index/types.ts +9 -0
  99. package/plug-api/lib/memory_cache.ts +0 -21
  100. /package/plug-api/{ui → lib}/panel_styles.ts +0 -0
@@ -24,7 +24,6 @@ const workerRuntimePlugin: esbuild.Plugin = {
24
24
  name: "worker-runtime",
25
25
  setup(build) {
26
26
  if (typeof __EMBEDDED_WORKER_RUNTIME_JS__ !== "undefined") {
27
- // Bundled CLI: serve pre-bundled JS from the embedded constant
28
27
  build.onResolve({ filter: /^worker-runtime$/ }, () => ({
29
28
  path: "worker-runtime",
30
29
  namespace: "worker-runtime",
@@ -34,7 +33,6 @@ const workerRuntimePlugin: esbuild.Plugin = {
34
33
  loader: "js",
35
34
  }));
36
35
  } else {
37
- // From source: point directly at the .ts file, esbuild handles it
38
36
  build.onResolve({ filter: /^worker-runtime$/ }, () => ({
39
37
  path: path.join(import.meta.dirname, "worker_runtime.ts"),
40
38
  }));
@@ -70,14 +68,12 @@ export async function compileManifest(
70
68
  delete manifest.build;
71
69
  }
72
70
 
73
- // Assets
74
71
  const assetsBundle = await bundleAssets(
75
72
  path.resolve(rootPath),
76
73
  (manifest.assets as string[]) || [],
77
74
  );
78
75
  manifest.assets = assetsBundle.toJSON();
79
76
 
80
- // Normalize the edge case of a plug with no functions
81
77
  if (!manifest.functions) {
82
78
  manifest.functions = {};
83
79
  }
@@ -92,13 +88,11 @@ ${Object.entries(manifest.functions)
92
88
  return "";
93
89
  }
94
90
  let [filePath, jsFunctionName] = def.path.split(":");
95
- // Resolve path
96
91
  filePath = path.join(rootPath, filePath);
97
92
 
98
- return `import {${jsFunctionName} as ${funcName}} from "${
99
- // Replacing \ with / for Windows
100
- path.resolve(filePath).replaceAll("\\", "\\\\")
101
- }";\n`;
93
+ return `import {${jsFunctionName} as ${funcName}} from "${path
94
+ .resolve(filePath)
95
+ .replaceAll("\\", "\\\\")}";\n`;
102
96
  })
103
97
  .join("")}
104
98
 
@@ -122,8 +116,6 @@ export const plug = {manifest, functionMapping};
122
116
  setupMessageListener(functionMapping, manifest, self.postMessage);
123
117
  `;
124
118
 
125
- // console.log("Code:", jsFile);
126
-
127
119
  const tempDir = await mkdtemp(path.join(tmpdir(), "plug-compile-"));
128
120
  const inFile = path.join(tempDir, "input.js");
129
121
  const outFile = `${destPath}/${manifest.name}.plug.js`;
@@ -152,7 +144,6 @@ setupMessageListener(functionMapping, manifest, self.postMessage);
152
144
  jsCode = patchBundledJS(jsCode);
153
145
  await writeFile(outFile, jsCode, "utf-8");
154
146
 
155
- // Clean up temp directory
156
147
  await rm(tempDir, { recursive: true, force: true });
157
148
 
158
149
  console.log(`Plug ${manifest.name} written to ${outFile}.`);
@@ -175,7 +166,6 @@ export async function compileManifests(
175
166
  building = true;
176
167
  await mkdir(dist, { recursive: true });
177
168
  const startTime = Date.now();
178
- // Build all plugs in parallel
179
169
  await Promise.all(
180
170
  manifestFiles.map(async (plugManifestPath) => {
181
171
  const manifestPath = plugManifestPath as string;
@@ -7,7 +7,6 @@ import { race, timeout } from "@silverbulletmd/silverbullet/lib/async";
7
7
  import { fsEndpoint } from "../../spaces/constants.ts";
8
8
 
9
9
  /**
10
- * Represents a "safe" execution environment for plug code
11
10
  * Effectively this wraps a web worker, the reason to have this split from Plugs is to allow plugs to manage multiple sandboxes, e.g. for performance in the future
12
11
  */
13
12
  export class WorkerSandbox<HookT> implements Sandbox<HookT> {
@@ -25,7 +24,6 @@ export class WorkerSandbox<HookT> implements Sandbox<HookT> {
25
24
  private workerOptions = {},
26
25
  ) {}
27
26
 
28
- /** Factory producing a WorkerSandbox for a plug file at a local space path. */
29
27
  static forPath<HookT>(path: string): SandboxFactory<HookT> {
30
28
  return (plug) =>
31
29
  new WorkerSandbox(
@@ -40,7 +38,6 @@ export class WorkerSandbox<HookT> implements Sandbox<HookT> {
40
38
  init(): Promise<void> {
41
39
  console.log("Booting up worker from", this.workerUrl.pathname);
42
40
  if (this.worker) {
43
- // Race condition
44
41
  console.warn("Double init of sandbox, ignoring");
45
42
  return Promise.resolve();
46
43
  }
@@ -54,14 +51,12 @@ export class WorkerSandbox<HookT> implements Sandbox<HookT> {
54
51
  timeout(5000).catch((_) =>
55
52
  Promise.reject(new Error("Plug timed out during creation")),
56
53
  ),
57
- new Promise((resolve) => {
54
+ new Promise<void>((resolve) => {
58
55
  this.worker!.onmessage = (ev) => {
59
56
  if (ev.data.type === "manifest") {
60
57
  this.manifest = ev.data.manifest;
61
- // Set manifest in the plug
62
58
  this.plug.manifest = this.manifest!;
63
59
 
64
- // Set assets in the plug
65
60
  this.plug.assets = new AssetBundle(
66
61
  this.manifest?.assets ? (this.manifest.assets as AssetJson) : {},
67
62
  );
@@ -72,7 +67,15 @@ export class WorkerSandbox<HookT> implements Sandbox<HookT> {
72
67
  void this.onMessage(ev.data);
73
68
  };
74
69
  }),
75
- ]);
70
+ ]).catch((e) => {
71
+ // A worker that failed to come up must not linger: its script fetch may
72
+ // still be in flight (e.g. on a slow link), and a late boot would
73
+ // resolve into a sandbox the system already gave up on. Terminating it
74
+ // also lets a retry start from a clean slate.
75
+ this.worker?.terminate();
76
+ this.worker = undefined;
77
+ throw e;
78
+ });
76
79
  }
77
80
 
78
81
  async onMessage(data: ControllerMessage) {
@@ -92,7 +95,6 @@ export class WorkerSandbox<HookT> implements Sandbox<HookT> {
92
95
  result: result,
93
96
  } as WorkerMessage);
94
97
  } catch (e: any) {
95
- // console.error("Syscall fail", e);
96
98
  this.worker &&
97
99
  this.worker!.postMessage({
98
100
  type: "sysr",
@@ -118,7 +120,6 @@ export class WorkerSandbox<HookT> implements Sandbox<HookT> {
118
120
 
119
121
  async invoke(name: string, args: any[]): Promise<any> {
120
122
  if (!this.worker) {
121
- // Lazy initialization
122
123
  await this.init();
123
124
  }
124
125
  this.reqId++;
@@ -138,5 +139,14 @@ export class WorkerSandbox<HookT> implements Sandbox<HookT> {
138
139
  this.worker.terminate();
139
140
  this.worker = undefined;
140
141
  }
142
+ // A terminated worker will never post the `invr` that would otherwise
143
+ // settle these.
144
+ if (this.outstandingInvocations.size > 0) {
145
+ const error = new Error("Plug sandbox stopped");
146
+ for (const { reject } of this.outstandingInvocations.values()) {
147
+ reject(error);
148
+ }
149
+ this.outstandingInvocations.clear();
150
+ }
141
151
  }
142
152
  }
@@ -1,3 +1,4 @@
1
+ import { copyToClipboard } from "../../clipboard.ts";
1
2
  import {
2
3
  acceptCompletion,
3
4
  closeCompletion,
@@ -72,6 +73,7 @@ import type {
72
73
  FilterOption,
73
74
  NotificationAction,
74
75
  NotificationType,
76
+ PanelMode,
75
77
  UploadFile,
76
78
  } from "@silverbulletmd/silverbullet/type/client";
77
79
  import type { VimConfig } from "@silverbulletmd/silverbullet/type/config";
@@ -79,6 +81,9 @@ import type { PageMeta } from "@silverbulletmd/silverbullet/type/index";
79
81
  import { updateBakedSections } from "../../baked_sections/bake.ts";
80
82
  import type { Client } from "../../client.ts";
81
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";
82
87
  import { getVimModule } from "../../vim_loader.ts";
83
88
  import type { SysCallMapping } from "../system.ts";
84
89
 
@@ -137,6 +142,40 @@ export function editorSyscalls(client: Client): SysCallMapping {
137
142
  "Returns page metadata ordered from most to least recently opened.",
138
143
  returns: [{ type: "PageMeta[]", description: "Recently opened pages." }],
139
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;
150
+ }
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
+ },
140
179
  "editor.getText": {
141
180
  callback: () => {
142
181
  return client.editorView.state.sliceDoc();
@@ -353,9 +392,6 @@ export function editorSyscalls(client: Client): SysCallMapping {
353
392
  },
354
393
  ],
355
394
  },
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
395
  "editor.updateBakedSections": {
360
396
  callback: (): Promise<void> => {
361
397
  return updateBakedSections(client);
@@ -594,13 +630,20 @@ export function editorSyscalls(client: Client): SysCallMapping {
594
630
  ],
595
631
  },
596
632
  "editor.showPanel": {
597
- callback: (
633
+ callback: async (
598
634
  _ctx,
599
635
  id: string,
600
- mode: number,
636
+ mode: PanelMode,
601
637
  html: HTMLElement | HTMLElement[] | string,
602
638
  script: string,
603
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
+ }
604
647
  client.ui.viewDispatch({
605
648
  type: "show-panel",
606
649
  id: id as any,
@@ -620,7 +663,7 @@ export function editorSyscalls(client: Client): SysCallMapping {
620
663
  },
621
664
  {
622
665
  name: "mode",
623
- type: "number",
666
+ type: "number | string",
624
667
  description: "The panel display mode or size.",
625
668
  },
626
669
  {
@@ -641,8 +684,33 @@ export function editorSyscalls(client: Client): SysCallMapping {
641
684
  },
642
685
  description: "Returns focus to the main editor.",
643
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
+ },
644
709
  "editor.hidePanel": {
645
- callback: (_ctx, id: string) => {
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);
646
714
  client.ui.viewDispatch({
647
715
  type: "hide-panel",
648
716
  id: id as any,
@@ -664,14 +732,19 @@ export function editorSyscalls(client: Client): SysCallMapping {
664
732
  "editor.showProgress": {
665
733
  callback: (
666
734
  _ctx,
735
+ progressType: "sync" | "index",
667
736
  progressPercentage?: number,
668
- progressType?: "sync" | "index",
669
737
  ) => {
670
- client.ui.showProgress(progressPercentage, progressType);
738
+ client.ui.showProgress(progressType, progressPercentage);
671
739
  },
672
740
  description:
673
741
  "Shows, updates, or hides a sync or indexing progress indicator.",
674
742
  parameters: [
743
+ {
744
+ name: "progressType",
745
+ type: "sync | index",
746
+ description: "The operation represented by the indicator.",
747
+ },
675
748
  {
676
749
  name: "progressPercentage",
677
750
  type: "number",
@@ -679,12 +752,6 @@ export function editorSyscalls(client: Client): SysCallMapping {
679
752
  "Completion percentage, or undefined to hide the indicator.",
680
753
  optional: true,
681
754
  },
682
- {
683
- name: "progressType",
684
- type: "sync | index",
685
- description: "The operation represented by the indicator.",
686
- optional: true,
687
- },
688
755
  ],
689
756
  },
690
757
  "editor.insertAtPos": {
@@ -828,11 +895,8 @@ export function editorSyscalls(client: Client): SysCallMapping {
828
895
  },
829
896
  "editor.moveCursorToLine": {
830
897
  callback: (_ctx, line: number, column = 1, center = false) => {
831
- // CodeMirror already keeps information about lines
832
898
  const cmLine = client.editorView.state.doc.line(line);
833
- // How much to move inside the line, column number starts from 1
834
899
  const offset = Math.max(0, Math.min(cmLine.length, column - 1));
835
- // Just reuse the implementation above
836
900
  const moveCursor = syscalls["editor.moveCursor"];
837
901
  const moveCursorCallback =
838
902
  typeof moveCursor === "function" ? moveCursor : moveCursor.callback;
@@ -1068,44 +1132,30 @@ export function editorSyscalls(client: Client): SysCallMapping {
1068
1132
  throw new Error("Vim module not loaded.");
1069
1133
  }
1070
1134
  const { Vim } = vimMod;
1071
- // Override the default "o" binding to be more intelligent and follow the markdown editor's behavior
1072
1135
  Vim.mapCommand("o", "action", "newline-continue-markup", {}, {});
1073
1136
  Vim.mapCommand("O", "action", "back-newline-continue-markup", {}, {});
1074
1137
  Vim.unmap("<C-q>", undefined as any);
1075
1138
  Vim.defineAction("newline-continue-markup", (cm) => {
1076
- // Append at end of line
1077
1139
  Vim.handleKey(cm, "A", "+input");
1078
- // Insert newline continuing markup where appropriate
1079
1140
  insertNewlineContinueMarkup(client.editorView) ||
1080
1141
  insertNewlineAndIndent(client.editorView);
1081
1142
  });
1082
1143
  Vim.defineAction("back-newline-continue-markup", (cm) => {
1083
- // Determine current line
1084
1144
  const pos = client.editorView.state.selection.main.from;
1085
1145
  const line = client.editorView.state.doc.lineAt(pos).number;
1086
1146
  if (line === 1) {
1087
- // We're on the top line
1088
- // Go to 0:0
1089
1147
  Vim.handleKey(cm, "0", "+input");
1090
- // Insert a newline
1091
1148
  insertNewline(client.editorView);
1092
- // Go up to the new line
1093
1149
  Vim.handleKey(cm, "k", "+input");
1094
- // Into insert mode
1095
1150
  Vim.handleKey(cm, "i", "+input");
1096
1151
  } else {
1097
- // We're elsewhere in the document
1098
- // Go up
1099
1152
  Vim.handleKey(cm, "k", "+input");
1100
- // Append mode at the end of the line
1101
1153
  Vim.handleKey(cm, "A", "+input");
1102
- // Insert a newline using the continue markup thing
1103
1154
  insertNewlineContinueMarkup(client.editorView) ||
1104
1155
  insertNewlineAndIndent(client.editorView);
1105
1156
  }
1106
1157
  });
1107
1158
 
1108
- // Load the config if any
1109
1159
  const config = client.config.get<VimConfig>("vim", {});
1110
1160
  if (config) {
1111
1161
  config.unmap?.forEach((binding) => {
@@ -1143,9 +1193,9 @@ export function editorSyscalls(client: Client): SysCallMapping {
1143
1193
  },
1144
1194
  "editor.openPageNavigator": {
1145
1195
  callback: (_ctx, mode: "page" | "meta" | "document" | "all" = "page") => {
1146
- client.startPageNavigate(mode);
1196
+ void client.startPageNavigate(mode);
1147
1197
  },
1148
- description: "Opens the page navigator in the requested browsing mode.",
1198
+ description: `Opens the page picker in the requested browsing mode. Each mode maps to a segment of the \`std.pages\` navigator view.`,
1149
1199
  parameters: [
1150
1200
  {
1151
1201
  name: "mode",
@@ -1161,6 +1211,30 @@ export function editorSyscalls(client: Client): SysCallMapping {
1161
1211
  },
1162
1212
  description: "Opens the command palette.",
1163
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
+ },
1164
1238
  "editor.deleteLine": {
1165
1239
  callback: () => {
1166
1240
  deleteLine(client.editorView);
@@ -1222,7 +1296,6 @@ export function editorSyscalls(client: Client): SysCallMapping {
1222
1296
  },
1223
1297
  description: "Moves the current line or selected lines downward.",
1224
1298
  },
1225
- // Folding
1226
1299
  "editor.fold": {
1227
1300
  callback: () => {
1228
1301
  foldCode(client.editorView);
@@ -1265,7 +1338,6 @@ export function editorSyscalls(client: Client): SysCallMapping {
1265
1338
  },
1266
1339
  description: "Redoes the most recently undone editor change.",
1267
1340
  },
1268
- // Cursor motion syscalls
1269
1341
  "editor.cursorCharLeft": {
1270
1342
  callback: () => cursorCharLeft(client.editorView),
1271
1343
  description:
@@ -1310,7 +1382,6 @@ export function editorSyscalls(client: Client): SysCallMapping {
1310
1382
  callback: () => cursorDocEnd(client.editorView),
1311
1383
  description: "Moves the cursor to the end of the document.",
1312
1384
  },
1313
- // Cursor motions that also navigate the completion popup if it's open
1314
1385
  "editor.cursorLineUp": {
1315
1386
  callback: () => {
1316
1387
  const view = client.editorView;
@@ -1347,7 +1418,6 @@ export function editorSyscalls(client: Client): SysCallMapping {
1347
1418
  description:
1348
1419
  "Moves completion selection down one page when open, otherwise moves the cursor down one viewport page.",
1349
1420
  },
1350
- // Selection-extending motions
1351
1421
  "editor.selectCharLeft": {
1352
1422
  callback: () => selectCharLeft(client.editorView),
1353
1423
  description:
@@ -1411,7 +1481,6 @@ export function editorSyscalls(client: Client): SysCallMapping {
1411
1481
  callback: () => selectPageDown(client.editorView),
1412
1482
  description: "Extends the selection downward by one viewport page.",
1413
1483
  },
1414
- // Delete
1415
1484
  "editor.deleteCharBackward": {
1416
1485
  callback: () => deleteCharBackward(client.editorView),
1417
1486
  description: "Deletes the selection or the character before the cursor.",
@@ -1444,7 +1513,6 @@ export function editorSyscalls(client: Client): SysCallMapping {
1444
1513
  callback: () => transposeChars(client.editorView),
1445
1514
  description: "Transposes the characters around the cursor.",
1446
1515
  },
1447
- // Enter: accept completion if popup is open, else newline-and-indent
1448
1516
  "editor.insertNewline": {
1449
1517
  callback: () => {
1450
1518
  const view = client.editorView;
@@ -1454,7 +1522,6 @@ export function editorSyscalls(client: Client): SysCallMapping {
1454
1522
  description:
1455
1523
  "Accepts the active completion, or inserts a newline with appropriate indentation.",
1456
1524
  },
1457
- // Completion popup control
1458
1525
  "editor.acceptCompletion": {
1459
1526
  callback: () => acceptCompletion(client.editorView),
1460
1527
  description:
@@ -1483,20 +1550,14 @@ export function editorSyscalls(client: Client): SysCallMapping {
1483
1550
  "editor.copyToClipboard": {
1484
1551
  callback: async (_ctx, data: string | Blob) => {
1485
1552
  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
- }
1553
+ await copyToClipboard(data);
1493
1554
  } catch (e) {
1494
1555
  console.error(e);
1495
1556
  client.ui.flashNotification(`Could not copy to clipboard: ${e}`);
1496
1557
  }
1497
1558
  },
1498
1559
  description:
1499
- "Copies text or binary Blob data to the system clipboard. Clipboard access requires a secure HTTPS context.",
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.",
1500
1561
  parameters: [
1501
1562
  {
1502
1563
  name: "data",
@@ -1529,10 +1590,7 @@ export function editorSyscalls(client: Client): SysCallMapping {
1529
1590
  },
1530
1591
 
1531
1592
  "editor.isMobile": {
1532
- callback: () => {
1533
- const mouseDetected = globalThis.matchMedia("(pointer:fine)").matches;
1534
- return !mouseDetected;
1535
- },
1593
+ callback: () => isMobileDevice(),
1536
1594
  description:
1537
1595
  "Checks whether the current device lacks a fine pointer and should be treated as mobile.",
1538
1596
  returns: [
@@ -1543,6 +1601,23 @@ export function editorSyscalls(client: Client): SysCallMapping {
1543
1601
  },
1544
1602
  ],
1545
1603
  },
1604
+
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
+ ],
1620
+ },
1546
1621
  };
1547
1622
 
1548
1623
  return syscalls;
@@ -38,11 +38,9 @@ export function eventSyscalls(
38
38
  */
39
39
  "event.listen": {
40
40
  callback: (_ctx, def: EventSubscription) => {
41
- // console.log("Registering Lua event listener: ", def.name);
42
41
  client.config.insert(
43
42
  ["eventListeners", def.name],
44
43
  async (...args: any[]) => {
45
- // Convert return value to JS
46
44
  const val = await def.run(...args);
47
45
  return luaValueToJS(val, LuaStackFrame.lostFrame);
48
46
  },
@@ -22,7 +22,6 @@ export function sandboxFetchSyscalls(client: Client): SysCallMapping {
22
22
  options: ProxyFetchRequest = {},
23
23
  ): Promise<ProxyFetchResponse> => {
24
24
  console.warn("Deprecated: use net.proxyFetch() instead");
25
- // JSONify any non-serializable body
26
25
  if (
27
26
  options?.body &&
28
27
  typeof options.body !== "string" &&
@@ -44,7 +43,6 @@ export function sandboxFetchSyscalls(client: Client): SysCallMapping {
44
43
  buildProxyUrl(client, url),
45
44
  fetchOptions,
46
45
  );
47
- // Do sensible things with the body based on the content type
48
46
  // Read as ArrayBuffer first to safely handle empty responses (e.g.
49
47
  // PUT/DELETE returning 204 with Content-Type: application/json).
50
48
  // resp.arrayBuffer() never throws on an empty body, whereas
@@ -100,7 +98,6 @@ export function sandboxFetchSyscalls(client: Client): SysCallMapping {
100
98
  url: string,
101
99
  options?: ProxyFetchRequest64,
102
100
  ): Promise<ProxyFetchResponse64> => {
103
- // console.log("Got sandbox fetch ", url, op);
104
101
  const fetchOptions = options
105
102
  ? {
106
103
  method: options.method,
@@ -147,7 +144,6 @@ export function sandboxFetchSyscalls(client: Client): SysCallMapping {
147
144
 
148
145
  function buildProxyUrl(client: Client, url: string) {
149
146
  url = url.replace(/^https?:\/\//, "");
150
- // Strip off the /.fs and replace with /.proxy
151
147
  return (
152
148
  client.httpSpacePrimitives.url.slice(0, -fsEndpoint.length) +
153
149
  "/.proxy/" +
@@ -0,0 +1,46 @@
1
+ import { resolveFeatherIcons } from "../../lib/feather_icons.ts";
2
+ import type { SysCallMapping } from "../system.ts";
3
+
4
+ export function iconSyscalls(): SysCallMapping {
5
+ return {
6
+ "icon.feather": {
7
+ callback: (_ctx, name: string): string | null => {
8
+ if (!name) return null;
9
+ return resolveFeatherIcons([name])[name] ?? null;
10
+ },
11
+ description:
12
+ "Renders a Feather icon to SVG markup by name, or nil when the name isn't a Feather icon. The markup carries no color of its own (`currentColor`) and a default 24x24 size, both of which CSS at the injection site can override.",
13
+ parameters: [
14
+ {
15
+ name: "name",
16
+ type: "string",
17
+ description: "A Feather icon name, e.g. `trash-2`.",
18
+ },
19
+ ],
20
+ returns: [
21
+ {
22
+ type: "string",
23
+ description: "The icon's SVG markup, or nil when unknown.",
24
+ },
25
+ ],
26
+ examples: [{ code: 'local lock = icon.feather("lock")' }],
27
+ },
28
+ "icon.resolveFeather": {
29
+ callback: (_ctx, names: string[]): Record<string, string> => {
30
+ return resolveFeatherIcons(names ?? []);
31
+ },
32
+ description:
33
+ "Renders a batch of Feather icons to SVG markup in one round trip, so panels can show icons without bundling the icon set themselves. Unknown names are omitted from the result. The markup carries no color of its own (`currentColor`) and a default 24x24 size, both of which CSS at the injection site can override.",
34
+ parameters: [
35
+ {
36
+ name: "names",
37
+ type: "table",
38
+ description: "Feather icon names, e.g. `trash-2`.",
39
+ },
40
+ ],
41
+ returns: [
42
+ { type: "table", description: "Map of icon name to SVG markup." },
43
+ ],
44
+ },
45
+ };
46
+ }