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 = min(end, buf.LineCount()-1) if end < start { end = 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} }