Commit Graph
4 Commits
Author SHA1 Message Date
l0ng-ai 87dc26f061 feat(diff): let the pointer take a range of diff lines and copy it (#721)
The overlay drew its patch as a grid of independent text elements, so
there was no way to get code out of it: no selection, no clipboard, and
nothing to right-click. Reading a diff and then retyping what you read
is not a workflow.

Pressing on a row starts a range, dragging extends it, and Ctrl/Cmd+C
copies it — as does the new Copy Selected Lines item on the card's
context menu. The rows carry an I-beam so the offer is visible before
anyone tries it.

What lands on the clipboard is the file's own text: no +/- marker, no
line numbers, and tabs left as the file wrote them rather than the four
spaces the grid draws. In the split view a drag reads the column it
started in, so the left one gives the code as it was and the right one
as it is; in the unified view a row is a line. A range never spans two
cards, and it is dropped when the view mode flips or a fresh patch
lands, since either one re-cuts the rows underneath it.

Selection is line-granular. Half a line, or a range spanning two files,
is not on offer here.
2026-09-07 18:48:42 +08:00
l0ng-ai 45879d1c22 feat(diff): add a unified view and name the source in the header
The overlay could only ever show one thing one way: `git diff HEAD`, side
by side. Both halves of that are now choices.

A segmented control in the header switches between side-by-side and
unified, stored in `Config::diff_view` so the choice survives the next
open. Unified is measured against the split cell rather than designed
next to it: the same 19px row, the same type, and the same 0.12 wash
behind an addition and a removal. It differs only where the shape forces
it — 34px per line-number gutter instead of 42, and a column of its own
for `+`/`−`, without which the context lines' code would start two
characters left of everything else.

The header now says which patch it is showing. A branch name for the
worktree and for HEAD as before; the same with a STAGED chip for the
index, which is otherwise indistinguishable; the commit glyph and a
short object id for a commit or a range. The subject and author of a
commit are not there yet — `DiffSource::Commit` carries only the rev,
and buying them costs another round trip that the commit detail view
will be making anyway.

Two pieces of coupling go with it. `PANEL_DIFF_SOURCE` no longer decides
whether the panel's snapshot may seed an overlay, or whether an overlay
has gone stale: the first is settled by the snapshot's own source, the
second by the overlay's. A commit and a range never go stale at all, and
the two sources the cached `--numstat HEAD` counts cannot describe now
compare `ScmData` epochs instead — read when the probe starts, so a
write landing under it is not mistaken for one the result reflects.

And the file cards drop their private A/M/D/R table for the shared
`status_glyph`/`status_color`, so a file wears the same letter here as
in the panel and the tree.
2026-08-09 01:19:50 +08:00
l0ng-ai 58e8ba16d1 feat(git): ask for three kinds of diff, and lift the row model out of the renderer
`probe` could only ever run `git diff HEAD`. A `DiffRequest` now names the
source (worktree, staged, HEAD, one commit, a range), the pathspecs, the
context width and the budget, and `DiffSource::args` is the single place any
diff argv is built — which is also the single place to test it.

Three things were verified against git 2.50.1 rather than assumed:

- `-c core.quotePath=false` is a real bug fix, not tidiness. With quoting on,
  `diff --git "a/\344\270\255\346\226\207\345\220\215.txt" …`; off, `diff --git
  a/中文名.txt b/中文名.txt`. `parse_quoted_pair` does not decode octal, so
  every non-ASCII path in the overlay was simply wrong. `ls-files` quotes the
  same way, so the untracked listing gets the flag too.

- `Commit` runs `log -p -1 --format= --first-parent`, not `diff-tree`.
  `diff-tree` does not honour `--first-parent` as a narrowing of a merge: over
  a merge of two branches that each added a file, `diff-tree -p -m
  --first-parent` emits both files (one patch per parent, concatenated) and
  dropping `-m` emits nothing at all. `log -p -1 --first-parent` gives the one
  first-parent patch for a merge, an ordinary commit and the initial commit
  alike, so there is no special case and no `--root`.

- A type change is not one patch. git emits a deletion and a creation for the
  same path, back to back, with nothing in either header saying why; the pair
  is folded back into a single `TypeChanged` entry. `old mode`/`new mode` only
  appears for permission changes, which stay `Modified`.

`FileStatus` also gains `Copied` and `Unmerged`. A conflicted path arrives as a
combined diff (`diff --cc`, `@@@`, one marker column per parent), so the body
parser reads its marker width from the hunk header instead of assuming one.

The budget is a parameter now (`DiffBudget::PANEL`, `::SINGLE_FILE`) rather
than three module constants read inside the parser. Defaults are unchanged.

On the UI side, the overlay's reuse test was `(cwd, host)` — clicking a staged
file while a worktree overlay was open took the "just move the focus" branch
and went on showing the unstaged patch under the staged file's name. The source
is part of an overlay's identity now, in that filter, in the panel-seed
shortcut and in the in-flight de-duplication key. `maybe_refresh_diff_overlay`
only compares HEAD snapshots against the cached `--numstat HEAD` counts, which
are the only counts they are comparable to.

`split_hunk` and tab expansion move to `ui::diff_rows` alongside a new
`unified_rows`, so the two renderings of a hunk are built and tested in one
place, without a window. The overlay's file list finally hangs on tty7's own
scrollbar instead of a bare `overflow_y_scroll`.
2026-08-09 00:40:21 +08:00
l0ng-ai 3b3e5fa046 chore(ui): reserve the diff_rows and scm modules
Two parallel strands of the source control work each need to add one line to
ui/mod.rs. Landing both declarations up front keeps them from colliding over
it.
2026-08-09 00:14:42 +08:00