@lapis-notes/api 0.1.4 → 0.2.1

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 (188) hide show
  1. package/CHANGELOG.md +206 -0
  2. package/README.md +21 -2
  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 +21 -9
  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 +21 -3
  31. package/dist/context.svelte.js +63 -20
  32. package/dist/diagnostics.d.ts +8 -0
  33. package/dist/diagnostics.js +6 -0
  34. package/dist/editor-save-context.d.ts +12 -1
  35. package/dist/editor-save-context.js +32 -1
  36. package/dist/editor-view-registry.d.ts +10 -0
  37. package/dist/editor-view-registry.js +44 -4
  38. package/dist/editor.svelte.js +128 -37
  39. package/dist/embed-registry.d.ts +10 -0
  40. package/dist/embed-registry.js +15 -0
  41. package/dist/file-editing-failures.d.ts +2 -0
  42. package/dist/file-editing-failures.js +29 -0
  43. package/dist/file-editing-native-issues.d.ts +19 -0
  44. package/dist/file-editing-native-issues.js +65 -0
  45. package/dist/file-editing-sessions.d.ts +56 -8
  46. package/dist/file-editing-sessions.js +185 -15
  47. package/dist/file-manager.d.ts +8 -1
  48. package/dist/file-manager.js +37 -3
  49. package/dist/file-state-store.d.ts +5 -0
  50. package/dist/file-state-store.js +34 -0
  51. package/dist/file-text-conflicts.d.ts +25 -0
  52. package/dist/file-text-conflicts.js +35 -0
  53. package/dist/file-text-mutation.d.ts +13 -0
  54. package/dist/file-text-mutation.js +100 -0
  55. package/dist/file-text-persistence.d.ts +36 -0
  56. package/dist/file-text-persistence.js +234 -0
  57. package/dist/file-text-state-writer.d.ts +34 -0
  58. package/dist/file-text-state-writer.js +183 -0
  59. package/dist/files-configuration.d.ts +20 -0
  60. package/dist/files-configuration.js +47 -0
  61. package/dist/generated/plugin-host-modules.schema.generated.d.ts +87 -37
  62. package/dist/generated/plugin-host-modules.schema.generated.js +102 -37
  63. package/dist/glob.d.ts +3 -0
  64. package/dist/glob.js +2 -1
  65. package/dist/icons/changes.d.ts +3 -0
  66. package/dist/icons/changes.js +16 -0
  67. package/dist/icons/index.d.ts +5 -1
  68. package/dist/icons/index.js +44 -3
  69. package/dist/icons/registry.d.ts +35 -0
  70. package/dist/icons/registry.js +154 -0
  71. package/dist/icons/safe-svg.d.ts +2 -0
  72. package/dist/icons/safe-svg.js +44 -0
  73. package/dist/index.d.ts +5 -1
  74. package/dist/index.js +3 -1
  75. package/dist/lapis-extension.d.ts +6 -2
  76. package/dist/lapis-extension.js +2 -0
  77. package/dist/metadata.svelte.js +45 -2
  78. package/dist/native-background-indexing.d.ts +48 -0
  79. package/dist/native-background-indexing.js +208 -0
  80. package/dist/native-database-protocol.d.ts +29 -0
  81. package/dist/native-database-protocol.js +1 -0
  82. package/dist/native-editing-intent-lease.d.ts +17 -0
  83. package/dist/native-editing-intent-lease.js +188 -0
  84. package/dist/native-editing-intents-protocol.d.ts +70 -0
  85. package/dist/native-editing-intents-protocol.js +1 -0
  86. package/dist/native-editing-intents.d.ts +8 -0
  87. package/dist/native-editing-intents.js +1 -0
  88. package/dist/native-editing-problems.d.ts +3 -0
  89. package/dist/native-editing-problems.js +180 -0
  90. package/dist/native-editing-recovery.d.ts +24 -0
  91. package/dist/native-editing-recovery.js +198 -0
  92. package/dist/native-editing-session.d.ts +51 -0
  93. package/dist/native-editing-session.js +251 -0
  94. package/dist/native-history-index-protocol.d.ts +80 -0
  95. package/dist/native-history-index-protocol.js +1 -0
  96. package/dist/native-history-index.d.ts +6 -0
  97. package/dist/native-history-index.js +1 -0
  98. package/dist/native-indexing-protocol.d.ts +63 -0
  99. package/dist/native-indexing-protocol.js +1 -0
  100. package/dist/native-participant-protocol.d.ts +67 -0
  101. package/dist/native-participant-protocol.js +3 -0
  102. package/dist/native-queue-protocol.d.ts +73 -0
  103. package/dist/native-queue-protocol.js +3 -0
  104. package/dist/native-tasks-protocol.d.ts +264 -0
  105. package/dist/native-tasks-protocol.js +1 -0
  106. package/dist/native-tasks.d.ts +35 -0
  107. package/dist/native-tasks.js +79 -0
  108. package/dist/native-turn-protocol.d.ts +81 -0
  109. package/dist/native-turn-protocol.js +1 -0
  110. package/dist/native-turns.d.ts +29 -0
  111. package/dist/native-turns.js +2 -0
  112. package/dist/plugin-asset-server.js +3 -2
  113. package/dist/plugin-dependency-resolver.d.ts +1 -1
  114. package/dist/plugin-distribution/compatibility.d.ts +1 -1
  115. package/dist/plugin-distribution/compatibility.js +10 -5
  116. package/dist/plugin-distribution/installed-plugin-state.js +2 -1
  117. package/dist/plugin-distribution/installer.d.ts +6 -0
  118. package/dist/plugin-distribution/installer.js +90 -37
  119. package/dist/plugin-distribution/manager.d.ts +4 -1
  120. package/dist/plugin-distribution/manager.js +12 -5
  121. package/dist/plugin-distribution/registry-cache.js +2 -1
  122. package/dist/plugin-distribution/registry-source-state.js +2 -1
  123. package/dist/plugin-distribution/schemas.d.ts +26 -8
  124. package/dist/plugin-distribution/schemas.js +2 -1
  125. package/dist/plugin-distribution/types.d.ts +4 -0
  126. package/dist/plugin-manager.d.ts +20 -4
  127. package/dist/plugin-manager.js +228 -64
  128. package/dist/plugin-runtime-entry.d.ts +1 -1
  129. package/dist/plugin-runtime-entry.js +16 -0
  130. package/dist/plugin.d.ts +18 -2
  131. package/dist/plugin.js +93 -39
  132. package/dist/registry-schema/catalog-v1.d.ts +60 -9
  133. package/dist/registry-schema/catalog-v1.js +2 -1
  134. package/dist/registry-schema/catalog-v2.d.ts +60 -9
  135. package/dist/registry-schema/catalog-v2.js +1 -1
  136. package/dist/search-result-provider.d.ts +95 -0
  137. package/dist/search-result-provider.js +42 -0
  138. package/dist/status-bar.svelte.d.ts +26 -20
  139. package/dist/status-bar.svelte.js +1 -0
  140. package/dist/storage/app-database.d.ts +6 -2
  141. package/dist/storage/app-database.js +41 -34
  142. package/dist/storage/desktop-native.d.ts +5 -3
  143. package/dist/storage/desktop-native.js +59 -6
  144. package/dist/storage/directory-watcher.d.ts +27 -0
  145. package/dist/storage/directory-watcher.js +201 -0
  146. package/dist/storage/fs.d.ts +14 -27
  147. package/dist/storage/fs.js +2 -168
  148. package/dist/storage/index-projection.js +33 -15
  149. package/dist/storage/memory-vault-adapter.d.ts +2 -0
  150. package/dist/storage/memory-vault-adapter.js +14 -1
  151. package/dist/storage/path.d.ts +6 -2
  152. package/dist/storage/path.js +8 -2
  153. package/dist/storage/projection-query-candidates.d.ts +3 -0
  154. package/dist/storage/projection-query-candidates.js +69 -0
  155. package/dist/storage/remote-app-database.d.ts +2 -2
  156. package/dist/storage/remote-app-database.js +1 -1
  157. package/dist/storage/runtime.d.ts +3 -0
  158. package/dist/storage/runtime.js +3 -0
  159. package/dist/storage/search-embedding-record.d.ts +3 -0
  160. package/dist/storage/search-embedding-record.js +18 -0
  161. package/dist/storage/turso-app-database.d.ts +2 -0
  162. package/dist/storage/turso-app-database.js +67 -30
  163. package/dist/storage/vault-listing.d.ts +3 -0
  164. package/dist/storage/vault-listing.js +36 -0
  165. package/dist/storage/vault-reconciliation.d.ts +19 -0
  166. package/dist/storage/vault-reconciliation.js +137 -0
  167. package/dist/storage/vault-session.d.ts +8 -0
  168. package/dist/storage/vault-session.js +10 -2
  169. package/dist/storage/vault.svelte.d.ts +53 -4
  170. package/dist/storage/vault.svelte.js +223 -87
  171. package/dist/sync-host.d.ts +12 -3
  172. package/dist/sync-host.js +5 -1
  173. package/dist/vault-change-views.d.ts +4 -0
  174. package/dist/vault-change-views.js +43 -0
  175. package/dist/vault-changes.d.ts +37 -0
  176. package/dist/vault-changes.js +228 -0
  177. package/dist/view.svelte.d.ts +182 -149
  178. package/dist/view.svelte.js +11 -2
  179. package/dist/workspace-close-guard.d.ts +3 -0
  180. package/dist/workspace-close-guard.js +35 -0
  181. package/dist/workspace-host-internal.js +10 -2
  182. package/dist/workspace-layout-normalizer.d.ts +4 -2
  183. package/dist/workspace-layout-normalizer.js +5 -5
  184. package/dist/workspace-view-document.d.ts +3 -0
  185. package/dist/workspace-view-document.js +12 -0
  186. package/dist/workspace.svelte.d.ts +13 -2
  187. package/dist/workspace.svelte.js +371 -82
  188. package/package.json +45 -8
@@ -0,0 +1,100 @@
1
+ import { FileTextPersistence } from "./file-text-persistence";
2
+ /** A retained one-shot draft; it has no view, timer or independent save path. */
3
+ class TextMutation {
4
+ app;
5
+ file;
6
+ transform;
7
+ options;
8
+ #dirty = true;
9
+ #path;
10
+ #draft = "";
11
+ #persistence;
12
+ constructor(app, file, transform, options) {
13
+ this.app = app;
14
+ this.file = file;
15
+ this.transform = transform;
16
+ this.options = options;
17
+ this.#path = file.path;
18
+ }
19
+ get dirty() {
20
+ return this.#dirty || !!this.#persistence?.hasConflict;
21
+ }
22
+ native = {
23
+ hold: () => () => { },
24
+ savedText: () => this.#persistence?.baseline,
25
+ };
26
+ async flush() {
27
+ if (!this.dirty)
28
+ return;
29
+ if (this.file.path !== this.#path)
30
+ throw new Error("File mutation target changed");
31
+ if (!this.#persistence) {
32
+ const expected = this.options.expectedText;
33
+ // Keep the admitted transform if initial IO fails before a draft exists.
34
+ const baseline = expected === undefined
35
+ ? await this.app.vault.read(this.file)
36
+ : expected;
37
+ try {
38
+ this.#draft = await this.transform(baseline ?? "");
39
+ if (this.file.path !== this.#path)
40
+ throw new Error("File mutation target changed");
41
+ if (this.#draft === baseline) {
42
+ this.#dirty = false;
43
+ return;
44
+ }
45
+ this.#persistence = new FileTextPersistence({
46
+ vault: {
47
+ read: (file) => this.app.vault.read(file),
48
+ modifyIfUnchanged: (file, value, expected) => this.app.vault.modifyIfUnchanged(file, value, expected, this.options),
49
+ },
50
+ file: this.file,
51
+ baseline,
52
+ conflicts: this.app.fileEditingSessions.textConflicts,
53
+ local: () => this.#draft,
54
+ protect: () => ({
55
+ release() { },
56
+ saved: () => this.#saved(),
57
+ reset: (text) => this.options.onReset?.(text, this.#persistence.baseline),
58
+ resetSaved: (text) => {
59
+ this.#draft = text;
60
+ this.#dirty = false;
61
+ },
62
+ }),
63
+ serialize: (run) => this.app.fileEditingSessions.recover(this, run),
64
+ });
65
+ }
66
+ catch (error) {
67
+ // Rejected transforms are invalid actions, rather than failed save drafts.
68
+ this.#dirty = false;
69
+ throw error;
70
+ }
71
+ }
72
+ if (this.options.validateBaseline?.() === false)
73
+ await this.#persistence.rejectPersistence();
74
+ try {
75
+ await this.#persistence.persist(this.#draft);
76
+ }
77
+ catch (error) {
78
+ if (!this.#persistence.hasConflict)
79
+ await this.#persistence.rejectPersistence(error);
80
+ throw error;
81
+ }
82
+ this.#saved();
83
+ }
84
+ #saved() {
85
+ this.#dirty = false;
86
+ this.options.onSaved?.(this.#draft);
87
+ }
88
+ async protect() {
89
+ // No user edits enter this captured one-shot draft after admission.
90
+ return () => { };
91
+ }
92
+ }
93
+ export function mutateFileText(app, file, transform, options = {}) {
94
+ const path = file.path;
95
+ return app.fileEditingSessions.mutate(path, () => {
96
+ if (file.path !== path)
97
+ throw new Error("File mutation target changed");
98
+ return new TextMutation(app, file, transform, options);
99
+ });
100
+ }
@@ -0,0 +1,36 @@
1
+ import type { TFile } from "./storage/fs";
2
+ import type { Vault } from "./storage/vault.svelte";
3
+ import type { FileTextConflictRegistry } from "./file-text-conflicts";
4
+ /** A single recovery operation owns its protected document acknowledgement. */
5
+ export type FileTextRecoveryLease = {
6
+ release(): void;
7
+ saved(): void;
8
+ reset(contents: string): void | Promise<void>;
9
+ resetSaved?(contents: string): void;
10
+ };
11
+ export type FileTextPersistenceOptions = {
12
+ vault: Pick<Vault, "read" | "modifyIfUnchanged">;
13
+ file: TFile;
14
+ baseline: string | null;
15
+ conflicts: FileTextConflictRegistry;
16
+ local(): string;
17
+ /** Acquire editing protection synchronously, before asynchronous admission. */
18
+ protect(): FileTextRecoveryLease | Promise<FileTextRecoveryLease>;
19
+ settle?(): Promise<void>;
20
+ serialize(run: () => Promise<void>): Promise<void>;
21
+ /** State restoration may itself write other owned files; revalidate afterward. */
22
+ recoverOutsideQueue?: boolean;
23
+ };
24
+ /** A single captured document's baseline and explicit text-conflict recovery. */
25
+ export declare class FileTextPersistence {
26
+ #private;
27
+ private readonly options;
28
+ constructor(options: FileTextPersistenceOptions);
29
+ get hasConflict(): boolean;
30
+ get baseline(): string | null;
31
+ acceptBaseline(contents: string | null): void;
32
+ updateLocal(contents: string): void;
33
+ /** Retain a computed draft and authoritative disk snapshot before rejecting. */
34
+ rejectPersistence(error?: unknown): Promise<never>;
35
+ persist(contents: string): Promise<void>;
36
+ }
@@ -0,0 +1,234 @@
1
+ function hasCode(error, code) {
2
+ return (!!error &&
3
+ typeof error === "object" &&
4
+ "code" in error &&
5
+ error.code === code);
6
+ }
7
+ /** A single captured document's baseline and explicit text-conflict recovery. */
8
+ export class FileTextPersistence {
9
+ options;
10
+ #id = crypto.randomUUID();
11
+ #path;
12
+ #error;
13
+ #baseline;
14
+ #conflict;
15
+ constructor(options) {
16
+ this.options = options;
17
+ this.#path = options.file.path;
18
+ this.#baseline = options.baseline;
19
+ this.#error = Object.assign(new Error(`File changed on disk: ${this.#path}`), { code: "ECONFLICT" });
20
+ }
21
+ get hasConflict() {
22
+ return !!this.#conflict;
23
+ }
24
+ get baseline() {
25
+ return this.#baseline;
26
+ }
27
+ acceptBaseline(contents) {
28
+ if (!this.#conflict)
29
+ this.#baseline = contents;
30
+ }
31
+ updateLocal(contents) {
32
+ if (this.#conflict && this.#conflict.local !== contents)
33
+ this.#publish(this.#conflict.external, this.#conflict.readError);
34
+ }
35
+ /** Retain a computed draft and authoritative disk snapshot before rejecting. */
36
+ async rejectPersistence(error = this.#error) {
37
+ this.#assertTarget();
38
+ await this.#captureExternal();
39
+ throw error;
40
+ }
41
+ async persist(contents) {
42
+ this.#assertTarget();
43
+ if (this.#conflict) {
44
+ this.updateLocal(this.options.local());
45
+ throw this.#error;
46
+ }
47
+ try {
48
+ await this.options.vault.modifyIfUnchanged(this.options.file, contents, this.#baseline);
49
+ this.#baseline = contents;
50
+ }
51
+ catch (error) {
52
+ if (hasCode(error, "ECONFLICT")) {
53
+ await this.#captureExternal();
54
+ throw this.#error;
55
+ }
56
+ throw error;
57
+ }
58
+ }
59
+ #assertTarget() {
60
+ if (this.options.file.path !== this.#path)
61
+ throw new Error("File text persistence target changed");
62
+ }
63
+ #assertCurrent(conflict) {
64
+ this.#assertTarget();
65
+ if (this.#conflict !== conflict)
66
+ throw new Error("Conflict changed; review the latest versions");
67
+ }
68
+ async #read() {
69
+ this.#assertTarget();
70
+ try {
71
+ return await this.options.vault.read(this.options.file);
72
+ }
73
+ catch (error) {
74
+ if (hasCode(error, "ENOENT"))
75
+ return null;
76
+ throw error;
77
+ }
78
+ }
79
+ async #captureExternal() {
80
+ try {
81
+ this.#publish(await this.#read());
82
+ }
83
+ catch (error) {
84
+ this.#publish(undefined, error instanceof Error ? error.message : String(error));
85
+ }
86
+ }
87
+ #publish(external, readError) {
88
+ const conflict = {
89
+ id: this.#id,
90
+ path: this.#path,
91
+ baseline: this.#baseline,
92
+ local: this.options.local(),
93
+ external,
94
+ ...(readError === undefined ? {} : { readError }),
95
+ resolve: (choice) => this.#recover(conflict, choice),
96
+ resolveMerged: (contents) => typeof contents === "string"
97
+ ? this.#recover(conflict, "keep-local", contents)
98
+ : Promise.reject(new Error("Merged file contents must be text")),
99
+ refresh: () => this.#recover(conflict),
100
+ };
101
+ this.#conflict = conflict;
102
+ this.options.conflicts.publish(conflict);
103
+ }
104
+ async #recover(conflict, choice, merged) {
105
+ this.#assertCurrent(conflict);
106
+ if (choice !== undefined &&
107
+ choice !== "keep-local" &&
108
+ choice !== "use-external")
109
+ throw new Error("Invalid text conflict resolution");
110
+ if (merged !== undefined && typeof merged !== "string")
111
+ throw new Error("Merged file contents must be text");
112
+ const lease = await this.options.protect();
113
+ try {
114
+ await this.options.settle?.();
115
+ if (this.options.recoverOutsideQueue && choice !== undefined) {
116
+ await this.#recoverState(conflict, choice, lease, merged);
117
+ return;
118
+ }
119
+ await this.options.serialize(async () => {
120
+ this.#assertCurrent(conflict);
121
+ if (choice === undefined) {
122
+ await this.#captureExternal();
123
+ return;
124
+ }
125
+ if (conflict.external === undefined)
126
+ throw new Error("Read the disk version before resolving this conflict");
127
+ if (choice === "keep-local") {
128
+ const contents = merged ?? this.options.local();
129
+ try {
130
+ await this.options.vault.modifyIfUnchanged(this.options.file, contents, conflict.external);
131
+ }
132
+ catch (error) {
133
+ if (hasCode(error, "ECONFLICT")) {
134
+ await this.#captureExternal();
135
+ throw this.#error;
136
+ }
137
+ throw error;
138
+ }
139
+ const previousBaseline = this.#baseline;
140
+ this.#baseline = contents;
141
+ if (merged !== undefined) {
142
+ try {
143
+ await lease.reset(contents);
144
+ }
145
+ catch (error) {
146
+ this.#baseline = previousBaseline;
147
+ throw error;
148
+ }
149
+ lease.resetSaved?.(contents);
150
+ }
151
+ else
152
+ lease.saved();
153
+ this.#clear();
154
+ }
155
+ else {
156
+ let external;
157
+ try {
158
+ external = await this.#read();
159
+ }
160
+ catch (error) {
161
+ await this.#captureExternal();
162
+ throw error;
163
+ }
164
+ if (external !== conflict.external) {
165
+ this.#publish(external);
166
+ throw this.#error;
167
+ }
168
+ const previousBaseline = this.#baseline;
169
+ this.#baseline = external;
170
+ try {
171
+ await lease.reset(external ?? "");
172
+ }
173
+ catch (error) {
174
+ this.#baseline = previousBaseline;
175
+ throw error;
176
+ }
177
+ this.#clear();
178
+ lease.resetSaved?.(external ?? "");
179
+ }
180
+ });
181
+ }
182
+ finally {
183
+ lease.release();
184
+ }
185
+ }
186
+ async #recoverState(conflict, choice, lease, merged) {
187
+ let selected = "";
188
+ await this.options.serialize(async () => {
189
+ this.#assertCurrent(conflict);
190
+ if (conflict.external === undefined)
191
+ throw new Error("Read the disk version before resolving this conflict");
192
+ const actual = await this.#read();
193
+ if (actual !== conflict.external) {
194
+ this.#publish(actual);
195
+ throw this.#error;
196
+ }
197
+ selected =
198
+ merged ??
199
+ (choice === "keep-local" ? this.options.local() : (actual ?? ""));
200
+ });
201
+ // The owner lease remains held, but other owned files can use the queue.
202
+ await lease.reset(selected);
203
+ await this.options.serialize(async () => {
204
+ this.#assertCurrent(conflict);
205
+ if (choice === "keep-local") {
206
+ try {
207
+ await this.options.vault.modifyIfUnchanged(this.options.file, selected, conflict.external);
208
+ }
209
+ catch (error) {
210
+ if (hasCode(error, "ECONFLICT"))
211
+ await this.#captureExternal();
212
+ throw error;
213
+ }
214
+ }
215
+ else {
216
+ const actual = await this.#read();
217
+ if (actual !== conflict.external) {
218
+ this.#publish(actual);
219
+ throw this.#error;
220
+ }
221
+ }
222
+ this.#baseline = choice === "keep-local" ? selected : conflict.external;
223
+ this.#clear();
224
+ if (choice === "keep-local" && merged === undefined)
225
+ lease.saved();
226
+ else
227
+ lease.resetSaved?.(selected);
228
+ });
229
+ }
230
+ #clear() {
231
+ this.#conflict = undefined;
232
+ this.options.conflicts.clear(this.#id);
233
+ }
234
+ }
@@ -0,0 +1,34 @@
1
+ import type { App } from "./context.svelte";
2
+ import type { FileEditingSession } from "./file-editing-sessions";
3
+ import type { TFile } from "./storage/fs";
4
+ /**
5
+ * App-owned mutable text state using the same persistence contract as
6
+ * documents.
7
+ */
8
+ export declare class FileTextStateWriter implements FileEditingSession {
9
+ #private;
10
+ private readonly app;
11
+ private readonly file;
12
+ private readonly reset;
13
+ private readonly options;
14
+ constructor(app: App, file: TFile, baseline: string | null | undefined, reset: (text: string) => void | Promise<void>, options?: {
15
+ recoverOutsideQueue?: boolean;
16
+ });
17
+ get dirty(): boolean;
18
+ readonly native: {
19
+ lifetime: "operation";
20
+ hold: () => () => void;
21
+ savedText: () => string | null | undefined;
22
+ };
23
+ acceptBaseline(contents: string | null): boolean;
24
+ assertEditable(): void;
25
+ set(contents: string): Promise<void>;
26
+ /** Calculate a logical change only after every earlier change is acknowledged. */
27
+ update(calculate: () => {
28
+ contents: string;
29
+ onSaved(): void;
30
+ } | undefined): Promise<void>;
31
+ flush(): Promise<void>;
32
+ protect(): Promise<() => void>;
33
+ dispose(): void;
34
+ }
@@ -0,0 +1,183 @@
1
+ import { FileTextPersistence } from "./file-text-persistence";
2
+ import { dirname } from "./storage/path";
3
+ /**
4
+ * App-owned mutable text state using the same persistence contract as
5
+ * documents.
6
+ */
7
+ export class FileTextStateWriter {
8
+ app;
9
+ file;
10
+ reset;
11
+ options;
12
+ #path;
13
+ #persistence;
14
+ #unregister;
15
+ #contents;
16
+ #dirty = false;
17
+ #locks = 0;
18
+ #disposed = false;
19
+ #onSaved;
20
+ constructor(app, file, baseline, reset, options = {}) {
21
+ this.app = app;
22
+ this.file = file;
23
+ this.reset = reset;
24
+ this.options = options;
25
+ this.#path = file.path;
26
+ this.#contents = baseline ?? "";
27
+ if (baseline !== undefined)
28
+ this.#persistence = this.#createPersistence(baseline);
29
+ this.#unregister = app.fileEditingSessions.register(file.path, this);
30
+ }
31
+ #createPersistence(baseline) {
32
+ return new FileTextPersistence({
33
+ vault: this.app.vault,
34
+ file: this.file,
35
+ baseline,
36
+ conflicts: this.app.fileEditingSessions.textConflicts,
37
+ local: () => this.#contents,
38
+ protect: () => {
39
+ const release = this.#protect();
40
+ return {
41
+ release,
42
+ saved: () => {
43
+ this.#dirty = false;
44
+ const notify = this.#onSaved;
45
+ this.#onSaved = undefined;
46
+ notify?.();
47
+ },
48
+ reset: (text) => this.reset(text),
49
+ resetSaved: (text) => {
50
+ this.#contents = text;
51
+ this.#dirty = false;
52
+ this.#onSaved = undefined;
53
+ },
54
+ };
55
+ },
56
+ serialize: (run) => this.app.fileEditingSessions.recover(this, async () => {
57
+ const result = await run();
58
+ await this.#unregister.idle();
59
+ return result;
60
+ }),
61
+ recoverOutsideQueue: this.options.recoverOutsideQueue,
62
+ });
63
+ }
64
+ get dirty() {
65
+ return this.#dirty || !!this.#persistence?.hasConflict;
66
+ }
67
+ native = {
68
+ lifetime: "operation",
69
+ hold: () => this.#protect(),
70
+ savedText: () => this.#persistence?.baseline,
71
+ };
72
+ acceptBaseline(contents) {
73
+ if (this.dirty || this.#locks || this.#disposed)
74
+ return false;
75
+ if (this.#persistence)
76
+ this.#persistence.acceptBaseline(contents);
77
+ else
78
+ this.#persistence = this.#createPersistence(contents);
79
+ return true;
80
+ }
81
+ assertEditable() {
82
+ if (this.#disposed || this.app.fileEditingSessions.closed)
83
+ throw new Error("File state writer is closed");
84
+ if (this.#locks)
85
+ throw new Error(`File editing is protected: ${this.#path}`);
86
+ if (this.file.path !== this.#path)
87
+ throw new Error("File state target changed");
88
+ }
89
+ set(contents) {
90
+ return this.#unregister.native
91
+ ? this.app.fileEditingSessions.track(() => this.app.fileEditingSessions.recover(this, async () => {
92
+ await this.#unregister.ready();
93
+ this.#captureContents(contents);
94
+ await this.flush();
95
+ }))
96
+ : this.#setContents(contents);
97
+ }
98
+ #captureContents(contents) {
99
+ this.assertEditable();
100
+ this.#contents = contents;
101
+ this.#dirty = true;
102
+ this.#unregister.changed();
103
+ this.#persistence?.updateLocal(contents);
104
+ }
105
+ #setContents(contents) {
106
+ this.#captureContents(contents);
107
+ return this.app.fileEditingSessions.recover(this, () => this.flush());
108
+ }
109
+ /** Calculate a logical change only after every earlier change is acknowledged. */
110
+ update(calculate) {
111
+ if (!this.#unregister.native)
112
+ this.assertEditable();
113
+ return this.app.fileEditingSessions.recover(this, async () => {
114
+ await this.flush();
115
+ if (this.#unregister.native) {
116
+ await this.#unregister.ready();
117
+ this.assertEditable();
118
+ }
119
+ const change = calculate();
120
+ if (!change) {
121
+ await this.#unregister.idle();
122
+ return;
123
+ }
124
+ this.#contents = change.contents;
125
+ this.#dirty = true;
126
+ this.#unregister.changed();
127
+ this.#onSaved = change.onSaved;
128
+ await this.flush();
129
+ });
130
+ }
131
+ async flush() {
132
+ if (!this.#dirty) {
133
+ await this.#unregister.idle();
134
+ return;
135
+ }
136
+ if (this.#unregister.native)
137
+ await this.#unregister.ready();
138
+ const contents = this.#contents;
139
+ const notify = this.#onSaved;
140
+ try {
141
+ if (!this.#persistence) {
142
+ const adapter = this.app.vault.adapter;
143
+ const baseline = (await adapter.exists(this.#path))
144
+ ? await adapter.read(this.#path)
145
+ : null;
146
+ this.#persistence = this.#createPersistence(baseline);
147
+ }
148
+ await this.app.vault.mkpath(dirname(this.#path));
149
+ await this.#persistence.persist(contents);
150
+ }
151
+ catch (error) {
152
+ if (this.#persistence && !this.#persistence.hasConflict)
153
+ await this.#persistence.rejectPersistence(error);
154
+ throw error;
155
+ }
156
+ this.#dirty = this.#contents !== contents;
157
+ if (!this.#dirty)
158
+ this.#onSaved = undefined;
159
+ notify?.();
160
+ if (!this.#dirty)
161
+ await this.#unregister.idle();
162
+ }
163
+ #protect() {
164
+ this.#locks++;
165
+ let held = true;
166
+ return () => {
167
+ if (held) {
168
+ held = false;
169
+ this.#locks--;
170
+ }
171
+ };
172
+ }
173
+ async protect() {
174
+ // All persistence is on the registry queue; no queued guard is re-entered here.
175
+ return this.#protect();
176
+ }
177
+ dispose() {
178
+ if (this.dirty)
179
+ throw new Error(`File state remains dirty: ${this.#path}`);
180
+ this.#disposed = true;
181
+ this.#unregister();
182
+ }
183
+ }
@@ -0,0 +1,20 @@
1
+ import type { WorkspaceSettingsController } from "@lapismd/design-core/workspace/settings";
2
+ /** Core application settings; no plugin owns file discovery policy. */
3
+ export declare const FILES_CONFIGURATION_SCHEMA: {
4
+ readonly id: "files";
5
+ readonly title: "Files";
6
+ readonly type: "object";
7
+ readonly properties: {
8
+ readonly exclude: {
9
+ readonly type: "array";
10
+ readonly title: "Excluded paths";
11
+ readonly description: "Glob patterns hidden from vault file and folder listings. Matching folders hide their contents at any depth. Explicit metadata access and backup exclusions are separate.";
12
+ readonly items: {
13
+ readonly type: "string";
14
+ };
15
+ readonly default: readonly string[];
16
+ };
17
+ };
18
+ };
19
+ /** Register core discovery policy beside the shell's existing Workspace fields. */
20
+ export declare function registerWorkspaceFileSettings(settings: WorkspaceSettingsController): void;
@@ -0,0 +1,47 @@
1
+ import { DEFAULT_VAULT_EXCLUDES } from "./storage/vault-listing";
2
+ /** Core application settings; no plugin owns file discovery policy. */
3
+ export const FILES_CONFIGURATION_SCHEMA = {
4
+ id: "files",
5
+ title: "Files",
6
+ type: "object",
7
+ properties: {
8
+ exclude: {
9
+ type: "array",
10
+ title: "Excluded paths",
11
+ description: "Glob patterns hidden from vault file and folder listings. Matching folders hide their contents at any depth. Explicit metadata access and backup exclusions are separate.",
12
+ items: { type: "string" },
13
+ default: [...DEFAULT_VAULT_EXCLUDES],
14
+ },
15
+ },
16
+ };
17
+ /** Register core discovery policy beside the shell's existing Workspace fields. */
18
+ export function registerWorkspaceFileSettings(settings) {
19
+ const workspace = settings.sections.find((section) => section.id === "workspace") ?? {
20
+ id: "workspace",
21
+ title: "Workspace",
22
+ navigationGroupId: "options",
23
+ order: 10,
24
+ };
25
+ settings.registerSection({
26
+ ...workspace,
27
+ fields: [
28
+ ...(workspace.fields ?? []).filter((field) => field.id !== "workspace.files"),
29
+ {
30
+ id: "workspace.files",
31
+ type: "group",
32
+ title: "File discovery",
33
+ description: "Choose which paths appear in file and folder lists.",
34
+ fields: [
35
+ {
36
+ id: "files.exclude",
37
+ type: "list",
38
+ itemType: "string",
39
+ title: "Excluded paths",
40
+ description: "Glob patterns hidden from file and folder lists. Folder names apply at any depth. Clear the list to show all paths. This does not change backups or delete files.",
41
+ default: [...DEFAULT_VAULT_EXCLUDES],
42
+ },
43
+ ],
44
+ },
45
+ ],
46
+ });
47
+ }