diff --git a/model.go b/model.go index 4e4b3c2..3669269 100644 --- a/model.go +++ b/model.go @@ -24,6 +24,11 @@ type model struct { win_w int command string input *InputHandler + + // Insert repetition + insertCount int + insertKeys []string + insertAction Action } func newModel() model { @@ -63,3 +68,76 @@ func (m *model) clampCursorX() { func (m model) getCursorPosition() Position { return 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 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 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) { + switch key { + case "enter": + y := m.cursor.y + x := m.cursor.x + + // Simple case, at end, just create a line + if x == len(m.lines[y]) { + m.lines = append(m.lines[:y+1], append([]string{""}, m.lines[y+1:]...)...) + + // otherwise, splice + } else { + l := m.lines[y] + m.lines[y] = l[:x] + m.lines = append(m.lines[:y+1], append([]string{l[x:]}, m.lines[y+1:]...)...) + } + + m.cursor.y++ + m.cursor.x = 0 + + case "backspace": + x := m.cursor.x + y := m.cursor.y + l := m.lines[y] + if x > 0 { + m.lines[y] = l[:x-1] + l[x:] + m.cursor.x-- + } else if y > 0 { + newX := len(m.lines[y-1]) + m.lines[y-1] = m.lines[y-1] + l + m.lines = append(m.lines[:y], m.lines[y+1:]...) + m.cursor.y-- + m.cursor.x = newX + } + + default: + // Regular character + x := m.cursor.x + y := m.cursor.y + l := m.lines[y] + if x < len(l) { + m.lines[y] = l[:x] + key + l[x:] + } else { + m.lines[y] = l + key + } + m.cursor.x += len(key) + } +} diff --git a/motion.go b/motion.go index 1bc2f41..94d9e57 100644 --- a/motion.go +++ b/motion.go @@ -69,22 +69,45 @@ func (a MoveRight) WithCount(n int) Action { return MoveRight{count: n} } -type EnterInsert struct{} +type EnterInsert struct { + count int +} func (a EnterInsert) Execute(m *model) tea.Cmd { + // Start recording + m.insertCount = a.count + m.insertKeys = []string{} + m.insertAction = a + m.mode = InsertMode return nil } -type EnterInsertAfter struct{} +func (a EnterInsert) WithCount(n int) Action { + return EnterInsert{count: n} +} + +type EnterInsertAfter struct { + count int +} func (a EnterInsertAfter) Execute(m *model) tea.Cmd { m.cursor.x++ m.clampCursorX() + + // Start recording + m.insertCount = a.count + m.insertKeys = []string{} + m.insertAction = a + m.mode = InsertMode return nil } +func (a EnterInsertAfter) WithCount(n int) Action { + return EnterInsertAfter{count: n} +} + type Quit struct{} func (a Quit) Execute(m *model) tea.Cmd { @@ -109,24 +132,48 @@ func (a MoveToBottom) Execute(m *model) tea.Cmd { return nil } -type EnterInsertLineStart struct{} +type EnterInsertLineStart struct { + count int +} func (a EnterInsertLineStart) Execute(m *model) tea.Cmd { m.cursor.x = 0 m.clampCursorX() + + // Start recording + m.insertCount = a.count + m.insertKeys = []string{} + m.insertAction = a + m.mode = InsertMode return nil } -type EnterInsertLineEnd struct{} +func (a EnterInsertLineStart) WithCount(n int) Action { + return EnterInsertLineStart{count: n} +} + +type EnterInsertLineEnd struct { + count int +} func (a EnterInsertLineEnd) Execute(m *model) tea.Cmd { m.cursor.x = len(m.lines[m.cursor.y]) m.clampCursorX() + + // Start recording + m.insertCount = a.count + m.insertKeys = []string{} + m.insertAction = a + m.mode = InsertMode return nil } +func (a EnterInsertLineEnd) WithCount(n int) Action { + return EnterInsertLineEnd{count: n} +} + type MoveToLineStart struct{} func (a MoveToLineStart) Execute(m *model) tea.Cmd { @@ -144,7 +191,9 @@ func (a MoveToLineEnd) Execute(m *model) tea.Cmd { } // TODO: Count -type OpenLineBelow struct{} +type OpenLineBelow struct { + count int +} func (a OpenLineBelow) Execute(m *model) tea.Cmd { pos := m.cursor.y @@ -156,13 +205,24 @@ func (a OpenLineBelow) Execute(m *model) tea.Cmd { } m.cursor.y++ - m.clampCursorX() + m.cursor.x = 0 + + // Start recording + m.insertCount = a.count + m.insertKeys = []string{} + m.insertAction = a + m.mode = InsertMode return nil } -// TODO: Count -type OpenLineAbove struct{} +func (a OpenLineBelow) WithCount(n int) Action { + return OpenLineBelow{count: n} +} + +type OpenLineAbove struct { + count int +} func (a OpenLineAbove) Execute(m *model) tea.Cmd { pos := m.cursor.y @@ -173,11 +233,21 @@ func (a OpenLineAbove) Execute(m *model) tea.Cmd { m.lines = append(m.lines[:pos], append([]string{""}, m.lines[pos:]...)...) } - m.clampCursorX() + m.cursor.x = 0 + + // Start recording + m.insertCount = a.count + m.insertKeys = []string{} + m.insertAction = a + m.mode = InsertMode return nil } +func (a OpenLineAbove) WithCount(n int) Action { + return OpenLineAbove{count: n} +} + // TODO: Visual mode type DeleteChar struct { count int diff --git a/update.go b/update.go index 3a67c4e..8199f65 100644 --- a/update.go +++ b/update.go @@ -14,92 +14,31 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case NormalMode: return m, m.input.Handle(&m, msg.String()) + // TODO: This should be handled elsewhere case InsertMode: - switch msg.String() { - case "ctrl+c", "ctrl+d": - return m, tea.Quit + key := msg.String() + switch key { case "esc": + if m.insertCount > 1 { + m.replayInsert() + } + // Allow i to step back, but a to stay put if m.cursor.x > 0 { m.cursor.x-- } m.mode = NormalMode + m.insertCount = 0 + m.insertKeys = nil - case "enter": - y := m.cursor.y - x := m.cursor.x - - // Simple case, at end, just create a line - if x == len(m.lines[y]) { - m.lines = append(m.lines[:y+1], append([]string{""}, m.lines[y+1:]...)...) - - // otherwise, splice - } else { - l := m.lines[y] - m.lines[y] = l[:x] - m.lines = append(m.lines[:y+1], append([]string{l[x:]}, m.lines[y+1:]...)...) - } - - m.cursor.y++ - m.cursor.x = 0 - - case "backspace": - x := m.cursor.x - y := m.cursor.y - l := m.lines[y] - if m.cursor.x > 0 { - m.lines[y] = l[:x-1] + l[x:] - m.cursor.x-- - } else if m.cursor.y > 0 { - new_x := len(m.lines[y-1]) - m.lines[y-1] = m.lines[y-1] + l - - m.lines = append(m.lines[:y], m.lines[y+1:]...) - - m.cursor.y-- - m.cursor.x = new_x - } - - case "left": - if m.cursor.x > 0 { - m.cursor.x-- - } - - case "right": - if m.cursor.x < len(m.lines[m.cursor.y]) { - m.cursor.x++ - } - - case "up": - if m.cursor.y > 0 { - m.cursor.y-- - } - - if m.cursor.x > len(m.lines[m.cursor.y])-1 { - m.cursor.x = len(m.lines[m.cursor.y]) - } - - case "down": - if m.cursor.y < len(m.lines)-1 { - m.cursor.y++ - } - - if m.cursor.x > len(m.lines[m.cursor.y])-1 { - m.cursor.x = len(m.lines[m.cursor.y]) - } + case "ctrl+c", "ctrl+d": + return m, tea.Quit default: - x := m.cursor.x - y := m.cursor.y - l := m.lines[y] - ch := msg.String() - if x < len(l) { - m.lines[y] = l[:x] + ch + l[x:] - } else { - m.lines[y] = l + ch - } - m.cursor.x += len(ch) + // Record and process + m.insertKeys = append(m.insertKeys, key) + m.processInsertKey(key) } case CommandMode: switch msg.String() { diff --git a/view.go b/view.go index f392732..70e195b 100644 --- a/view.go +++ b/view.go @@ -69,9 +69,9 @@ func (m model) View() string { var bar string if m.mode == CommandMode { - bar = fmt.Sprintf(" %6s | %d:%d (%d:%d) %s | %s", modeString, m.cursor.x, m.cursor.y, m.win_w, m.win_h, m.command, m.input.buffer) + bar = fmt.Sprintf(" %6s | %d:%d (%d:%d) %s ", modeString, m.cursor.x, m.cursor.y, m.win_w, m.win_h, m.command) } else { - bar = fmt.Sprintf(" %6s | %d:%d (%d:%d) | %s", modeString, m.cursor.x, m.cursor.y, m.win_w, m.win_h, m.input.buffer) + bar = fmt.Sprintf(" %6s | %d:%d (%d:%d) | %s | %+v | %d", modeString, m.cursor.x, m.cursor.y, m.win_w, m.win_h, m.input.buffer, m.insertKeys, m.insertCount) } view.WriteString(bar)