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 } func NewModel(lines []string, pos action.Position) *Model { m := Model{ mode: action.NormalMode, command: "", input: input.NewHandler(), settings: action.NewDefaultSettings(), registers: action.DefaultRegisters(), windows: []*action.Window{}, } // TODO: Temporary: Build the single buffer and window buf := action. NewBufferBuilder(). WithLines(lines). Build() m.buffers = append(m.buffers, &buf) win := action. NewWindowBuilder(). WithBuffer(&buf). WithCursor(pos). Build() m.windows = append(m.windows, &win) m.activeWindowId = 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.activeWindowId } 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)) } }