@lapis-notes/api 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/CHANGELOG.md +193 -0
  2. package/README.md +19 -0
  3. package/dist/__tests__/fixtures/indexed-mutations.json +6862 -0
  4. package/dist/__tests__/fixtures/memory-storage.json +309 -0
  5. package/dist/__tests__/fixtures/native-facade-contract.json +3224 -0
  6. package/dist/__tests__/fixtures/projection-query.json +195 -0
  7. package/dist/__tests__/fixtures/projection-values.json +60 -0
  8. package/dist/__tests__/fixtures/search-preparation.json +1513 -0
  9. package/dist/__tests__/fixtures/search-retrieval.json +15409 -0
  10. package/dist/__tests__/fixtures/search-semantic-model.json +3691 -0
  11. package/dist/__tests__/fixtures/search-semantic-retrieval.json +6603 -0
  12. package/dist/__tests__/fixtures/search-storage.json +409 -0
  13. package/dist/__tests__/fixtures/task-projection.json +428 -0
  14. package/dist/__tests__/indexed-mutation-support.js +262 -0
  15. package/dist/__tests__/projection-query-support.js +40 -0
  16. package/dist/__tests__/search-retrieval-support.js +47 -0
  17. package/dist/app-database-host.d.ts +1 -0
  18. package/dist/app-shell-plugin-persistence.d.ts +3 -2
  19. package/dist/app-shell-plugin-persistence.js +37 -3
  20. package/dist/cache.svelte.d.ts +8 -1
  21. package/dist/cache.svelte.js +103 -5
  22. package/dist/command.svelte.js +29 -2
  23. package/dist/components/editor/editor.css +4 -0
  24. package/dist/components/editor/editor.svelte +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
@@ -1,3 +1,4 @@
1
+ import { appStatePath } from "./storage/path";
1
2
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
3
  import { EventDispatcher } from "./events";
3
4
  import { EmptyView, FileView, ItemView, TextFileView, View, } from "./view.svelte";
@@ -7,10 +8,13 @@ import { tick, untrack } from "svelte";
7
8
  import { toast } from "svelte-sonner";
8
9
  import { SidebarState, } from "@lapis-notes/ui/sidebar-custom";
9
10
  import { uniqueId } from "./utils";
11
+ import { attachWorkspaceView } from "./workspace-view-document";
12
+ import { workspaceCloseGuard } from "./workspace-close-guard";
10
13
  import { normalizeWorkspaceJson } from "./workspace-layout-normalizer";
11
14
  import { promptLoadWorkspaceLayout, promptSaveWorkspaceLayout, } from "./workspace-layout-commands";
12
15
  import { NAMED_WORKSPACES_FILE, parseNamedWorkspaceStore, serializeNamedWorkspaceStore, } from "./workspace-layouts";
13
16
  import { debounce, isEqual } from "lodash-es";
17
+ import { registerWorkspaceFileSettings } from "./files-configuration";
14
18
  import { basename, dirname, joinPath } from "./storage";
15
19
  import { HistoryManager } from "./history.svelte";
16
20
  import { EditorViewRegistry, } from "./editor-view-registry";
@@ -25,7 +29,7 @@ import { problemsPlugin, PROBLEMS_VIEW_TYPE, } from "@lapismd/design-core/worksp
25
29
  import { createAppShellPluginPersistence } from "./app-shell-plugin-persistence";
26
30
  import { resolveWorkspaceHostBinding, setWorkspaceHostBinding, } from "./workspace-host-internal";
27
31
  import { getWorkspaceHostBinding } from "./workspace-host";
28
- import { DiagnosticsManager, pathFromDiagnosticResource } from "./diagnostics";
32
+ import { DiagnosticsManager, pathFromDiagnosticResource, } from "./diagnostics";
29
33
  import { getApplicationCompatibility, resolveApplication, } from "./application-compatibility";
30
34
  const DEFAULT_LAPIS_LOGO_URL = new URL("./assets/lapis-logo.svg", import.meta.url).href;
31
35
  const WORKSPACE_HINT_TARGET_SELECTOR = "[data-hint-target]";
@@ -205,8 +209,8 @@ function collectWorkspaceProjectionItem(item, items) {
205
209
  item.children.forEach((child) => collectWorkspaceProjectionItem(child, items));
206
210
  }
207
211
  }
208
- function beginWorkspaceProjection(workspace) {
209
- const items = new Map();
212
+ function beginWorkspaceProjection(workspace, isCurrent) {
213
+ const items = Object.assign(new Map(), { isCurrent });
210
214
  workspace.rootSplit.children.forEach((child) => collectWorkspaceProjectionItem(child, items));
211
215
  workspace.leftSplit.children.forEach((child) => collectWorkspaceProjectionItem(child, items));
212
216
  workspace.rightSplit.children.forEach((child) => collectWorkspaceProjectionItem(child, items));
@@ -223,8 +227,13 @@ function claimWorkspaceProjectionItem(projectionItems, id, matches, create) {
223
227
  }
224
228
  return create();
225
229
  }
226
- function finishWorkspaceProjection(projectionItems) {
230
+ function finishWorkspaceProjection(projectionItems, workspace) {
231
+ const retained = projectionItems.isCurrent?.() === false
232
+ ? beginWorkspaceProjection(workspace, () => true)
233
+ : undefined;
227
234
  for (const item of projectionItems.values()) {
235
+ if (retained?.get(item.id) === item)
236
+ continue;
228
237
  if (item instanceof WorkspaceLeaf) {
229
238
  item.view.unload();
230
239
  if (hasDestroyableEditor(item.view))
@@ -238,6 +247,10 @@ function finishWorkspaceProjection(projectionItems) {
238
247
  export const WORKSPACE_ROOT_HOST_ID = "root";
239
248
  export const WORKSPACE_POPOUT_UNSUPPORTED_ERROR_MESSAGE = "Workspace popout windows are not supported in the current renderer host.";
240
249
  let workspacePopoutHost = null;
250
+ const layoutMutationRevisions = new WeakMap();
251
+ function recordLayoutMutation(workspace) {
252
+ layoutMutationRevisions.set(workspace, (layoutMutationRevisions.get(workspace) ?? 0) + 1);
253
+ }
241
254
  export function setWorkspacePopoutHost(host) {
242
255
  workspacePopoutHost = host;
243
256
  }
@@ -279,6 +292,8 @@ export class WorkspaceSplit extends WorkspaceParent {
279
292
  this.sizes[i] = sizes[i] ?? 50;
280
293
  });
281
294
  return Promise.all(promises).then(() => {
295
+ if (projectionItems?.isCurrent?.() === false)
296
+ return;
282
297
  this.id = layout.id;
283
298
  this.type = layout.direction;
284
299
  });
@@ -417,6 +432,8 @@ export class WorkspaceSidedock extends WorkspaceSplit {
417
432
  }
418
433
  loadJson(layout, projectionItems) {
419
434
  return super.loadJson(layout, projectionItems).then(() => {
435
+ if (projectionItems?.isCurrent?.() === false)
436
+ return;
420
437
  if (/^0(?:px|rem|em|%)?$/.test(layout.width.trim())) {
421
438
  this.open = false;
422
439
  }
@@ -520,6 +537,8 @@ export class WorkspaceWindow extends WorkspaceContainer {
520
537
  children: layout.children,
521
538
  }, projectionItems)
522
539
  .then(() => {
540
+ if (projectionItems?.isCurrent?.() === false)
541
+ return;
523
542
  this.id = layout.id;
524
543
  this.applyInitData({
525
544
  mode: layout.mode,
@@ -574,22 +593,26 @@ function hasDestroyableEditor(view) {
574
593
  }
575
594
  export class WorkspaceFloating extends WorkspaceParent {
576
595
  children = $state([]);
577
- loadJson(layouts = [], projectionItems) {
596
+ loadJson(layouts = [], projectionItems, resolveHandle) {
578
597
  const promises = [];
579
598
  this.children.slice().forEach((child) => this.removeChild(child, true));
580
599
  layouts.forEach((layout) => {
581
600
  const child = claimWorkspaceProjectionItem(projectionItems, layout.id, (item) => item instanceof WorkspaceWindow, () => new WorkspaceWindow());
582
601
  this.addChild(child);
583
602
  promises.push(child.loadWindowJson(layout, projectionItems).then(() => {
603
+ if (projectionItems?.isCurrent?.() === false)
604
+ return;
584
605
  if (child.mode !== "popout") {
585
606
  return;
586
607
  }
587
- const handle = openWorkspacePopoutHandle({
588
- x: child.x,
589
- y: child.y,
590
- size: { width: child.width, height: child.height },
591
- mode: "popout",
592
- });
608
+ const handle = resolveHandle
609
+ ? resolveHandle(child.id)
610
+ : openWorkspacePopoutHandle({
611
+ x: child.x,
612
+ y: child.y,
613
+ size: { width: child.width, height: child.height },
614
+ mode: "popout",
615
+ });
593
616
  if (!handle) {
594
617
  child.mode = "floating";
595
618
  return;
@@ -741,9 +764,7 @@ export class WorkspaceSidebarGroup extends WorkspaceParent {
741
764
  const selected = this.selectedLeafId
742
765
  ? this.children.find((leaf) => leaf.id === this.selectedLeafId && !this.isLeafHidden(leaf))
743
766
  : undefined;
744
- return (selected ??
745
- this.children.find((leaf) => !this.isLeafHidden(leaf)) ??
746
- null);
767
+ return (selected ?? this.children.find((leaf) => !this.isLeafHidden(leaf)) ?? null);
747
768
  }
748
769
  selectLeaf(leaf) {
749
770
  if (!this.children.includes(leaf)) {
@@ -812,9 +833,11 @@ export class WorkspaceSidebarGroup extends WorkspaceParent {
812
833
  layout.children.forEach((config) => {
813
834
  const leaf = claimWorkspaceProjectionItem(projectionItems, config.id, (item) => item instanceof WorkspaceLeaf, () => new WorkspaceLeaf());
814
835
  this.addChild(leaf);
815
- promises.push(leaf.loadJson(config));
836
+ promises.push(loadWorkspaceLeafJson(leaf, config, projectionItems?.isCurrent));
816
837
  });
817
838
  return Promise.all(promises).then(() => {
839
+ if (projectionItems?.isCurrent?.() === false)
840
+ return;
818
841
  this.id = layout.id;
819
842
  this.name = layout.name;
820
843
  this.icon = layout.icon;
@@ -874,10 +897,12 @@ export class WorkspaceTabs extends WorkspaceParent {
874
897
  else {
875
898
  const leaf = claimWorkspaceProjectionItem(projectionItems, config.id, (item) => item instanceof WorkspaceLeaf, () => new WorkspaceLeaf());
876
899
  this.addChild(leaf);
877
- promises.push(leaf.loadJson(config));
900
+ promises.push(loadWorkspaceLeafJson(leaf, config, projectionItems?.isCurrent));
878
901
  }
879
902
  });
880
903
  return Promise.all(promises).then(() => {
904
+ if (projectionItems?.isCurrent?.() === false)
905
+ return;
881
906
  this.id = layout.id;
882
907
  this.stacked = layout.stacked;
883
908
  this.selected = layout.currentTab;
@@ -1035,6 +1060,8 @@ export class WorkspaceBottomPanel extends WorkspaceTabs {
1035
1060
  }
1036
1061
  loadJson(layout, projectionItems) {
1037
1062
  return super.loadJson(layout, projectionItems).then(() => {
1063
+ if (projectionItems?.isCurrent?.() === false)
1064
+ return;
1038
1065
  const collapsed = /^0(?:px|rem|em|%)?$/.test(layout.height.trim());
1039
1066
  this.open = !collapsed;
1040
1067
  if (!collapsed)
@@ -1253,6 +1280,9 @@ export class Workspace extends EventDispatcher {
1253
1280
  viewTypes = new Map([
1254
1281
  ["empty", (leaf) => new EmptyView(leaf)],
1255
1282
  ]);
1283
+ fileIconListeners = new Set();
1284
+ iconExtensionSequence = 0;
1285
+ iconExtensions = new Map();
1256
1286
  viewExtensions = new Map();
1257
1287
  editorViews = new EditorViewRegistry();
1258
1288
  sidebarViewPlacements = new Map();
@@ -1281,6 +1311,7 @@ export class Workspace extends EventDispatcher {
1281
1311
  layoutHandlers = [];
1282
1312
  suspendedLayoutPersistence = 0;
1283
1313
  #workspaceHostController = null;
1314
+ #diagnosticOpeners = new Set();
1284
1315
  diagnostics;
1285
1316
  #syncingWorkspaceHost = 0;
1286
1317
  #workspaceHostProjection = Promise.resolve();
@@ -1292,27 +1323,53 @@ export class Workspace extends EventDispatcher {
1292
1323
  #notificationProgressBridgeDisposer = null;
1293
1324
  #commandBridgeDisposer = null;
1294
1325
  workspaceLayoutFile = "workspace.json";
1326
+ #layoutSaveTail = Promise.resolve();
1327
+ #layoutPersistenceDisposed = false;
1295
1328
  statusEl = $state();
1296
1329
  statusCompatEl = $state();
1297
1330
  leftRibbon = new WorkspaceRibbon(this, "left");
1298
1331
  rightRibbon = new WorkspaceRibbon(this, "right");
1299
1332
  containerEl = $state(createDiv("workspace"));
1300
1333
  saveLayoutDebounced = debounce(() => {
1301
- tick().then(() => {
1302
- if (!this.layoutReady) {
1303
- return;
1304
- }
1305
- return this.app.vault.create(joinPath("/.obsidian", this.workspaceLayoutFile), JSON.stringify(this.toJson(), null, 2));
1334
+ void this.saveLayoutNow().catch((error) => {
1335
+ console.error("Unable to save workspace layout", error);
1306
1336
  });
1307
1337
  }, 1000);
1338
+ saveLayoutNow() {
1339
+ const write = this.#layoutSaveTail
1340
+ .catch(() => undefined)
1341
+ .then(async () => {
1342
+ await tick();
1343
+ await this.#workspaceHostProjection;
1344
+ if (!this.layoutReady || this.#layoutPersistenceDisposed)
1345
+ return;
1346
+ await this.app.vault.create(joinPath("/", appStatePath(this.workspaceLayoutFile)), JSON.stringify(this.toJson(), null, 2));
1347
+ });
1348
+ this.#layoutSaveTail = write;
1349
+ return write;
1350
+ }
1351
+ /** Await admitted App Shell transitions before the owning host tears down. */
1352
+ async flushPluginChanges() {
1353
+ await this.#workspaceHostController?.plugins.flushChanges();
1354
+ }
1355
+ /** Drain the single layout writer before a host closes, reloads or switches. */
1356
+ async flushLayout() {
1357
+ await tick();
1358
+ await this.#workspaceHostProjection;
1359
+ this.saveLayoutDebounced.cancel();
1360
+ await this.saveLayoutNow();
1361
+ }
1308
1362
  requestSaveLayout(event = { source: "api" }) {
1309
- if (this.suspendedLayoutPersistence > 0) {
1363
+ if (this.suspendedLayoutPersistence > 0 ||
1364
+ this.#layoutPersistenceDisposed) {
1310
1365
  return;
1311
1366
  }
1312
1367
  untrack(() => this.commitCompatibilityLayoutToHost(event));
1313
1368
  this.emitLayoutChangeAndScheduleSave(event);
1314
1369
  }
1315
1370
  emitLayoutChangeAndScheduleSave(event) {
1371
+ if (this.#layoutPersistenceDisposed)
1372
+ return;
1316
1373
  this.trigger("layout-change", event);
1317
1374
  this.saveLayoutDebounced();
1318
1375
  }
@@ -1730,23 +1787,44 @@ export class Workspace extends EventDispatcher {
1730
1787
  active: this.activeLeaf?.id,
1731
1788
  };
1732
1789
  }
1733
- async restoreLayoutJson(config) {
1734
- this.exitFocusMode();
1735
- const normalized = normalizeWorkspaceJson(config);
1790
+ async restoreLayoutJson(config, live = false, isCurrent = () => true) {
1791
+ if (!live)
1792
+ this.exitFocusMode();
1793
+ const normalized = normalizeWorkspaceJson(config, {
1794
+ preservePopouts: live,
1795
+ });
1736
1796
  await this.activateLayoutPlugins(normalized);
1737
- const projectionItems = beginWorkspaceProjection(this);
1797
+ if (!isCurrent())
1798
+ return;
1799
+ const projectionItems = beginWorkspaceProjection(this, isCurrent);
1738
1800
  try {
1739
1801
  await Promise.all([
1740
1802
  this.rootSplit.loadJson(normalized.main, projectionItems),
1741
1803
  this.leftSplit.loadJson(normalized.left, projectionItems),
1742
1804
  this.rightSplit.loadJson(normalized.right, projectionItems),
1743
1805
  this.bottomPanel.loadJson(normalized.bottom, projectionItems),
1744
- this.floating.loadJson(normalized.floating, projectionItems),
1806
+ this.floating.loadJson(normalized.floating, projectionItems, live
1807
+ ? (id) => {
1808
+ const handle = this.#workspaceHostController?.renderer.getPopoutHandle(id);
1809
+ return handle
1810
+ ? {
1811
+ win: handle.window,
1812
+ doc: handle.document,
1813
+ focus: () => handle.focus(),
1814
+ // The controller owns host removal; wrappers only project identity.
1815
+ close: () => { },
1816
+ onClose: () => () => { },
1817
+ }
1818
+ : null;
1819
+ }
1820
+ : undefined),
1745
1821
  ]);
1746
1822
  }
1747
1823
  finally {
1748
- finishWorkspaceProjection(projectionItems);
1824
+ finishWorkspaceProjection(projectionItems, this);
1749
1825
  }
1826
+ if (!isCurrent())
1827
+ return;
1750
1828
  if (!normalized.active) {
1751
1829
  if (!this.containsLeaf(this.activeLeaf))
1752
1830
  this.activeLeaf = null;
@@ -1768,7 +1846,7 @@ export class Workspace extends EventDispatcher {
1768
1846
  this.workspaceLayoutFile = file;
1769
1847
  return this.app.telemetry.measureAsync("workspace.load_layout", async (span) => {
1770
1848
  span.setAttribute("workspace.layout_file", file);
1771
- const workspaceFile = this.app.vault.getFileByPath(joinPath("/.obsidian", file));
1849
+ const workspaceFile = this.app.vault.getFileByPath(joinPath("/", appStatePath(file)));
1772
1850
  this.layoutReady = false;
1773
1851
  if (workspaceFile) {
1774
1852
  await this.restoreLayoutJson(JSON.parse(await this.app.vault.read(workspaceFile)));
@@ -1814,7 +1892,7 @@ export class Workspace extends EventDispatcher {
1814
1892
  await this.revealLeaf(rightLeaves[0]);
1815
1893
  }
1816
1894
  namedWorkspacesPath() {
1817
- return joinPath("/.obsidian", NAMED_WORKSPACES_FILE);
1895
+ return joinPath("/", appStatePath(NAMED_WORKSPACES_FILE));
1818
1896
  }
1819
1897
  async readNamedWorkspaceStore() {
1820
1898
  const file = this.app.vault.getFileByPath(this.namedWorkspacesPath());
@@ -1993,12 +2071,30 @@ export class Workspace extends EventDispatcher {
1993
2071
  values: Object.fromEntries(this.app.configuration.getConfiguration().entries()),
1994
2072
  }),
1995
2073
  save: async (snapshot) => {
2074
+ // Conditional hosts admit changes in bindConfiguration, before debounce.
2075
+ if (this.app.vault?.supportsConditionalTextWrites)
2076
+ return;
1996
2077
  await this.app.configuration.updateConfigurationOptions(snapshot.values);
1997
2078
  },
1998
2079
  },
1999
- plugins: createAppShellPluginPersistence(() => this.app.vault),
2080
+ plugins: createAppShellPluginPersistence(() => this.app.vault, this.app, async (enabled) => {
2081
+ const plugins = this.#workspaceHostController.plugins;
2082
+ for (const state of plugins.states) {
2083
+ if (state.required || typeof enabled[state.id] !== "boolean")
2084
+ continue;
2085
+ const success = enabled[state.id]
2086
+ ? await plugins.enable(state.id)
2087
+ : await plugins.disable(state.id);
2088
+ if (!success)
2089
+ throw new Error(`Could not restore App Shell plugin ${state.id}`);
2090
+ }
2091
+ }),
2000
2092
  },
2001
2093
  });
2094
+ this.#workspaceHostController.renderer.setCloseGuard(workspaceCloseGuard(this, () => this.#workspaceHostController.renderer.layout.windows.flatMap((win) => {
2095
+ const handle = this.#workspaceHostController.renderer.getPopoutHandle(win.id);
2096
+ return handle ? [handle.document] : [];
2097
+ })));
2002
2098
  setWorkspaceHostBinding(this, {
2003
2099
  controller: this.#workspaceHostController,
2004
2100
  });
@@ -2036,7 +2132,17 @@ export class Workspace extends EventDispatcher {
2036
2132
  });
2037
2133
  });
2038
2134
  }
2135
+ /** Register an owned opener for application-specific diagnostic resources. */
2136
+ registerDiagnosticOpener(opener) {
2137
+ this.#diagnosticOpeners.add(opener);
2138
+ return () => {
2139
+ this.#diagnosticOpeners.delete(opener);
2140
+ };
2141
+ }
2039
2142
  async openDiagnosticLocation(location) {
2143
+ for (const opener of [...this.#diagnosticOpeners])
2144
+ if (await opener(location))
2145
+ return;
2040
2146
  const path = pathFromDiagnosticResource(location.resource);
2041
2147
  if (!path)
2042
2148
  return;
@@ -2070,6 +2176,27 @@ export class Workspace extends EventDispatcher {
2070
2176
  const controller = this.#workspaceHostController;
2071
2177
  if (!controller)
2072
2178
  return;
2179
+ let projecting = false;
2180
+ registerWorkspaceFileSettings(controller.settings);
2181
+ const project = (values) => {
2182
+ projecting = true;
2183
+ try {
2184
+ controller.settings.changeSnapshot({ version: 1, values });
2185
+ }
2186
+ finally {
2187
+ projecting = false;
2188
+ }
2189
+ };
2190
+ const settingsRef = controller.settings.on("change", ({ id }) => {
2191
+ if (projecting || !this.app.vault?.supportsConditionalTextWrites)
2192
+ return;
2193
+ const { values } = controller.settings.getSnapshot();
2194
+ if (id && !Object.hasOwn(values, id))
2195
+ return;
2196
+ const change = this.app.configuration.updateConfigurationOptions(id ? { [id]: values[id] } : values);
2197
+ // The App-owned writer retains failures in the visible conflict registry.
2198
+ void change.catch(() => undefined);
2199
+ });
2073
2200
  const syncAll = () => {
2074
2201
  const snapshot = controller.settings.getSnapshot();
2075
2202
  const apiValues = Object.fromEntries(this.app.configuration.getConfiguration().entries());
@@ -2079,7 +2206,7 @@ export class Workspace extends EventDispatcher {
2079
2206
  next[key] = apiValues[key];
2080
2207
  }
2081
2208
  if (!isEqual(next, snapshot.values)) {
2082
- controller.settings.changeSnapshot({ version: 1, values: next });
2209
+ project(next);
2083
2210
  }
2084
2211
  };
2085
2212
  const configurationRef = this.app.configuration.on("updated", ({ key, value }) => {
@@ -2092,13 +2219,14 @@ export class Workspace extends EventDispatcher {
2092
2219
  else
2093
2220
  next[key] = value;
2094
2221
  if (!isEqual(next, snapshot.values)) {
2095
- controller.settings.changeSnapshot({ version: 1, values: next });
2222
+ project(next);
2096
2223
  }
2097
2224
  });
2098
2225
  const schemaRef = controller.settings.on("schema-change", syncAll);
2099
2226
  this.#configurationBridgeDisposer = () => {
2100
2227
  this.app.configuration.offref(configurationRef);
2101
2228
  controller.settings.offref(schemaRef);
2229
+ controller.settings.offref(settingsRef);
2102
2230
  };
2103
2231
  }
2104
2232
  /** @internal Connect the API plugin lifecycle to Design Core presentation. */
@@ -2130,11 +2258,13 @@ export class Workspace extends EventDispatcher {
2130
2258
  const loaded = manager.on("plugins-loaded", listener);
2131
2259
  const enabled = manager.on("plugin-enabled", listener);
2132
2260
  const disabled = manager.on("plugin-disabled", listener);
2261
+ const unloaded = manager.on("plugin-unloaded", listener);
2133
2262
  const error = manager.on("plugin-error", listener);
2134
2263
  return () => {
2135
2264
  manager.offref(loaded);
2136
2265
  manager.offref(enabled);
2137
2266
  manager.offref(disabled);
2267
+ manager.offref(unloaded);
2138
2268
  manager.offref(error);
2139
2269
  };
2140
2270
  },
@@ -2144,6 +2274,12 @@ export class Workspace extends EventDispatcher {
2144
2274
  }
2145
2275
  /** Dispose the API-owned shell controller and its compatibility bridges. */
2146
2276
  async disposeWorkspaceHost() {
2277
+ await this.#workspaceHostProjection;
2278
+ this.saveLayoutDebounced.cancel();
2279
+ await this.#layoutSaveTail;
2280
+ this.#layoutPersistenceDisposed = true;
2281
+ this.saveLayoutDebounced.cancel();
2282
+ this.app.icons?.dispose();
2147
2283
  this.app.languageServices.unbindDiagnostics();
2148
2284
  this.#configurationBridgeDisposer?.();
2149
2285
  this.#configurationBridgeDisposer = null;
@@ -2160,6 +2296,7 @@ export class Workspace extends EventDispatcher {
2160
2296
  this.#workspaceHostViewDisposers.forEach((dispose) => dispose());
2161
2297
  this.#workspaceHostViewDisposers.clear();
2162
2298
  await this.#workspaceHostController?.dispose();
2299
+ this.#diagnosticOpeners.clear();
2163
2300
  }
2164
2301
  installCommandBridge(controller) {
2165
2302
  const disposers = new Map();
@@ -2208,6 +2345,7 @@ export class Workspace extends EventDispatcher {
2208
2345
  projectStatusBarItem(controller, item, align) {
2209
2346
  controller.status.addItem({
2210
2347
  id: item.id,
2348
+ mount: item.mount,
2211
2349
  align,
2212
2350
  priority: item.priority,
2213
2351
  label: item.text,
@@ -2233,13 +2371,11 @@ export class Workspace extends EventDispatcher {
2233
2371
  for (const id of projectedIds)
2234
2372
  controller.status.removeItem(id);
2235
2373
  projectedIds.clear();
2236
- for (const item of this.app.statusBar.getVisibleItems("left", this.app.contextKeys)) {
2237
- projectedIds.add(item.id);
2238
- this.projectStatusBarItem(controller, item, "left");
2239
- }
2240
- for (const item of this.app.statusBar.getVisibleItems("right", this.app.contextKeys)) {
2241
- projectedIds.add(item.id);
2242
- this.projectStatusBarItem(controller, item, "right");
2374
+ for (const align of ["left", "center", "right"]) {
2375
+ for (const item of this.app.statusBar.getVisibleItems(align, this.app.contextKeys)) {
2376
+ projectedIds.add(item.id);
2377
+ this.projectStatusBarItem(controller, item, align);
2378
+ }
2243
2379
  }
2244
2380
  };
2245
2381
  const unsubscribeStatus = this.app.statusBar.subscribe(sync);
@@ -2290,9 +2426,19 @@ export class Workspace extends EventDispatcher {
2290
2426
  }
2291
2427
  };
2292
2428
  const changed = notifications.on("changed", sync);
2429
+ const notified = notifications.on("notify", (record) => {
2430
+ controller.notifications.notify({
2431
+ id: record.id,
2432
+ title: record.title,
2433
+ message: record.message,
2434
+ severity: record.severity,
2435
+ source: record.source,
2436
+ });
2437
+ });
2293
2438
  sync();
2294
2439
  this.#notificationProgressBridgeDisposer = () => {
2295
2440
  notifications.offref(changed);
2441
+ notifications.offref(notified);
2296
2442
  for (const handle of projected.values()) {
2297
2443
  handle.complete();
2298
2444
  }
@@ -2346,7 +2492,10 @@ export class Workspace extends EventDispatcher {
2346
2492
  });
2347
2493
  }
2348
2494
  enqueueWorkspaceHostProjection(layout, event) {
2349
- const projected = workspaceJsonForCompatibility(normalizeWorkspaceJson(layout));
2495
+ if (this.#layoutPersistenceDisposed)
2496
+ return;
2497
+ const mutationRevision = layoutMutationRevisions.get(this) ?? 0;
2498
+ const projected = workspaceJsonForCompatibility(normalizeWorkspaceJson(layout, { preservePopouts: true }));
2350
2499
  const preserveSidebarWidth = (side, current) => {
2351
2500
  const value = projected[side].width.trim();
2352
2501
  if (/^0(?:px|rem|em|%)?$/.test(value)) {
@@ -2359,13 +2508,26 @@ export class Workspace extends EventDispatcher {
2359
2508
  preserveSidebarWidth("left", this.leftSplit);
2360
2509
  preserveSidebarWidth("right", this.rightSplit);
2361
2510
  const run = this.#workspaceHostProjection.then(async () => {
2511
+ const isCurrent = () => mutationRevision === (layoutMutationRevisions.get(this) ?? 0);
2512
+ if (!isCurrent())
2513
+ return;
2362
2514
  await this.withoutLayoutPersistence(async () => {
2363
- await this.restoreLayoutJson(projected);
2515
+ await this.restoreLayoutJson(projected, true, isCurrent);
2516
+ if (!isCurrent())
2517
+ return;
2518
+ this.focusedHostId = this.getCommandHostIdForLeaf(this.activeLeaf);
2364
2519
  // Sidedock compatibility effects flush after loadJson resolves. Keep
2365
2520
  // persistence suppressed through that flush so an older queued
2366
2521
  // projection cannot be committed back over a newer controller layout.
2367
2522
  await tick();
2368
2523
  });
2524
+ if (!isCurrent()) {
2525
+ this.requestSaveLayout({
2526
+ source: "api",
2527
+ operation: "superseded-projection",
2528
+ });
2529
+ return;
2530
+ }
2369
2531
  this.emitLayoutChangeAndScheduleSave(compatibilityLayoutEventFromDesign(event));
2370
2532
  });
2371
2533
  this.#workspaceHostProjection = run.catch((error) => {
@@ -2487,13 +2649,27 @@ export class Workspace extends EventDispatcher {
2487
2649
  };
2488
2650
  },
2489
2651
  mount: (target, context) => {
2490
- const leaf = this.getLeafById(context.tab.id);
2491
- if (!leaf)
2492
- return;
2493
- target.replaceChildren(leaf.containerEl);
2652
+ let active = true;
2653
+ let attached = null;
2654
+ const attach = () => {
2655
+ if (!active)
2656
+ return;
2657
+ const leaf = this.getLeafById(context.tab.id);
2658
+ if (!leaf || leaf.containerEl.parentElement === target)
2659
+ return;
2660
+ attachWorkspaceView(target, leaf);
2661
+ attached = leaf;
2662
+ };
2663
+ attach();
2664
+ // Controller leaves can render before their compatibility projection.
2665
+ // Reattach the same retained view; do not create another lifecycle owner.
2666
+ void this.#workspaceHostProjection.then(attach).catch((error) => {
2667
+ console.error("Unable to attach projected workspace view", error);
2668
+ });
2494
2669
  return () => {
2495
- if (leaf.containerEl.parentElement === target) {
2496
- leaf.containerEl.remove();
2670
+ active = false;
2671
+ if (attached?.containerEl.parentElement === target) {
2672
+ attached.containerEl.remove();
2497
2673
  }
2498
2674
  };
2499
2675
  },
@@ -2565,6 +2741,15 @@ export class Workspace extends EventDispatcher {
2565
2741
  * @public
2566
2742
  */
2567
2743
  async revealLeaf(leaf) {
2744
+ const wasActive = this.activeLeaf === leaf;
2745
+ const revision = layoutMutationRevisions.get(this) ?? 0;
2746
+ await this.#workspaceHostProjection;
2747
+ if (revision !== (layoutMutationRevisions.get(this) ?? 0) ||
2748
+ !this.containsLeaf(leaf))
2749
+ return;
2750
+ recordLayoutMutation(this);
2751
+ if (wasActive)
2752
+ this.activeLeaf = leaf;
2568
2753
  let child = leaf;
2569
2754
  let parent = leaf.parent;
2570
2755
  while (parent) {
@@ -2587,12 +2772,22 @@ export class Workspace extends EventDispatcher {
2587
2772
  if (floatingWindow) {
2588
2773
  this.focusFloatingWindow(floatingWindow);
2589
2774
  }
2775
+ this.requestSaveLayout({ source: "api", operation: "reveal-leaf" });
2590
2776
  }
2591
2777
  registerView(type, viewCreator) {
2592
2778
  this.viewTypes.set(type, viewCreator);
2593
2779
  this.registerWorkspaceHostView(type);
2780
+ for (const listener of this.fileIconListeners)
2781
+ listener();
2594
2782
  }
2595
2783
  registerEditorView(contribution) {
2784
+ const existing = this.editorViews.get(contribution.id);
2785
+ if (existing?.source === "manifest" &&
2786
+ contribution.source === "plugin" &&
2787
+ existing.pluginId &&
2788
+ existing.pluginId === contribution.pluginId) {
2789
+ return this.editorViews.overlay(contribution);
2790
+ }
2596
2791
  return this.editorViews.register(contribution);
2597
2792
  }
2598
2793
  registerSidebarView(type, options = {}) {
@@ -2629,7 +2824,11 @@ export class Workspace extends EventDispatcher {
2629
2824
  unregisterView(type) {
2630
2825
  this.#workspaceHostViewDisposers.get(type)?.();
2631
2826
  this.#workspaceHostViewDisposers.delete(type);
2632
- return this.viewTypes.delete(type);
2827
+ const removed = this.viewTypes.delete(type);
2828
+ if (removed)
2829
+ for (const listener of this.fileIconListeners)
2830
+ listener();
2831
+ return removed;
2633
2832
  }
2634
2833
  normalizeExtensionKey(extension) {
2635
2834
  const normalized = extension.trim().toLowerCase();
@@ -2642,7 +2841,10 @@ export class Workspace extends EventDispatcher {
2642
2841
  if (!extensionKey) {
2643
2842
  return undefined;
2644
2843
  }
2645
- return this.viewExtensions.get(extensionKey);
2844
+ return ([...this.iconExtensions.values()]
2845
+ .reverse()
2846
+ .find((entry) => entry.keys.includes(extensionKey))?.viewType ??
2847
+ this.viewExtensions.get(extensionKey));
2646
2848
  }
2647
2849
  resolveViewTypeForPath(path) {
2648
2850
  const normalizedPath = path.trim().toLowerCase();
@@ -2669,13 +2871,14 @@ export class Workspace extends EventDispatcher {
2669
2871
  .replace(/[._-]+/g, " ")
2670
2872
  .replace(/\b\w/gu, (char) => char.toUpperCase());
2671
2873
  }
2672
- registeredEditorViewForPath(path) {
2874
+ registeredEditorViewForPath(path, metadataOnly = false) {
2673
2875
  return this.editorViews
2674
2876
  .getAll()
2675
2877
  .flatMap((view, viewIndex) => view.filenamePatterns
2676
2878
  .filter((pattern) => matchesEditorAssociationGlob(pattern, path))
2677
2879
  .map((pattern) => ({ view, viewIndex, pattern })))
2678
- .filter(({ view }) => this.viewTypes.has(view.viewType))
2880
+ .filter(({ view }) => this.viewTypes.has(view.viewType) ||
2881
+ (metadataOnly && view.source === "manifest"))
2679
2882
  .sort((left, right) => {
2680
2883
  const priority = { option: 0, default: 1, exclusive: 2 };
2681
2884
  const priorityDelta = priority[right.view.priority] - priority[left.view.priority];
@@ -2722,7 +2925,55 @@ export class Workspace extends EventDispatcher {
2722
2925
  }
2723
2926
  return this.registeredEditorViewForPath(path)?.viewType;
2724
2927
  }
2725
- registerExtensions(extensions, viewType) {
2928
+ /** Resolve presentation metadata without activating plugins or creating views. */
2929
+ getFileIconForPath(path) {
2930
+ const association = this.getEditorAssociationForPath(path)?.view;
2931
+ const eligible = association &&
2932
+ (this.viewTypes.has(association.viewType) ||
2933
+ association.source === "manifest");
2934
+ return (eligible ? association : this.registeredEditorViewForPath(path, true))?.fileIcon;
2935
+ }
2936
+ onFileIconsChanged(listener) {
2937
+ this.fileIconListeners.add(listener);
2938
+ const view = this.editorViews.on("changed", listener);
2939
+ const config = this.app.configuration.on("updated", ({ key }) => {
2940
+ if (key === "workspace.editorAssociations")
2941
+ listener();
2942
+ });
2943
+ return () => {
2944
+ this.fileIconListeners.delete(listener);
2945
+ this.editorViews.offref(view);
2946
+ this.app.configuration.offref(config);
2947
+ };
2948
+ }
2949
+ registerExtensions(extensions, viewType, options) {
2950
+ if (options) {
2951
+ const keys = [
2952
+ ...new Set(extensions
2953
+ .map((ext) => this.normalizeExtensionKey(ext))
2954
+ .filter(Boolean)),
2955
+ ];
2956
+ if (!keys.length)
2957
+ return () => { };
2958
+ const id = `compat:${viewType}:${++this.iconExtensionSequence}`;
2959
+ const dispose = this.editorViews.register({
2960
+ id,
2961
+ viewType,
2962
+ label: this.humanizeViewType(viewType),
2963
+ filenamePatterns: keys.map((key) => this.extensionKeyToFilenamePattern(key)),
2964
+ priority: "default",
2965
+ source: "compat",
2966
+ fileIcon: options.fileIcon,
2967
+ });
2968
+ const entry = { keys, viewType, dispose };
2969
+ this.iconExtensions.set(id, entry);
2970
+ return () => {
2971
+ if (this.iconExtensions.get(id) !== entry)
2972
+ return;
2973
+ this.iconExtensions.delete(id);
2974
+ dispose();
2975
+ };
2976
+ }
2726
2977
  const filenamePatterns = [];
2727
2978
  extensions.forEach((ext) => {
2728
2979
  const extensionKey = this.normalizeExtensionKey(ext);
@@ -2733,12 +2984,12 @@ export class Workspace extends EventDispatcher {
2733
2984
  filenamePatterns.push(this.extensionKeyToFilenamePattern(extensionKey));
2734
2985
  });
2735
2986
  if (filenamePatterns.length === 0) {
2736
- return;
2987
+ return () => { };
2737
2988
  }
2738
2989
  const existing = this.editorViews.getByViewType(viewType)[0];
2739
2990
  if (existing) {
2740
2991
  this.editorViews.update(existing.id, { filenamePatterns });
2741
- return;
2992
+ return () => this.unregisterExtensions(extensions, viewType);
2742
2993
  }
2743
2994
  this.editorViews.upsert({
2744
2995
  id: viewType,
@@ -2748,8 +2999,19 @@ export class Workspace extends EventDispatcher {
2748
2999
  priority: "default",
2749
3000
  source: "compat",
2750
3001
  });
3002
+ return () => this.unregisterExtensions(extensions, viewType);
2751
3003
  }
2752
3004
  unregisterExtensions(extensions, viewType) {
3005
+ const removed = new Set(extensions.map((ext) => this.normalizeExtensionKey(ext)));
3006
+ for (const [id, entry] of this.iconExtensions) {
3007
+ if (entry.viewType !== viewType)
3008
+ continue;
3009
+ entry.keys = entry.keys.filter((key) => !removed.has(key));
3010
+ if (!entry.keys.length) {
3011
+ this.iconExtensions.delete(id);
3012
+ entry.dispose();
3013
+ }
3014
+ }
2753
3015
  const filenamePatterns = [];
2754
3016
  extensions.forEach((ext) => {
2755
3017
  const extensionKey = this.normalizeExtensionKey(ext);
@@ -2900,6 +3162,11 @@ export class Workspace extends EventDispatcher {
2900
3162
  }
2901
3163
  return this.lastActiveFile;
2902
3164
  }
3165
+ /** Reconcile the remembered file through its workspace owner. */
3166
+ reconcileFileReference(oldPath, file) {
3167
+ if (this.lastActiveFile?.path === oldPath)
3168
+ this.lastActiveFile = file?.copy() ?? null;
3169
+ }
2903
3170
  /**
2904
3171
  * Iterate through all leaves, including main area leaves, floating leaves,
2905
3172
  * and sidebar leaves.
@@ -3023,6 +3290,8 @@ export class Workspace extends EventDispatcher {
3023
3290
  if (!this.containsLeaf(leaf)) {
3024
3291
  return false;
3025
3292
  }
3293
+ if (options.source !== "layout-load")
3294
+ recordLayoutMutation(this);
3026
3295
  const tabs = this.tabsForLeaf(leaf);
3027
3296
  if (leaf.parent instanceof WorkspaceSidebarGroup) {
3028
3297
  leaf.parent.selectLeaf(leaf);
@@ -3676,7 +3945,9 @@ export class Workspace extends EventDispatcher {
3676
3945
  this.app.workspace.activeLeaf = duplicateLeaf;
3677
3946
  this.app.workspace.requestSaveLayout({
3678
3947
  source: "api",
3679
- operation: leafType === "split" ? `duplicate-leaf-${direction}` : "duplicate-leaf-tab",
3948
+ operation: leafType === "split"
3949
+ ? `duplicate-leaf-${direction}`
3950
+ : "duplicate-leaf-tab",
3680
3951
  });
3681
3952
  return duplicateLeaf;
3682
3953
  });
@@ -3889,7 +4160,7 @@ export class OnDemandPluginInstallView extends FileView {
3889
4160
  function canReuseLeafForFileNavigation(leaf) {
3890
4161
  if (!leaf)
3891
4162
  return false;
3892
- return (leaf.view.getViewType() === "empty" || leaf.view instanceof FileView);
4163
+ return leaf.view.getViewType() === "empty" || leaf.view instanceof FileView;
3893
4164
  }
3894
4165
  function isPoorerViewState(incoming, live) {
3895
4166
  const next = incoming ?? {};
@@ -3900,6 +4171,25 @@ function isPoorerViewState(incoming, live) {
3900
4171
  return true;
3901
4172
  return liveEntries.some(([key]) => !(key in next));
3902
4173
  }
4174
+ function loadWorkspaceLeafJson(leaf, layout, isCurrent = () => true) {
4175
+ if (!isCurrent())
4176
+ return Promise.resolve();
4177
+ leaf.id = layout.id;
4178
+ const liveState = leaf.view.getState();
4179
+ if (leaf.view.getViewType() === layout.state.type &&
4180
+ (isEqual(liveState, layout.state.state) ||
4181
+ isPoorerViewState(layout.state.state, liveState))) {
4182
+ leaf.state = { type: layout.state.type, state: { ...liveState } };
4183
+ return Promise.resolve();
4184
+ }
4185
+ return leaf.setViewState({ type: layout.state.type, state: layout.state.state }, {
4186
+ history: true,
4187
+ layoutProjection: true,
4188
+ projectionIsCurrent: isCurrent,
4189
+ title: layout.state.title,
4190
+ icon: layout.state.icon,
4191
+ });
4192
+ }
3903
4193
  function collectLayoutViewTypes(node, target) {
3904
4194
  if (node.type === "split" || node.type === "floating") {
3905
4195
  for (const child of node.children) {
@@ -3914,8 +4204,8 @@ function collectLayoutViewTypes(node, target) {
3914
4204
  }
3915
4205
  }
3916
4206
  /**
3917
- * Compatibility stand-in for a Design Core class view. The host owns the
3918
- * live mount; this view only preserves type and chrome across API commits.
4207
+ * Compatibility stand-in for a Design Core class view. The host owns the live
4208
+ * mount; this view only preserves type and chrome across API commits.
3919
4209
  */
3920
4210
  class HostOwnedWorkspaceView extends View {
3921
4211
  #viewType;
@@ -4170,6 +4460,9 @@ export class WorkspaceLeaf extends WorkspaceItem {
4170
4460
  });
4171
4461
  }
4172
4462
  async setViewState(viewState, eState) {
4463
+ if (!eState?.layoutProjection)
4464
+ recordLayoutMutation(this.app.workspace);
4465
+ const isCurrent = eState?.projectionIsCurrent ?? (() => true);
4173
4466
  return this.app.telemetry.measureAsync("workspace.leaf.set_view_state", async (span) => {
4174
4467
  span.setAttribute("leaf.id", this.id);
4175
4468
  span.setAttribute("view.type", viewState.type);
@@ -4184,13 +4477,19 @@ export class WorkspaceLeaf extends WorkspaceItem {
4184
4477
  if (requestedViewType === ON_DEMAND_PLUGIN_INSTALL_VIEW_TYPE &&
4185
4478
  file instanceof TFile) {
4186
4479
  const promptView = await this.app.workspace.createOnDemandPluginInstallView(this, file, { confirm: false });
4480
+ if (!isCurrent())
4481
+ return;
4187
4482
  if (promptView) {
4188
- this.state = viewState;
4189
- this.pinned = !!viewState.pinned;
4190
4483
  const result = {
4191
4484
  history: eState?.history === true,
4192
4485
  };
4193
4486
  await promptView.setState({ ...(viewState.state || {}) });
4487
+ if (!isCurrent()) {
4488
+ promptView.unload();
4489
+ return;
4490
+ }
4491
+ this.state = viewState;
4492
+ this.pinned = !!viewState.pinned;
4194
4493
  await this.open(promptView, result, viewState);
4195
4494
  return;
4196
4495
  }
@@ -4198,6 +4497,8 @@ export class WorkspaceLeaf extends WorkspaceItem {
4198
4497
  if (eState?.activatePlugins !== false) {
4199
4498
  await this.app.plugins.activateForViewType(requestedViewType);
4200
4499
  }
4500
+ if (!isCurrent())
4501
+ return;
4201
4502
  let viewCreator = this.app.workspace.viewCreator(requestedViewType);
4202
4503
  let resolvedViewState = viewState;
4203
4504
  if (viewCreator && persistedMissingViewType) {
@@ -4238,11 +4539,17 @@ export class WorkspaceLeaf extends WorkspaceItem {
4238
4539
  this.app.workspace.viewCreator("empty") ||
4239
4540
  ((leaf) => new EmptyView(leaf));
4240
4541
  }
4241
- this.state = resolvedViewState;
4242
- this.pinned = !!viewState.pinned;
4243
4542
  const view = viewCreator(this);
4244
4543
  const result = { history: eState?.history === true };
4245
4544
  await view.setState({ ...(resolvedViewState.state || {}) });
4545
+ if (!isCurrent()) {
4546
+ view.unload();
4547
+ if (hasDestroyableEditor(view))
4548
+ view.editor.destroy();
4549
+ return;
4550
+ }
4551
+ this.state = resolvedViewState;
4552
+ this.pinned = !!viewState.pinned;
4246
4553
  await this.open(view, result, resolvedViewState);
4247
4554
  }, {
4248
4555
  attributes: {
@@ -4298,25 +4605,7 @@ export class WorkspaceLeaf extends WorkspaceItem {
4298
4605
  this.parent.removeChild(this);
4299
4606
  }
4300
4607
  loadJson(layout) {
4301
- this.id = layout.id;
4302
- const liveState = this.view.getState();
4303
- if (this.view.getViewType() === layout.state.type &&
4304
- (isEqual(liveState, layout.state.state) ||
4305
- isPoorerViewState(layout.state.state, liveState))) {
4306
- this.state = {
4307
- type: layout.state.type,
4308
- state: { ...liveState },
4309
- };
4310
- return Promise.resolve();
4311
- }
4312
- return this.setViewState({
4313
- type: layout.state.type,
4314
- state: layout.state.state,
4315
- }, {
4316
- history: true,
4317
- title: layout.state.title,
4318
- icon: layout.state.icon,
4319
- });
4608
+ return loadWorkspaceLeafJson(this, layout);
4320
4609
  }
4321
4610
  reload() {
4322
4611
  this.loadJson(this.toJson());