import { useCallback, useMemo, useRef, useState } from 'react' import { invoke } from '@tauri-apps/api/core' import { isTauri, mockInvoke } from '../mock-tauri' import type { DeletedNoteEntry } from '../components/note-list/noteListUtils' import { extractDeletedContentFromDiff } from '../components/note-list/noteListUtils' import type { CommitDiffRequest } from './useDiffMode' import type { GitCommit, ModifiedFile, SidebarSelection, VaultEntry } from '../types' import { filenameStemToTitle } from '../utils/noteTitle' import { vaultPathForEntry } from '../utils/workspaces' type AppTab = { entry: VaultEntry content: string } interface GitFileWorkflowParams { activeTabPath: string | null allGitModifiedFiles: ModifiedFile[] changesRepositoryPath: string effectiveSelection: SidebarSelection entriesByPath: Map historyRepositoryPath: string loadModifiedFilesForRepository: (vaultPath: string, options?: { includeStats?: boolean }) => Promise onCloseAllTabs: () => void onOpenTabWithContent: (entry: DeletedNoteEntry, content: string) => void onReplaceActiveTab: (entry: VaultEntry) => Promise | unknown onSelectNote: (entry: VaultEntry) => Promise | unknown reloadVault: () => Promise resolvedPath: string selectedChangesModifiedFiles: ModifiedFile[] setToastMessage: (message: string) => void tabs: AppTab[] vaultEntries: VaultEntry[] visibleEntries: VaultEntry[] } interface QueuedDiffRequest { pendingDiffRequest: CommitDiffRequest | null queuePendingDiff: (path: string, commitHash?: string) => void handlePendingDiffHandled: (requestId: number) => void } const DELETED_NOTE_PREVIEW_DEFAULTS = { isA: 'Note', aliases: [], belongsTo: [], relatedTo: [], status: null, archived: false, modifiedAt: null, createdAt: null, fileSize: 0, snippet: '', wordCount: 0, relationships: {}, icon: null, color: null, order: null, sidebarLabel: null, template: null, sort: null, view: null, visible: null, organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [], outgoingLinks: [], properties: {}, hasH1: true, fileKind: 'markdown', __deletedNotePreview: true, __changeAddedLines: null, __changeDeletedLines: null, __changeBinary: false, } satisfies Omit function appTauriCall(command: string, args: Record): Promise { return isTauri() ? invoke(command, args) : mockInvoke(command, args) } function createPulseDeletedNoteEntry(fullPath: string, relativePath: string): DeletedNoteEntry { const filename = relativePath.split('/').pop() ?? relativePath return { ...DELETED_NOTE_PREVIEW_DEFAULTS, path: fullPath, filename, title: filenameStemToTitle(filename), __deletedRelativePath: relativePath, } } function useQueuedDiffRequest(): QueuedDiffRequest { const pendingDiffRequestIdRef = useRef(0) const [pendingDiffRequest, setPendingDiffRequest] = useState(null) const queuePendingDiff = useCallback((path: string, commitHash?: string) => { pendingDiffRequestIdRef.current += 1 setPendingDiffRequest({ requestId: pendingDiffRequestIdRef.current, path, commitHash, }) }, []) const handlePendingDiffHandled = useCallback((requestId: number) => { setPendingDiffRequest((current) => current?.requestId === requestId ? null : current, ) }, []) return { pendingDiffRequest, queuePendingDiff, handlePendingDiffHandled, } } function useVaultPathResolver({ allGitModifiedFiles, resolvedPath, tabs, vaultEntries, visibleEntries, }: Pick) { const findEntryForPath = useCallback((path: string) => { const openTabEntry = tabs.find((tab) => tab.entry.path === path)?.entry if (openTabEntry) return openTabEntry const visibleEntry = visibleEntries.find((entry) => entry.path === path) if (visibleEntry) return visibleEntry return vaultEntries.find((entry) => entry.path === path) ?? null }, [tabs, vaultEntries, visibleEntries]) return useCallback((path: string) => { const entry = findEntryForPath(path) if (entry) return vaultPathForEntry(entry, resolvedPath) const modifiedFile = allGitModifiedFiles.find((file) => file.path === path || file.relativePath === path || path.endsWith('/' + file.relativePath), ) return modifiedFile?.vaultPath ?? resolvedPath }, [allGitModifiedFiles, findEntryForPath, resolvedPath]) } function useGitDiffLoaders(vaultPathForNotePath: (path: string) => string) { const loadGitHistoryForPath = useCallback(async (path: string): Promise => { try { return await appTauriCall('get_file_history', { vaultPath: vaultPathForNotePath(path), path, }) } catch (err) { console.warn('Failed to load git history:', err) return [] } }, [vaultPathForNotePath]) const loadDiffForPath = useCallback((path: string): Promise => appTauriCall('get_file_diff', { vaultPath: vaultPathForNotePath(path), path, }), [vaultPathForNotePath]) const loadDiffAtCommitForPath = useCallback((path: string, commitHash: string): Promise => appTauriCall('get_file_diff_at_commit', { vaultPath: vaultPathForNotePath(path), path, commitHash, }), [vaultPathForNotePath]) return { loadGitHistoryForPath, loadDiffForPath, loadDiffAtCommitForPath, } } function usePulseNoteOpen({ entriesByPath, historyRepositoryPath, onOpenTabWithContent, onSelectNote, queuePendingDiff, }: Pick & { queuePendingDiff: (path: string, commitHash?: string) => void }) { return useCallback((relativePath: string, commitHash?: string) => { const fullPath = `${historyRepositoryPath}/${relativePath}` const entry = entriesByPath.get(fullPath) ?? entriesByPath.get(relativePath) if (commitHash) { const targetPath = entry?.path ?? fullPath queuePendingDiff(targetPath, commitHash) if (entry) { void onSelectNote(entry) } else { onOpenTabWithContent(createPulseDeletedNoteEntry(fullPath, relativePath), 'Content not available') } return } if (entry) { void onSelectNote(entry) } }, [ entriesByPath, historyRepositoryPath, onOpenTabWithContent, onSelectNote, queuePendingDiff, ]) } function isDiscardedFileActive( activePath: string | null, targetFile: ModifiedFile | undefined, relativePath: string, ) { return !!activePath && (activePath === targetFile?.path || activePath.endsWith('/' + relativePath)) } function findReloadedDiscardTarget( entries: VaultEntry[], targetFile: ModifiedFile | undefined, relativePath: string, ) { return entries.find((entry) => entry.path === targetFile?.path || entry.path.endsWith('/' + relativePath), ) } async function syncActiveTabAfterDiscard({ activePathBefore, onCloseAllTabs, onReplaceActiveTab, reloadedEntries, relativePath, targetFile, }: { activePathBefore: string | null onCloseAllTabs: () => void onReplaceActiveTab: (entry: VaultEntry) => Promise | unknown reloadedEntries: VaultEntry[] relativePath: string targetFile: ModifiedFile | undefined }) { if (!isDiscardedFileActive(activePathBefore, targetFile, relativePath)) return const refreshedEntry = findReloadedDiscardTarget(reloadedEntries, targetFile, relativePath) if (refreshedEntry) { await onReplaceActiveTab(refreshedEntry) } else { onCloseAllTabs() } } function useDiscardFileAction({ activeTabPath, changesRepositoryPath, loadModifiedFilesForRepository, onCloseAllTabs, onReplaceActiveTab, reloadVault, selectedChangesModifiedFiles, setToastMessage, }: Pick) { return useCallback(async (relativePath: string) => { const targetFile = selectedChangesModifiedFiles.find((file) => file.relativePath === relativePath) try { await appTauriCall('git_discard_file', { vaultPath: changesRepositoryPath, relativePath }) await loadModifiedFilesForRepository(changesRepositoryPath, { includeStats: true }) await syncActiveTabAfterDiscard({ activePathBefore: activeTabPath, onCloseAllTabs, onReplaceActiveTab, reloadedEntries: await reloadVault(), relativePath, targetFile, }) } catch (err) { setToastMessage(typeof err === 'string' ? err : 'Failed to discard changes') } }, [ activeTabPath, changesRepositoryPath, loadModifiedFilesForRepository, onCloseAllTabs, onReplaceActiveTab, reloadVault, selectedChangesModifiedFiles, setToastMessage, ]) } function useOpenDeletedNoteAction({ loadDiffForPath, onOpenTabWithContent, queuePendingDiff, setToastMessage, }: Pick & { loadDiffForPath: (path: string) => Promise queuePendingDiff: (path: string, commitHash?: string) => void }) { return useCallback(async (entry: DeletedNoteEntry) => { let previewContent = 'Content not available (untracked)' let hasDiff = false try { const diff = await loadDiffForPath(entry.path) hasDiff = diff.length > 0 previewContent = extractDeletedContentFromDiff(diff) ?? previewContent } catch (err) { console.warn('Failed to load deleted note preview:', err) } onOpenTabWithContent(entry, previewContent) if (hasDiff) { queuePendingDiff(entry.path) } else { setToastMessage('Content not available (untracked)') } }, [loadDiffForPath, onOpenTabWithContent, queuePendingDiff, setToastMessage]) } function useDeletedNoteWorkflow({ activeTabPath, changesRepositoryPath, loadDiffForPath, loadModifiedFilesForRepository, onCloseAllTabs, onOpenTabWithContent, onReplaceActiveTab, queuePendingDiff, reloadVault, selectedChangesModifiedFiles, setToastMessage, }: Pick & { loadDiffForPath: (path: string) => Promise queuePendingDiff: (path: string, commitHash?: string) => void }) { const handleDiscardFile = useDiscardFileAction({ activeTabPath, changesRepositoryPath, loadModifiedFilesForRepository, onCloseAllTabs, onReplaceActiveTab, reloadVault, selectedChangesModifiedFiles, setToastMessage, }) const handleOpenDeletedNote = useOpenDeletedNoteAction({ loadDiffForPath, onOpenTabWithContent, queuePendingDiff, setToastMessage, }) return { handleDiscardFile, handleOpenDeletedNote, } } function useReplaceActiveTabWithQueuedDiff({ effectiveSelection, onReplaceActiveTab, queuePendingDiff, }: Pick & { queuePendingDiff: (path: string, commitHash?: string) => void }) { return useCallback((entry: VaultEntry) => { onReplaceActiveTab(entry) if (effectiveSelection.kind === 'filter' && effectiveSelection.filter === 'changes') { queuePendingDiff(entry.path) } }, [effectiveSelection, onReplaceActiveTab, queuePendingDiff]) } function useActiveDeletedFile(activeTabPath: string | null, allGitModifiedFiles: ModifiedFile[]) { return useMemo(() => { if (!activeTabPath) return null return allGitModifiedFiles.find((file) => file.status === 'deleted' && (file.path === activeTabPath || activeTabPath.endsWith('/' + file.relativePath)), ) ?? null }, [activeTabPath, allGitModifiedFiles]) } function useActiveNoteModified(activeTabPath: string | null, allGitModifiedFiles: ModifiedFile[]) { return useMemo( () => allGitModifiedFiles.some((file) => file.path === activeTabPath), [activeTabPath, allGitModifiedFiles], ) } function useDeletedWorkflowForParams( params: GitFileWorkflowParams, loadDiffForPath: (path: string) => Promise, queuePendingDiff: (path: string, commitHash?: string) => void, ) { return useDeletedNoteWorkflow({ activeTabPath: params.activeTabPath, changesRepositoryPath: params.changesRepositoryPath, loadDiffForPath, loadModifiedFilesForRepository: params.loadModifiedFilesForRepository, onCloseAllTabs: params.onCloseAllTabs, onOpenTabWithContent: params.onOpenTabWithContent, onReplaceActiveTab: params.onReplaceActiveTab, queuePendingDiff, reloadVault: params.reloadVault, selectedChangesModifiedFiles: params.selectedChangesModifiedFiles, setToastMessage: params.setToastMessage, }) } export function useGitFileWorkflows(params: GitFileWorkflowParams) { const { pendingDiffRequest, queuePendingDiff, handlePendingDiffHandled, } = useQueuedDiffRequest() const vaultPathForNotePath = useVaultPathResolver(params) const { loadGitHistoryForPath, loadDiffForPath, loadDiffAtCommitForPath, } = useGitDiffLoaders(vaultPathForNotePath) const handlePulseOpenNote = usePulseNoteOpen({ entriesByPath: params.entriesByPath, historyRepositoryPath: params.historyRepositoryPath, onOpenTabWithContent: params.onOpenTabWithContent, onSelectNote: params.onSelectNote, queuePendingDiff, }) const { handleDiscardFile, handleOpenDeletedNote, } = useDeletedWorkflowForParams(params, loadDiffForPath, queuePendingDiff) const handleReplaceActiveTabWithQueuedDiff = useReplaceActiveTabWithQueuedDiff({ effectiveSelection: params.effectiveSelection, onReplaceActiveTab: params.onReplaceActiveTab, queuePendingDiff, }) const activeDeletedFile = useActiveDeletedFile(params.activeTabPath, params.allGitModifiedFiles) const activeNoteModified = useActiveNoteModified(params.activeTabPath, params.allGitModifiedFiles) return { activeDeletedFile, activeNoteModified, handleDiscardFile, handleOpenDeletedNote, handlePendingDiffHandled, handlePulseOpenNote, handleReplaceActiveTabWithQueuedDiff, loadDiffAtCommitForPath, loadDiffForPath, loadGitHistoryForPath, pendingDiffRequest, } }