import type { VaultEntry } from '../types' import { findByNotePath, joinVaultPath, normalizeNotePathForIdentity, notePathsMatch } from './notePathIdentity' interface PulledVaultRefreshOptions { activeTabPath: string | null getActiveTabPath?: () => string | null closeAllTabs: () => void hasUnsavedChanges: (path: string) => boolean shouldKeepActiveEditorMounted?: () => boolean reloadFolders: () => Promise | unknown reloadVault: () => Promise reloadViews: () => Promise | unknown replaceActiveTab: (entry: VaultEntry) => Promise updatedFiles: string[] vaultPath: string } function resolveUpdatedFilePath(options: { path: string; vaultPath: string }): string { const { path, vaultPath } = options if (path.startsWith('/')) return normalizeNotePathForIdentity(path) return normalizeNotePathForIdentity(joinVaultPath(vaultPath, path)) } function didPullUpdateActiveNote(options: { updatedFiles: string[] vaultPath: string activeTabPath: string }): boolean { const { activeTabPath, updatedFiles, vaultPath } = options return updatedFiles.some((path) => notePathsMatch(resolveUpdatedFilePath({ path, vaultPath }), activeTabPath)) } function didActivePathChange(options: { initialPath: string; latestPath: string }): boolean { const { initialPath, latestPath } = options return !notePathsMatch(initialPath, latestPath) } function findExternallyMovedActiveEntry(options: { activeTabPath: string entries: VaultEntry[] updatedFiles: string[] vaultPath: string }): VaultEntry | null { const { activeTabPath, entries, updatedFiles, vaultPath } = options if (updatedFiles.length === 0) return null const activeFilename = normalizeNotePathForIdentity(activeTabPath).split('/').pop() const updatedPaths = new Set(updatedFiles.map((path) => resolveUpdatedFilePath({ path, vaultPath }))) const candidates = entries.filter((entry) => !notePathsMatch(entry.path, activeTabPath) && entry.filename === activeFilename && updatedPaths.has(normalizeNotePathForIdentity(entry.path)), ) const [candidate] = candidates return candidates.length === 1 && candidate ? candidate : null } function isActivePathBlocked(options: { activeTabPath: string | null latestActiveTabPath: string | null hasUnsavedChanges: PulledVaultRefreshOptions['hasUnsavedChanges'] }): boolean { const { activeTabPath, latestActiveTabPath, hasUnsavedChanges } = options if (!activeTabPath) return true if (!latestActiveTabPath) return true if (didActivePathChange({ initialPath: activeTabPath, latestPath: latestActiveTabPath })) return true return hasUnsavedChanges(latestActiveTabPath) } function shouldReplaceActiveEntry(options: { activePath: string movedEntry: VaultEntry | null refreshedEntry: VaultEntry | null updatedFiles: string[] vaultPath: string }): boolean { const { activePath, movedEntry, refreshedEntry, updatedFiles, vaultPath, } = options if (movedEntry) return true if (!refreshedEntry) return false return didPullUpdateActiveNote({ updatedFiles, vaultPath, activeTabPath: activePath }) } function shouldPreserveFocusedActiveEntry(options: { movedEntry: VaultEntry | null shouldKeepActiveEditorMounted?: () => boolean }): boolean { const { movedEntry, shouldKeepActiveEditorMounted } = options if (movedEntry || !shouldKeepActiveEditorMounted) return false return shouldKeepActiveEditorMounted() } async function applyActiveEntryReplacement(options: { closeAllTabs: PulledVaultRefreshOptions['closeAllTabs'] movedEntry: VaultEntry | null replaceActiveTab: PulledVaultRefreshOptions['replaceActiveTab'] replacementEntry: VaultEntry | null shouldKeepActiveEditorMounted?: PulledVaultRefreshOptions['shouldKeepActiveEditorMounted'] shouldReplace: boolean | null }): Promise { const { closeAllTabs, movedEntry, replaceActiveTab, replacementEntry, shouldKeepActiveEditorMounted, shouldReplace, } = options if (!replacementEntry || !shouldReplace) return false if (shouldPreserveFocusedActiveEntry({ movedEntry, shouldKeepActiveEditorMounted })) return true closeAllTabs() await replaceActiveTab(replacementEntry) return true } export function getPulledVaultUpdateOptions(): { preserveFocusedEditor: true } { return { preserveFocusedEditor: true } } export async function refreshPulledVaultState(options: PulledVaultRefreshOptions): Promise { const { activeTabPath, closeAllTabs, getActiveTabPath, hasUnsavedChanges, reloadFolders, reloadVault, reloadViews, replaceActiveTab, shouldKeepActiveEditorMounted, updatedFiles, vaultPath, } = options const [entries] = await Promise.all([ reloadVault(), Promise.resolve(reloadFolders()), Promise.resolve(reloadViews()), ]) const latestActiveTabPath = getActiveTabPath?.() ?? activeTabPath if (isActivePathBlocked({ activeTabPath, latestActiveTabPath, hasUnsavedChanges })) return entries const activePath = latestActiveTabPath as string const refreshedEntry = findByNotePath(entries, activePath) ?? null const movedEntry = refreshedEntry ? null : findExternallyMovedActiveEntry({ activeTabPath: activePath, entries, updatedFiles, vaultPath, }) const replacementEntry = refreshedEntry ?? movedEntry const shouldReplace = replacementEntry && shouldReplaceActiveEntry({ activePath, movedEntry, refreshedEntry, updatedFiles, vaultPath, }) const handledReplacement = await applyActiveEntryReplacement({ closeAllTabs, movedEntry, replaceActiveTab, replacementEntry, shouldKeepActiveEditorMounted, shouldReplace, }) if (handledReplacement) return entries if (!replacementEntry) closeAllTabs() return entries }