feat: implemented insert mode keymaps and ctrl+w, tested
This commit is contained in:
parent
49ef0212a6
commit
39c4bf1b6b
@ -8,7 +8,7 @@ import (
|
||||
|
||||
func main() {
|
||||
|
||||
lines := []string{"Hello world", "line 2", "line 3", "line 4", "line 5"}
|
||||
lines := []string{"Hello world testing main.go", "line 2", "line 3", "line 4", "line 5"}
|
||||
tea.NewProgram(
|
||||
editor.NewModel(lines, action.Position{Line: 0, Col: 0}),
|
||||
tea.WithAltScreen(),
|
||||
|
||||
@ -37,6 +37,13 @@ type Model interface {
|
||||
SetAnchorX(x int)
|
||||
SetAnchorY(y int)
|
||||
|
||||
// Insert
|
||||
InsertKeys() []string
|
||||
SetInsertKeys(keys []string)
|
||||
|
||||
// Settings
|
||||
TabSize() int
|
||||
|
||||
// Mode
|
||||
Mode() Mode
|
||||
SetMode(mode Mode)
|
||||
@ -44,6 +51,9 @@ type Model interface {
|
||||
|
||||
// Insert recording (for count replay)
|
||||
SetInsertRecording(count int, action Action)
|
||||
|
||||
// ExitInsertMode handles replay, cursor step-back, and mode transition on esc
|
||||
ExitInsertMode()
|
||||
}
|
||||
|
||||
// Position represents a location in the buffer
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
package action
|
||||
|
||||
import tea "github.com/charmbracelet/bubbletea"
|
||||
import (
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
// EnterInsert implements Action (i)
|
||||
type EnterInsert struct {
|
||||
@ -121,3 +125,154 @@ func (a OpenLineAbove) Execute(m Model) tea.Cmd {
|
||||
func (a OpenLineAbove) WithCount(n int) Action {
|
||||
return OpenLineAbove{Count: n}
|
||||
}
|
||||
|
||||
// --- Insert mode edit actions ---
|
||||
|
||||
// InsertChar inserts a single character (or rune sequence) at the cursor
|
||||
type InsertChar struct {
|
||||
Char string
|
||||
}
|
||||
|
||||
func (a InsertChar) Execute(m Model) tea.Cmd {
|
||||
x, y := m.CursorX(), m.CursorY()
|
||||
l := m.Line(y)
|
||||
if x < len(l) {
|
||||
m.SetLine(y, l[:x]+a.Char+l[x:])
|
||||
} else {
|
||||
m.SetLine(y, l+a.Char)
|
||||
}
|
||||
m.SetCursorX(x + len(a.Char))
|
||||
return nil
|
||||
}
|
||||
|
||||
// InsertNewline splits the current line at the cursor (enter key)
|
||||
type InsertNewline struct{}
|
||||
|
||||
func (a InsertNewline) Execute(m Model) tea.Cmd {
|
||||
x, y := m.CursorX(), m.CursorY()
|
||||
l := m.Line(y)
|
||||
if x == len(l) {
|
||||
m.InsertLine(y+1, "")
|
||||
} else {
|
||||
m.SetLine(y, l[:x])
|
||||
m.InsertLine(y+1, l[x:])
|
||||
}
|
||||
m.SetCursorY(y + 1)
|
||||
m.SetCursorX(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
// InsertBackspace deletes the character before the cursor
|
||||
type InsertBackspace struct{}
|
||||
|
||||
func (a InsertBackspace) Execute(m Model) tea.Cmd {
|
||||
x, y := m.CursorX(), m.CursorY()
|
||||
l := m.Line(y)
|
||||
if x > 0 {
|
||||
m.SetLine(y, l[:x-1]+l[x:])
|
||||
m.SetCursorX(x - 1)
|
||||
} else if y > 0 {
|
||||
prevLine := m.Line(y - 1)
|
||||
newX := len(prevLine)
|
||||
m.SetLine(y-1, prevLine+l)
|
||||
m.DeleteLine(y)
|
||||
m.SetCursorY(y - 1)
|
||||
m.SetCursorX(newX)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// InsertDelete deletes the character under/after the cursor (delete key)
|
||||
type InsertDelete struct{}
|
||||
|
||||
func (a InsertDelete) Execute(m Model) tea.Cmd {
|
||||
x, y := m.CursorX(), m.CursorY()
|
||||
l := m.Line(y)
|
||||
if x == len(l) && y < m.LineCount()-1 {
|
||||
nextLine := m.Line(y + 1)
|
||||
m.SetLine(y, l+nextLine)
|
||||
m.DeleteLine(y + 1)
|
||||
} else if x < len(l) {
|
||||
m.SetLine(y, l[:x]+l[x+1:])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// InsertTab inserts spaces equal to the tab size
|
||||
type InsertTab struct{}
|
||||
|
||||
func (a InsertTab) Execute(m Model) tea.Cmd {
|
||||
x, y := m.CursorX(), m.CursorY()
|
||||
l := m.Line(y)
|
||||
tabs := strings.Repeat(" ", m.TabSize())
|
||||
if x < len(l) {
|
||||
m.SetLine(y, l[:x]+tabs+l[x:])
|
||||
} else {
|
||||
m.SetLine(y, l+tabs)
|
||||
}
|
||||
m.SetCursorX(x + len(tabs))
|
||||
return nil
|
||||
}
|
||||
|
||||
// InsertDeletePreviousWord deletes the word before the cursor (ctrl+w)
|
||||
type InsertDeletePreviousWord struct{}
|
||||
|
||||
func isWordChar(c byte) bool {
|
||||
return (c >= 'a' && c <= 'z') ||
|
||||
(c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') ||
|
||||
c == '_'
|
||||
}
|
||||
|
||||
func isPunctuation(c byte) bool {
|
||||
return c != ' ' && c != '\t' && !isWordChar(c)
|
||||
}
|
||||
|
||||
func (a InsertDeletePreviousWord) Execute(m Model) tea.Cmd {
|
||||
x, y := m.CursorX(), m.CursorY()
|
||||
line := m.Line(y)
|
||||
|
||||
// At start of line: merge with previous line (same as backspace)
|
||||
if x == 0 {
|
||||
if y > 0 {
|
||||
prevLine := m.Line(y - 1)
|
||||
newX := len(prevLine)
|
||||
m.SetLine(y-1, prevLine+line)
|
||||
m.DeleteLine(y)
|
||||
m.SetCursorY(y - 1)
|
||||
m.SetCursorX(newX)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Scan backwards to find the new cursor position (don't mutate yet)
|
||||
newX := x
|
||||
|
||||
// If we are on puncuation, we should just skip them all and quit
|
||||
if isPunctuation(line[newX-1]) {
|
||||
for newX > 0 && isPunctuation(line[newX-1]) {
|
||||
newX--
|
||||
}
|
||||
|
||||
m.SetLine(y, line[:newX]+line[x:])
|
||||
m.SetCursorX(newX)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Skip whitespace immediately before the cursor
|
||||
for newX > 0 && (line[newX-1] == ' ' || line[newX-1] == '\t') {
|
||||
newX--
|
||||
}
|
||||
|
||||
// Skip the word characters before the cursor
|
||||
for newX > 0 && isWordChar(line[newX-1]) {
|
||||
newX--
|
||||
}
|
||||
|
||||
// Delete everything from newX up to x in one operation
|
||||
m.SetLine(y, line[:newX]+line[x:])
|
||||
m.SetCursorX(newX)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -25,6 +25,8 @@ func sendKeys(tm *teatest.TestModel, keys ...string) {
|
||||
tm.Send(tea.KeyMsg{Type: tea.KeyCtrlD})
|
||||
case "ctrl+v":
|
||||
tm.Send(tea.KeyMsg{Type: tea.KeyCtrlV})
|
||||
case "ctrl+w":
|
||||
tm.Send(tea.KeyMsg{Type: tea.KeyCtrlW})
|
||||
default:
|
||||
tm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(key)})
|
||||
}
|
||||
|
||||
@ -456,3 +456,174 @@ func TestInsertModeDelete(t *testing.T) {
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestInsertModeDeletePreviousWord(t *testing.T) {
|
||||
t.Run("test 'ctrl+w' deletes word", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
|
||||
sendKeys(tm, "a", "ctrl+w", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.lines[0] != "hello " {
|
||||
t.Errorf("lines[0] = %q, want 'hello '", m.lines[0])
|
||||
}
|
||||
if m.CursorX() != 5 {
|
||||
t.Errorf("CursorX() = %d, want '5'", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'ctrl+w' deletes word with whitespace", func(t *testing.T) {
|
||||
lines := []string{"hello "}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
|
||||
sendKeys(tm, "a", "ctrl+w", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.lines[0] != "" {
|
||||
t.Errorf("lines[0] = %q, want ''", m.lines[0])
|
||||
}
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("CursorX() = %d, want '0'", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'ctrl+w' deletes word until period", func(t *testing.T) {
|
||||
lines := []string{"hello wo..."}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
|
||||
sendKeys(tm, "a", "ctrl+w", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.lines[0] != "hello wo" {
|
||||
t.Errorf("lines[0] = %q, want 'hello wo'", m.lines[0])
|
||||
}
|
||||
if m.CursorX() != 7 {
|
||||
t.Errorf("CursorX() = %d, want '7'", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'ctrl+w' deletes line when blank", func(t *testing.T) {
|
||||
lines := []string{"", ""}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
|
||||
sendKeys(tm, "a", "ctrl+w", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.LineCount() != 1 {
|
||||
t.Errorf("LineCount() = %d, want '1'", m.LineCount())
|
||||
}
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("CursorX() = %d, want '0'", m.CursorX())
|
||||
}
|
||||
if m.CursorY() != 0 {
|
||||
t.Errorf("CursorY() = %d, want '0'", m.CursorY())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'ctrl+w' deletes all whitespace when line is only whitespace", func(t *testing.T) {
|
||||
lines := []string{" "}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
|
||||
sendKeys(tm, "a", "ctrl+w", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.LineCount() != 1 {
|
||||
t.Errorf("LineCount() = %d, want '1'", m.LineCount())
|
||||
}
|
||||
if m.Line(0) != "" {
|
||||
t.Errorf("Line(0) = %s, want ''", m.Line(0))
|
||||
}
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("CursorX() = %d, want '0'", m.CursorX())
|
||||
}
|
||||
if m.CursorY() != 0 {
|
||||
t.Errorf("CursorY() = %d, want '0'", m.CursorY())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'ctrl+w' at start of first line does nothing", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "i", "ctrl+w", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.lines[0] != "hello" {
|
||||
t.Errorf("lines[0] = %q, want 'hello'", m.lines[0])
|
||||
}
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'ctrl+w' from middle of word", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
|
||||
sendKeys(tm, "i", "ctrl+w", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.lines[0] != "lo" {
|
||||
t.Errorf("lines[0] = %q, want 'lo'", m.lines[0])
|
||||
}
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'ctrl+w' deletes word after punctuation", func(t *testing.T) {
|
||||
lines := []string{"...hello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 7, Line: 0})
|
||||
sendKeys(tm, "a", "ctrl+w", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.lines[0] != "..." {
|
||||
t.Errorf("lines[0] = %q, want '...'", m.lines[0])
|
||||
}
|
||||
if m.CursorX() != 2 {
|
||||
t.Errorf("CursorX() = %d, want 2", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'ctrl+w' with tabs as whitespace", func(t *testing.T) {
|
||||
lines := []string{"hello\tworld"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
|
||||
sendKeys(tm, "a", "ctrl+w", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.lines[0] != "hello\t" {
|
||||
t.Errorf("lines[0] = %q, want 'hello\\t'", m.lines[0])
|
||||
}
|
||||
if m.CursorX() != 5 {
|
||||
t.Errorf("CursorX() = %d, want 5", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'ctrl+w' at start of line merges with previous line content", func(t *testing.T) {
|
||||
lines := []string{"hello", "world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
|
||||
sendKeys(tm, "i", "ctrl+w", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.LineCount() != 1 {
|
||||
t.Errorf("LineCount() = %d, want 1", m.LineCount())
|
||||
}
|
||||
if m.lines[0] != "helloworld" {
|
||||
t.Errorf("lines[0] = %q, want 'helloworld'", m.lines[0])
|
||||
}
|
||||
if m.CursorX() != 4 {
|
||||
t.Errorf("CursorX() = %d, want 4", m.CursorX())
|
||||
}
|
||||
if m.CursorY() != 0 {
|
||||
t.Errorf("CursorY() = %d, want 0", m.CursorY())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'ctrl+w' with underscore in word", func(t *testing.T) {
|
||||
lines := []string{"hello_world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
|
||||
sendKeys(tm, "a", "ctrl+w", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.lines[0] != "" {
|
||||
t.Errorf("lines[0] = %q, want ''", m.lines[0])
|
||||
}
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -124,6 +124,20 @@ func (m *Model) SetAnchorY(y int) {
|
||||
m.anchor.y = y
|
||||
}
|
||||
|
||||
// Insert methods
|
||||
func (m *Model) InsertKeys() []string {
|
||||
return m.insertKeys
|
||||
}
|
||||
|
||||
func (m *Model) SetInsertKeys(keys []string) {
|
||||
m.insertKeys = keys
|
||||
}
|
||||
|
||||
// Settings
|
||||
func (m *Model) TabSize() int {
|
||||
return m.tabSize
|
||||
}
|
||||
|
||||
func (m *Model) ClampCursorX() {
|
||||
lineLen := len(m.lines[m.cursor.y])
|
||||
if lineLen == 0 {
|
||||
@ -181,6 +195,18 @@ func (m *Model) replayInsert() {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) ExitInsertMode() {
|
||||
if m.insertCount > 1 {
|
||||
m.replayInsert()
|
||||
}
|
||||
if m.cursor.x > 0 {
|
||||
m.cursor.x--
|
||||
}
|
||||
m.mode = action.NormalMode
|
||||
m.insertCount = 0
|
||||
m.insertKeys = nil
|
||||
}
|
||||
|
||||
func (m *Model) processInsertKey(key string) {
|
||||
x := m.CursorX()
|
||||
y := m.CursorY()
|
||||
@ -188,17 +214,12 @@ func (m *Model) processInsertKey(key string) {
|
||||
|
||||
switch key {
|
||||
case "enter":
|
||||
|
||||
// Simple case, at end, just create a line
|
||||
if x == len(l) {
|
||||
m.InsertLine(y+1, "")
|
||||
|
||||
// otherwise, splice
|
||||
} else {
|
||||
m.SetLine(y, l[:x])
|
||||
m.InsertLine(y+1, l[x:])
|
||||
}
|
||||
|
||||
m.SetCursorY(y + 1)
|
||||
m.SetCursorX(0)
|
||||
|
||||
@ -216,15 +237,14 @@ func (m *Model) processInsertKey(key string) {
|
||||
}
|
||||
|
||||
case "delete":
|
||||
if x == len(l) && y < m.LineCount() {
|
||||
if x == len(l) && y < m.LineCount()-1 {
|
||||
nextLine := m.Line(y + 1)
|
||||
m.SetLine(y, l+nextLine)
|
||||
m.DeleteLine(y + 1)
|
||||
} else if x >= 0 {
|
||||
} else if x < len(l) {
|
||||
m.SetLine(y, l[:x]+l[x+1:])
|
||||
}
|
||||
|
||||
// TODO: This handling is wrong, we should be able to delete an entire tab with a single space
|
||||
case "tab":
|
||||
tabs := strings.Repeat(" ", m.tabSize)
|
||||
if x < len(l) {
|
||||
@ -263,7 +283,6 @@ func (m *Model) processInsertKey(key string) {
|
||||
m.SetCursorY(y + 1)
|
||||
}
|
||||
|
||||
// Regular character
|
||||
default:
|
||||
if x < len(l) {
|
||||
m.SetLine(y, l[:x]+key+l[x:])
|
||||
|
||||
@ -13,43 +13,20 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.win_w = msg.Width
|
||||
|
||||
case tea.KeyMsg:
|
||||
// BUG: for use in debugging, until we have command mode
|
||||
if msg.String() == "ctrl+c" {
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
||||
switch m.mode {
|
||||
case action.NormalMode,
|
||||
action.InsertMode,
|
||||
action.VisualMode,
|
||||
action.VisualBlockMode,
|
||||
action.VisualLineMode:
|
||||
return m, m.input.Handle(&m, msg.String())
|
||||
|
||||
// TODO: This should be handled elsewhere
|
||||
case action.InsertMode:
|
||||
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 = action.NormalMode
|
||||
m.insertCount = 0
|
||||
m.insertKeys = nil
|
||||
|
||||
case "ctrl+c", "ctrl+d":
|
||||
return m, tea.Quit
|
||||
|
||||
default:
|
||||
// Record and process
|
||||
m.insertKeys = append(m.insertKeys, key)
|
||||
m.processInsertKey(key)
|
||||
}
|
||||
// The only one left to migrate!
|
||||
case action.CommandMode:
|
||||
switch msg.String() {
|
||||
case "esc":
|
||||
|
||||
@ -31,6 +31,7 @@ type Handler struct {
|
||||
// Keymaps
|
||||
normalKeymap *Keymap
|
||||
visualKeymap *Keymap
|
||||
insertKeymap *Keymap
|
||||
|
||||
currentKeymap *Keymap
|
||||
}
|
||||
@ -40,19 +41,28 @@ func NewHandler() *Handler {
|
||||
// keymap: NewNormalKeymap(),
|
||||
normalKeymap: NewNormalKeymap(),
|
||||
visualKeymap: NewVisualKeymap(),
|
||||
insertKeymap: NewInsertKeymap(),
|
||||
currentKeymap: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) Handle(m action.Model, key string) tea.Cmd {
|
||||
// ESC always resets everything
|
||||
// TODO: This should prob be relocated
|
||||
if key == "esc" {
|
||||
h.Reset()
|
||||
m.SetMode(action.NormalMode)
|
||||
if m.Mode() == action.InsertMode {
|
||||
m.ExitInsertMode()
|
||||
} else {
|
||||
m.SetMode(action.NormalMode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Insert mode bypasses the normal state machine entirely
|
||||
if m.Mode() == action.InsertMode {
|
||||
return h.handleInsertKey(m, key)
|
||||
}
|
||||
|
||||
// Try to accumulate count (only if no pending sequence)
|
||||
if h.pending == "" && h.tryAccumulateCount(key) {
|
||||
return nil
|
||||
@ -62,7 +72,6 @@ func (h *Handler) Handle(m action.Model, key string) tea.Cmd {
|
||||
sequence := h.pending + key
|
||||
|
||||
// Set working keymap
|
||||
// TODO: Do we need to reset anywhere?
|
||||
switch m.Mode() {
|
||||
case action.NormalMode:
|
||||
h.currentKeymap = h.normalKeymap
|
||||
@ -244,6 +253,23 @@ func (h *Handler) Pending() string {
|
||||
return h.buffer
|
||||
}
|
||||
|
||||
func (h *Handler) handleInsertKey(m action.Model, key string) tea.Cmd {
|
||||
// Record the key for count replay (e.g. 5i...)
|
||||
m.SetInsertKeys(append(m.InsertKeys(), key))
|
||||
|
||||
// Check the insert keymap first
|
||||
kind, binding := h.insertKeymap.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.InsertChar{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()}
|
||||
|
||||
@ -85,6 +85,27 @@ func NewVisualKeymap() *Keymap {
|
||||
}
|
||||
}
|
||||
|
||||
func NewInsertKeymap() *Keymap {
|
||||
return &Keymap{
|
||||
motions: map[string]action.Motion{
|
||||
"down": motion.MoveDown{Count: 1},
|
||||
"up": motion.MoveUp{Count: 1},
|
||||
"left": motion.MoveLeft{Count: 1},
|
||||
"right": motion.MoveRight{Count: 1},
|
||||
},
|
||||
operators: map[string]action.Operator{}, // this will likely be empty
|
||||
actions: map[string]action.Action{
|
||||
"enter": action.InsertNewline{},
|
||||
"backspace": action.InsertBackspace{},
|
||||
"delete": action.InsertDelete{},
|
||||
"tab": action.InsertTab{},
|
||||
"ctrl+w": action.InsertDeletePreviousWord{},
|
||||
"ctrl+c": action.Quit{},
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user