Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion cmd/openapi/internal/explore/tui/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}

case "ctrl+d":
if !m.showHelp {
if !m.showHelp && len(m.operations) > 0 {
maxItems := len(m.operations) - 1
newCursorPos := m.cursor + scrollHalfScreenLines

Expand Down Expand Up @@ -388,6 +388,7 @@ func (m *Model) ensureCursorVisible() {
// If the cursor item extends beyond available content height, scroll down
if linesUsed > contentHeight {
// Find the minimum scroll offset that keeps cursor visible
found := false
for newScrollOffset := m.scrollOffset + 1; newScrollOffset <= m.cursor; newScrollOffset++ {
testLinesUsed := 0

Expand All @@ -403,9 +404,17 @@ func (m *Model) ensureCursorVisible() {

if testLinesUsed <= contentHeight {
m.scrollOffset = newScrollOffset
found = true
break
}
}

// Nothing fits when the indicator and the cursor row compete for a single
// line, or the cursor item is taller than the content area. Show the cursor
// row at the top rather than leaving it off-screen.
if !found {
m.scrollOffset = m.cursor
}
}

// Ensure scroll offset doesn't go negative
Expand Down
69 changes: 53 additions & 16 deletions cmd/openapi/internal/explore/tui/model_keys_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,24 +404,21 @@ func TestUpdate_HalfPageKeys(t *testing.T) {
)

tests := []struct {
name string
height int
cursor int
key tea.KeyPressMsg
wantCursor int
viewportUnfittable bool
name string
height int
cursor int
key tea.KeyPressMsg
wantCursor int
}{
{name: "ctrl+d moves down a half screen", height: height, cursor: 0, key: ctrlKey('d'), wantCursor: scrollHalfScreenLines},
{name: "ctrl+d clamps at the last operation", height: height, cursor: 15, key: ctrlKey('d'), wantCursor: lastRow},
{name: "ctrl+d at the last operation stays put", height: height, cursor: lastRow, key: ctrlKey('d'), wantCursor: lastRow},
{name: "ctrl+u moves up half the content height", height: height, cursor: 25, key: ctrlKey('u'), wantCursor: 25 - halfContent},
{name: "ctrl+u clamps at the first operation", height: height, cursor: 5, key: ctrlKey('u'), wantCursor: 0},
{name: "ctrl+u at the first operation stays put", height: height, cursor: 0, key: ctrlKey('u'), wantCursor: 0},
// At height 8 the content area is a single row. ensureCursorVisible cannot
// fit the "more items above" indicator plus the cursor row into it, so the
// cursor stays off-screen; that predates the v2 migration, so this case
// checks only the movement and skips the viewport assertion.
{name: "ctrl+u still moves one row on a tiny terminal", height: 8, cursor: 5, key: ctrlKey('u'), wantCursor: 4, viewportUnfittable: true},
// At height 8 the content area is a single row.
{name: "ctrl+u still moves one row on a tiny terminal", height: 8, cursor: 5, key: ctrlKey('u'), wantCursor: 4},
{name: "ctrl+d still moves down on a tiny terminal", height: 8, cursor: 0, key: ctrlKey('d'), wantCursor: scrollHalfScreenLines},
}

for _, tt := range tests {
Expand All @@ -435,15 +432,55 @@ func TestUpdate_HalfPageKeys(t *testing.T) {
m, cmd := press(t, m, tt.key)
assert.Nil(t, cmd)
assert.Equal(t, tt.wantCursor, m.cursor)
if tt.viewportUnfittable {
assert.GreaterOrEqual(t, m.scrollOffset, 0)
} else {
assertCursorOnScreen(t, m)
}
assertCursorOnScreen(t, m)
})
}
}

func TestUpdate_HalfPageDownOnEmptyList(t *testing.T) {
t.Parallel()

m := resize(t, NewModel(nil, "Test API", "1.0.0"), 28)

m, cmd := press(t, m, ctrlKey('d'))
assert.Nil(t, cmd)
assert.Equal(t, 0, m.cursor)
assert.Equal(t, 0, m.scrollOffset)
}

func TestEnsureCursorVisible_SingleRowContentArea(t *testing.T) {
t.Parallel()

// height 8 leaves one content row, so the "more items above" indicator and
// the cursor row cannot both fit; the cursor row must win.
m := resize(t, NewModel(manyOperations(), "Test API", "1.0.0"), 8)
assert.Equal(t, 1, m.calculateContentHeight())

for _, cursor := range []int{1, 5, manyOperationCount - 1} {
m.cursor = cursor
m.ensureCursorVisible()
assert.Equal(t, cursor, m.scrollOffset, "cursor %d", cursor)
assertCursorOnScreen(t, m)
}
}

func TestEnsureCursorVisible_UnfoldedItemTallerThanContentArea(t *testing.T) {
t.Parallel()

// height 10 leaves two content rows; an unfolded item spans more than that,
// so the cursor row must be placed at the top rather than left off-screen.
m := resize(t, NewModel(manyOperations(), "Test API", "1.0.0"), 10)
assert.Equal(t, 2, m.calculateContentHeight())

m.cursor = 5
m.operations[5].Folded = false
assert.Greater(t, m.getItemHeight(5), m.calculateContentHeight())

m.ensureCursorVisible()
assert.Equal(t, 5, m.scrollOffset)
assertCursorOnScreen(t, m)
}

func TestUpdate_SelectAllKeys(t *testing.T) {
t.Parallel()

Expand Down
Loading