@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,16 +1,16 @@
1
- import type { SysCallMapping } from "../system.ts";
2
- import type {
3
- LuaCollectionQuery,
4
- LuaQueryCollection,
5
- } from "../../space_lua/query_collection.ts";
1
+ import type { ObjectValue } from "@silverbulletmd/silverbullet/type/index";
2
+ import type { Client } from "../../client.ts";
6
3
 
7
4
  import {
8
5
  type ObjectIndex,
9
6
  ObjectValidationError,
10
7
  } from "../../data/object_index.ts";
11
- import type { ObjectValue } from "@silverbulletmd/silverbullet/type/index";
12
- import type { Client } from "../../client.ts";
8
+ import type {
9
+ LuaCollectionQuery,
10
+ LuaQueryCollection,
11
+ } from "../../space_lua/query_collection.ts";
13
12
  import type { LuaTable } from "../../space_lua/runtime.ts";
13
+ import type { SysCallMapping } from "../system.ts";
14
14
  import { describeSchemas, tagSchema } from "./schema_introspection.ts";
15
15
 
16
16
  export function indexSyscalls(
@@ -18,7 +18,6 @@ export function indexSyscalls(
18
18
  client: Client,
19
19
  ): SysCallMapping {
20
20
  return {
21
- // Query collection API
22
21
  "index.tag": {
23
22
  callback: (_ctx, tagName: string): LuaQueryCollection => {
24
23
  return objectIndex.objectsWithTag(tagName);
@@ -63,10 +62,12 @@ export function indexSyscalls(
63
62
  description: "Returns all indexed links as a query collection.",
64
63
  },
65
64
  "index.relations": {
66
- callback: (): LuaQueryCollection => {
67
- return objectIndex.objectsWithTag("relation");
65
+ callback: (_ctx, kind?: string): LuaQueryCollection => {
66
+ return objectIndex.relations(kind);
68
67
  },
69
- description: "Returns all indexed relations as a query collection.",
68
+ description:
69
+ "Returns all indexed relations, optionally filtered by kind, as a query collection.",
70
+ signatures: ["index.relations(kind?)"],
70
71
  },
71
72
  "index.contentPages": {
72
73
  callback: (_ctx, tagName?: string): LuaQueryCollection => {
@@ -128,7 +129,6 @@ export function indexSyscalls(
128
129
  },
129
130
  description: "Returns all indexed tag objects as a query collection.",
130
131
  },
131
- // Schema introspection: indexed object-type / tag schemas
132
132
  "index.describeSchema": {
133
133
  callback: (): Record<string, unknown> => {
134
134
  return describeSchemas(client.config.get(["tags"], {}));
@@ -144,6 +144,13 @@ export function indexSyscalls(
144
144
  "Returns the raw JSON Schema for a tag, or nil when none is declared.",
145
145
  signatures: ["index.tagSchema(tagName)"],
146
146
  },
147
+ "index.isAvailable": {
148
+ callback: (): Promise<boolean> => objectIndex.isIndexAvailable(),
149
+ description: `Whether a full indexing pass has ever completed for this space. False on a fresh client, and for as long as the first index takes on a large one: every object query answers with whatever has been indexed so far, which is nothing to begin with. Code that must work in that window reads the space directly instead.`,
150
+ returns: [
151
+ { type: "boolean", description: "Whether the index can be trusted." },
152
+ ],
153
+ },
147
154
  "index.aspiringPages": {
148
155
  callback: (): LuaQueryCollection => {
149
156
  return objectIndex.aspiringPages();
@@ -151,7 +158,13 @@ export function indexSyscalls(
151
158
  description:
152
159
  "Returns linked but not yet created pages as a query collection.",
153
160
  },
154
- // Internals
161
+ "index.ambiguousLinks": {
162
+ callback: (): LuaQueryCollection => {
163
+ return objectIndex.ambiguousLinks();
164
+ },
165
+ description:
166
+ "Returns links whose page name matches more than one page as a query collection.",
167
+ },
155
168
  "index.aggregates": {
156
169
  callback: (): LuaQueryCollection => {
157
170
  return objectIndex.aggregates();
@@ -246,6 +259,13 @@ export function indexSyscalls(
246
259
  signatures: ["index.queryLuaObjects(tag, query, scopedVariables?)"],
247
260
  },
248
261
 
262
+ "index.clearFileIndex": {
263
+ callback: (_ctx, path: string): Promise<void> =>
264
+ objectIndex.clearFileIndex(path),
265
+ description:
266
+ "Drops every indexed object belonging to a file, so a re-index replaces them rather than merging on top.",
267
+ signatures: ["index.clearFileIndex(path)"],
268
+ },
249
269
  "index.deleteObject": {
250
270
  callback: (
251
271
  _ctx,
@@ -2,7 +2,6 @@ import type { SysCallMapping } from "../system.ts";
2
2
  import { type OutputUnit, Validator, format } from "@cfworker/json-schema";
3
3
  import { stripFunctions } from "../util.ts";
4
4
 
5
- // Register custom formats
6
5
  format.email = (data: string) => data.includes("@");
7
6
  format["page-ref"] = (data: string) =>
8
7
  data.startsWith("[[") && data.endsWith("]]");
@@ -14,7 +14,6 @@ export function mqSyscalls(mq: DataStoreMQ): SysCallMapping {
14
14
  "mq.subscribe": {
15
15
  callback: (_ctx, def: MQListenerSpec) => {
16
16
  def.autoAck = def.autoAck !== false;
17
- // console.log("Registering Lua event listener: ", def.name);
18
17
  client.config.insert(["mqSubscriptions", def.queue], def);
19
18
  },
20
19
  description: "Subscribes a Space Lua callback to a message queue.",
@@ -0,0 +1,138 @@
1
+ import {
2
+ defineView,
3
+ focusPanel,
4
+ moveByRename,
5
+ openView,
6
+ pickView,
7
+ } from "../../navigator/navigator.ts";
8
+ import { newView } from "../../navigator/view_value.ts";
9
+ import type { SysCallMapping } from "../system.ts";
10
+
11
+ export function navigatorSyscalls(): SysCallMapping {
12
+ const viewSyscalls: SysCallMapping = {
13
+ "view.open": {
14
+ callback: (_ctx, name: string, opts?: any) => {
15
+ return openView(name, opts);
16
+ },
17
+ description: "Opens a navigator view by name.",
18
+ parameters: [
19
+ { name: "name", type: "string", description: "View name." },
20
+ {
21
+ name: "opts",
22
+ type: "table",
23
+ description:
24
+ "Optional segment, phrase, dropdown, focus and quiet flags.",
25
+ optional: true,
26
+ },
27
+ ],
28
+ returns: [
29
+ { type: "boolean", description: "Whether the view actually opened." },
30
+ ],
31
+ examples: [{ code: 'view.open("std.spaceTree")' }],
32
+ },
33
+ "view.focus": {
34
+ callback: (_ctx, slot?: string) => focusPanel(slot),
35
+ description:
36
+ "Returns focus to an open navigator panel's input, keeping its current selection.",
37
+ parameters: [
38
+ {
39
+ name: "slot",
40
+ type: "string",
41
+ description:
42
+ "Which panel: modal, lhs, rhs or bhs. Defaults to any open one.",
43
+ optional: true,
44
+ },
45
+ ],
46
+ returns: [
47
+ { type: "boolean", description: "Whether a panel was there to focus." },
48
+ ],
49
+ examples: [{ code: 'view.focus("rhs")' }],
50
+ },
51
+ "view.moveByRename": {
52
+ callback: (_ctx, obj: any, newName: string) => {
53
+ return moveByRename(obj, newName);
54
+ },
55
+ description:
56
+ "Renames the page, document or folder an object stands for -- the default `onMove` for space-backed views.",
57
+ parameters: [
58
+ { name: "obj", type: "table", description: "The row's object." },
59
+ { name: "newName", type: "string", description: "New name." },
60
+ ],
61
+ examples: [{ code: "onMove = view.moveByRename" }],
62
+ },
63
+ "lua:widget.newLive": {
64
+ callback: (_ctx, spec: any) => newView(spec),
65
+ description:
66
+ "Internal: builds a live widget for `widget.new` when its spec has `source` or `content`. Call `widget.new` instead.",
67
+ parameters: [
68
+ {
69
+ name: "spec",
70
+ type: "table",
71
+ description: "A `widget.new` spec with `source` or `content`.",
72
+ },
73
+ ],
74
+ returns: [{ type: "table", description: "The live widget value." }],
75
+ },
76
+ "lua:view.define": {
77
+ callback: (_ctx, spec: any) => {
78
+ return defineView(spec);
79
+ },
80
+ description:
81
+ "Registers a navigator view, and optionally a command that opens it. Rejects a bad spec at definition time.",
82
+ parameters: [
83
+ {
84
+ name: "spec",
85
+ type: "table",
86
+ description: "View definition; see the Navigator API docs.",
87
+ },
88
+ ],
89
+ examples: [
90
+ {
91
+ code: 'view.define {\n name = "myView",\n source = function() return {} end,\n onSelect = function(obj) editor.navigate(obj.ref) end,\n}',
92
+ },
93
+ ],
94
+ },
95
+ "lua:view.pick": {
96
+ callback: (_ctx, spec: any) => {
97
+ return pickView(spec);
98
+ },
99
+ description:
100
+ "Opens a one-shot picker over the supplied source and returns the object picked (or nil).",
101
+ parameters: [
102
+ {
103
+ name: "spec",
104
+ type: "table",
105
+ description:
106
+ "Picker definition; a view.define spec minus its name and chrome.",
107
+ },
108
+ ],
109
+ returns: [{ description: "The picked object, or nil." }],
110
+ examples: [
111
+ {
112
+ code: 'local picked = view.pick { source = function() return { { name = "One" } } end }',
113
+ },
114
+ ],
115
+ },
116
+ };
117
+
118
+ return {
119
+ ...viewSyscalls,
120
+ ...aliasNamespace(viewSyscalls, "view", "navigator"),
121
+ };
122
+ }
123
+
124
+ function aliasNamespace(
125
+ mapping: SysCallMapping,
126
+ from: string,
127
+ to: string,
128
+ ): SysCallMapping {
129
+ const aliased: SysCallMapping = {};
130
+ for (const [name, definition] of Object.entries(mapping)) {
131
+ const isLuaNative = name.startsWith("lua:");
132
+ const cleanName = isLuaNative ? name.slice("lua:".length) : name;
133
+ if (!cleanName.startsWith(`${from}.`)) continue;
134
+ const aliasedName = `${to}.${cleanName.slice(from.length + 1)}`;
135
+ aliased[isLuaNative ? `lua:${aliasedName}` : aliasedName] = definition;
136
+ }
137
+ return aliased;
138
+ }
@@ -0,0 +1,44 @@
1
+ import type { SysCallMapping } from "../system.ts";
2
+ import { rank, type RankOptions } from "../../../plug-api/lib/fuzzy.ts";
3
+
4
+ export function searchSyscalls(): SysCallMapping {
5
+ return {
6
+ "search.rank": {
7
+ callback: (
8
+ _ctx,
9
+ objects: Record<string, any>[],
10
+ phrase: string,
11
+ options?: {
12
+ fields?: Record<
13
+ string,
14
+ number | { weight: number; segments?: boolean }
15
+ >;
16
+ },
17
+ ) => rank(objects, phrase, options as RankOptions<any>),
18
+ description:
19
+ "Fuzzy-ranks objects against a phrase, best match first. The same ranker the navigator's own filtering uses.",
20
+ parameters: [
21
+ { name: "objects", type: "table", description: "Objects to rank." },
22
+ {
23
+ name: "phrase",
24
+ type: "string",
25
+ description:
26
+ "Phrase to match against. An empty phrase returns every object, in the order given.",
27
+ },
28
+ {
29
+ name: "options",
30
+ type: "table",
31
+ description:
32
+ "Ranking options. `fields` maps a field name to its weight, or to `{ weight, segments = true }` to also score the parts of a `/`-separated value.",
33
+ optional: true,
34
+ },
35
+ ],
36
+ returns: [
37
+ {
38
+ type: "table",
39
+ description: "The matching objects, best match first.",
40
+ },
41
+ ],
42
+ },
43
+ };
44
+ }
@@ -62,6 +62,5 @@ export function shellSyscalls(client: Client): SysCallMapping {
62
62
  }
63
63
 
64
64
  function buildShellUrl(client: Client) {
65
- // Strip off the /.fs and replace with /.shell
66
65
  return `${client.httpSpacePrimitives.url.slice(0, -fsEndpoint.length)}/.shell`;
67
66
  }
@@ -1,15 +1,120 @@
1
- import { parseToRef, type Ref } from "@silverbulletmd/silverbullet/lib/ref";
2
- import type { Client } from "../../client.ts";
3
- import type { SysCallMapping } from "../system.ts";
4
-
1
+ import {
2
+ encodePageURI,
3
+ type Path,
4
+ parseToRef,
5
+ type Ref,
6
+ } from "@silverbulletmd/silverbullet/lib/ref";
7
+ import { fileName } from "@silverbulletmd/silverbullet/lib/resolve";
8
+ import {
9
+ BasenameIndex,
10
+ type PathLookup,
11
+ resolvePath,
12
+ } from "@silverbulletmd/silverbullet/lib/resolve_path";
5
13
  import type {
6
14
  DocumentMeta,
7
15
  FileMeta,
8
16
  PageMeta,
9
17
  } from "@silverbulletmd/silverbullet/type/index";
18
+ import type {
19
+ FileRevisions,
20
+ GitSyncSnapshot,
21
+ GitConflicts,
22
+ GitConflictAction,
23
+ SpaceLog,
24
+ } from "@silverbulletmd/silverbullet/type/revisions";
25
+ import type { Client } from "../../client.ts";
26
+ import { fsEndpoint } from "../../spaces/constants.ts";
27
+ import type { SysCallMapping } from "../system.ts";
28
+
29
+ function revisionsUrl(client: Client, suffix: string): string {
30
+ return `${client.httpSpacePrimitives.url.slice(0, -fsEndpoint.length)}/.revisions/${suffix}`;
31
+ }
32
+
33
+ async function fetchRevisionsJson(
34
+ client: Client,
35
+ suffix: string,
36
+ body?: unknown,
37
+ ): Promise<any> {
38
+ const resp = await client.httpSpacePrimitives.authenticatedFetch(
39
+ revisionsUrl(client, suffix),
40
+ {
41
+ method: body === undefined ? "GET" : "POST",
42
+ headers: {
43
+ Accept: "application/json",
44
+ ...(body === undefined ? {} : { "Content-Type": "application/json" }),
45
+ },
46
+ body: body === undefined ? undefined : JSON.stringify(body),
47
+ },
48
+ );
49
+ if (resp.status === 404) {
50
+ throw Object.assign(
51
+ new Error("Revisions are not available for this space"),
52
+ { status: resp.status },
53
+ );
54
+ }
55
+ if (!resp.ok) {
56
+ throw Object.assign(new Error(`Revisions request failed: ${resp.status}`), {
57
+ status: resp.status,
58
+ });
59
+ }
60
+ return resp.json();
61
+ }
62
+
63
+ /**
64
+ * The index resolution syscalls answer from. The client's own index is only
65
+ * trustworthy once the file list has loaded; before that, ask the space
66
+ * directly rather than reporting everything as missing.
67
+ */
68
+ async function resolutionIndex(client: Client): Promise<BasenameIndex> {
69
+ if (client.clientSystem.knownFilesLoaded) {
70
+ return client.clientSystem.allKnownFiles;
71
+ }
72
+ const listing = new BasenameIndex();
73
+ listing.rebuild(
74
+ (await client.space.spacePrimitives.fetchFileList()).map((f) => f.name),
75
+ );
76
+ return listing;
77
+ }
10
78
 
11
79
  export function spaceReadSyscalls(client: Client): SysCallMapping {
12
80
  return {
81
+ "space.getGitSyncStatus": {
82
+ callback: (): Promise<GitSyncSnapshot> =>
83
+ fetchRevisionsJson(client, "_sync"),
84
+ description: "Reads Git sync status and its last successful check.",
85
+ },
86
+ "space.getGitConflictVersion": {
87
+ callback: async (
88
+ _ctx,
89
+ id: string,
90
+ generation: string,
91
+ side: "local" | "remote",
92
+ ): Promise<Uint8Array> => {
93
+ if (side !== "local" && side !== "remote")
94
+ throw new Error("Unknown conflict side");
95
+ const response = await client.httpSpacePrimitives.authenticatedFetch(
96
+ revisionsUrl(
97
+ client,
98
+ `_conflicts/${encodeURIComponent(id)}/${side}?generation=${encodeURIComponent(generation)}`,
99
+ ),
100
+ { method: "GET" },
101
+ );
102
+ if (!response.ok)
103
+ throw Object.assign(
104
+ new Error("Could not read this conflict version"),
105
+ { status: response.status },
106
+ );
107
+ return new Uint8Array(await response.arrayBuffer());
108
+ },
109
+ description: "Downloads one original side of an unresolved Git conflict.",
110
+ signatures: ["space.getGitConflictVersion(id, generation, side)"],
111
+ },
112
+ "space.getGitConflicts": {
113
+ callback: (): Promise<GitConflicts> =>
114
+ fetchRevisionsJson(client, "_conflicts"),
115
+ description:
116
+ "Lists unresolved Git conflicts with saved content versions.",
117
+ },
13
118
  "space.listPages": {
14
119
  callback: (): Promise<PageMeta[]> => client.space.fetchPageList(),
15
120
  description: "Lists all pages in the space.",
@@ -46,7 +151,7 @@ export function spaceReadSyscalls(client: Client): SysCallMapping {
46
151
  },
47
152
  "space.pageExists": {
48
153
  callback: (_ctx, name: string): boolean =>
49
- client.clientSystem.allKnownFiles.has(`${name}.md`),
154
+ resolvePath(`${name}.md`, "", client.clientSystem.allKnownFiles).exists,
50
155
  description: "Checks whether a page exists in the space.",
51
156
  signatures: ["space.pageExists(name)"],
52
157
  },
@@ -98,7 +203,6 @@ export function spaceReadSyscalls(client: Client): SysCallMapping {
98
203
  deprecated: "Use space.getDocumentMeta instead.",
99
204
  signatures: ["space.getAttachmentMeta(name)"],
100
205
  },
101
- // FS
102
206
  "space.listFiles": {
103
207
  callback: (): Promise<FileMeta[]> =>
104
208
  client.space.spacePrimitives.fetchFileList(),
@@ -125,6 +229,33 @@ export function spaceReadSyscalls(client: Client): SysCallMapping {
125
229
  description: "Reads an arbitrary space file together with its metadata.",
126
230
  signatures: ["space.readFileWithMeta(name)"],
127
231
  },
232
+ "space.collidingBasenames": {
233
+ callback: async (): Promise<Record<string, Path[]>> =>
234
+ (await resolutionIndex(client)).collidingBuckets(),
235
+ description:
236
+ "Returns every basename carried by more than one file, with the files carrying it.",
237
+ signatures: ["space.collidingBasenames()"],
238
+ },
239
+ "space.lookupPaths": {
240
+ callback: async (
241
+ _ctx,
242
+ paths: string[],
243
+ ): Promise<Record<string, PathLookup>> => {
244
+ const index = await resolutionIndex(client);
245
+
246
+ const result: Record<string, PathLookup> = {};
247
+ for (const path of paths) {
248
+ result[path] = {
249
+ exact: index.has(path),
250
+ candidates: index.candidates(fileName(path)),
251
+ };
252
+ }
253
+ return result;
254
+ },
255
+ description:
256
+ "Looks up, for each path, whether it exists exactly and which files share its basename.",
257
+ signatures: ["space.lookupPaths(paths)"],
258
+ },
128
259
  "space.fileExists": {
129
260
  callback: async (_ctx, name: string): Promise<boolean> => {
130
261
  // If a full sync has successfully completed (so we know what files exist)
@@ -137,7 +268,6 @@ export function spaceReadSyscalls(client: Client): SysCallMapping {
137
268
  }
138
269
  try {
139
270
  await client.space.spacePrimitives.getFileMeta(name);
140
- // If this returned the file exists
141
271
  return true;
142
272
  } catch {
143
273
  // Assumption: any error means the file does not exist
@@ -147,11 +277,103 @@ export function spaceReadSyscalls(client: Client): SysCallMapping {
147
277
  description: "Checks whether an arbitrary file exists in the space.",
148
278
  signatures: ["space.fileExists(name)"],
149
279
  },
280
+ "space.listRevisions": {
281
+ callback: (_ctx, path: string, before?: string): Promise<FileRevisions> =>
282
+ fetchRevisionsJson(
283
+ client,
284
+ `${encodePageURI(path)}${before ? `?before=${before}` : ""}`,
285
+ ),
286
+ description: "Lists the revision history of a file.",
287
+ signatures: ["space.listRevisions(path, before?)"],
288
+ },
289
+ "space.getRevision": {
290
+ callback: async (
291
+ _ctx,
292
+ path: string,
293
+ rev: string,
294
+ parent?: boolean,
295
+ ): Promise<string> => {
296
+ const resp = await client.httpSpacePrimitives.authenticatedFetch(
297
+ revisionsUrl(
298
+ client,
299
+ `${encodePageURI(path)}?rev=${rev}${parent ? "&parent=1" : ""}`,
300
+ ),
301
+ { method: "GET" },
302
+ );
303
+ if (!resp.ok) {
304
+ throw Object.assign(
305
+ new Error(`Could not load revision: ${resp.status}`),
306
+ { status: resp.status },
307
+ );
308
+ }
309
+ return resp.text();
310
+ },
311
+ description:
312
+ "Reads the text of a file as it was at a given revision, or at that revision's parent.",
313
+ signatures: ["space.getRevision(path, rev, parent?)"],
314
+ },
315
+ "space.getRevisionDiff": {
316
+ callback: async (_ctx, path: string, rev?: string): Promise<string> => {
317
+ const resp = await client.httpSpacePrimitives.authenticatedFetch(
318
+ revisionsUrl(
319
+ client,
320
+ `${encodePageURI(path)}?${rev ? `rev=${rev}&` : ""}format=diff`,
321
+ ),
322
+ { method: "GET" },
323
+ );
324
+ if (!resp.ok) {
325
+ throw Object.assign(
326
+ new Error(`Could not load revision diff: ${resp.status}`),
327
+ { status: resp.status },
328
+ );
329
+ }
330
+ return resp.text();
331
+ },
332
+ description:
333
+ "Reads a unified diff of a revision's own change (vs its parent), or of the uncommitted change when no revision is given.",
334
+ signatures: ["space.getRevisionDiff(path, rev?)"],
335
+ },
336
+ "space.getSpaceLog": {
337
+ callback: (_ctx, before?: string, q?: string): Promise<SpaceLog> => {
338
+ const params: string[] = [];
339
+ if (before) params.push(`before=${before}`);
340
+ if (q) params.push(`q=${encodeURIComponent(q)}`);
341
+ return fetchRevisionsJson(
342
+ client,
343
+ params.length ? `?${params.join("&")}` : "",
344
+ );
345
+ },
346
+ description: "Lists the space-wide commit log.",
347
+ signatures: ["space.getSpaceLog(before?, q?)"],
348
+ },
150
349
  };
151
350
  }
152
351
 
153
352
  export function spaceWriteSyscalls(editor: Client): SysCallMapping {
154
353
  return {
354
+ "space.syncGitNow": {
355
+ callback: () => fetchRevisionsJson(editor, "_sync", {}),
356
+ description: "Requests sync on the existing Git connection.",
357
+ },
358
+ "space.resolveGitConflict": {
359
+ callback: (
360
+ _ctx,
361
+ id: string,
362
+ generation: string,
363
+ contentRevision: string,
364
+ action: GitConflictAction,
365
+ ): Promise<GitConflicts> =>
366
+ fetchRevisionsJson(editor, `_conflicts/${encodeURIComponent(id)}`, {
367
+ generation,
368
+ contentRevision,
369
+ action,
370
+ }),
371
+ description:
372
+ "Resolves a Git conflict if the merge and saved content still match.",
373
+ signatures: [
374
+ "space.resolveGitConflict(id, generation, contentRevision, action)",
375
+ ],
376
+ },
155
377
  "space.writePage": {
156
378
  callback: (_ctx, name: string, text: string): Promise<PageMeta> =>
157
379
  editor.space.writePage(name, text),
@@ -191,5 +413,26 @@ export function spaceWriteSyscalls(editor: Client): SysCallMapping {
191
413
  description: "Deletes an arbitrary file from the space.",
192
414
  signatures: ["space.deleteFile(name)"],
193
415
  },
416
+ "space.createRevisionSnapshot": {
417
+ callback: async (): Promise<boolean> => {
418
+ const resp = await editor.httpSpacePrimitives.authenticatedFetch(
419
+ revisionsUrl(editor, ""),
420
+ { method: "POST", headers: { Accept: "application/json" } },
421
+ );
422
+ if (resp.status === 404) {
423
+ throw new Error("Revisions are not available for this space");
424
+ }
425
+ if (resp.status === 409) {
426
+ throw new Error("Revisions are not managed for this space");
427
+ }
428
+ if (!resp.ok) {
429
+ throw new Error(`Snapshot request failed: ${resp.status}`);
430
+ }
431
+ return (await resp.json()).committed === true;
432
+ },
433
+ description:
434
+ "Commits everything outstanding as a revision now, rather than waiting for the automatic commit. False if there was nothing to commit.",
435
+ signatures: ["space.createRevisionSnapshot()"],
436
+ },
194
437
  };
195
438
  }