feat: implemented command mode mappings, needs review and tests
This commit is contained in:
parent
6b93fb2f6c
commit
307f89bcd1
@ -72,6 +72,12 @@ type Model interface {
|
|||||||
InsertKeys() []string
|
InsertKeys() []string
|
||||||
SetInsertKeys(keys []string)
|
SetInsertKeys(keys []string)
|
||||||
|
|
||||||
|
// Command mode
|
||||||
|
Command() string
|
||||||
|
SetCommand(cmd string)
|
||||||
|
CommandCursor() int
|
||||||
|
SetCommandCursor(cur int)
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
Settings() Settings
|
Settings() Settings
|
||||||
|
|
||||||
|
|||||||
112
internal/action/command.go
Normal file
112
internal/action/command.go
Normal 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
|
||||||
|
}
|
||||||
@ -14,6 +14,8 @@ type EnterComandMode struct{}
|
|||||||
|
|
||||||
func (a EnterComandMode) Execute(m Model) tea.Cmd {
|
func (a EnterComandMode) Execute(m Model) tea.Cmd {
|
||||||
m.SetMode(CommandMode)
|
m.SetMode(CommandMode)
|
||||||
|
m.SetCommand("")
|
||||||
|
m.SetCommandCursor(0)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,6 @@ type Model struct {
|
|||||||
mode action.Mode
|
mode action.Mode
|
||||||
win_h int
|
win_h int
|
||||||
win_w int
|
win_w int
|
||||||
command string
|
|
||||||
input *input.Handler
|
input *input.Handler
|
||||||
|
|
||||||
// Insert repetition
|
// Insert repetition
|
||||||
@ -29,6 +28,10 @@ type Model struct {
|
|||||||
insertKeys []string
|
insertKeys []string
|
||||||
insertAction action.Action
|
insertAction action.Action
|
||||||
|
|
||||||
|
// Command mode
|
||||||
|
command string
|
||||||
|
commandCursor int
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
settings action.Settings
|
settings action.Settings
|
||||||
}
|
}
|
||||||
@ -133,6 +136,29 @@ func (m *Model) SetInsertKeys(keys []string) {
|
|||||||
m.insertKeys = keys
|
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
|
// Settings
|
||||||
func (m *Model) Settings() action.Settings {
|
func (m *Model) Settings() action.Settings {
|
||||||
return m.settings
|
return m.settings
|
||||||
|
|||||||
@ -17,7 +17,7 @@ func (m Model) cursorStyle() lipgloss.Style {
|
|||||||
// Bar/underline for insert mode
|
// Bar/underline for insert mode
|
||||||
return lipgloss.NewStyle().Underline(true)
|
return lipgloss.NewStyle().Underline(true)
|
||||||
case action.CommandMode:
|
case action.CommandMode:
|
||||||
return lipgloss.NewStyle()
|
return lipgloss.NewStyle().Reverse(true)
|
||||||
default:
|
default:
|
||||||
return lipgloss.NewStyle().Reverse(true)
|
return lipgloss.NewStyle().Reverse(true)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package editor
|
package editor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -20,25 +19,28 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
}
|
}
|
||||||
|
|
||||||
switch m.mode {
|
cmd = m.input.Handle(&m, msg.String())
|
||||||
case action.NormalMode,
|
|
||||||
action.InsertMode,
|
|
||||||
action.VisualMode,
|
|
||||||
action.VisualBlockMode,
|
|
||||||
action.VisualLineMode:
|
|
||||||
cmd = m.input.Handle(&m, msg.String())
|
|
||||||
|
|
||||||
// The only one left to migrate!
|
// switch m.mode {
|
||||||
case action.CommandMode:
|
// case action.NormalMode,
|
||||||
switch msg.String() {
|
// action.InsertMode,
|
||||||
case "esc":
|
// action.VisualMode,
|
||||||
m.mode = action.NormalMode
|
// action.VisualBlockMode,
|
||||||
m.command = ""
|
// action.VisualLineMode:
|
||||||
|
// cmd = m.input.Handle(&m, msg.String())
|
||||||
default:
|
//
|
||||||
m.command += 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
|
// Keep cursor in view after any update
|
||||||
|
|||||||
@ -158,7 +158,21 @@ func drawStatusBar(m Model) string {
|
|||||||
|
|
||||||
func leftBar(m Model) (bar string) {
|
func leftBar(m Model) (bar string) {
|
||||||
if m.Mode() == action.CommandMode {
|
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 {
|
} else {
|
||||||
bar = fmt.Sprintf(" %s", m.Mode().ToString())
|
bar = fmt.Sprintf(" %s", m.Mode().ToString())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,9 +29,10 @@ type Handler struct {
|
|||||||
pending string // partial key sequence (e.g., "g" waiting for second key)
|
pending string // partial key sequence (e.g., "g" waiting for second key)
|
||||||
|
|
||||||
// Keymaps
|
// Keymaps
|
||||||
normalKeymap *Keymap
|
normalKeymap *Keymap
|
||||||
visualKeymap *Keymap
|
visualKeymap *Keymap
|
||||||
insertKeymap *Keymap
|
insertKeymap *Keymap
|
||||||
|
commandKeymap *Keymap
|
||||||
|
|
||||||
currentKeymap *Keymap
|
currentKeymap *Keymap
|
||||||
}
|
}
|
||||||
@ -42,6 +43,7 @@ func NewHandler() *Handler {
|
|||||||
normalKeymap: NewNormalKeymap(),
|
normalKeymap: NewNormalKeymap(),
|
||||||
visualKeymap: NewVisualKeymap(),
|
visualKeymap: NewVisualKeymap(),
|
||||||
insertKeymap: NewInsertKeymap(),
|
insertKeymap: NewInsertKeymap(),
|
||||||
|
commandKeymap: NewCommandKeymap(),
|
||||||
currentKeymap: nil,
|
currentKeymap: nil,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -58,9 +60,12 @@ func (h *Handler) Handle(m action.Model, key string) tea.Cmd {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert mode bypasses the normal state machine entirely
|
// Insert/command mode bypasses the normal state machine entirely
|
||||||
if m.Mode() == action.InsertMode {
|
switch m.Mode() {
|
||||||
|
case action.InsertMode:
|
||||||
return h.handleInsertKey(m, key)
|
return h.handleInsertKey(m, key)
|
||||||
|
case action.CommandMode:
|
||||||
|
return h.handleCommandKey(m, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to accumulate count (only if no pending sequence)
|
// 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)
|
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) {
|
func normalizeVisualSelection(m action.Model) (action.Position, action.Position) {
|
||||||
a := action.Position{Line: m.AnchorY(), Col: m.AnchorX()}
|
a := action.Position{Line: m.AnchorY(), Col: m.AnchorX()}
|
||||||
c := action.Position{Line: m.CursorY(), Col: m.CursorX()}
|
c := action.Position{Line: m.CursorY(), Col: m.CursorX()}
|
||||||
|
|||||||
@ -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
|
// Lookup returns what type of binding a key is
|
||||||
func (km *Keymap) Lookup(key string) (kind string, value any) {
|
func (km *Keymap) Lookup(key string) (kind string, value any) {
|
||||||
if m, ok := km.motions[key]; ok {
|
if m, ok := km.motions[key]; ok {
|
||||||
|
|||||||
26
internal/motion/command.go
Normal file
26
internal/motion/command.go
Normal 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 }
|
||||||
Loading…
x
Reference in New Issue
Block a user