-- SPDX-FileCopyrightText: 2026 Yuki Otsuka
--
-- SPDX-License-Identifier: BSD-3
import ViE.State
import ViE.Types
import ViE.Key.Handler
import ViE.Window.Actions
import ViE.Command.Explorer
import ViE.Command.Dispatch
import ViE.Basic
import ViE.Buffer.EditQueue
import ViE.Language.Key
namespace ViE.Key
open ViE
open ViE.Window
open ViE.Feature
open ViE.Key
def arrayInsertAt (arr : Array α) (idx : Nat) (x : α) : Array α :=
let i := min idx arr.size
(arr.toList.take i ++ [x] ++ arr.toList.drop i).toArray
def arrayEraseAt (arr : Array α) (idx : Nat) : Array α :=
if idx >= arr.size then arr
else
(arr.toList.take idx ++ arr.toList.drop (idx + 1)).toArray
def overlayLineLen (line : String) : Nat :=
line.toList.length
def overlayNormalize (ov : FloatingOverlay) : FloatingOverlay :=
if ov.lines.isEmpty then
{ ov with lines := #[""], cursorRow := 0, cursorCol := 0 }
else
let row := min ov.cursorRow (ov.lines.size - 1)
let col := min ov.cursorCol (overlayLineLen (ov.lines[row]!))
{ ov with cursorRow := row, cursorCol := col }
def updateOverlay (s : EditorState) (f : FloatingOverlay → FloatingOverlay) : EditorState :=
match s.floatingOverlay with
| none => s
| some ov =>
{ s with
floatingOverlay := some (overlayNormalize (f ov))
dirty := true
}
def overlayMoveLeft (s : EditorState) : EditorState :=
updateOverlay s (fun ov =>
if ov.cursorCol > 0 then { ov with cursorCol := ov.cursorCol - 1 } else ov)
def overlayMoveRight (s : EditorState) : EditorState :=
updateOverlay s (fun ov =>
let ov := overlayNormalize ov
let len := overlayLineLen (ov.lines[ov.cursorRow]!)
if ov.cursorCol < len then { ov with cursorCol := ov.cursorCol + 1 } else ov)
def overlayMoveUp (s : EditorState) : EditorState :=
updateOverlay s (fun ov =>
if ov.cursorRow > 0 then { ov with cursorRow := ov.cursorRow - 1 } else ov)
def overlayMoveDown (s : EditorState) : EditorState :=
updateOverlay s (fun ov =>
let ov := overlayNormalize ov
if ov.cursorRow + 1 < ov.lines.size then { ov with cursorRow := ov.cursorRow + 1 } else ov)
def overlayMoveLineStart (s : EditorState) : EditorState :=
updateOverlay s (fun ov => { ov with cursorCol := 0 })
def overlayMoveLineEnd (s : EditorState) : EditorState :=
updateOverlay s (fun ov =>
let ov := overlayNormalize ov
{ ov with cursorCol := overlayLineLen (ov.lines[ov.cursorRow]!) })
def overlayInsertChar (s : EditorState) (c : Char) : EditorState :=
updateOverlay s (fun ov =>
let ov := overlayNormalize ov
let row := ov.cursorRow
let col := ov.cursorCol
let line := ov.lines[row]!
let chars := line.toList
let newLine := String.ofList (chars.take col ++ [c] ++ chars.drop col)
{ ov with lines := ov.lines.set! row newLine, cursorCol := col + 1 })
def overlayBackspace (s : EditorState) : EditorState :=
updateOverlay s (fun ov =>
let ov := overlayNormalize ov
let row := ov.cursorRow
let col := ov.cursorCol
let line := ov.lines[row]!
if col > 0 then
let chars := line.toList
let newLine := String.ofList (chars.take (col - 1) ++ chars.drop col)
{ ov with lines := ov.lines.set! row newLine, cursorCol := col - 1 }
else if row > 0 then
let prev := ov.lines[row - 1]!
let merged := prev ++ line
let lines := (ov.lines.set! (row - 1) merged)
let lines := arrayEraseAt lines row
{ ov with lines := lines, cursorRow := row - 1, cursorCol := overlayLineLen prev }
else
ov)
def overlayDeleteCharAt (s : EditorState) : EditorState :=
updateOverlay s (fun ov =>
let ov := overlayNormalize ov
let row := ov.cursorRow
let col := ov.cursorCol
let line := ov.lines[row]!
let len := overlayLineLen line
if col < len then
let chars := line.toList
let newLine := String.ofList (chars.take col ++ chars.drop (col + 1))
{ ov with lines := ov.lines.set! row newLine }
else if row + 1 < ov.lines.size then
let next := ov.lines[row + 1]!
let merged := line ++ next
let lines := (ov.lines.set! row merged)
let lines := arrayEraseAt lines (row + 1)
{ ov with lines := lines }
else
ov)
def overlayInsertNewline (s : EditorState) : EditorState :=
updateOverlay s (fun ov =>
let ov := overlayNormalize ov
let row := ov.cursorRow
let col := ov.cursorCol
let line := ov.lines[row]!
let chars := line.toList
let left := String.ofList (chars.take col)
let right := String.ofList (chars.drop col)
let lines := (ov.lines.set! row left)
let lines := arrayInsertAt lines (row + 1) right
{ ov with lines := lines, cursorRow := row + 1, cursorCol := 0 })
def overlayOpenLineBelow (s : EditorState) : EditorState :=
updateOverlay s (fun ov =>
let ov := overlayNormalize ov
let row := ov.cursorRow + 1
let lines := arrayInsertAt ov.lines row ""
{ ov with lines := lines, cursorRow := row, cursorCol := 0 })
def overlayOpenLineAbove (s : EditorState) : EditorState :=
updateOverlay s (fun ov =>
let ov := overlayNormalize ov
let row := ov.cursorRow
let lines := arrayInsertAt ov.lines row ""
{ ov with lines := lines, cursorRow := row, cursorCol := 0 })
def shouldRenderMessageAsFloat (msg : String) : Bool :=
let m := msg.trimAscii.toString
if m.isEmpty then
false
else
m.startsWith "Error" ||
m.startsWith "Cannot" ||
m.startsWith "Invalid" ||
m.startsWith "Unknown" ||
m.startsWith "No " ||
m.startsWith "Empty " ||
m.startsWith "Usage:" ||
m.startsWith "failed" ||
m.startsWith "Failed" ||
m.contains "not found"
/-- Default key bindings. -/
def makeKeyMap (commands : CommandMap) : KeyMap := {
normal := fun s k => do
let s ← ViE.Buffer.EditQueue.awaitActiveInsertOps s
let rawCount (state : EditorState) : Nat :=
match state.inputState.countBuffer.toNat? with
| some n => n
| none => 0
let handleMotion (state : EditorState) (motion : EditorState → EditorState) : IO EditorState :=
let n := if state.getCount == 0 then 1 else state.getCount
let rec applyN (st : EditorState) (count : Nat) : EditorState :=
match count with
| 0 => st
| n + 1 => applyN (motion st) n
termination_by count
match state.inputState.previousKey with
| none => pure $ clearInput (EditorState.clampCursor (applyN state n))
| some 'd' =>
let start := state.getCursor
let endP := (applyN state n).getCursor
pure $ clearInput (state.deleteRange start endP)
| some 'c' =>
let start := state.getCursor
let endP := (applyN state n).getCursor
pure $ clearInput (state.deleteRange start endP |>.setMode Mode.insert)
| _ => pure $ clearInput (EditorState.clampCursor (applyN state n))
let submitFloatingInput (state : EditorState) : IO EditorState := do
match state.floatingInputCommand, state.floatingOverlay with
| some cmdPrefix, some ov =>
let input := (ov.lines[0]?.getD "").trimAscii.toString
let cmd := s!"{cmdPrefix}{input}"
let pre := {
state with
floatingOverlay := none
floatingInputCommand := none
mode := .normal
inputState := {
state.inputState with
commandBuffer := cmd
countBuffer := ""
previousKey := none
pendingKeys := ""
}
}
let out ← ViE.Command.executeCommand commands pre
return {
out with
mode := .normal
inputState := {
out.inputState with
commandBuffer := ""
countBuffer := ""
previousKey := none
pendingKeys := ""
}
dirty := true
}
| _, _ =>
return {
state with
floatingOverlay := none
floatingInputCommand := none
dirty := true
}
let closeOverlay : EditorState :=
{ s with floatingOverlay := none, floatingInputCommand := none, dirty := true, message := "floating overlay cleared" }
if s.floatingOverlay.isSome then
if s.floatingInputCommand.isSome then
match k with
| Key.esc => pure { s with floatingOverlay := none, floatingInputCommand := none, mode := .normal, dirty := true, message := "" }
| Key.enter => submitFloatingInput s
| Key.char 'q' => pure { s with floatingOverlay := none, floatingInputCommand := none, mode := .normal, dirty := true, message := "" }
| Key.left => pure (overlayMoveLeft s)
| Key.right => pure (overlayMoveRight s)
| Key.up => pure (overlayMoveUp s)
| Key.down => pure (overlayMoveDown s)
| Key.backspace => pure (overlayBackspace s)
| Key.char c => pure (overlayInsertChar s c)
| _ => pure s
else
match k with
| Key.esc => pure closeOverlay
| Key.enter => pure closeOverlay
| Key.char 'q' => pure closeOverlay
| Key.char ':' => pure $ s.setMode Mode.command
| Key.char 'i' => pure { s with mode := .insert, dirty := true }
| Key.char 'a' => pure { (overlayMoveRight s) with mode := .insert }
| Key.char 'h' => pure (overlayMoveLeft s)
| Key.char 'j' => pure (overlayMoveDown s)
| Key.char 'k' => pure (overlayMoveUp s)
| Key.char 'l' => pure (overlayMoveRight s)
| Key.char '0' => pure (overlayMoveLineStart s)
| Key.char '$' => pure (overlayMoveLineEnd s)
| Key.char 'x' => pure (overlayDeleteCharAt s)
| Key.char 'o' => pure { (overlayOpenLineBelow s) with mode := .insert }
| Key.char 'O' => pure { (overlayOpenLineAbove s) with mode := .insert }
| _ => pure s
else
if s.inputState.previousKey == some 'f' then
match k with
| Key.char c =>
let s' := { s with inputState := { s.inputState with previousKey := none } }
let n := if s'.getCount == 0 then 1 else s'.getCount
pure $ clearInput (EditorState.findCharMotion s' c true false n true)
| _ =>
pure { s with inputState := { s.inputState with previousKey := none, countBuffer := "" } }
else if s.inputState.previousKey == some 't' then
match k with
| Key.char c =>
let s' := { s with inputState := { s.inputState with previousKey := none } }
let n := if s'.getCount == 0 then 1 else s'.getCount
pure $ clearInput (EditorState.findCharMotion s' c true true n true)
| _ =>
pure { s with inputState := { s.inputState with previousKey := none, countBuffer := "" } }
else
if shouldRenderMessageAsFloat s.message then
match k with
| Key.esc => pure { s with message := "", dirty := true }
| Key.enter => pure { s with message := "", dirty := true }
| _ => pure s
else
match k with
| Key.esc =>
pure { s with inputState := { s.inputState with countBuffer := "", previousKey := none } }
| Key.char 'h' =>
-- Check if in explorer buffer
let buf := s.getActiveBuffer
let isExplorer := s.explorers.any (fun (id, _) => id == buf.id)
if isExplorer then
let explorerOpt := s.explorers.find? (fun (id, _) => id == buf.id)
match explorerOpt with
| some (_, explorer) =>
if explorer.mode == .files then
let parentPath := match (System.FilePath.mk explorer.currentPath).parent with
| some p => p.toString
| none => "/"
ViE.Feature.openExplorerWithPreview s parentPath explorer.previewWindowId explorer.previewBufferId explorer.targetWindowId
else
handleMotion s EditorState.moveCursorLeft
| none => pure $ clearInput (EditorState.moveCursorLeftN s s.getCount)
else
handleMotion s EditorState.moveCursorLeft
| Key.char 'j' => do
let s' ← handleMotion s EditorState.moveCursorDown
ViE.Feature.refreshExplorerPreview s'
| Key.char 'k' => do
let s' ← handleMotion s EditorState.moveCursorUp
ViE.Feature.refreshExplorerPreview s'
| Key.char 'l' => handleMotion s EditorState.moveCursorRight
| Key.enter => ViE.Feature.handleExplorerEnter s
| Key.char 'i' => pure $ s.setMode Mode.insert
| Key.char 'a' =>
let s' := EditorState.moveCursorRight s
pure $ s'.setMode Mode.insert
| Key.char 'A' =>
let s' := EditorState.moveToLineEnd s
pure $ s'.setMode Mode.insert
| Key.char ':' => pure $ s.setMode Mode.command
| Key.char '/' =>
pure {
s with
mode := Mode.searchForward
searchState := none
message := ""
inputState := {
s.inputState with
commandBuffer := ""
countBuffer := ""
previousKey := none
pendingSearch := false
}
}
| Key.char '?' =>
pure {
s with
mode := Mode.searchBackward
searchState := none
message := ""
inputState := {
s.inputState with
commandBuffer := ""
countBuffer := ""
previousKey := none
pendingSearch := false
}
}
| Key.char 'q' => pure { s with message := "macro recording not implemented", dirty := true }
| Key.char 'o' => pure $ (EditorState.insertLineBelow s).setMode Mode.insert
| Key.char 'O' => pure $ (EditorState.insertLineAbove s).setMode Mode.insert
| Key.char 'v' => pure $ EditorState.startVisualMode s
| Key.char 'V' => pure $ EditorState.startVisualLineMode s
| Key.ctrl 'v' => pure $ EditorState.startVisualBlockMode s
| Key.char '0' =>
if s.inputState.countBuffer.isEmpty then handleMotion s EditorState.moveToLineStart
else
pure { s with inputState := { s.inputState with countBuffer := s.inputState.countBuffer.push '0' } }
| Key.char '$' => handleMotion s EditorState.moveToLineEnd
| Key.char 'H' =>
let n := rawCount s
pure $ clearInput ((s.pushJumpPoint).moveToScreenTop n)
| Key.char 'M' =>
pure $ clearInput ((s.pushJumpPoint).moveToScreenMiddle)
| Key.char 'L' =>
let n := rawCount s
pure $ clearInput ((s.pushJumpPoint).moveToScreenBottom n)
| Key.char 'f' =>
pure { s with inputState := { s.inputState with previousKey := some 'f' } }
| Key.char 't' =>
pure { s with inputState := { s.inputState with previousKey := some 't' } }
| Key.char ';' =>
pure $ clearInput (EditorState.repeatFindChar s false)
| Key.char ',' =>
pure $ clearInput (EditorState.repeatFindChar s true)
| Key.char '%' =>
pure $ clearInput ((s.pushJumpPoint).jumpMatchingBracket)
| Key.char '*' =>
pure $ clearInput (EditorState.searchWordUnderCursor s .forward)
| Key.char '#' =>
pure $ clearInput (EditorState.searchWordUnderCursor s .backward)
| Key.char 'w' =>
match s.inputState.previousKey with
| some 'c' => pure $ clearInput (s.changeWord)
| _ => handleMotion s EditorState.moveWordForward
| Key.char 'b' => handleMotion s EditorState.moveWordBackward
| Key.char 'e' => handleMotion s EditorState.moveWordEnd
| Key.char 'x' => pure $ clearInput (EditorState.deleteCharAfterCursor s)
| Key.char 'n' => pure $ clearInput (ViE.findNextMatch s)
| Key.char 'N' =>
let overrideDir :=
match s.searchState with
| some st =>
if st.direction == .forward then some SearchDirection.backward else some SearchDirection.forward
| none => none
pure $ clearInput (ViE.findNextMatch s overrideDir)
| Key.char 'p' =>
let buf := s.getActiveBuffer
let isExplorer := s.explorers.any (fun (id, _) => id == buf.id)
if isExplorer then
ViE.Feature.toggleExplorerPreview s
else
pure $ clearInput (EditorState.pasteBelow s)
| Key.char 'P' => pure $ clearInput (EditorState.pasteAbove s)
| Key.char 'y' =>
match s.inputState.previousKey with
| some 'y' => pure $ clearInput (EditorState.yankCurrentLine s)
| _ => pure { s with inputState := { s.inputState with previousKey := some 'y' } }
| Key.char '|' =>
let n := s.getCount
pure $ clearInput (EditorState.jumpToColumn s ⟨n⟩)
| Key.char 'c' =>
match s.inputState.previousKey with
| some 'c' =>
-- cc: delete line and enter insert mode
-- For now, approximate with delete line. Ideally should preserve line as empty.
pure $ (s.deleteCurrentLine).setMode Mode.insert
| _ => pure { s with inputState := { s.inputState with previousKey := some 'c' } }
| Key.char 'd' =>
match s.inputState.previousKey with
| some 'd' => pure $ s.deleteCurrentLine
| _ => pure { s with inputState := { s.inputState with previousKey := some 'd' } }
| Key.char 'u' => pure $ s.undo
| Key.ctrl 'd' =>
let n := rawCount s
pure $ clearInput (EditorState.scrollHalfPageDown s n)
| Key.ctrl 'u' =>
let n := rawCount s
pure $ clearInput (EditorState.scrollHalfPageUp s n)
| Key.ctrl 'f' =>
let n := rawCount s
pure $ clearInput (EditorState.scrollPageDown s n)
| Key.ctrl 'b' =>
let n := rawCount s
pure $ clearInput (EditorState.scrollPageUp s n)
| Key.ctrl 'e' =>
let n := rawCount s
pure $ clearInput (EditorState.scrollWindowDown s n)
| Key.ctrl 'y' =>
let n := rawCount s
pure $ clearInput (EditorState.scrollWindowUp s n)
| Key.ctrl 'o' =>
pure $ clearInput (EditorState.jumpBackInList s)
| Key.char '\t' =>
pure $ clearInput (EditorState.jumpForwardInList s)
| Key.ctrl 'r' => pure $ s.redo
| Key.ctrl 'l' => pure { s with dirty := true, message := "redraw" }
| Key.ctrl 'w' =>
pure { s with inputState := { s.inputState with previousKey := some '\x17' } }
| Key.char c =>
if s.inputState.previousKey == some '\x17' then
-- Handle window command
let s' := { s with inputState := { s.inputState with previousKey := none } }
match c with
| 'h' => pure $ switchWindow s' .left
| 'j' => pure $ switchWindow s' .down
| 'k' => pure $ switchWindow s' .up
| 'l' => pure $ switchWindow s' .right
| 's' => pure $ splitWindow s' true
| 'v' => pure $ splitWindow s' false
| 'q' => pure $ closeActiveWindow s'
| 'w' => pure $ cycleWindow s'
| 'c' => pure $ cycleWindow s'
| _ => pure s'
else if s.inputState.previousKey == some 'g' then
-- Handle g commands
let s' := { s with inputState := { s.inputState with previousKey := none } }
match c with
| 't' =>
let size := s.workgroups.size
if size == 0 then
pure s'
else
let next := (s.currentGroup + 1) % size
let s'' := s.switchToWorkgroup next
pure { s'' with message := s!"Switched to workgroup {next}" }
| 'T' =>
let size := s.workgroups.size
if size == 0 then
pure s'
else
let prev := if s.currentGroup == 0 then size - 1 else s.currentGroup - 1
let s'' := s.switchToWorkgroup prev
pure { s'' with message := s!"Switched to workgroup {prev}" }
| 'g' =>
-- gg implementation
let n := s.getCount
let line := if n > 0 then n else 1
pure $ clearInput ((EditorState.jumpToLine s line).moveToLineStart)
| _ => pure s'
else
match c with
| 'g' =>
pure { s with inputState := { s.inputState with previousKey := some 'g' } }
| 'G' =>
let line := match s.inputState.countBuffer.toNat? with
| some n => n
| none => s.getActiveBuffer.lineCount
pure $ clearInput ((EditorState.jumpToLine s line).moveToLineStart)
| _ =>
if c.isDigit then
pure { s with inputState := { s.inputState with countBuffer := s.inputState.countBuffer.push c } }
else
pure { s with inputState := { s.inputState with countBuffer := "", previousKey := none } }
| Key.alt c =>
let s' := { s with inputState := { s.inputState with previousKey := none } }
let activeIsFloating := s'.getCurrentWorkspace.isFloatingWindow s'.getCurrentWorkspace.activeWindowId
match c with
| 'h' => pure $ switchWindow s' .left
| 'j' => pure $ switchWindow s' .down
| 'k' => pure $ switchWindow s' .up
| 'l' => pure $ switchWindow s' .right
| 'H' =>
if activeIsFloating then
pure $ nudgeActiveFloatingWindow s' .left
else
pure $ resizeWindow s' .left 0.05
| 'J' =>
if activeIsFloating then
pure $ nudgeActiveFloatingWindow s' .down
else
pure $ resizeWindow s' .down 0.05
| 'K' =>
if activeIsFloating then
pure $ nudgeActiveFloatingWindow s' .up
else
pure $ resizeWindow s' .up 0.05
| 'L' =>
if activeIsFloating then
pure $ nudgeActiveFloatingWindow s' .right
else
pure $ resizeWindow s' .right 0.05
| _ =>
if c.isDigit then
match c.toString.toNat? with
| some idx =>
if idx < s.workgroups.size then
let s'' := s.switchToWorkgroup idx
pure { s'' with message := s!"Switched to workgroup {idx}" }
else
pure s'
| none => pure s'
else
pure s'
| _ => pure { s with inputState := { s.inputState with countBuffer := "", previousKey := none } },
insert := fun s k =>
if s.floatingOverlay.isSome then
if s.floatingInputCommand.isSome then
let submitFloatingInput (state : EditorState) : IO EditorState := do
match state.floatingInputCommand, state.floatingOverlay with
| some cmdPrefix, some ov =>
let input := (ov.lines[0]?.getD "").trimAscii.toString
let cmd := s!"{cmdPrefix}{input}"
let pre := {
state with
floatingOverlay := none
floatingInputCommand := none
mode := .normal
inputState := {
state.inputState with
commandBuffer := cmd
countBuffer := ""
previousKey := none
pendingKeys := ""
}
}
let out ← ViE.Command.executeCommand commands pre
return {
out with
mode := .normal
inputState := {
out.inputState with
commandBuffer := ""
countBuffer := ""
previousKey := none
pendingKeys := ""
}
dirty := true
}
| _, _ =>
return {
state with
floatingOverlay := none
floatingInputCommand := none
mode := .normal
dirty := true
}
match k with
| Key.esc => pure { s with floatingOverlay := none, floatingInputCommand := none, mode := .normal, dirty := true, message := "" }
| Key.backspace => pure (overlayBackspace s)
| Key.char c => pure (overlayInsertChar s c)
| Key.enter => submitFloatingInput s
| Key.left => pure (overlayMoveLeft s)
| Key.right => pure (overlayMoveRight s)
| Key.up => pure (overlayMoveUp s)
| Key.down => pure (overlayMoveDown s)
| _ => pure s
else
match k with
| Key.esc => pure { s with mode := .normal, dirty := true }
| Key.backspace => pure (overlayBackspace s)
| Key.char c => pure (overlayInsertChar s c)
| Key.enter => pure (overlayInsertNewline s)
| Key.left => pure (overlayMoveLeft s)
| Key.right => pure (overlayMoveRight s)
| Key.up => pure (overlayMoveUp s)
| Key.down => pure (overlayMoveDown s)
| _ => pure s
else
do
let handled ← ViE.Language.Key.handleInsertKey? s k
match handled with
| some s' => pure s'
| none =>
match k with
| Key.esc => do
let s' ← ViE.Buffer.EditQueue.awaitActiveInsertOps s
pure $ (s'.commitEdit.moveCursorLeft).setMode Mode.normal
| Key.backspace => do
let s' ← ViE.Buffer.EditQueue.enqueueBackspace s
ViE.Buffer.EditQueue.awaitActiveInsertOps s'
| Key.char c => ViE.Buffer.EditQueue.enqueueInsertChar s c
| Key.enter => do
let s' ← ViE.Buffer.EditQueue.enqueueInsertNewline s
let s'' ← ViE.Buffer.EditQueue.awaitActiveInsertOps s'
pure { s'' with completionPopup := none }
| _ => pure s,
command := handleCommandInput commands,
visual := fun s k => match k with
| Key.esc => pure $ EditorState.exitVisualMode s
| Key.char 'v' => pure $ EditorState.exitVisualMode s
| Key.char 'V' =>
if s.mode == .visualLine then
pure $ EditorState.exitVisualMode s
else
pure $ EditorState.startVisualLineMode s
| Key.char 'h' => pure $ clearInput (EditorState.moveCursorLeftN s s.getCount)
| Key.char 'j' => pure $ clearInput (EditorState.moveCursorDownN s s.getCount)
| Key.char 'k' => pure $ clearInput (EditorState.moveCursorUpN s s.getCount)
| Key.char 'l' => pure $ clearInput (EditorState.moveCursorRightN s s.getCount)
| Key.char 'w' => pure $ clearInput (EditorState.moveWordForward s)
| Key.char 'b' => pure $ clearInput (EditorState.moveWordBackward s)
| Key.char 'e' => pure $ clearInput (EditorState.moveWordEnd s)
| Key.char '0' => pure $ clearInput (EditorState.moveToLineStart s)
| Key.char '$' => pure $ clearInput (EditorState.moveToLineEnd s)
| Key.char 'G' =>
let line := match s.inputState.countBuffer.toNat? with
| some n => n
| none => s.getActiveBuffer.lineCount
pure $ clearInput ((EditorState.jumpToLine s line).moveToLineStart)
| Key.char 'd' => pure $ EditorState.deleteSelection s
| Key.char 'x' => pure $ EditorState.deleteSelection s
| Key.char 'y' => pure $ EditorState.yankSelection s
| Key.char c =>
match c with
| 'g' =>
match s.inputState.previousKey with
| some 'g' =>
let n := s.getCount
let line := if n > 0 then n else 1
pure $ clearInput ((EditorState.jumpToLine s line).moveToLineStart)
| _ => pure { s with inputState := { s.inputState with previousKey := some 'g' } }
| _ =>
if c.isDigit then
pure { s with inputState := { s.inputState with countBuffer := s.inputState.countBuffer.push c } }
else
pure s
| _ => pure s,
visualBlock := fun s k => match k with
| Key.esc => pure $ EditorState.exitVisualMode s
| Key.char 'v' => pure $ EditorState.exitVisualMode s
| Key.char 'V' => pure $ EditorState.exitVisualMode s
| Key.char 'h' => pure $ clearInput (EditorState.moveCursorLeftN s s.getCount)
| Key.char 'j' => pure $ clearInput (EditorState.moveCursorDownN s s.getCount)
| Key.char 'k' => pure $ clearInput (EditorState.moveCursorUpN s s.getCount)
| Key.char 'l' => pure $ clearInput (EditorState.moveCursorRightN s s.getCount)
| Key.char '0' => pure $ clearInput (EditorState.moveToLineStart s)
| Key.char '$' => pure $ clearInput (EditorState.moveToLineEnd s)
| Key.char 'G' =>
let line := match s.inputState.countBuffer.toNat? with
| some n => n
| none => s.getActiveBuffer.lineCount
pure $ clearInput ((EditorState.jumpToLine s line).moveToLineStart)
| Key.char 'd' => pure $ EditorState.deleteSelection s
| Key.char 'x' => pure $ EditorState.deleteSelection s
| Key.char 'y' => pure $ EditorState.yankSelection s
| Key.char c =>
match c with
| 'g' =>
match s.inputState.previousKey with
| some 'g' =>
let n := s.getCount
let line := if n > 0 then n else 1
pure $ clearInput ((EditorState.jumpToLine s line).moveToLineStart)
| _ => pure { s with inputState := { s.inputState with previousKey := some 'g' } }
| _ =>
if c.isDigit then
pure { s with inputState := { s.inputState with countBuffer := s.inputState.countBuffer.push c } }
else
pure s
| _ => pure s
}
end ViE.Key