summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/tui/flamegraph/model.go56
-rw-r--r--internal/tui/flamegraph/model_test.go45
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