@silverbulletmd/silverbullet 2.10.0 → 2.12.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 (105) hide show
  1. package/README.md +6 -6
  2. package/client/asset_bundle/bundle.ts +0 -1
  3. package/client/config.ts +343 -0
  4. package/client/data/kv_primitives.ts +2 -2
  5. package/client/markdown_parser/constants.ts +5 -1
  6. package/client/plugos/hooks/command.ts +8 -6
  7. package/client/plugos/hooks/event.ts +60 -24
  8. package/client/plugos/hooks/mq.ts +0 -3
  9. package/client/plugos/hooks/slash_command.ts +3 -20
  10. package/client/plugos/hooks/syscall.ts +0 -4
  11. package/client/plugos/manifest_cache.ts +2 -34
  12. package/client/plugos/plug.ts +0 -3
  13. package/client/plugos/plug_compile.ts +3 -13
  14. package/client/plugos/sandboxes/sandbox.ts +2 -2
  15. package/client/plugos/sandboxes/worker_sandbox.ts +19 -9
  16. package/client/plugos/syscalls/capture.ts +104 -0
  17. package/client/plugos/syscalls/editor.ts +197 -56
  18. package/client/plugos/syscalls/event.ts +12 -9
  19. package/client/plugos/syscalls/fetch.ts +0 -4
  20. package/client/plugos/syscalls/icon.ts +46 -0
  21. package/client/plugos/syscalls/index.ts +33 -13
  22. package/client/plugos/syscalls/jsonschema.ts +0 -1
  23. package/client/plugos/syscalls/mq.ts +0 -1
  24. package/client/plugos/syscalls/navigator.ts +138 -0
  25. package/client/plugos/syscalls/search.ts +44 -0
  26. package/client/plugos/syscalls/shell.ts +0 -1
  27. package/client/plugos/syscalls/space.ts +250 -7
  28. package/client/plugos/syscalls/sync.ts +70 -4
  29. package/client/plugos/syscalls/system.ts +39 -0
  30. package/client/plugos/system.ts +9 -7
  31. package/client/plugos/types.ts +6 -6
  32. package/client/plugos/worker_runtime.ts +0 -2
  33. package/client/space_lua/aggregates.ts +6 -22
  34. package/client/space_lua/ast.ts +0 -5
  35. package/client/space_lua/ast_narrow.ts +0 -5
  36. package/client/space_lua/budget.ts +113 -0
  37. package/client/space_lua/budget_ui.ts +49 -0
  38. package/client/space_lua/eval.ts +113 -88
  39. package/client/space_lua/labels.ts +0 -4
  40. package/client/space_lua/numeric.ts +2 -7
  41. package/client/space_lua/parse.ts +0 -17
  42. package/client/space_lua/quarantine.ts +127 -0
  43. package/client/space_lua/query_collection.ts +18 -51
  44. package/client/space_lua/query_env.ts +0 -2
  45. package/client/space_lua/render_lua_markdown.ts +2 -7
  46. package/client/space_lua/render_widget.ts +18 -1
  47. package/client/space_lua/runtime.ts +64 -77
  48. package/client/space_lua/stdlib/format.ts +0 -12
  49. package/client/space_lua/stdlib/js.ts +0 -2
  50. package/client/space_lua/stdlib/load.ts +0 -1
  51. package/client/space_lua/stdlib/math.ts +0 -7
  52. package/client/space_lua/stdlib/net.ts +0 -4
  53. package/client/space_lua/stdlib/os.ts +0 -10
  54. package/client/space_lua/stdlib/pattern.ts +18 -23
  55. package/client/space_lua/stdlib/space_lua.ts +1 -2
  56. package/client/space_lua/stdlib/string_pack.ts +4 -7
  57. package/client/space_lua/stdlib/table.ts +0 -5
  58. package/client/space_lua/stdlib.ts +18 -31
  59. package/client/space_lua/tonumber.ts +0 -9
  60. package/dist/plug-compile.js +3 -4
  61. package/package.json +52 -41
  62. package/plug-api/lib/async.ts +1 -6
  63. package/plug-api/lib/collation.ts +22 -0
  64. package/plug-api/lib/crypto.ts +16 -15
  65. package/plug-api/lib/dates.ts +33 -0
  66. package/plug-api/lib/fuzzy.ts +282 -0
  67. package/plug-api/lib/json.ts +0 -6
  68. package/plug-api/lib/limited_map.ts +0 -2
  69. package/plug-api/lib/link_write.ts +27 -0
  70. package/plug-api/lib/ref.ts +28 -5
  71. package/plug-api/lib/resolve.ts +1 -9
  72. package/plug-api/lib/resolve_path.ts +290 -0
  73. package/plug-api/lib/shortcut.ts +15 -0
  74. package/plug-api/lib/tags.ts +0 -3
  75. package/plug-api/lib/transclusion.ts +28 -1
  76. package/plug-api/lib/tree.ts +0 -5
  77. package/plug-api/lib/yaml.ts +5 -44
  78. package/plug-api/syscall.ts +1 -5
  79. package/plug-api/syscalls/config.ts +2 -2
  80. package/plug-api/syscalls/editor.ts +75 -213
  81. package/plug-api/syscalls/icon.ts +18 -0
  82. package/plug-api/syscalls/index.ts +22 -4
  83. package/plug-api/syscalls/search.ts +11 -0
  84. package/plug-api/syscalls/space.ts +122 -5
  85. package/plug-api/syscalls/sync.ts +31 -2
  86. package/plug-api/syscalls/system.ts +46 -0
  87. package/plug-api/syscalls.ts +2 -0
  88. package/plug-api/system_mock.ts +13 -0
  89. package/plug-api/types/client.ts +20 -4
  90. package/plug-api/types/datastore.ts +11 -2
  91. package/plug-api/types/index.ts +10 -0
  92. package/plug-api/types/manifest.ts +7 -0
  93. package/plug-api/types/profile.ts +13 -0
  94. package/plug-api/types/revisions.ts +67 -0
  95. package/plug-api/ui/description.ts +64 -0
  96. package/plug-api/ui/hover.ts +69 -0
  97. package/plug-api/ui/index.ts +58 -2
  98. package/plug-api/ui/scroll.ts +30 -0
  99. package/plug-api/ui/tree_model.ts +285 -0
  100. package/plug-api/ui/tree_types.ts +67 -0
  101. package/plug-api/ui/use_fit_collapse.ts +67 -0
  102. package/plugs/builtin_plugs.ts +0 -1
  103. package/plugs/index/types.ts +9 -0
  104. package/plug-api/lib/memory_cache.ts +0 -21
  105. /package/plug-api/{ui → lib}/panel_styles.ts +0 -0
@@ -1,11 +1,15 @@
1
- import { syscall } from "../syscall.ts";
2
-
1
+ import type { Path, Ref } from "@silverbulletmd/silverbullet/lib/ref";
3
2
  import type {
4
3
  DocumentMeta,
5
4
  FileMeta,
6
5
  PageMeta,
7
6
  } from "../../plug-api/types/index.ts";
8
- import type { Ref } from "@silverbulletmd/silverbullet/lib/ref";
7
+ import type {
8
+ FileRevisions,
9
+ SpaceLog,
10
+ } from "../../plug-api/types/revisions.ts";
11
+ import type { PathLookup } from "../lib/resolve_path.ts";
12
+ import { syscall } from "../syscall.ts";
9
13
 
10
14
  /**
11
15
  * Exposes the space with its pages, documents and plugs.
@@ -134,8 +138,6 @@ export function deleteDocument(name: string): Promise<void> {
134
138
  return syscall("space.deleteDocument", name);
135
139
  }
136
140
 
137
- // Lower level-file operations
138
-
139
141
  /**
140
142
  * List all files in the space (pages, documents and plugs).
141
143
  * @returns a list of all files in the space represented as FileMeta objects
@@ -203,3 +205,118 @@ export function deleteFile(name: string): Promise<void> {
203
205
  export function fileExists(name: string): Promise<boolean> {
204
206
  return syscall("space.fileExists", name);
205
207
  }
208
+
209
+ /**
210
+ * Every basename carried by more than one file, with the files that carry it.
211
+ * Small under the bare-iff-unique invariant; pair with `collisionIndex` and
212
+ * `writeLinkPath` from `lib/resolve_path` to decide link write formats.
213
+ */
214
+ export function collidingBasenames(): Promise<Record<string, Path[]>> {
215
+ return syscall("space.collidingBasenames");
216
+ }
217
+
218
+ /**
219
+ * Looks up a batch of paths at once: for each, whether that exact path exists
220
+ * and which files share its basename. Pair with `lookupIndex` and `resolvePath`
221
+ * from `lib/resolve_path` to resolve them.
222
+ */
223
+ export function lookupPaths(
224
+ paths: string[],
225
+ ): Promise<Record<string, PathLookup>> {
226
+ return syscall("space.lookupPaths", paths);
227
+ }
228
+
229
+ /**
230
+ * Lists the revision history of a file.
231
+ * @param path the path of the file to list revisions for
232
+ * @param before list revisions older than this revision id
233
+ */
234
+ export function listRevisions(
235
+ path: string,
236
+ before?: string,
237
+ ): Promise<FileRevisions> {
238
+ return syscall("space.listRevisions", path, before);
239
+ }
240
+
241
+ /**
242
+ * Reads the text of a file as it was at a given revision, or at that
243
+ * revision's parent.
244
+ * @param path the path of the file to read
245
+ * @param rev the revision id to read
246
+ * @param parent read the file as of the revision's parent instead (e.g. the
247
+ * version immediately before a deletion commit)
248
+ */
249
+ export function getRevision(
250
+ path: string,
251
+ rev: string,
252
+ parent?: boolean,
253
+ ): Promise<string> {
254
+ return syscall("space.getRevision", path, rev, parent);
255
+ }
256
+
257
+ /**
258
+ * Reads a unified diff of a revision's own change (vs its parent; a root
259
+ * commit reads as fully added).
260
+ * @param path the path of the file to diff
261
+ * @param rev the revision id to diff; omitted, diffs the uncommitted change
262
+ */
263
+ export function getRevisionDiff(path: string, rev?: string): Promise<string> {
264
+ return syscall("space.getRevisionDiff", path, rev);
265
+ }
266
+
267
+ /**
268
+ * Lists the space-wide commit log.
269
+ * @param before list commits older than this revision id
270
+ * @param q match commits whose message or author contains this phrase
271
+ */
272
+ export function getSpaceLog(before?: string, q?: string): Promise<SpaceLog> {
273
+ return syscall("space.getSpaceLog", before, q);
274
+ }
275
+
276
+ /**
277
+ * Commits everything outstanding as a revision now, rather than waiting for
278
+ * the automatic commit.
279
+ * @returns false if there was nothing to commit
280
+ */
281
+ export function createRevisionSnapshot(): Promise<boolean> {
282
+ return syscall("space.createRevisionSnapshot");
283
+ }
284
+
285
+ export function getGitSyncStatus(): Promise<
286
+ import("../types/revisions.ts").GitSyncSnapshot
287
+ > {
288
+ return syscall("space.getGitSyncStatus");
289
+ }
290
+
291
+ export function getGitConflicts(): Promise<
292
+ import("../types/revisions.ts").GitConflicts
293
+ > {
294
+ return syscall("space.getGitConflicts");
295
+ }
296
+
297
+ export function syncGitNow(): Promise<void> {
298
+ return syscall("space.syncGitNow");
299
+ }
300
+
301
+ export function resolveGitConflict(
302
+ id: string,
303
+ generation: string,
304
+ contentRevision: string,
305
+ action: import("../types/revisions.ts").GitConflictAction,
306
+ ): Promise<import("../types/revisions.ts").GitConflicts> {
307
+ return syscall(
308
+ "space.resolveGitConflict",
309
+ id,
310
+ generation,
311
+ contentRevision,
312
+ action,
313
+ );
314
+ }
315
+
316
+ export function getGitConflictVersion(
317
+ id: string,
318
+ generation: string,
319
+ side: "local" | "remote",
320
+ ): Promise<Uint8Array> {
321
+ return syscall("space.getGitConflictVersion", id, generation, side);
322
+ }
@@ -5,6 +5,14 @@ import { syscall } from "../syscall.ts";
5
5
  * @module
6
6
  */
7
7
 
8
+ /**
9
+ * Whether client Sync is enabled. False when the service worker is off
10
+ * (`SB_DISABLE_SERVICE_WORKER` / `bootConfig.disableServiceWorker`).
11
+ */
12
+ export function isEnabled(): Promise<boolean> {
13
+ return syscall("sync.isEnabled");
14
+ }
15
+
8
16
  /**
9
17
  * Checks if an initial sync has completed
10
18
  */
@@ -12,12 +20,33 @@ export function hasInitialSyncCompleted(): Promise<boolean> {
12
20
  return syscall("sync.hasInitialSyncCompleted");
13
21
  }
14
22
 
23
+ /**
24
+ * For each file, whether indexing it now would read it locally or cheaply
25
+ * (true), or race the initial sync and expensively re-download it (false).
26
+ * Always all-true outside a fresh install's initial index window.
27
+ * @param paths the space-relative file paths
28
+ */
29
+ export function areFilesReadyToIndex(paths: string[]): Promise<boolean[]> {
30
+ return syscall("sync.areFilesReadyToIndex", paths);
31
+ }
32
+
15
33
  /**
16
34
  * Syncs a file immediately. Sync would happen automatically, but this prioritizes the file.
17
35
  * @param path the path to the file to sync
36
+ * @param remoteLastModified lastModified of the remote change event, used for echo suppression
37
+ * @param remoteRevisionHash content revision the remote change event reported, which tells a same-millisecond change from an echo
18
38
  */
19
- export function performFileSync(path: string): Promise<void> {
20
- return syscall("sync.performFileSync", path);
39
+ export function performFileSync(
40
+ path: string,
41
+ remoteLastModified?: number,
42
+ remoteRevisionHash?: string,
43
+ ): Promise<void> {
44
+ return syscall(
45
+ "sync.performFileSync",
46
+ path,
47
+ remoteLastModified,
48
+ remoteRevisionHash,
49
+ );
21
50
  }
22
51
 
23
52
  /**
@@ -1,6 +1,7 @@
1
1
  import { syscall } from "../syscall.ts";
2
2
  import type { CommandDef } from "../types/manifest.ts";
3
3
  import type { SyscallMeta } from "@silverbulletmd/silverbullet/type/index";
4
+ import type { Account, ClientProfile } from "../types/profile.ts";
4
5
 
5
6
  /**
6
7
  * System level syscalls
@@ -35,6 +36,34 @@ export function listCommands(): Promise<Record<string, CommandDef>> {
35
36
  return syscall("system.listCommands");
36
37
  }
37
38
 
39
+ /**
40
+ * Lists the commands the command palette would show right now: context- and
41
+ * mode-filtered, hidden ones dropped, each with its prettified key hint and
42
+ * the time it was last run on this client.
43
+ * Note: only available on the client
44
+ */
45
+ export function listPaletteCommands(): Promise<
46
+ {
47
+ name: string;
48
+ priority: number;
49
+ lastRun?: number;
50
+ hint?: string;
51
+ }[]
52
+ > {
53
+ return syscall("system.listPaletteCommands");
54
+ }
55
+
56
+ /**
57
+ * Runs a command as if it had been picked from the command palette: records
58
+ * it as the most recently run (which is what orders the palette), then
59
+ * invokes it.
60
+ * Note: only available on the client
61
+ * @returns whatever the command returned
62
+ */
63
+ export function runPaletteCommand(name: string): Promise<any> {
64
+ return syscall("system.runPaletteCommand", name);
65
+ }
66
+
38
67
  /**
39
68
  * Lists all syscalls available
40
69
  * @returns a list of all available syscalls
@@ -101,6 +130,23 @@ export function getVersion(): Promise<string> {
101
130
  return syscall("system.getVersion");
102
131
  }
103
132
 
133
+ /**
134
+ * The current user's identity. `username` falls back to "me" when the
135
+ * deployment has no accounts.
136
+ */
137
+ export function getProfile(): Promise<ClientProfile> {
138
+ return syscall("system.getProfile");
139
+ }
140
+
141
+ /**
142
+ * Every account with access to this space. `username` falls back to "me" for a
143
+ * deployment without accounts, and exactly one entry is marked `me` whenever
144
+ * the current user is known.
145
+ */
146
+ export function listAccounts(): Promise<Account[]> {
147
+ return syscall("system.listAccounts");
148
+ }
149
+
104
150
  export function getConfig<T = any>(
105
151
  key: string,
106
152
  defaultValue: any = undefined,
@@ -14,6 +14,8 @@ export * as datastore from "./syscalls/datastore.ts";
14
14
  export * as jsonschema from "./syscalls/jsonschema.ts";
15
15
  export * as lua from "./syscalls/lua.ts";
16
16
  export * as config from "./syscalls/config.ts";
17
+ export * as search from "./syscalls/search.ts";
18
+ export * as icon from "./syscalls/icon.ts";
17
19
 
18
20
  // Not technically syscalls, but we want to export them for convenience
19
21
  export * as index from "./syscalls/index.ts";
@@ -4,6 +4,7 @@ import { MemoryKvPrimitives } from "../client/data/memory_kv_primitives.ts";
4
4
  import { DataStoreMQ } from "../client/data/mq.datastore.ts";
5
5
  import { ObjectIndex } from "../client/data/object_index.ts";
6
6
  import { EventHook } from "../client/plugos/hooks/event.ts";
7
+ import { configSyscalls } from "../client/plugos/syscalls/config.ts";
7
8
  import {
8
9
  dataStoreReadSyscalls,
9
10
  dataStoreWriteSyscalls,
@@ -19,6 +20,7 @@ import {
19
20
  spaceReadSyscalls,
20
21
  spaceWriteSyscalls,
21
22
  } from "../client/plugos/syscalls/space.ts";
23
+ import { syncSyscalls } from "../client/plugos/syscalls/sync.ts";
22
24
  import { systemSyscalls } from "../client/plugos/syscalls/system.ts";
23
25
  import { System } from "../client/plugos/system.ts";
24
26
  import { Space } from "../client/space.ts";
@@ -45,6 +47,13 @@ export function createMockSystem() {
45
47
  space,
46
48
  config,
47
49
  eventedSpacePrimitives: spacePrimitives,
50
+ // Sync/index state consulted by sync syscalls; defaults simulate a
51
+ // steady-state client (everything synced).
52
+ bootConfig: {},
53
+ fullSyncCompleted: true,
54
+ fullIndexCompleted: true,
55
+ syncedPaths: new Set<string>(),
56
+ serverPingMs: undefined,
48
57
  };
49
58
 
50
59
  const objectIndex = new ObjectIndex(ds, config, eventHook, mq);
@@ -64,11 +73,13 @@ export function createMockSystem() {
64
73
  languageSyscalls(),
65
74
  jsonschemaSyscalls(),
66
75
  indexSyscalls(objectIndex, clientMock),
76
+ configSyscalls(config),
67
77
  luaSyscalls(system, () => clientSystemMock.spaceLuaEnv.env),
68
78
  mqSyscalls(mq),
69
79
  dataStoreReadSyscalls(ds, clientSystemMock),
70
80
  dataStoreWriteSyscalls(ds),
71
81
  systemSyscalls(clientMock, false),
82
+ syncSyscalls(clientMock),
72
83
  );
73
84
 
74
85
  globalThis.syscall = (name: string, ...args: any): Promise<any> => {
@@ -77,6 +88,7 @@ export function createMockSystem() {
77
88
 
78
89
  return {
79
90
  system,
91
+ objectIndex,
80
92
  eventHook,
81
93
  config,
82
94
  kv,
@@ -84,5 +96,6 @@ export function createMockSystem() {
84
96
  mq,
85
97
  ds,
86
98
  space,
99
+ clientMock,
87
100
  };
88
101
  }
@@ -1,5 +1,8 @@
1
1
  import type { ParseTree } from "@silverbulletmd/silverbullet/lib/tree";
2
- import type { PageMeta } from "@silverbulletmd/silverbullet/type/index";
2
+ import type {
3
+ DocumentMeta,
4
+ PageMeta,
5
+ } from "@silverbulletmd/silverbullet/type/index";
3
6
 
4
7
  export type FilterOption = {
5
8
  name: string;
@@ -33,7 +36,10 @@ export type Notification = {
33
36
  persistent?: boolean;
34
37
  };
35
38
 
36
- export type PanelMode = number;
39
+ // A plain number is a flex-grow ratio (or, for the modal slot, a pixel
40
+ // inset); a string is a raw CSS `flex` shorthand, e.g. "0 0 260px" for a
41
+ // fixed-width sidebar.
42
+ export type PanelMode = number | string;
37
43
 
38
44
  export type CodeWidgetContent = {
39
45
  html?: string;
@@ -43,12 +49,22 @@ export type CodeWidgetContent = {
43
49
  url?: string;
44
50
  };
45
51
 
46
- // Document editors stuff
47
52
  export type DocumentEditorCallback = () => Promise<DocumentEditorContent>;
48
53
  export type DocumentEditorContent = {
49
54
  html: string;
50
55
  };
51
56
 
57
+ export type DocumentCapabilityDescriptor = Pick<
58
+ DocumentMeta,
59
+ "name" | "extension" | "contentType" | "size"
60
+ >;
61
+
62
+ export type DocumentCapability = {
63
+ kind: "plug" | "text" | "media" | "external";
64
+ editor?: string;
65
+ reason?: string;
66
+ };
67
+
52
68
  export type LintDiagnostic = {
53
69
  from: number;
54
70
  to: number;
@@ -93,6 +109,7 @@ export type AppEvent =
93
109
  | "editor:unfold"
94
110
  | "plugs:loaded"
95
111
  | "cron:secondPassed"
112
+ // Deprecated: dock a view at "page-top"/"page-bottom" with view.define instead.
96
113
  | "hooks:renderTopWidgets"
97
114
  | "hooks:renderBottomWidgets";
98
115
 
@@ -136,7 +153,6 @@ export type SlashCompletions = {
136
153
  options: SlashCompletionOption[];
137
154
  };
138
155
 
139
- // Code widget stuff
140
156
  export type CodeWidgetCallback = (
141
157
  bodyText: string,
142
158
  pageName: string,
@@ -1,4 +1,3 @@
1
- // Message Queue related types
2
1
  export type MQMessage = {
3
2
  id: string;
4
3
  queue: string;
@@ -13,9 +12,10 @@ export type MQStats = {
13
12
  export type MQSubscribeOptions = {
14
13
  batchSize?: number;
15
14
  pollInterval?: number;
15
+ /** How often a running callback refreshes its messages' processing lease. */
16
+ leaseRenewIntervalMs?: number;
16
17
  };
17
18
 
18
- // KV types
19
19
  export type KvKey = string[];
20
20
 
21
21
  export type KV<T = any> = {
@@ -26,3 +26,12 @@ export type KV<T = any> = {
26
26
  export type KvQuery = {
27
27
  prefix?: KvKey;
28
28
  };
29
+
30
+ /**
31
+ * Body of an `indexQueue` message: a plain path, or a path a producer has
32
+ * already cleared the index for (a full reindex empties the index wholesale
33
+ * before queueing). Shared by the producers (`client/data/object_index.ts`)
34
+ * and the consumer (`plugs/index/queue.ts`) so the two ends of the queue
35
+ * cannot drift.
36
+ */
37
+ export type IndexQueueBody = string | { path: string; cleared?: boolean };
@@ -14,6 +14,16 @@ export type PageDecoration = {
14
14
  cssClasses?: string[];
15
15
  hide?: boolean;
16
16
  renderWidgets?: boolean; // Defaults to true
17
+ /** A Feather name ("star"), a namespaced one ("feather:star"), or literal
18
+ * SVG markup, shown alongside the page in the top bar, navigation, links,
19
+ * and completion. */
20
+ icon?: string;
21
+ tree?: PageTreeDecoration;
22
+ };
23
+ export type PageTreeDecoration = {
24
+ /** Floats the page above its lower-priority siblings (default 0). */
25
+ priority?: number;
26
+ hide?: boolean;
17
27
  };
18
28
  export type PageMeta = ObjectValue<
19
29
  {
@@ -77,6 +77,13 @@ export type CommandDef = {
77
77
  hide?: boolean;
78
78
  requireMode?: "rw" | "ro";
79
79
  requireEditor?: "any" | "page" | "notpage" | string;
80
+ /**
81
+ * When true, the command is omitted from the palette, menus, and
82
+ * `buildAllCommands()` while `bootConfig.disableServiceWorker` is set
83
+ * (`SB_DISABLE_SERVICE_WORKER`). Same space-wide drop as `requireMode: rw`
84
+ * in a read-only space.
85
+ */
86
+ requireServiceWorker?: boolean;
80
87
 
81
88
  // When true, this binding is NOT registered while vim mode is active,
82
89
  // so the key falls through to vim.
@@ -0,0 +1,13 @@
1
+ export type ClientProfile = {
2
+ username: string;
3
+ fullName?: string;
4
+ };
5
+
6
+ /** One account with access to the space. `username` is absent where the
7
+ * deployment keeps no accounts at all, and exactly one entry is marked `me`
8
+ * whenever the current user is known. */
9
+ export type Account = {
10
+ username: string | null;
11
+ fullName?: string;
12
+ me: boolean;
13
+ };
@@ -0,0 +1,67 @@
1
+ export type RevisionEntry = {
2
+ rev: string;
3
+ timestamp: number;
4
+ author: string;
5
+ message: string;
6
+ added?: number;
7
+ removed?: number;
8
+ };
9
+
10
+ export type FileRevisions = {
11
+ mode: "managed" | "unmanaged" | "disabled";
12
+ uncommitted: boolean;
13
+ revisions: RevisionEntry[];
14
+ more: boolean;
15
+ };
16
+
17
+ export type FileStatus = "added" | "modified" | "deleted" | "renamed";
18
+
19
+ export type LogFile = { path: string; status: FileStatus };
20
+
21
+ export type LogCommit = RevisionEntry & { files: LogFile[] };
22
+
23
+ export type SpaceLog = {
24
+ mode: "managed" | "unmanaged" | "disabled";
25
+ commits: LogCommit[];
26
+ more: boolean;
27
+ /** What differs from HEAD right now -- what a snapshot would capture. */
28
+ uncommitted: LogFile[];
29
+ sync?: SyncState | null;
30
+ };
31
+
32
+ /** Mirrors the server's `SyncState` (`revisions::engine`), which serializes
33
+ * with `#[serde(tag = "state", rename_all = "camelCase")]` -- that renames
34
+ * variant names but not their fields, so `Paused`'s field stays `reason`,
35
+ * not `message`. `message` is absent on the `/.events` frame, which any
36
+ * Read-level visitor receives; `/.revisions/` and the admin route carry it. */
37
+ export type SyncState =
38
+ | { state: "idle" }
39
+ | { state: "syncing" }
40
+ | { state: "conflicted"; paths: string[] }
41
+ | { state: "paused"; reason: string }
42
+ | { state: "error"; kind: string; message?: string };
43
+
44
+ export type GitSyncSnapshot = {
45
+ sync: SyncState;
46
+ lastAttempt: number | null;
47
+ lastSuccess: number | null;
48
+ version: number;
49
+ enabled: boolean;
50
+ paused: boolean;
51
+ dirty?: boolean;
52
+ pending: number | null;
53
+ incoming: number | null;
54
+ };
55
+
56
+ export type GitConflict = {
57
+ id: string;
58
+ path: string;
59
+ kind: "text" | "binary" | "deleteModify" | "unsupported";
60
+ local: boolean;
61
+ remote: boolean;
62
+ contentRevision: string;
63
+ canResolve?: boolean;
64
+ };
65
+
66
+ export type GitConflicts = { generation: string; conflicts: GitConflict[] };
67
+ export type GitConflictAction = "local" | "remote" | "edited" | "delete";
@@ -0,0 +1,64 @@
1
+ export type StructuredDescription = {
2
+ label?: string;
3
+ text: string;
4
+ highlights?: [number, number][];
5
+ };
6
+
7
+ export type Description = string | StructuredDescription;
8
+
9
+ export function normalizeDescription(value: unknown): Description | undefined {
10
+ if (typeof value === "string") return value;
11
+ if (
12
+ !value ||
13
+ typeof value !== "object" ||
14
+ !("text" in value) ||
15
+ typeof value.text !== "string"
16
+ )
17
+ return undefined;
18
+ const text = value.text;
19
+ const boundary = (offset: number) =>
20
+ !(
21
+ offset > 0 &&
22
+ offset < text.length &&
23
+ /[\uD800-\uDBFF]/.test(text[offset - 1]) &&
24
+ /[\uDC00-\uDFFF]/.test(text[offset])
25
+ );
26
+ const ranges =
27
+ "highlights" in value && Array.isArray(value.highlights)
28
+ ? value.highlights
29
+ : [];
30
+ const valid = ranges
31
+ .filter(
32
+ (range): range is [number, number] =>
33
+ Array.isArray(range) &&
34
+ range.length === 2 &&
35
+ range.every(Number.isInteger) &&
36
+ range[0] >= 0 &&
37
+ range[1] <= text.length &&
38
+ range[0] < range[1] &&
39
+ boundary(range[0]) &&
40
+ boundary(range[1]),
41
+ )
42
+ .sort((a, b) => a[0] - b[0]);
43
+ const highlights: [number, number][] = [];
44
+ for (const [start, end] of valid) {
45
+ const previous = highlights.at(-1);
46
+ if (previous && start <= previous[1])
47
+ previous[1] = Math.max(previous[1], end);
48
+ else highlights.push([start, end]);
49
+ }
50
+ return {
51
+ text,
52
+ ...("label" in value && typeof value.label === "string"
53
+ ? { label: value.label }
54
+ : {}),
55
+ highlights,
56
+ };
57
+ }
58
+
59
+ export function descriptionText(description?: Description): string {
60
+ if (typeof description === "string") return description;
61
+ return description
62
+ ? [description.label, description.text].filter(Boolean).join(" ")
63
+ : "";
64
+ }
@@ -0,0 +1,69 @@
1
+ import { useEffect, useState } from "preact/hooks";
2
+
3
+ /**
4
+ * Tracks hover outside parent state. Individual subscriptions re-render only
5
+ * the two affected rows instead of the entire expanded list.
6
+ */
7
+ export class HoverTracker {
8
+ private key: unknown = undefined;
9
+ private subscribers = new Set<(key: unknown) => void>();
10
+ /** Where the pointer was last seen -- see `resolveHover`. */
11
+ private x = -1;
12
+ private y = -1;
13
+
14
+ get(): unknown {
15
+ return this.key;
16
+ }
17
+
18
+ set(key: unknown): void {
19
+ if (this.key === key) return;
20
+ this.key = key;
21
+ for (const notify of this.subscribers) notify(key);
22
+ }
23
+
24
+ /**
25
+ * A pointer event over the row container: remembers where, and over what.
26
+ * Any `MouseEvent` will do -- a drop is the other event that moves the
27
+ * pointer somewhere the tracker has to hear about (see `onDrop`), and its
28
+ * coordinates are the last ones a drag leaves behind.
29
+ */
30
+ track(event: MouseEvent, rowAt: (node: Element | null) => unknown): void {
31
+ this.x = event.clientX;
32
+ this.y = event.clientY;
33
+ this.set(rowAt(event.target as Element | null));
34
+ }
35
+
36
+ elementUnderPointer(): Element | null {
37
+ if (this.x < 0 && this.y < 0) return null;
38
+ return document.elementFromPoint(this.x, this.y);
39
+ }
40
+
41
+ subscribe(notify: (key: unknown) => void): () => void {
42
+ this.subscribers.add(notify);
43
+ return () => {
44
+ this.subscribers.delete(notify);
45
+ };
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Re-answers "what is the pointer over" without the pointer having moved --
51
+ * after a refresh replaced the rows, or after the keyboard scrolled different
52
+ * ones underneath it. Identity, never position: the row that was under the
53
+ * pointer may be gone, and the one now in its slot was never pointed at.
54
+ */
55
+ export function resolveHover(
56
+ tracker: HoverTracker,
57
+ rowAt: (node: Element | null) => unknown,
58
+ ): void {
59
+ tracker.set(rowAt(tracker.elementUnderPointer()));
60
+ }
61
+
62
+ export function useHovered(tracker: HoverTracker, key: unknown): boolean {
63
+ const [hovered, setHovered] = useState(() => tracker.get() === key);
64
+ useEffect(() => {
65
+ setHovered(tracker.get() === key);
66
+ return tracker.subscribe((current) => setHovered(current === key));
67
+ }, [tracker, key]);
68
+ return hovered;
69
+ }