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 // Command line state command string commandCursor int commandError error commandOutput string // Global settings (TODO: This needs to be refactored) settings core.Settings // 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) 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) 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().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() core.Mode { return m.mode } func (m *Model) SetMode(mode core.Mode) { m.mode = mode } func (m *Model) Settings() core.Settings { return m.settings } func (m *Model) SetSettings(s core.Settings) { 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) } // ================================================== // 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))) // }