From 307f89bcd1535386cf4bd358b6abb65804472748 Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Fri, 13 Feb 2026 17:44:27 -0700 Subject: [PATCH] feat: implemented command mode mappings, needs review and tests --- internal/action/action.go | 6 ++ internal/action/command.go | 112 +++++++++++++++++++++++++++++++++++++ internal/action/misc.go | 2 + internal/editor/model.go | 28 +++++++++- internal/editor/style.go | 2 +- internal/editor/update.go | 40 ++++++------- internal/editor/view.go | 16 +++++- internal/input/handler.go | 28 ++++++++-- internal/input/keymap.go | 18 ++++++ internal/motion/command.go | 26 +++++++++ 10 files changed, 251 insertions(+), 27 deletions(-) create mode 100644 internal/action/command.go create mode 100644 internal/motion/command.go diff --git a/internal/action/action.go b/internal/action/action.go index d431c97..c3888d3 100644 --- a/internal/action/action.go +++ b/internal/action/action.go @@ -72,6 +72,12 @@ type Model interface { InsertKeys() []string SetInsertKeys(keys []string) + // Command mode + Command() string + SetCommand(cmd string) + CommandCursor() int + SetCommandCursor(cur int) + // Settings Settings() Settings diff --git a/internal/action/command.go b/internal/action/command.go new file mode 100644 index 0000000..d4089a8 --- /dev/null +++ b/internal/action/command.go @@ -0,0 +1,112 @@ +package action + +import ( + tea "github.com/charmbracelet/bubbletea" +) + +type ExitCommandMode struct{} + +func (a ExitCommandMode) Execute(m Model) tea.Cmd { + m.SetCommandCursor(0) + m.SetCommand("") + m.SetMode(NormalMode) + return nil +} + +type InsertCommandChar struct { + Char string +} + +func (a InsertCommandChar) Execute(m Model) tea.Cmd { + cur := m.CommandCursor() + cmd := m.Command() + + m.SetCommand(cmd[:cur] + a.Char + cmd[cur:]) + m.SetCommandCursor(cur + 1) + return nil +} + +type CommandBackspace struct{} + +func (a CommandBackspace) Execute(m Model) tea.Cmd { + cur := m.CommandCursor() + cmd := m.Command() + + if cur > 0 { + m.SetCommand(cmd[:cur-1] + cmd[cur:]) + m.SetCommandCursor(cur - 1) + } + + return nil +} + +type CommandDelete struct{} + +func (a CommandDelete) Execute(m Model) tea.Cmd { + cur := m.CommandCursor() + cmd := m.Command() + + if cur < len(cmd)-1 { + m.SetCommand(cmd[:cur+1] + cmd[cur+2:]) + } else if cur == len(cmd)-1 { + // last text char, delete it + m.SetCommand(cmd[:cur] + cmd[cur+1:]) + } else if cur == len(cmd) && cur > 0 { + // if at end, we do backspace op + m.SetCommand(cmd[:cur-1] + cmd[cur:]) + m.SetCommandCursor(cur - 1) + } + + return nil +} + +type CommandDeletePreviousWord struct{} + +func (a CommandDeletePreviousWord) Execute(m Model) tea.Cmd { + cur := m.CommandCursor() + cmd := m.Command() + + if cur > 0 { + newCur := cur + + // If we are on puncuation, we should just skip them all and quit + if isPunctuation(cmd[newCur-1]) { + for newCur > 0 && isPunctuation(cmd[newCur-1]) { + newCur-- + } + + m.SetCommand(cmd[:newCur] + cmd[cur:]) + m.SetCommandCursor(newCur) + + return nil + } + + // Skip whitespace immediately before the cursor + for newCur > 0 && (cmd[newCur-1] == ' ' || cmd[newCur-1] == '\t') { + newCur-- + } + + // Skip the word characters before the cursor + for newCur > 0 && isWordChar(cmd[newCur-1]) { + newCur-- + } + + // Delete everything from newCur up to cur in one operation + m.SetCommand(cmd[:newCur] + cmd[cur:]) + m.SetCommandCursor(newCur) + } + + return nil +} + +type CommandExecute struct{} + +func (a CommandExecute) Execute(m Model) tea.Cmd { + // TODO: Implement + + m.SetCommandCursor(0) + m.SetCommand("") + m.SetMode(NormalMode) + + return nil +} diff --git a/internal/action/misc.go b/internal/action/misc.go index 76cbbed..f21818a 100644 --- a/internal/action/misc.go +++ b/internal/action/misc.go @@ -14,6 +14,8 @@ type EnterComandMode struct{} func (a EnterComandMode) Execute(m Model) tea.Cmd { m.SetMode(CommandMode) + m.SetCommand("") + m.SetCommandCursor(0) return nil } diff --git a/internal/editor/model.go b/internal/editor/model.go index 6d2089e..c42da80 100644 --- a/internal/editor/model.go +++ b/internal/editor/model.go @@ -21,7 +21,6 @@ type Model struct { mode action.Mode win_h int win_w int - command string input *input.Handler // Insert repetition @@ -29,6 +28,10 @@ type Model struct { insertKeys []string insertAction action.Action + // Command mode + command string + commandCursor int + // Settings settings action.Settings } @@ -133,6 +136,29 @@ 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 + } +} + // Settings func (m *Model) Settings() action.Settings { return m.settings diff --git a/internal/editor/style.go b/internal/editor/style.go index 8ad7e21..01ffe91 100644 --- a/internal/editor/style.go +++ b/internal/editor/style.go @@ -17,7 +17,7 @@ func (m Model) cursorStyle() lipgloss.Style { // Bar/underline for insert mode return lipgloss.NewStyle().Underline(true) case action.CommandMode: - return lipgloss.NewStyle() + return lipgloss.NewStyle().Reverse(true) default: return lipgloss.NewStyle().Reverse(true) } diff --git a/internal/editor/update.go b/internal/editor/update.go index 0d03a1d..aef1eeb 100644 --- a/internal/editor/update.go +++ b/internal/editor/update.go @@ -1,7 +1,6 @@ package editor import ( - "git.gophernest.net/azpect/TextEditor/internal/action" tea "github.com/charmbracelet/bubbletea" ) @@ -20,25 +19,28 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Quit } - switch m.mode { - case action.NormalMode, - action.InsertMode, - action.VisualMode, - action.VisualBlockMode, - action.VisualLineMode: - cmd = m.input.Handle(&m, msg.String()) + cmd = m.input.Handle(&m, msg.String()) - // The only one left to migrate! - case action.CommandMode: - switch msg.String() { - case "esc": - m.mode = action.NormalMode - m.command = "" - - default: - m.command += msg.String() - } - } + // switch m.mode { + // case action.NormalMode, + // action.InsertMode, + // action.VisualMode, + // action.VisualBlockMode, + // action.VisualLineMode: + // cmd = m.input.Handle(&m, msg.String()) + // + // // The only one left to migrate! + // case action.CommandMode: + // switch msg.String() { + // case "esc": + // m.mode = action.NormalMode + // m.command = "" + // + // default: + // m.command += msg.String() + // m.SetCommandCursor(len(m.command)) + // } + // } } // Keep cursor in view after any update diff --git a/internal/editor/view.go b/internal/editor/view.go index 8bfc756..1e7824c 100644 --- a/internal/editor/view.go +++ b/internal/editor/view.go @@ -158,7 +158,21 @@ func drawStatusBar(m Model) string { func leftBar(m Model) (bar string) { if m.Mode() == action.CommandMode { - bar = fmt.Sprintf(":%s", m.command) + bar = ":" + cmd := m.Command() + cur := m.CommandCursor() + for i := 0; i < len(cmd); i++ { + if i == cur { + bar += m.cursorStyle().Render(string(cmd[i])) + } else { + bar += string(cmd[i]) + } + } + // Cursor at end of command + if cur >= len(cmd) { + bar += m.cursorStyle().Render(" ") + } + // bar = fmt.Sprintf("%s %d", bar, cur) } else { bar = fmt.Sprintf(" %s", m.Mode().ToString()) } diff --git a/internal/input/handler.go b/internal/input/handler.go index 7c436c7..88b1902 100644 --- a/internal/input/handler.go +++ b/internal/input/handler.go @@ -29,9 +29,10 @@ type Handler struct { pending string // partial key sequence (e.g., "g" waiting for second key) // Keymaps - normalKeymap *Keymap - visualKeymap *Keymap - insertKeymap *Keymap + normalKeymap *Keymap + visualKeymap *Keymap + insertKeymap *Keymap + commandKeymap *Keymap currentKeymap *Keymap } @@ -42,6 +43,7 @@ func NewHandler() *Handler { normalKeymap: NewNormalKeymap(), visualKeymap: NewVisualKeymap(), insertKeymap: NewInsertKeymap(), + commandKeymap: NewCommandKeymap(), currentKeymap: nil, } } @@ -58,9 +60,12 @@ func (h *Handler) Handle(m action.Model, key string) tea.Cmd { return nil } - // Insert mode bypasses the normal state machine entirely - if m.Mode() == action.InsertMode { + // Insert/command mode bypasses the normal state machine entirely + switch m.Mode() { + case action.InsertMode: return h.handleInsertKey(m, key) + case action.CommandMode: + return h.handleCommandKey(m, key) } // Try to accumulate count (only if no pending sequence) @@ -275,6 +280,19 @@ func (h *Handler) handleInsertKey(m action.Model, key string) tea.Cmd { return action.InsertChar{Char: key}.Execute(m) } +func (h *Handler) handleCommandKey(m action.Model, key string) tea.Cmd { + kind, binding := h.commandKeymap.Lookup(key) + switch kind { + case "action": + return binding.(action.Action).Execute(m) + case "motion": + return binding.(action.Motion).Execute(m) + } + + // Fallback: treat as a regular character to insert + return action.InsertCommandChar{Char: key}.Execute(m) +} + func normalizeVisualSelection(m action.Model) (action.Position, action.Position) { a := action.Position{Line: m.AnchorY(), Col: m.AnchorX()} c := action.Position{Line: m.CursorY(), Col: m.CursorX()} diff --git a/internal/input/keymap.go b/internal/input/keymap.go index 06fd829..765d20e 100644 --- a/internal/input/keymap.go +++ b/internal/input/keymap.go @@ -106,6 +106,24 @@ func NewInsertKeymap() *Keymap { } +func NewCommandKeymap() *Keymap { + return &Keymap{ + motions: map[string]action.Motion{ + "left": motion.MoveCommandLeft{}, + "right": motion.MoveCommandRight{}, + }, + operators: map[string]action.Operator{}, // this will likely be empty + actions: map[string]action.Action{ + "esc": action.ExitCommandMode{}, + "enter": action.CommandExecute{}, + "backspace": action.CommandBackspace{}, + "delete": action.CommandDelete{}, + "ctrl+w": action.CommandDeletePreviousWord{}, + }, + } + +} + // Lookup returns what type of binding a key is func (km *Keymap) Lookup(key string) (kind string, value any) { if m, ok := km.motions[key]; ok { diff --git a/internal/motion/command.go b/internal/motion/command.go new file mode 100644 index 0000000..1710b63 --- /dev/null +++ b/internal/motion/command.go @@ -0,0 +1,26 @@ +package motion + +import ( + "git.gophernest.net/azpect/TextEditor/internal/action" + tea "github.com/charmbracelet/bubbletea" +) + +type MoveCommandLeft struct{} + +func (a MoveCommandLeft) Execute(m action.Model) tea.Cmd { + // The set function handles bounds + m.SetCommandCursor(m.CommandCursor() - 1) + return nil +} + +func (a MoveCommandLeft) Type() action.MotionType { return action.Charwise } + +type MoveCommandRight struct{} + +func (a MoveCommandRight) Execute(m action.Model) tea.Cmd { + // The set function handles bounds + m.SetCommandCursor(m.CommandCursor() + 1) + return nil +} + +func (a MoveCommandRight) Type() action.MotionType { return action.Charwise }