diff --git a/src/App.tsx b/src/App.tsx index 5c88e606..1ba2b3c9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -486,6 +486,20 @@ function App() { return handleUpdateFrontmatter(path, key, value) }, [handleUpdateFrontmatter]) + // Diff loading + const handleLoadDiff = useCallback(async (path: string): Promise => { + if (isTauri()) { + const vaultPath = TEST_VAULT_PATH.replace('~', '/Users/luca') + return invoke('get_file_diff', { vaultPath, path }) + } else { + return mockInvoke('get_file_diff', { path }) + } + }, []) + + const isFileModified = useCallback((path: string): boolean => { + return modifiedFiles.some((f) => f.path === path) + }, [modifiedFiles]) + return (
@@ -503,6 +517,8 @@ function App() { onSwitchTab={handleSwitchTab} onCloseTab={handleCloseTab} onNavigateWikilink={handleNavigateWikilink} + onLoadDiff={handleLoadDiff} + isModified={isFileModified} />
{!inspectorCollapsed && } diff --git a/src/components/Editor.css b/src/components/Editor.css index 159dabdf..e4bffa36 100644 --- a/src/components/Editor.css +++ b/src/components/Editor.css @@ -117,3 +117,123 @@ .editor__cm-container .cm-editor { height: 100%; } + +/* Tab bar actions (diff toggle) */ +.editor__tab-bar-actions { + display: flex; + align-items: center; + margin-left: auto; + padding: 0 8px; + -webkit-app-region: no-drag; + app-region: no-drag; +} + +.editor__diff-toggle { + padding: 3px 10px; + font-size: 11px; + border: 1px solid var(--border-primary); + border-radius: 4px; + background: transparent; + color: var(--text-tertiary); + cursor: pointer; + transition: all 0.15s; + font-weight: 500; +} + +.editor__diff-toggle:hover { + background: var(--bg-button); + color: var(--text-secondary); +} + +.editor__diff-toggle--active { + background: var(--accent-blue-bg); + color: var(--accent-blue); + border-color: var(--accent-blue-bg); +} + +.editor__diff-toggle:disabled { + opacity: 0.5; + cursor: default; +} + +/* Diff view container */ +.editor__diff-container { + flex: 1; + overflow: auto; +} + +.diff-view { + font-family: 'SF Mono', 'Menlo', 'Consolas', monospace; + font-size: 13px; + line-height: 1.6; + padding: 12px 0; +} + +.diff-view__empty { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + color: var(--text-muted); + font-size: 14px; +} + +.diff-view__line { + display: flex; + padding: 0 16px; + min-height: 22px; +} + +.diff-view__line-number { + width: 40px; + flex-shrink: 0; + text-align: right; + padding-right: 12px; + color: var(--text-faint); + user-select: none; +} + +.diff-view__line-content { + flex: 1; + white-space: pre-wrap; + word-break: break-all; + padding: 0 8px; +} + +.diff-view__line--added { + background: rgba(76, 175, 80, 0.12); +} + +.diff-view__line--added .diff-view__line-content { + color: #4caf50; +} + +.diff-view__line--removed { + background: rgba(244, 67, 54, 0.12); +} + +.diff-view__line--removed .diff-view__line-content { + color: #f44336; +} + +.diff-view__line--hunk { + background: rgba(33, 150, 243, 0.08); +} + +.diff-view__line--hunk .diff-view__line-content { + color: var(--accent-blue, #2196f3); + font-style: italic; +} + +.diff-view__line--header { + background: var(--bg-hover-subtle); +} + +.diff-view__line--header .diff-view__line-content { + color: var(--text-muted); + font-weight: 600; +} + +.diff-view__line--context .diff-view__line-content { + color: var(--text-secondary); +} diff --git a/src/components/Editor.tsx b/src/components/Editor.tsx index bf359228..c5341528 100644 --- a/src/components/Editor.tsx +++ b/src/components/Editor.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react' +import { useEffect, useRef, useState, useCallback } from 'react' import { EditorState } from '@codemirror/state' import { EditorView, keymap, lineNumbers, highlightActiveLine, highlightActiveLineGutter } from '@codemirror/view' import { defaultKeymap, history, historyKeymap } from '@codemirror/commands' @@ -21,6 +21,8 @@ interface EditorProps { onSwitchTab: (path: string) => void onCloseTab: (path: string) => void onNavigateWikilink: (target: string) => void + onLoadDiff?: (path: string) => Promise + isModified?: (path: string) => boolean } const editorTheme = EditorView.theme({ @@ -67,17 +69,83 @@ const editorTheme = EditorView.theme({ }, }) -export function Editor({ tabs, activeTabPath, onSwitchTab, onCloseTab, onNavigateWikilink }: EditorProps) { +function DiffView({ diff }: { diff: string }) { + if (!diff) { + return ( +
+ No changes to display +
+ ) + } + + const lines = diff.split('\n') + + return ( +
+ {lines.map((line, i) => { + let className = 'diff-view__line diff-view__line--context' + if (line.startsWith('+') && !line.startsWith('+++')) { + className = 'diff-view__line diff-view__line--added' + } else if (line.startsWith('-') && !line.startsWith('---')) { + className = 'diff-view__line diff-view__line--removed' + } else if (line.startsWith('@@')) { + className = 'diff-view__line diff-view__line--hunk' + } else if (line.startsWith('diff') || line.startsWith('index') || line.startsWith('---') || line.startsWith('+++') || line.startsWith('new file')) { + className = 'diff-view__line diff-view__line--header' + } + + return ( +
+ {i + 1} + {line || '\u00A0'} +
+ ) + })} +
+ ) +} + +export function Editor({ tabs, activeTabPath, onSwitchTab, onCloseTab, onNavigateWikilink, onLoadDiff, isModified }: EditorProps) { const containerRef = useRef(null) const viewRef = useRef(null) const navigateRef = useRef(onNavigateWikilink) navigateRef.current = onNavigateWikilink + const [diffMode, setDiffMode] = useState(false) + const [diffContent, setDiffContent] = useState(null) + const [diffLoading, setDiffLoading] = useState(false) + const activeTab = tabs.find((t) => t.entry.path === activeTabPath) ?? null + const showDiffToggle = activeTab && isModified?.(activeTab.entry.path) + + // Reset diff mode when switching tabs + useEffect(() => { + setDiffMode(false) + setDiffContent(null) + }, [activeTabPath]) + + const handleToggleDiff = useCallback(async () => { + if (diffMode) { + setDiffMode(false) + setDiffContent(null) + return + } + if (!activeTabPath || !onLoadDiff) return + setDiffLoading(true) + try { + const diff = await onLoadDiff(activeTabPath) + setDiffContent(diff) + setDiffMode(true) + } catch (err) { + console.warn('Failed to load diff:', err) + } finally { + setDiffLoading(false) + } + }, [diffMode, activeTabPath, onLoadDiff]) // Create/destroy editor view when active tab changes useEffect(() => { - if (!containerRef.current || !activeTab) return + if (!containerRef.current || !activeTab || diffMode) return // If view already exists for this tab, skip if (viewRef.current) { @@ -120,9 +188,8 @@ export function Editor({ tabs, activeTabPath, onSwitchTab, onCloseTab, onNavigat view.destroy() viewRef.current = null } - // Re-create when active tab path changes OR when tab data becomes available // eslint-disable-next-line react-hooks/exhaustive-deps - }, [activeTabPath, activeTab?.content]) + }, [activeTabPath, activeTab?.content, diffMode]) if (tabs.length === 0) { return ( @@ -157,8 +224,26 @@ export function Editor({ tabs, activeTabPath, onSwitchTab, onCloseTab, onNavigat
))} + {showDiffToggle && ( +
+ +
+ )} -
+ {diffMode ? ( +
+ +
+ ) : ( +
+ )}
) }