The tests are starting to get messy, lots of duplication. Going to resolve that. Lots of this is due to AI generation of tests.
363 lines
7.7 KiB
Go
363 lines
7.7 KiB
Go
package editor
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
|
"git.gophernest.net/azpect/TextEditor/internal/input"
|
|
"git.gophernest.net/azpect/TextEditor/internal/style"
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
)
|
|
|
|
type Model struct {
|
|
// Buffers
|
|
buffers []*core.Buffer
|
|
//next buffer id?
|
|
|
|
// Windows
|
|
windows []*core.Window
|
|
activeWindowId int
|
|
|
|
// Editor wide state
|
|
mode core.Mode
|
|
|
|
// Terminal dimensions
|
|
termWidth int
|
|
termHeight int
|
|
|
|
// Input and key handling
|
|
input *input.Handler
|
|
|
|
// Insert mode state & repetition (applied to active window)
|
|
insertCount int
|
|
insertKeys []string
|
|
insertAction action.Action
|
|
lastFind core.LastFindCommand
|
|
|
|
// Command line state
|
|
command string
|
|
commandCursor int
|
|
commandOutput *core.CommandOutput
|
|
commandHistory []string
|
|
commandHistoryCursor int
|
|
|
|
// Global settings
|
|
settings core.EditorSettings
|
|
|
|
// Registers
|
|
registers map[rune]core.Register // name -> register
|
|
|
|
// Visual styles
|
|
styles style.Styles
|
|
}
|
|
|
|
// Model.Init: Initialize the model and start any commands that may need to run. Required
|
|
// for the bubbletea architecture.
|
|
func (m Model) Init() tea.Cmd {
|
|
return nil
|
|
}
|
|
|
|
// Implement action.Model interface
|
|
|
|
// ==================================================
|
|
// Core Data Access
|
|
// ==================================================
|
|
func (m *Model) Windows() []*core.Window {
|
|
return m.windows
|
|
}
|
|
|
|
func (m *Model) ActiveWindow() *core.Window {
|
|
winId := m.activeWindowId
|
|
for i := range m.Windows() {
|
|
if m.windows[i].Id == winId {
|
|
return m.windows[i]
|
|
}
|
|
}
|
|
panic("Could not find window")
|
|
}
|
|
|
|
func (m *Model) Buffers() []*core.Buffer {
|
|
return m.buffers
|
|
}
|
|
|
|
func (m *Model) SetBuffers(bufs []*core.Buffer) {
|
|
m.buffers = bufs
|
|
}
|
|
|
|
func (m *Model) ActiveBuffer() *core.Buffer {
|
|
win := m.ActiveWindow()
|
|
return win.Buffer
|
|
}
|
|
|
|
// ==================================================
|
|
// Insert Mode Methods
|
|
// ==================================================
|
|
func (m *Model) InsertKeys() []string {
|
|
return m.insertKeys
|
|
}
|
|
|
|
func (m *Model) SetInsertKeys(keys []string) {
|
|
m.insertKeys = keys
|
|
}
|
|
|
|
func (m *Model) SetInsertRecording(count int, act action.Action) {
|
|
m.insertCount = count
|
|
m.insertKeys = []string{}
|
|
m.insertAction = act
|
|
}
|
|
|
|
func (m *Model) SetLastFind(char string, forward, inclusive bool) {
|
|
m.lastFind = core.LastFindCommand{
|
|
Char: char,
|
|
Forward: forward,
|
|
Inclusive: inclusive,
|
|
}
|
|
}
|
|
|
|
func (m *Model) GetLastFind() *core.LastFindCommand {
|
|
return &m.lastFind
|
|
}
|
|
|
|
func (m *Model) ExitInsertMode() {
|
|
win := m.ActiveWindow()
|
|
if m.insertCount > 1 {
|
|
m.replayInsert()
|
|
}
|
|
if win.Cursor.Col > 0 {
|
|
win.Cursor.Col--
|
|
}
|
|
m.mode = core.NormalMode
|
|
m.insertCount = 0
|
|
m.insertKeys = nil
|
|
}
|
|
|
|
func (m *Model) replayInsert() {
|
|
win := m.ActiveWindow()
|
|
buf := m.ActiveBuffer()
|
|
|
|
// Replay (count - 1) more times
|
|
for i := 1; i < m.insertCount; i++ {
|
|
// For 'o' and 'O', we need to create a new line first
|
|
switch m.insertAction.(type) {
|
|
case action.OpenLineBelow:
|
|
pos := win.Cursor.Line
|
|
buf.InsertLine(pos+1, "")
|
|
win.SetCursorLine(pos + 1)
|
|
|
|
case action.OpenLineAbove:
|
|
pos := win.Cursor.Line
|
|
buf.InsertLine(pos, "")
|
|
|
|
// 'i' and 'a' don't need setup - just replay keys
|
|
}
|
|
|
|
// Replay each recorded keystroke
|
|
for _, key := range m.insertKeys {
|
|
m.processInsertKey(key)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: Fix this shitty shit shit shit
|
|
func (m *Model) processInsertKey(key string) {
|
|
win := m.ActiveWindow()
|
|
buf := m.ActiveBuffer()
|
|
|
|
col := win.Cursor.Col
|
|
line := win.Cursor.Line
|
|
l := buf.Line(line)
|
|
|
|
switch key {
|
|
case "enter":
|
|
if col == len(l) {
|
|
buf.InsertLine(line+1, "")
|
|
} else {
|
|
buf.SetLine(line, l[:col])
|
|
buf.InsertLine(line+1, l[col:])
|
|
}
|
|
win.SetCursorLine(line + 1)
|
|
win.SetCursorCol(0)
|
|
|
|
case "backspace":
|
|
if col > 0 {
|
|
buf.SetLine(line, l[:col-1]+l[col:])
|
|
win.SetCursorCol(col - 1)
|
|
} else if line > 0 {
|
|
prevLine := buf.Line(line - 1)
|
|
newCol := len(prevLine)
|
|
buf.SetLine(line-1, prevLine+l)
|
|
buf.DeleteLine(line)
|
|
win.SetCursorLine(line - 1)
|
|
win.SetCursorCol(newCol)
|
|
}
|
|
|
|
case "delete":
|
|
if col == len(l) && line < buf.LineCount()-1 {
|
|
nextLine := buf.Line(line + 1)
|
|
buf.SetLine(line, l+nextLine)
|
|
buf.DeleteLine(line + 1)
|
|
} else if col < len(l) {
|
|
buf.SetLine(line, l[:col]+l[col+1:])
|
|
}
|
|
|
|
case "tab":
|
|
tabs := strings.Repeat(" ", m.Settings().TabStop)
|
|
if col < len(l) {
|
|
buf.SetLine(line, l[:col]+tabs+l[col:])
|
|
} else {
|
|
buf.SetLine(line, l+tabs)
|
|
}
|
|
win.SetCursorCol(col + len(tabs))
|
|
|
|
case "up":
|
|
if line > 0 {
|
|
win.SetCursorLine(line - 1)
|
|
}
|
|
|
|
case "down":
|
|
if line+1 < buf.LineCount() {
|
|
win.SetCursorLine(line + 1)
|
|
}
|
|
|
|
case "left":
|
|
if col > 0 {
|
|
win.SetCursorCol(col - 1)
|
|
} else if line > 0 {
|
|
prevLine := buf.Line(line - 1)
|
|
win.SetCursorCol(len(prevLine))
|
|
win.SetCursorLine(line - 1)
|
|
}
|
|
|
|
case "right":
|
|
if col < len(l) {
|
|
win.SetCursorCol(col + 1)
|
|
} else if line+1 < buf.LineCount() {
|
|
win.SetCursorCol(0)
|
|
win.SetCursorLine(line + 1)
|
|
}
|
|
|
|
default:
|
|
if col < len(l) {
|
|
buf.SetLine(line, l[:col]+key+l[col:])
|
|
} else {
|
|
buf.SetLine(line, l+key)
|
|
}
|
|
win.SetCursorCol(col + len(key))
|
|
}
|
|
}
|
|
|
|
// ==================================================
|
|
// Command Mode State
|
|
// ==================================================
|
|
func (m *Model) Command() string {
|
|
return m.command
|
|
}
|
|
|
|
func (m *Model) SetCommand(cmd string) {
|
|
m.command = cmd
|
|
}
|
|
|
|
func (m *Model) CommandCursor() int {
|
|
return m.commandCursor
|
|
}
|
|
|
|
func (m *Model) SetCommandCursor(cur int) {
|
|
if cur < 0 {
|
|
m.commandCursor = 0
|
|
} else if cur >= len(m.command) {
|
|
m.commandCursor = len(m.command)
|
|
} else {
|
|
m.commandCursor = cur
|
|
}
|
|
}
|
|
|
|
func (m *Model) CommandOutput() *core.CommandOutput {
|
|
return m.commandOutput
|
|
}
|
|
|
|
func (m *Model) SetCommandOutput(out *core.CommandOutput) {
|
|
m.commandOutput = out
|
|
}
|
|
|
|
func (m *Model) CommandHistory() []string {
|
|
return m.commandHistory
|
|
}
|
|
|
|
func (m *Model) SetCommandHistory(history []string) {
|
|
m.commandHistory = history
|
|
}
|
|
|
|
func (m *Model) CommandHistoryCursor() int {
|
|
return m.commandHistoryCursor
|
|
}
|
|
|
|
func (m *Model) SetCommandHistoryCursor(cur int) {
|
|
m.commandHistoryCursor = cur
|
|
}
|
|
|
|
// ==================================================
|
|
// Editor-wide State
|
|
// ==================================================
|
|
func (m *Model) Mode() core.Mode {
|
|
return m.mode
|
|
}
|
|
|
|
func (m *Model) SetMode(mode core.Mode) {
|
|
m.mode = mode
|
|
}
|
|
|
|
func (m *Model) Settings() core.EditorSettings {
|
|
return m.settings
|
|
}
|
|
|
|
func (m *Model) SetSettings(s core.EditorSettings) {
|
|
m.settings = s
|
|
}
|
|
|
|
// Model.Styles: Returns the visual styles used for rendering.
|
|
func (m *Model) Styles() style.Styles {
|
|
return m.styles
|
|
}
|
|
|
|
// Model.SetStyles: Sets the visual styles used for rendering.
|
|
func (m *Model) SetStyles(s style.Styles) {
|
|
m.styles = s
|
|
}
|
|
|
|
// ==================================================
|
|
// Registers
|
|
// ==================================================
|
|
func (m *Model) Registers() map[rune]core.Register {
|
|
return m.registers
|
|
}
|
|
|
|
func (m *Model) GetRegister(name rune) (core.Register, bool) {
|
|
reg, found := m.registers[name]
|
|
return reg, found
|
|
}
|
|
|
|
func (m *Model) SetRegister(name rune, t core.RegisterType, cnt []string) error {
|
|
if _, found := m.GetRegister(name); !found {
|
|
return fmt.Errorf("Register '%c' does not exist.", name)
|
|
}
|
|
|
|
// TODO: This might be slow, pointers maybe?
|
|
reg := core.Register{Type: t, Content: cnt}
|
|
m.registers[name] = reg
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *Model) UpdateDefaultRegister(t core.RegisterType, cnt []string) {
|
|
// Shift numbered registers: 0 -> 1 -> 2 -> ... -> 9 -> _ (discarded)
|
|
for i := rune('9'); i > '0'; i-- {
|
|
m.registers[i] = m.registers[i-1]
|
|
}
|
|
|
|
// 0 and " both hold the new content independently
|
|
m.SetRegister('0', t, cnt)
|
|
m.SetRegister('"', t, cnt)
|
|
}
|