465 lines
9.9 KiB
Go
465 lines
9.9 KiB
Go
package editor
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
|
"git.gophernest.net/azpect/TextEditor/internal/input"
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
)
|
|
|
|
type Model struct {
|
|
// Buffers
|
|
buffers []*action.Buffer
|
|
//next buffer id?
|
|
|
|
// Windows
|
|
windows []*action.Window
|
|
activeWindowId int
|
|
|
|
// Editor wide state
|
|
mode action.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
|
|
|
|
// Command line state
|
|
command string
|
|
commandCursor int
|
|
commandError error
|
|
commandOutput string
|
|
|
|
// Global settings (TODO: This needs to be refactored)
|
|
settings action.Settings
|
|
|
|
// Registers
|
|
registers map[rune]action.Register // name -> register
|
|
}
|
|
|
|
// 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() []*action.Window {
|
|
return m.windows
|
|
}
|
|
|
|
func (m *Model) ActiveWindow() *action.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() []*action.Buffer {
|
|
return m.buffers
|
|
}
|
|
|
|
func (m *Model) ActiveBuffer() *action.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) ExitInsertMode() {
|
|
win := m.ActiveWindow()
|
|
if m.insertCount > 1 {
|
|
m.replayInsert()
|
|
}
|
|
if win.Cursor.Col > 0 {
|
|
win.Cursor.Col--
|
|
}
|
|
m.mode = action.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().TabSize)
|
|
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) CommandError() error {
|
|
return m.commandError
|
|
}
|
|
|
|
func (m *Model) SetCommandError(err error) {
|
|
m.commandError = err
|
|
}
|
|
|
|
func (m *Model) CommandOutput() string {
|
|
return m.commandOutput
|
|
}
|
|
|
|
func (m *Model) SetCommandOutput(out string) {
|
|
m.commandOutput = out
|
|
}
|
|
|
|
// ==================================================
|
|
// Editor-wide State
|
|
// ==================================================
|
|
func (m *Model) Mode() action.Mode {
|
|
return m.mode
|
|
}
|
|
|
|
func (m *Model) SetMode(mode action.Mode) {
|
|
m.mode = mode
|
|
}
|
|
|
|
func (m *Model) Settings() action.Settings {
|
|
return m.settings
|
|
}
|
|
|
|
func (m *Model) SetSettings(s action.Settings) {
|
|
m.settings = s
|
|
}
|
|
|
|
// ==================================================
|
|
// Registers
|
|
// ==================================================
|
|
func (m *Model) Registers() map[rune]action.Register {
|
|
return m.registers
|
|
}
|
|
|
|
func (m *Model) GetRegister(name rune) (action.Register, bool) {
|
|
reg, found := m.registers[name]
|
|
return reg, found
|
|
}
|
|
|
|
func (m *Model) SetRegister(name rune, t action.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 := action.Register{Type: t, Content: cnt}
|
|
m.registers[name] = reg
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *Model) UpdateDefaultRegister(t action.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)
|
|
}
|
|
|
|
// ==================================================
|
|
// Depreciated
|
|
// ==================================================
|
|
// func (m *Model) Lines() []string {
|
|
// win := m.ActiveWindow()
|
|
// return win.Buffer.Lines
|
|
// }
|
|
//
|
|
// func (m *Model) Line(idx int) string {
|
|
// win := m.ActiveWindow()
|
|
// return win.Buffer.Line(idx)
|
|
// }
|
|
//
|
|
// func (m *Model) SetLine(idx int, content string) {
|
|
// win := m.ActiveWindow()
|
|
// win.Buffer.SetLine(idx, content)
|
|
// }
|
|
//
|
|
// func (m *Model) InsertLine(idx int, content string) {
|
|
// win := m.ActiveWindow()
|
|
// win.Buffer.InsertLine(idx, content)
|
|
// }
|
|
//
|
|
// func (m *Model) DeleteLine(idx int) {
|
|
// win := m.ActiveWindow()
|
|
// win.Buffer.DeleteLine(idx)
|
|
// }
|
|
//
|
|
// func (m *Model) LineCount() int {
|
|
// win := m.ActiveWindow()
|
|
// return win.Buffer.LineCount()
|
|
// }
|
|
//
|
|
// func (m *Model) CursorX() int {
|
|
// win := m.ActiveWindow()
|
|
// return win.Cursor.Col
|
|
// }
|
|
//
|
|
// func (m *Model) CursorY() int {
|
|
// win := m.ActiveWindow()
|
|
// return win.Cursor.Line
|
|
// }
|
|
//
|
|
// func (m *Model) SetCursorX(x int) {
|
|
// win := m.ActiveWindow()
|
|
// win.Cursor.Col = x
|
|
// }
|
|
//
|
|
// func (m *Model) SetCursorY(y int) {
|
|
// win := m.ActiveWindow()
|
|
// win.Cursor.Line = y
|
|
// }
|
|
//
|
|
// // Anchor methods
|
|
// func (m *Model) AnchorX() int {
|
|
// win := m.ActiveWindow()
|
|
// return win.Anchor.Col
|
|
// }
|
|
//
|
|
// func (m *Model) AnchorY() int {
|
|
// win := m.ActiveWindow()
|
|
// return win.Anchor.Line
|
|
// }
|
|
//
|
|
// func (m *Model) SetAnchorX(x int) {
|
|
// win := m.ActiveWindow()
|
|
// win.Anchor.Col = x
|
|
// }
|
|
//
|
|
// func (m *Model) SetAnchorY(y int) {
|
|
// win := m.ActiveWindow()
|
|
// win.Anchor.Line = y
|
|
// }
|
|
//
|
|
// func (m *Model) GetCursorPosition() *action.Position {
|
|
// // Return a copy of the position
|
|
// win := m.ActiveWindow()
|
|
// pos := win.Cursor
|
|
// return &pos
|
|
// }
|
|
//
|
|
// // Window
|
|
// func (m *Model) ScrollY() int {
|
|
// win := m.ActiveWindow()
|
|
// return win.ScrollY
|
|
// }
|
|
//
|
|
// func (m *Model) SetScrollY(y int) {
|
|
// win := m.ActiveWindow()
|
|
// win.ScrollY = y
|
|
// }
|
|
//
|
|
// func (m *Model) WinH() int {
|
|
// win := m.ActiveWindow()
|
|
// return win.Height
|
|
// }
|
|
//
|
|
// func (m *Model) WinW() int {
|
|
// win := m.ActiveWindow()
|
|
// return win.Width
|
|
// }
|
|
//
|
|
// func (m *Model) ViewPortH() int {
|
|
// win := m.ActiveWindow()
|
|
// return win.Height - 2
|
|
// }
|
|
//
|
|
// func (m *Model) ClampCursorX() {
|
|
// win := m.ActiveWindow()
|
|
// win.ClampCursorX()
|
|
// }
|
|
//
|
|
// func (m *Model) ActiveWindowId() int {
|
|
// return m.activeWindowId
|
|
// }
|
|
//
|
|
// // TODO: MOVE THIS
|
|
// // AdjustScroll ensures the cursor stays within the viewport with scrollOff margins.
|
|
// // Call this after any cursor movement.
|
|
// func (m *Model) AdjustScroll() {
|
|
// viewportHeight := m.ViewPortH()
|
|
// if viewportHeight <= 0 {
|
|
// return
|
|
// }
|
|
//
|
|
// // Effective scrollOff (can't be more than half the viewport)
|
|
// off := min(m.Settings().ScrollOff, viewportHeight/2)
|
|
//
|
|
// // Cursor too close to top — scroll up
|
|
// if m.CursorY() < m.ScrollY()+off {
|
|
// m.SetScrollY(m.CursorY() - off)
|
|
// }
|
|
//
|
|
// // Cursor too close to bottom — scroll down
|
|
// if m.CursorY() > m.ScrollY()+viewportHeight-1-off {
|
|
// m.SetScrollY(m.CursorY() - viewportHeight + 1 + off)
|
|
// }
|
|
//
|
|
// // Clamp scrollY to valid range
|
|
// maxScroll := max(0, m.LineCount()-viewportHeight)
|
|
// m.SetScrollY(max(0, min(m.ScrollY(), maxScroll)))
|
|
// }
|