List everything in a folder with dataview/dataviewjs?

What I’m trying to do

This may seem simple, but I just want to use dataview (or dataviewjs) to list every file and folder in the folder containing the current note. That is, I’d like a “folder note” but without other plugins.

Example structure:

test folder/

subfolder A
subfolder B
test folder.md
file1.md
file2.pdf
file3.png

I’d like a dataview (or dataviewjs) query that I can put in test folder.md that will produce:

  • subfolder A
  • subfolder B
  • file1
  • file2
  • file3

Things I have tried

The nearest I’ve gotten is this:

let parentFolder = dv.current().file.folder
let thispath = dv.current().file.path

if (parentFolder == "")
  parentFolder = "/"
  
let lsFolder = app.vault.getFiles()
  .filter(file => file.parent.path == parentFolder)
  .filter(file => file.path != thispath)
  .sort((a, b) => a.name.localeCompare(b.name))
  .map(file => dv.fileLink(file.path))
dv.list(lsFolder)

But this query only shows files and not the subfolders.
I’ve tried all kinds of variations of filter to get both files and subfolders, but nothing works.

Can someone please help?

Does this thread help?

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]);
3 Likes

Oh! Well, that explains it. The missing piece was that folders aren’t objects. I come from a Unix background where anything in a folder is an object.

Thanks very much for the detailed explanation!

@JLDiaz - one more question, if you can spare a moment.

Your “final solution” query works great, except in one case: If a subfolder has a file in it that has the same same as the subfolder, then I’d like it to show up as a clickable item in folderItems.

Sample structure:

currentFolder/
   subFolder/
      subFolder.md

I’d like “subFolder” to show up as a clickable link in the query variable folderItems that takes me to currentFolder/subFolder/subFolder.md.

I think something would have to change in the else clause of your query, but my javascript isn’t strong enough to do it.

Can you help?

Making that clickable element conditionally, depending on whether the file exists in the folder, is tricky. However, creating it unconditionally is simple, and perhaps even more useful. This is the code (I refactored it slightly and added a filter to exclude the current note, as per your initial specifications.). The part related to this new behaviour is the second-to-last line.

const currentFile = dv.current().file
const parentFolder = currentFile.folder;
const allFiles = app.vault.getFiles();

// Files directly in current folder
const filesHere = allFiles
  .filter(f => parentFolder === "" ? !f.path.includes("/") : f.parent.path === parentFolder)
  .filter(f => f.path != currentFile.path)
  .map(f => dv.fileLink(f.path));

// Subfolders of current folder

const subFolderSet = new Set();
const prefix = parentFolder ? parentFolder + "/" : "";
for (const f of allFiles) {
  if (parentFolder === "") {
    if (!f.path.includes("/")) continue;
    subFolderSet.add(f.path.split("/")[0]);
  } else if (f.path.startsWith(prefix)) {
    const rest = f.path.slice(prefix.length);
    if (!rest.includes("/")) continue;
    subFolderSet.add(rest.split("/")[0]);
  }
}

const subFolders = [...subFolderSet].sort();
const folderItems = subFolders.map(n => dv.fileLink(`${n}/${n}`, false, `📁 ${n}`));
dv.list([...folderItems, ...filesHere]);

This way every folder is clickable. If a note with the same name as the folder exists inside it, the link leads to that note. If it does not exist, the link leads to the creation of that note. Visually, all links have the same aspect, whether the target exists or not, because the html generated by dataview does not insert the appropriate class (is-unresolved) for the non-existing elements. That’s unfortunate, but you can detect which ones do not exist by hovering (+Ctrl) the link.

Final version

I coded a solution in which unresolved links to folder notes have the proper style/color.

This solution uses dataview custom views, which has also the advantage of reducing the clutter in the notes, since the code to be inserted is now minimal. This is the snippet required in folder notes:

```dataviewjs
await dv.view("Misc/Views/folder_list"); 
```

Setup

You need to create a folder in your vault to store the files view.js and view.css. In my case I used the folder Misc/Views/folder_list. If you use a different one you’ll have to adjust the above snippet.

Once that folder is created, you have to put there the following two files:

view.js

function folderList() {
    const currentFile = dv.current().file
    const parentFolder = currentFile.folder;
    const allFiles = app.vault.getFiles();

    // Files directly in current folder
    const filesHere = allFiles
        .filter(f => parentFolder === "" ? !f.path.includes("/") : f.parent.path === parentFolder)
        .filter(f => f.path != currentFile.path)
        .map(f => dv.fileLink(f.path));

    // Subfolders of current folder

    const subFolderSet = new Set();
    const prefix = parentFolder ? parentFolder + "/" : "";
    for (const f of allFiles) {
        if (parentFolder === "") {
            if (!f.path.includes("/")) continue;
            subFolderSet.add(f.path.split("/")[0]);
        } else if (f.path.startsWith(prefix)) {
            const rest = f.path.slice(prefix.length);
            if (!rest.includes("/")) continue;
            subFolderSet.add(rest.split("/")[0]);
        }
    }

    const subFolders = [...subFolderSet].sort();
    const existingFilePaths = new Set(allFiles.map(f => f.path));

    const folderItems = subFolders.map(folderName => {
        const targetFilePath = parentFolder
            ? `${parentFolder}/${folderName}/${folderName}.md`
            : `${folderName}/${folderName}.md`;
        const link = dv.fileLink(targetFilePath, false, `📁 ${folderName}`);
        console.log(targetFilePath, link);
        if (existingFilePaths.has(targetFilePath)) {
            return link;
        } else {
            console.warn(`Unresolved link: ${targetFilePath}`);
            return `<span class="is-unresolved">${link}</span>`;
        }
    });

    return dv.list([...folderItems, ...filesHere]);
}

folderList();

view.css

span.is-unresolved > a.internal-link {
    color: var(--link-unresolved-color);
    opacity: var(--link-unresolved-opacity);
    filter: var(--link-unresolved-filter);
    text-decoration-style: var(--link-unresolved-decoration-style);
    text-decoration-color: var(--link-unresolved-decoration-color);
}

Then, every note containing the call to await dv.view("Misc/Views/folder_list"); will show something like:

(in this example, “1. Projects” and “2. Areas” also contain folder notes, while the other folders do not)

2 Likes

That’s wonderful @JLDiaz! Thanks so very much!

1 Like