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 } // MoveForwardWord implements Motion (w) - charwise type MoveForwardWord struct { Count int } 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) Type() action.MotionType { return action.Charwise } func (a MoveForwardWord) WithCount(n int) action.Action { return MoveForwardWord{Count: n} } // MoveForwardWordEnd implements Motion (e) - charwise type MoveForwardWordEnd struct { Count int } 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) Type() action.MotionType { return action.Charwise } func (a MoveForwardWordEnd) WithCount(n int) action.Action { return MoveForwardWordEnd{Count: n} } // MoveBackwardWord implements Motion (b) - charwise type MoveBackwardWord struct { Count int } 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) Type() action.MotionType { return action.Charwise } func (a MoveBackwardWord) WithCount(n int) action.Action { return MoveBackwardWord{Count: n} }