All checks were successful
Run Test Suite / test (push) Successful in 56s
Not sure if this is perfect, but it seems to be working
323 lines
7.8 KiB
Go
323 lines
7.8 KiB
Go
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
|
|
}
|