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 cursor struct { x int y int } 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 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 { return 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(), } } func (m Model) Init() tea.Cmd { return nil } // Implement action.Model interface func (m *Model) Lines() []string { return m.lines } func (m *Model) Line(idx int) string { if idx < 0 || idx >= len(m.lines) { return "" } return m.lines[idx] } func (m *Model) SetLine(idx int, content string) { if idx >= 0 && idx < len(m.lines) { m.lines[idx] = content } } func (m *Model) InsertLine(idx int, content string) { if idx < 0 { idx = 0 } if idx > len(m.lines) { idx = len(m.lines) } m.lines = append(m.lines[:idx], append([]string{content}, m.lines[idx:]...)...) } func (m *Model) DeleteLine(idx int) { if idx >= 0 && idx < len(m.lines) { m.lines = append(m.lines[:idx], m.lines[idx+1:]...) } } func (m *Model) LineCount() int { return len(m.lines) } func (m *Model) CursorX() int { return m.cursor.x } func (m *Model) CursorY() int { return m.cursor.y } func (m *Model) SetCursorX(x int) { m.cursor.x = x } func (m *Model) SetCursorY(y int) { m.cursor.y = y } // Anchor methods func (m *Model) AnchorX() int { return m.anchor.x } func (m *Model) AnchorY() int { return m.anchor.y } func (m *Model) SetAnchorX(x int) { m.anchor.x = x } func (m *Model) SetAnchorY(y int) { m.anchor.y = 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 { return m.scrollY } func (m *Model) SetScrollY(y int) { m.scrollY = y } func (m *Model) WinH() int { return m.win_h } func (m *Model) WinW() int { return m.win_w } func (m *Model) ViewPortH() int { return m.win_h - 2 // -2 for status bar and commmand bar } func (m *Model) ClampCursorX() { lineLen := len(m.lines[m.cursor.y]) if lineLen == 0 { m.cursor.x = 0 } else if m.cursor.x >= lineLen { m.cursor.x = lineLen } } // 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))) } 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 action.Position{Line: m.cursor.y, Col: m.cursor.x} } func (m *Model) replayInsert() { // 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 := m.cursor.y m.lines = append(m.lines[:pos+1], append([]string{""}, m.lines[pos+1:]...)...) m.cursor.y++ m.cursor.x = 0 case action.OpenLineAbove: pos := m.cursor.y m.lines = append(m.lines[:pos], append([]string{""}, m.lines[pos:]...)...) m.cursor.x = 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() { if m.insertCount > 1 { m.replayInsert() } if m.cursor.x > 0 { m.cursor.x-- } 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)) } }