333 lines
8.4 KiB
Go
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 }
|