@lapis-notes/api 0.1.3 → 0.1.4
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.
- package/CHANGELOG.md +112 -14
- package/dist/components/editor/editor.svelte +3 -2
- package/dist/context.svelte.d.ts +2 -0
- package/dist/context.svelte.js +6 -11
- package/dist/editor-save-context.d.ts +24 -0
- package/dist/editor-save-context.js +101 -0
- package/dist/editor.svelte.d.ts +19 -1
- package/dist/editor.svelte.js +416 -34
- package/dist/file-editing-sessions.d.ts +28 -0
- package/dist/file-editing-sessions.js +197 -0
- package/dist/file-manager.d.ts +1 -0
- package/dist/file-manager.js +8 -0
- package/dist/generated/plugin-host-modules.schema.generated.d.ts +64 -34
- package/dist/generated/plugin-host-modules.schema.generated.js +67 -34
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/metadata-store.d.ts +23 -0
- package/dist/metadata-store.js +1 -0
- package/dist/plugin-manager.d.ts +1 -1
- package/dist/plugin.d.ts +1 -1
- package/dist/plugin.js +17 -5
- package/dist/storage/app-database.d.ts +3 -21
- package/dist/sync-editor-flush.d.ts +6 -0
- package/dist/sync-editor-flush.js +22 -0
- package/dist/sync-host-local.d.ts +15 -0
- package/dist/sync-host-local.js +228 -0
- package/dist/sync-host-security.d.ts +24 -9
- package/dist/sync-host-security.js +47 -14
- package/dist/sync-host.d.ts +23 -3
- package/dist/sync-host.js +155 -9
- package/dist/sync-repository.d.ts +106 -0
- package/dist/sync-repository.js +18 -0
- package/dist/view.svelte.js +28 -19
- package/package.json +17 -1
package/dist/editor.svelte.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { Annotation, EditorState, Transaction, StateField, EditorSelection as CodeMirrorSelection, } from "@codemirror/state";
|
|
1
|
+
import { Annotation, Compartment, EditorState, 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
|
|
5
|
+
import { tick } from "svelte";
|
|
6
|
+
import { EditorSaveContext, } from "./editor-save-context";
|
|
6
7
|
import { resolveApplication } from "./application-compatibility";
|
|
7
8
|
export const editorViewField = StateField.define({
|
|
8
9
|
create() {
|
|
@@ -59,6 +60,12 @@ export function createEditorState(editor, doc, extensions = [], selection) {
|
|
|
59
60
|
return editor.file;
|
|
60
61
|
},
|
|
61
62
|
})),
|
|
63
|
+
editor.protectionExtensions(),
|
|
64
|
+
EditorState.transactionFilter.of((transaction) => transaction.docChanged &&
|
|
65
|
+
editor.editingProtected &&
|
|
66
|
+
!transaction.annotation(syncAnnotation)
|
|
67
|
+
? []
|
|
68
|
+
: transaction),
|
|
62
69
|
EditorView.updateListener.of((v) => {
|
|
63
70
|
if (v.docChanged &&
|
|
64
71
|
!v.transactions.some((it) => it.annotation(syncAnnotation))) {
|
|
@@ -97,21 +104,304 @@ export class Editor extends EventDispatcher {
|
|
|
97
104
|
data;
|
|
98
105
|
extensions;
|
|
99
106
|
view;
|
|
100
|
-
file = $state(null);
|
|
101
|
-
persistence = "vault";
|
|
107
|
+
#file = $state.raw(null);
|
|
108
|
+
#persistence = "vault";
|
|
109
|
+
#externalWriter;
|
|
110
|
+
#editingProtected = $state(0);
|
|
111
|
+
#protection = new Compartment();
|
|
112
|
+
#documents = new Set();
|
|
113
|
+
#document;
|
|
102
114
|
id = crypto.randomUUID();
|
|
103
115
|
destroyed = false;
|
|
104
116
|
application;
|
|
105
|
-
|
|
106
|
-
|
|
117
|
+
#writeTail = Promise.resolve();
|
|
118
|
+
#sequence = 0;
|
|
119
|
+
#completedSequence = 0;
|
|
120
|
+
#documentGeneration = 0;
|
|
121
|
+
#fileReadGeneration = 0;
|
|
122
|
+
#refreshError;
|
|
123
|
+
#loading;
|
|
124
|
+
#controlledLoad = false;
|
|
125
|
+
get file() {
|
|
126
|
+
return this.#file;
|
|
127
|
+
}
|
|
128
|
+
set file(file) {
|
|
129
|
+
if (file === this.#file && file?.path === this.#document?.path)
|
|
107
130
|
return;
|
|
131
|
+
this.#assertWritable();
|
|
132
|
+
const next = this.#createDocument(this.app, file, this.#persistence, this.#externalWriter);
|
|
133
|
+
this.#retireDocument();
|
|
134
|
+
this.#file = file;
|
|
135
|
+
this.#document = next;
|
|
136
|
+
this.#documentGeneration++;
|
|
137
|
+
}
|
|
138
|
+
get persistence() {
|
|
139
|
+
return this.#persistence;
|
|
140
|
+
}
|
|
141
|
+
set persistence(value) {
|
|
142
|
+
if (value === this.#persistence)
|
|
143
|
+
return;
|
|
144
|
+
this.#assertWritable();
|
|
145
|
+
const next = this.#createDocument(this.app, this.#file, value, this.#externalWriter);
|
|
146
|
+
this.#retireDocument();
|
|
147
|
+
this.#persistence = value;
|
|
148
|
+
this.#document = next;
|
|
149
|
+
this.#documentGeneration++;
|
|
150
|
+
}
|
|
151
|
+
/** Capture an explicit asynchronous owner for external-mode persistence. */
|
|
152
|
+
setExternalPersistenceWriter(writer) {
|
|
153
|
+
if (writer === this.#externalWriter)
|
|
154
|
+
return this;
|
|
155
|
+
this.#assertWritable();
|
|
156
|
+
const next = this.#createDocument(this.app, this.#file, this.#persistence, writer);
|
|
157
|
+
this.#retireDocument();
|
|
158
|
+
this.#externalWriter = writer;
|
|
159
|
+
this.#document = next;
|
|
160
|
+
this.#documentGeneration++;
|
|
161
|
+
return this;
|
|
162
|
+
}
|
|
163
|
+
#assertWritable() {
|
|
164
|
+
if (this.destroyed)
|
|
165
|
+
throw new Error("Editor is destroyed");
|
|
166
|
+
if (this.editingProtected)
|
|
167
|
+
throw new Error("Editor editing is protected");
|
|
168
|
+
}
|
|
169
|
+
#retireDocument() {
|
|
170
|
+
this.#document?.saves.retire();
|
|
171
|
+
}
|
|
172
|
+
#createDocument(application, file, persistence, writer) {
|
|
173
|
+
const document = {
|
|
174
|
+
application,
|
|
175
|
+
file,
|
|
176
|
+
path: file?.path,
|
|
177
|
+
persistence,
|
|
178
|
+
writer,
|
|
179
|
+
};
|
|
180
|
+
document.saves = new EditorSaveContext({
|
|
181
|
+
unownedExternal: persistence === "external" && !writer,
|
|
182
|
+
serialize: (run) => {
|
|
183
|
+
const result = this.#writeTail.then(run);
|
|
184
|
+
this.#writeTail = result.catch(() => undefined);
|
|
185
|
+
return result;
|
|
186
|
+
},
|
|
187
|
+
persist: async (snapshot) => {
|
|
188
|
+
if (file?.path !== document.path)
|
|
189
|
+
throw new Error("Editor persistence target changed");
|
|
190
|
+
if (persistence === "vault" && file)
|
|
191
|
+
await application.vault.modify(file, snapshot.content);
|
|
192
|
+
else if (persistence === "external" && writer)
|
|
193
|
+
await writer({ application, file, content: snapshot.content });
|
|
194
|
+
},
|
|
195
|
+
saved: (snapshot) => {
|
|
196
|
+
this.#completedSequence = Math.max(this.#completedSequence, snapshot.sequence);
|
|
197
|
+
if (!this.destroyed &&
|
|
198
|
+
this.#document === document &&
|
|
199
|
+
this.app === application &&
|
|
200
|
+
this.file === file &&
|
|
201
|
+
snapshot.documentGeneration === this.#documentGeneration) {
|
|
202
|
+
this.data = snapshot.content;
|
|
203
|
+
if (snapshot.content === this.getValue())
|
|
204
|
+
this.onChange(snapshot.content);
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
released: () => {
|
|
208
|
+
document.unregister?.();
|
|
209
|
+
this.#documents.delete(document);
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
if (file) {
|
|
213
|
+
if (!application.fileEditingSessions)
|
|
214
|
+
throw new Error("App has no file editing-session registry");
|
|
215
|
+
document.unregister = application.fileEditingSessions.register(file.path, {
|
|
216
|
+
get dirty() {
|
|
217
|
+
return !!document.loading || document.saves.dirty;
|
|
218
|
+
},
|
|
219
|
+
flush: async () => {
|
|
220
|
+
await document.loading;
|
|
221
|
+
await document.saves.flush();
|
|
222
|
+
},
|
|
223
|
+
protect: () => this.#protectDocument(document),
|
|
224
|
+
});
|
|
108
225
|
}
|
|
109
|
-
|
|
110
|
-
|
|
226
|
+
this.#documents.add(document);
|
|
227
|
+
return document;
|
|
228
|
+
}
|
|
229
|
+
#currentDocument() {
|
|
230
|
+
return (this.#document ??= this.#createDocument(this.app, this.file, this.persistence, this.#externalWriter));
|
|
231
|
+
}
|
|
232
|
+
captureChange(content) {
|
|
233
|
+
return {
|
|
234
|
+
content,
|
|
235
|
+
documentGeneration: this.#documentGeneration,
|
|
236
|
+
sequence: ++this.#sequence,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
get hasUnsavedChanges() {
|
|
240
|
+
return ((!this.destroyed && this.getValue() !== this.data) ||
|
|
241
|
+
[...this.#documents].some((document) => document.saves.dirty));
|
|
242
|
+
}
|
|
243
|
+
get editingProtected() {
|
|
244
|
+
return this.destroyed || this.#editingProtected > 0;
|
|
245
|
+
}
|
|
246
|
+
/** Native CodeMirror protection, retained whenever editor state is rebuilt. */
|
|
247
|
+
protectionExtensions() {
|
|
248
|
+
return this.#protection.of([
|
|
249
|
+
EditorState.readOnly.of(this.editingProtected),
|
|
250
|
+
EditorView.editable.of(!this.editingProtected),
|
|
251
|
+
]);
|
|
252
|
+
}
|
|
253
|
+
#holdEditing() {
|
|
254
|
+
this.#editingProtected++;
|
|
255
|
+
this.#updateProtection();
|
|
256
|
+
let held = true;
|
|
257
|
+
return () => {
|
|
258
|
+
if (!held)
|
|
259
|
+
return;
|
|
260
|
+
held = false;
|
|
261
|
+
this.#editingProtected--;
|
|
262
|
+
this.#updateProtection();
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
#updateProtection() {
|
|
266
|
+
if (!this.destroyed)
|
|
267
|
+
this.view.dispatch({
|
|
268
|
+
effects: this.#protection.reconfigure([
|
|
269
|
+
EditorState.readOnly.of(this.editingProtected),
|
|
270
|
+
EditorView.editable.of(!this.editingProtected),
|
|
271
|
+
]),
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
async #protectDocument(document) {
|
|
275
|
+
if (document.persistence === "external" && !document.writer)
|
|
276
|
+
throw new Error("External editor persistence requires an explicit writer for protection");
|
|
277
|
+
const release = !this.destroyed && (this.#document === document || !!document.loading)
|
|
278
|
+
? this.#holdEditing()
|
|
279
|
+
: () => undefined;
|
|
280
|
+
try {
|
|
281
|
+
await tick();
|
|
282
|
+
await document.loading;
|
|
283
|
+
await document.saves.flush();
|
|
284
|
+
return release;
|
|
111
285
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
286
|
+
catch (error) {
|
|
287
|
+
release();
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/** Protect the current document and drain its captured persistence. */
|
|
292
|
+
protect() {
|
|
293
|
+
if (this.destroyed)
|
|
294
|
+
return Promise.reject(new Error("Editor is destroyed"));
|
|
295
|
+
return this.#protectDocument(this.#currentDocument());
|
|
296
|
+
}
|
|
297
|
+
/** Register and protect a target, then publish its file and contents atomically. */
|
|
298
|
+
async loadDocument(file, read, publish) {
|
|
299
|
+
if (this.destroyed)
|
|
300
|
+
throw new Error("Editor is destroyed");
|
|
301
|
+
if (this.#editingProtected > (this.#loading ? 1 : 0))
|
|
302
|
+
throw new Error("Editor editing is protected");
|
|
303
|
+
const application = this.app, path = file.path;
|
|
304
|
+
// Admission failure leaves any previous load and binding untouched.
|
|
305
|
+
const document = this.#createDocument(application, file, this.persistence, this.#externalWriter);
|
|
306
|
+
const release = this.#holdEditing();
|
|
307
|
+
let cancelRead;
|
|
308
|
+
const cancellation = new Promise((resolve) => {
|
|
309
|
+
cancelRead = resolve;
|
|
310
|
+
});
|
|
311
|
+
let finish;
|
|
312
|
+
document.loading = new Promise((resolve) => {
|
|
313
|
+
finish = resolve;
|
|
314
|
+
});
|
|
315
|
+
let canceled = false, completed = false;
|
|
316
|
+
const load = {
|
|
317
|
+
cancel: () => {
|
|
318
|
+
if (completed || canceled)
|
|
319
|
+
return;
|
|
320
|
+
canceled = true;
|
|
321
|
+
cancelRead({ canceled: true });
|
|
322
|
+
document.loading = undefined;
|
|
323
|
+
document.saves.retire();
|
|
324
|
+
finish();
|
|
325
|
+
release();
|
|
326
|
+
if (this.#loading === load)
|
|
327
|
+
this.#loading = undefined;
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
this.#loading?.cancel();
|
|
331
|
+
this.#loading = load;
|
|
332
|
+
let published = false;
|
|
333
|
+
try {
|
|
334
|
+
const drained = await Promise.race([
|
|
335
|
+
this.flushChanges().then(() => ({ drained: true })),
|
|
336
|
+
cancellation,
|
|
337
|
+
]);
|
|
338
|
+
if ("canceled" in drained || canceled)
|
|
339
|
+
return false;
|
|
340
|
+
const result = await Promise.race([
|
|
341
|
+
Promise.resolve()
|
|
342
|
+
.then(read)
|
|
343
|
+
.then((contents) => ({ contents })),
|
|
344
|
+
cancellation,
|
|
345
|
+
]);
|
|
346
|
+
if ("canceled" in result || canceled)
|
|
347
|
+
return false;
|
|
348
|
+
if (this.destroyed || this.app !== application || file.path !== path)
|
|
349
|
+
throw new Error("Editor document load authority changed");
|
|
350
|
+
const previous = {
|
|
351
|
+
document: this.#document,
|
|
352
|
+
file: this.#file,
|
|
353
|
+
data: this.data,
|
|
354
|
+
state: this.view.state,
|
|
355
|
+
extensions: this.extensions,
|
|
356
|
+
};
|
|
357
|
+
this.#document = document;
|
|
358
|
+
this.#file = file;
|
|
359
|
+
try {
|
|
360
|
+
this.#controlledLoad = true;
|
|
361
|
+
this.setValue(result.contents);
|
|
362
|
+
publish?.(result.contents);
|
|
363
|
+
if (this.destroyed || canceled)
|
|
364
|
+
throw new Error("Editor document load canceled during publication");
|
|
365
|
+
if (this.app !== application || file.path !== path)
|
|
366
|
+
throw new Error("Editor document load authority changed");
|
|
367
|
+
published = true;
|
|
368
|
+
previous.document?.saves.retire();
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
this.#document = previous.document;
|
|
372
|
+
this.#file = previous.file;
|
|
373
|
+
this.data = previous.data;
|
|
374
|
+
this.extensions = previous.extensions;
|
|
375
|
+
this.#documentGeneration++;
|
|
376
|
+
if (!this.destroyed)
|
|
377
|
+
this.view.setState(previous.state);
|
|
378
|
+
throw error;
|
|
379
|
+
}
|
|
380
|
+
finally {
|
|
381
|
+
this.#controlledLoad = false;
|
|
382
|
+
}
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
finally {
|
|
386
|
+
if (!canceled) {
|
|
387
|
+
completed = true;
|
|
388
|
+
document.loading = undefined;
|
|
389
|
+
if (!published)
|
|
390
|
+
document.saves.retire();
|
|
391
|
+
finish();
|
|
392
|
+
release();
|
|
393
|
+
}
|
|
394
|
+
if (this.#loading === load)
|
|
395
|
+
this.#loading = undefined;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
/** Cancel a pending publication while observing any late read result. */
|
|
399
|
+
cancelDocumentLoad() {
|
|
400
|
+
this.#loading?.cancel();
|
|
401
|
+
}
|
|
402
|
+
get refreshError() {
|
|
403
|
+
return this.#refreshError;
|
|
404
|
+
}
|
|
115
405
|
constructor(data = "", extensions = [], application) {
|
|
116
406
|
super();
|
|
117
407
|
this.data = data;
|
|
@@ -121,19 +411,30 @@ export class Editor extends EventDispatcher {
|
|
|
121
411
|
}
|
|
122
412
|
/** Bind this editor to the application that owns its view. */
|
|
123
413
|
bindApplication(application) {
|
|
414
|
+
if (this.application === application)
|
|
415
|
+
return this;
|
|
416
|
+
this.#assertWritable();
|
|
417
|
+
const next = this.#createDocument(application, this.#file, this.#persistence, this.#externalWriter);
|
|
418
|
+
this.#retireDocument();
|
|
124
419
|
this.application = application;
|
|
420
|
+
this.#document = next;
|
|
421
|
+
this.#documentGeneration++;
|
|
125
422
|
return this;
|
|
126
423
|
}
|
|
127
424
|
/** The owning application, with the global alias retained as fallback. */
|
|
128
425
|
get app() {
|
|
129
426
|
return resolveApplication(this.application);
|
|
130
427
|
}
|
|
131
|
-
save =
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
428
|
+
save = Object.assign(() => {
|
|
429
|
+
this.#assertWritable();
|
|
430
|
+
return this.#currentDocument().saves.queue(this.captureChange(this.getValue()), true);
|
|
431
|
+
}, {
|
|
432
|
+
flush: () => this.#document?.saves.flushSave(),
|
|
433
|
+
cancel: () => {
|
|
434
|
+
this.#assertWritable();
|
|
435
|
+
this.#document?.saves.cancelSave();
|
|
436
|
+
},
|
|
437
|
+
});
|
|
137
438
|
get cm() {
|
|
138
439
|
return this.view;
|
|
139
440
|
}
|
|
@@ -142,15 +443,38 @@ export class Editor extends EventDispatcher {
|
|
|
142
443
|
}
|
|
143
444
|
/** Queue the latest editor contents for persistence/change notification. */
|
|
144
445
|
queueChange(data) {
|
|
145
|
-
this
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
446
|
+
this.#assertWritable();
|
|
447
|
+
this.#currentDocument().saves.queue(this.captureChange(data));
|
|
448
|
+
}
|
|
449
|
+
/** Drain every captured document, including failed or retired saves. */
|
|
450
|
+
async flushChanges() {
|
|
451
|
+
const errors = new Map();
|
|
452
|
+
const visited = new Set();
|
|
453
|
+
let sequence, generation;
|
|
454
|
+
do {
|
|
455
|
+
sequence = this.#sequence;
|
|
456
|
+
generation = this.#documentGeneration;
|
|
457
|
+
const documents = [...this.#documents];
|
|
458
|
+
const results = await Promise.allSettled(documents.map((document) => {
|
|
459
|
+
const retry = !visited.has(document);
|
|
460
|
+
visited.add(document);
|
|
461
|
+
return document.saves.flush(retry);
|
|
462
|
+
}));
|
|
463
|
+
results.forEach((result, index) => {
|
|
464
|
+
if (result.status === "rejected")
|
|
465
|
+
errors.set(documents[index], result.reason);
|
|
466
|
+
});
|
|
467
|
+
} while (sequence !== this.#sequence ||
|
|
468
|
+
generation !== this.#documentGeneration);
|
|
469
|
+
if (errors.size === 1)
|
|
470
|
+
throw errors.values().next().value;
|
|
471
|
+
if (errors.size)
|
|
472
|
+
throw new AggregateError([...errors.values()], "Editor persistence failed");
|
|
150
473
|
}
|
|
151
474
|
/** Cancel a queued change when the owning document is intentionally reset. */
|
|
152
475
|
cancelPendingChanges() {
|
|
153
|
-
this
|
|
476
|
+
this.#assertWritable();
|
|
477
|
+
this.#document?.saves.cancelChange();
|
|
154
478
|
}
|
|
155
479
|
getValue() {
|
|
156
480
|
return this.view.state.doc.toString();
|
|
@@ -159,16 +483,25 @@ export class Editor extends EventDispatcher {
|
|
|
159
483
|
this.view.requestMeasure();
|
|
160
484
|
}
|
|
161
485
|
destroy() {
|
|
162
|
-
if (this.destroyed)
|
|
486
|
+
if (this.destroyed)
|
|
163
487
|
return;
|
|
164
|
-
}
|
|
165
488
|
this.destroyed = true;
|
|
166
|
-
this.
|
|
167
|
-
this
|
|
489
|
+
this.cancelDocumentLoad();
|
|
490
|
+
for (const document of this.#documents)
|
|
491
|
+
document.saves.retire();
|
|
168
492
|
this.view.destroy();
|
|
169
493
|
}
|
|
494
|
+
/** Stop editing immediately and await all retained document persistence. */
|
|
495
|
+
async dispose() {
|
|
496
|
+
this.destroy();
|
|
497
|
+
await this.flushChanges();
|
|
498
|
+
}
|
|
170
499
|
trackChanges(callback) {
|
|
171
|
-
const
|
|
500
|
+
const owner = this.app;
|
|
501
|
+
let active = true;
|
|
502
|
+
const editorUpdated = owner.workspace.on("editor-updated", (editor, transactions) => {
|
|
503
|
+
if (!active || this.destroyed || this.app !== owner)
|
|
504
|
+
return;
|
|
172
505
|
if (editor.id !== this.id && editor.file?.path === this.file?.path) {
|
|
173
506
|
if (this.getValue() !== editor.getValue()) {
|
|
174
507
|
this.view.dispatch(...transactions);
|
|
@@ -177,22 +510,60 @@ export class Editor extends EventDispatcher {
|
|
|
177
510
|
}
|
|
178
511
|
}
|
|
179
512
|
});
|
|
180
|
-
const fileChanged =
|
|
513
|
+
const fileChanged = owner.workspace.on("file-change", (file, event) => {
|
|
514
|
+
if (!active || this.destroyed || this.app !== owner)
|
|
515
|
+
return;
|
|
181
516
|
if (file.path === this.file?.path) {
|
|
182
517
|
if (event !== "delete" && file) {
|
|
183
|
-
this.app.
|
|
518
|
+
const application = this.app, target = this.file, path = target?.path;
|
|
519
|
+
const readGeneration = ++this.#fileReadGeneration, documentGeneration = this.#documentGeneration;
|
|
520
|
+
const sequence = this.#sequence, completed = this.#completedSequence;
|
|
521
|
+
void application.vault
|
|
522
|
+
.read(file)
|
|
523
|
+
.then((contents) => {
|
|
524
|
+
if (!active ||
|
|
525
|
+
this.destroyed ||
|
|
526
|
+
this.app !== application ||
|
|
527
|
+
this.file !== target ||
|
|
528
|
+
this.file?.path !== path ||
|
|
529
|
+
this.#fileReadGeneration !== readGeneration ||
|
|
530
|
+
this.#documentGeneration !== documentGeneration ||
|
|
531
|
+
this.#sequence !== sequence ||
|
|
532
|
+
this.#completedSequence !== completed)
|
|
533
|
+
return;
|
|
534
|
+
this.#refreshError = undefined;
|
|
535
|
+
if (this.getValue() === contents) {
|
|
536
|
+
this.data = contents;
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
if (this.getValue() !== this.data ||
|
|
540
|
+
this.#document?.saves.dirty)
|
|
541
|
+
return;
|
|
184
542
|
if (this.getValue() !== contents) {
|
|
185
|
-
this
|
|
543
|
+
this.#replaceContent(contents);
|
|
186
544
|
this.onChange(this.getValue());
|
|
187
545
|
callback?.(this.getValue(), this);
|
|
188
546
|
}
|
|
547
|
+
})
|
|
548
|
+
.catch((error) => {
|
|
549
|
+
if (active &&
|
|
550
|
+
!this.destroyed &&
|
|
551
|
+
this.app === application &&
|
|
552
|
+
this.file?.path === path &&
|
|
553
|
+
this.#fileReadGeneration === readGeneration &&
|
|
554
|
+
this.file === target &&
|
|
555
|
+
this.#documentGeneration === documentGeneration &&
|
|
556
|
+
this.#sequence === sequence &&
|
|
557
|
+
this.#completedSequence === completed)
|
|
558
|
+
this.#refreshError = error;
|
|
189
559
|
});
|
|
190
560
|
}
|
|
191
561
|
}
|
|
192
562
|
});
|
|
193
563
|
return () => {
|
|
194
|
-
|
|
195
|
-
|
|
564
|
+
active = false;
|
|
565
|
+
owner.workspace.offref(editorUpdated);
|
|
566
|
+
owner.workspace.offref(fileChanged);
|
|
196
567
|
};
|
|
197
568
|
}
|
|
198
569
|
updateExtensions(extensions, context) {
|
|
@@ -207,7 +578,6 @@ export class Editor extends EventDispatcher {
|
|
|
207
578
|
const content = this.getValue();
|
|
208
579
|
const selection = this.view.state.selection;
|
|
209
580
|
this.extensions = editorExtensions;
|
|
210
|
-
this.data = content;
|
|
211
581
|
this.view.setState(createEditorState(this, content, this.extensions, selection));
|
|
212
582
|
return this;
|
|
213
583
|
}, {
|
|
@@ -216,6 +586,12 @@ export class Editor extends EventDispatcher {
|
|
|
216
586
|
});
|
|
217
587
|
}
|
|
218
588
|
setValue(content) {
|
|
589
|
+
if (!this.#controlledLoad)
|
|
590
|
+
this.#assertWritable();
|
|
591
|
+
else if (this.destroyed)
|
|
592
|
+
throw new Error("Editor is destroyed");
|
|
593
|
+
this.#documentGeneration++;
|
|
594
|
+
this.#refreshError = undefined;
|
|
219
595
|
this.app.telemetry.measure("editor.set_value", (span) => {
|
|
220
596
|
span.setAttribute("editor.content_length", content.length);
|
|
221
597
|
span.setAttribute("file.extension", this.file?.extension ?? "");
|
|
@@ -227,6 +603,12 @@ export class Editor extends EventDispatcher {
|
|
|
227
603
|
});
|
|
228
604
|
}
|
|
229
605
|
replaceContent(content, options = {}) {
|
|
606
|
+
this.#assertWritable();
|
|
607
|
+
this.#replaceContent(content, options);
|
|
608
|
+
}
|
|
609
|
+
#replaceContent(content, options = {}) {
|
|
610
|
+
this.#documentGeneration++;
|
|
611
|
+
this.#refreshError = undefined;
|
|
230
612
|
this.data = content;
|
|
231
613
|
const annotations = [syncAnnotation.of(true)];
|
|
232
614
|
this.view.dispatch({
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Independent file writers implement this contract without importing a renderer. */
|
|
2
|
+
export interface FileEditingSession {
|
|
3
|
+
/** Includes queued, in-flight and failed persistence. */
|
|
4
|
+
readonly dirty: boolean;
|
|
5
|
+
flush(): Promise<void>;
|
|
6
|
+
/** Block edits before yielding; drain pending saves and return an idempotent release. */
|
|
7
|
+
protect(): Promise<() => void | Promise<void>>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* An App-owned persistence and protection boundary for independent file writers.
|
|
11
|
+
* Hosts must register every writer before selecting this as an apply guard.
|
|
12
|
+
*/
|
|
13
|
+
export declare class FileEditingSessionRegistry {
|
|
14
|
+
#private;
|
|
15
|
+
register(path: string, session: FileEditingSession): () => void;
|
|
16
|
+
/** Flush the captured sessions; protection is required for mutation admission. */
|
|
17
|
+
flush(path?: string): Promise<{
|
|
18
|
+
flushed: boolean;
|
|
19
|
+
path?: string;
|
|
20
|
+
}>;
|
|
21
|
+
/**
|
|
22
|
+
* Serialize mutations, holding matching sessions protected throughout the callback.
|
|
23
|
+
* Callbacks and session methods must not re-enter queued registry operations.
|
|
24
|
+
*/
|
|
25
|
+
guard<T>(path: string | undefined, operation: () => Promise<T>): Promise<T>;
|
|
26
|
+
/** Stop admission immediately, drain callbacks and retry failed persistence/cleanup. */
|
|
27
|
+
dispose(): Promise<void>;
|
|
28
|
+
}
|