diff options
Diffstat (limited to 'internal/tui/flamegraph')
| -rw-r--r-- | internal/tui/flamegraph/model.go | 56 | ||||
| -rw-r--r-- | internal/tui/flamegraph/model_test.go | 45 |
2 files changed, 99 insertions, 2 deletions
diff --git a/internal/tui/flamegraph/model.go b/internal/tui/flamegraph/model.go index 4453452..27e356b 100644 --- a/internal/tui/flamegraph/model.go +++ b/internal/tui/flamegraph/model.go @@ -194,9 +194,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case key.Matches(msg, m.keys.ZoomReset): m.zoomReset() case key.Matches(msg, m.keys.MoveShallower): - m.moveVertical(-1) + m.moveVerticalWithFallback(-1, 1) case key.Matches(msg, m.keys.MoveDeeper): - m.moveVertical(1) + m.moveVerticalWithFallback(1, -1) case key.Matches(msg, m.keys.PrevSibling): m.moveSibling(-1) case key.Matches(msg, m.keys.NextSibling): @@ -348,6 +348,7 @@ func (m *Model) rebuildFrames(animate bool) { m.frames = append(m.frames[:0], m.targetFrames...) } m.clampSelection() + m.ensureSelectionVisible() m.subtreeSet = computeSubtreeSetInto(m.frames, m.selectedIdx, m.subtreeSet) } @@ -435,6 +436,14 @@ func (m *Model) moveVertical(delta int) { m.selectedIdx = best } +func (m *Model) moveVerticalWithFallback(primaryDelta, fallbackDelta int) { + before := m.selectedIdx + m.moveVertical(primaryDelta) + if m.selectedIdx == before && fallbackDelta != 0 { + m.moveVertical(fallbackDelta) + } +} + func (m *Model) moveSibling(delta int) { if len(m.frames) == 0 { return @@ -517,3 +526,46 @@ func (m Model) currentRootPath() string { } return m.frames[0].Path } + +func (m Model) visibleRowOffset() int { + if len(m.frames) == 0 { + return 0 + } + availableRows := m.height - 2 // toolbar + status + if availableRows <= 0 { + return 0 + } + maxRow := maxFrameRow(m.frames) + if maxRow+1 <= availableRows { + return 0 + } + return maxRow + 1 - availableRows +} + +func (m *Model) ensureSelectionVisible() { + if len(m.frames) == 0 { + return + } + m.clampSelection() + rowOffset := m.visibleRowOffset() + selected := m.frames[m.selectedIdx] + if selected.Row >= rowOffset { + return + } + + bestIdx := -1 + bestScore := int(^uint(0) >> 1) + for idx, frame := range m.frames { + if frame.Row < rowOffset { + continue + } + score := abs(frame.Row-rowOffset)*1000 + abs(frame.Col-selected.Col) + if score < bestScore { + bestIdx = idx + bestScore = score + } + } + if bestIdx >= 0 { + m.selectedIdx = bestIdx + } +} diff --git a/internal/tui/flamegraph/model_test.go b/internal/tui/flamegraph/model_test.go index ccb5c94..0301b93 100644 --- a/internal/tui/flamegraph/model_test.go +++ b/internal/tui/flamegraph/model_test.go @@ -128,6 +128,20 @@ func TestKeyboardNavigationSingleNodeClamped(t *testing.T) { } } +func TestArrowDownFallsBackToVisibleDepthFromRoot(t *testing.T) { + m := NewModel(nil) + m.frames = []tuiFrame{ + {Name: "root", Depth: 0, Col: 0, Path: "root"}, + {Name: "child", Depth: 1, Col: 0, Path: "root" + pathSeparator + "child"}, + } + m.selectedIdx = 0 + + m = pressFlameKey(t, m, tea.KeyPressMsg{Code: tea.KeyDown}) + if m.selectedIdx != 1 { + t.Fatalf("expected down arrow to move selection to child when root has no shallower row, got %d", m.selectedIdx) + } +} + func TestZoomInUndoResetAndNestedZoom(t *testing.T) { m := newZoomModel() @@ -399,6 +413,37 @@ func TestDataRefreshAnimationConvergesOverTicks(t *testing.T) { } } +func TestRebuildKeepsSelectionOnVisibleRowsWhenTruncated(t *testing.T) { + m := NewModel(nil) + m.width = 80 + m.height = 4 // only 2 render rows remain after toolbar+status + m.snapshot = &snapshotNode{ + Name: "root", + Children: []*snapshotNode{ + { + Name: "a", + Children: []*snapshotNode{ + { + Name: "b", + Children: []*snapshotNode{ + {Name: "c", Total: 5}, + }, + }, + }, + }, + }, + } + + m.rebuildFrames(false) + if len(m.frames) == 0 { + t.Fatalf("expected rebuilt frames") + } + rowOffset := m.visibleRowOffset() + if m.frames[m.selectedIdx].Row < rowOffset { + t.Fatalf("expected selected frame row %d to be visible (offset=%d)", m.frames[m.selectedIdx].Row, rowOffset) + } +} + func TestResizeRecalculatesLayoutAndCullsNarrowFrames(t *testing.T) { m := NewModel(nil) m.width = 120 |
