Folders are not considered “objects” in Obsidian. And since they are not files either, they won’t be listed by app.vault.getFiles()
. However, we can indirectly find (non-empty) subfolders by extracting the path of each found file, and use it to determine which files are located inside the current folder (perhaps several levels depth), and then keep the relevant part of the path as the name of the found subfolder.
The proposed strategy is to split each file.path
at the "/"
char, and store in a set all the names of the folders which are direct children of the current subfolder. Using a set avoids storing duplicates.
Note that, since folders are not objects, the final listing should not try to convert folder names into links (this would create links to non-existent notes).
Before a complete solution, let’s start with the case of the root folder, which is different because the path
property of a file in the root folder is ""
(instead of "/
"). this special case makes it easier to code a solution, which would be this:
// Files directly in root folder
// Files in root folder do not have "/" in the path
const filesHere = app.vault.getFiles()
.filter(f => !f.path.includes("/"))
.map(f => dv.fileLink(f.path));
// Subfolders of root folder
const subFolderSet = new Set();
for (const f of app.vault.getFiles()) {
if (!f.path.includes("/")) continue; // this is a regular file, skip
subFolderSet.add(f.path.split("/")[0]); // store the first part of the path
}
const subFolders = [...subFolderSet].sort();
const folderItems = subFolders.map(n => `📁 ${n}/`);
dv.list([...folderItems, ...filesHere]);
This produces for example:
So it works!
Now the case of non-root notes. For those, the folder
attribute contains somethin/like/this
. For the current note, this attribute gives us the “prefix” we will try to match in the .parent.path
of every file found by app.vault.getFiles()
. For each file, we remove that prefix from its file.path
, and keep the first part of the remaining string—skipping cases where the remaining does not contain any "/"
, because those will be regular files directly inside current folder.
The explanation is admittedly complex, but the code is short:
const parentFolder = dv.current().file.folder;
// Files directly in this folder
const filesHere = app.vault.getFiles()
.filter(f => f.parent.path === parentFolder)
.map(f => dv.fileLink(f.path));
// Subfolders directly in this folder
const subFolderSet = new Set();
const prefix = parentFolder + "/";
for (const f of app.vault.getFiles()) {
if (f.path.startsWith(prefix)) { // Inside this folder!
const rest = f.path.slice(prefix.length); // Remove the "prefix" part
if (!rest.includes("/")) continue; // it is a regular file, skip
subFolderSet.add(rest.split("/")[0]); // it is not, store it
}
}
const subFolders = [...subFolderSet].sort();
const folderItems = subFolders.map(n => `📁 ${n}/`);
dv.list([...folderItems, ...filesHere]);
This produces for example:
So it also works.
Final solution
I presented the root and non-root folder scripts separately, because I think the code would be easier to understand that way. But it is possible to merge both in a single code that works for both cases:
let parentFolder = dv.current().file.folder;
// Files directly in current folder
const filesHere = app.vault.getFiles()
.filter(f => parentFolder === "" ? !f.path.includes("/") : f.parent.path === parentFolder)
.map(f => dv.fileLink(f.path));
// Subfolders of current folder
const subFolderSet = new Set();
const prefix = parentFolder ? parentFolder + "/" : "";
for (const f of app.vault.getFiles()) {
if (parentFolder === "") { // deal differently in root folder
if (!f.path.includes("/")) continue; // this is a regular file, skip
subFolderSet.add(f.path.split("/")[0]); // store folder name
} else if (f.path.startsWith(prefix)) { // Not root folder, but in current tree
const rest = f.path.slice(prefix.length); // remove the prefix part
if (!rest.includes("/")) continue; // this is a regular file, skip
subFolderSet.add(rest.split("/")[0]); // store folder name
}
}
const subFolders = [...subFolderSet].sort();
const folderItems = subFolders.map(n => `📁 ${n}/`);
dv.list([...folderItems, ...filesHere]);