feat: implement scroll actions, tested
This is control+d and control+u
This commit is contained in:
parent
0e8ca21f7f
commit
774d0d0071
@ -57,11 +57,13 @@ type Model interface {
|
|||||||
SetCursorX(x int)
|
SetCursorX(x int)
|
||||||
SetCursorY(y int)
|
SetCursorY(y int)
|
||||||
ClampCursorX()
|
ClampCursorX()
|
||||||
ClampCursorXNormal()
|
|
||||||
|
|
||||||
// Window
|
// Window
|
||||||
ScrollY() int
|
ScrollY() int
|
||||||
SetScrollY(y int)
|
SetScrollY(y int)
|
||||||
|
WinH() int
|
||||||
|
WinW() int
|
||||||
|
ViewPortH() int
|
||||||
|
|
||||||
// Anchor
|
// Anchor
|
||||||
AnchorX() int
|
AnchorX() int
|
||||||
|
|||||||
@ -23,6 +23,8 @@ func sendKeys(tm *teatest.TestModel, keys ...string) {
|
|||||||
tm.Send(tea.KeyMsg{Type: tea.KeyCtrlC})
|
tm.Send(tea.KeyMsg{Type: tea.KeyCtrlC})
|
||||||
case "ctrl+d":
|
case "ctrl+d":
|
||||||
tm.Send(tea.KeyMsg{Type: tea.KeyCtrlD})
|
tm.Send(tea.KeyMsg{Type: tea.KeyCtrlD})
|
||||||
|
case "ctrl+u":
|
||||||
|
tm.Send(tea.KeyMsg{Type: tea.KeyCtrlU})
|
||||||
case "ctrl+v":
|
case "ctrl+v":
|
||||||
tm.Send(tea.KeyMsg{Type: tea.KeyCtrlV})
|
tm.Send(tea.KeyMsg{Type: tea.KeyCtrlV})
|
||||||
case "ctrl+w":
|
case "ctrl+w":
|
||||||
|
|||||||
@ -87,11 +87,10 @@ func TestScrollBasic(t *testing.T) {
|
|||||||
if m.CursorY() != 99 {
|
if m.CursorY() != 99 {
|
||||||
t.Errorf("CursorY() = %d, want 99", m.CursorY())
|
t.Errorf("CursorY() = %d, want 99", m.CursorY())
|
||||||
}
|
}
|
||||||
// With 100 lines and viewport 19, max scrollY = 100 - 19 = 81
|
// With 100 lines and viewport 18 (height - 2 for status + command bar),
|
||||||
// Cursor at 99 with scrollOff=8 means cursor at position 10 from top
|
// max scrollY = 100 - 18 = 82
|
||||||
// scrollY = 99 - 10 = 89, but clamped to maxScroll = 81
|
if m.ScrollY() != 82 {
|
||||||
if m.ScrollY() != 81 {
|
t.Errorf("ScrollY() = %d, want 82", m.ScrollY())
|
||||||
t.Errorf("ScrollY() = %d, want 81", m.ScrollY())
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -132,8 +131,8 @@ func TestScrollEdgeCases(t *testing.T) {
|
|||||||
sendKeys(tm, "G")
|
sendKeys(tm, "G")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
// 30 lines, viewport 19 -> maxScroll = 30 - 19 = 11
|
// 30 lines, viewport 18 (height - 2) -> maxScroll = 30 - 18 = 12
|
||||||
maxScroll := 30 - 19
|
maxScroll := 30 - 18
|
||||||
if m.ScrollY() > maxScroll {
|
if m.ScrollY() > maxScroll {
|
||||||
t.Errorf("ScrollY() = %d, want <= %d", m.ScrollY(), maxScroll)
|
t.Errorf("ScrollY() = %d, want <= %d", m.ScrollY(), maxScroll)
|
||||||
}
|
}
|
||||||
@ -156,6 +155,263 @@ func TestScrollEdgeCases(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tests use terminal 80x30: viewportH=28, half-scroll=14, scrollOff=8, safe zone relY 8-19.
|
||||||
|
|
||||||
|
func TestHalfPageScrollDown(t *testing.T) {
|
||||||
|
t.Run("ctrl+d scrolls viewport down by half", func(t *testing.T) {
|
||||||
|
// cursor at line 15 (relY=15, in safe zone), scrollY starts at 0
|
||||||
|
// After ctrl+d: newScrollY=14, newCursorY=14+15=29
|
||||||
|
lines := generateLines(100)
|
||||||
|
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 15}, 80, 30)
|
||||||
|
sendKeys(tm, "ctrl+d")
|
||||||
|
|
||||||
|
m := getFinalModel(t, tm)
|
||||||
|
if m.ScrollY() != 14 {
|
||||||
|
t.Errorf("ScrollY() = %d, want 14", m.ScrollY())
|
||||||
|
}
|
||||||
|
if m.CursorY() != 29 {
|
||||||
|
t.Errorf("CursorY() = %d, want 29", m.CursorY())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ctrl+d preserves cursor relative position in viewport", func(t *testing.T) {
|
||||||
|
// relY=15 before and after
|
||||||
|
lines := generateLines(100)
|
||||||
|
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 15}, 80, 30)
|
||||||
|
sendKeys(tm, "ctrl+d")
|
||||||
|
|
||||||
|
m := getFinalModel(t, tm)
|
||||||
|
relY := m.CursorY() - m.ScrollY()
|
||||||
|
if relY != 15 {
|
||||||
|
t.Errorf("relative position = %d, want 15", relY)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ctrl+d clamps cursor to scrollOff when cursor is near top", func(t *testing.T) {
|
||||||
|
// cursor at line 0 (relY=0 < scrollOff=8), clamp to scrollOff
|
||||||
|
// After ctrl+d: newScrollY=14, newCursorY=14+8=22
|
||||||
|
lines := generateLines(100)
|
||||||
|
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 0}, 80, 30)
|
||||||
|
sendKeys(tm, "ctrl+d")
|
||||||
|
|
||||||
|
m := getFinalModel(t, tm)
|
||||||
|
if m.ScrollY() != 14 {
|
||||||
|
t.Errorf("ScrollY() = %d, want 14", m.ScrollY())
|
||||||
|
}
|
||||||
|
if m.CursorY() != 22 {
|
||||||
|
t.Errorf("CursorY() = %d, want 22", m.CursorY())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ctrl+d at end of file does not scroll past max", func(t *testing.T) {
|
||||||
|
// 40 lines, viewport 28: maxScroll=12
|
||||||
|
// AdjustScroll puts cursor 35 at scrollY=12 (clamped), relY=23
|
||||||
|
// After ctrl+d: newScrollY clamped to 12, relY=23>19 clamped to 19, newCursorY=31
|
||||||
|
lines := generateLines(40)
|
||||||
|
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 35}, 80, 30)
|
||||||
|
sendKeys(tm, "ctrl+d")
|
||||||
|
|
||||||
|
m := getFinalModel(t, tm)
|
||||||
|
maxScroll := 40 - 28
|
||||||
|
if m.ScrollY() > maxScroll {
|
||||||
|
t.Errorf("ScrollY() = %d, want <= %d", m.ScrollY(), maxScroll)
|
||||||
|
}
|
||||||
|
if m.CursorY() != 31 {
|
||||||
|
t.Errorf("CursorY() = %d, want 31", m.CursorY())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ctrl+d on file smaller than viewport does not crash", func(t *testing.T) {
|
||||||
|
// 20 lines < viewport 28: maxScroll=0, scrollY stays 0
|
||||||
|
// relY=0 < scrollOff, clamp to 8; newCursorY=0+8=8
|
||||||
|
lines := generateLines(20)
|
||||||
|
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 0}, 80, 30)
|
||||||
|
sendKeys(tm, "ctrl+d")
|
||||||
|
|
||||||
|
m := getFinalModel(t, tm)
|
||||||
|
if m.ScrollY() != 0 {
|
||||||
|
t.Errorf("ScrollY() = %d, want 0", m.ScrollY())
|
||||||
|
}
|
||||||
|
if m.CursorY() != 8 {
|
||||||
|
t.Errorf("CursorY() = %d, want 8", m.CursorY())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ctrl+d clamps cursor x to new line length", func(t *testing.T) {
|
||||||
|
// Lines 0-14: "hello world" (11 chars), lines 15-99: "hi" (2 chars)
|
||||||
|
// cursor at line 5, cursorX=10
|
||||||
|
// ctrl+d: relY=5<8 clamp to 8; newScrollY=14; newCursorY=22 (a "hi" line)
|
||||||
|
// ClampCursorX: 10 >= 2, so cursorX=2
|
||||||
|
lines := make([]string, 100)
|
||||||
|
for i := range 15 {
|
||||||
|
lines[i] = "hello world"
|
||||||
|
}
|
||||||
|
for i := 15; i < 100; i++ {
|
||||||
|
lines[i] = "hi"
|
||||||
|
}
|
||||||
|
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 10, Line: 5}, 80, 30)
|
||||||
|
sendKeys(tm, "ctrl+d")
|
||||||
|
|
||||||
|
m := getFinalModel(t, tm)
|
||||||
|
if m.CursorY() != 22 {
|
||||||
|
t.Errorf("CursorY() = %d, want 22", m.CursorY())
|
||||||
|
}
|
||||||
|
if m.CursorX() > len(m.Line(m.CursorY())) {
|
||||||
|
t.Errorf("CursorX() = %d exceeds line length %d", m.CursorX(), len(m.Line(m.CursorY())))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("multiple ctrl+d presses scroll incrementally", func(t *testing.T) {
|
||||||
|
// cursor at line 15, scrollY=0, relY=15
|
||||||
|
// ctrl+d #1: scrollY=14, cursorY=29, relY=15
|
||||||
|
// ctrl+d #2: scrollY=28, cursorY=43, relY=15
|
||||||
|
lines := generateLines(200)
|
||||||
|
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 15}, 80, 30)
|
||||||
|
sendKeys(tm, "ctrl+d", "ctrl+d")
|
||||||
|
|
||||||
|
m := getFinalModel(t, tm)
|
||||||
|
if m.ScrollY() != 28 {
|
||||||
|
t.Errorf("ScrollY() = %d, want 28", m.ScrollY())
|
||||||
|
}
|
||||||
|
if m.CursorY() != 43 {
|
||||||
|
t.Errorf("CursorY() = %d, want 43", m.CursorY())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHalfPageScrollUp(t *testing.T) {
|
||||||
|
t.Run("ctrl+u scrolls viewport up by half", func(t *testing.T) {
|
||||||
|
// cursor at line 50: AdjustScroll -> scrollY=31, relY=19
|
||||||
|
// After ctrl+u: newScrollY=17, newCursorY=17+19=36
|
||||||
|
lines := generateLines(100)
|
||||||
|
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 50}, 80, 30)
|
||||||
|
sendKeys(tm, "ctrl+u")
|
||||||
|
|
||||||
|
m := getFinalModel(t, tm)
|
||||||
|
if m.ScrollY() != 17 {
|
||||||
|
t.Errorf("ScrollY() = %d, want 17", m.ScrollY())
|
||||||
|
}
|
||||||
|
if m.CursorY() != 36 {
|
||||||
|
t.Errorf("CursorY() = %d, want 36", m.CursorY())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ctrl+u preserves cursor relative position in viewport", func(t *testing.T) {
|
||||||
|
// relY=19 before and after
|
||||||
|
lines := generateLines(100)
|
||||||
|
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 50}, 80, 30)
|
||||||
|
sendKeys(tm, "ctrl+u")
|
||||||
|
|
||||||
|
m := getFinalModel(t, tm)
|
||||||
|
relY := m.CursorY() - m.ScrollY()
|
||||||
|
if relY != 19 {
|
||||||
|
t.Errorf("relative position = %d, want 19", relY)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ctrl+u at top of file does not make scrollY negative", func(t *testing.T) {
|
||||||
|
// cursor at line 10, scrollY=0, relY=10 (in safe zone)
|
||||||
|
// ctrl+u: newScrollY=max(0,-14)=0, relY=10 preserved, cursorY=10
|
||||||
|
lines := generateLines(100)
|
||||||
|
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 10}, 80, 30)
|
||||||
|
sendKeys(tm, "ctrl+u")
|
||||||
|
|
||||||
|
m := getFinalModel(t, tm)
|
||||||
|
if m.ScrollY() < 0 {
|
||||||
|
t.Errorf("ScrollY() = %d, want >= 0", m.ScrollY())
|
||||||
|
}
|
||||||
|
if m.ScrollY() != 0 {
|
||||||
|
t.Errorf("ScrollY() = %d, want 0", m.ScrollY())
|
||||||
|
}
|
||||||
|
if m.CursorY() != 10 {
|
||||||
|
t.Errorf("CursorY() = %d, want 10", m.CursorY())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ctrl+u clamps cursor to scrollOff when cursor is near top of viewport", func(t *testing.T) {
|
||||||
|
// cursor at line 5, scrollY=0, relY=5 < scrollOff=8
|
||||||
|
// ctrl+u: newScrollY=0; relY clamp to 8; newCursorY=8
|
||||||
|
lines := generateLines(100)
|
||||||
|
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 5}, 80, 30)
|
||||||
|
sendKeys(tm, "ctrl+u")
|
||||||
|
|
||||||
|
m := getFinalModel(t, tm)
|
||||||
|
if m.ScrollY() != 0 {
|
||||||
|
t.Errorf("ScrollY() = %d, want 0", m.ScrollY())
|
||||||
|
}
|
||||||
|
if m.CursorY() != 8 {
|
||||||
|
t.Errorf("CursorY() = %d, want 8", m.CursorY())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("multiple ctrl+u presses scroll incrementally", func(t *testing.T) {
|
||||||
|
// cursor at line 80: AdjustScroll -> scrollY=61, relY=19
|
||||||
|
// ctrl+u #1: newScrollY=47, cursorY=66
|
||||||
|
// ctrl+u #2: newScrollY=33, cursorY=52
|
||||||
|
lines := generateLines(200)
|
||||||
|
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 80}, 80, 30)
|
||||||
|
sendKeys(tm, "ctrl+u", "ctrl+u")
|
||||||
|
|
||||||
|
m := getFinalModel(t, tm)
|
||||||
|
if m.ScrollY() != 33 {
|
||||||
|
t.Errorf("ScrollY() = %d, want 33", m.ScrollY())
|
||||||
|
}
|
||||||
|
if m.CursorY() != 52 {
|
||||||
|
t.Errorf("CursorY() = %d, want 52", m.CursorY())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHalfPageScrollRoundTrip(t *testing.T) {
|
||||||
|
t.Run("ctrl+d then ctrl+u returns cursor to original position", func(t *testing.T) {
|
||||||
|
// cursor at line 15, scrollY=0, relY=15
|
||||||
|
// ctrl+d: scrollY=14, cursorY=29, relY=15
|
||||||
|
// ctrl+u: newScrollY=max(0,14-14)=0, cursorY=0+15=15
|
||||||
|
lines := generateLines(200)
|
||||||
|
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 15}, 80, 30)
|
||||||
|
sendKeys(tm, "ctrl+d", "ctrl+u")
|
||||||
|
|
||||||
|
m := getFinalModel(t, tm)
|
||||||
|
if m.ScrollY() != 0 {
|
||||||
|
t.Errorf("ScrollY() = %d, want 0", m.ScrollY())
|
||||||
|
}
|
||||||
|
if m.CursorY() != 15 {
|
||||||
|
t.Errorf("CursorY() = %d, want 15", m.CursorY())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ctrl+u then ctrl+d returns cursor to original position", func(t *testing.T) {
|
||||||
|
// cursor at line 50: AdjustScroll -> scrollY=31, relY=19
|
||||||
|
// ctrl+u: scrollY=17, cursorY=36, relY=19
|
||||||
|
// ctrl+d: scrollY=31, cursorY=50, relY=19
|
||||||
|
lines := generateLines(200)
|
||||||
|
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 50}, 80, 30)
|
||||||
|
sendKeys(tm, "ctrl+u", "ctrl+d")
|
||||||
|
|
||||||
|
m := getFinalModel(t, tm)
|
||||||
|
if m.ScrollY() != 31 {
|
||||||
|
t.Errorf("ScrollY() = %d, want 31", m.ScrollY())
|
||||||
|
}
|
||||||
|
if m.CursorY() != 50 {
|
||||||
|
t.Errorf("CursorY() = %d, want 50", m.CursorY())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("alternating ctrl+d and ctrl+u maintains scroll stability", func(t *testing.T) {
|
||||||
|
lines := generateLines(200)
|
||||||
|
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 15}, 80, 30)
|
||||||
|
sendKeys(tm, "ctrl+d", "ctrl+u", "ctrl+d", "ctrl+u")
|
||||||
|
|
||||||
|
m := getFinalModel(t, tm)
|
||||||
|
if m.ScrollY() != 0 {
|
||||||
|
t.Errorf("ScrollY() = %d, want 0 after 2 round trips", m.ScrollY())
|
||||||
|
}
|
||||||
|
if m.CursorY() != 15 {
|
||||||
|
t.Errorf("CursorY() = %d, want 15 after 2 round trips", m.CursorY())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestScrollWithCount(t *testing.T) {
|
func TestScrollWithCount(t *testing.T) {
|
||||||
t.Run("5j scrolls appropriately", func(t *testing.T) {
|
t.Run("5j scrolls appropriately", func(t *testing.T) {
|
||||||
lines := generateLines(50)
|
lines := generateLines(50)
|
||||||
|
|||||||
@ -195,6 +195,18 @@ func (m *Model) SetScrollY(y int) {
|
|||||||
m.scrollY = y
|
m.scrollY = y
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Model) WinH() int {
|
||||||
|
return m.win_h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) WinW() int {
|
||||||
|
return m.win_w
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) ViewPortH() int {
|
||||||
|
return m.win_h - 2 // -2 for status bar and commmand bar
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Model) ClampCursorX() {
|
func (m *Model) ClampCursorX() {
|
||||||
lineLen := len(m.lines[m.cursor.y])
|
lineLen := len(m.lines[m.cursor.y])
|
||||||
if lineLen == 0 {
|
if lineLen == 0 {
|
||||||
@ -207,7 +219,7 @@ func (m *Model) ClampCursorX() {
|
|||||||
// AdjustScroll ensures the cursor stays within the viewport with scrollOff margins.
|
// AdjustScroll ensures the cursor stays within the viewport with scrollOff margins.
|
||||||
// Call this after any cursor movement.
|
// Call this after any cursor movement.
|
||||||
func (m *Model) AdjustScroll() {
|
func (m *Model) AdjustScroll() {
|
||||||
viewportHeight := m.win_h - 1 // -1 for status bar
|
viewportHeight := m.ViewPortH()
|
||||||
if viewportHeight <= 0 {
|
if viewportHeight <= 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.win_w = msg.Width
|
m.win_w = msg.Width
|
||||||
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
|
// TODO: This needs to be removed, but for now its required for the tests.
|
||||||
|
// Ctrl+C always quits regardless of mode
|
||||||
|
if msg.Type == tea.KeyCtrlC {
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
cmd = m.input.Handle(&m, msg.String())
|
cmd = m.input.Handle(&m, msg.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -59,7 +59,7 @@ func posIsAnchor(m Model, col, line int) bool {
|
|||||||
func (m Model) View() string {
|
func (m Model) View() string {
|
||||||
var view strings.Builder
|
var view strings.Builder
|
||||||
|
|
||||||
viewportHeight := m.win_h - 2 // -2 for status bar and command bar
|
viewportHeight := m.ViewPortH()
|
||||||
start := m.ScrollY()
|
start := m.ScrollY()
|
||||||
end := m.ScrollY() + viewportHeight
|
end := m.ScrollY() + viewportHeight
|
||||||
|
|
||||||
|
|||||||
@ -16,18 +16,20 @@ type Keymap struct {
|
|||||||
func NewNormalKeymap() *Keymap {
|
func NewNormalKeymap() *Keymap {
|
||||||
return &Keymap{
|
return &Keymap{
|
||||||
motions: map[string]action.Motion{
|
motions: map[string]action.Motion{
|
||||||
"j": motion.MoveDown{Count: 1},
|
"j": motion.MoveDown{Count: 1},
|
||||||
"k": motion.MoveUp{Count: 1},
|
"k": motion.MoveUp{Count: 1},
|
||||||
"h": motion.MoveLeft{Count: 1},
|
"h": motion.MoveLeft{Count: 1},
|
||||||
"l": motion.MoveRight{Count: 1},
|
"l": motion.MoveRight{Count: 1},
|
||||||
"G": motion.MoveToBottom{},
|
"G": motion.MoveToBottom{},
|
||||||
"gg": motion.MoveToTop{},
|
"gg": motion.MoveToTop{},
|
||||||
"0": motion.MoveToLineStart{},
|
"0": motion.MoveToLineStart{},
|
||||||
"$": motion.MoveToLineEnd{},
|
"$": motion.MoveToLineEnd{},
|
||||||
"_": motion.MoveToLineContentStart{},
|
"_": motion.MoveToLineContentStart{},
|
||||||
"w": motion.MoveForwardWord{Count: 1},
|
"w": motion.MoveForwardWord{Count: 1},
|
||||||
"e": motion.MoveForwardWordEnd{Count: 1},
|
"e": motion.MoveForwardWordEnd{Count: 1},
|
||||||
"b": motion.MoveBackwardWord{Count: 1},
|
"b": motion.MoveBackwardWord{Count: 1},
|
||||||
|
"ctrl+u": motion.ScrollUpHalfPage{},
|
||||||
|
"ctrl+d": motion.ScrollDownHalfPage{},
|
||||||
},
|
},
|
||||||
operators: map[string]action.Operator{
|
operators: map[string]action.Operator{
|
||||||
"d": operator.DeleteOperator{},
|
"d": operator.DeleteOperator{},
|
||||||
@ -45,7 +47,6 @@ func NewNormalKeymap() *Keymap {
|
|||||||
"o": action.OpenLineBelow{},
|
"o": action.OpenLineBelow{},
|
||||||
"O": action.OpenLineAbove{},
|
"O": action.OpenLineAbove{},
|
||||||
"x": action.DeleteChar{Count: 1},
|
"x": action.DeleteChar{Count: 1},
|
||||||
"ctrl+c": action.Quit{},
|
|
||||||
":": action.EnterComandMode{},
|
":": action.EnterComandMode{},
|
||||||
"v": action.EnterVisualMode{},
|
"v": action.EnterVisualMode{},
|
||||||
"V": action.EnterVisualLineMode{},
|
"V": action.EnterVisualLineMode{},
|
||||||
@ -81,7 +82,6 @@ func NewVisualKeymap() *Keymap {
|
|||||||
// "~": SwapCaseOp{},
|
// "~": SwapCaseOp{},
|
||||||
},
|
},
|
||||||
actions: map[string]action.Action{
|
actions: map[string]action.Action{
|
||||||
"ctrl+c": action.Quit{},
|
|
||||||
// ":": action.EnterComandMode{}, // Different OP
|
// ":": action.EnterComandMode{}, // Different OP
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -102,7 +102,6 @@ func NewInsertKeymap() *Keymap {
|
|||||||
"delete": action.InsertDelete{},
|
"delete": action.InsertDelete{},
|
||||||
"tab": action.InsertTab{},
|
"tab": action.InsertTab{},
|
||||||
"ctrl+w": action.InsertDeletePreviousWord{},
|
"ctrl+w": action.InsertDeletePreviousWord{},
|
||||||
"ctrl+c": action.Quit{},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -73,3 +73,81 @@ func (a MoveToLineContentStart) Execute(m action.Model) tea.Cmd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a MoveToLineContentStart) Type() action.MotionType { return action.Charwise }
|
func (a MoveToLineContentStart) Type() action.MotionType { return action.Charwise }
|
||||||
|
|
||||||
|
// TODO: Count for these, maybe?
|
||||||
|
|
||||||
|
// ScrollDownHalfPage implements Motion (ctrl+d) - linewise
|
||||||
|
type ScrollDownHalfPage struct{}
|
||||||
|
|
||||||
|
func (a ScrollDownHalfPage) Execute(m action.Model) tea.Cmd {
|
||||||
|
viewportHeight := m.ViewPortH()
|
||||||
|
if viewportHeight <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
scroll := viewportHeight / 2
|
||||||
|
scrollOff := m.Settings().ScrollOff
|
||||||
|
|
||||||
|
// Current relative position in viewport
|
||||||
|
relY := m.CursorY() - m.ScrollY()
|
||||||
|
|
||||||
|
// Scroll down, clamped to valid range
|
||||||
|
newScrollY := m.ScrollY() + scroll
|
||||||
|
maxScroll := max(0, m.LineCount()-viewportHeight)
|
||||||
|
newScrollY = min(newScrollY, maxScroll)
|
||||||
|
m.SetScrollY(newScrollY)
|
||||||
|
|
||||||
|
// Maintain relative position, respecting scrollOff
|
||||||
|
if relY < scrollOff {
|
||||||
|
relY = scrollOff
|
||||||
|
}
|
||||||
|
if relY > viewportHeight-1-scrollOff {
|
||||||
|
relY = viewportHeight - 1 - scrollOff
|
||||||
|
}
|
||||||
|
|
||||||
|
newCursorY := newScrollY + relY
|
||||||
|
newCursorY = max(0, min(newCursorY, m.LineCount()-1))
|
||||||
|
m.SetCursorY(newCursorY)
|
||||||
|
m.ClampCursorX()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a ScrollDownHalfPage) Type() action.MotionType { return action.Linewise }
|
||||||
|
|
||||||
|
// ScrollUpHalfPage implements Motion (ctrl+u) - linewise
|
||||||
|
type ScrollUpHalfPage struct{}
|
||||||
|
|
||||||
|
func (a ScrollUpHalfPage) Execute(m action.Model) tea.Cmd {
|
||||||
|
viewportHeight := m.ViewPortH()
|
||||||
|
if viewportHeight <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
scroll := viewportHeight / 2
|
||||||
|
scrollOff := m.Settings().ScrollOff
|
||||||
|
|
||||||
|
// Current relative position in viewport
|
||||||
|
relY := m.CursorY() - m.ScrollY()
|
||||||
|
|
||||||
|
// Scroll up, clamped to valid range
|
||||||
|
newScrollY := m.ScrollY() - scroll
|
||||||
|
newScrollY = max(0, newScrollY)
|
||||||
|
m.SetScrollY(newScrollY)
|
||||||
|
|
||||||
|
// Maintain relative position, respecting scrollOff
|
||||||
|
if relY < scrollOff {
|
||||||
|
relY = scrollOff
|
||||||
|
}
|
||||||
|
if relY > viewportHeight-1-scrollOff {
|
||||||
|
relY = viewportHeight - 1 - scrollOff
|
||||||
|
}
|
||||||
|
|
||||||
|
newCursorY := newScrollY + relY
|
||||||
|
newCursorY = max(0, min(newCursorY, m.LineCount()-1))
|
||||||
|
m.SetCursorY(newCursorY)
|
||||||
|
m.ClampCursorX()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a ScrollUpHalfPage) Type() action.MotionType { return action.Linewise }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user