Skip to content

Commit 393141f

Browse files
committed
feat(vscode): show subfolders in Move/Duplicate into Vault picker
transferToVault() now enumerates subfolders via tracker.listFolders() and includes descendants of mounted folders in the QuickPick, falling back to mount roots only when the call fails. Assisted-by: Claude
1 parent 38f0b2e commit 393141f

3 files changed

Lines changed: 96 additions & 9 deletions

File tree

packages/vscode/package.json

Lines changed: 1 addition & 1 deletion
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": "1.2.0",
5+
"version": "1.2.1",
66
"private": true,
77
"publisher": "otaviof",
88
"engines": {

packages/vscode/src/commands.test.ts

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -834,7 +834,9 @@ function describeVaultTransferCommand(
834834

835835
vi.mocked(vscode.window.showQuickPick).mockResolvedValueOnce(undefined);
836836

837-
const tracker = mockLocalIndexTracker();
837+
const tracker = mockLocalIndexTracker({
838+
listFolders: vi.fn().mockResolvedValue({ ok: true, value: [] }),
839+
});
838840
const { handler } = invokeCommandWithUri(COMMAND[commandKey], tracker);
839841
await handler();
840842

@@ -858,7 +860,9 @@ function describeVaultTransferCommand(
858860
});
859861
vi.mocked(vscode.window.showInputBox).mockResolvedValueOnce(undefined);
860862

861-
const tracker = mockLocalIndexTracker();
863+
const tracker = mockLocalIndexTracker({
864+
listFolders: vi.fn().mockResolvedValue({ ok: true, value: [] }),
865+
});
862866
const { handler } = invokeCommandWithUri(COMMAND[commandKey], tracker);
863867
await handler();
864868

@@ -883,6 +887,7 @@ function describeVaultTransferCommand(
883887
vi.mocked(vscode.window.showInputBox).mockResolvedValueOnce("meeting.md");
884888

885889
const tracker = mockLocalIndexTracker({
890+
listFolders: vi.fn().mockResolvedValue({ ok: true, value: [] }),
886891
stat: vi.fn().mockResolvedValue({
887892
ok: true,
888893
value: { type: "file", mtime: 0, ctime: 0, size: 0 },
@@ -914,6 +919,7 @@ function describeVaultTransferCommand(
914919
vi.mocked(vscode.window.showInputBox).mockResolvedValueOnce("file.md");
915920

916921
const tracker = mockLocalIndexTracker({
922+
listFolders: vi.fn().mockResolvedValue({ ok: true, value: [] }),
917923
stat: vi.fn().mockResolvedValue({
918924
ok: false,
919925
error: { code: "FILE_NOT_FOUND", message: "not found" },
@@ -951,6 +957,7 @@ function describeVaultTransferCommand(
951957
vi.mocked(vscode.window.showInputBox).mockResolvedValueOnce("file.md");
952958

953959
const tracker = mockLocalIndexTracker({
960+
listFolders: vi.fn().mockResolvedValue({ ok: true, value: [] }),
954961
stat: vi.fn().mockResolvedValue({
955962
ok: false,
956963
error: { code: "FILE_NOT_FOUND", message: "not found" },
@@ -987,6 +994,7 @@ function describeVaultTransferCommand(
987994
vi.mocked(vscode.window.showInformationMessage).mockResolvedValueOnce(undefined);
988995

989996
const tracker = mockLocalIndexTracker({
997+
listFolders: vi.fn().mockResolvedValue({ ok: true, value: [] }),
990998
stat: vi.fn().mockResolvedValue({
991999
ok: false,
9921000
error: { code: "FILE_NOT_FOUND", message: "not found" },
@@ -1038,6 +1046,7 @@ function describeVaultTransferCommand(
10381046
);
10391047

10401048
const tracker = mockLocalIndexTracker({
1049+
listFolders: vi.fn().mockResolvedValue({ ok: true, value: [] }),
10411050
stat: vi.fn().mockResolvedValue({
10421051
ok: false,
10431052
error: { code: "FILE_NOT_FOUND", message: "not found" },
@@ -1072,6 +1081,7 @@ function describeVaultTransferCommand(
10721081
vi.mocked(vscode.window.showInformationMessage).mockResolvedValueOnce(undefined);
10731082

10741083
const tracker = mockLocalIndexTracker({
1084+
listFolders: vi.fn().mockResolvedValue({ ok: true, value: [] }),
10751085
stat: vi.fn().mockResolvedValue({
10761086
ok: false,
10771087
error: { code: "FILE_NOT_FOUND", message: "not found" },
@@ -1106,16 +1116,85 @@ function describeVaultTransferCommand(
11061116

11071117
vi.mocked(vscode.window.showQuickPick).mockResolvedValueOnce(undefined);
11081118

1109-
const tracker = mockLocalIndexTracker();
1119+
const tracker = mockLocalIndexTracker({
1120+
listFolders: vi.fn().mockResolvedValue({ ok: true, value: [] }),
1121+
});
11101122
const { handler } = invokeCommandWithUri(COMMAND[commandKey], tracker);
11111123
await handler();
11121124

11131125
expect(vscode.window.showQuickPick).toHaveBeenCalledWith(
11141126
[
1115-
{ label: "30-resources", description: "30-resources" },
11161127
{ label: "active", description: "10-projects/active" },
1128+
{ label: "30-resources", description: "30-resources" },
11171129
],
1118-
expect.objectContaining({ placeHolder: "Select destination folder" }),
1130+
expect.objectContaining({
1131+
placeHolder: "Select destination folder",
1132+
matchOnDescription: true,
1133+
}),
1134+
);
1135+
});
1136+
1137+
it("includes subfolders in QuickPick when listFolders succeeds", async () => {
1138+
mockReadAutoMount.mockReturnValue(["30-resources"]);
1139+
setupConfigMock();
1140+
1141+
Object.defineProperty(vscode.window, "activeTextEditor", {
1142+
value: { document: { uri: { scheme: "file", fsPath: "/project/file.md" } } },
1143+
writable: true,
1144+
configurable: true,
1145+
});
1146+
1147+
vi.mocked(vscode.window.showQuickPick).mockResolvedValueOnce(undefined);
1148+
1149+
const tracker = mockLocalIndexTracker({
1150+
listFolders: vi.fn().mockResolvedValue({
1151+
ok: true,
1152+
value: ["30-resources", "30-resources/books", "30-resources/papers", "10-projects"],
1153+
}),
1154+
});
1155+
const { handler } = invokeCommandWithUri(COMMAND[commandKey], tracker);
1156+
await handler();
1157+
1158+
expect(vscode.window.showQuickPick).toHaveBeenCalledWith(
1159+
[
1160+
{ label: "30-resources", description: "30-resources" },
1161+
{ label: "books", description: "30-resources/books" },
1162+
{ label: "papers", description: "30-resources/papers" },
1163+
],
1164+
expect.objectContaining({
1165+
placeHolder: "Select destination folder",
1166+
matchOnDescription: true,
1167+
}),
1168+
);
1169+
});
1170+
1171+
it("falls back to mounted folders when listFolders fails", async () => {
1172+
mockReadAutoMount.mockReturnValue(["30-resources"]);
1173+
setupConfigMock();
1174+
1175+
Object.defineProperty(vscode.window, "activeTextEditor", {
1176+
value: { document: { uri: { scheme: "file", fsPath: "/project/file.md" } } },
1177+
writable: true,
1178+
configurable: true,
1179+
});
1180+
1181+
vi.mocked(vscode.window.showQuickPick).mockResolvedValueOnce(undefined);
1182+
1183+
const tracker = mockLocalIndexTracker({
1184+
listFolders: vi.fn().mockResolvedValue({
1185+
ok: false,
1186+
error: { code: "CLI_ERROR", message: "failed" },
1187+
}),
1188+
});
1189+
const { handler } = invokeCommandWithUri(COMMAND[commandKey], tracker);
1190+
await handler();
1191+
1192+
expect(vscode.window.showQuickPick).toHaveBeenCalledWith(
1193+
[{ label: "30-resources", description: "30-resources" }],
1194+
expect.objectContaining({
1195+
placeHolder: "Select destination folder",
1196+
matchOnDescription: true,
1197+
}),
11191198
);
11201199
});
11211200
});

packages/vscode/src/commands.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -201,20 +201,28 @@ async function transferToVault(
201201
return;
202202
}
203203

204-
const folders = mountedFolders();
205-
if (folders.length === 0) {
204+
const mounts = mountedFolders();
205+
if (mounts.length === 0) {
206206
await vscode.window.showInformationMessage(
207207
"No mounted folders available. Mount a folder first.",
208208
);
209209
return;
210210
}
211211

212-
const items = folders.map((f) => ({
212+
let candidates = mounts;
213+
const foldersResult = await tracker.listFolders(readDepthLimit());
214+
if (foldersResult.ok) {
215+
const descendants = foldersResult.value.filter((f) => mounts.some((m) => f.startsWith(`${m}/`)));
216+
candidates = [...mounts, ...descendants].sort();
217+
}
218+
219+
const items = candidates.map((f) => ({
213220
label: f.split("/").pop()!,
214221
description: f,
215222
}));
216223
const picked = await vscode.window.showQuickPick(items, {
217224
placeHolder: "Select destination folder",
225+
matchOnDescription: true,
218226
});
219227
if (!picked) return;
220228

0 commit comments

Comments
 (0)