Merge pull request #221 from LostPaul/Improve-plugin-performance

Improve plugin performance
This commit is contained in:
Lost Paul 2025-06-04 15:02:32 +02:00 committed by GitHub
commit 153aeebb13
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 237 additions and 117 deletions

View file

@ -33,7 +33,7 @@ export class FrontMatterTitlePluginHandler {
const event: Listener<Events, 'manager:update'> = {
name: 'manager:update',
cb: (data) => {
this.handleRename(data as any, true);
this.fmptUpdateFileName(data as any, true);
},
};
// Keep ref to remove listener
@ -41,10 +41,10 @@ export class FrontMatterTitlePluginHandler {
if (ref) {
this.eventRef = ref;
}
this.plugin.app.vault.getFiles().forEach((file) => {
this.handleRename({ id: '', result: false, path: file.path }, false);
});
this.plugin.updateBreadcrumbs();
// this.plugin.app.vault.getFiles().forEach((file) => {
// this.handleRename({ id: '', result: false, path: file.path }, false);
// });
this.plugin.updateAllBreadcrumbs();
})();
}
deleteEvent() {
@ -52,10 +52,12 @@ export class FrontMatterTitlePluginHandler {
this.dispatcher.removeListener(this.eventRef);
}
}
async handleRename(data: {
async fmptUpdateFileName(data: {
id: string;
result: boolean;
path: string;
pathOnly: boolean;
breadcrumb?: HTMLElement;
}, isEvent: boolean) {
if ((data as any).data) data = (data as any).data;
const file = this.app.vault.getAbstractFileByPath(data.path);
@ -69,12 +71,19 @@ export class FrontMatterTitlePluginHandler {
const folderNote = getFolderNote(this.plugin, folder.path);
if (!folderNote) { return; }
if (folderNote !== file) { return; }
if (!data.pathOnly) {
this.plugin.changeFolderNameInExplorer(folder, newName);
}
const breadcrumb = data.breadcrumb;
if (breadcrumb) {
this.plugin.changeFolderNameInPath(folder, newName, breadcrumb);
}
if (isEvent) {
this.plugin.changeName(folder, newName, true);
} else {
this.plugin.changeName(folder, newName, false);
this.plugin.updateAllBreadcrumbs();
}
if (newName) {
folder.newName = newName;
this.modifiedFolders.set(folder.path, folder);
@ -84,4 +93,34 @@ export class FrontMatterTitlePluginHandler {
}
}
async fmptUpdateFolderName(data: {
id: string;
result: boolean;
path: string;
pathOnly: boolean;
breadcrumb?: HTMLElement;
}, replacePath: boolean) {
if ((data as any).data) data = (data as any).data;
const folder = this.app.vault.getAbstractFileByPath(data.path);
if (!(folder instanceof TFolder)) { return; }
const folderNote = getFolderNote(this.plugin, folder.path);
if (!folderNote) { return; }
const resolver = this.api?.getResolverFactory()?.createResolver('#feature-id#');
const newName = resolver?.resolve(folderNote?.path ?? '');
if (!newName) return;
if (!data.pathOnly) {
this.plugin.changeFolderNameInExplorer(folder, newName);
}
const breadcrumb = data.breadcrumb;
if (breadcrumb) {
this.plugin.changeFolderNameInPath(folder, newName, breadcrumb);
}
folder.newName = newName;
this.modifiedFolders.set(folder.path, folder);
}
}

View file

@ -1,96 +1,133 @@
import { Keymap, Platform } from 'obsidian';
import FolderNotesPlugin from 'src/main';
import { Platform, Keymap } from 'obsidian';
import { getFolderNote } from 'src/functions/folderNoteFunctions';
import { handleFolderClick, handleViewHeaderClick } from './handleClick';
import { getExcludedFolder } from 'src/ExcludeFolders/functions/folderFunctions';
import { applyCSSClassesToFolder } from 'src/functions/styleFunctions';
export async function addObserver(plugin: FolderNotesPlugin) {
plugin.observer = new MutationObserver((mutations: MutationRecord[]) => {
mutations.forEach((rec) => {
if (rec.type === 'childList') {
(<Element>rec.target).querySelectorAll('div.nav-folder')
.forEach(async (element: HTMLElement) => {
let folderTitle = element.querySelector('div.nav-folder-title-content') as HTMLElement;
const filePath = folderTitle.parentElement?.getAttribute('data-path') || '';
let fileExplorerMutationObserver: MutationObserver | null = null;
applyCSSClassesToFolder(filePath, plugin);
export function registerFileExplorerObserver(plugin: FolderNotesPlugin) {
// Run once on initial layout
plugin.app.workspace.onLayoutReady(() => {
initializeFolderNoteFeatures(plugin);
initializeBreadcrumbs(plugin);
});
if (folderTitle) {
await initializeFolderTitle(folderTitle, plugin);
} else {
// Re-run when layout changes (e.g. File Explorer is reopened)
plugin.registerEvent(
plugin.app.workspace.on('layout-change', () => {
initializeFolderNoteFeatures(plugin);
const observer = new MutationObserver(async (mutations, obs) => {
folderTitle = element.querySelector('div.nav-folder-title-content') as HTMLElement;
const activeLeaf = plugin.app.workspace.getActiveFileView()?.containerEl;
if (!activeLeaf) return;
if (folderTitle) {
await initializeFolderTitle(folderTitle, plugin);
obs.disconnect();
}
});
const titleContainer = activeLeaf.querySelector('.view-header-title-container');
if (!(titleContainer instanceof HTMLElement)) return;
observer.observe(element, { childList: true, subtree: true });
}
});
if (!plugin.settings.openFolderNoteOnClickInPath) { return; }
// (<Element>rec.target).querySelectorAll('div.nav-file-title-content')
// .forEach(async (element: HTMLElement) => {
// const filePath = element.parentElement?.getAttribute('data-path') || '';
// applyCSSClassesToFolderNote(filePath, plugin);
// });
(<Element>rec.target).querySelectorAll('span.view-header-breadcrumb')
.forEach((element: HTMLElement) => {
const breadcrumbs = element.parentElement?.querySelectorAll('span.view-header-breadcrumb');
if (!breadcrumbs) return;
let path = '';
breadcrumbs.forEach(async (breadcrumb: HTMLElement) => {
if (breadcrumb.hasAttribute('old-name')) {
path += breadcrumb.getAttribute('old-name') + '/';
} else {
path += breadcrumb.innerText.trim() + '/';
}
const folderPath = path.slice(0, -1);
breadcrumb.setAttribute('data-path', folderPath);
const folder = plugin.fmtpHandler?.modifiedFolders.get(folderPath);
if (folder && plugin.settings.frontMatterTitle.path && plugin.settings.frontMatterTitle.enabled) {
breadcrumb.setAttribute('old-name', folder.name || '');
breadcrumb.innerText = folder.newName || '';
}
const excludedFolder = getExcludedFolder(plugin, folderPath, true);
if (excludedFolder?.disableFolderNote) return;
const folderNote = getFolderNote(plugin, folderPath);
if (folderNote) {
breadcrumb.classList.add('has-folder-note');
}
});
element.parentElement?.setAttribute('data-path', path.slice(0, -1));
if (breadcrumbs.length > 0) {
breadcrumbs.forEach((breadcrumb: HTMLElement) => {
if (breadcrumb.onclick) return;
breadcrumb.addEventListener('click', (e) => {
handleViewHeaderClick(e, plugin);
}, { capture: true });
});
}
});
}
});
updateFolderNamesInPath(plugin, titleContainer);
})
);
}
export function unregisterFileExplorerObserver() {
if (fileExplorerMutationObserver) {
fileExplorerMutationObserver.disconnect();
fileExplorerMutationObserver = null;
}
}
function initializeFolderNoteFeatures(plugin: FolderNotesPlugin) {
const explorer = document.querySelector('.workspace-leaf-content[data-type="file-explorer"] .nav-files-container');
if (!explorer) return;
initializeAllFolderTitles(explorer, plugin);
observeFolderTitleMutations(explorer, plugin);
}
function initializeBreadcrumbs(plugin: FolderNotesPlugin) {
const titleContainers = document.querySelectorAll('.view-header-title-container');
if (!titleContainers.length) return;
titleContainers.forEach((container) => {
if (!(container instanceof HTMLElement)) return;
scheduleIdle(() => updateFolderNamesInPath(plugin, container), { timeout: 1000 });
});
}
async function initializeFolderTitle(folderTitle: HTMLElement, plugin: any) {
if (folderTitle.onclick) return;
/**
* Observes the File Explorer for newly added folder elements and applies plugin logic (e.g., styles, event listeners)
* automatically when folders are created, expanded, or when the File Explorer view is reopened.
*/
function observeFolderTitleMutations(container: Element, plugin: FolderNotesPlugin) {
if (fileExplorerMutationObserver) {
fileExplorerMutationObserver.disconnect();
}
fileExplorerMutationObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of Array.from(mutation.addedNodes)) {
if (!(node instanceof HTMLElement)) continue;
processAddedFolders(node, plugin);
}
}
});
fileExplorerMutationObserver.observe(container, { childList: true, subtree: true });
}
function initializeAllFolderTitles(container: Element, plugin: FolderNotesPlugin) {
const allTitles = container.querySelectorAll('.nav-folder-title-content');
for (const title of Array.from(allTitles)) {
const folderTitle = title as HTMLElement;
const folderEl = folderTitle.closest('.nav-folder-title');
if (!folderEl) continue;
const folderPath = folderEl.getAttribute('data-path') || '';
setTimeout(() => {
setupFolderTitle(folderTitle, plugin, folderPath);
}, 1000);
}
}
function processAddedFolders(node: HTMLElement, plugin: FolderNotesPlugin) {
const titles: HTMLElement[] = [];
if (node.matches('.nav-folder-title-content')) {
titles.push(node);
}
node.querySelectorAll('.nav-folder-title-content').forEach((el) => {
titles.push(el as HTMLElement);
});
titles.forEach((folderTitle) => {
const folderEl = folderTitle.closest('.nav-folder-title');
const folderPath = folderEl?.getAttribute('data-path') || '';
if (!folderEl || !folderPath) {
setTimeout(() => {
const retryFolderEl = folderTitle.closest('.nav-folder-title');
const retryFolderPath = retryFolderEl?.getAttribute('data-path') || '';
if (retryFolderEl && retryFolderPath) {
setupFolderTitle(folderTitle, plugin, retryFolderPath);
}
}, 50);
return;
}
setupFolderTitle(folderTitle, plugin, folderPath);
});
}
async function setupFolderTitle(folderTitle: HTMLElement, plugin: FolderNotesPlugin, folderPath: string) {
if (folderTitle.dataset.initialized === 'true') return;
if (!folderPath) return;
if (Platform.isMobile && plugin.settings.disableOpenFolderNoteOnClick) return;
const folderPath = folderTitle.parentElement?.getAttribute('data-path') || '';
folderTitle.dataset.initialized = 'true';
await applyCSSClassesToFolder(folderPath, plugin);
// Handle middle click (auxclick)
if (plugin.settings.frontMatterTitle.enabled) {
plugin.fmtpHandler?.fmptUpdateFolderName({ id: '', result: false, path: folderPath, pathOnly: false }, false);
}
folderTitle.addEventListener('auxclick', (event: MouseEvent) => {
if (event.button === 1) {
handleFolderClick(event, plugin);
}
if (event.button === 1) handleFolderClick(event, plugin);
}, { capture: true });
folderTitle.onclick = (event: MouseEvent) => handleFolderClick(event, plugin);
@ -102,17 +139,16 @@ async function initializeFolderTitle(folderTitle: HTMLElement, plugin: any) {
if (!Keymap.isModEvent(event)) return;
if (!(event.target instanceof HTMLElement)) return;
const folderPath = event?.target.parentElement?.getAttribute('data-path') || '';
const folderNote = getFolderNote(plugin, folderPath);
if (!folderNote) return;
plugin.app.workspace.trigger('hover-link', {
event: event,
event,
source: 'preview',
hoverParent: { file: folderNote },
targetEl: event.target,
linktext: folderNote?.basename,
sourcePath: folderNote?.path,
linktext: folderNote.basename,
sourcePath: folderNote.path,
});
plugin.hoverLinkTriggered = true;
});
@ -123,3 +159,40 @@ async function initializeFolderTitle(folderTitle: HTMLElement, plugin: any) {
plugin.hoverLinkTriggered = false;
});
}
async function updateFolderNamesInPath(plugin: FolderNotesPlugin, titleContainer: HTMLElement) {
const headers = titleContainer.querySelectorAll('span.view-header-breadcrumb');
let path = '';
headers.forEach(async (breadcrumb: HTMLElement) => {
path += breadcrumb.getAttribute('old-name') ?? (breadcrumb as HTMLElement).innerText.trim();
path += '/';
const folderPath = path.slice(0, -1);
const excludedFolder = getExcludedFolder(plugin, folderPath, true);
if (excludedFolder?.disableFolderNote) return;
const folderNote = getFolderNote(plugin, folderPath);
if (folderNote) breadcrumb.classList.add('has-folder-note');
breadcrumb?.setAttribute('data-path', path.slice(0, -1));
if (!breadcrumb.onclick) {
breadcrumb.addEventListener('click', (e) => {
handleViewHeaderClick(e as MouseEvent, plugin);
}, { capture: true });
}
if (plugin.settings.frontMatterTitle.enabled) {
plugin.fmtpHandler?.fmptUpdateFolderName({ id: '', result: false, path: folderPath, pathOnly: true, breadcrumb: breadcrumb }, true);
}
});
}
// Schedules a callback to run when the browser is idle, or after a timeout as a fallback.
// - callback: The function to execute when idle or after the timeout.
// - options: Optional object with a 'timeout' property (in milliseconds).
function scheduleIdle(callback: () => void, options?: { timeout: number }) {
if ('requestIdleCallback' in window) {
(window as any).requestIdleCallback(callback, options);
} else {
setTimeout(callback, options?.timeout || 200);
}
}

View file

@ -36,7 +36,7 @@ export function handleRename(file: TAbstractFile, oldPath: string, plugin: Folde
}
} else if (file instanceof TFile) {
if (isRename) {
return handleFileRename(file, oldPath, plugin);
return fmptUpdateFileName(file, oldPath, plugin);
} else {
return handleFileMove(file, oldPath, plugin);
}
@ -129,7 +129,7 @@ export async function handleFolderRename(file: TFolder, oldPath: string, plugin:
plugin.app.fileManager.renameFile(folderNote, newPath);
}
export async function handleFileRename(file: TFile, oldPath: string, plugin: FolderNotesPlugin) {
export async function fmptUpdateFileName(file: TFile, oldPath: string, plugin: FolderNotesPlugin) {
const oldFileName = removeExtension(getFileNameFromPathString(oldPath));
const newFileName = file.basename;
if (oldFileName === newFileName) { return; }

View file

@ -5,7 +5,7 @@ import { getFolder, getFolderNote } from 'src/functions/folderNoteFunctions';
import { getFileExplorer } from './utils';
import FolderOverviewPlugin from 'src/obsidian-folder-overview/src/main';
export function loadFileClasses(forceReload = false, plugin: FolderNotesPlugin) {
export function updateAllFileStyles(forceReload = false, plugin: FolderNotesPlugin) {
if (plugin.activeFileExplorer === getFileExplorer(plugin) && !forceReload) { return; }
plugin.activeFileExplorer = getFileExplorer(plugin);
plugin.app.vault.getAllLoadedFiles().forEach(async (file) => {

View file

@ -4,7 +4,7 @@ import { DEFAULT_SETTINGS, FolderNotesSettings, SettingsTab } from './settings/S
import { Commands } from './Commands';
import { FileExplorerWorkspaceLeaf } from './globals';
import { handleFolderClick } from './events/handleClick';
import { addObserver } from './events/MutationObserver';
import { registerFileExplorerObserver, unregisterFileExplorerObserver } from './events/MutationObserver';
import { handleRename } from './events/handleRename';
import { getFolderNote, getFolder, openFolderNote } from './functions/folderNoteFunctions';
import { handleCreate } from './events/handleCreate';
@ -14,7 +14,7 @@ import { FolderOverview } from './obsidian-folder-overview/src/FolderOverview';
import { TabManager } from './events/TabManager';
import './functions/ListComponent';
import { handleDelete } from './events/handleDelete';
import { addCSSClassToTitleEL, getEl, loadFileClasses } from './functions/styleFunctions';
import { addCSSClassToTitleEL, getEl, updateAllFileStyles } from './functions/styleFunctions';
import { getExcludedFolder } from './ExcludeFolders/functions/folderFunctions';
import { FileExplorerView, InternalPlugin } from 'obsidian-typings';
import { getFocusedItem } from './functions/utils';
@ -67,12 +67,10 @@ export default class FolderNotesPlugin extends Plugin {
this.app.workspace.onLayoutReady(this.onLayoutReady.bind(this));
await addObserver(this);
this.observer.observe(document.body, {
childList: true,
subtree: true,
});
// this.observer.observe(document.body, {
// childList: true,
// subtree: true,
// });
if (!this.settings.persistentSettingsTab.afterRestart) {
this.settings.settingsTab = 'general';
@ -144,6 +142,7 @@ export default class FolderNotesPlugin extends Plugin {
}
onLayoutReady() {
registerFileExplorerObserver(this);
this.registerView(FOLDER_OVERVIEW_VIEW, (leaf: WorkspaceLeaf) => {
return new FolderOverviewView(leaf, this);
});
@ -285,13 +284,13 @@ export default class FolderNotesPlugin extends Plugin {
return true;
}
async changeName(folder: TFolder, name: string | null | undefined, replacePath: boolean, waitForCreate = false, count = 0) {
if (!name) name = folder.name;
async changeFolderNameInExplorer(folder: TFolder, newName: string | null | undefined, waitForCreate = false, count = 0) {
if (!newName) newName = folder.name;
let fileExplorerItem = getEl(folder.path, this);
if (!fileExplorerItem) {
if (waitForCreate && count < 5) {
await new Promise((r) => setTimeout(r, 500));
this.changeName(folder, name, replacePath, waitForCreate, count + 1);
this.changeFolderNameInExplorer(folder, newName, waitForCreate, count + 1);
return;
}
return;
@ -300,18 +299,26 @@ export default class FolderNotesPlugin extends Plugin {
fileExplorerItem = fileExplorerItem?.querySelector('div.nav-folder-title-content');
if (!fileExplorerItem) { return; }
if (this.settings.frontMatterTitle.explorer && this.settings.frontMatterTitle.enabled) {
fileExplorerItem.innerText = name;
fileExplorerItem.innerText = newName;
fileExplorerItem.setAttribute('old-name', folder.name);
} else {
fileExplorerItem.innerText = folder.name;
fileExplorerItem.removeAttribute('old-name');
}
if (replacePath) {
this.updateBreadcrumbs();
}
}
updateBreadcrumbs(remove?: boolean) {
async changeFolderNameInPath(folder: TFolder, newName: string | null | undefined, breadcrumb: HTMLElement) {
if (!newName) newName = folder.name;
breadcrumb.textContent = folder.newName || folder.name;
breadcrumb.setAttribute('old-name', folder.name);
breadcrumb.setAttribute('data-path', folder.path);
}
/**
* Updates all folder names in the path above the note editor
*/
updateAllBreadcrumbs(remove?: boolean) {
if (!this.settings.frontMatterTitle.path && !remove) { return; }
const viewHeaderItems = document.querySelectorAll('span.view-header-breadcrumb');
const files = this.app.vault.getAllLoadedFiles().filter((file) => file instanceof TFolder);
@ -340,8 +347,7 @@ export default class FolderNotesPlugin extends Plugin {
}
onunload() {
console.log('unloading folder notes plugin');
this.observer.disconnect();
unregisterFileExplorerObserver();
document.body.classList.remove('folder-notes-plugin');
document.body.classList.remove('folder-note-underline');
document.body.classList.remove('hide-folder-note');
@ -379,7 +385,7 @@ export default class FolderNotesPlugin extends Plugin {
await this.saveData(this.settings);
// cleanup any css if we need too
if ((!this.settingsOpened || reloadStyles === true) && reloadStyles !== false) {
loadFileClasses(true, this);
updateAllFileStyles(true, this);
}
}

View file

@ -91,7 +91,7 @@ export async function renderFileExplorer(settingsTab: SettingsTab) {
settingsTab.plugin.settings.frontMatterTitle.explorer = value;
await settingsTab.plugin.saveSettings();
settingsTab.plugin.app.vault.getFiles().forEach((file) => {
settingsTab.plugin.fmtpHandler?.handleRename({ id: '', result: false, path: file.path }, false);
settingsTab.plugin.fmtpHandler?.fmptUpdateFileName({ id: '', result: false, path: file.path, pathOnly: false }, false);
});
})
);

View file

@ -5,7 +5,7 @@ import AddSupportedFileModal from '../modals/AddSupportedFileType';
import { FrontMatterTitlePluginHandler } from '../events/FrontMatterTitle';
import ConfirmationModal from './modals/CreateFnForEveryFolder';
import { TemplateSuggest } from '../suggesters/TemplateSuggester';
import { loadFileClasses } from '../functions/styleFunctions';
import { updateAllFileStyles } from '../functions/styleFunctions';
import BackupWarningModal from './modals/BackupWarning';
import RenameFolderNotesModal from './modals/RenameFns';
@ -186,7 +186,7 @@ export async function renderGeneral(settingsTab: SettingsTab) {
settingsTab.plugin.settings.storageLocation = value;
await settingsTab.plugin.saveSettings();
settingsTab.display();
loadFileClasses(undefined, settingsTab.plugin);
updateAllFileStyles(undefined, settingsTab.plugin);
})
)
.addButton((button) =>
@ -429,7 +429,7 @@ export async function renderGeneral(settingsTab: SettingsTab) {
' with folder notes. It allows you to set the folder name to some name you set in the front matter.',
);
new Setting(containerEl)
const fmtpSetting = new Setting(containerEl)
.setName('Enable front matter title plugin integration')
.setDesc(desc1)
.addToggle((toggle) =>
@ -442,10 +442,10 @@ export async function renderGeneral(settingsTab: SettingsTab) {
settingsTab.plugin.fmtpHandler = new FrontMatterTitlePluginHandler(settingsTab.plugin);
} else {
if (settingsTab.plugin.fmtpHandler) {
settingsTab.plugin.updateBreadcrumbs(true);
settingsTab.plugin.updateAllBreadcrumbs(true);
}
settingsTab.plugin.app.vault.getFiles().forEach((file) => {
settingsTab.plugin.fmtpHandler?.handleRename({ id: '', result: false, path: file.path }, false);
settingsTab.plugin.fmtpHandler?.fmptUpdateFileName({ id: '', result: false, path: file.path, pathOnly: false }, false);
});
settingsTab.plugin.fmtpHandler?.deleteEvent();
settingsTab.plugin.fmtpHandler = null;
@ -453,6 +453,8 @@ export async function renderGeneral(settingsTab: SettingsTab) {
settingsTab.display();
})
);
fmtpSetting.infoEl.appendText('Requires a restart to take effect');
fmtpSetting.infoEl.style.color = settingsTab.app.vault.getConfig('accentColor') as string || '#7d5bed';
settingsTab.settingsPage.createEl('h3', { text: 'Session & Persistence' });

View file

@ -49,9 +49,9 @@ export async function renderPath(settingsTab: SettingsTab) {
settingsTab.plugin.settings.frontMatterTitle.path = value;
await settingsTab.plugin.saveSettings();
if (value) {
settingsTab.plugin.updateBreadcrumbs();
settingsTab.plugin.updateAllBreadcrumbs();
} else {
settingsTab.plugin.updateBreadcrumbs(true);
settingsTab.plugin.updateAllBreadcrumbs(true);
}
})
);