add new style and global search
This commit is contained in:
+102
-4
@@ -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
@@ -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
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user