package action import ( "strings" "git.gophernest.net/azpect/TextEditor/internal/core" tea "github.com/charmbracelet/bubbletea" ) // EnterInsert implements Action (i) type EnterInsert struct { Count int } // EnterInsert.Execute: Enters insert mode at the cursor position (i key). func (a EnterInsert) Execute(m Model) tea.Cmd { // Start recording m.SetInsertRecording(a.Count, a) m.SetMode(core.InsertMode) return nil } // EnterInsert.WithCount: Returns a new EnterInsert with the given count. func (a EnterInsert) WithCount(n int) Action { return EnterInsert{Count: n} } // EnterInsertAfter implements Action (a) type EnterInsertAfter struct { Count int } // EnterInsertAfter.Execute: Enters insert mode after the cursor position (a key). func (a EnterInsertAfter) Execute(m Model) tea.Cmd { win := m.ActiveWindow() win.SetCursorCol(win.Cursor.Col + 1) // Start recording m.SetInsertRecording(a.Count, a) m.SetMode(core.InsertMode) return nil } // EnterInsertAfter.WithCount: Returns a new EnterInsertAfter with the given count. func (a EnterInsertAfter) WithCount(n int) Action { return EnterInsertAfter{Count: n} } // EnterInsertLineStart implements Action (I) type EnterInsertLineStart struct { Count int } // EnterInsertLineStart.Execute: Enters insert mode at the start of the line (I key). func (a EnterInsertLineStart) Execute(m Model) tea.Cmd { win := m.ActiveWindow() win.SetCursorCol(0) // Start recording m.SetInsertRecording(a.Count, a) m.SetMode(core.InsertMode) return nil } // EnterInsertLineStart.WithCount: Returns a new EnterInsertLineStart with the given count. func (a EnterInsertLineStart) WithCount(n int) Action { return EnterInsertLineStart{Count: n} } // EnterInsertLineEnd implements Action (A) type EnterInsertLineEnd struct { Count int } // EnterInsertLineEnd.Execute: Enters insert mode at the end of the line (A key). func (a EnterInsertLineEnd) Execute(m Model) tea.Cmd { win := m.ActiveWindow() buf := m.ActiveBuffer() win.SetCursorCol(buf.Lines[win.Cursor.Line].Len()) // Start recording m.SetInsertRecording(a.Count, a) m.SetMode(core.InsertMode) return nil } // EnterInsertLineEnd.WithCount: Returns a new EnterInsertLineEnd with the given count. func (a EnterInsertLineEnd) WithCount(n int) Action { return EnterInsertLineEnd{Count: n} } // OpenLineBelow implements Action (o) type OpenLineBelow struct { Count int } // OpenLineBelow.Execute: Opens a new line below the cursor and enters insert mode (o key). func (a OpenLineBelow) Execute(m Model) tea.Cmd { win := m.ActiveWindow() buf := m.ActiveBuffer() pos := win.Cursor.Line if pos >= buf.LineCount() { buf.InsertLine(buf.LineCount(), "") } else { buf.InsertLine(pos+1, "") } win.SetCursorPos(win.Cursor.Line+1, 0) // Start recording m.SetInsertRecording(a.Count, a) m.SetMode(core.InsertMode) return nil } // OpenLineBelow.WithCount: Returns a new OpenLineBelow with the given count. func (a OpenLineBelow) WithCount(n int) Action { return OpenLineBelow{Count: n} } // OpenLineAbove implements Action (O) type OpenLineAbove struct { Count int } // OpenLineAbove.Execute: Opens a new line above the cursor and enters insert mode (O key). func (a OpenLineAbove) Execute(m Model) tea.Cmd { win := m.ActiveWindow() buf := m.ActiveBuffer() pos := win.Cursor.Line buf.InsertLine(pos, "") win.SetCursorCol(0) // Start recording m.SetInsertRecording(a.Count, a) m.SetMode(core.InsertMode) return nil } // OpenLineAbove.WithCount: Returns a new OpenLineAbove with the given count. func (a OpenLineAbove) WithCount(n int) Action { return OpenLineAbove{Count: n} } // --- Insert mode edit actions --- // InsertChar inserts a single character (or rune sequence) at the cursor type InsertChar struct { Char string } // InsertChar.Execute: Inserts a single character at the cursor position. func (a InsertChar) Execute(m Model) tea.Cmd { win := m.ActiveWindow() buf := m.ActiveBuffer() x, y := win.Cursor.Col, win.Cursor.Line l := buf.Line(y) if x < len(l) { buf.SetLine(y, l[:x]+a.Char+l[x:]) } else { buf.SetLine(y, l+a.Char) } win.SetCursorCol(x + len(a.Char)) return nil } // InsertNewline splits the current line at the cursor (enter key) type InsertNewline struct{} // InsertNewline.Execute: Splits the current line at the cursor (Enter key). func (a InsertNewline) Execute(m Model) tea.Cmd { win := m.ActiveWindow() buf := m.ActiveBuffer() x, y := win.Cursor.Col, win.Cursor.Line l := buf.Line(y) if x == len(l) { buf.InsertLine(y+1, "") } else { buf.SetLine(y, l[:x]) buf.InsertLine(y+1, l[x:]) } win.SetCursorPos(y+1, 0) return nil } // InsertBackspace deletes the character before the cursor type InsertBackspace struct{} // InsertBackspace.Execute: Deletes the character before the cursor (Backspace key). func (a InsertBackspace) Execute(m Model) tea.Cmd { win := m.ActiveWindow() buf := m.ActiveBuffer() x, y := win.Cursor.Col, win.Cursor.Line l := buf.Line(y) if x > 0 { buf.SetLine(y, l[:x-1]+l[x:]) win.SetCursorCol(x - 1) } else if y > 0 { prevLine := buf.Line(y - 1) newX := len(prevLine) buf.SetLine(y-1, prevLine+l) buf.DeleteLine(y) win.SetCursorPos(y-1, newX) } return nil } // InsertDelete deletes the character under/after the cursor (delete key) type InsertDelete struct{} // InsertDelete.Execute: Deletes the character at the cursor position (Delete key). func (a InsertDelete) Execute(m Model) tea.Cmd { win := m.ActiveWindow() buf := m.ActiveBuffer() x, y := win.Cursor.Col, win.Cursor.Line l := buf.Line(y) if x == len(l) && y < buf.LineCount()-1 { nextLine := buf.Line(y + 1) buf.SetLine(y, l+nextLine) buf.DeleteLine(y + 1) } else if x < len(l) { buf.SetLine(y, l[:x]+l[x+1:]) } return nil } // InsertTab inserts spaces equal to the tab size type InsertTab struct{} // InsertTab.Execute: Inserts spaces equal to the tab size (Tab key). func (a InsertTab) Execute(m Model) tea.Cmd { win := m.ActiveWindow() buf := m.ActiveBuffer() x, y := win.Cursor.Col, win.Cursor.Line l := buf.Line(y) tabs := strings.Repeat(" ", m.Settings().TabStop) if x < len(l) { buf.SetLine(y, l[:x]+tabs+l[x:]) } else { buf.SetLine(y, l+tabs) } win.SetCursorCol(x + len(tabs)) return nil } // InsertDeletePreviousWord deletes the word before the cursor (ctrl+w) type InsertDeletePreviousWord struct{} // isWordChar: Returns true if the character is a word character (alphanumeric // or underscore). func isWordChar(c byte) bool { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' } // isPunctuation: Returns true if the character is punctuation (not whitespace // and not a word character). func isPunctuation(c byte) bool { return c != ' ' && c != '\t' && !isWordChar(c) } // InsertDeletePreviousWord.Execute: Deletes the word before the cursor (Ctrl+W). func (a InsertDeletePreviousWord) Execute(m Model) tea.Cmd { win := m.ActiveWindow() buf := m.ActiveBuffer() x, y := win.Cursor.Col, win.Cursor.Line line := buf.Line(y) // At start of line: merge with previous line (same as backspace) if x == 0 { if y > 0 { prevLine := buf.Line(y - 1) newX := len(prevLine) buf.SetLine(y-1, prevLine+line) buf.DeleteLine(y) win.SetCursorPos(y-1, newX) } return nil } // Scan backwards to find the new cursor position (don't mutate yet) newX := x // If we are on puncuation, we should just skip them all and quit if isPunctuation(line[newX-1]) { for newX > 0 && isPunctuation(line[newX-1]) { newX-- } buf.SetLine(y, line[:newX]+line[x:]) win.SetCursorCol(newX) return nil } // Skip whitespace immediately before the cursor for newX > 0 && (line[newX-1] == ' ' || line[newX-1] == '\t') { newX-- } // Skip the word characters before the cursor for newX > 0 && isWordChar(line[newX-1]) { newX-- } // Delete everything from newX up to x in one operation buf.SetLine(y, line[:newX]+line[x:]) win.SetCursorCol(newX) return nil }