feat(diff): enable line-level patch selection and actions

Introduces granular control over diff operations by enabling users to select individual lines within a hunk. This refactors the core logic across multiple components, allowing for precise staging, unstaging, or discarding of specific changes rather than operating on entire hunks. The UI now features dedicated controls and visual feedback for line-level selection.

- Refactored patch parsing to track selected line IDs and calculate range information.
- Updated the diff view CSS and component structure to display interactive line selection bars.
- Extended action dialogs to support confirming operations on selected lines.
This commit is contained in:
Christoph Brandau
2026-07-26 22:10:33 +02:00
parent 38b7f1a536
commit 24201c1302
5 changed files with 350 additions and 70 deletions
+75 -3
View File
@@ -4167,11 +4167,37 @@
.line-patch-body {
display: grid;
grid-template-rows: minmax(0, 1fr);
grid-template-rows: auto minmax(0, 1fr);
min-height: 0;
overflow: hidden;
}
.line-patch-selection-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
min-height: 46px;
padding: 7px 12px;
border-bottom: 1px solid var(--color-border-subtle);
background: var(--app-dialog-chrome);
}
.line-patch-selection-bar.active {
background:
linear-gradient(90deg, rgba(77, 182, 214, 0.1), transparent 44%),
var(--app-dialog-chrome);
}
.line-patch-selection-bar > div:first-child {
display: flex;
align-items: center;
gap: 7px;
min-width: 0;
color: var(--color-accent);
}
.line-patch-selection-bar strong { color: var(--color-ink); font-size: 11.5px; }
.line-patch-selection-bar span { color: var(--color-ink-faint); font-size: 10px; }
.line-patch-selected-actions { display: flex; align-items: center; justify-content: flex-end; gap: 6px; }
.line-patch-scroll {
min-height: 0;
overflow: auto;
@@ -4197,6 +4223,33 @@
border-bottom: 1px solid var(--color-border-subtle);
background: color-mix(in srgb, var(--code-surface-raised) 96%, transparent);
}
.line-patch-select-hunk,
.line-patch-line-select {
display: grid;
place-items: center;
width: 16px;
min-width: 16px;
height: 16px;
min-height: 16px;
padding: 0;
border: 1px solid var(--color-border);
border-radius: 4px;
color: #ffffff;
background: var(--code-surface);
box-shadow: none;
}
.line-patch-select-hunk:hover:not(:disabled),
.line-patch-line-select:hover:not(:disabled) {
border-color: rgba(77, 182, 214, 0.65);
background: rgba(77, 182, 214, 0.1);
}
.line-patch-select-hunk.all,
.line-patch-select-hunk.some,
.line-patch-line-select[aria-pressed="true"] {
border-color: rgba(77, 182, 214, 0.78);
background: #238eb4;
}
.line-patch-select-hunk.some { background: rgba(35, 142, 180, 0.55); }
.line-patch-hunk-head code {
color: var(--color-accent);
font-family: var(--font-mono);
@@ -4242,12 +4295,17 @@
.line-patch-row {
display: grid;
grid-template-columns: 22px minmax(max-content, 1fr);
grid-template-columns: 18px 38px 38px 22px minmax(max-content, 1fr);
align-items: start;
min-height: 22px;
padding: 1px 10px 1px 28px;
padding: 1px 10px 1px 8px;
color: var(--color-ink-muted);
}
.line-patch-row.selectable { cursor: default; }
.line-patch-row.selected {
box-shadow: inset 3px 0 0 rgba(77, 182, 214, 0.86);
filter: saturate(1.12) brightness(1.06);
}
.line-patch-row.add {
background: var(--code-add-bg);
color: var(--code-add-text);
@@ -4264,12 +4322,26 @@
text-align: center;
user-select: none;
}
.line-patch-line-select { align-self: center; }
.line-patch-line-select-placeholder { width: 16px; }
.line-patch-line-number {
padding-right: 7px;
color: var(--color-ink-faint);
font-size: 10px;
line-height: 20px;
text-align: right;
user-select: none;
}
.line-patch-row.add .line-patch-prefix { color: var(--code-add-strong); }
.line-patch-row.delete .line-patch-prefix { color: var(--code-delete-strong); }
.line-patch-row code {
white-space: pre;
font-family: var(--font-mono);
}
@media (max-width: 760px) {
.line-patch-selection-bar { align-items: stretch; flex-direction: column; }
.line-patch-selected-actions { justify-content: flex-start; flex-wrap: wrap; }
}
.blame-body {
min-height: 0;