@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,40 @@
1
+ import { connect } from "@tursodatabase/database";
2
+ import fixture from "./fixtures/projection-query.json";
3
+ import { MemoryAppDatabase } from "../storage/app-database";
4
+ import { TursoAppDatabase, } from "../storage/turso-app-database";
5
+ export async function database(native, edges = []) {
6
+ const db = native
7
+ ? new TursoAppDatabase("projection-query-contract", {
8
+ kind: "turso-native",
9
+ transport: "native",
10
+ connectionFactory: async () => (await connect(":memory:", {
11
+ experimental: ["index_method"],
12
+ })),
13
+ })
14
+ : new MemoryAppDatabase("projection-query-contract");
15
+ await db.open();
16
+ await db.registerProjectionDefinition({
17
+ projectionId: fixture.projectionId,
18
+ ownerPluginId: "books",
19
+ schemaVersion: 1,
20
+ configHash: "fixture-1",
21
+ visibility: "public",
22
+ fields: {
23
+ title: { type: "string", sortable: true },
24
+ rank: { type: "number", sortable: true },
25
+ optional: { type: "string", indexed: true },
26
+ "literal.key": { type: "string", indexed: true },
27
+ },
28
+ active: true,
29
+ updatedAt: 1,
30
+ });
31
+ for (const row of fixture.rows)
32
+ await db.replaceProjectionSource({
33
+ projectionId: fixture.projectionId,
34
+ sourcePath: `${row.id}.md`,
35
+ sourceHash: "fixture-hash",
36
+ rows: [{ id: row.id, kind: "book", data: row }],
37
+ edges: edges.filter((edge) => edge.sourceRowId === row.id),
38
+ });
39
+ return db;
40
+ }
@@ -0,0 +1,47 @@
1
+ import { connect } from "@tursodatabase/database";
2
+ import { MemoryAppDatabase, } from "../storage/app-database";
3
+ import { TursoAppDatabase, } from "../storage/turso-app-database";
4
+ import input from "./fixtures/search-retrieval.json";
5
+ export const fixture = input;
6
+ export async function database(native) {
7
+ const db = native
8
+ ? new TursoAppDatabase("retrieval", {
9
+ kind: "turso-native",
10
+ transport: "native",
11
+ connectionFactory: async () => (await connect(":memory:", {
12
+ experimental: ["index_method"],
13
+ })),
14
+ })
15
+ : new MemoryAppDatabase("retrieval");
16
+ await db.open();
17
+ for (const { document, properties } of fixture.records) {
18
+ const file = {
19
+ path: document.path,
20
+ normalizedPath: document.path,
21
+ extension: "md",
22
+ mtime: 1,
23
+ size: 1,
24
+ hash: "reference",
25
+ indexed: true,
26
+ };
27
+ await db.upsertIndexedFile({
28
+ file,
29
+ metadata: {
30
+ path: file.path,
31
+ hash: file.hash,
32
+ parserVersion: "fixture",
33
+ metadata: {},
34
+ },
35
+ links: [],
36
+ tags: [],
37
+ properties: Object.entries(properties).map(([name, value]) => ({
38
+ path: file.path,
39
+ name,
40
+ value,
41
+ inferredType: typeof value,
42
+ })),
43
+ });
44
+ await db.upsertSearchDocument(document);
45
+ }
46
+ return db;
47
+ }
@@ -3,3 +3,4 @@ export * from "./storage/remote-app-database";
3
3
  export * from "./storage/index-projection";
4
4
  export * from "./storage/task-projection";
5
5
  export * from "./storage/turso-app-database";
6
+ export type * from "./native-database-protocol";
@@ -1,4 +1,5 @@
1
- export declare const APP_SHELL_PLUGINS_PATH = ".obsidian/app-shell-plugins.json";
1
+ import type { App } from "./context.svelte";
2
+ export declare const APP_SHELL_PLUGINS_PATH: string;
2
3
  export interface AppShellPluginPersistenceVault {
3
4
  mkpath?(path: string): Promise<unknown>;
4
5
  adapter?: {
@@ -12,4 +13,4 @@ export interface AppShellPluginEnablementPersistence {
12
13
  load(): Promise<unknown | null>;
13
14
  save(enabledById: Record<string, boolean>): Promise<void>;
14
15
  }
15
- export declare function createAppShellPluginPersistence(vault: AppShellPluginPersistenceVaultSource): AppShellPluginEnablementPersistence;
16
+ export declare function createAppShellPluginPersistence(vault: AppShellPluginPersistenceVaultSource, app?: App, apply?: (enabled: Record<string, boolean>) => Promise<void>): AppShellPluginEnablementPersistence;
@@ -1,17 +1,42 @@
1
- export const APP_SHELL_PLUGINS_PATH = ".obsidian/app-shell-plugins.json";
1
+ import { appStatePath, DEFAULT_APP_STATE_DIRECTORY } from "./storage/path";
2
+ import { readFileState, saveFileState } from "./file-state-store";
3
+ export const APP_SHELL_PLUGINS_PATH = appStatePath("app-shell-plugins.json");
2
4
  function resolvePluginPersistenceVault(source) {
3
5
  if (typeof source === "function") {
4
6
  return source() ?? undefined;
5
7
  }
6
8
  return source ?? undefined;
7
9
  }
8
- export function createAppShellPluginPersistence(vault) {
10
+ export function createAppShellPluginPersistence(vault, app, apply) {
11
+ let resetting = false;
12
+ let snapshot = {};
13
+ const reset = async (text) => {
14
+ const value = JSON.parse(text || "{}");
15
+ if (!value ||
16
+ typeof value !== "object" ||
17
+ Array.isArray(value) ||
18
+ Object.values(value).some((enabled) => typeof enabled !== "boolean"))
19
+ throw new Error("Invalid App Shell plugin configuration");
20
+ resetting = true;
21
+ try {
22
+ await apply?.(value);
23
+ snapshot = value;
24
+ }
25
+ finally {
26
+ resetting = false;
27
+ }
28
+ };
9
29
  return {
10
30
  async load() {
11
31
  const adapter = resolvePluginPersistenceVault(vault)?.adapter;
12
32
  if (!adapter?.read)
13
33
  return null;
14
34
  try {
35
+ if (app?.vault.supportsConditionalTextWrites) {
36
+ const raw = await readFileState(app, APP_SHELL_PLUGINS_PATH, reset);
37
+ snapshot = raw ? JSON.parse(raw) : {};
38
+ return raw ? snapshot : null;
39
+ }
15
40
  if (adapter.exists && !(await adapter.exists(APP_SHELL_PLUGINS_PATH))) {
16
41
  return null;
17
42
  }
@@ -23,11 +48,20 @@ export function createAppShellPluginPersistence(vault) {
23
48
  }
24
49
  },
25
50
  async save(enabledById) {
51
+ if (resetting)
52
+ return;
53
+ if (app?.vault.supportsConditionalTextWrites) {
54
+ snapshot = { ...snapshot, ...enabledById };
55
+ await saveFileState(app, APP_SHELL_PLUGINS_PATH, `${JSON.stringify(snapshot, null, 2)}\n`);
56
+ return;
57
+ }
26
58
  const resolved = resolvePluginPersistenceVault(vault);
27
59
  const adapter = resolved?.adapter;
28
60
  if (!adapter?.write)
29
61
  return;
30
- await resolved?.mkpath?.(".obsidian").catch(() => undefined);
62
+ await resolved
63
+ ?.mkpath?.(DEFAULT_APP_STATE_DIRECTORY)
64
+ .catch(() => undefined);
31
65
  await adapter.write(APP_SHELL_PLUGINS_PATH, `${JSON.stringify(enabledById, null, 2)}\n`);
32
66
  },
33
67
  };
@@ -1,3 +1,5 @@
1
+ import type { ContributionStatus } from "./native-indexing-protocol";
2
+ import type { IndexProjectionRegistration } from "./storage/index-projection";
1
3
  import type { App } from "./context.svelte";
2
4
  import { EventDispatcher } from "./events";
3
5
  import {
@@ -310,6 +312,7 @@ export declare class MetadataCache extends EventDispatcher<{
310
312
  readonly logger: import("./logging").Logger;
311
313
  private readonly saveSnapshotDebounced;
312
314
  constructor(app: App);
315
+ get backgroundIndexing(): import("./native-background-indexing").NativeBackgroundIndexingObserver | undefined;
313
316
  get metadataCache(): Record<string, CachedMetadata>;
314
317
  get resolvedLinks(): Record<string, Record<string, number>>;
315
318
  get unresolvedLinks(): Record<string, Record<string, number>>;
@@ -552,7 +555,11 @@ export declare class MetadataCache extends EventDispatcher<{
552
555
  getFirstLinkpathDest(linkpath: string, sourcePath: string): TFile | null;
553
556
  fileToLinktext(file: TFile, sourcePath: string, omitMdExtension?: boolean): string;
554
557
  rebuild(): Promise<void>;
555
- addProcessor(ext: string, processor: MetadataProcessor): void;
558
+ registerNativeProjection(owner: string, id: string, registration: IndexProjectionRegistration): void;
559
+ finishNativeContributions(owner: string): Promise<void>;
560
+ disableNativeContributions(owner: string): Promise<void>;
561
+ get nativeContributionStatuses(): ContributionStatus[];
562
+ addProcessor(ext: string, processor: MetadataProcessor, owner?: string): void;
556
563
  removeProcessor(ext: string, processor: MetadataProcessor): boolean;
557
564
  addIndexedProjectionContributor(contributor: IndexedProjectionContributor): void;
558
565
  removeIndexedProjectionContributor(contributor: IndexedProjectionContributor): boolean;
@@ -5,6 +5,7 @@ import { defaultLinkSettings, getLinkPath, normalizeVaultPath, } from "./links";
5
5
  import { basename, TFile, } from "./storage";
6
6
  import { debounce } from "lodash-es";
7
7
  import { dirname, resolvePath } from "./storage/path";
8
+ import { NativeBackgroundIndexingSession, } from "./native-background-indexing";
8
9
  export const METADATA_CACHE_BACKUP_PATH = ".lapis/cache/metadata-cache.json";
9
10
  export const METADATA_CACHE_HOT_LIMIT = 512;
10
11
  const METADATA_RECONCILE_CHECKPOINT_KEY = "metadata-cache.reconcile-checkpoint.v1";
@@ -226,6 +227,7 @@ export class MetadataCache extends EventDispatcher {
226
227
  #unresolvedLinks = $state({});
227
228
  logger = logging.getLogger("cache");
228
229
  disposed = false;
230
+ nativeIndexing;
229
231
  didLoad = false;
230
232
  loadPromise = null;
231
233
  disposePromise = null;
@@ -249,11 +251,17 @@ export class MetadataCache extends EventDispatcher {
249
251
  constructor(app) {
250
252
  super();
251
253
  this.app = app;
254
+ const port = app.session?.backgroundIndexing;
255
+ if (port)
256
+ this.nativeIndexing = new NativeBackgroundIndexingSession(app, port);
252
257
  app.vault.on("all", this.handleVaultChange);
253
258
  app.vault.on("rename", this.handleVaultRename);
254
259
  this.databaseChangeUnsubscribe =
255
260
  app.appDatabase.subscribeToChanges?.((change) => this.handleDatabaseChange(change)) ?? null;
256
261
  }
262
+ get backgroundIndexing() {
263
+ return this.nativeIndexing;
264
+ }
257
265
  get metadataCache() {
258
266
  return this.#metadataCache;
259
267
  }
@@ -281,7 +289,7 @@ export class MetadataCache extends EventDispatcher {
281
289
  return map;
282
290
  }
283
291
  scheduleSnapshotSave() {
284
- if (this.disposed || this.snapshotLeaseCount === 0)
292
+ if (this.nativeIndexing || this.disposed || this.snapshotLeaseCount === 0)
285
293
  return;
286
294
  this.saveSnapshotDebounced();
287
295
  }
@@ -292,7 +300,9 @@ export class MetadataCache extends EventDispatcher {
292
300
  async dispose(options = {}) {
293
301
  if (this.disposePromise)
294
302
  return this.disposePromise;
303
+ const nativeTasksDetached = this.app.nativeTasks?.detach();
295
304
  this.disposed = true;
305
+ this.nativeIndexing?.dispose();
296
306
  this.app.vault.off("all", this.handleVaultChange);
297
307
  this.app.vault.off("rename", this.handleVaultRename);
298
308
  this.databaseChangeUnsubscribe?.();
@@ -302,13 +312,20 @@ export class MetadataCache extends EventDispatcher {
302
312
  this.snapshotLeaseValue = null;
303
313
  this.saveSnapshotDebounced.cancel();
304
314
  this.disposePromise = (async () => {
315
+ if (nativeTasksDetached)
316
+ await nativeTasksDetached;
317
+ if (this.nativeIndexing)
318
+ return;
305
319
  await Promise.allSettled([...this.pendingOperations]);
306
- await this.persistReconcileCheckpoint({ allowDisposed: true });
320
+ if (!this.nativeIndexing)
321
+ await this.persistReconcileCheckpoint({ allowDisposed: true });
307
322
  void options;
308
323
  })();
309
324
  return this.disposePromise;
310
325
  }
311
326
  async saveSnapshotNow(_options = {}) {
327
+ if (this.nativeIndexing)
328
+ return;
312
329
  const snapshot = await this.buildCompatibilitySnapshot();
313
330
  try {
314
331
  await this.app.appDatabase.saveMetadataSnapshot(snapshot);
@@ -345,6 +362,14 @@ export class MetadataCache extends EventDispatcher {
345
362
  return this.loadPromise;
346
363
  }
347
364
  async performLoad() {
365
+ if (this.nativeIndexing) {
366
+ await this.nativeIndexing.load();
367
+ if (this.disposed)
368
+ return;
369
+ this.didLoad = true;
370
+ this.trigger("loaded");
371
+ return;
372
+ }
348
373
  try {
349
374
  await this.app.telemetry.measureAsync("metadata.cache.load", async (loadSpan) => this.app.notifications.withProgress({
350
375
  title: "Loading metadata cache",
@@ -585,8 +610,8 @@ export class MetadataCache extends EventDispatcher {
585
610
  }
586
611
  /**
587
612
  * Return the current vault/parser manifest fingerprint used to validate the
588
- * durable metadata index and disposable indexed-consumer snapshots.
589
- * Consumers may restrict the fingerprint to their accepted metadata files.
613
+ * durable metadata index and disposable indexed-consumer snapshots. Consumers
614
+ * may restrict the fingerprint to their accepted metadata files.
590
615
  */
591
616
  getReconciliationFingerprint(acceptsFile) {
592
617
  const fingerprint = new StreamingManifestFingerprint();
@@ -887,6 +912,8 @@ export class MetadataCache extends EventDispatcher {
887
912
  }, vaultIndex);
888
913
  }
889
914
  rebuild(progress) {
915
+ if (this.nativeIndexing)
916
+ return this.nativeIndexing.refresh(true);
890
917
  if (progress) {
891
918
  return this.performRebuild(progress);
892
919
  }
@@ -945,7 +972,19 @@ export class MetadataCache extends EventDispatcher {
945
972
  this.markReconcileCheckpointDirty();
946
973
  await this.persistReconcileCheckpoint();
947
974
  }
948
- addProcessor(ext, processor) {
975
+ registerNativeProjection(owner, id, registration) {
976
+ this.nativeIndexing?.registerProjection(owner, id, registration);
977
+ }
978
+ async finishNativeContributions(owner) {
979
+ await this.nativeIndexing?.ready(owner);
980
+ }
981
+ async disableNativeContributions(owner) {
982
+ await this.nativeIndexing?.disable(owner);
983
+ }
984
+ get nativeContributionStatuses() {
985
+ return this.nativeIndexing?.getContributionStatuses() ?? [];
986
+ }
987
+ addProcessor(ext, processor, owner) {
949
988
  if (!ext?.trim())
950
989
  return;
951
990
  ext = ext.trim().toLowerCase();
@@ -953,6 +992,7 @@ export class MetadataCache extends EventDispatcher {
953
992
  this.processors.set(ext, new Set());
954
993
  }
955
994
  this.processors.get(ext).add(processor);
995
+ this.nativeIndexing?.registerProcessor(owner ?? "application", ext);
956
996
  }
957
997
  removeProcessor(ext, processor) {
958
998
  if (!ext?.trim())
@@ -1064,6 +1104,13 @@ export class MetadataCache extends EventDispatcher {
1064
1104
  handleDatabaseChange(change) {
1065
1105
  if (this.disposed)
1066
1106
  return;
1107
+ if (this.nativeIndexing) {
1108
+ this.trackOperation(this.observeNativeChange(change).finally(() => {
1109
+ if (!this.disposed)
1110
+ this.trigger("index-changed", change);
1111
+ }));
1112
+ return;
1113
+ }
1067
1114
  if (this.snapshotLeaseCount > 0) {
1068
1115
  this.trackOperation(this.refreshCompatibilitySnapshot(change));
1069
1116
  }
@@ -1086,6 +1133,41 @@ export class MetadataCache extends EventDispatcher {
1086
1133
  }
1087
1134
  this.trigger("index-changed", change);
1088
1135
  }
1136
+ async observeNativeChange(change) {
1137
+ if (change.reset) {
1138
+ if (this.snapshotLeaseCount > 0)
1139
+ await this.refreshCompatibilitySnapshot(change);
1140
+ else
1141
+ for (const path of [...this.hotPathOrder.keys()])
1142
+ this.evictHotPath(path);
1143
+ }
1144
+ if (change.domains.includes("metadata")) {
1145
+ for (const path of change.paths) {
1146
+ await this.observeNativeFile(path);
1147
+ }
1148
+ }
1149
+ }
1150
+ async observeNativeFile(path) {
1151
+ if (this.disposed)
1152
+ return;
1153
+ const row = await this.app.appDatabase.getIndexedFile(path);
1154
+ if (this.disposed)
1155
+ return;
1156
+ this.evictHotPath(path);
1157
+ const file = this.app.vault.getFileByPath(path);
1158
+ if (!row?.metadata)
1159
+ return;
1160
+ this.cacheDatabaseRow(row);
1161
+ if (!file)
1162
+ return;
1163
+ const text = await this.app.vault.read(file);
1164
+ if (this.disposed ||
1165
+ file.path !== path ||
1166
+ !this.app.vault.getFileByPath(path) ||
1167
+ md5(text) !== row.metadata.hash)
1168
+ return;
1169
+ this.trigger("changed", file, text, row.metadata.metadata);
1170
+ }
1089
1171
  deferReconcileIndexChange(change) {
1090
1172
  if (!this.deferredReconcileIndexChange) {
1091
1173
  this.deferredReconcileIndexChange = cloneAppDatabaseChange(change);
@@ -1187,6 +1269,16 @@ export class MetadataCache extends EventDispatcher {
1187
1269
  return;
1188
1270
  if (!(file instanceof TFile))
1189
1271
  return;
1272
+ if (this.nativeIndexing) {
1273
+ if (event === "delete") {
1274
+ this.trigger("deleted", file, this.getCache(file.path));
1275
+ this.evictHotPath(file.path);
1276
+ }
1277
+ else if (event === "create" || event === "modify") {
1278
+ this.trackOperation(this.observeNativeFile(file.path));
1279
+ }
1280
+ return;
1281
+ }
1190
1282
  switch (event) {
1191
1283
  case "create":
1192
1284
  case "modify":
@@ -1207,6 +1299,12 @@ export class MetadataCache extends EventDispatcher {
1207
1299
  return;
1208
1300
  if (!(file instanceof TFile))
1209
1301
  return;
1302
+ if (this.nativeIndexing) {
1303
+ this.evictHotPath(oldPath);
1304
+ this.evictHotPath(file.path);
1305
+ this.trackOperation(this.observeNativeFile(file.path));
1306
+ return;
1307
+ }
1210
1308
  const existing = this.fileCache[oldPath];
1211
1309
  if (existing) {
1212
1310
  this.fileCache[file.path] = {
@@ -1,6 +1,9 @@
1
+ import { appStatePath, DEFAULT_APP_STATE_DIRECTORY, normalizePath, } from "./storage/path";
1
2
  import { EventDispatcher } from "./events";
3
+ import { TFile as VaultFile } from "./storage/fs";
4
+ import { FileTextStateWriter } from "./file-text-state-writer";
2
5
  import { resolveApplication } from "./application-compatibility";
3
- const HOTKEYS_PATH = "/.obsidian/hotkeys.json";
6
+ const HOTKEYS_PATH = "/" + appStatePath("hotkeys.json");
4
7
  const MODIFIER_ORDER = ["Mod", "Ctrl", "Meta", "Alt", "Shift"];
5
8
  const WORKSPACE_ROOT_COMMAND_HOST_ID = "root";
6
9
  function isModifierValue(value) {
@@ -63,6 +66,8 @@ export class CommandManager extends EventDispatcher {
63
66
  editorCommands = $state({});
64
67
  bindings = $state({});
65
68
  hotkeyOverrides = $state({});
69
+ hotkeysBaseline = null;
70
+ hotkeysWriter;
66
71
  constructor(app) {
67
72
  super();
68
73
  this.app = app;
@@ -157,13 +162,21 @@ export class CommandManager extends EventDispatcher {
157
162
  return deduped;
158
163
  }
159
164
  async loadHotkeys() {
165
+ if (this.hotkeysWriter?.dirty)
166
+ return;
160
167
  const file = this.readHotkeyOverrideFile();
161
168
  if (!file) {
169
+ this.hotkeysBaseline = null;
170
+ if (this.hotkeysWriter && !this.hotkeysWriter.acceptBaseline(null))
171
+ return;
162
172
  this.replaceHotkeyOverrides({});
163
173
  return;
164
174
  }
165
175
  try {
166
176
  const raw = await this.app.vault.read(file);
177
+ if (this.hotkeysWriter && !this.hotkeysWriter.acceptBaseline(raw))
178
+ return;
179
+ this.hotkeysBaseline = raw;
167
180
  this.replaceHotkeyOverrides(this.normalizeOverrides(JSON.parse(raw)));
168
181
  }
169
182
  catch (error) {
@@ -173,14 +186,24 @@ export class CommandManager extends EventDispatcher {
173
186
  }
174
187
  async saveHotkeys() {
175
188
  const data = JSON.stringify(this.hotkeyOverrides, null, 2);
189
+ if (this.app.vault.supportsConditionalTextWrites)
190
+ return this.getHotkeysWriter().set(data);
176
191
  const file = this.readHotkeyOverrideFile();
177
192
  if (file) {
178
193
  await this.app.vault.modify(file, data);
179
194
  return;
180
195
  }
181
- await this.app.vault.mkpath("/.obsidian");
196
+ await this.app.vault.mkpath("/" + DEFAULT_APP_STATE_DIRECTORY);
182
197
  await this.app.vault.create(HOTKEYS_PATH, data);
183
198
  }
199
+ getHotkeysWriter() {
200
+ if (this.hotkeysWriter)
201
+ return this.hotkeysWriter;
202
+ const file = this.readHotkeyOverrideFile() ??
203
+ new VaultFile(normalizePath(HOTKEYS_PATH), { ctime: 0, mtime: 0, size: 0 }, null);
204
+ this.hotkeysWriter = new FileTextStateWriter(this.app, file, this.hotkeysBaseline, (text) => this.replaceHotkeyOverrides(this.normalizeOverrides(JSON.parse(text || "{}"))));
205
+ return this.hotkeysWriter;
206
+ }
184
207
  replaceHotkeyOverrides(overrides) {
185
208
  for (const id of Object.keys(this.hotkeyOverrides)) {
186
209
  delete this.hotkeyOverrides[id];
@@ -206,6 +229,8 @@ export class CommandManager extends EventDispatcher {
206
229
  return this.hasHotkeyOverride(commandId);
207
230
  }
208
231
  async setHotkeys(commandId, hotkeys) {
232
+ if (this.app.vault.supportsConditionalTextWrites)
233
+ this.getHotkeysWriter().assertEditable();
209
234
  this.hotkeyOverrides[commandId] = this.dedupeHotkeys(hotkeys);
210
235
  this.rebuildBindings();
211
236
  this.emit("hotkeys-updated", {
@@ -225,6 +250,8 @@ export class CommandManager extends EventDispatcher {
225
250
  await this.setHotkeys(commandId, this.getEffectiveHotkeys(commandId).filter((candidate) => keyId(candidate) !== removeId));
226
251
  }
227
252
  async resetHotkeys(commandId) {
253
+ if (this.app.vault.supportsConditionalTextWrites)
254
+ this.getHotkeysWriter().assertEditable();
228
255
  delete this.hotkeyOverrides[commandId];
229
256
  this.rebuildBindings();
230
257
  this.emit("hotkeys-updated", {
@@ -101,6 +101,8 @@
101
101
  */
102
102
  [data-ui-component="editor"] .inline-title,
103
103
  [data-ui-component="editor"] .cm-editor-inline-title {
104
+ box-sizing: border-box;
105
+ padding-inline-start: var(--ui-editor-inline-title-inset, 0px);
104
106
  color: var(--ui-editor-inline-title-color);
105
107
  font-family: var(--ui-editor-inline-title-font);
106
108
  font-size: var(--ui-editor-inline-title-size);
@@ -112,12 +114,14 @@
112
114
  margin-block-end: var(--ui-editor-inline-title-margin-bottom);
113
115
  width: 100%;
114
116
  white-space: pre-wrap;
117
+ overflow-wrap: anywhere;
115
118
  border: none;
116
119
  outline: none;
117
120
  }
118
121
 
119
122
  [data-ui-component="editor"]
120
123
  .cm-editor-inline-title[data-mobile-inline-title="true"] {
124
+ padding-inline-start: 0;
121
125
  text-align: center;
122
126
  }
123
127
 
@@ -4,6 +4,7 @@
4
4
  import { dirname } from "../../storage/path";
5
5
  import { WorkspaceLeaf } from "../../workspace.svelte";
6
6
  import { editorConfig } from "./editor";
7
+ import { syncInlineTitleAlignment } from "./inline-title-alignment";
7
8
  import { ScrollArea } from "@lapismd/design-core/shadcn/scroll-area";
8
9
  import { cn } from "../../utils";
9
10
  import type { App } from "../../context.svelte";
@@ -53,10 +54,11 @@
53
54
  const codeMirror = (node: HTMLDivElement) => {
54
55
  let focusTimeout: ReturnType<typeof setTimeout> | null = null;
55
56
  const root = node.getRootNode();
56
- if (root instanceof Document || root instanceof ShadowRoot) {
57
- editor.view.setRoot(root);
57
+ if (root.nodeType === 9 || root.nodeType === 11) {
58
+ editor.view.setRoot(root as Document | ShadowRoot);
58
59
  }
59
60
  node.append(editor.view.dom);
61
+ const stopTitleAlignment = syncInlineTitleAlignment(editor.view, node);
60
62
  focusTimeout = setTimeout(() => {
61
63
  editor.view.focus();
62
64
  });
@@ -64,6 +66,7 @@
64
66
  return {
65
67
  update(qs: string) {},
66
68
  destroy() {
69
+ stopTitleAlignment();
67
70
  if (focusTimeout !== null) {
68
71
  clearTimeout(focusTimeout);
69
72
  focusTimeout = null;
@@ -93,9 +96,20 @@
93
96
  }
94
97
  }
95
98
 
99
+ function titleText(node: HTMLDivElement, name: string) {
100
+ const update = (value: string) => {
101
+ // Native contenteditable input can replace Svelte's original text node.
102
+ // Project committed filenames onto the element, including after renames.
103
+ if (node.textContent !== value) node.textContent = value;
104
+ };
105
+ update(name);
106
+ return { update };
107
+ }
108
+
96
109
  function renameDone(evt: KeyboardEvent) {
97
110
  if (evt.key == "Enter") {
98
- renameFile((evt.target as HTMLDivElement).getText());
111
+ // Focusing the editor commits through onfocusout. Starting another rename
112
+ // here races that same file operation against its original source path.
99
113
  editor.view.focus();
100
114
  evt.preventDefault();
101
115
  }
@@ -122,6 +136,7 @@
122
136
  <!-- svelte-ignore a11y_no_static_element_interactions -->
123
137
  {#if shouldShowDesktopInlineTitle && file}
124
138
  <div
139
+ use:titleText={file?.name ?? ""}
125
140
  onkeydown={renameDone}
126
141
  onfocusout={(evt) =>
127
142
  renameFile((evt.target as HTMLDivElement).getText())}
@@ -131,13 +146,12 @@
131
146
  autocapitalize="on"
132
147
  tabindex="-1"
133
148
  enterkeyhint="done"
134
- >
135
- {file?.name}
136
- </div>
149
+ ></div>
137
150
  {/if}
138
151
  {#if shouldShowMobileInlineTitle && file}
139
152
  <!-- svelte-ignore a11y_no_static_element_interactions -->
140
153
  <div
154
+ use:titleText={file?.name ?? ""}
141
155
  onkeydown={renameDone}
142
156
  onfocusout={(evt) =>
143
157
  renameFile((evt.target as HTMLDivElement).getText())}
@@ -150,9 +164,7 @@
150
164
  autocapitalize="on"
151
165
  tabindex="-1"
152
166
  enterkeyhint="done"
153
- >
154
- {file?.name}
155
- </div>
167
+ ></div>
156
168
  {/if}
157
169
  <div use:codeMirror class="cm-editor-content"></div>
158
170
  </div>
@@ -1,3 +1,4 @@
1
+ import { untrack } from "svelte";
1
2
  import { markupEditor } from "./editor";
2
3
  export function embeddedEditorContext(options) {
3
4
  return {
@@ -18,5 +19,6 @@ export function resolveEmbeddedEditorExtensions(app, options) {
18
19
  ];
19
20
  }
20
21
  export function applyEmbeddedEditorExtensions(app, editor, options) {
21
- editor.updateExtensions(resolveEmbeddedEditorExtensions(app, options));
22
+ const extensions = resolveEmbeddedEditorExtensions(app, options);
23
+ untrack(() => editor.updateExtensions(extensions));
22
24
  }
@@ -0,0 +1,3 @@
1
+ import type { EditorView } from "@codemirror/view";
2
+ /** Keep the sibling filename aligned with CodeMirror's unindented text column. */
3
+ export declare function syncInlineTitleAlignment(view: EditorView, host: HTMLElement): () => void;