diff --git a/src-tauri/src/git.rs b/src-tauri/src/git.rs index 7f36f5c..eaa705f 100644 --- a/src-tauri/src/git.rs +++ b/src-tauri/src/git.rs @@ -1338,6 +1338,10 @@ fn search_candidate_commits( OsString::from("--all"), OsString::from("--reverse"), OsString::from("--format=%H"), + // Skip textconv diff drivers so git does not extract binary files + // (e.g. .docx / Office temp "~$" lock files) to temp files, which can + // fail with "unsupported filetype" and abort the whole search. + OsString::from("--no-textconv"), ]; if !case_sensitive { args.push(OsString::from("-i")); @@ -1447,7 +1451,7 @@ fn first_added_match_line( check_search_cancelled(cancellation)?; let output = run_git_with_paths_cancellable( repo, - &["diff", "--unified=0", parent, commit], + &["diff", "--no-textconv", "--unified=0", parent, commit], &[file.to_string()], cancellation, "Git-Diff fuer Suchtreffer fehlgeschlagen", diff --git a/src/App.svelte b/src/App.svelte index fca7b05..a5aba5a 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -96,8 +96,10 @@ let comparison: GitCommitComparison | null = null; let compareDialogOpen = false; let selectedDiffPath = ""; + let diffHighlightQuery = ""; let pendingRestoreFile: { commit: GitCommit; file: GitCommitFile } | null = null; let globalSearchOpen = false; + let lastSearchQuery = ""; let globalSearchResults: GitSearchHit[] = []; let globalSearchBusy = false; let globalSearchError = ""; @@ -746,6 +748,7 @@ const result = await compareCommits(activeRepoPath, compareFrom, compareTo); comparison = result; selectedDiffPath = result.files[0]?.path ?? ""; + diffHighlightQuery = ""; pendingRestoreFile = null; compareDialogOpen = true; }); @@ -757,6 +760,19 @@ const result = await diffFileAgainstWorkingTree(activeRepoPath, historyCommit.hash, selectedExplorerPath); comparison = result; selectedDiffPath = result.files[0]?.path ?? selectedExplorerPath; + diffHighlightQuery = ""; + pendingRestoreFile = null; + compareDialogOpen = true; + }); + } + + async function diffSearchHit(hit: GitSearchHit) { + if (!activeRepoPath) return; + await runOperation(`Diffing ${hit.file}`, async () => { + const result = await diffFileAgainstWorkingTree(activeRepoPath, hit.commit_hash, hit.file); + comparison = result; + selectedDiffPath = result.files[0]?.path ?? hit.file; + diffHighlightQuery = lastSearchQuery; pendingRestoreFile = null; compareDialogOpen = true; }); @@ -785,6 +801,7 @@ if (!activeRepoPath || globalSearchBusy) return; const searchId = `search-${Date.now()}-${Math.random().toString(36).slice(2)}`; globalSearchId = searchId; + lastSearchQuery = query; globalSearchBusy = true; globalSearchError = ""; globalSearchResults = []; @@ -1109,19 +1126,6 @@ /> {/if} - -{#if compareDialogOpen && comparison} - -{/if} - {#if globalSearchOpen} +{/if} + + +{#if compareDialogOpen && comparison} + {/if} diff --git a/src/app.css b/src/app.css index 16f8750..27ae071 100644 --- a/src/app.css +++ b/src/app.css @@ -1123,6 +1123,19 @@ .split-cell.add { background: rgba(78,202,118,0.09); color: #5dd88a; } .split-cell.empty { background: rgba(0,0,0,0.06); } + /* Search-hit highlight: amber, distinct from add (green) / del (red). + Higher specificity so it overrides the add/del backgrounds on a matched line. */ + .split-diff .split-cell.match { + background: rgba(240,182,72,0.22); + color: #f3c969; + box-shadow: inset 2px 0 0 rgba(240,182,72,0.9); + } + .split-diff .split-num.match { + background: rgba(240,182,72,0.2); + color: rgba(240,182,72,0.9); + border-right-color: rgba(240,182,72,0.35); + } + .split-col-headers { display: grid; grid-template-columns: 1fr 1fr; @@ -1254,11 +1267,21 @@ } .search-hit-top { display: grid; - grid-template-columns: auto minmax(0, 1fr) auto; + grid-template-columns: auto minmax(0, 1fr) auto auto; align-items: center; gap: 8px; min-width: 0; } + .search-hit-diff { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 3px 9px; + font-size: 11px; + font-weight: 800; + letter-spacing: 0.04em; + white-space: nowrap; + } .search-hit-top .hash { padding: 2px 7px; border: 1px solid rgba(90,140,248,0.22); diff --git a/src/lib/components/CompareDialog.svelte b/src/lib/components/CompareDialog.svelte index effcb7b..7639eab 100644 --- a/src/lib/components/CompareDialog.svelte +++ b/src/lib/components/CompareDialog.svelte @@ -15,6 +15,8 @@ selectedDiffPath: string; isBusy: boolean; restoreLabel?: string; + /** When opened from a search hit, the term to highlight on matching lines. */ + highlightQuery?: string; onClose: () => void; onRestore?: () => void; onSelectFile: (file: GitDiffFile) => void; @@ -25,11 +27,25 @@ selectedDiffPath = "", isBusy = false, restoreLabel = "", + highlightQuery = "", onClose = () => {}, onRestore = undefined, onSelectFile = () => {}, }: Props = $props(); + // Needle = first non-empty line of the search query, lowercased for matching. + let highlightNeedle = $derived( + highlightQuery + .split("\n") + .map((line) => line.trim()) + .find((line) => line.length > 0) + ?.toLowerCase() ?? "" + ); + + function isMatch(text?: string): boolean { + return highlightNeedle.length > 0 && !!text && text.toLowerCase().includes(highlightNeedle); + } + let beforePane = $state(null); let afterPane = $state(null); let isSyncingSplitScroll = false; @@ -259,8 +275,8 @@ {#if row.type === "span"}
{row.text}
{:else} -
{row.leftNum ?? ""}
-
{row.leftText ?? " "}
+
{row.leftNum ?? ""}
+
{row.leftText ?? " "}
{/if} {/each} @@ -276,8 +292,8 @@ {#if row.type === "span"}
{row.text}
{:else} -
{row.rightNum ?? ""}
-
{row.rightText ?? " "}
+
{row.rightNum ?? ""}
+
{row.rightText ?? " "}
{/if} {/each} diff --git a/src/lib/components/GlobalSearchDialog.svelte b/src/lib/components/GlobalSearchDialog.svelte index b05fab7..a7e69e4 100644 --- a/src/lib/components/GlobalSearchDialog.svelte +++ b/src/lib/components/GlobalSearchDialog.svelte @@ -1,5 +1,5 @@