mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-12 08:36:25 -04:00
The spec regained file-header (f) and hunk-header (h) records: f never carries a line number, h always does (the first line of the hunk it heads). Accept them in the OSC metadata backend, so a conforming pager's header rows resolve to the same DiffLineFileHeader/DiffLineHunkHeader identities the buffer parser already reports for raw diffs. With header rows located, next/previous file navigation and the jump-to-file menu land on a file's header row, and header rows become usable scroll-restore anchors. The consumers need a few adjustments: - File navigation used to reach a file's top by backing up over the untagged rows above its first located row. With tagged headers that overshoots onto the blank separator row above the file header, so drop the back-up (backUpOverHeader) and land on the first located row itself: the header for any conforming source, or the first content line under a pager that leaves its headers untagged — an accepted degradation for non-conforming pagers, now that the spec makes f/h mandatory. - SamePatchLine now requires headers to match headers of the same kind. A hunk header shares its line number with the hunk's first content line (and a file header shares "0" with a deleted file's hunk header), so without this a position restore aiming at one could land on the other. This also applied to raw diffs before, but headers used to be unlikely restore targets; now that navigation deliberately lands on them, the ambiguity would bite. - Editing a file-header row opens the file without jumping to a line, like pressing edit on a whole file in a side panel. (It used to open at line 1 for raw diffs, where headers resolved already.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
117 lines
5.1 KiB
Go
117 lines
5.1 KiB
Go
package helpers
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestChangeBlockStart(t *testing.T) {
|
|
// A diff with three change blocks separated by context:
|
|
// 0 file header 1 hunk header 2 context
|
|
// 3 + 4 + (block A)
|
|
// 5 context
|
|
// 6 - (block B)
|
|
// 7 context
|
|
// 8 + (block C)
|
|
isChange := []bool{false, false, false, true, true, false, true, false, true}
|
|
|
|
scenarios := []struct {
|
|
name string
|
|
from int
|
|
forward bool
|
|
expected int
|
|
found bool
|
|
}{
|
|
{"forward from a header lands on the first block", 0, true, 3, true},
|
|
{"forward from separating context lands on the next block", 5, true, 6, true},
|
|
{"forward from the start of a block skips to the next", 3, true, 6, true},
|
|
{"forward from inside a block skips the rest of it", 4, true, 6, true},
|
|
{"forward from the last block finds nothing", 8, true, 0, false},
|
|
{"backward from a later block lands on the previous one's start", 8, false, 6, true},
|
|
{"backward from a block start lands on the previous block's start", 6, false, 3, true},
|
|
{"backward from inside the first block finds nothing", 4, false, 0, false},
|
|
{"backward from the first block's start finds nothing", 3, false, 0, false},
|
|
{"backward from context lands on the preceding block's start", 7, false, 6, true},
|
|
}
|
|
|
|
for _, s := range scenarios {
|
|
t.Run(s.name, func(t *testing.T) {
|
|
got, found := changeBlockStart(isChange, s.from, s.forward)
|
|
assert.Equal(t, s.found, found)
|
|
if s.found {
|
|
assert.Equal(t, s.expected, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFileStart(t *testing.T) {
|
|
// A parseable two-file diff: every row carries its file's path (the headers
|
|
// included), as the buffer parser reports.
|
|
parseable := []string{"a", "a", "a", "a", "b", "b", "b", "b"}
|
|
|
|
// The same diff as a pager without `f`/`h` header records emits it: only content
|
|
// lines carry the path; the file/hunk header rows above each file are untagged
|
|
// (empty), so navigation can only land on each file's first content line.
|
|
tagged := []string{"", "", "a", "a", "", "", "b", "b"}
|
|
|
|
// Three such files, to exercise navigating from one file's untagged header to the
|
|
// next: the row just above b's header is a's content, so the anchor file must be
|
|
// found by scanning down (b), not up (a) — otherwise next-file would jump back
|
|
// into b and a second `n` couldn't advance.
|
|
taggedThree := []string{"", "", "a", "a", "", "", "b", "b", "", "", "c", "c"}
|
|
|
|
// A pager that tags its header rows with `f`/`h` records (delta): the two-row
|
|
// file header and the hunk-header box carry the file's path, but the blank
|
|
// separator rows — above each file header, and between it and the first hunk
|
|
// header — carry nothing. Navigation must land on the header's first row, not
|
|
// the blank line above it.
|
|
// 0 blank 1 file hdr 2 file hdr 3 blank 4-6 hunk hdr box 7 content
|
|
// 8 blank 9 file hdr 10 file hdr 11 blank 12-13 hunk hdr box 14 content
|
|
headerTagged := []string{"", "a", "a", "", "a", "a", "a", "a", "", "b", "b", "", "b", "b", "b"}
|
|
|
|
scenarios := []struct {
|
|
name string
|
|
paths []string
|
|
from int
|
|
forward bool
|
|
expected int
|
|
found bool
|
|
}{
|
|
{"parseable: next file lands on its header", parseable, 1, true, 4, true},
|
|
{"parseable: next from the last file finds nothing", parseable, 6, true, 0, false},
|
|
{"parseable: previous file lands on its header", parseable, 5, false, 0, true},
|
|
{"parseable: previous from the first file finds nothing", parseable, 1, false, 0, false},
|
|
|
|
// With only content tagged, both directions land on the file's first content
|
|
// line — the untagged header rows above it can't be told apart from the
|
|
// blank separator rows, so they are never a landing spot.
|
|
{"tagged: next file lands on its first content line", tagged, 2, true, 6, true},
|
|
{"tagged: next from an untagged header still advances", tagged, 0, true, 6, true},
|
|
{"tagged: previous file lands on its first content line", tagged, 7, false, 2, true},
|
|
{"tagged: previous from the first file finds nothing", tagged, 2, false, 0, false},
|
|
|
|
// From b's untagged header (row 4), the anchor file is b (below), so next goes
|
|
// to c and previous goes to a — neither sticks on b.
|
|
{"tagged: next from a middle file's header advances past it", taggedThree, 4, true, 10, true},
|
|
{"tagged: previous from a middle file's header lands on the prior file", taggedThree, 4, false, 2, true},
|
|
|
|
// With headers tagged, both directions land on the header's first row —
|
|
// crucially not on the blank separator row above it.
|
|
{"header-tagged: next file lands on the header, not the blank above it", headerTagged, 7, true, 9, true},
|
|
{"header-tagged: next from inside a file's header advances", headerTagged, 2, true, 9, true},
|
|
{"header-tagged: previous file lands on the header, not the blank above it", headerTagged, 14, false, 1, true},
|
|
}
|
|
|
|
for _, s := range scenarios {
|
|
t.Run(s.name, func(t *testing.T) {
|
|
got, found := fileStart(s.paths, s.from, s.forward)
|
|
assert.Equal(t, s.found, found)
|
|
if s.found {
|
|
assert.Equal(t, s.expected, got)
|
|
}
|
|
})
|
|
}
|
|
}
|