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
|
||||
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
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 {
|
||||
m.SetMode(CommandMode)
|
||||
m.SetCommand("")
|
||||
m.SetCommandCursor(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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())
|
||||
}
|
||||
|
||||
@ -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()}
|
||||
|
||||
@ -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 {
|
||||
|
||||
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