Dynamic Tag-Based Note List Plugin for Obsidian

Hey,

Just so you know, coding isn’t really my forte and I’ve had a lot of support from GPT to piece this together.

I’m currently working on a plugin that’s supposed to create a note list based on tags. The idea is that you open up the plugin, pick a tag, and then it generates a list of all the notes with that tag. Whenever a note gets the chosen tag, the list gets updated automatically.

I’m not entirely sure if this can be achieved without creating a plugin (would be super helpful if there’s another way around this), but I’ve got some code up and running already.

Cheers,
Tai

import { App, Plugin, MarkdownView, FuzzySuggestModal, TFile } from 'obsidian';

class TagSuggester extends FuzzySuggestModal<string> {
    tags: string[];
    plugin: TagFlowPlugin;

    constructor(app: App, plugin: TagFlowPlugin, tags: string[]) {
        super(app);
        this.tags = tags;
        this.plugin = plugin;
    }

    getItems(): string[] {
        return this.tags;
    }

    getItemText(item: string): string {
        return item;
    }

    onChooseItem(item: string) {
        this.plugin.handleTagSelection(item);
    }
}

export default class TagFlowPlugin extends Plugin {
    allTags: string[] = [];
    currentTag: string = null;
    listNotes: TFile[] = [];

    async onload() {
        console.log("Plugin loaded");
        this.allTags = await this.fetchAllTags();

        this.registerCodeMirror((cm: CodeMirror.Editor) => {
            cm.on("change", this.handleFileChange.bind(this));
        });

        this.addCommand({
            id: 'open-tag-flow',
            name: 'Open Tag Flow',
            callback: () => this.createTagList()
        });

        this.app.workspace.onLayoutReady(() => {
            this.loadData();
        });
    }

    async fetchAllTags() {
        const allTags = new Set<string>();
        
        for (const file of this.app.vault.getMarkdownFiles()) {
            const fileContent = await this.app.vault.cachedRead(file);
            const tagRegex = /#([a-zA-Z0-9_-]+)/g;
            let match;
            while (match = tagRegex.exec(fileContent)) {
                allTags.add(match[1]);
            }
        }

        return Array.from(allTags);
    }

    async createTagList() {
        if (this.allTags.length > 0) {
            new TagSuggester(this.app, this, this.allTags).open();
        }
    }

    async handleTagSelection(tag: string) {
        this.currentTag = `#${tag}`;
        const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
        if (activeView) {
            this.listNotes.push(activeView.file);
        }
        await this.updateList();
        await this.saveData();
    }

    async handleFileChange(change) {
        const file = this.app.vault.getAbstractFileByPath(change.doc.file.path);
        if (file instanceof TFile) {
            this.allTags = await this.fetchAllTags();
            const content = await this.app.vault.read(file);
            if (this.currentTag && content.includes(this.currentTag)) {
                await this.updateList();
            }
        }
    }

    async updateList() {
        if (!this.currentTag || this.listNotes.length === 0) {
            return;
        }

        const files = (await Promise.all(
            this.app.vault.getMarkdownFiles().map(async file => {
                const content = await this.app.vault.read(file);
                return content.includes(this.currentTag) ? file : null;
            })
        )).filter(Boolean) as TFile[];

        const links = files.map(file => `[[${file.basename}]]`).join('\n');

        for (let listNote of this.listNotes) {
            console.log(`Updating list in note: ${listNote.path}`);
            await this.app.vault.modify(listNote, links);
        }
    }

    async saveData() {
        const data = {
            currentTag: this.currentTag,
            listNotePaths: this.listNotes.map(note => note.path),
        };
        console.log("Saving data:", data);
        await this.app.vault.adapter.write('tagFlowData.json', JSON.stringify(data));
    }

    async loadData() {
        try {
            const content = await this.app.vault.adapter.read('tagFlowData.json');
            console.log("`tagFlowData.json` content:", content);
            const data = JSON.parse(content);
            console.log("Loaded data:", data);
            this.currentTag = data.currentTag;
            const notes: TFile[] = [];
            for (let path of data.listNotePaths) {
                const file = this.app.vault.getAbstractFileByPath(path);
                if (file instanceof TFile) {
                    notes.push(file);
                }
            }
            this.listNotes = notes;
        } catch (e) {
            console.log("`tagFlowData.json` file not found");
        }
    }

    onunload() {
        console.log('unloading plugin');
    }
}

1 Like