Skip to content

Commit a703151

Browse files
committed
fix(vscode): preserve TreeView when dragged between Explorer and sidebar
The view disappeared when moved between containers because the codicon icon `$(circuit-board)` cannot render in the activity bar, and the `when` clause prevented VSCode from re-rendering the view after a container move. Replace the codicon with an SVG file, remove the `when` clause entirely, and control the `obsidianVFS.explorer` setting via a tree-provider `enabled` property instead of a context key. Add an `onDidChangeVisibility` handler to refresh tree data on container transitions. Move icon assets into a `resources/` directory. Assisted-by: Claude
1 parent 2fa4cbb commit a703151

11 files changed

Lines changed: 131 additions & 34 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<p align="center">
2-
<img src="packages/vscode/obsidian-vfs.png" alt="Obsidian VFS" width="200" />
2+
<img src="packages/vscode/resources/obsidian-vfs.png" alt="Obsidian VFS" width="200" />
33
<br>
44
<br>
55
<a href="https://www.npmjs.com/package/@obsidian-vfs/core"><img src="https://img.shields.io/npm/v/@obsidian-vfs/core?label=%40obsidian-vfs%2Fcore" alt="@obsidian-vfs/core"></a>

packages/vscode/.vscodeignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
!dist/extension.js
33
!dist/extension.js.map
44
!package.json
5-
!obsidian-vfs.png
5+
!resources/obsidian-vfs.png
6+
!resources/obsidian-vfs.svg
67
!README.md

packages/vscode/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<p align="center">
2-
<img src="https://raw.githubusercontent.com/otaviof/obsidian-vfs/refs/heads/main/packages/vscode/obsidian-vfs.png" alt="Obsidian VFS" width="200" />
2+
<img src="https://raw.githubusercontent.com/otaviof/obsidian-vfs/refs/heads/main/packages/vscode/resources/obsidian-vfs.png" alt="Obsidian VFS" width="200" />
33
<br>
44
</p>
55

packages/vscode/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "obsidian-vfs",
33
"displayName": "Obsidian VFS",
44
"description": "Browse, search, and edit your Obsidian vault directly in VSCode via a virtual file system (obs://)",
5-
"version": "0.2.0",
5+
"version": "0.2.1",
66
"private": true,
77
"publisher": "otaviof",
88
"engines": {
@@ -28,7 +28,7 @@
2828
"url": "https://github.com/otaviof/obsidian-vfs",
2929
"directory": "packages/vscode"
3030
},
31-
"icon": "obsidian-vfs.png",
31+
"icon": "resources/obsidian-vfs.png",
3232
"main": "./dist/extension.js",
3333
"contributes": {
3434
"commands": [
@@ -67,9 +67,9 @@
6767
"views": {
6868
"explorer": [
6969
{
70+
"icon": "resources/obsidian-vfs.svg",
7071
"id": "obsidianVFS",
71-
"name": "Obsidian VFS",
72-
"when": "obsidianVFS.active && obsidianVFS.explorerEnabled"
72+
"name": "Obsidian VFS"
7373
}
7474
]
7575
},
File renamed without changes.
Lines changed: 3 additions & 0 deletions
Loading

packages/vscode/src/extension.test.ts

Lines changed: 58 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -166,11 +166,6 @@ describe("activate", () => {
166166
title: string;
167167
};
168168
expect(treeView.title).toBe(`${FOLDER_NAME_PREFIX}MyVault`);
169-
expect(vscode.commands.executeCommand).toHaveBeenCalledWith(
170-
"setContext",
171-
"obsidianVFS.active",
172-
true,
173-
);
174169
expect(registerCommands).toHaveBeenCalledWith(
175170
ctx,
176171
fakeTracker,
@@ -373,7 +368,45 @@ describe("activate", () => {
373368
expect(statusBarInstance.show).toHaveBeenCalled();
374369
});
375370

376-
it("does not set explorerEnabled context when explorer is false", async () => {
371+
it("refreshes tree provider when view becomes visible", async () => {
372+
const fakeTracker = {
373+
context: {
374+
name: "MyVault",
375+
physicalPath: "/vault",
376+
mode: "full",
377+
vfsConfig: { agents: [], skills: [], allowed: [], blocked: [] },
378+
},
379+
} as unknown as LocalIndexTracker;
380+
381+
mockBootstrap.mockResolvedValueOnce({
382+
ok: true,
383+
value: { tracker: fakeTracker, initMs: 42 },
384+
});
385+
386+
await activate(fakeContext() as never);
387+
388+
const treeView = vi.mocked(vscode.window.createTreeView).mock.results[0].value as {
389+
onDidChangeVisibility: ReturnType<typeof vi.fn>;
390+
};
391+
expect(treeView.onDidChangeVisibility).toHaveBeenCalledWith(expect.any(Function));
392+
393+
const treeProviderInstance = vi.mocked(VaultTreeDataProvider).mock.results[0].value as {
394+
refresh: ReturnType<typeof vi.fn>;
395+
};
396+
397+
const visibilityCallback = treeView.onDidChangeVisibility.mock.calls[0][0] as (e: {
398+
visible: boolean;
399+
}) => void;
400+
401+
visibilityCallback({ visible: true });
402+
expect(treeProviderInstance.refresh).toHaveBeenCalled();
403+
404+
treeProviderInstance.refresh.mockClear();
405+
visibilityCallback({ visible: false });
406+
expect(treeProviderInstance.refresh).not.toHaveBeenCalled();
407+
});
408+
409+
it("disables tree provider when explorer is false", async () => {
377410
const fakeTracker = {
378411
context: {
379412
name: "MyVault",
@@ -398,12 +431,13 @@ describe("activate", () => {
398431

399432
await activate(fakeContext() as never);
400433

401-
const executeCommand = vi.mocked(vscode.commands.executeCommand);
402-
expect(executeCommand).toHaveBeenCalledWith("setContext", "obsidianVFS.active", true);
403-
expect(executeCommand).toHaveBeenCalledWith("setContext", "obsidianVFS.explorerEnabled", false);
434+
const treeProviderInstance = vi.mocked(VaultTreeDataProvider).mock.results[0].value as {
435+
enabled: boolean;
436+
};
437+
expect(treeProviderInstance.enabled).toBe(false);
404438
});
405439

406-
it("sets explorerEnabled context when explorer is true", async () => {
440+
it("keeps tree provider enabled when explorer is true", async () => {
407441
const fakeTracker = {
408442
context: {
409443
name: "MyVault",
@@ -428,9 +462,10 @@ describe("activate", () => {
428462

429463
await activate(fakeContext() as never);
430464

431-
const executeCommand = vi.mocked(vscode.commands.executeCommand);
432-
expect(executeCommand).toHaveBeenCalledWith("setContext", "obsidianVFS.active", true);
433-
expect(executeCommand).toHaveBeenCalledWith("setContext", "obsidianVFS.explorerEnabled", true);
465+
const treeProviderInstance = vi.mocked(VaultTreeDataProvider).mock.results[0].value as {
466+
enabled: boolean;
467+
};
468+
expect(treeProviderInstance.enabled).toBe(true);
434469
});
435470
});
436471

@@ -460,7 +495,7 @@ describe("configuration change listener", () => {
460495
expect(vscode.workspace.onDidChangeConfiguration).toHaveBeenCalled();
461496
});
462497

463-
it("updates explorerEnabled context when explorer config changes to true", async () => {
498+
it("enables tree provider when explorer config changes to true", async () => {
464499
const fakeTracker = {
465500
context: {
466501
name: "MyVault",
@@ -498,11 +533,13 @@ describe("configuration change listener", () => {
498533

499534
configChangeListener!({ affectsConfiguration: (key) => key === "obsidianVFS.explorer" });
500535

501-
const executeCommand = vi.mocked(vscode.commands.executeCommand);
502-
expect(executeCommand).toHaveBeenCalledWith("setContext", "obsidianVFS.explorerEnabled", true);
536+
const treeProviderInstance = vi.mocked(VaultTreeDataProvider).mock.results[0].value as {
537+
enabled: boolean;
538+
};
539+
expect(treeProviderInstance.enabled).toBe(true);
503540
});
504541

505-
it("updates explorerEnabled context when explorer config changes to false", async () => {
542+
it("disables tree provider when explorer config changes to false", async () => {
506543
const fakeTracker = {
507544
context: {
508545
name: "MyVault",
@@ -538,8 +575,10 @@ describe("configuration change listener", () => {
538575

539576
configChangeListener!({ affectsConfiguration: (key) => key === "obsidianVFS.explorer" });
540577

541-
const executeCommand = vi.mocked(vscode.commands.executeCommand);
542-
expect(executeCommand).toHaveBeenCalledWith("setContext", "obsidianVFS.explorerEnabled", false);
578+
const treeProviderInstance = vi.mocked(VaultTreeDataProvider).mock.results[0].value as {
579+
enabled: boolean;
580+
};
581+
expect(treeProviderInstance.enabled).toBe(false);
543582
});
544583

545584
it("shows status bar when statusBar config changes to true", async () => {

packages/vscode/src/extension.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,13 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
5656
cfg.get<string>("treeViewTitle", "") || `${FOLDER_NAME_PREFIX}${tracker.context.name}`;
5757
context.subscriptions.push(treeView);
5858

59-
await vscode.commands.executeCommand("setContext", "obsidianVFS.active", true);
59+
context.subscriptions.push(
60+
treeView.onDidChangeVisibility((e) => {
61+
if (e.visible) {
62+
treeProvider.refresh();
63+
}
64+
}),
65+
);
6066

6167
registerCommands(context, tracker, treeProvider, outputChannel);
6268

@@ -74,7 +80,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
7480
),
7581
);
7682

77-
await vscode.commands.executeCommand("setContext", "obsidianVFS.explorerEnabled", config.explorer);
83+
treeProvider.enabled = config.explorer;
7884
if (config.statusBar) {
7985
statusBar.show();
8086
}
@@ -94,11 +100,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
94100
vscode.workspace.onDidChangeConfiguration((e) => {
95101
const updated = readConfig();
96102
if (e.affectsConfiguration("obsidianVFS.explorer")) {
97-
void vscode.commands.executeCommand(
98-
"setContext",
99-
"obsidianVFS.explorerEnabled",
100-
updated.explorer,
101-
);
103+
treeProvider.enabled = updated.explorer;
102104
}
103105
if (e.affectsConfiguration("obsidianVFS.statusBar")) {
104106
if (updated.statusBar) {

packages/vscode/src/test-mocks.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,10 @@ export function createVscodeMock(
117117
mock.StatusBarAlignment = { Left: 1, Right: 2 };
118118
}
119119
if (parts.treeView) {
120-
const treeView = { dispose: vi.fn() };
120+
const treeView = {
121+
dispose: vi.fn(),
122+
onDidChangeVisibility: vi.fn(() => ({ dispose: vi.fn() })),
123+
};
121124
(mock.window as Record<string, unknown>).createTreeView = vi.fn(() => treeView);
122125
}
123126
}

packages/vscode/src/vault-tree-provider.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,46 @@ describe("VaultTreeDataProvider", () => {
218218
provider.dispose();
219219
});
220220

221+
it("returns empty tree when disabled", async () => {
222+
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
223+
get: vi.fn((_key: string, defaultValue: unknown) => {
224+
if (_key === "autoMount") return ["10-projects"];
225+
return defaultValue;
226+
}),
227+
} as never);
228+
229+
const tracker = mockTracker({
230+
stat: vi.fn().mockResolvedValue({
231+
ok: true,
232+
value: { type: "directory", mtime: 0, ctime: 0, size: 0 },
233+
}),
234+
});
235+
const provider = new VaultTreeDataProvider(tracker);
236+
237+
provider.enabled = false;
238+
const children = await provider.getChildren();
239+
expect(children).toEqual([]);
240+
241+
provider.dispose();
242+
});
243+
244+
it("refreshes when enabled changes", () => {
245+
const tracker = mockTracker();
246+
const provider = new VaultTreeDataProvider(tracker);
247+
248+
const listener = vi.fn();
249+
provider.onDidChangeTreeData(listener);
250+
251+
provider.enabled = false;
252+
expect(listener).toHaveBeenCalledTimes(1);
253+
254+
listener.mockClear();
255+
provider.enabled = false;
256+
expect(listener).not.toHaveBeenCalled();
257+
258+
provider.dispose();
259+
});
260+
221261
it("getTreeItem returns the element itself", () => {
222262
const tracker = mockTracker();
223263
const provider = new VaultTreeDataProvider(tracker);

0 commit comments

Comments
 (0)