add new style and global search

This commit is contained in:
Christoph Brandau
2026-06-29 17:24:08 +02:00
parent 9861fa2446
commit b0491d1479
17 changed files with 1805 additions and 70 deletions
+102 -4
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { onDestroy, onMount } from "svelte";
import { AlertCircle, Check, GitBranch, GitMerge, LoaderCircle } from "@lucide/svelte";
import { open as openDialog } from "@tauri-apps/plugin-dialog";
import { AlertCircle, Check, FolderOpen, GitBranch, GitMerge, LoaderCircle } from "@lucide/svelte";
import TitleBar from "./lib/TitleBar.svelte";
import BranchPanel from "./lib/components/BranchPanel.svelte";
@@ -10,6 +11,7 @@
import CredentialDialog from "./lib/components/CredentialDialog.svelte";
import ExplorerPanel from "./lib/components/ExplorerPanel.svelte";
import FileHistoryPanel from "./lib/components/FileHistoryPanel.svelte";
import GlobalSearchDialog from "./lib/components/GlobalSearchDialog.svelte";
import HistoryPanel from "./lib/components/HistoryPanel.svelte";
import ResolveDialog from "./lib/components/ResolveDialog.svelte";
import StatusPanel from "./lib/components/StatusPanel.svelte";
@@ -18,6 +20,7 @@
checkoutBranch,
commit,
compareCommits,
cancelCodeSearch,
diffFileAgainstWorkingTree,
getStatus,
listBranches,
@@ -34,6 +37,7 @@
restoreFileFromCommit,
restoreFiles,
restoreToCommit,
searchCodeIntroductions,
stageFiles,
unstageFiles,
} from "./lib/git";
@@ -49,6 +53,7 @@
GitDiffFile,
GitFileStatus,
GitRepositoryFile,
GitSearchHit,
GitStatus,
PreparedResolution,
} from "./lib/types";
@@ -74,6 +79,11 @@
let comparison: GitCommitComparison | null = null;
let compareDialogOpen = false;
let selectedDiffPath = "";
let globalSearchOpen = false;
let globalSearchResults: GitSearchHit[] = [];
let globalSearchBusy = false;
let globalSearchError = "";
let globalSearchId = "";
let resolveDialogOpen = false;
let conflictTarget = "";
let conflict: ConflictFile | null = null;
@@ -122,7 +132,7 @@
}
async function autoRefreshTick() {
if (!autoRefreshEnabled || !activeRepoPath || isBusy || autoRefreshInFlight || resolveDialogOpen || compareDialogOpen) return;
if (!autoRefreshEnabled || !activeRepoPath || isBusy || autoRefreshInFlight || resolveDialogOpen || compareDialogOpen || globalSearchOpen) return;
autoRefreshInFlight = true;
try {
const nextStatus = await getStatus(activeRepoPath);
@@ -220,9 +230,10 @@
// ── Repository operations ──────────────────────────────────────────────────
async function openRepo() {
const path = repoPath.trim();
async function openRepo(pathOverride?: string) {
const path = (pathOverride ?? repoPath).trim();
if (!path) { errorMessage = "Enter a repository path."; return; }
repoPath = path;
await runOperation("Opening repository", async () => {
const nextStatus = await openRepository(path);
@@ -232,6 +243,8 @@
expandedExplorerPaths = new Set(); expandedCommitHashes = new Set();
fileHistory = []; compareFrom = ""; compareTo = "";
comparison = null; compareDialogOpen = false; selectedDiffPath = "";
if (globalSearchBusy) void cancelGlobalSearch();
globalSearchResults = []; globalSearchOpen = false; globalSearchError = "";
resolveDialogOpen = false; conflictTarget = ""; conflict = null;
preparedResolutions = {};
await refreshBranchList(activeRepoPath);
@@ -240,6 +253,23 @@
});
}
async function chooseRepositoryFolder() {
if (isBusy) return;
try {
const selected = await openDialog({
title: "Repository folder auswaehlen",
directory: true,
multiple: false,
defaultPath: repoPath.trim() || activeRepoPath || undefined,
});
if (typeof selected !== "string") return;
repoPath = selected;
await openRepo(selected);
} catch (error) {
errorMessage = errorToMessage(error);
}
}
async function refreshRepo() {
if (!activeRepoPath) { await openRepo(); return; }
await runOperation("Refreshing", async () => {
@@ -474,6 +504,48 @@
selectedDiffPath = file.path;
}
async function runGlobalSearch(query: string, caseSensitive: boolean, limit: number) {
if (!activeRepoPath || globalSearchBusy) return;
const searchId = `search-${Date.now()}-${Math.random().toString(36).slice(2)}`;
globalSearchId = searchId;
globalSearchBusy = true;
globalSearchError = "";
globalSearchResults = [];
try {
const results = await searchCodeIntroductions(activeRepoPath, query, caseSensitive, limit, searchId);
if (globalSearchId === searchId) {
globalSearchResults = results;
}
} catch (error) {
if (globalSearchId === searchId) {
const message = errorToMessage(error);
globalSearchError = message.includes("abgebrochen") ? "Suche wurde abgebrochen." : message;
}
} finally {
if (globalSearchId === searchId) {
globalSearchBusy = false;
globalSearchId = "";
}
}
}
async function cancelGlobalSearch() {
if (!globalSearchId) return;
const searchId = globalSearchId;
globalSearchError = "Abbruch wird angefordert...";
try {
await cancelCodeSearch(searchId);
} catch (error) {
globalSearchError = errorToMessage(error);
}
}
function closeGlobalSearchDialog() {
if (globalSearchBusy) void cancelGlobalSearch();
globalSearchOpen = false;
}
// ── Conflict resolution ────────────────────────────────────────────────────
async function loadConflict(file: string) {
@@ -543,6 +615,7 @@
function handleWindowKeydown(event: KeyboardEvent) {
if (event.key === "Escape" && compareDialogOpen) compareDialogOpen = false;
else if (event.key === "Escape" && globalSearchOpen) closeGlobalSearchDialog();
}
</script>
@@ -566,6 +639,7 @@
onPull={pullRepo}
onPush={pushRepo}
onRefresh={refreshRepo}
onSearch={() => { globalSearchOpen = true; }}
onToggleAutoRefresh={toggleAutoRefresh}
/>
@@ -583,6 +657,17 @@
placeholder="/path/to/repository"
disabled={isBusy}
/>
<button
class="btn-secondary repo-browse"
type="button"
onclick={chooseRepositoryFolder}
disabled={isBusy}
title="Repository-Ordner auswaehlen"
aria-label="Repository-Ordner auswaehlen"
>
<FolderOpen size={16} aria-hidden="true" />
Browse
</button>
<button class="btn-primary" type="submit" disabled={isBusy || repoPath.trim().length === 0}>
{#if operation === "Opening repository"}
<LoaderCircle class="spin" size={16} aria-hidden="true" />
@@ -743,6 +828,19 @@
/>
{/if}
{#if globalSearchOpen}
<GlobalSearchDialog
{hasRepository}
{isBusy}
isSearching={globalSearchBusy}
error={globalSearchError}
results={globalSearchResults}
onClose={closeGlobalSearchDialog}
onSearch={runGlobalSearch}
onCancel={cancelGlobalSearch}
/>
{/if}
<!-- Credential dialog for push/pull -->
{#if credDialogOpen && credDialogAction}
<CredentialDialog
+286 -50
View File
@@ -1,29 +1,29 @@
@import "tailwindcss";
@theme {
--color-ink: #dde1f0;
--color-ink-muted: #8090be;
--color-ink-faint: #3c4668;
--color-ink-dim: #5e6e9c;
--color-ink-quiet: #7880ac;
--color-ink: #f5f7ff;
--color-ink-muted: #a8b1d8;
--color-ink-faint: #687197;
--color-ink-dim: #838cb8;
--color-ink-quiet: #949bc8;
--color-surface: #161a24;
--color-surface-alt: #0e1118;
--color-surface-dim: #191d2a;
--color-surface-hover: #202438;
--color-surface-raised: #1c2033;
--color-surface: rgba(22, 22, 36, 0.82);
--color-surface-alt: #0b0b14;
--color-surface-dim: rgba(18, 18, 30, 0.9);
--color-surface-hover: rgba(47, 48, 78, 0.76);
--color-surface-raised: rgba(28, 29, 48, 0.88);
--color-border: #2a2f45;
--color-border-subtle: #1e2236;
--color-border-input: #323658;
--color-border: rgba(100, 108, 255, 0.28);
--color-border-subtle: rgba(255, 255, 255, 0.08);
--color-border-input: rgba(65, 209, 255, 0.26);
--color-primary: #5a8cf8;
--color-primary-dark: #4878f0;
--color-accent: #6a9aff;
--color-primary: #646cff;
--color-primary-dark: #535bf2;
--color-accent: #41d1ff;
--color-bar: #090c14;
--color-bar-text: #c0cce0;
--color-bar-muted: #4a5a78;
--color-bar: rgba(9, 9, 18, 0.92);
--color-bar-text: #f4f6ff;
--color-bar-muted: #7b84b2;
--font-mono: "Cascadia Code", "SFMono-Regular", Consolas, monospace;
}
@@ -43,7 +43,11 @@
body {
overflow: hidden;
color: var(--color-ink);
background: var(--color-surface-alt);
background:
radial-gradient(circle at 22% 8%, rgba(189, 52, 254, 0.28), transparent 34%),
radial-gradient(circle at 74% 10%, rgba(65, 209, 255, 0.22), transparent 30%),
radial-gradient(circle at 88% 78%, rgba(255, 211, 67, 0.1), transparent 26%),
linear-gradient(135deg, #090912 0%, #111122 45%, #0b0b14 100%);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-synthesis: none;
text-rendering: optimizeLegibility;
@@ -61,9 +65,9 @@
min-height: 30px;
padding: 0 11px;
border: 1px solid var(--color-border);
border-radius: 6px;
border-radius: 8px;
color: var(--color-ink-muted);
background: var(--color-surface-raised);
background: linear-gradient(180deg, rgba(42, 43, 70, 0.9), rgba(27, 28, 48, 0.92));
cursor: pointer;
transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
font-size: 13px;
@@ -75,20 +79,21 @@
}
button:disabled { cursor: not-allowed; opacity: 0.4; }
input, textarea {
input, textarea, select {
width: 100%;
border: 1px solid var(--color-border-input);
border-radius: 6px;
border-radius: 8px;
color: var(--color-ink);
background: var(--color-surface-raised);
background: rgba(13, 14, 26, 0.78);
outline: none;
}
input:focus, textarea:focus {
input:focus, textarea:focus, select:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(90, 140, 248, 0.18);
box-shadow: 0 0 0 3px rgba(100, 108, 255, 0.22), 0 0 32px rgba(65, 209, 255, 0.08);
}
input { height: 34px; padding: 0 11px; }
textarea { resize: vertical; min-height: 132px; padding: 10px 11px; line-height: 1.45; }
select { height: 34px; padding: 0 9px; }
select {
appearance: none;
@@ -120,22 +125,40 @@
.btn-sm { min-height: 26px; padding: 0 8px; font-size: 12px; }
.btn-primary {
border-color: var(--color-primary);
border-color: rgba(100, 108, 255, 0.75);
color: #ffffff;
background: var(--color-primary);
background: linear-gradient(135deg, #646cff 0%, #bd34fe 100%);
box-shadow: 0 0 24px rgba(100, 108, 255, 0.2);
font-weight: 600;
}
.btn-primary:hover:not(:disabled) {
border-color: var(--color-primary-dark);
background: var(--color-primary-dark);
border-color: rgba(65, 209, 255, 0.82);
background: linear-gradient(135deg, #747bff 0%, #c966ff 100%);
color: #ffffff;
}
.btn-secondary {
border-color: rgba(65, 209, 255, 0.32);
color: #b9e9ff;
background: linear-gradient(180deg, rgba(65, 209, 255, 0.12), rgba(100, 108, 255, 0.1));
font-weight: 700;
}
.btn-secondary:hover:not(:disabled) {
border-color: rgba(65, 209, 255, 0.64);
color: #ffffff;
background: linear-gradient(180deg, rgba(65, 209, 255, 0.18), rgba(100, 108, 255, 0.16));
}
.panel {
min-width: 0;
min-height: 0;
border: 1px solid var(--color-border);
border-radius: 10px;
background: var(--color-surface);
border-radius: 12px;
background:
linear-gradient(180deg, rgba(255,255,255,0.045), transparent 72%),
var(--color-surface);
box-shadow: 0 18px 60px rgba(0,0,0,0.24), inset 0 1px 0 rgba(255,255,255,0.05);
backdrop-filter: blur(16px);
}
.section-head {
@@ -146,6 +169,7 @@
min-height: 46px;
padding: 8px 12px;
border-bottom: 1px solid var(--color-border-subtle);
background: linear-gradient(90deg, rgba(100,108,255,0.09), rgba(65,209,255,0.025));
}
.section-head h2 { margin: 1px 0 0; color: var(--color-ink); font-size: 14px; line-height: 1.2; font-weight: 600; }
@@ -209,14 +233,14 @@
/* --- App shell --- */
.shell { display: flex; flex-direction: column; height: 100%; background: var(--color-surface-alt); }
.shell { display: flex; flex-direction: column; height: 100%; background: transparent; }
.shell-body {
display: flex;
flex-direction: column;
flex: 1 1 0;
min-height: 0;
padding: 0 10px 10px;
padding: 8px 12px 12px;
gap: 8px;
}
@@ -226,8 +250,10 @@
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
height: 38px;
background: var(--color-bar);
height: 42px;
background:
linear-gradient(90deg, rgba(100,108,255,0.16), rgba(65,209,255,0.08), rgba(255,211,67,0.035)),
var(--color-bar);
color: var(--color-bar-text);
user-select: none;
flex-shrink: 0;
@@ -246,7 +272,7 @@
color: var(--color-bar-text);
border-right: 1px solid rgba(255,255,255,0.06);
}
.titlebar-brand svg { color: #5a8cf8; flex-shrink: 0; }
.titlebar-brand svg { color: #ffd343; filter: drop-shadow(0 0 10px rgba(255,211,67,0.3)); flex-shrink: 0; }
.titlebar-info {
display: flex;
@@ -257,17 +283,17 @@
height: 100%;
overflow: hidden;
}
.titlebar-info svg { color: #5a8cf8; flex-shrink: 0; }
.titlebar-info svg { color: #41d1ff; flex-shrink: 0; }
.tb-repo { color: #506070; font-size: 12px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 160px; }
.tb-sep { color: #2a3440; font-size: 13px; }
.tb-branch { color: #c0cce0; font-size: 12px; font-weight: 700; font-family: var(--font-mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 220px; }
.tb-repo { color: #8e95c9; font-size: 12px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 160px; }
.tb-sep { color: rgba(255,255,255,0.22); font-size: 13px; }
.tb-branch { color: #f5f7ff; font-size: 12px; font-weight: 700; font-family: var(--font-mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 220px; }
.tb-sync { display: inline-flex; align-items: center; padding: 1px 6px; border-radius: 999px; font-size: 11px; font-weight: 800; white-space: nowrap; flex-shrink: 0; }
.tb-sync.ahead { color: #e0a040; background: rgba(224,160,64,0.13); }
.tb-sync.behind { color: #7aacff; background: rgba(122,172,255,0.13); }
.tb-no-repo { color: #3a4a5a; font-size: 12px; font-style: italic; }
.tb-no-repo { color: #7b84b2; font-size: 12px; font-style: italic; }
.titlebar-right { display: flex; align-items: stretch; height: 100%; border-left: 1px solid rgba(255,255,255,0.06); }
.titlebar-actions { display: flex; align-items: stretch; }
@@ -319,15 +345,25 @@
/* --- Top bar (repo form) --- */
.topbar { padding: 7px 10px; border: 1px solid var(--color-border); border-radius: 8px; background: var(--color-surface); }
.topbar {
padding: 9px 10px;
border: 1px solid rgba(100,108,255,0.28);
border-radius: 12px;
background:
linear-gradient(90deg, rgba(189,52,254,0.1), rgba(65,209,255,0.075), rgba(255,211,67,0.035)),
var(--color-surface);
box-shadow: inset 0 1px 0 rgba(255,255,255,0.05);
backdrop-filter: blur(16px);
}
.repo-form {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
grid-template-columns: auto minmax(0, 1fr) auto auto;
align-items: center;
gap: 9px;
}
.repo-form label { color: var(--color-ink-faint); font-size: 11.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; white-space: nowrap; }
.repo-browse { min-width: 104px; }
/* --- Notices --- */
@@ -367,6 +403,7 @@
display: grid;
grid-template-columns: clamp(220px, 18vw, 280px) minmax(0, 1fr) clamp(400px, 40vw, 620px);
flex: 1 1 0;
min-width: 0;
min-height: 0;
gap: 8px;
}
@@ -374,6 +411,7 @@
.history-aside {
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
min-width: 0;
min-height: 0;
gap: 8px;
}
@@ -381,6 +419,7 @@
.left-sidebar {
display: grid;
grid-template-rows: minmax(200px, 0.9fr) minmax(240px, 1.1fr);
min-width: 0;
min-height: 0;
gap: 8px;
}
@@ -390,6 +429,7 @@
.main-panel {
display: grid;
grid-template-rows: auto minmax(0, 1fr) auto;
min-width: 0;
min-height: 0;
border: 1px solid var(--color-border);
border-radius: 10px;
@@ -619,9 +659,9 @@
/* --- Commit history --- */
.history-list { padding: 6px; overflow: auto; }
.history-list { min-width: 0; padding: 6px; overflow: auto; }
.commit-row { display: grid; gap: 8px; padding: 10px; border: 1px solid var(--color-border-subtle); border-radius: 8px; background: var(--color-surface-raised); transition: border-color 120ms; }
.commit-row { display: grid; min-width: 0; gap: 8px; padding: 10px; border: 1px solid var(--color-border-subtle); border-radius: 8px; background: var(--color-surface-raised); transition: border-color 120ms; }
.commit-row + .commit-row { margin-top: 5px; }
.commit-row:hover { border-color: var(--color-border); }
.commit-row.compact { padding: 8px; }
@@ -629,6 +669,7 @@
.commit-line { display: flex; align-items: flex-start; min-width: 0; gap: 8px; }
.commit-line svg { flex: 0 0 auto; margin-top: 2px; color: var(--color-accent); }
.commit-line div { min-width: 0; }
.commit-line-text { max-width: 100%; overflow: hidden; }
.commit-line strong { display: block; overflow: hidden; color: var(--color-ink); font-size: 13px; line-height: 1.3; text-overflow: ellipsis; white-space: nowrap; }
.commit-line span { display: block; overflow: hidden; margin-top: 3px; color: var(--color-ink-dim); font-family: var(--font-mono); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
@@ -643,9 +684,40 @@
.commit-file-button { display: grid; grid-template-columns: auto minmax(0, 1fr); justify-content: stretch; width: 100%; min-height: 28px; padding: 4px 7px; text-align: left; border-color: var(--color-border-subtle); background: rgba(255,255,255,0.025); }
.commit-file-button strong { overflow: hidden; color: var(--color-ink-muted); font-family: var(--font-mono); font-size: 11.5px; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
.commit-actions { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.commit-actions time { overflow: hidden; color: var(--color-ink-faint); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
.commit-action-buttons { display: flex; align-items: center; gap: 5px; }
.commit-actions { display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: 8px; }
.commit-actions time { min-width: 0; overflow: hidden; color: var(--color-ink-faint); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
.commit-action-buttons { display: flex; flex: 0 0 auto; align-items: center; gap: 5px; }
.commit-action-buttons button { flex: 0 0 auto; white-space: nowrap; }
.file-history-head { align-items: flex-start; }
.file-history-heading { min-width: 0; flex: 1 1 auto; overflow: hidden; }
.section-head .file-history-name {
display: -webkit-box;
max-height: 2.4em;
margin: 2px 0 0;
overflow: hidden;
color: var(--color-ink);
font-size: 14px;
font-weight: 700;
line-height: 1.2;
overflow-wrap: anywhere;
word-break: break-word;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.file-history-count { flex: 0 0 auto; margin-top: 2px; }
.file-history-row { grid-template-columns: minmax(0, 1fr); min-width: 0; overflow: hidden; }
.file-history-row .commit-line { overflow: hidden; }
.file-history-row .commit-line strong {
display: -webkit-box;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.file-history-actions { display: grid; grid-template-columns: minmax(0, 1fr); width: 100%; align-items: center; gap: 6px; }
.file-history-actions .commit-action-buttons { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); width: 100%; justify-content: stretch; }
.file-history-actions .commit-action-buttons button { min-width: 0; justify-content: center; padding-inline: 6px; }
/* --- Git graph --- */
@@ -739,6 +811,10 @@
width: min(1560px, calc(100vw - 32px));
height: min(940px, calc(100vh - 32px));
}
.global-search-dialog {
width: min(1180px, calc(100vw - 32px));
height: min(840px, calc(100vh - 32px));
}
.dialog-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 16px; border-bottom: 1px solid var(--color-border-subtle); background: var(--color-surface-dim); }
@@ -890,6 +966,165 @@
.prepared-tag { display: inline-flex; align-items: center; gap: 4px; margin-right: auto; color: #4eca76; font-size: 12px; font-weight: 700; }
.global-search-body {
display: grid;
grid-template-rows: auto minmax(0, 1fr);
min-width: 0;
min-height: 0;
}
.global-search-form {
display: grid;
gap: 10px;
padding: 12px;
border-bottom: 1px solid var(--color-border-subtle);
background: linear-gradient(90deg, rgba(100,108,255,0.08), rgba(65,209,255,0.035));
}
.global-search-query { display: grid; gap: 6px; min-width: 0; }
.global-search-query span,
.search-limit span {
color: var(--color-ink-faint);
font-size: 10.5px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.global-search-query textarea {
min-height: 112px;
max-height: 240px;
resize: vertical;
font-family: var(--font-mono);
font-size: 12px;
white-space: pre;
overflow: auto;
}
.global-search-options {
display: grid;
grid-template-columns: minmax(0, 1fr) 120px auto auto;
align-items: end;
gap: 10px;
}
.check-row {
display: inline-flex;
align-items: center;
gap: 8px;
min-width: 0;
color: var(--color-ink-muted);
font-size: 12px;
font-weight: 700;
}
.check-row input { width: 15px; height: 15px; flex: 0 0 auto; }
.search-limit { display: grid; gap: 5px; }
.search-cancel {
border-color: rgba(232,96,96,0.35);
color: #ef9090;
background: rgba(232,96,96,0.1);
}
.search-cancel:hover:not(:disabled) {
border-color: rgba(232,96,96,0.55);
color: #ffb0b0;
background: rgba(232,96,96,0.16);
}
.global-search-results {
min-width: 0;
min-height: 0;
overflow: auto;
padding: 10px;
}
.search-result-head {
display: flex;
align-items: center;
gap: 7px;
min-width: 0;
margin-bottom: 8px;
color: var(--color-ink-dim);
font-size: 12px;
font-weight: 700;
}
.search-result-head strong { color: var(--color-accent); font-family: var(--font-mono); }
.search-result-head span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.search-error { color: #ef9090; }
.search-hit-list { display: grid; gap: 8px; }
.search-hit {
display: grid;
gap: 7px;
min-width: 0;
padding: 10px;
border: 1px solid var(--color-border-subtle);
border-radius: 8px;
background: var(--color-surface-raised);
}
.search-hit-top {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 8px;
min-width: 0;
}
.search-hit-top .hash {
padding: 2px 7px;
border: 1px solid rgba(90,140,248,0.22);
border-radius: 6px;
color: var(--color-accent);
background: rgba(90,140,248,0.13);
font-family: var(--font-mono);
font-size: 11.5px;
font-weight: 800;
}
.search-hit-top strong {
min-width: 0;
overflow: hidden;
color: var(--color-ink);
font-size: 13px;
text-overflow: ellipsis;
white-space: nowrap;
}
.search-hit-meta {
display: flex;
flex-wrap: wrap;
gap: 8px 12px;
color: var(--color-ink-faint);
font-size: 11.5px;
}
.search-hit-meta span,
.search-hit-file {
display: inline-flex;
align-items: center;
min-width: 0;
gap: 5px;
}
.search-hit-meta svg,
.search-hit-file svg { flex: 0 0 auto; color: var(--color-ink-faint); }
.search-hit-file {
overflow: hidden;
color: var(--color-ink-muted);
font-family: var(--font-mono);
font-size: 12px;
}
.search-hit-file span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.search-hit-file strong { flex: 0 0 auto; color: var(--color-accent); }
.search-hit-line {
overflow: auto;
margin: 0;
padding: 8px 10px;
border-radius: 7px;
color: #5dd88a;
background: rgba(78,202,118,0.08);
font-family: var(--font-mono);
font-size: 12px;
line-height: 1.45;
white-space: pre;
}
/* --- Credential dialog --- */
.cred-card {
@@ -1415,6 +1650,7 @@
.compare-arrow { display: none; }
.dialog-body { grid-template-columns: 1fr; grid-template-rows: minmax(120px, 0.4fr) minmax(0, 1fr); }
.compare-dialog .dialog-body { grid-template-columns: 1fr; grid-template-rows: minmax(120px, 0.4fr) minmax(0, 1fr); }
.global-search-options { grid-template-columns: 1fr; }
.dialog-files { border-right: none; border-bottom: 1px solid var(--color-border-subtle); }
.branch-actions { flex-direction: row; justify-content: flex-start; }
.tb-action-label { display: none; }
+13 -1
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import { onDestroy, onMount } from "svelte";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { Download, GitBranch, LoaderCircle, Minus, RefreshCw, Upload, X } from "@lucide/svelte";
import { Download, GitBranch, LoaderCircle, Minus, RefreshCw, Search, Upload, X } from "@lucide/svelte";
export let branch: string = "";
export let ahead: number = 0;
@@ -15,6 +15,7 @@
export let onPull: () => void = () => {};
export let onPush: () => void = () => {};
export let onRefresh: () => void = () => {};
export let onSearch: () => void = () => {};
export let onToggleAutoRefresh: () => void = () => {};
const win = getCurrentWindow();
@@ -80,6 +81,17 @@
<!-- Right: actions + window controls -->
<div class="titlebar-right">
<div class="titlebar-actions" role="toolbar" aria-label="Repository actions">
<button
class="tb-action"
onclick={onSearch}
disabled={!hasRepository || isBusy}
title="Global search"
aria-label="Global search"
>
<Search size={14} aria-hidden="true" />
<span class="tb-action-label">Search</span>
</button>
<button
class="tb-action"
onclick={onPull}
+7 -7
View File
@@ -74,15 +74,15 @@
</script>
<section class="panel grid grid-rows-[auto_1fr] overflow-hidden" aria-label="Selected file history">
<div class="section-head">
<div class="min-w-0 flex-1 overflow-hidden">
<div class="section-head file-history-head">
<div class="file-history-heading">
<span class="eyebrow">{selectedExplorerLabel}</span>
<h2
class="mt-0.5 text-ink text-base font-bold leading-tight truncate"
class="file-history-name"
use:pathTooltip={selectedExplorerPath}
>{selectedExplorerPath ? fileName(selectedExplorerPath) : "No file"}</h2>
</div>
<span class="pill pill-count flex-shrink-0">{fileHistory.length}</span>
<span class="pill pill-count file-history-count">{fileHistory.length}</span>
</div>
{#if !hasRepository}
@@ -94,16 +94,16 @@
{:else}
<div class="history-list overflow-auto p-2">
{#each fileHistory as item (item.hash)}
<article class="commit-row compact">
<article class="commit-row compact file-history-row">
<div class="commit-line">
<History size={16} aria-hidden="true" />
<div>
<div class="commit-line-text">
<strong title={item.summary}>{item.summary}</strong>
<span>{item.short_hash} - {item.author_name}</span>
</div>
</div>
<div class="commit-actions">
<div class="commit-actions file-history-actions">
<time datetime={item.date}>{formatCommitDate(item.date)}</time>
<div class="commit-action-buttons">
<button class="btn-sm" type="button" onclick={() => onDiff(item)} disabled={isBusy} title="Show changes vs working tree">
@@ -0,0 +1,174 @@
<script lang="ts">
import { CalendarDays, FileCode, LoaderCircle, Search, User, X } from "@lucide/svelte";
import type { GitSearchHit } from "../types";
interface Props {
hasRepository: boolean;
isBusy: boolean;
isSearching: boolean;
error: string;
results: GitSearchHit[];
onClose: () => void;
onSearch: (query: string, caseSensitive: boolean, limit: number) => void | Promise<void>;
onCancel: () => void | Promise<void>;
}
let {
hasRepository = false,
isBusy = false,
isSearching = false,
error = "",
results = [],
onClose = () => {},
onSearch = () => {},
onCancel = () => {},
}: Props = $props();
let query = $state("");
let caseSensitive = $state(false);
let limit = $state(250);
let searchedQuery = $state("");
let searched = $state(false);
function submit(event?: SubmitEvent) {
event?.preventDefault();
const value = query.trim();
if (!value || !hasRepository || isBusy || isSearching) return;
searched = true;
searchedQuery = value;
void onSearch(value, caseSensitive, limit);
}
function handleKeydown(event: KeyboardEvent) {
if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
submit();
}
}
function formatCommitDate(value: string): string {
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(date);
}
function displayPath(hit: GitSearchHit): string {
return hit.old_file ? `${hit.old_file} -> ${hit.file}` : hit.file;
}
</script>
<div
class="dialog-backdrop"
role="presentation"
onclick={(e) => { if (e.target === e.currentTarget) onClose(); }}
>
<div class="dialog global-search-dialog" role="dialog" aria-modal="true" aria-label="Global code search" tabindex="-1">
<header class="dialog-header">
<div>
<span class="eyebrow">Global search</span>
<h2 class="dialog-title">Find where code was introduced</h2>
</div>
<button class="dialog-close" type="button" onclick={onClose} title="Close">
<X size={18} aria-hidden="true" />
</button>
</header>
<div class="global-search-body">
<form class="global-search-form" onsubmit={submit}>
<label class="global-search-query">
<span>String or function</span>
<textarea
bind:value={query}
onkeydown={handleKeydown}
disabled={!hasRepository || isBusy || isSearching}
spellcheck="false"
placeholder={"Paste a string, symbol, or full function body..."}
></textarea>
</label>
<div class="global-search-options">
<label class="check-row">
<input type="checkbox" bind:checked={caseSensitive} disabled={!hasRepository || isBusy || isSearching} />
<span>Exact case</span>
</label>
<label class="search-limit">
<span>Results</span>
<select bind:value={limit} disabled={!hasRepository || isBusy || isSearching}>
<option value={100}>100</option>
<option value={250}>250</option>
<option value={500}>500</option>
<option value={1000}>1000</option>
</select>
</label>
<button class="btn-primary" type="submit" disabled={!hasRepository || isBusy || isSearching || query.trim().length === 0}>
{#if isSearching}
<LoaderCircle class="spin" size={16} aria-hidden="true" />
{:else}
<Search size={16} aria-hidden="true" />
{/if}
Search
</button>
{#if isSearching}
<button class="btn-secondary search-cancel" type="button" onclick={onCancel}>
<X size={16} aria-hidden="true" />
Cancel
</button>
{/if}
</div>
</form>
<section class="global-search-results" aria-live="polite">
{#if !hasRepository}
<div class="blank-state">Open a repository first.</div>
{:else if isSearching}
<div class="blank-state">
<LoaderCircle class="spin" size={20} aria-hidden="true" />
Searching all branches...
</div>
{:else if error}
<div class="blank-state search-error">{error}</div>
{:else if !searched}
<div class="blank-state">Search a string or paste a complete function to find where it was added.</div>
{:else if results.length === 0}
<div class="blank-state">No introduction found for "{searchedQuery}".</div>
{:else}
<div class="search-result-head">
<strong>{results.length}</strong>
<span>{results.length === 1 ? "introduction" : "introductions"} found for "{searchedQuery}"</span>
</div>
<div class="search-hit-list">
{#each results as hit (`${hit.commit_hash}:${hit.file}:${hit.line_number ?? 0}`)}
<article class="search-hit">
<header class="search-hit-top">
<span class="hash">{hit.short_hash}</span>
<strong title={hit.summary}>{hit.summary || "No commit message"}</strong>
{#if hit.matches_added > 1}
<span class="pill pill-active">+{hit.matches_added} matches</span>
{/if}
</header>
<div class="search-hit-meta">
<span><User size={12} aria-hidden="true" />{hit.author_name || "Unknown author"}</span>
<span><CalendarDays size={12} aria-hidden="true" />{formatCommitDate(hit.date)}</span>
</div>
<div class="search-hit-file" title={displayPath(hit)}>
<FileCode size={14} aria-hidden="true" />
<span>{displayPath(hit)}</span>
{#if hit.line_number}
<strong>:{hit.line_number}</strong>
{/if}
</div>
<pre class="search-hit-line">{hit.line || searchedQuery}</pre>
</article>
{/each}
</div>
{/if}
</section>
</div>
</div>
</div>
+21
View File
@@ -6,6 +6,7 @@ import type {
GitCommit,
GitCommitComparison,
GitRepositoryFile,
GitSearchHit,
GitStatus,
} from "./types";
@@ -97,6 +98,26 @@ export function diffFileAgainstWorkingTree(
return invoke<GitCommitComparison>("diff_file_against_working_tree", { path, commit, file });
}
export function searchCodeIntroductions(
path: string,
query: string,
caseSensitive = false,
limit = 250,
searchId?: string,
): Promise<GitSearchHit[]> {
return invoke<GitSearchHit[]>("search_code_introductions", {
path,
query,
caseSensitive,
limit,
searchId: searchId ?? null,
});
}
export function cancelCodeSearch(searchId: string): Promise<void> {
return invoke<void>("cancel_code_search", { searchId });
}
export function readConflict(path: string, file: string): Promise<ConflictFile> {
return invoke<ConflictFile>("read_conflict", { path, file });
}
+14
View File
@@ -71,6 +71,20 @@ export interface GitCommitComparison {
patch: string;
}
export interface GitSearchHit {
commit_hash: string;
short_hash: string;
summary: string;
author_name: string;
author_email: string;
date: string;
file: string;
old_file: string | null;
line_number: number | null;
line: string;
matches_added: number;
}
export type ExplorerNodeKind = "folder" | "file";
export interface ExplorerNode {