feat: implemented command mode mappings, needs review and tests

This commit is contained in:
Hayden Hargreaves 2026-02-13 17:44:27 -07:00
parent 6b93fb2f6c
commit 307f89bcd1
10 changed files with 251 additions and 27 deletions

View File

@ -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

112
internal/action/command.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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)
}

View File

@ -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())
// 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

View File

@ -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())
}

View File

@ -32,6 +32,7 @@ type Handler struct {
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()}

View File

@ -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 {

View File

@ -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 }