@lapis-notes/file-explorer 0.1.2 → 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.
package/CHANGELOG.md CHANGED
@@ -1,12 +1,127 @@
1
1
  # @lapis-notes/file-explorer
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`55be8dc`](https://github.com/lapismd/lapis-notes/commit/55be8dce6f7b19b0e8036be86a3cf5305db25317)
8
+ Thanks [@stevejuma](https://github.com/stevejuma)! - Let file-view plugins
9
+ contribute a fileIcon ID for their filename patterns or registered extensions.
10
+ Add App-owned SVG and icon-pack registration with plugin lifecycle cleanup,
11
+ preserve legacy icon registrations, and refresh Explorer artwork on editor,
12
+ plugin and file changes.
13
+
14
+ Release the scoped Design Core resolver first and raise the dependent minimum
15
+ ranges to the releases containing these contracts before publishing consumers.
16
+
17
+ Upgrade API-owned Zod to the patched package while retaining its public v3
18
+ schema entry for compatibility.
19
+
20
+ ### Patch Changes
21
+
22
+ - [`fbba8d4`](https://github.com/lapismd/lapis-notes/commit/fbba8d48498c34939fec475e6bed67a682045820)
23
+ Thanks [@stevejuma](https://github.com/stevejuma)! - Retain plugin-state and
24
+ compatibility-file drafts through close and conflict recovery, drain admitted
25
+ plugin/file operations before teardown, and preserve Properties mutations
26
+ after initial read failures.
27
+
28
+ - [`d8946b9`](https://github.com/lapismd/lapis-notes/commit/d8946b941569484b813eb0c1e8b1c5729fffc1a7)
29
+ Thanks [@stevejuma](https://github.com/stevejuma)! - Add owned
30
+ diagnostic-resource navigation and themed recovery-banner actions. Accept
31
+ Design Core 0.5.1 alongside the existing 0.4 range.
32
+
33
+ - [`023259d`](https://github.com/lapismd/lapis-notes/commit/023259d96d4ff0414fec6427924ed917f89405a3)
34
+ Thanks [@stevejuma](https://github.com/stevejuma)! - Add an optional exclusive
35
+ file-creation transport and route Explorer new-note actions through the
36
+ FileManager creation intent. Preserve existing destinations on collisions and
37
+ keep legacy Vault creation semantics explicit.
38
+
39
+ - [`717f0ff`](https://github.com/lapismd/lapis-notes/commit/717f0ff3956a5d33cba10311fca9c41a06e53b21)
40
+ Thanks [@stevejuma](https://github.com/stevejuma)! - Connect the existing
41
+ Explorer file-drop surface to FileManager new-file creation. Preserve binary
42
+ bytes and existing destinations, validate bounded input before reading it, and
43
+ retain App admission through asynchronous import and projection.
44
+
45
+ - Require released Design Core 0.6 for the close-guard, plugin-flush and
46
+ centered panel contracts used by current source. Retain portable registry
47
+ dependency ranges for the shared application runtime.
48
+
49
+ - [`bac3e5e`](https://github.com/lapismd/lapis-notes/commit/bac3e5eda6f467dbc86ec88c9f27a275f2b27953)
50
+ Thanks [@stevejuma](https://github.com/stevejuma)! - Expose optional
51
+ implemented system trash through the neutral native adapter with explicit
52
+ unsupported fallback and validated acknowledgements. Attribute Explorer
53
+ native/copy feedback to its owning App and reject unavailable clipboard
54
+ support without reporting success.
55
+
56
+ - [`501c50e`](https://github.com/lapismd/lapis-notes/commit/501c50e049264560e13e29c75a74c089922c7297)
57
+ Thanks [@stevejuma](https://github.com/stevejuma)! - Project owned App notices
58
+ through the existing workspace notification UI and retire their subscription
59
+ on host disposal. Pass the explicit Explorer App when reporting file-action
60
+ failures.
61
+
62
+ - [`98b333f`](https://github.com/lapismd/lapis-notes/commit/98b333f73ff5ba8d5f28c52e4e3a2ceaa5ade0fb)
63
+ Thanks [@stevejuma](https://github.com/stevejuma)! - Protect and flush
64
+ registered file writers before FileManager trash or deletion, including folder
65
+ descendants. Route Explorer deletion through that guard so failed writes
66
+ retain the file and dirty draft until explicit recovery.
67
+ - Updated dependencies
68
+ [[`bdd4923`](https://github.com/lapismd/lapis-notes/commit/bdd492357135de2cc0175fa42413709b2a38cd63),
69
+ [`86e85f6`](https://github.com/lapismd/lapis-notes/commit/86e85f6e814430c1755147c913eb91090a439bf4),
70
+ [`631aeec`](https://github.com/lapismd/lapis-notes/commit/631aeec539e2fb061dc80c5c3a30a248ee15c529),
71
+ [`fdee7e4`](https://github.com/lapismd/lapis-notes/commit/fdee7e4defde17a1009e16138d78c93c3ead8e2f),
72
+ [`a91d5e2`](https://github.com/lapismd/lapis-notes/commit/a91d5e2eb5ec037029f6a7a2df316a9bd9440af7),
73
+ [`fbba8d4`](https://github.com/lapismd/lapis-notes/commit/fbba8d48498c34939fec475e6bed67a682045820),
74
+ [`d8946b9`](https://github.com/lapismd/lapis-notes/commit/d8946b941569484b813eb0c1e8b1c5729fffc1a7),
75
+ [`ebf033a`](https://github.com/lapismd/lapis-notes/commit/ebf033af0fc0752be5ce5ffa7d55784163d291b4),
76
+ [`023259d`](https://github.com/lapismd/lapis-notes/commit/023259d96d4ff0414fec6427924ed917f89405a3),
77
+ [`63a5e7f`](https://github.com/lapismd/lapis-notes/commit/63a5e7fe40eea45762b5842b588c742076c7dd1d),
78
+ [`2bffe48`](https://github.com/lapismd/lapis-notes/commit/2bffe484b38761af197afe7ab9b0d7b3affb50d7),
79
+ [`86f6316`](https://github.com/lapismd/lapis-notes/commit/86f63166c3d5be648c595ef4e230ea6c4081bc48),
80
+ [`edf1d76`](https://github.com/lapismd/lapis-notes/commit/edf1d76f4f3aa0442c1d9153d812f29b7d14e5c2),
81
+ [`b3be91d`](https://github.com/lapismd/lapis-notes/commit/b3be91daecbd43187bacf8cfdc39f8311d700720),
82
+ [`5ef0a63`](https://github.com/lapismd/lapis-notes/commit/5ef0a6331c1b846b70357f990d919977a561112c),
83
+ [`bac3e5e`](https://github.com/lapismd/lapis-notes/commit/bac3e5eda6f467dbc86ec88c9f27a275f2b27953),
84
+ [`978b987`](https://github.com/lapismd/lapis-notes/commit/978b987ba4c95408a63e49fad11bc44bab020758),
85
+ [`ad4490c`](https://github.com/lapismd/lapis-notes/commit/ad4490ca1cd808002765582a17a5c81fa5335e0d),
86
+ [`74f544e`](https://github.com/lapismd/lapis-notes/commit/74f544e2aa8a967524c0c218f3db7e1a495a1a17),
87
+ [`55be8dc`](https://github.com/lapismd/lapis-notes/commit/55be8dce6f7b19b0e8036be86a3cf5305db25317),
88
+ [`501c50e`](https://github.com/lapismd/lapis-notes/commit/501c50e049264560e13e29c75a74c089922c7297),
89
+ [`98b333f`](https://github.com/lapismd/lapis-notes/commit/98b333f73ff5ba8d5f28c52e4e3a2ceaa5ade0fb),
90
+ [`535a6cc`](https://github.com/lapismd/lapis-notes/commit/535a6cc7b39f397cf8761a06db7b831386121928),
91
+ [`8a27bc3`](https://github.com/lapismd/lapis-notes/commit/8a27bc36a7a7fa81594e517908a6f5ed8ca77313),
92
+ [`109e208`](https://github.com/lapismd/lapis-notes/commit/109e208a63253dcc1a7abad3fd5a9b13c39d3def),
93
+ [`1e64589`](https://github.com/lapismd/lapis-notes/commit/1e6458922b486c07f36a5bc7d85d020b5805cd49),
94
+ [`f828eae`](https://github.com/lapismd/lapis-notes/commit/f828eaeba8411aa342d7ab4954393e84612af2f3)]:
95
+ - @lapis-notes/api@0.2.0
96
+
97
+ ## 0.1.3
98
+
99
+ ### Patch Changes
100
+
101
+ - [#36](https://github.com/lapismd/lapis-notes/pull/36)
102
+ [`17060a1`](https://github.com/lapismd/lapis-notes/commit/17060a1df152c9fc2302c93767cd7172868ff030)
103
+ Thanks [@stevejuma](https://github.com/stevejuma)! - Consume published Design
104
+ Core 0.5.0 with prepared browser CSS and bundled font assets through normal
105
+ npm ranges.
106
+ - Updated dependencies
107
+ [[`17060a1`](https://github.com/lapismd/lapis-notes/commit/17060a1df152c9fc2302c93767cd7172868ff030)]:
108
+ - @lapis-notes/api@0.1.5
109
+
3
110
  ## 0.1.2
4
111
 
5
112
  ### Patch Changes
6
113
 
7
- - [#4](https://github.com/lapismd/lapis-notes/pull/4) [`c1ad3c4`](https://github.com/lapismd/lapis-notes/commit/c1ad3c4acee2eac1ae87c22f66e946d2e0db3bf2) Thanks [@stevejuma](https://github.com/stevejuma)! - Require published Design Core 0.3.2 for native window-controls spacing, bold settings headings, selected-tab indicators and mobile shell teardown. Consume the npm release through portable semver ranges and lockfile integrity.
114
+ - [#4](https://github.com/lapismd/lapis-notes/pull/4)
115
+ [`c1ad3c4`](https://github.com/lapismd/lapis-notes/commit/c1ad3c4acee2eac1ae87c22f66e946d2e0db3bf2)
116
+ Thanks [@stevejuma](https://github.com/stevejuma)! - Require published Design
117
+ Core 0.3.2 for native window-controls spacing, bold settings headings,
118
+ selected-tab indicators and mobile shell teardown. Consume the npm release
119
+ through portable semver ranges and lockfile integrity.
8
120
 
9
- - Updated dependencies [[`39f886d`](https://github.com/lapismd/lapis-notes/commit/39f886d017a28d916b3f6cac7d08b7723a04f45b), [`c1ad3c4`](https://github.com/lapismd/lapis-notes/commit/c1ad3c4acee2eac1ae87c22f66e946d2e0db3bf2), [`1e949fc`](https://github.com/lapismd/lapis-notes/commit/1e949fc9d51b795a50760e1146a9599602746c6b)]:
121
+ - Updated dependencies
122
+ [[`39f886d`](https://github.com/lapismd/lapis-notes/commit/39f886d017a28d916b3f6cac7d08b7723a04f45b),
123
+ [`c1ad3c4`](https://github.com/lapismd/lapis-notes/commit/c1ad3c4acee2eac1ae87c22f66e946d2e0db3bf2),
124
+ [`1e949fc`](https://github.com/lapismd/lapis-notes/commit/1e949fc9d51b795a50760e1146a9599602746c6b)]:
10
125
  - @lapis-notes/api@0.1.2
11
126
 
12
127
  ## 0.1.1
package/README.md CHANGED
@@ -30,3 +30,9 @@ pnpm --filter @lapis-notes/file-explorer build
30
30
  pnpm --filter @lapis-notes/file-explorer check
31
31
  pnpm --filter @lapis-notes/file-explorer test
32
32
  ```
33
+
34
+ Plugin-contributed file icons resolve through the owning App's editor-view and
35
+ icon registries. Contributions can reference built-in names, registered custom
36
+ SVG IDs or icon-pack entries. Registration, plugin enablement, editor
37
+ associations and rename refresh rows while Design Core retains filename
38
+ fallback, row accessibility and rename presentation.
@@ -0,0 +1,18 @@
1
+ <script lang="ts">
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+
4
+ let {
5
+ svg,
6
+ class: className = "",
7
+ ...rest
8
+ }: { svg: string; class?: string } & HTMLAttributes<HTMLSpanElement> =
9
+ $props();
10
+ </script>
11
+
12
+ <span
13
+ {...rest}
14
+ class={["ui-workspace-icon", className].filter(Boolean).join(" ")}
15
+ data-ui-component="workspace-icon"
16
+ >
17
+ {@html svg}
18
+ </span>
@@ -0,0 +1,8 @@
1
+ import type { HTMLAttributes } from "svelte/elements";
2
+ type $$ComponentProps = {
3
+ svg: string;
4
+ class?: string;
5
+ } & HTMLAttributes<HTMLSpanElement>;
6
+ declare const ArtworkIcon: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type ArtworkIcon = ReturnType<typeof ArtworkIcon>;
8
+ export default ArtworkIcon;
@@ -0,0 +1,7 @@
1
+ import type { App } from "@lapis-notes/api";
2
+ /** Publish the selected file-view artwork through Design Core's scoped resolver. */
3
+ export declare function createExplorerFileIcons(app: App): {
4
+ nameForPath(path: string): string | undefined;
5
+ publish(): Promise<void>;
6
+ release(): void;
7
+ };
@@ -0,0 +1,55 @@
1
+ import { registerWorkspaceIcon } from "@lapismd/design-core/workspace/icon";
2
+ import ArtworkIcon from "./ArtworkIcon.svelte";
3
+ import { listExplorerVaultEntries } from "./explorer-vault-entries";
4
+ function artworkComponent(svg) {
5
+ const Icon = (anchor, props) => ArtworkIcon(anchor, { ...props, svg });
6
+ return Icon;
7
+ }
8
+ /** Publish the selected file-view artwork through Design Core's scoped resolver. */
9
+ export function createExplorerFileIcons(app) {
10
+ const disposers = new Map();
11
+ const published = new Map();
12
+ let generation = 0;
13
+ function names() {
14
+ return new Set(listExplorerVaultEntries(app.vault.getAllLoadedFiles()).flatMap((node) => {
15
+ if (node.kind !== "file")
16
+ return [];
17
+ const name = app.workspace.getFileIconForPath(node.path);
18
+ return name ? [name] : [];
19
+ }));
20
+ }
21
+ return {
22
+ nameForPath(path) {
23
+ const name = app.workspace.getFileIconForPath(path);
24
+ return name && published.has(name) ? name : undefined;
25
+ },
26
+ async publish() {
27
+ const current = ++generation;
28
+ const selected = names();
29
+ const artwork = await Promise.all([...selected].map(async (name) => [name, await app.icons.resolve(name)]));
30
+ if (current !== generation)
31
+ return;
32
+ for (const [name, svg] of artwork) {
33
+ if (!svg || published.get(name) === svg)
34
+ continue;
35
+ disposers.get(name)?.();
36
+ published.set(name, svg);
37
+ disposers.set(name, registerWorkspaceIcon(name, artworkComponent(svg)));
38
+ }
39
+ for (const name of [...disposers.keys()]) {
40
+ if (selected.has(name))
41
+ continue;
42
+ disposers.get(name)?.();
43
+ disposers.delete(name);
44
+ published.delete(name);
45
+ }
46
+ },
47
+ release() {
48
+ generation += 1;
49
+ for (const dispose of disposers.values())
50
+ dispose();
51
+ disposers.clear();
52
+ published.clear();
53
+ },
54
+ };
55
+ }
@@ -0,0 +1,3 @@
1
+ import type { App } from "@lapis-notes/api";
2
+ /** Reuse the Explorer drop surface and App-owned exclusive new-file intent. */
3
+ export declare function importExplorerFiles(app: App, folder: string, files: readonly File[]): Promise<void>;
@@ -0,0 +1,24 @@
1
+ const MAX_IMPORT_BYTES = 64 * 1024 * 1024;
2
+ /** Reuse the Explorer drop surface and App-owned exclusive new-file intent. */
3
+ export function importExplorerFiles(app, folder, files) {
4
+ return app.fileEditingSessions.track(async () => {
5
+ const imports = files.map((file) => {
6
+ const name = file.name;
7
+ if (!name || name === "." || name === ".." || /[\\/\0]/u.test(name))
8
+ throw new Error("Import requires a single valid filename");
9
+ if (!Number.isFinite(file.size) ||
10
+ file.size < 0 ||
11
+ file.size > MAX_IMPORT_BYTES)
12
+ throw new Error("Imported files must be at most 64 MiB");
13
+ return { file, path: folder ? `${folder}/${name}` : name };
14
+ });
15
+ for (const { file, path } of imports) {
16
+ const bytes = await file.arrayBuffer();
17
+ if (bytes.byteLength > MAX_IMPORT_BYTES)
18
+ throw new Error("Imported files must be at most 64 MiB");
19
+ await app.fileManager.createFile(path, bytes, {
20
+ mtime: file.lastModified,
21
+ });
22
+ }
23
+ });
24
+ }
package/dist/index.js CHANGED
@@ -1,17 +1,18 @@
1
1
  import { Notice, Plugin, TFile, View, basename, dirname, joinPath, } from "@lapis-notes/api";
2
- import { getWorkspaceHostBinding } from "@lapis-notes/api/workspace-host";
3
2
  import { ExplorerController } from "@lapismd/design-core/workspace/explorer";
4
3
  import { mount, unmount } from "svelte";
5
4
  import ExplorerPanel from "./LapisExplorerView.svelte";
6
5
  import LapisLanding from "./LapisLandingView.svelte";
7
6
  import { appendNativeExplorerMenu, copyExplorerText, } from "./native-explorer-actions";
8
7
  import { openExplorerFile } from "./open-explorer-file";
8
+ import { importExplorerFiles } from "./explorer-imports";
9
9
  import { DEFAULT_VAULT_PALETTE_FILE_EXTENSIONS, EXPLORER_SETTING_IDS, } from "./explorer-settings";
10
10
  import { listExplorerVaultEntries } from "./explorer-vault-entries";
11
- import { registerExplorerSettings } from "./register-explorer-settings";
11
+ import { readWorkspaceHostBinding, registerExplorerSettings, } from "./register-explorer-settings";
12
12
  import { subscribeExplorerVaultTreeChanges } from "./explorer-tree-subscription";
13
13
  import { listVaultPaletteFiles, VAULT_PALETTE_FILES_TAB, VAULT_PALETTE_RECENT_GROUP, } from "./vault-palette-files";
14
14
  import { bindExplorerSelectionNotifications } from "./explorer-selection";
15
+ import { createExplorerFileIcons } from "./explorer-file-icons";
15
16
  const EXPLORER_MANIFEST = {
16
17
  id: "lapis-file-explorer",
17
18
  name: "Explorer",
@@ -69,21 +70,37 @@ async function uniquePath(app, parent, requestedName) {
69
70
  return path;
70
71
  }
71
72
  }
72
- async function withNotice(operation) {
73
+ async function withNotice(app, operation) {
73
74
  try {
74
- return await operation();
75
+ return await (app.vault.supportsConditionalTextWrites
76
+ ? app.fileEditingSessions.track(operation)
77
+ : operation());
75
78
  }
76
79
  catch (error) {
77
- new Notice(displayError(error));
80
+ new Notice(displayError(error), undefined, app);
78
81
  throw error;
79
82
  }
80
83
  }
81
84
  function createExplorerController(app, loading) {
85
+ const fileIcons = createExplorerFileIcons(app);
82
86
  const controller = new ExplorerController({
83
87
  loading,
88
+ getIcon: (node) => node.kind === "file" ? fileIcons.nameForPath(node.path) : undefined,
84
89
  tree: {
85
90
  listEntries: () => listExplorerVaultEntries(app.vault.getAllLoadedFiles()),
86
- subscribe: (onChange) => subscribeExplorerVaultTreeChanges(app, onChange),
91
+ subscribe: (onChange) => {
92
+ const refreshIcons = () => {
93
+ void fileIcons.publish().then(onChange);
94
+ };
95
+ const stopTree = subscribeExplorerVaultTreeChanges(app, onChange);
96
+ const stopIcons = app.workspace.onFileIconsChanged(refreshIcons);
97
+ refreshIcons();
98
+ return () => {
99
+ stopTree();
100
+ stopIcons();
101
+ fileIcons.release();
102
+ };
103
+ },
87
104
  },
88
105
  selection: {
89
106
  subscribe: (onActivePath) => {
@@ -111,24 +128,25 @@ function createExplorerController(app, loading) {
111
128
  setShowHiddenFiles: (value) => app.configuration.updateConfigurationOption(EXPLORER_SETTING_IDS.showHiddenFiles, value),
112
129
  },
113
130
  actions: {
114
- openFile: (path, options) => withNotice(async () => {
131
+ importExternalFiles: (folder, files) => withNotice(app, () => importExplorerFiles(app, folder, files)),
132
+ openFile: (path, options) => withNotice(app, async () => {
115
133
  const file = app.vault.getFileByPath(path);
116
134
  if (!file)
117
135
  throw new Error(`Unable to find file: ${path}`);
118
136
  await openExplorerFile(app, file, options?.disposition ?? "current");
119
137
  }),
120
- createFile: (parent) => withNotice(async () => {
138
+ createFile: (parent) => withNotice(app, async () => {
121
139
  const path = await uniquePath(app, parent, "Untitled.md");
122
- const file = await app.vault.create(path, "");
140
+ const file = await app.fileManager.createFile(path, "");
123
141
  await app.openFile(file);
124
142
  return path;
125
143
  }),
126
- createFolder: (parent) => withNotice(async () => {
144
+ createFolder: (parent) => withNotice(app, async () => {
127
145
  const path = await uniquePath(app, parent, "New folder");
128
146
  await app.vault.createFolder(path);
129
147
  return path;
130
148
  }),
131
- rename: (path, nextName) => withNotice(async () => {
149
+ rename: (path, nextName) => withNotice(app, async () => {
132
150
  const file = app.vault.getAbstractFileByPath(path);
133
151
  if (!file)
134
152
  throw new Error(`Unable to find path: ${path}`);
@@ -139,7 +157,7 @@ function createExplorerController(app, loading) {
139
157
  await app.fileManager.renameFile(file, nextPath);
140
158
  return nextPath;
141
159
  }),
142
- move: (path, destinationFolderPath) => withNotice(async () => {
160
+ move: (path, destinationFolderPath) => withNotice(app, async () => {
143
161
  const file = app.vault.getAbstractFileByPath(path);
144
162
  if (!file)
145
163
  throw new Error(`Unable to find path: ${path}`);
@@ -150,13 +168,13 @@ function createExplorerController(app, loading) {
150
168
  await app.fileManager.renameFile(file, nextPath);
151
169
  return nextPath;
152
170
  }),
153
- delete: (path) => withNotice(async () => {
171
+ delete: (path) => withNotice(app, async () => {
154
172
  const file = app.vault.getAbstractFileByPath(path);
155
173
  if (!file)
156
174
  throw new Error(`Unable to find path: ${path}`);
157
- await app.vault.trash(file, false);
175
+ await app.fileManager.trashFile(file);
158
176
  }),
159
- copyText: copyExplorerText,
177
+ copyText: (label, value) => withNotice(app, () => copyExplorerText(app, label, value)),
160
178
  },
161
179
  buildItemMenu: (menu, node) => {
162
180
  appendNativeExplorerMenu(menu, node, app);
@@ -227,11 +245,11 @@ export class LapisLandingView extends View {
227
245
  id: "create-note",
228
246
  label: "Create new note",
229
247
  icon: "square-pen",
230
- onSelect: async () => {
248
+ onSelect: () => withNotice(this.app, async () => {
231
249
  const path = await uniquePath(this.app, "", "Untitled.md");
232
- const file = await this.app.vault.create(path, "");
250
+ const file = await this.app.fileManager.createFile(path, "");
233
251
  await this.app.openFile(file);
234
- },
252
+ }),
235
253
  },
236
254
  {
237
255
  id: "go-to-file",
@@ -277,7 +295,7 @@ export function createFileExplorerPlugin(options = {}) {
277
295
  this.register(() => {
278
296
  this.app.configuration.schema.unregister(EXPLORER_SCHEMA);
279
297
  });
280
- registerExplorerSettings(this);
298
+ const settingsRegistered = registerExplorerSettings(this);
281
299
  const configurationRef = this.app.configuration.on("updated", ({ key, value }) => {
282
300
  if (key !== EXPLORER_SETTING_IDS.showHiddenFiles)
283
301
  return;
@@ -321,32 +339,52 @@ export function createFileExplorerPlugin(options = {}) {
321
339
  void this.app.configuration.updateConfigurationOption(EXPLORER_SETTING_IDS.showHiddenFiles, !readShowHiddenFiles(this.app));
322
340
  },
323
341
  });
324
- const { controller } = getWorkspaceHostBinding(this.app.workspace);
325
- this.register(controller.commands.registerPaletteProvider({
326
- id: "lapis-vault-files",
327
- tab: VAULT_PALETTE_FILES_TAB,
328
- emptyQueryLimit: 5,
329
- search: (query) => {
330
- const starterFiles = query.trim().length === 0;
331
- const recentPaths = starterFiles
332
- ? new Set(this.app.workspace
333
- .getRecentFiles()
334
- .map((recent) => recent.path))
335
- : null;
336
- return listVaultPaletteFiles(this.app, query).map((file) => ({
337
- id: `vault-file:${file.path}`,
338
- title: file.name,
339
- subtitle: file.path,
340
- icon: "file",
341
- providerId: "lapis-vault-files",
342
- tab: VAULT_PALETTE_FILES_TAB.id,
343
- group: recentPaths?.has(file.path)
344
- ? VAULT_PALETTE_RECENT_GROUP
345
- : undefined,
346
- run: () => this.app.openFile(file),
347
- }));
348
- },
349
- }));
342
+ let paletteAttached = false;
343
+ let paletteDisposed = false;
344
+ this.register(() => {
345
+ paletteDisposed = true;
346
+ });
347
+ const attachVaultPalette = () => {
348
+ if (paletteDisposed || paletteAttached)
349
+ return;
350
+ const binding = readWorkspaceHostBinding(this.app.workspace);
351
+ if (!binding)
352
+ return;
353
+ paletteAttached = true;
354
+ this.register(binding.controller.commands.registerPaletteProvider({
355
+ id: "lapis-vault-files",
356
+ tab: VAULT_PALETTE_FILES_TAB,
357
+ emptyQueryLimit: 5,
358
+ search: (query) => {
359
+ const starterFiles = query.trim().length === 0;
360
+ const recentPaths = starterFiles
361
+ ? new Set(this.app.workspace
362
+ .getRecentFiles()
363
+ .map((recent) => recent.path))
364
+ : null;
365
+ return listVaultPaletteFiles(this.app, query).map((file) => ({
366
+ id: `vault-file:${file.path}`,
367
+ title: file.name,
368
+ subtitle: file.path,
369
+ icon: "file",
370
+ providerId: "lapis-vault-files",
371
+ tab: VAULT_PALETTE_FILES_TAB.id,
372
+ group: recentPaths?.has(file.path)
373
+ ? VAULT_PALETTE_RECENT_GROUP
374
+ : undefined,
375
+ run: () => this.app.openFile(file),
376
+ }));
377
+ },
378
+ }));
379
+ };
380
+ attachVaultPalette();
381
+ if (!paletteAttached || !settingsRegistered) {
382
+ this.app.workspace.onLayoutReady(() => {
383
+ if (!settingsRegistered)
384
+ registerExplorerSettings(this);
385
+ attachVaultPalette();
386
+ });
387
+ }
350
388
  await this.app.configuration.materializeSchemaDefaults();
351
389
  }
352
390
  syncShowHiddenFiles(value) {
@@ -4,5 +4,5 @@ import type { ExplorerNode } from "@lapismd/design-core/workspace/explorer";
4
4
  import type { WorkspaceMenu } from "@lapismd/design-core/workspace/core";
5
5
  export declare function hasNativeExplorerActions(adapter: App["vault"]["adapter"]): adapter is NativeDesktopVaultAdapter;
6
6
  export declare function revealInFileManagerLabel(os: NativeDesktopPlatformOs | undefined): string;
7
- export declare function copyExplorerText(label: string, value: string): Promise<void>;
7
+ export declare function copyExplorerText(app: App, label: string, value: string): Promise<void>;
8
8
  export declare function appendNativeExplorerMenu(menu: WorkspaceMenu, node: ExplorerNode, app: App): void;
@@ -11,11 +11,11 @@ export function revealInFileManagerLabel(os) {
11
11
  return "Reveal in File Explorer";
12
12
  return "Reveal in file manager";
13
13
  }
14
- export async function copyExplorerText(label, value) {
15
- if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
16
- await navigator.clipboard.writeText(value);
17
- }
18
- new Notice(`Copied ${label}`);
14
+ export async function copyExplorerText(app, label, value) {
15
+ if (typeof navigator === "undefined" || !navigator.clipboard?.writeText)
16
+ throw new Error("Clipboard is unavailable");
17
+ await navigator.clipboard.writeText(value);
18
+ new Notice(`Copied ${label}`, undefined, app);
19
19
  }
20
20
  function vaultPath(node) {
21
21
  return node.path === "/" ? "/" : node.path;
@@ -26,49 +26,52 @@ export function appendNativeExplorerMenu(menu, node, app) {
26
26
  return;
27
27
  const copyPath = menu.entries.find((entry) => entry.kind === "submenu" && entry.title === "Copy Path");
28
28
  if (copyPath?.kind === "submenu") {
29
- copyPath.menu.addItem((item) => item.setTitle("From system root").onClick(() => {
30
- void copySystemRootPath(adapter, node);
31
- }));
29
+ copyPath.menu.addItem((item) => item
30
+ .setTitle("From system root")
31
+ .onClick(() => copySystemRootPath(app, adapter, node)));
32
32
  if (node.kind === "file") {
33
33
  copyPath.menu.addItem((item) => item.setTitle("As Lapis URL").onClick(() => {
34
- void copyExplorerText("Lapis URL", createLapisFileUrl(app.vault.getName(), vaultPath(node)));
34
+ return copyExplorerText(app, "Lapis URL", createLapisFileUrl(app.vault.getName(), vaultPath(node))).catch((error) => {
35
+ report(app, error);
36
+ });
35
37
  }));
36
38
  }
37
39
  }
38
40
  menu.addSeparator();
39
- menu.addItem((item) => item.setTitle("Open in default app").onClick(() => {
40
- void openVaultPath(adapter, node);
41
- }));
41
+ menu.addItem((item) => item
42
+ .setTitle("Open in default app")
43
+ .onClick(() => openVaultPath(app, adapter, node)));
42
44
  menu.addItem((item) => item
43
45
  .setTitle(revealInFileManagerLabel(getNativeDesktopPlatform()?.os))
44
- .onClick(() => {
45
- void revealVaultPath(adapter, node);
46
- }));
46
+ .onClick(() => revealVaultPath(app, adapter, node)));
47
47
  }
48
- async function copySystemRootPath(adapter, node) {
48
+ async function copySystemRootPath(app, adapter, node) {
49
49
  try {
50
50
  const absolute = await resolveNativeDesktopVaultPath(adapter.rootPath, vaultPath(node));
51
51
  if (!absolute)
52
52
  throw new Error("Unable to resolve the system path");
53
- await copyExplorerText("system path", absolute);
53
+ await copyExplorerText(app, "system path", absolute);
54
54
  }
55
55
  catch (error) {
56
- new Notice(error instanceof Error ? error.message : String(error));
56
+ report(app, error);
57
57
  }
58
58
  }
59
- async function openVaultPath(adapter, node) {
59
+ async function openVaultPath(app, adapter, node) {
60
60
  try {
61
61
  await openNativeDesktopVaultPath(adapter.rootPath, vaultPath(node));
62
62
  }
63
63
  catch (error) {
64
- new Notice(error instanceof Error ? error.message : String(error));
64
+ report(app, error);
65
65
  }
66
66
  }
67
- async function revealVaultPath(adapter, node) {
67
+ async function revealVaultPath(app, adapter, node) {
68
68
  try {
69
69
  await revealNativeDesktopVaultPath(adapter.rootPath, vaultPath(node));
70
70
  }
71
71
  catch (error) {
72
- new Notice(error instanceof Error ? error.message : String(error));
72
+ report(app, error);
73
73
  }
74
74
  }
75
+ function report(app, error) {
76
+ new Notice(error instanceof Error ? error.message : String(error), undefined, app);
77
+ }
@@ -1,4 +1,5 @@
1
1
  import type { Plugin } from "@lapis-notes/api";
2
+ import { getWorkspaceHostBinding, type WorkspaceHostBinding } from "@lapis-notes/api/workspace-host";
2
3
  export declare function createExplorerSettingsSection(): {
3
4
  id: string;
4
5
  title: string;
@@ -27,7 +28,8 @@ export declare function createExplorerSettingsSection(): {
27
28
  itemType: "string";
28
29
  title: string;
29
30
  description: string;
30
- default: ("md" | "markdown" | "txt" | "text" | "json" | "data" | "yaml" | "yml")[];
31
+ default: ("text" | "md" | "markdown" | "txt" | "json" | "data" | "yaml" | "yml")[];
31
32
  })[];
32
33
  };
33
- export declare function registerExplorerSettings(plugin: Plugin): void;
34
+ export declare function readWorkspaceHostBinding(workspace: Parameters<typeof getWorkspaceHostBinding>[0]): WorkspaceHostBinding | null;
35
+ export declare function registerExplorerSettings(plugin: Plugin): boolean;
@@ -1,4 +1,4 @@
1
- import { getWorkspaceHostBinding } from "@lapis-notes/api/workspace-host";
1
+ import { getWorkspaceHostBinding, } from "@lapis-notes/api/workspace-host";
2
2
  import { DEFAULT_VAULT_PALETTE_FILE_EXTENSIONS, EXPLORER_SETTING_IDS, EXPLORER_SETTINGS_SECTION_ID, } from "./explorer-settings";
3
3
  export function createExplorerSettingsSection() {
4
4
  return {
@@ -35,11 +35,24 @@ export function createExplorerSettingsSection() {
35
35
  ],
36
36
  };
37
37
  }
38
+ export function readWorkspaceHostBinding(workspace) {
39
+ try {
40
+ return getWorkspaceHostBinding(workspace);
41
+ }
42
+ catch (error) {
43
+ if (error instanceof Error &&
44
+ error.message === "Workspace host binding is not initialized") {
45
+ return null;
46
+ }
47
+ throw error;
48
+ }
49
+ }
38
50
  export function registerExplorerSettings(plugin) {
39
51
  if (!plugin.app.workspace)
40
- return;
41
- const binding = getWorkspaceHostBinding(plugin.app.workspace);
52
+ return false;
53
+ const binding = readWorkspaceHostBinding(plugin.app.workspace);
42
54
  if (!binding)
43
- return;
55
+ return false;
44
56
  plugin.register(binding.controller.registerSettingsSection(createExplorerSettingsSection()));
57
+ return true;
45
58
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lapis-notes/file-explorer",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Reusable Lapis Notes file explorer plugin",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -38,8 +38,8 @@
38
38
  }
39
39
  },
40
40
  "dependencies": {
41
- "@lapis-notes/api": "^0.1.2",
42
- "@lapismd/design-core": "^0.3.2"
41
+ "@lapis-notes/api": "^0.2.0",
42
+ "@lapismd/design-core": "^0.7.0"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "svelte": "^5.0.0"