Skip to content

Commit e8f6d0c

Browse files
authored
Merge pull request #24 from JanSmrcka/fix/preserve-scroll-on-refresh
fix: preserve diff scroll position during auto-refresh
2 parents da47534 + 131bb55 commit e8f6d0c

2 files changed

Lines changed: 75 additions & 15 deletions

File tree

internal/ui/model.go

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ type tickMsg time.Time
3232

3333
// Messages
3434
type diffLoadedMsg struct {
35-
content string
36-
index int
35+
content string
36+
index int
37+
resetScroll bool
3738
}
3839

3940
type filesRefreshedMsg struct {
@@ -94,6 +95,8 @@ type Model struct {
9495
ready bool
9596
SelectedFile string // set on "open in editor" action, read after Run()
9697

98+
lastDiffContent string
99+
97100
// Branch picker state
98101
branches []string
99102
branchCursor int
@@ -192,7 +195,7 @@ func (m *Model) StartInCommitMode() {
192195
}
193196

194197
func (m Model) Init() tea.Cmd {
195-
cmds := []tea.Cmd{m.loadDiffCmd(), m.fetchUpstreamStatusCmd(), tickCmd()}
198+
cmds := []tea.Cmd{m.loadDiffCmd(true), m.fetchUpstreamStatusCmd(), tickCmd()}
196199
if m.mode == modeCommit {
197200
cmds = append(cmds, textinput.Blink)
198201
}
@@ -249,32 +252,41 @@ func (m Model) handleResize(msg tea.WindowSizeMsg) (tea.Model, tea.Cmd) {
249252
m.width = msg.Width
250253
m.height = msg.Height
251254
m.viewport = viewport.New(m.diffWidth(), m.contentHeight())
255+
m.lastDiffContent = "" // force re-apply after viewport recreation
252256
m.ready = true
253-
return m, m.loadDiffCmd()
257+
return m, m.loadDiffCmd(true)
254258
}
255259

256260
func (m Model) handleDiffLoaded(msg diffLoadedMsg) (tea.Model, tea.Cmd) {
257-
if msg.index == m.cursor {
258-
m.viewport.SetContent(msg.content)
261+
if msg.index != m.cursor {
262+
return m, nil
263+
}
264+
if msg.content == m.lastDiffContent {
265+
return m, nil
266+
}
267+
m.lastDiffContent = msg.content
268+
m.viewport.SetContent(msg.content)
269+
if msg.resetScroll {
259270
m.viewport.GotoTop()
260271
}
261272
return m, nil
262273
}
263274

264275
func (m Model) handleFilesRefreshed(msg filesRefreshedMsg) (tea.Model, tea.Cmd) {
265276
if filesEqual(m.files, msg.files) {
266-
return m, m.loadDiffCmd()
277+
return m, m.loadDiffCmd(false)
267278
}
268279
m.files = msg.files
269280
if m.cursor >= len(m.files) {
270281
m.cursor = max(0, len(m.files)-1)
271282
}
272283
m.prevCurs = -1
284+
m.lastDiffContent = ""
273285
if len(m.files) == 0 {
274286
m.viewport.SetContent("")
275287
return m, nil
276288
}
277-
return m, m.loadDiffCmd()
289+
return m, m.loadDiffCmd(true)
278290
}
279291

280292
func (m Model) handleCommitDone(msg commitDoneMsg) (tea.Model, tea.Cmd) {
@@ -453,7 +465,8 @@ func (m Model) updateFileListMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
453465
case "v":
454466
m.splitDiff = !m.splitDiff
455467
m.prevCurs = -1
456-
return m, tea.Batch(m.loadDiffCmd(), m.saveSplitPrefCmd())
468+
m.lastDiffContent = ""
469+
return m, tea.Batch(m.loadDiffCmd(true), m.saveSplitPrefCmd())
457470
case "F":
458471
if m.upstream.Upstream == "" {
459472
m.statusMsg = "no upstream configured"
@@ -464,7 +477,7 @@ func (m Model) updateFileListMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
464477
}
465478
if m.cursor != m.prevCurs {
466479
m.prevCurs = m.cursor
467-
return m, m.loadDiffCmd()
480+
return m, m.loadDiffCmd(true)
468481
}
469482
return m, nil
470483
}
@@ -493,7 +506,8 @@ func (m Model) updateDiffMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
493506
case "v":
494507
m.splitDiff = !m.splitDiff
495508
m.prevCurs = -1
496-
return m, tea.Batch(m.loadDiffCmd(), m.saveSplitPrefCmd())
509+
m.lastDiffContent = ""
510+
return m, tea.Batch(m.loadDiffCmd(true), m.saveSplitPrefCmd())
497511
}
498512
var cmd tea.Cmd
499513
m.viewport, cmd = m.viewport.Update(msg)
@@ -623,7 +637,7 @@ func (m Model) nextFile() (tea.Model, tea.Cmd) {
623637
if m.cursor < len(m.files)-1 {
624638
m.cursor++
625639
m.prevCurs = m.cursor
626-
return m, m.loadDiffCmd()
640+
return m, m.loadDiffCmd(true)
627641
}
628642
return m, nil
629643
}
@@ -632,13 +646,13 @@ func (m Model) prevFile() (tea.Model, tea.Cmd) {
632646
if m.cursor > 0 {
633647
m.cursor--
634648
m.prevCurs = m.cursor
635-
return m, m.loadDiffCmd()
649+
return m, m.loadDiffCmd(true)
636650
}
637651
return m, nil
638652
}
639653

640654
// Commands
641-
func (m Model) loadDiffCmd() tea.Cmd {
655+
func (m Model) loadDiffCmd(resetScroll bool) tea.Cmd {
642656
if len(m.files) == 0 {
643657
return nil
644658
}
@@ -677,7 +691,7 @@ func (m Model) loadDiffCmd() tea.Cmd {
677691
}
678692
}
679693
}
680-
return diffLoadedMsg{content: content, index: idx}
694+
return diffLoadedMsg{content: content, index: idx, resetScroll: resetScroll}
681695
}
682696
}
683697

internal/ui/model_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,52 @@ func TestHandleBranchesLoaded_Error(t *testing.T) {
317317
}
318318
}
319319

320+
func TestHandleResize_ClearsDiffCache(t *testing.T) {
321+
t.Parallel()
322+
m := newTestModel(t, []fileItem{
323+
{change: git.FileChange{Path: "a.go", Status: git.StatusModified}},
324+
})
325+
m.cursor = 0
326+
327+
// Simulate having cached diff content
328+
m.lastDiffContent = "old diff"
329+
m.viewport.SetContent("old diff")
330+
331+
// Resize creates new viewport — cache must be cleared
332+
result, _ := m.handleResize(tea.WindowSizeMsg{Width: 100, Height: 40})
333+
rm := result.(Model)
334+
335+
if rm.lastDiffContent != "" {
336+
t.Error("handleResize should clear lastDiffContent to force re-apply")
337+
}
338+
339+
// handleDiffLoaded with same content should apply (not skip) after resize
340+
result2, _ := rm.handleDiffLoaded(diffLoadedMsg{content: "old diff", index: 0})
341+
rm2 := result2.(Model)
342+
if rm2.lastDiffContent != "old diff" {
343+
t.Error("handleDiffLoaded should apply content after resize cleared cache")
344+
}
345+
if !strings.Contains(rm2.viewport.View(), "old diff") {
346+
t.Errorf("viewport should contain reapplied content, got %q", rm2.viewport.View())
347+
}
348+
}
349+
350+
func TestHandleDiffLoaded_SkipsDuplicate(t *testing.T) {
351+
t.Parallel()
352+
m := newTestModel(t, []fileItem{
353+
{change: git.FileChange{Path: "a.go", Status: git.StatusModified}},
354+
})
355+
m.cursor = 0
356+
m.lastDiffContent = "same diff"
357+
358+
// Same content as cache — should be a no-op
359+
result, _ := m.handleDiffLoaded(diffLoadedMsg{content: "same diff", index: 0})
360+
rm := result.(Model)
361+
if rm.lastDiffContent != "same diff" {
362+
t.Error("cache should remain unchanged on duplicate")
363+
}
364+
}
365+
320366
func TestBranchListScroll(t *testing.T) {
321367
t.Parallel()
322368
m := newTestModel(t, nil)

0 commit comments

Comments
 (0)