package editor import ( "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 s_gutter int mode action.Mode win_h int win_w int command string input *input.Handler // Insert repetition insertCount int insertKeys []string insertAction action.Action } func NewModel(lines []string, pos action.Position) Model { return Model{ lines: lines, cursor: cursor{ x: pos.Col, y: pos.Line, }, s_gutter: 5, mode: action.NormalMode, command: "", input: input.NewHandler(), } } 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 } 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 } } 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) processInsertKey(key string) { x := m.CursorX() y := m.CursorY() l := m.Line(y) switch key { case "enter": // Simple case, at end, just create a line if x == len(l) { m.InsertLine(y+1, "") // otherwise, splice } 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() { nextLine := m.Line(y + 1) m.SetLine(y, l+nextLine) m.DeleteLine(y + 1) } else if x >= 0 { m.SetLine(y, l[:x]+l[x+1:]) } // Regular character default: if x < len(l) { m.SetLine(y, l[:x]+key+l[x:]) } else { m.SetLine(y, l+key) } m.SetCursorX(x + len(key)) } }