Gim/internal/editor/model.go
Hayden Hargreaves 10e37b82af
All checks were successful
Run Test Suite / test (push) Successful in 13s
feat: implemented the command window! Not tested. Maybe we need some?
2026-03-14 23:13:59 -07:00

345 lines
7.3 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
// 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
}
// ==================================================
// 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)
}