Gim/internal/motion/jump.go
Hayden Hargreaves 1aa1954d35
All checks were successful
Run Test Suite / test (push) Successful in 42s
feat: implemented the % motion! tested as well
2026-04-09 09:32:25 -07:00

333 lines
8.4 KiB
Go

package motion
import (
"git.gophernest.net/azpect/TextEditor/internal/action"
"git.gophernest.net/azpect/TextEditor/internal/core"
tea "github.com/charmbracelet/bubbletea"
)
func firstNonBlankCol(line string) int {
for i := 0; i < len(line); i++ {
if line[i] != ' ' && line[i] != '\t' {
return i
}
}
return 0
}
func visibleLineBounds(win *core.Window, buf *core.Buffer) (int, int) {
if buf.LineCount() == 0 {
return 0, 0
}
start := win.ScrollY
end := start + win.ViewportHeight() - 1
end = max(min(end, buf.LineCount()-1), start)
return start, end
}
// MoveToTop implements Motion (gg) - linewise
type MoveToTop struct{}
// MoveToTop.Execute: Moves the cursor to the first line of the buffer.
func (a MoveToTop) Execute(m action.Model) tea.Cmd {
win := m.ActiveWindow()
win.SetCursorLine(0)
return nil
}
func (a MoveToTop) Type() core.MotionType { return core.Linewise }
// MoveToBottom implements Motion (G) - linewise
type MoveToBottom struct{}
// MoveToBottom.Execute: Moves the cursor to the last line of the buffer.
func (a MoveToBottom) Execute(m action.Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
win.SetCursorLine(buf.LineCount() - 1)
return nil
}
func (a MoveToBottom) Type() core.MotionType { return core.Linewise }
// MoveToLineStart implements Motion (0) - charwise
type MoveToLineStart struct{}
// MoveToLineStart.Execute: Moves the cursor to the beginning of the current line.
func (a MoveToLineStart) Execute(m action.Model) tea.Cmd {
win := m.ActiveWindow()
win.SetCursorCol(0)
return nil
}
func (a MoveToLineStart) Type() core.MotionType { return core.CharwiseExclusive }
// MoveToLineEnd implements Motion ($) - charwise
type MoveToLineEnd struct{}
// MoveToLineEnd.Execute: Moves the cursor to the end of the current line.
func (a MoveToLineEnd) Execute(m action.Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
win.SetCursorCol(buf.Lines[win.Cursor.Line].Len() - 1)
return nil
}
func (a MoveToLineEnd) Type() core.MotionType { return core.CharwiseInclusive }
// MoveToLineContentStart implements Motion (_) - charwise
type MoveToLineContentStart struct{}
// MoveToLineContentStart.Execute: Moves the cursor to the first non-whitespace
// character on the current line.
func (a MoveToLineContentStart) Execute(m action.Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
line := buf.Line(win.Cursor.Line)
x := 0
for x < len(line) {
ch := line[x]
if ch != ' ' && ch != '\t' {
break
}
x++
}
// If we are on the last char, we overflew, back once
if x == len(line) && x > 0 {
x--
}
win.SetCursorCol(x)
return nil
}
func (a MoveToLineContentStart) Type() core.MotionType { return core.CharwiseExclusive }
// MoveToColumn implements Motion (|) - charwise
type MoveToColumn struct {
Count int
}
// MoveToColumn.Execute: Moves the cursor to the column specified by Count.
func (a MoveToColumn) Execute(m action.Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
line := buf.Line(win.Cursor.Line)
col := min(a.Count-1, len(line)-1)
win.SetCursorCol(col)
return nil
}
func (a MoveToColumn) Type() core.MotionType { return core.CharwiseExclusive }
func (a MoveToColumn) WithCount(n int) action.Action {
return MoveToColumn{Count: n}
}
// TODO: Count for these, maybe?
// ScrollDownPage implements Motion (ctrl+d) - linewise
type ScrollDownPage struct {
Divisor int
}
// ScrollDownHalfPage.Execute: Scrolls down half a page while maintaining the
// cursor's relative position in the viewport.
func (a ScrollDownPage) Execute(m action.Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
viewportHeight := win.ViewportHeight()
if viewportHeight <= 0 {
return nil
}
scroll := viewportHeight / a.Divisor
scrollOff := win.Options.ScrollOff
// Current relative position in viewport
relY := win.Cursor.Line - win.ScrollY
// Scroll down, clamped to valid range
newScrollY := win.ScrollY + scroll
maxScroll := max(0, buf.LineCount()-viewportHeight)
newScrollY = min(newScrollY, maxScroll)
win.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, buf.LineCount()-1))
win.SetCursorLine(newCursorY)
return nil
}
func (a ScrollDownPage) Type() core.MotionType { return core.Linewise }
// ScrollUpPage implements Motion (ctrl+u) - linewise
type ScrollUpPage struct {
Divisor int
}
// ScrollUpHalfPage.Execute: Scrolls up half a page while maintaining the
// cursor's relative position in the viewport.
func (a ScrollUpPage) Execute(m action.Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
viewportHeight := win.ViewportHeight()
if viewportHeight <= 0 {
return nil
}
scroll := viewportHeight / a.Divisor
scrollOff := win.Options.ScrollOff
// Current relative position in viewport
relY := win.Cursor.Line - win.ScrollY
// Scroll up, clamped to valid range
newScrollY := win.ScrollY - scroll
newScrollY = max(0, newScrollY)
win.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, buf.LineCount()-1))
win.SetCursorLine(newCursorY)
return nil
}
func (a ScrollUpPage) Type() core.MotionType { return core.Linewise }
// MoveToScreenTop implements Motion (H) - linewise
type MoveToScreenTop struct {
Count int
}
// MoveToScreenTop.Execute: Moves the cursor to the count-th line from the top
// of the visible window and places it on the first non-blank character.
func (a MoveToScreenTop) Execute(m action.Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
start, end := visibleLineBounds(win, buf)
count := max(1, a.Count)
targetLine := min(start+count-1, end)
targetCol := firstNonBlankCol(buf.Line(targetLine))
win.SetCursorPos(targetLine, targetCol)
return nil
}
func (a MoveToScreenTop) Type() core.MotionType { return core.Linewise }
func (a MoveToScreenTop) WithCount(n int) action.Action {
return MoveToScreenTop{Count: n}
}
// MoveToScreenMiddle implements Motion (M) - linewise
type MoveToScreenMiddle struct{}
// MoveToScreenMiddle.Execute: Moves the cursor to the middle visible line and
// places it on the first non-blank character.
func (a MoveToScreenMiddle) Execute(m action.Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
start, end := visibleLineBounds(win, buf)
targetLine := start + (end-start)/2
targetCol := firstNonBlankCol(buf.Line(targetLine))
win.SetCursorPos(targetLine, targetCol)
return nil
}
func (a MoveToScreenMiddle) Type() core.MotionType { return core.Linewise }
// MoveToScreenBottom implements Motion (L) - linewise
type MoveToScreenBottom struct {
Count int
}
// MoveToScreenBottom.Execute: Moves the cursor to the count-th line from the
// bottom of the visible window and places it on the first non-blank character.
func (a MoveToScreenBottom) Execute(m action.Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
start, end := visibleLineBounds(win, buf)
count := max(1, a.Count)
targetLine := max(end-count+1, start)
targetCol := firstNonBlankCol(buf.Line(targetLine))
win.SetCursorPos(targetLine, targetCol)
return nil
}
func (a MoveToScreenBottom) Type() core.MotionType { return core.Linewise }
func (a MoveToScreenBottom) WithCount(n int) action.Action {
return MoveToScreenBottom{Count: n}
}
// Used for the % motion, not countable
type JumpToMatchingDelimiter struct{}
func (a JumpToMatchingDelimiter) Execute(m action.Model) tea.Cmd {
buf := m.ActiveBuffer()
if buf.LineCount() == 0 {
return nil
}
win := m.ActiveWindow()
lineIdx := win.Cursor.Line
line := buf.Line(lineIdx)
col, startDelim, found := findDelimiterOnLine(line, win.Cursor.Col)
if !found {
return nil
}
matchDelim, ok := getOppositeDelimiter(startDelim)
if !ok {
return nil
}
var target core.Position
if isOpeningDelimiter(startDelim) {
target, found = findMatchingForward(buf, lineIdx, col, startDelim, matchDelim)
} else {
target, found = findMatchingBackward(buf, lineIdx, col, startDelim, matchDelim)
}
if !found {
return nil
}
win.SetCursorPos(target.Line, target.Col)
return nil
}
func (a JumpToMatchingDelimiter) Type() core.MotionType { return core.CharwiseInclusive }