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');
}
}