@lapis-notes/api 0.1.3 → 0.2.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 (199) hide show
  1. package/CHANGELOG.md +304 -13
  2. package/README.md +19 -0
  3. package/dist/__tests__/fixtures/indexed-mutations.json +6862 -0
  4. package/dist/__tests__/fixtures/memory-storage.json +309 -0
  5. package/dist/__tests__/fixtures/native-facade-contract.json +3224 -0
  6. package/dist/__tests__/fixtures/projection-query.json +195 -0
  7. package/dist/__tests__/fixtures/projection-values.json +60 -0
  8. package/dist/__tests__/fixtures/search-preparation.json +1513 -0
  9. package/dist/__tests__/fixtures/search-retrieval.json +15409 -0
  10. package/dist/__tests__/fixtures/search-semantic-model.json +3691 -0
  11. package/dist/__tests__/fixtures/search-semantic-retrieval.json +6603 -0
  12. package/dist/__tests__/fixtures/search-storage.json +409 -0
  13. package/dist/__tests__/fixtures/task-projection.json +428 -0
  14. package/dist/__tests__/indexed-mutation-support.js +262 -0
  15. package/dist/__tests__/projection-query-support.js +40 -0
  16. package/dist/__tests__/search-retrieval-support.js +47 -0
  17. package/dist/app-database-host.d.ts +1 -0
  18. package/dist/app-shell-plugin-persistence.d.ts +3 -2
  19. package/dist/app-shell-plugin-persistence.js +37 -3
  20. package/dist/cache.svelte.d.ts +8 -1
  21. package/dist/cache.svelte.js +103 -5
  22. package/dist/command.svelte.js +29 -2
  23. package/dist/components/editor/editor.css +4 -0
  24. package/dist/components/editor/editor.svelte +24 -11
  25. package/dist/components/editor/embedded-editor-surface.js +3 -1
  26. package/dist/components/editor/inline-title-alignment.d.ts +3 -0
  27. package/dist/components/editor/inline-title-alignment.js +57 -0
  28. package/dist/configuration.svelte.d.ts +270 -206
  29. package/dist/configuration.svelte.js +161 -86
  30. package/dist/context.svelte.d.ts +23 -3
  31. package/dist/context.svelte.js +68 -30
  32. package/dist/diagnostics.d.ts +8 -0
  33. package/dist/diagnostics.js +6 -0
  34. package/dist/editor-save-context.d.ts +35 -0
  35. package/dist/editor-save-context.js +132 -0
  36. package/dist/editor-view-registry.d.ts +10 -0
  37. package/dist/editor-view-registry.js +44 -4
  38. package/dist/editor.svelte.d.ts +19 -1
  39. package/dist/editor.svelte.js +528 -55
  40. package/dist/embed-registry.d.ts +10 -0
  41. package/dist/embed-registry.js +15 -0
  42. package/dist/file-editing-failures.d.ts +2 -0
  43. package/dist/file-editing-failures.js +29 -0
  44. package/dist/file-editing-native-issues.d.ts +19 -0
  45. package/dist/file-editing-native-issues.js +65 -0
  46. package/dist/file-editing-sessions.d.ts +76 -0
  47. package/dist/file-editing-sessions.js +367 -0
  48. package/dist/file-manager.d.ts +9 -1
  49. package/dist/file-manager.js +45 -3
  50. package/dist/file-state-store.d.ts +5 -0
  51. package/dist/file-state-store.js +34 -0
  52. package/dist/file-text-conflicts.d.ts +25 -0
  53. package/dist/file-text-conflicts.js +35 -0
  54. package/dist/file-text-mutation.d.ts +13 -0
  55. package/dist/file-text-mutation.js +100 -0
  56. package/dist/file-text-persistence.d.ts +36 -0
  57. package/dist/file-text-persistence.js +234 -0
  58. package/dist/file-text-state-writer.d.ts +34 -0
  59. package/dist/file-text-state-writer.js +183 -0
  60. package/dist/files-configuration.d.ts +20 -0
  61. package/dist/files-configuration.js +47 -0
  62. package/dist/generated/plugin-host-modules.schema.generated.d.ts +114 -34
  63. package/dist/generated/plugin-host-modules.schema.generated.js +132 -34
  64. package/dist/glob.d.ts +3 -0
  65. package/dist/glob.js +2 -1
  66. package/dist/icons/changes.d.ts +3 -0
  67. package/dist/icons/changes.js +16 -0
  68. package/dist/icons/index.d.ts +5 -1
  69. package/dist/icons/index.js +44 -3
  70. package/dist/icons/registry.d.ts +35 -0
  71. package/dist/icons/registry.js +154 -0
  72. package/dist/icons/safe-svg.d.ts +2 -0
  73. package/dist/icons/safe-svg.js +44 -0
  74. package/dist/index.d.ts +6 -1
  75. package/dist/index.js +4 -1
  76. package/dist/lapis-extension.d.ts +6 -2
  77. package/dist/lapis-extension.js +2 -0
  78. package/dist/metadata-store.d.ts +23 -0
  79. package/dist/metadata-store.js +1 -0
  80. package/dist/metadata.svelte.js +45 -2
  81. package/dist/native-background-indexing.d.ts +48 -0
  82. package/dist/native-background-indexing.js +208 -0
  83. package/dist/native-database-protocol.d.ts +29 -0
  84. package/dist/native-database-protocol.js +1 -0
  85. package/dist/native-editing-intent-lease.d.ts +17 -0
  86. package/dist/native-editing-intent-lease.js +188 -0
  87. package/dist/native-editing-intents-protocol.d.ts +70 -0
  88. package/dist/native-editing-intents-protocol.js +1 -0
  89. package/dist/native-editing-intents.d.ts +8 -0
  90. package/dist/native-editing-intents.js +1 -0
  91. package/dist/native-editing-problems.d.ts +3 -0
  92. package/dist/native-editing-problems.js +180 -0
  93. package/dist/native-editing-recovery.d.ts +24 -0
  94. package/dist/native-editing-recovery.js +198 -0
  95. package/dist/native-editing-session.d.ts +51 -0
  96. package/dist/native-editing-session.js +251 -0
  97. package/dist/native-history-index-protocol.d.ts +80 -0
  98. package/dist/native-history-index-protocol.js +1 -0
  99. package/dist/native-history-index.d.ts +6 -0
  100. package/dist/native-history-index.js +1 -0
  101. package/dist/native-indexing-protocol.d.ts +63 -0
  102. package/dist/native-indexing-protocol.js +1 -0
  103. package/dist/native-participant-protocol.d.ts +67 -0
  104. package/dist/native-participant-protocol.js +3 -0
  105. package/dist/native-queue-protocol.d.ts +73 -0
  106. package/dist/native-queue-protocol.js +3 -0
  107. package/dist/native-tasks-protocol.d.ts +264 -0
  108. package/dist/native-tasks-protocol.js +1 -0
  109. package/dist/native-tasks.d.ts +35 -0
  110. package/dist/native-tasks.js +79 -0
  111. package/dist/native-turn-protocol.d.ts +81 -0
  112. package/dist/native-turn-protocol.js +1 -0
  113. package/dist/native-turns.d.ts +29 -0
  114. package/dist/native-turns.js +2 -0
  115. package/dist/plugin-asset-server.js +3 -2
  116. package/dist/plugin-dependency-resolver.d.ts +1 -1
  117. package/dist/plugin-distribution/compatibility.d.ts +1 -1
  118. package/dist/plugin-distribution/compatibility.js +10 -5
  119. package/dist/plugin-distribution/installed-plugin-state.js +2 -1
  120. package/dist/plugin-distribution/installer.d.ts +6 -0
  121. package/dist/plugin-distribution/installer.js +90 -37
  122. package/dist/plugin-distribution/manager.d.ts +4 -1
  123. package/dist/plugin-distribution/manager.js +12 -5
  124. package/dist/plugin-distribution/registry-cache.js +2 -1
  125. package/dist/plugin-distribution/registry-source-state.js +2 -1
  126. package/dist/plugin-distribution/schemas.d.ts +26 -8
  127. package/dist/plugin-distribution/schemas.js +2 -1
  128. package/dist/plugin-distribution/types.d.ts +4 -0
  129. package/dist/plugin-manager.d.ts +21 -5
  130. package/dist/plugin-manager.js +228 -64
  131. package/dist/plugin-runtime-entry.d.ts +1 -1
  132. package/dist/plugin-runtime-entry.js +16 -0
  133. package/dist/plugin.d.ts +19 -3
  134. package/dist/plugin.js +110 -44
  135. package/dist/registry-schema/catalog-v1.d.ts +60 -9
  136. package/dist/registry-schema/catalog-v1.js +2 -1
  137. package/dist/registry-schema/catalog-v2.d.ts +60 -9
  138. package/dist/registry-schema/catalog-v2.js +1 -1
  139. package/dist/search-result-provider.d.ts +95 -0
  140. package/dist/search-result-provider.js +42 -0
  141. package/dist/status-bar.svelte.d.ts +26 -20
  142. package/dist/status-bar.svelte.js +1 -0
  143. package/dist/storage/app-database.d.ts +9 -23
  144. package/dist/storage/app-database.js +41 -34
  145. package/dist/storage/desktop-native.d.ts +5 -3
  146. package/dist/storage/desktop-native.js +59 -6
  147. package/dist/storage/directory-watcher.d.ts +27 -0
  148. package/dist/storage/directory-watcher.js +201 -0
  149. package/dist/storage/fs.d.ts +14 -27
  150. package/dist/storage/fs.js +2 -168
  151. package/dist/storage/index-projection.js +33 -15
  152. package/dist/storage/memory-vault-adapter.d.ts +2 -0
  153. package/dist/storage/memory-vault-adapter.js +14 -1
  154. package/dist/storage/path.d.ts +6 -2
  155. package/dist/storage/path.js +8 -2
  156. package/dist/storage/projection-query-candidates.d.ts +3 -0
  157. package/dist/storage/projection-query-candidates.js +69 -0
  158. package/dist/storage/remote-app-database.d.ts +2 -2
  159. package/dist/storage/remote-app-database.js +1 -1
  160. package/dist/storage/runtime.d.ts +3 -0
  161. package/dist/storage/runtime.js +3 -0
  162. package/dist/storage/search-embedding-record.d.ts +3 -0
  163. package/dist/storage/search-embedding-record.js +18 -0
  164. package/dist/storage/turso-app-database.d.ts +2 -0
  165. package/dist/storage/turso-app-database.js +67 -30
  166. package/dist/storage/vault-listing.d.ts +3 -0
  167. package/dist/storage/vault-listing.js +36 -0
  168. package/dist/storage/vault-reconciliation.d.ts +19 -0
  169. package/dist/storage/vault-reconciliation.js +137 -0
  170. package/dist/storage/vault-session.d.ts +8 -0
  171. package/dist/storage/vault-session.js +10 -2
  172. package/dist/storage/vault.svelte.d.ts +53 -4
  173. package/dist/storage/vault.svelte.js +223 -87
  174. package/dist/sync-editor-flush.d.ts +6 -0
  175. package/dist/sync-editor-flush.js +22 -0
  176. package/dist/sync-host-local.d.ts +15 -0
  177. package/dist/sync-host-local.js +228 -0
  178. package/dist/sync-host-security.d.ts +24 -9
  179. package/dist/sync-host-security.js +47 -14
  180. package/dist/sync-host.d.ts +33 -4
  181. package/dist/sync-host.js +160 -10
  182. package/dist/sync-repository.d.ts +106 -0
  183. package/dist/sync-repository.js +18 -0
  184. package/dist/vault-change-views.d.ts +4 -0
  185. package/dist/vault-change-views.js +43 -0
  186. package/dist/vault-changes.d.ts +37 -0
  187. package/dist/vault-changes.js +228 -0
  188. package/dist/view.svelte.d.ts +182 -149
  189. package/dist/view.svelte.js +39 -21
  190. package/dist/workspace-close-guard.d.ts +3 -0
  191. package/dist/workspace-close-guard.js +35 -0
  192. package/dist/workspace-host-internal.js +10 -2
  193. package/dist/workspace-layout-normalizer.d.ts +4 -2
  194. package/dist/workspace-layout-normalizer.js +5 -5
  195. package/dist/workspace-view-document.d.ts +3 -0
  196. package/dist/workspace-view-document.js +12 -0
  197. package/dist/workspace.svelte.d.ts +13 -2
  198. package/dist/workspace.svelte.js +371 -82
  199. package/package.json +61 -8
@@ -1,9 +1,11 @@
1
- import { Annotation, EditorState, Transaction, StateField, EditorSelection as CodeMirrorSelection, } from "@codemirror/state";
1
+ import { Annotation, Compartment, EditorState, StateEffect, Transaction, StateField, EditorSelection as CodeMirrorSelection, } from "@codemirror/state";
2
2
  import { EditorView } from "@codemirror/view";
3
3
  import { redo as redoCommand, undo as undoCommand } from "@codemirror/commands";
4
4
  import { EventDispatcher } from "./events";
5
- import debounce from "lodash-es/debounce";
5
+ import { tick } from "svelte";
6
+ import { EditorSaveContext, } from "./editor-save-context";
6
7
  import { resolveApplication } from "./application-compatibility";
8
+ import { FileTextPersistence } from "./file-text-persistence";
7
9
  export const editorViewField = StateField.define({
8
10
  create() {
9
11
  return {
@@ -49,26 +51,35 @@ export function createEditorState(editor, doc, extensions = [], selection) {
49
51
  return EditorState.create({
50
52
  doc,
51
53
  selection,
52
- extensions: [
53
- editorViewField.init(() => ({
54
- get app() {
55
- return editor.app;
56
- },
57
- editor,
58
- get file() {
59
- return editor.file;
60
- },
61
- })),
62
- EditorView.updateListener.of((v) => {
63
- if (v.docChanged &&
64
- !v.transactions.some((it) => it.annotation(syncAnnotation))) {
65
- editor.queueChange(v.state.doc.toString());
66
- }
67
- }),
68
- ...extensions,
69
- ],
54
+ extensions: editorStateExtensions(editor, extensions),
70
55
  });
71
56
  }
57
+ function editorStateExtensions(editor, extensions) {
58
+ return [
59
+ editorViewField.init(() => ({
60
+ get app() {
61
+ return editor.app;
62
+ },
63
+ editor,
64
+ get file() {
65
+ return editor.file;
66
+ },
67
+ })),
68
+ editor.protectionExtensions(),
69
+ EditorState.transactionFilter.of((transaction) => transaction.docChanged &&
70
+ editor.editingProtected &&
71
+ !transaction.annotation(syncAnnotation)
72
+ ? []
73
+ : transaction),
74
+ EditorView.updateListener.of((v) => {
75
+ if (v.docChanged &&
76
+ !v.transactions.some((it) => it.annotation(syncAnnotation))) {
77
+ editor.queueChange(v.state.doc.toString());
78
+ }
79
+ }),
80
+ ...extensions,
81
+ ];
82
+ }
72
83
  export function createEditor(editor, doc, ...extensions) {
73
84
  return new EditorView({
74
85
  doc,
@@ -97,21 +108,380 @@ export class Editor extends EventDispatcher {
97
108
  data;
98
109
  extensions;
99
110
  view;
100
- file = $state(null);
101
- persistence = "vault";
111
+ #file = $state.raw(null);
112
+ #persistence = "vault";
113
+ #externalWriter;
114
+ #editingProtected = $state(0);
115
+ #protection = new Compartment();
116
+ #documents = new Set();
117
+ #document;
102
118
  id = crypto.randomUUID();
103
119
  destroyed = false;
104
120
  application;
105
- pendingChange = debounce(async (content) => {
106
- if (this.data === content) {
121
+ #writeTail = Promise.resolve();
122
+ #sequence = 0;
123
+ #completedSequence = 0;
124
+ #documentGeneration = 0;
125
+ #fileReadGeneration = 0;
126
+ #refreshError;
127
+ #loading;
128
+ #controlledLoad = false;
129
+ get file() {
130
+ return this.#file;
131
+ }
132
+ set file(file) {
133
+ if (file === this.#file && file?.path === this.#document?.path)
134
+ return;
135
+ this.#assertWritable();
136
+ const next = this.#createDocument(this.app, file, this.#persistence, this.#externalWriter);
137
+ this.#retireDocument();
138
+ this.#file = file;
139
+ this.#document = next;
140
+ this.#documentGeneration++;
141
+ }
142
+ get persistence() {
143
+ return this.#persistence;
144
+ }
145
+ set persistence(value) {
146
+ if (value === this.#persistence)
107
147
  return;
148
+ this.#assertWritable();
149
+ const next = this.#createDocument(this.app, this.#file, value, this.#externalWriter);
150
+ this.#retireDocument();
151
+ this.#persistence = value;
152
+ this.#document = next;
153
+ this.#documentGeneration++;
154
+ }
155
+ /** Capture an explicit asynchronous owner for external-mode persistence. */
156
+ setExternalPersistenceWriter(writer) {
157
+ if (writer === this.#externalWriter)
158
+ return this;
159
+ this.#assertWritable();
160
+ const next = this.#createDocument(this.app, this.#file, this.#persistence, writer);
161
+ this.#retireDocument();
162
+ this.#externalWriter = writer;
163
+ this.#document = next;
164
+ this.#documentGeneration++;
165
+ return this;
166
+ }
167
+ #assertWritable() {
168
+ if (this.destroyed)
169
+ throw new Error("Editor is destroyed");
170
+ if (this.editingProtected)
171
+ throw new Error("Editor editing is protected");
172
+ }
173
+ #retireDocument() {
174
+ this.#document?.saves.retire();
175
+ }
176
+ #serializeWrite(run) {
177
+ const result = this.#writeTail.then(run);
178
+ this.#writeTail = result.catch(() => undefined);
179
+ return result;
180
+ }
181
+ #createDocument(application, file, persistence, writer) {
182
+ const initialText = this.data;
183
+ const document = {
184
+ application,
185
+ file,
186
+ path: file?.path,
187
+ persistence,
188
+ writer,
189
+ };
190
+ document.saves = new EditorSaveContext({
191
+ unownedExternal: persistence === "external" && !writer,
192
+ serialize: (run) => this.#serializeWrite(run),
193
+ persist: async (snapshot) => {
194
+ if (document.unregister?.native)
195
+ await document.unregister.ready();
196
+ if (file?.path !== document.path)
197
+ throw new Error("Editor persistence target changed");
198
+ if (document.text)
199
+ await document.text.persist(snapshot.content);
200
+ else if (persistence === "vault" && file)
201
+ await application.vault.modify(file, snapshot.content);
202
+ else if (persistence === "external" && writer)
203
+ await writer({ application, file, content: snapshot.content });
204
+ },
205
+ saved: (snapshot) => {
206
+ this.#completedSequence = Math.max(this.#completedSequence, snapshot.sequence);
207
+ if (!this.destroyed &&
208
+ this.#document === document &&
209
+ this.app === application &&
210
+ this.file === file &&
211
+ snapshot.documentGeneration === this.#documentGeneration) {
212
+ this.data = snapshot.content;
213
+ if (snapshot.content === this.getValue())
214
+ this.onChange(snapshot.content);
215
+ }
216
+ },
217
+ released: () => {
218
+ document.unregister?.();
219
+ this.#documents.delete(document);
220
+ },
221
+ });
222
+ if (persistence === "vault" &&
223
+ file &&
224
+ application.vault.supportsConditionalTextWrites) {
225
+ document.text = new FileTextPersistence({
226
+ vault: application.vault,
227
+ file,
228
+ baseline: this.data,
229
+ conflicts: application.fileEditingSessions.textConflicts,
230
+ local: () => document.saves.latest?.content ?? this.data,
231
+ protect: () => ({
232
+ release: !this.destroyed && this.#document === document
233
+ ? this.#holdEditing()
234
+ : () => undefined,
235
+ saved: () => {
236
+ const snapshot = document.saves.latest;
237
+ if (!snapshot)
238
+ throw new Error("No retained editor snapshot");
239
+ document.saves.accept(snapshot);
240
+ },
241
+ reset: (contents) => {
242
+ if (!this.destroyed && this.#document === document) {
243
+ this.#replaceContent(contents);
244
+ this.onChange(contents);
245
+ }
246
+ document.saves.discard();
247
+ },
248
+ }),
249
+ settle: async () => {
250
+ await document.loading;
251
+ try {
252
+ await document.saves.flush(false);
253
+ }
254
+ catch (error) {
255
+ if (!document.text?.hasConflict)
256
+ throw error;
257
+ }
258
+ },
259
+ serialize: (run) => this.#serializeWrite(run),
260
+ });
108
261
  }
109
- if (this.file && this.persistence === "vault") {
110
- await this.app.vault.modify(this.file, content);
262
+ if (file) {
263
+ if (!application.fileEditingSessions)
264
+ throw new Error("App has no file editing-session registry");
265
+ document.unregister = application.fileEditingSessions.register(file.path, {
266
+ native: {
267
+ hold: () => this.#holdEditing(),
268
+ savedText: () => document.text
269
+ ? document.text.baseline
270
+ : (document.saves.latest?.content ?? initialText),
271
+ refreshSavedText: (contents) => {
272
+ if (document.loading || document.saves.dirty || !document.text)
273
+ throw new Error("Native editor refresh requires clean conditional text authority");
274
+ document.text.acceptBaseline(contents);
275
+ if (!this.destroyed &&
276
+ this.#document === document &&
277
+ contents !== null) {
278
+ if (this.getValue() !== contents)
279
+ this.#replaceContent(contents);
280
+ this.data = contents;
281
+ this.onChange(contents);
282
+ }
283
+ },
284
+ },
285
+ get dirty() {
286
+ return !!document.loading || document.saves.dirty;
287
+ },
288
+ flush: async () => {
289
+ await document.loading;
290
+ await document.saves.flush();
291
+ },
292
+ protect: () => this.#protectDocument(document),
293
+ });
111
294
  }
112
- this.data = content;
113
- this.onChange(content);
114
- }, 500);
295
+ this.#documents.add(document);
296
+ return document;
297
+ }
298
+ #currentDocument() {
299
+ return (this.#document ??= this.#createDocument(this.app, this.file, this.persistence, this.#externalWriter));
300
+ }
301
+ captureChange(content) {
302
+ return {
303
+ content,
304
+ documentGeneration: this.#documentGeneration,
305
+ sequence: ++this.#sequence,
306
+ };
307
+ }
308
+ get hasUnsavedChanges() {
309
+ return ((!this.destroyed && this.getValue() !== this.data) ||
310
+ [...this.#documents].some((document) => document.saves.dirty));
311
+ }
312
+ get editingProtected() {
313
+ return this.destroyed || this.#editingProtected > 0;
314
+ }
315
+ /** Native CodeMirror protection, retained whenever editor state is rebuilt. */
316
+ protectionExtensions() {
317
+ return this.#protection.of([
318
+ EditorState.readOnly.of(this.editingProtected),
319
+ EditorView.editable.of(!this.editingProtected),
320
+ ]);
321
+ }
322
+ #holdEditing() {
323
+ this.#editingProtected++;
324
+ this.#updateProtection();
325
+ let held = true;
326
+ return () => {
327
+ if (!held)
328
+ return;
329
+ held = false;
330
+ this.#editingProtected--;
331
+ this.#updateProtection();
332
+ };
333
+ }
334
+ #updateProtection() {
335
+ if (!this.destroyed)
336
+ this.view.dispatch({
337
+ effects: this.#protection.reconfigure([
338
+ EditorState.readOnly.of(this.editingProtected),
339
+ EditorView.editable.of(!this.editingProtected),
340
+ ]),
341
+ });
342
+ }
343
+ async #protectDocument(document) {
344
+ if (document.persistence === "external" && !document.writer)
345
+ throw new Error("External editor persistence requires an explicit writer for protection");
346
+ const release = !this.destroyed && (this.#document === document || !!document.loading)
347
+ ? this.#holdEditing()
348
+ : () => undefined;
349
+ try {
350
+ await tick();
351
+ await document.loading;
352
+ await document.saves.flush();
353
+ return release;
354
+ }
355
+ catch (error) {
356
+ release();
357
+ throw error;
358
+ }
359
+ }
360
+ /** Protect the current document and drain its captured persistence. */
361
+ protect() {
362
+ if (this.destroyed)
363
+ return Promise.reject(new Error("Editor is destroyed"));
364
+ return this.#protectDocument(this.#currentDocument());
365
+ }
366
+ /**
367
+ * Register and protect a target, then publish its file and contents
368
+ * atomically.
369
+ */
370
+ async loadDocument(file, read, publish) {
371
+ if (this.destroyed)
372
+ throw new Error("Editor is destroyed");
373
+ if (this.#editingProtected > (this.#loading ? 1 : 0))
374
+ throw new Error("Editor editing is protected");
375
+ const application = this.app, path = file.path;
376
+ // Admission failure leaves any previous load and binding untouched.
377
+ const document = this.#createDocument(application, file, this.persistence, this.#externalWriter);
378
+ const release = this.#holdEditing();
379
+ let cancelRead;
380
+ const cancellation = new Promise((resolve) => {
381
+ cancelRead = resolve;
382
+ });
383
+ let finish;
384
+ document.loading = new Promise((resolve) => {
385
+ finish = resolve;
386
+ });
387
+ let canceled = false, completed = false;
388
+ const load = {
389
+ cancel: () => {
390
+ if (completed || canceled)
391
+ return;
392
+ canceled = true;
393
+ cancelRead({ canceled: true });
394
+ document.loading = undefined;
395
+ document.saves.retire();
396
+ finish();
397
+ release();
398
+ if (this.#loading === load)
399
+ this.#loading = undefined;
400
+ },
401
+ };
402
+ this.#loading?.cancel();
403
+ this.#loading = load;
404
+ let published = false;
405
+ try {
406
+ if (document.unregister?.native) {
407
+ const admission = await Promise.race([
408
+ document.unregister.ready().then(() => ({ ready: true })),
409
+ cancellation,
410
+ ]);
411
+ if ("canceled" in admission || canceled)
412
+ return false;
413
+ }
414
+ const drained = await Promise.race([
415
+ this.flushChanges().then(() => ({ drained: true })),
416
+ cancellation,
417
+ ]);
418
+ if ("canceled" in drained || canceled)
419
+ return false;
420
+ const result = await Promise.race([
421
+ Promise.resolve()
422
+ .then(read)
423
+ .then((contents) => ({ contents })),
424
+ cancellation,
425
+ ]);
426
+ if ("canceled" in result || canceled)
427
+ return false;
428
+ if (this.destroyed || this.app !== application || file.path !== path)
429
+ throw new Error("Editor document load authority changed");
430
+ const previous = {
431
+ document: this.#document,
432
+ file: this.#file,
433
+ data: this.data,
434
+ state: this.view.state,
435
+ extensions: this.extensions,
436
+ };
437
+ this.#document = document;
438
+ this.#file = file;
439
+ try {
440
+ this.#controlledLoad = true;
441
+ this.setValue(result.contents);
442
+ publish?.(result.contents);
443
+ if (this.destroyed || canceled)
444
+ throw new Error("Editor document load canceled during publication");
445
+ if (this.app !== application || file.path !== path)
446
+ throw new Error("Editor document load authority changed");
447
+ published = true;
448
+ previous.document?.saves.retire();
449
+ }
450
+ catch (error) {
451
+ this.#document = previous.document;
452
+ this.#file = previous.file;
453
+ this.data = previous.data;
454
+ this.extensions = previous.extensions;
455
+ this.#documentGeneration++;
456
+ if (!this.destroyed)
457
+ this.view.setState(previous.state);
458
+ throw error;
459
+ }
460
+ finally {
461
+ this.#controlledLoad = false;
462
+ }
463
+ return true;
464
+ }
465
+ finally {
466
+ if (!canceled) {
467
+ completed = true;
468
+ document.loading = undefined;
469
+ if (!published)
470
+ document.saves.retire();
471
+ finish();
472
+ release();
473
+ }
474
+ if (this.#loading === load)
475
+ this.#loading = undefined;
476
+ }
477
+ }
478
+ /** Cancel a pending publication while observing any late read result. */
479
+ cancelDocumentLoad() {
480
+ this.#loading?.cancel();
481
+ }
482
+ get refreshError() {
483
+ return this.#refreshError;
484
+ }
115
485
  constructor(data = "", extensions = [], application) {
116
486
  super();
117
487
  this.data = data;
@@ -121,19 +491,32 @@ export class Editor extends EventDispatcher {
121
491
  }
122
492
  /** Bind this editor to the application that owns its view. */
123
493
  bindApplication(application) {
494
+ if (this.application === application)
495
+ return this;
496
+ this.#assertWritable();
497
+ const next = this.#createDocument(application, this.#file, this.#persistence, this.#externalWriter);
498
+ this.#retireDocument();
124
499
  this.application = application;
500
+ this.#document = next;
501
+ this.#documentGeneration++;
125
502
  return this;
126
503
  }
127
504
  /** The owning application, with the global alias retained as fallback. */
128
505
  get app() {
129
506
  return resolveApplication(this.application);
130
507
  }
131
- save = debounce(() => {
132
- if (this.file) {
133
- return this.app.vault.modify(this.file, this.getValue());
134
- }
135
- return Promise.resolve();
136
- }, 500);
508
+ save = Object.assign(() => {
509
+ this.#assertWritable();
510
+ const document = this.#currentDocument();
511
+ document.unregister?.changed();
512
+ return document.saves.queue(this.captureChange(this.getValue()), true);
513
+ }, {
514
+ flush: () => this.#document?.saves.flushSave(),
515
+ cancel: () => {
516
+ this.#assertWritable();
517
+ this.#document?.saves.cancelSave();
518
+ },
519
+ });
137
520
  get cm() {
138
521
  return this.view;
139
522
  }
@@ -142,15 +525,41 @@ export class Editor extends EventDispatcher {
142
525
  }
143
526
  /** Queue the latest editor contents for persistence/change notification. */
144
527
  queueChange(data) {
145
- this.pendingChange(data);
146
- }
147
- /** Flush the latest queued change before a view or embedded surface closes. */
148
- flushChanges() {
149
- return Promise.resolve(this.pendingChange.flush()).then(() => undefined);
528
+ this.#assertWritable();
529
+ const document = this.#currentDocument();
530
+ document.unregister?.changed();
531
+ document.saves.queue(this.captureChange(data));
532
+ document.text?.updateLocal(data);
533
+ }
534
+ /** Drain every captured document, including failed or retired saves. */
535
+ async flushChanges() {
536
+ const errors = new Map();
537
+ const visited = new Set();
538
+ let sequence, generation;
539
+ do {
540
+ sequence = this.#sequence;
541
+ generation = this.#documentGeneration;
542
+ const documents = [...this.#documents];
543
+ const results = await Promise.allSettled(documents.map((document) => {
544
+ const retry = !visited.has(document);
545
+ visited.add(document);
546
+ return document.saves.flush(retry);
547
+ }));
548
+ results.forEach((result, index) => {
549
+ if (result.status === "rejected")
550
+ errors.set(documents[index], result.reason);
551
+ });
552
+ } while (sequence !== this.#sequence ||
553
+ generation !== this.#documentGeneration);
554
+ if (errors.size === 1)
555
+ throw errors.values().next().value;
556
+ if (errors.size)
557
+ throw new AggregateError([...errors.values()], "Editor persistence failed");
150
558
  }
151
559
  /** Cancel a queued change when the owning document is intentionally reset. */
152
560
  cancelPendingChanges() {
153
- this.pendingChange.cancel();
561
+ this.#assertWritable();
562
+ this.#document?.saves.cancelChange();
154
563
  }
155
564
  getValue() {
156
565
  return this.view.state.doc.toString();
@@ -159,16 +568,25 @@ export class Editor extends EventDispatcher {
159
568
  this.view.requestMeasure();
160
569
  }
161
570
  destroy() {
162
- if (this.destroyed) {
571
+ if (this.destroyed)
163
572
  return;
164
- }
165
573
  this.destroyed = true;
166
- this.pendingChange.cancel();
167
- this.save.cancel();
574
+ this.cancelDocumentLoad();
575
+ for (const document of this.#documents)
576
+ document.saves.retire();
168
577
  this.view.destroy();
169
578
  }
579
+ /** Stop editing immediately and await all retained document persistence. */
580
+ async dispose() {
581
+ this.destroy();
582
+ await this.flushChanges();
583
+ }
170
584
  trackChanges(callback) {
171
- const editorUpdated = this.app.workspace.on("editor-updated", (editor, transactions) => {
585
+ const owner = this.app;
586
+ let active = true;
587
+ const editorUpdated = owner.workspace.on("editor-updated", (editor, transactions) => {
588
+ if (!active || this.destroyed || this.app !== owner)
589
+ return;
172
590
  if (editor.id !== this.id && editor.file?.path === this.file?.path) {
173
591
  if (this.getValue() !== editor.getValue()) {
174
592
  this.view.dispatch(...transactions);
@@ -177,22 +595,62 @@ export class Editor extends EventDispatcher {
177
595
  }
178
596
  }
179
597
  });
180
- const fileChanged = this.app.workspace.on("file-change", (file, event) => {
598
+ const fileChanged = owner.workspace.on("file-change", (file, event) => {
599
+ if (!active || this.destroyed || this.app !== owner)
600
+ return;
181
601
  if (file.path === this.file?.path) {
182
602
  if (event !== "delete" && file) {
183
- this.app.vault.read(file).then((contents) => {
603
+ const application = this.app, target = this.file, path = target?.path;
604
+ const readGeneration = ++this.#fileReadGeneration, documentGeneration = this.#documentGeneration;
605
+ const sequence = this.#sequence, completed = this.#completedSequence;
606
+ void application.vault
607
+ .read(file)
608
+ .then((contents) => {
609
+ if (!active ||
610
+ this.destroyed ||
611
+ this.app !== application ||
612
+ this.file !== target ||
613
+ this.file?.path !== path ||
614
+ this.#fileReadGeneration !== readGeneration ||
615
+ this.#documentGeneration !== documentGeneration ||
616
+ this.#sequence !== sequence ||
617
+ this.#completedSequence !== completed)
618
+ return;
619
+ this.#refreshError = undefined;
620
+ if (this.getValue() === contents) {
621
+ this.data = contents;
622
+ if (!this.#document?.saves.dirty)
623
+ this.#document?.text?.acceptBaseline(contents);
624
+ return;
625
+ }
626
+ if (this.getValue() !== this.data ||
627
+ this.#document?.saves.dirty)
628
+ return;
184
629
  if (this.getValue() !== contents) {
185
- this.replaceContent(contents);
630
+ this.#replaceContent(contents);
186
631
  this.onChange(this.getValue());
187
632
  callback?.(this.getValue(), this);
188
633
  }
634
+ })
635
+ .catch((error) => {
636
+ if (active &&
637
+ !this.destroyed &&
638
+ this.app === application &&
639
+ this.file?.path === path &&
640
+ this.#fileReadGeneration === readGeneration &&
641
+ this.file === target &&
642
+ this.#documentGeneration === documentGeneration &&
643
+ this.#sequence === sequence &&
644
+ this.#completedSequence === completed)
645
+ this.#refreshError = error;
189
646
  });
190
647
  }
191
648
  }
192
649
  });
193
650
  return () => {
194
- this.app.workspace.offref(editorUpdated);
195
- this.app.workspace.offref(fileChanged);
651
+ active = false;
652
+ owner.workspace.offref(editorUpdated);
653
+ owner.workspace.offref(fileChanged);
196
654
  };
197
655
  }
198
656
  updateExtensions(extensions, context) {
@@ -204,11 +662,10 @@ export class Editor extends EventDispatcher {
204
662
  }
205
663
  span.setAttribute("editor.extension_count", editorExtensions.length);
206
664
  span.setAttribute("file.extension", ext ?? "");
207
- const content = this.getValue();
208
- const selection = this.view.state.selection;
209
665
  this.extensions = editorExtensions;
210
- this.data = content;
211
- this.view.setState(createEditorState(this, content, this.extensions, selection));
666
+ this.view.dispatch({
667
+ effects: StateEffect.reconfigure.of(editorStateExtensions(this, this.extensions)),
668
+ });
212
669
  return this;
213
670
  }, {
214
671
  attributes: { "file.extension": this.file?.extension ?? "" },
@@ -216,6 +673,14 @@ export class Editor extends EventDispatcher {
216
673
  });
217
674
  }
218
675
  setValue(content) {
676
+ if (!this.#controlledLoad)
677
+ this.#assertWritable();
678
+ else if (this.destroyed)
679
+ throw new Error("Editor is destroyed");
680
+ this.#documentGeneration++;
681
+ this.#refreshError = undefined;
682
+ if (!this.#document?.saves.dirty)
683
+ this.#document?.text?.acceptBaseline(content);
219
684
  this.app.telemetry.measure("editor.set_value", (span) => {
220
685
  span.setAttribute("editor.content_length", content.length);
221
686
  span.setAttribute("file.extension", this.file?.extension ?? "");
@@ -227,6 +692,14 @@ export class Editor extends EventDispatcher {
227
692
  });
228
693
  }
229
694
  replaceContent(content, options = {}) {
695
+ this.#assertWritable();
696
+ this.#replaceContent(content, options);
697
+ }
698
+ #replaceContent(content, options = {}) {
699
+ this.#documentGeneration++;
700
+ this.#refreshError = undefined;
701
+ if (!this.#document?.saves.dirty)
702
+ this.#document?.text?.acceptBaseline(content);
230
703
  this.data = content;
231
704
  const annotations = [syncAnnotation.of(true)];
232
705
  this.view.dispatch({