385 lines
8.1 KiB
Go
385 lines
8.1 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
|
|
|
|
// Dot operator state
|
|
lastChangeKeys []string
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Does update the '.' register
|
|
func (m *Model) SetLastChangeKeys(keys []string) {
|
|
m.lastChangeKeys = keys
|
|
|
|
m.SetRegister('.', core.CharwiseRegister, []string{strings.Join(keys, "")})
|
|
}
|
|
|
|
func (m *Model) LastChangeKeys() []string {
|
|
return m.lastChangeKeys
|
|
}
|
|
|
|
func (m *Model) ClearLastChangeKeys() {
|
|
m.lastChangeKeys = []string{}
|
|
}
|
|
|
|
func (m *Model) HandleKey(key string) tea.Cmd {
|
|
return m.input.Handle(m, key)
|
|
}
|
|
|
|
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)
|
|
}
|