Gim/internal/editor/model.go
2026-02-26 12:14:59 -07:00

458 lines
8.8 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 {
// lines []string
// cursor cursor
// anchor cursor // starting point for visual modes
// scrollY int
mode action.Mode
win_h int
win_w int
activeWindow int
windows []*action.Window
input *input.Handler
// Insert repetition
insertCount int
insertKeys []string
insertAction action.Action
// Command mode
command string
commandCursor int
commandError error
commandOutput string
// Settings
settings action.Settings
// Registers
registers map[rune]action.Register // name -> register
}
func NewModel(lines []string, pos action.Position) *Model {
m := Model{
// lines: lines,
// cursor: cursor{
// x: pos.Col,
// y: pos.Line,
// },
// scrollY: 0,
mode: action.NormalMode,
command: "",
input: input.NewHandler(),
settings: action.NewDefaultSettings(),
registers: action.DefaultRegisters(),
windows: []*action.Window{},
}
// Temporary
win := action.NewEmptyWindow(lines, 0, 0)
win.Cursor = pos // Set initial cursor position
m.windows = append(m.windows, win)
m.activeWindow = win.Id
return &m
}
func (m Model) Init() tea.Cmd {
return nil
}
// Implement action.Model interface
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
}
// Insert methods
func (m *Model) InsertKeys() []string {
return m.insertKeys
}
func (m *Model) SetInsertKeys(keys []string) {
m.insertKeys = keys
}
// Command mode
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
}
// Settings
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)
}
// 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()
}
// 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)))
}
// Windows
func (m *Model) Windows() []*action.Window {
return m.windows
}
func (m *Model) ActiveWindowId() int {
return m.activeWindow
}
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) Mode() action.Mode {
return m.mode
}
func (m *Model) SetMode(mode action.Mode) {
m.mode = mode
}
func (m *Model) SetInsertRecording(count int, act action.Action) {
m.insertCount = count
m.insertKeys = []string{}
m.insertAction = act
}
func (m *Model) GetCursorPosition() *action.Position {
// Return a copy of the position
win := m.ActiveWindow()
pos := win.Cursor
return &pos
}
func (m *Model) replayInsert() {
win := m.ActiveWindow()
// 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
win.Buffer.Lines = append(win.Buffer.Lines[:pos+1], append([]string{""}, win.Buffer.Lines[pos+1:]...)...)
win.Cursor.Line++
win.Cursor.Col = 0
case action.OpenLineAbove:
pos := win.Cursor.Line
win.Buffer.Lines = append(win.Buffer.Lines[:pos], append([]string{""}, win.Buffer.Lines[pos:]...)...)
win.Cursor.Col = 0
// 'i' and 'a' don't need setup - just replay keys
}
// Replay each recorded keystroke
for _, key := range m.insertKeys {
m.processInsertKey(key)
}
}
}
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) processInsertKey(key string) {
x := m.CursorX()
y := m.CursorY()
l := m.Line(y)
switch key {
case "enter":
if x == len(l) {
m.InsertLine(y+1, "")
} else {
m.SetLine(y, l[:x])
m.InsertLine(y+1, l[x:])
}
m.SetCursorY(y + 1)
m.SetCursorX(0)
case "backspace":
if x > 0 {
m.SetLine(y, l[:x-1]+l[x:])
m.SetCursorX(x - 1)
} else if y > 0 {
prevLine := m.Line(y - 1)
newX := len(prevLine)
m.SetLine(y-1, prevLine+l)
m.DeleteLine(y)
m.SetCursorY(y - 1)
m.SetCursorX(newX)
}
case "delete":
if x == len(l) && y < m.LineCount()-1 {
nextLine := m.Line(y + 1)
m.SetLine(y, l+nextLine)
m.DeleteLine(y + 1)
} else if x < len(l) {
m.SetLine(y, l[:x]+l[x+1:])
}
case "tab":
tabs := strings.Repeat(" ", m.Settings().TabSize)
if x < len(l) {
m.SetLine(y, l[:x]+tabs+l[x:])
} else {
m.SetLine(y, l+tabs)
}
m.SetCursorX(x + len(tabs))
case "up":
if y > 0 {
m.SetCursorY(y - 1)
m.ClampCursorX()
}
case "down":
if y+1 < m.LineCount() {
m.SetCursorY(y + 1)
m.ClampCursorX()
}
case "left":
if x > 0 {
m.SetCursorX(x - 1)
} else if y > 0 {
prevLine := m.Line(y - 1)
m.SetCursorX(len(prevLine))
m.SetCursorY(y - 1)
}
case "right":
if x < len(l) {
m.SetCursorX(x + 1)
} else if y+1 < m.LineCount() {
m.SetCursorX(0)
m.SetCursorY(y + 1)
}
default:
if x < len(l) {
m.SetLine(y, l[:x]+key+l[x:])
} else {
m.SetLine(y, l+key)
}
m.SetCursorX(x + len(key))
}
}