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(buf *action.Buffer, x, y int) (int, int) { line := buf.Lines[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 >= buf.LineCount() { return x, y } // Move to first char of next line y++ line = buf.Lines[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 nextWORDStart(buf *action.Buffer, x, y int) (int, int) { line := buf.Lines[y] // Skip current WORD (all non-whitespace is one class for W) for x < len(line) && line[x] != ' ' && line[x] != '\t' { 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 >= buf.LineCount() { return x, y } // Move to first char of next line y++ line = buf.Lines[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(buf *action.Buffer, x, y int) (int, int) { line := buf.Lines[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 >= buf.LineCount() { return len(line) - 1, y } // Otherwise, move to next line y++ x = 0 line = buf.Lines[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 >= buf.LineCount() { return x, y } // Move to first char of next line y++ line = buf.Lines[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 nextWORDEnd(buf *action.Buffer, x, y int) (int, int) { line := buf.Lines[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 >= buf.LineCount() { return len(line) - 1, y } // Otherwise, move to next line y++ x = 0 line = buf.Lines[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 >= buf.LineCount() { return x, y } // Move to first char of next line y++ line = buf.Lines[y] x = 0 } // Move to end of current WORD (all non-whitespace is one class) for x+1 < len(line) && line[x+1] != ' ' && line[x+1] != '\t' { x++ } return x, y } func prevWordStart(buf *action.Buffer, x, y int) (int, int) { line := buf.Lines[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 = buf.Lines[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 = buf.Lines[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 { win := m.ActiveWindow() buf := m.ActiveBuffer() x := win.Cursor.Col y := win.Cursor.Line for i := 0; i < a.Count; i++ { x, y = nextWordStart(buf, x, y) } win.SetCursorCol(x) win.SetCursorLine(y) return nil } func (a MoveForwardWord) Type() action.MotionType { return action.CharwiseExclusive } func (a MoveForwardWord) WithCount(n int) action.Action { return MoveForwardWord{Count: n} } // MoveForwardWORD implements Motion (W) - charwise type MoveForwardWORD struct { Count int } func (a MoveForwardWORD) Execute(m action.Model) tea.Cmd { win := m.ActiveWindow() buf := m.ActiveBuffer() x := win.Cursor.Col y := win.Cursor.Line for i := 0; i < a.Count; i++ { x, y = nextWORDStart(buf, x, y) } win.SetCursorCol(x) win.SetCursorLine(y) return nil } func (a MoveForwardWORD) Type() action.MotionType { return action.CharwiseExclusive } 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 { win := m.ActiveWindow() buf := m.ActiveBuffer() x := win.Cursor.Col y := win.Cursor.Line for i := 0; i < a.Count; i++ { x, y = nextWordEnd(buf, x, y) } win.SetCursorCol(x) win.SetCursorLine(y) return nil } func (a MoveForwardWordEnd) Type() action.MotionType { return action.CharwiseInclusive } func (a MoveForwardWordEnd) WithCount(n int) action.Action { return MoveForwardWordEnd{Count: n} } // MoveForwardWORDEnd implements Motion (E) - charwise type MoveForwardWORDEnd struct { Count int } func (a MoveForwardWORDEnd) Execute(m action.Model) tea.Cmd { win := m.ActiveWindow() buf := m.ActiveBuffer() x := win.Cursor.Col y := win.Cursor.Line for i := 0; i < a.Count; i++ { x, y = nextWORDEnd(buf, x, y) } win.SetCursorCol(x) win.SetCursorLine(y) return nil } func (a MoveForwardWORDEnd) Type() action.MotionType { return action.CharwiseInclusive } 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 { win := m.ActiveWindow() buf := m.ActiveBuffer() x := win.Cursor.Col y := win.Cursor.Line for i := 0; i < a.Count; i++ { x, y = prevWordStart(buf, x, y) } win.SetCursorCol(x) win.SetCursorLine(y) return nil } func (a MoveBackwardWord) Type() action.MotionType { return action.CharwiseExclusive } func (a MoveBackwardWord) WithCount(n int) action.Action { return MoveBackwardWord{Count: n} }