Gim/internal/motion/word.go

233 lines
4.3 KiB
Go

package motion
import (
"git.gophernest.net/azpect/TextEditor/internal/action"
tea "github.com/charmbracelet/bubbletea"
)
func isWordChar(c byte) bool {
return (c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
c == '_'
}
func isWordPunctuation(c byte) bool {
return c != ' ' && c != '\t' && !isWordChar(c)
}
func nextWordStart(m action.Model, x, y int) (int, int) {
line := m.Line(y)
// Skip current class
if x < len(line) {
if isWordChar(line[x]) {
for x < len(line) && isWordChar(line[x]) {
x++
}
} else if line[x] != ' ' && line[x] != '\t' {
// punctuation class
for x < len(line) && isWordPunctuation(line[x]) {
x++
}
}
}
// Skip whitespace and cross lines if needed
for {
// Walk over white space
for x < len(line) && (line[x] == ' ' || line[x] == '\t') {
x++
}
// Were on the new word, nothing else to do (no lines to cross
if x < len(line) {
break
}
// If next line is the end of the file, exit now
if y+1 >= m.LineCount() {
return x, y
}
// Move to first char of next line
y++
line = m.Line(y)
x = 0
// If the first char of the new line is no whitespace, stay here!
if len(line) > 0 && line[0] != ' ' && line[0] != '\t' {
break
}
}
return x, y
}
func nextWordEnd(m action.Model, x, y int) (int, int) {
line := m.Line(y)
// Advance once to avoid being stuck on the current end
x++
if x >= len(line) {
// At last line of file, pin cursor to end of file
if y+1 >= m.LineCount() {
return len(line) - 1, y
}
// Otherwise, move to next line
y++
x = 0
line = m.Line(y)
}
// Skip whitespace and cross lines if needed
for {
// Walk over white space
for x < len(line) && (line[x] == ' ' || line[x] == '\t') {
x++
}
// Were on the new word, nothing else to do (no lines to cross
if x < len(line) {
break
}
// If next line is the end of the file, exit now
if y+1 >= m.LineCount() {
return x, y
}
// Move to first char of next line
y++
line = m.Line(y)
x = 0
}
// Move to end of current char class, stop before it ends
if isWordChar(line[x]) {
for x+1 < len(line) && isWordChar(line[x+1]) {
x++
}
} else {
for x+1 < len(line) &&
line[x+1] != ' ' &&
line[x+1] != '\t' &&
!isWordChar(line[x+1]) {
x++
}
}
return x, y
}
func prevWordStart(m action.Model, x, y int) (int, int) {
line := m.Line(y)
// Back one to avoid being stuck on the current start
x--
if x < 0 {
if y == 0 {
return 0, 0 // beginning of file, stay put
}
y--
line = m.Line(y)
x = len(line) - 1
if x < 0 {
return 0, y // landed on an empty line
}
}
// Skip whitespace backward, crossing lines if needed
for {
for x >= 0 && (line[x] == ' ' || line[x] == '\t') {
x--
}
if x >= 0 {
break // landed on a non-whitespace char
}
if y == 0 {
return 0, 0
}
y--
line = m.Line(y)
x = len(line) - 1
if len(line) == 0 {
return 0, y // empty line acts as a word boundary
}
}
// Skip to the start of the current char class
if isWordChar(line[x]) {
for x-1 >= 0 && isWordChar(line[x-1]) {
x--
}
} else {
for x-1 >= 0 && isWordPunctuation(line[x-1]) {
x--
}
}
return x, y
}
type MoveForwardWord struct {
Count int
}
// Execute implements [action.Action].
func (a MoveForwardWord) Execute(m action.Model) tea.Cmd {
x := m.CursorX()
y := m.CursorY()
for i := 0; i < a.Count; i++ {
x, y = nextWordStart(m, x, y)
}
m.SetCursorX(x)
m.SetCursorY(y)
return nil
}
func (a MoveForwardWord) WithCount(n int) action.Action {
return MoveForwardWord{Count: n}
}
type MoveForwardWordEnd struct {
Count int
}
// Execute implements [action.Action].
func (a MoveForwardWordEnd) Execute(m action.Model) tea.Cmd {
x := m.CursorX()
y := m.CursorY()
for i := 0; i < a.Count; i++ {
x, y = nextWordEnd(m, x, y)
}
m.SetCursorX(x)
m.SetCursorY(y)
return nil
}
func (a MoveForwardWordEnd) WithCount(n int) action.Action {
return MoveForwardWordEnd{Count: n}
}
type MoveBackwardWord struct {
Count int
}
// Execute implements [action.Action].
func (a MoveBackwardWord) Execute(m action.Model) tea.Cmd {
x := m.CursorX()
y := m.CursorY()
for i := 0; i < a.Count; i++ {
x, y = prevWordStart(m, x, y)
}
m.SetCursorX(x)
m.SetCursorY(y)
return nil
}
func (a MoveBackwardWord) WithCount(n int) action.Action {
return MoveBackwardWord{Count: n}
}