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 lastFind core.LastFindCommand // Command line state command string commandCursor int commandOutput *core.CommandOutput commandHistory []string commandHistoryCursor int // Global settings settings core.EditorSettings // Registers registers map[rune]core.Register // name -> register // Visual styles styles style.Styles // Dot operator state lastChangeKeys []string } // 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) SetBuffers(bufs []*core.Buffer) { m.buffers = bufs } 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) SetLastFind(char string, forward, inclusive bool) { m.lastFind = core.LastFindCommand{ Char: char, Forward: forward, Inclusive: inclusive, } } func (m *Model) GetLastFind() *core.LastFindCommand { return &m.lastFind } // Does update the '.' register func (m *Model) SetLastChangeKeys(keys []string) { m.lastChangeKeys = keys m.SetRegister('.', core.CharwiseRegister, []string{strings.Join(keys, "")}) } func (m *Model) LastChangeKeys() []string { return m.lastChangeKeys } func (m *Model) ClearLastChangeKeys() { m.lastChangeKeys = []string{} } func (m *Model) HandleKey(key string) tea.Cmd { return m.input.Handle(m, key) } 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().TabStop) 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) CommandOutput() *core.CommandOutput { return m.commandOutput } func (m *Model) SetCommandOutput(out *core.CommandOutput) { m.commandOutput = out } func (m *Model) CommandHistory() []string { return m.commandHistory } func (m *Model) SetCommandHistory(history []string) { m.commandHistory = history } func (m *Model) CommandHistoryCursor() int { return m.commandHistoryCursor } func (m *Model) SetCommandHistoryCursor(cur int) { m.commandHistoryCursor = cur } // ================================================== // 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.EditorSettings { return m.settings } func (m *Model) SetSettings(s core.EditorSettings) { 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) }