import ViE.State.Movement
import ViE.State.Edit
import ViE.Key.Map
import ViE.Types
import ViE.Buffer.Content
import ViE.Config
import ViE.Command.Impl
import Test.Utils
namespace Test.Keybinds
open ViE
open Test.Utils
def makeTestConfig : Config := {
settings := ViE.defaultConfig
commands := ViE.Command.defaultCommandMap
bindings := ViE.Key.makeKeyMap ViE.Command.defaultCommandMap
}
def keys (s : String) : List Key :=
s.toList.map fun c =>
if c == '\n' then Key.enter else Key.char c
def runKeys (s : EditorState) (ks : List Key) : IO EditorState := do
let config := makeTestConfig
ks.foldlM (fun s k => ViE.update config s k) s
def findEntryIndex (entries : List FileEntry) (name : String) : Option Nat :=
let rec loop (rest : List FileEntry) (idx : Nat) : Option Nat :=
match rest with
| [] => none
| e :: tail =>
if e.name == name then
some idx
else
loop tail (idx + 1)
loop entries 0
def findEntryIndexByPath (entries : List FileEntry) (path : String) : Option Nat :=
let rec loop (rest : List FileEntry) (idx : Nat) : Option Nat :=
match rest with
| [] => none
| e :: tail =>
if e.path == path then
some idx
else
loop tail (idx + 1)
loop entries 0
def testMotions : IO Unit := do
IO.println " Testing Motions..."
let s0 := ViE.initialState
let s1 ← runKeys s0 ([Key.char 'i'] ++ keys "line1\nline2\nline3" ++ [Key.esc] ++ keys "gg0")
-- "line1\nline2\nline3" cursor at (0,0)
-- l, h
let s_l ← runKeys s1 [Key.char 'l']
assertCursor "l moves right" s_l 0 1
let s_lh ← runKeys s_l [Key.char 'h']
assertCursor "h moves left" s_lh 0 0
-- j, k
let s_j ← runKeys s1 [Key.char 'j']
assertCursor "j moves down" s_j 1 0
let s_jk ← runKeys s_j [Key.char 'k']
assertCursor "k moves up" s_jk 0 0
-- 0, $
let s_end ← runKeys s1 [Key.char '$']
assertCursor "$ moves to end" s_end 0 4 -- '1' is at col 4
let s_start ← runKeys s_end [Key.char '0']
assertCursor "0 moves to start" s_start 0 0
-- w, b, e
let s_text ← runKeys s0 ([Key.char 'i'] ++ keys "word1 word2" ++ [Key.esc] ++ [Key.char '0'])
let s_w ← runKeys s_text [Key.char 'w']
assertCursor "w moves to next word" s_w 0 6
let s_e ← runKeys s_text [Key.char 'e']
assertCursor "e moves to end of word" s_e 0 4
let s_we ← runKeys s_w [Key.char 'e']
assertCursor "we moves to end of next word" s_we 0 10
let s_web ← runKeys s_we [Key.char 'b']
assertCursor "b moves to start of word" s_web 0 6
-- gg, G
let s_G ← runKeys s1 [Key.char 'G']
assertCursor "G moves to last line" s_G 2 0
let s_gg ← runKeys s_G [Key.char 'g', Key.char 'g']
assertCursor "gg moves to first line" s_gg 0 0
-- | (jump to column)
let s_pipe ← runKeys s1 [Key.char '3', Key.char '|']
assertCursor "| jumps to column" s_pipe 0 2 -- 1-indexed count 3 -> col 2
def testEditing : IO Unit := do
IO.println " Testing Editing..."
let s0 := ViE.initialState
-- i, a, A
let s_i ← runKeys s0 [Key.char 'i', Key.char 'x', Key.esc]
assertBuffer "i inserts" s_i "x"
let s_a ← runKeys s_i [Key.char 'a', Key.char 'y', Key.esc]
assertBuffer "a appends" s_a "xy"
let s_A ← runKeys s_a [Key.char '0', Key.char 'A', Key.char 'z', Key.esc]
assertBuffer "A appends at end" s_A "xyz"
let s_tab_insert ← runKeys s0 [Key.char 'i', Key.char '\t']
assertCursor "Tab advances cursor to next tab stop in insert mode" s_tab_insert 0 4
let s_tab_backspace ← runKeys s0 [Key.char 'i', Key.char '\t', Key.backspace]
assertBuffer "Tab backspace deletes tab in insert mode" s_tab_backspace ""
assertCursor "Tab backspace restores cursor column" s_tab_backspace 0 0
let s_tab ← runKeys s0 [Key.char 'i', Key.char '\t', Key.esc]
assertBuffer "Tab inserts in insert mode" s_tab "\t"
assertCursor "Esc after tab returns to tab char in normal mode" s_tab 0 0
let s0_tab8 := { s0 with config := { s0.config with tabStop := 8 } }
let s_tab8 ← runKeys s0_tab8 [Key.char 'i', Key.char '\t']
assertCursor "Tab follows custom tabStop in insert mode" s_tab8 0 8
-- o, O
let s_o ← runKeys s_i [Key.char 'o', Key.char 'y', Key.esc]
assertBuffer "o inserts line below" s_o "x\ny"
let s_O ← runKeys s_o [Key.char 'k', Key.char 'O', Key.char 'z', Key.esc]
assertBuffer "O inserts line above" s_O "z\nx\ny"
-- x
let s_x ← runKeys s_i [Key.char 'x']
assertBuffer "x deletes char" s_x ""
match s_x.clipboard with
| some reg =>
assertEqual "x register kind" RegisterKind.charwise reg.kind
assertEqual "x register text" "x" reg.text
| none => assertEqual "x register set" true false
def testOperators : IO Unit := do
IO.println " Testing Operators..."
let s0 := ViE.initialState
let s1 ← runKeys s0 ([Key.char 'i'] ++ keys "hello world" ++ [Key.esc] ++ [Key.char '0'])
-- dw
let s_dw ← runKeys s1 [Key.char 'd', Key.char 'w']
assertBuffer "dw deletes word" s_dw "world"
match s_dw.clipboard with
| some reg =>
assertEqual "dw register kind" RegisterKind.charwise reg.kind
assertEqual "dw register text" "hello " reg.text
| none => assertEqual "dw register set" true false
-- cw
let s_cw ← runKeys s1 [Key.char 'c', Key.char 'w', Key.char 'h', Key.char 'i', Key.esc]
assertBuffer "cw changes word" s_cw "hi world"
-- dd
let s_lines ← runKeys s0 ([Key.char 'i'] ++ keys "line1\nline2\nline3" ++ [Key.esc] ++ keys "ggj")
let s_dd ← runKeys s_lines [Key.char 'd', Key.char 'd']
assertBuffer "dd deletes current line" s_dd "line1\nline3"
match s_dd.clipboard with
| some reg =>
assertEqual "dd register kind" RegisterKind.linewise reg.kind
assertEqual "dd register text" "line2\n" reg.text
| none => assertEqual "dd register set" true false
-- yy, p, P
let s_yy ← runKeys s_lines [Key.char 'g', Key.char 'g', Key.char 'y', Key.char 'y']
let s_p ← runKeys s_yy [Key.char 'G', Key.char 'p']
assertBuffer "p pastes below" s_p "line1\nline2\nline3\nline1\n"
let s_P ← runKeys s_yy [Key.char 'g', Key.char 'g', Key.char 'P']
assertBuffer "P pastes above" s_P "line1\nline1\nline2\nline3"
let s_indent ← runKeys s0 ([Key.char 'i'] ++ keys " indented\nx" ++ [Key.esc] ++ [Key.char 'g', Key.char 'g', Key.char 'y', Key.char 'y', Key.char 'j', Key.char 'p'])
assertCursor "linewise paste moves to first non-blank" s_indent 2 2
-- paste should not overwrite register
let s_reg0 ← runKeys s0 ([Key.char 'i'] ++ keys "one\ntwo" ++ [Key.esc] ++ [Key.char 'g', Key.char 'g', Key.char 'y', Key.char 'y'])
let regBefore := s_reg0.clipboard
let s_regP ← runKeys s_reg0 [Key.char 'j', Key.char 'p']
match (regBefore, s_regP.clipboard) with
| (some before, some after) =>
assertEqual "paste keeps register kind" before.kind after.kind
assertEqual "paste keeps register text" before.text after.text
| _ => assertEqual "paste keeps register" true false
-- charwise y/p from normal mode
let s_char0 ← runKeys s0 ([Key.char 'i'] ++ keys "hello" ++ [Key.esc] ++ [Key.char '0'])
let s_charY ← runKeys s_char0 [Key.char 'v', Key.char 'l', Key.char 'y']
let s_charP ← runKeys s_charY [Key.char '$', Key.char 'p']
assertBuffer "charwise paste in normal mode" s_charP "hellohe"
assertCursor "charwise paste cursor at end" s_charP 0 6
let s_charP2 ← runKeys s_charY [Key.char '0', Key.char 'P']
assertBuffer "charwise P pastes before cursor" s_charP2 "hehello"
assertCursor "charwise P cursor at end" s_charP2 0 1
def testVisual : IO Unit := do
IO.println " Testing Visual Mode..."
let s0 := ViE.initialState
let s1 ← runKeys s0 ([Key.char 'i'] ++ keys "highlight me" ++ [Key.esc] ++ [Key.char '0'])
-- v, d
let s_v ← runKeys s1 [Key.char 'v', Key.char 'l', Key.char 'd']
assertBuffer "visual d deletes selection" s_v "ghlight me"
match s_v.clipboard with
| some reg =>
assertEqual "visual d register kind" RegisterKind.charwise reg.kind
assertEqual "visual d register text" "hi" reg.text
| none => assertEqual "visual d register set" true false
-- v, y, p
let s_vy ← runKeys s1 [Key.char 'v', Key.char 'e', Key.char 'y']
let s_vyp ← runKeys s_vy [Key.char '$', Key.char 'p']
assertBuffer "visual y yanks selection" s_vyp "highlight mehighlight"
let s_char0 ← runKeys s0 ([Key.char 'i'] ++ keys "abc" ++ [Key.esc] ++ [Key.char '0'])
let s_charY ← runKeys s_char0 [Key.char 'l', Key.char 'v', Key.char 'y']
let s_charP ← runKeys s_charY [Key.char '$', Key.char 'p']
assertBuffer "charwise paste appends" s_charP "abcb"
assertCursor "charwise paste cursor at end" s_charP 0 3
let s_charP2 ← runKeys s_charY [Key.char '0', Key.char 'P']
assertBuffer "charwise P inserts before" s_charP2 "babc"
assertCursor "charwise P cursor at end" s_charP2 0 0
-- visual block yank/paste cursor position
let s_blk0 ← runKeys s0 ([Key.char 'i'] ++ keys "abcd\nefgh" ++ [Key.esc] ++ [Key.char 'g', Key.char 'g', Key.char '0'])
let s_blkY ← runKeys s_blk0 [Key.char 'l', Key.char 'V', Key.char 'l', Key.char 'j', Key.char 'y']
let s_blkP ← runKeys s_blkY [Key.char 'g', Key.char 'g', Key.char '0', Key.char 'p']
assertBuffer "visual block y/p inserts block" s_blkP "abcbcd\nefgfgh"
assertCursor "visual block paste cursor at block start" s_blkP 0 1
let s_blkP2 ← runKeys s_blkY [Key.char 'g', Key.char 'g', Key.char '0', Key.char 'P']
assertBuffer "visual block P inserts block" s_blkP2 "bcabcd\nfgefgh"
assertCursor "visual block P cursor at block start" s_blkP2 0 0
let s_blkD0 ← runKeys s0 ([Key.char 'i'] ++ keys "abcd\nefgh" ++ [Key.esc] ++ [Key.char 'g', Key.char 'g', Key.char '0'])
let s_blkD ← runKeys s_blkD0 [Key.char 'l', Key.char 'V', Key.char 'l', Key.char 'j', Key.char 'd']
assertBuffer "visual block d deletes block" s_blkD "ad\neh"
match s_blkD.clipboard with
| some reg =>
assertEqual "visual block d register kind" RegisterKind.blockwise reg.kind
assertEqual "visual block d register text" "bc\nfg" reg.text
| none => assertEqual "visual block d register set" true false
-- linewise yank/paste (via yy + P)
let s_line0 ← runKeys s0 ([Key.char 'i'] ++ keys "aa\nbb\ncc" ++ [Key.esc] ++ [Key.char 'g', Key.char 'g'])
let s_lineY ← runKeys s_line0 [Key.char 'y', Key.char 'y']
let s_lineP ← runKeys s_lineY [Key.char 'j', Key.char 'P']
assertBuffer "linewise paste above keeps lines" s_lineP "aa\naa\nbb\ncc"
let s_lineP2 ← runKeys s_lineY [Key.char 'j', Key.char 'p']
assertBuffer "linewise paste below keeps lines" s_lineP2 "aa\nbb\naa\ncc"
def testCounted : IO Unit := do
IO.println " Testing Counted Actions..."
let s0 := ViE.initialState
let s1 ← runKeys s0 ([Key.char 'i'] ++ keys "a b c d e" ++ [Key.esc] ++ [Key.char '0'])
-- 3w
let s_3w ← runKeys s1 [Key.char '3', Key.char 'w']
assertCursor "3w moves 3 words" s_3w 0 6
-- 2j
let s_lines ← runKeys s0 ([Key.char 'i'] ++ keys "1\n2\n3\n4" ++ [Key.esc] ++ [Key.char 'g', Key.char 'g'])
let s_2j ← runKeys s_lines [Key.char '2', Key.char 'j']
assertCursor "2j moves 2 lines down" s_2j 2 0
def testVimCompatMotions : IO Unit := do
IO.println " Testing Vim Compatibility Motions..."
let s0 := { ViE.initialState with windowHeight := 12, windowWidth := 80 }
let lines := String.intercalate "\n" ((List.range 30).map (fun i => s!" line{i}")) ++ "\n"
let s1 ← runKeys s0 ([Key.char 'i'] ++ keys lines ++ [Key.esc] ++ keys "gg0")
let sCd ← runKeys s1 [Key.ctrl 'd']
assertCursor "Ctrl-d scrolls half page down" sCd 5 0
let sCu ← runKeys sCd [Key.ctrl 'u']
assertCursor "Ctrl-u scrolls half page up" sCu 0 0
let sCf ← runKeys s1 [Key.ctrl 'f']
assertCursor "Ctrl-f scrolls one page down" sCf 10 0
let sCb ← runKeys sCf [Key.ctrl 'b']
assertCursor "Ctrl-b scrolls one page up" sCb 0 0
let sCe ← runKeys s1 [Key.ctrl 'e']
let (ceRow, _) := sCe.getScroll
assertEqual "Ctrl-e scrolls window down by one line" 1 ceRow.val
let sCy ← runKeys sCe [Key.ctrl 'y']
let (cyRow, _) := sCy.getScroll
assertEqual "Ctrl-y scrolls window up by one line" 0 cyRow.val
let sH ← runKeys sCf [Key.char 'H']
assertCursor "H moves to top line of screen" sH 10 2
let sM ← runKeys sCf [Key.char 'M']
assertCursor "M moves to middle line of screen" sM 15 2
let sL ← runKeys sCf [Key.char 'L']
assertCursor "L moves to bottom line of screen" sL 19 2
let sF0 ← runKeys s0 ([Key.char 'i'] ++ keys "abcaXcaYca" ++ [Key.esc] ++ [Key.char '0'])
let sF1 ← runKeys sF0 [Key.char 'f', Key.char 'a']
assertCursor "fa finds next character" sF1 0 3
let sF2 ← runKeys sF1 [Key.char ';']
assertCursor "; repeats last f motion" sF2 0 6
let sF3 ← runKeys sF2 [Key.char ',']
assertCursor ", reverses last f motion" sF3 0 3
let sT1 ← runKeys sF3 [Key.char 't', Key.char 'a']
assertCursor "ta moves before target" sT1 0 5
let sT2 ← runKeys sT1 [Key.char ';']
assertCursor "; repeats last t motion" sT2 0 8
let sPct0 ← runKeys s0 ([Key.char 'i'] ++ keys "(a[b]c)" ++ [Key.esc] ++ [Key.char '0'])
let sPct1 ← runKeys sPct0 [Key.char '%']
assertCursor "% jumps to matching bracket" sPct1 0 6
let sPct2 ← runKeys sPct1 [Key.ctrl 'o']
assertCursor "Ctrl-o jumps back in jump list" sPct2 0 0
let sPct3 ← runKeys sPct2 [Key.char '\t']
assertCursor "Ctrl-i (Tab) jumps forward in jump list" sPct3 0 6
let sStar0 := ViE.initialState
let sStar1 ← runKeys sStar0 ([Key.char 'i'] ++ keys "foo bar\nfoo baz\nbar foo\n" ++ [Key.esc] ++ keys "gg0")
let sStar2 ← runKeys sStar1 [Key.char '*']
assertCursor "* searches next word under cursor" sStar2 1 0
let sStar3 ← runKeys sStar2 [Key.char '#']
assertCursor "# searches previous word under cursor" sStar3 0 0
def testWorkgroupSwitch : IO Unit := do
IO.println " Testing Workgroup Switching..."
let s0 := ViE.initialState
let s1 ← runKeys s0 [Key.alt '3']
assertEqual "Alt-3 switches workgroup" 3 s1.currentGroup
let s2 ← runKeys s1 [Key.alt '0']
assertEqual "Alt-0 switches workgroup" 0 s2.currentGroup
def testSearch : IO Unit := do
IO.println " Testing Search..."
let s0 := ViE.initialState
let s1 ← runKeys s0 ([Key.char 'i'] ++ keys "hello world\nhello again\n" ++ [Key.esc])
let s2 ← runKeys s1 ([Key.char '/'] ++ keys "hello" ++ [Key.enter])
assertCursor "/hello finds first match" s2 0 0
let s2Prompt ← runKeys s2 [Key.char '/']
let promptCleared :=
match s2Prompt.searchState with
| none => true
| some _ => false
assertEqual "Starting new search prompt clears old search highlight state" true promptCleared
let s2e ← runKeys s2 [Key.enter]
assertCursor "Enter after search jumps to next match" s2e 1 0
assertBuffer "Enter after search does not insert newline" s2e "hello world\nhello again\n"
let s3 ← runKeys s2 [Key.char 'n']
assertCursor "n finds next match" s3 1 0
let s4 ← runKeys s3 [Key.char 'N']
assertCursor "N finds previous match" s4 0 0
let s5 ← runKeys s4 ([Key.char '?'] ++ keys "world" ++ [Key.enter])
assertCursor "?world searches backward" s5 0 6
def testCommandSubstitute : IO Unit := do
IO.println " Testing Command Substitute..."
let s0 := ViE.initialState
let s1 ← runKeys s0 ([Key.char 'i'] ++ keys "foo bar\nfoo baz\nbar foo\n" ++ [Key.esc] ++ keys "gg0")
let s2 ← runKeys s1 ([Key.char ':'] ++ keys "s/foo/xxx/" ++ [Key.enter])
assertBuffer ":s replaces first match on current line" s2 "xxx bar\nfoo baz\nbar foo\n"
let s3 := ViE.initialState
let s4 ← runKeys s3 ([Key.char 'i'] ++ keys "foo foo\nfoo foo\n" ++ [Key.esc] ++ keys "gg0")
let s5 ← runKeys s4 ([Key.char ':'] ++ keys "%s/foo/yyy/" ++ [Key.enter])
assertBuffer ":%s replaces first match per line" s5 "yyy foo\nyyy foo\n"
let s6 := ViE.initialState
let s7 ← runKeys s6 ([Key.char 'i'] ++ keys "foo bar\nfoo baz\nbar foo\n" ++ [Key.esc] ++ keys "gg0")
let s8 ← runKeys s7 ([Key.char ':'] ++ keys "%s/foo/yyy/g" ++ [Key.enter])
assertBuffer ":%s with g replaces all matches" s8 "yyy bar\nyyy baz\nbar yyy\n"
def testCommandGlobal : IO Unit := do
IO.println " Testing Command Global..."
let s0 := ViE.initialState
let s1 ← runKeys s0 ([Key.char 'i'] ++ keys "a\nfoo1\nb\nfoo2\n" ++ [Key.esc] ++ keys "gg0")
let s2 ← runKeys s1 ([Key.char ':'] ++ keys "g/foo/ d" ++ [Key.enter])
assertBuffer ":g/pat/ d deletes matching lines" s2 "a\nb\n"
let s3 := ViE.initialState
let s4 ← runKeys s3 ([Key.char 'i'] ++ keys "foo foo\nbar foo\nfoo bar\n" ++ [Key.esc] ++ keys "gg0")
let s5 ← runKeys s4 ([Key.char ':'] ++ keys "g/foo/ s/foo/xxx/" ++ [Key.enter])
assertBuffer ":g/pat/ s replaces first match per line" s5 "xxx foo\nbar xxx\nxxx bar\n"
let s6 := ViE.initialState
let s7 ← runKeys s6 ([Key.char 'i'] ++ keys "keep1\nfoo\nkeep2\n" ++ [Key.esc] ++ keys "gg0")
let s8 ← runKeys s7 ([Key.char ':'] ++ keys "v/foo/ d" ++ [Key.enter])
assertBuffer ":v/pat/ d deletes non-matching lines" s8 "foo\n"
let s9 := ViE.initialState
let s10 ← runKeys s9 ([Key.char 'i'] ++ keys "a\nfoo1\nb\nfoo2\nc" ++ [Key.esc] ++ [Key.char 'g', Key.char 'g', Key.char 'j', Key.char 'j', Key.char 'j', Key.char '0'])
assertCursor ":g/pat/ d precondition cursor on deletable line" s10 3 0
let s11 ← runKeys s10 ([Key.char ':'] ++ keys "g/foo/ d" ++ [Key.enter])
assertBuffer ":g/pat/ d keeps expected text when cursor line is deleted" s11 "a\nb\nc"
assertCursor ":g/pat/ d clamps cursor when current line is deleted" s11 2 0
def testCursorDriftCustomTabStop : IO Unit := do
IO.println " Testing Cursor Drift (custom tabStop)..."
let cfg8 := { ViE.defaultConfig with tabStop := 8 }
let s0 := { ViE.initialState with config := cfg8 }
let s1 ← runKeys s0 ([Key.char 'i', Key.char '\t'] ++ keys "foo\n" ++ [Key.esc])
let s2 ← runKeys s1 [Key.char 'g', Key.char 'g', Key.char '0', Key.char 'l']
assertCursor "custom tabStop: l moves to first char after tab" s2 0 8
let s3 ← runKeys s2 ([Key.char ':'] ++ keys "s/foo/bar/" ++ [Key.enter])
assertBuffer "custom tabStop: :s updates current line" s3 "\tbar\n"
let s4 ← runKeys s3 [Key.char 'u']
assertBuffer "custom tabStop: undo restores text after :s" s4 "\tfoo\n"
assertCursor "custom tabStop: undo restores cursor col after :s" s4 0 8
let s5 ← runKeys s4 [Key.ctrl 'r']
assertBuffer "custom tabStop: redo reapplies :s" s5 "\tbar\n"
assertCursor "custom tabStop: redo restores cursor col after :s" s5 0 8
let s6 ← runKeys s2 [Key.char 'v', Key.char 'l', Key.char 'd']
assertBuffer "custom tabStop: visual delete uses configured offsets" s6 "\to\n"
assertCursor "custom tabStop: visual delete keeps cursor aligned" s6 0 8
def testCommandBloom : IO Unit := do
IO.println " Testing Command Bloom..."
let s0 := ViE.initialState
let s1 ← runKeys s0 ([Key.char 'i'] ++ keys "hello bloom\nbloom hello\n" ++ [Key.esc] ++ keys "gg0")
let s2 ← runKeys s1 ([Key.char ':'] ++ keys "bloom /bloom" ++ [Key.enter])
assertCursor ":bloom moves to first match" s2 0 6
let s3 ← runKeys s2 ([Key.char ':'] ++ keys "bloom /nomatch" ++ [Key.enter])
assertEqual ":bloom not found message" "Pattern not found: nomatch" s3.message
def testUiCommands : IO Unit := do
IO.println " Testing UI Commands..."
let s0 := ViE.initialState
let s1 ← runKeys s0 ([Key.char ':'] ++ keys "float hello" ++ [Key.enter])
assertEqual ":float shows overlay" true s1.floatingOverlay.isSome
let s2 ← runKeys s1 [Key.esc]
assertEqual "Esc closes overlay" false s2.floatingOverlay.isSome
let s3 ← runKeys s0 ([Key.char ':'] ++ keys "float alpha\\nbeta" ++ [Key.enter])
match s3.floatingOverlay with
| some overlay =>
assertEqual ":float parses newline escape (line count)" 2 overlay.lines.size
assertEqual ":float parses newline escape (line 1)" "alpha" overlay.lines[0]!
assertEqual ":float parses newline escape (line 2)" "beta" overlay.lines[1]!
| none =>
assertEqual ":float newline overlay exists" true false
let s4 ← runKeys s3 ([Key.char ':'] ++ keys "nofloat" ++ [Key.enter])
assertEqual ":nofloat clears overlay" false s4.floatingOverlay.isSome
let s5 ← runKeys s0 ([Key.char ':'] ++ keys "redraw" ++ [Key.enter])
assertEqual ":redraw sets message" "redraw" s5.message
assertEqual ":redraw marks dirty" true s5.dirty
let s6 ← runKeys s0 ([Key.char ':'] ++ keys "redraw!" ++ [Key.enter])
assertEqual ":redraw! alias works" "redraw" s6.message
let s7 ← runKeys s0 [Key.ctrl 'l']
assertEqual "Ctrl-l redraw marks dirty" true s7.dirty
let s8 ← runKeys s0 ([Key.char ':'] ++ keys "float --title Note --width 32 hello world" ++ [Key.enter])
match s8.floatingOverlay with
| some overlay =>
assertEqual ":float --title sets title" "Note" overlay.title
assertEqual ":float --width sets width" 32 overlay.maxWidth
assertEqual ":float with options keeps text" "hello world" overlay.lines[0]!
| none =>
assertEqual ":float with options shows overlay" true false
let s9 ← runKeys s0 ([Key.char ':'] ++ keys "float --title=Panel --width=28 hi" ++ [Key.enter])
match s9.floatingOverlay with
| some overlay =>
assertEqual ":float --title= sets title" "Panel" overlay.title
assertEqual ":float --width= sets width" 28 overlay.maxWidth
| none =>
assertEqual ":float with inline options shows overlay" true false
let s10 ← runKeys s0 ([Key.char ':'] ++ keys "float --width nope hi" ++ [Key.enter])
assertEqual ":float invalid width message" "Invalid float width: nope" s10.message
let s11 ← runKeys s0 ([Key.char 'i'] ++ keys "abc" ++ [Key.esc] ++ [Key.char ':'] ++ keys "float guard" ++ [Key.enter])
let s12 ← runKeys s11 [Key.char 'i']
assertEqual "floating overlay enters insert mode" Mode.insert s12.mode
let s13 ← runKeys s12 (keys "X" ++ [Key.enter] ++ keys "Y")
match s13.floatingOverlay with
| some overlay =>
assertEqual "floating overlay writes text" "guardX" overlay.lines[0]!
assertEqual "floating overlay writes next line" "Y" overlay.lines[1]!
| none =>
assertEqual "floating overlay remains open while editing" true false
let s14 ← runKeys s13 [Key.esc]
assertEqual "Esc exits floating overlay insert mode" Mode.normal s14.mode
let s15 ← runKeys s14 [Key.enter]
assertEqual "Enter closes floating overlay" false s15.floatingOverlay.isSome
let sMsg0 := { s0 with message := "Error: sample message", dirty := true }
let sMsg1 ← runKeys sMsg0 [Key.enter]
assertEqual "Enter closes message float" "" sMsg1.message
let sMsg2 := { s0 with message := "Cannot write preview buffer", dirty := true }
let sMsg3 ← runKeys sMsg2 [Key.esc]
assertEqual "Esc closes message float" "" sMsg3.message
let sPrompt0 ← runKeys s0 ([Key.char ':'] ++ keys "ws list" ++ [Key.enter])
let sPrompt1 := sPrompt0.updateActiveView fun v => { v with cursor := { row := ⟨2⟩, col := 0 } }
let sPrompt2 ← runKeys sPrompt1 [Key.enter]
assertEqual "Workspace explorer New opens floating input" true sPrompt2.floatingOverlay.isSome
assertEqual "Workspace explorer New sets floating command prefix" (some "ws new ") sPrompt2.floatingInputCommand
let sPrompt3 ← runKeys sPrompt2 (keys "TmpWS" ++ [Key.enter])
assertEqual "Workspace explorer floating input submits with Enter" "TmpWS" sPrompt3.getCurrentWorkspace.name
assertEqual "Workspace explorer floating input closes after submit" false sPrompt3.floatingOverlay.isSome
let sWsCmd0 ← runKeys s0 ([Key.char ':'] ++ keys "ws new" ++ [Key.enter])
assertEqual ":ws new opens floating input" true sWsCmd0.floatingOverlay.isSome
assertEqual ":ws new floating command prefix" (some "ws new ") sWsCmd0.floatingInputCommand
let sWsCmd1 ← runKeys sWsCmd0 (keys "CmdWorkspace" ++ [Key.enter])
assertEqual ":ws new floating input submits name" "CmdWorkspace" sWsCmd1.getCurrentWorkspace.name
let sWgCmd0 ← runKeys s0 ([Key.char ':'] ++ keys "wg new" ++ [Key.enter])
assertEqual ":wg new opens floating input" true sWgCmd0.floatingOverlay.isSome
assertEqual ":wg new floating command prefix" (some "wg new ") sWgCmd0.floatingInputCommand
let sWgCmd1 ← runKeys sWgCmd0 (keys "CmdGroup" ++ [Key.enter])
assertEqual ":wg new floating input submits name" "CmdGroup" sWgCmd1.getCurrentWorkgroup.name
let stamp ← IO.monoMsNow
let tmpRoot := s!"/tmp/vie-explorer-create-{stamp}"
IO.FS.createDirAll tmpRoot
let sExp0 := { s0 with windowHeight := 30, windowWidth := 100 }
let sExp1 ← runKeys sExp0 ([Key.char ':'] ++ keys s!"ex list {tmpRoot}" ++ [Key.enter])
let explorerOpt := sExp1.explorers.find? (fun (id, _) => id == sExp1.getActiveBuffer.id)
match explorerOpt with
| none =>
assertEqual "Explorer create test has active explorer" true false
| some (_, explorer) =>
let newFileIdx := (findEntryIndex explorer.entries "[New File]").getD 0
let sExp2 := sExp1.updateActiveView fun v => { v with cursor := { row := ⟨2 + newFileIdx⟩, col := 0 } }
let sExp3 ← runKeys sExp2 [Key.enter]
assertEqual "Explorer [New File] opens floating input" true sExp3.floatingOverlay.isSome
assertEqual "Explorer [New File] sets command prefix" (some s!"mkfile {tmpRoot}/") sExp3.floatingInputCommand
let sExp4 ← runKeys sExp3 (keys "alpha.txt" ++ [Key.enter])
let fileCreated ← (System.FilePath.mk s!"{tmpRoot}/alpha.txt").pathExists
assertEqual "Explorer [New File] creates file" true fileCreated
let explorerOpt2 := sExp4.explorers.find? (fun (id, _) => id == sExp4.getActiveBuffer.id)
match explorerOpt2 with
| none =>
assertEqual "Explorer refreshed after file create" true false
| some (_, explorer2) =>
assertEqual "Explorer list includes created file" true (findEntryIndex explorer2.entries "alpha.txt").isSome
let newDirIdx := (findEntryIndex explorer.entries "[New Directory]").getD 1
let sExp5 := sExp4.updateActiveView fun v => { v with cursor := { row := ⟨2 + newDirIdx⟩, col := 0 } }
let sExp6 ← runKeys sExp5 [Key.enter]
assertEqual "Explorer [New Directory] opens floating input" true sExp6.floatingOverlay.isSome
assertEqual "Explorer [New Directory] sets command prefix" (some s!"mkdir {tmpRoot}/") sExp6.floatingInputCommand
let sExp7 ← runKeys sExp6 (keys "subdir" ++ [Key.enter])
let createdDirPath := System.FilePath.mk s!"{tmpRoot}/subdir"
let dirExists ← createdDirPath.pathExists
let dirIsDir ← if dirExists then createdDirPath.isDir else pure false
assertEqual "Explorer [New Directory] creates directory" true dirIsDir
let explorerOpt3 := sExp7.explorers.find? (fun (id, _) => id == sExp7.getActiveBuffer.id)
match explorerOpt3 with
| none =>
assertEqual "Explorer refreshed after directory create" true false
| some (_, explorer3) =>
assertEqual "Explorer list includes created directory" true (findEntryIndex explorer3.entries "subdir").isSome
let s16 ← runKeys s0 ([Key.char ':'] ++ keys "vs" ++ [Key.enter])
let ws16 := s16.getCurrentWorkspace
assertEqual "split creates second window" true (ws16.nextWindowId >= 2)
let s17 ← runKeys s16 ([Key.char ':'] ++ keys "floatwin on" ++ [Key.enter])
let ws17 := s17.getCurrentWorkspace
assertEqual ":floatwin on marks active window floating" true (ws17.isFloatingWindow ws17.activeWindowId)
let s18 ← runKeys s17 ([Key.char ':'] ++ keys "floatwin off" ++ [Key.enter])
let ws18 := s18.getCurrentWorkspace
assertEqual ":floatwin off clears floating flag" false (ws18.isFloatingWindow ws18.activeWindowId)
let s19 ← runKeys s18 ([Key.char ':'] ++ keys "floatwin" ++ [Key.enter])
let ws19 := s19.getCurrentWorkspace
assertEqual ":floatwin toggles floating flag" true (ws19.isFloatingWindow ws19.activeWindowId)
let sFloatMove0 := { s0 with windowHeight := 24, windowWidth := 80 }
let sFloatMove1 ← runKeys sFloatMove0 ([Key.char ':'] ++ keys "floatwin on" ++ [Key.enter])
let floatMoveId := sFloatMove1.getCurrentWorkspace.activeWindowId
let beforeFloatMove := sFloatMove1.getFloatingWindowBounds floatMoveId
let sFloatMove2 ← runKeys sFloatMove1 [Key.alt 'L']
let afterFloatMove := sFloatMove2.getFloatingWindowBounds floatMoveId
match beforeFloatMove, afterFloatMove with
| some (_, left0, _, _), some (_, left1, _, _) =>
assertEqual "Alt-Shift-l moves floating window right" (left0 + 1) left1
| _, _ =>
assertEqual "Alt-Shift-l moves floating window right" true false
let sFloatMoveGrp0 := { s0 with windowHeight := 24, windowWidth := 80 }
let sFloatMoveGrp1 ← runKeys sFloatMoveGrp0 ([Key.char ':'] ++ keys "floatwin on" ++ [Key.enter])
let sFloatMoveGrp2 ← runKeys sFloatMoveGrp1 ([Key.char ':'] ++ keys "vsplit" ++ [Key.enter])
let moveGrpIds := sFloatMoveGrp2.getCurrentWorkspace.getFloatingWindowIds.toList
let beforeGrp := moveGrpIds.filterMap (fun wid => (sFloatMoveGrp2.getFloatingWindowBounds wid).map (fun b => (wid, b)))
let sFloatMoveGrp3 ← runKeys sFloatMoveGrp2 [Key.alt 'J']
let afterGrp := moveGrpIds.filterMap (fun wid => (sFloatMoveGrp3.getFloatingWindowBounds wid).map (fun b => (wid, b)))
let movedAllDown :=
moveGrpIds.all (fun wid =>
match beforeGrp.find? (fun (id, _) => id == wid), afterGrp.find? (fun (id, _) => id == wid) with
| some (_, (top0, _, _, _)), some (_, (top1, _, _, _)) => top1 == top0 + 1
| _, _ => false)
assertEqual "Alt-Shift-j moves all panes in active floating subtree down" true movedAllDown
let sFloatVs0 := { s0 with windowHeight := 24, windowWidth := 80 }
let sFloatVs1 ← runKeys sFloatVs0 ([Key.char ':'] ++ keys "floatwin on" ++ [Key.enter])
let sFloatVs2 ← runKeys sFloatVs1 ([Key.char ':'] ++ keys "vsplit" ++ [Key.enter])
let wsFloatVs2 := sFloatVs2.getCurrentWorkspace
let floatingIdsVs := wsFloatVs2.getFloatingWindowIds.toList
assertEqual "vsplit in floating window keeps new pane floating" true (floatingIdsVs.length >= 2)
match floatingIdsVs with
| a :: b :: _ =>
let sideBySide :=
match sFloatVs2.getFloatingWindowBounds a, sFloatVs2.getFloatingWindowBounds b with
| some (t1, l1, h1, w1), some (t2, l2, h2, w2) =>
t1 == t2 && h1 == h2 && ((l1 + w1 == l2) || (l2 + w2 == l1))
| _, _ => false
assertEqual "vsplit in floating window keeps pair side-by-side" true sideBySide
| _ =>
assertEqual "vsplit in floating window keeps pair side-by-side" true false
let sFloatSp0 := { s0 with windowHeight := 24, windowWidth := 80 }
let sFloatSp1 ← runKeys sFloatSp0 ([Key.char ':'] ++ keys "floatwin on" ++ [Key.enter])
let sFloatSp2 ← runKeys sFloatSp1 ([Key.char ':'] ++ keys "split" ++ [Key.enter])
let wsFloatSp2 := sFloatSp2.getCurrentWorkspace
let floatingIdsSp := wsFloatSp2.getFloatingWindowIds.toList
assertEqual "split in floating window keeps new pane floating" true (floatingIdsSp.length >= 2)
match floatingIdsSp with
| a :: b :: _ =>
let stacked :=
match sFloatSp2.getFloatingWindowBounds a, sFloatSp2.getFloatingWindowBounds b with
| some (t1, l1, h1, w1), some (t2, l2, h2, w2) =>
l1 == l2 && w1 == w2 && ((t1 + h1 == t2) || (t2 + h2 == t1))
| _, _ => false
assertEqual "split in floating window keeps pair stacked" true stacked
| _ =>
assertEqual "split in floating window keeps pair stacked" true false
let sFloatMix0 := { s0 with windowHeight := 24, windowWidth := 80 }
let sFloatMix1 ← runKeys sFloatMix0 ([Key.char ':'] ++ keys "floatwin on" ++ [Key.enter])
let sFloatMix2 ← runKeys sFloatMix1 ([Key.char ':'] ++ keys "vsplit" ++ [Key.enter])
let sFloatMix3 ← runKeys sFloatMix2 ([Key.char ':'] ++ keys "split" ++ [Key.enter])
let wsFloatMix3 := sFloatMix3.getCurrentWorkspace
let floatingIdsMix := wsFloatMix3.getFloatingWindowIds.toList
assertEqual "vsplit then split in floating window keeps three panes floating" 3 floatingIdsMix.length
let boundsMix := floatingIdsMix.filterMap (fun wid => sFloatMix3.getFloatingWindowBounds wid)
assertEqual "vsplit then split in floating window resolves bounds for all panes" 3 boundsMix.length
let overlaps (a b : Nat × Nat × Nat × Nat) : Bool :=
let (ta, la, ha, wa) := a
let (tb, lb, hb, wb) := b
la < lb + wb && lb < la + wa && ta < tb + hb && tb < ta + ha
let rec hasOverlap (xs : List (Nat × Nat × Nat × Nat)) : Bool :=
match xs with
| [] => false
| x :: rest => rest.any (fun y => overlaps x y) || hasOverlap rest
assertEqual "vsplit then split in floating window panes do not overlap" false (hasOverlap boundsMix)
let sFloatDeep0 := { s0 with windowHeight := 24, windowWidth := 80 }
let sFloatDeep1 ← runKeys sFloatDeep0 ([Key.char ':'] ++ keys "floatwin on" ++ [Key.enter])
let sFloatDeep2 ← runKeys sFloatDeep1 ([Key.char ':'] ++ keys "vs" ++ [Key.enter])
let sFloatDeep3 ← runKeys sFloatDeep2 ([Key.char ':'] ++ keys "hs" ++ [Key.enter])
let sFloatDeep4 ← runKeys sFloatDeep3 ([Key.char ':'] ++ keys "vs" ++ [Key.enter])
let wsFloatDeep4 := sFloatDeep4.getCurrentWorkspace
let floatingIdsDeep := wsFloatDeep4.getFloatingWindowIds.toList
assertEqual "vs -> hs -> vs in floating window creates four panes" 4 floatingIdsDeep.length
let boundsDeep := floatingIdsDeep.filterMap (fun wid => sFloatDeep4.getFloatingWindowBounds wid)
assertEqual "vs -> hs -> vs in floating window resolves bounds for all panes" 4 boundsDeep.length
assertEqual "vs -> hs -> vs in floating window panes do not overlap" false (hasOverlap boundsDeep)
let sideBySideTouch (a b : Nat × Nat × Nat × Nat) : Bool :=
let (t1, l1, h1, w1) := a
let (t2, l2, h2, w2) := b
((l1 + w1 == l2) || (l2 + w2 == l1)) && (t1 < t2 + h2 && t2 < t1 + h1)
let stackedTouch (a b : Nat × Nat × Nat × Nat) : Bool :=
let (t1, l1, h1, w1) := a
let (t2, l2, h2, w2) := b
((t1 + h1 == t2) || (t2 + h2 == t1)) && (l1 < l2 + w2 && l2 < l1 + w1)
let rec hasPairWith
(pred : (Nat × Nat × Nat × Nat) → (Nat × Nat × Nat × Nat) → Bool)
(xs : List (Nat × Nat × Nat × Nat)) : Bool :=
match xs with
| [] => false
| x :: rest => rest.any (fun y => pred x y) || hasPairWith pred rest
assertEqual "vs -> hs -> vs in floating window keeps horizontal adjacency" true (hasPairWith sideBySideTouch boundsDeep)
assertEqual "vs -> hs -> vs in floating window keeps vertical adjacency" true (hasPairWith stackedTouch boundsDeep)
let longLine := "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
let longText := String.intercalate "\n" [longLine, longLine, longLine, longLine, longLine, longLine, longLine, longLine, longLine, longLine, longLine, longLine] ++ "\n"
let s20 := { s0 with windowHeight := 14, windowWidth := 40 }
let s21 ← runKeys s20 ([Key.char 'i'] ++ keys longText ++ [Key.esc] ++ keys "gg0")
let s22 ← runKeys s21 ([Key.char ':'] ++ keys "floatwin on" ++ [Key.enter])
let s23 ← runKeys s22 [Key.char '1', Key.char '0', Key.char 'j']
let (sRow23, _) := s23.getScroll
assertEqual "floating window vertical scroll follows cursor" true (sRow23.val > 0)
let s24 ← runKeys s23 [Key.char '3', Key.char '5', Key.char 'l']
let (_, sCol24) := s24.getScroll
assertEqual "floating window horizontal scroll follows cursor" true (sCol24.val > 0)
let s25 ← runKeys s0 ([Key.char 'i'] ++ keys "abcd\n" ++ [Key.esc] ++ keys "gg0")
let s26 ← runKeys s25 ([Key.char ':'] ++ keys "floatwin on" ++ [Key.enter])
let s27 ← runKeys s26 [Key.char 'v', Key.char 'l', Key.char 'd']
assertBuffer "visual mode edits active floating window buffer" s27 "cd\n"
let s28 ← runKeys s0 ([Key.char 'i'] ++ keys "abcd\nefgh\n" ++ [Key.esc] ++ keys "gg0")
let s29 ← runKeys s28 ([Key.char ':'] ++ keys "floatwin on" ++ [Key.enter])
let s30 ← runKeys s29 [Key.char 'l', Key.char 'V', Key.char 'l', Key.char 'j', Key.char 'd']
assertBuffer "visual block edits active floating window buffer" s30 "ad\neh\n"
def testBufferExplorerCommand : IO Unit := do
IO.println " Testing Buffer Explorer..."
let s0 := ViE.initialState
let stamp ← IO.monoMsNow
let tmpRoot := s!"/tmp/vie-buffer-explorer-{stamp}"
IO.FS.createDirAll tmpRoot
let bufAPath := s!"{tmpRoot}/bufA.txt"
let bufBPath := s!"{tmpRoot}/bufB.txt"
IO.FS.writeFile bufAPath "alpha\n"
IO.FS.writeFile bufBPath "beta\n"
let sBuf0 := { s0 with windowHeight := 30, windowWidth := 100 }
let sBuf1 ← runKeys sBuf0 ([Key.char ':'] ++ keys s!"e {bufAPath}" ++ [Key.enter])
let sBuf2 ← runKeys sBuf1 ([Key.char ':'] ++ keys s!"e {bufBPath}" ++ [Key.enter])
let targetBufIdOpt := sBuf2.getCurrentWorkspace.buffers.find? (fun b => b.filename == some bufAPath) |>.map (fun b => b.id)
assertEqual "Buffer explorer target buffer exists" true targetBufIdOpt.isSome
let targetBufId := targetBufIdOpt.getD 0
let sBuf3 ← runKeys sBuf2 ([Key.char ':'] ++ keys "buf list" ++ [Key.enter])
assertEqual ":buf list opens buffer explorer" true ((sBuf3.getActiveBuffer.filename.getD "").startsWith "explorer://buffers")
let explorerBufId := sBuf3.getActiveBuffer.id
let explorerBufOpt := sBuf3.explorers.find? (fun (id, _) => id == explorerBufId)
match explorerBufOpt with
| none =>
assertEqual "Buffer explorer registered" true false
| some (_, explorerBuf) =>
let targetPath := s!"buffer://{targetBufId}"
let targetIdxOpt := findEntryIndexByPath explorerBuf.entries targetPath
assertEqual "Buffer explorer contains target buffer entry" true targetIdxOpt.isSome
let targetIdx := targetIdxOpt.getD 0
let sBuf4 := sBuf3.updateActiveView fun v => { v with cursor := { row := ⟨2 + targetIdx⟩, col := 0 } }
let sBuf5 ← runKeys sBuf4 [Key.enter]
assertEqual "Buffer explorer Enter switches active buffer" (some bufAPath) sBuf5.getActiveBuffer.filename
assertEqual "Buffer explorer closes after selection" false (sBuf5.explorers.any (fun (id, _) => id == explorerBufId))
let sBufCompat ← runKeys sBuf2 ([Key.char ':'] ++ keys "buffers" ++ [Key.enter])
assertEqual ":buffers alias opens buffer explorer" true ((sBufCompat.getActiveBuffer.filename.getD "").startsWith "explorer://buffers")
let sBufAlias ← runKeys sBuf2 ([Key.char ':'] ++ keys "ls" ++ [Key.enter])
assertEqual ":ls alias opens buffer explorer" true ((sBufAlias.getActiveBuffer.filename.getD "").startsWith "explorer://buffers")
def testExplorerCommandAliases : IO Unit := do
IO.println " Testing Explorer Command Names..."
let s0 := ViE.initialState
let stamp ← IO.monoMsNow
let tmpRoot := s!"/tmp/vie-ex-alias-{stamp}"
IO.FS.createDirAll tmpRoot
IO.FS.writeFile s!"{tmpRoot}/x.txt" "x\n"
let s1 ← runKeys s0 ([Key.char ':'] ++ keys s!"ex list {tmpRoot}" ++ [Key.enter])
assertEqual ":ex list opens file explorer" true ((s1.getActiveBuffer.filename.getD "").startsWith "explorer://")
let s2 ← runKeys s0 ([Key.char ':'] ++ keys s!"ee {tmpRoot}" ++ [Key.enter])
assertEqual ":ee alias opens file explorer" true ((s2.getActiveBuffer.filename.getD "").startsWith "explorer://")
let s3 ← runKeys s0 ([Key.char ':'] ++ keys "wgex" ++ [Key.enter])
assertEqual ":wgex alias opens workgroup explorer" (some "explorer://workgroups") s3.getActiveBuffer.filename
def testExplorerOpenUsesFocusedWindow : IO Unit := do
IO.println " Testing Explorer Open Target Window..."
let s0 := { ViE.initialState with windowHeight := 30, windowWidth := 100 }
let stamp ← IO.monoMsNow
let tmpRoot := s!"/tmp/vie-ex-target-{stamp}"
IO.FS.createDirAll tmpRoot
let leftPath := s!"{tmpRoot}/left.txt"
let rightPath := s!"{tmpRoot}/right.txt"
let targetPath := s!"{tmpRoot}/target.txt"
IO.FS.writeFile leftPath "left\n"
IO.FS.writeFile rightPath "right\n"
IO.FS.writeFile targetPath "target\n"
let s1 ← runKeys s0 ([Key.char ':'] ++ keys s!"e {leftPath}" ++ [Key.enter])
let s2 ← runKeys s1 ([Key.char ':'] ++ keys "vsplit" ++ [Key.enter])
let rightWinId := s2.getCurrentWorkspace.activeWindowId
let s3 ← runKeys s2 ([Key.char ':'] ++ keys s!"e {rightPath}" ++ [Key.enter])
let s4 ← runKeys s3 ([Key.char ':'] ++ keys "wincmd w" ++ [Key.enter])
let targetWinId := s4.getCurrentWorkspace.activeWindowId
assertEqual "wincmd w changes active target window" true (targetWinId != rightWinId)
let s5 ← runKeys s4 ([Key.char ':'] ++ keys s!"ex list {tmpRoot}" ++ [Key.enter])
let explorerBufId := s5.getActiveBuffer.id
let explorerOpt := s5.explorers.find? (fun (id, _) => id == explorerBufId)
match explorerOpt with
| none =>
assertEqual "File explorer registered" true false
| some (_, explorer) =>
let targetIdxOpt := findEntryIndex explorer.entries "target.txt"
assertEqual "Explorer contains target file" true targetIdxOpt.isSome
let targetIdx := targetIdxOpt.getD 0
let s6 := s5.updateActiveView fun v => { v with cursor := { row := ⟨2 + targetIdx⟩, col := 0 } }
let s7 ← runKeys s6 [Key.enter]
assertEqual "Explorer Enter uses focused window from before open" targetWinId s7.getCurrentWorkspace.activeWindowId
assertEqual "Selected file opens in focused window" (some targetPath) s7.getActiveBuffer.filename
let ws7 := s7.getCurrentWorkspace
match ws7.layout.findView rightWinId with
| none =>
assertEqual "Other split window still exists" true false
| some rightView =>
let rightBufName := ws7.buffers.find? (fun b => b.id == rightView.bufferId) |>.bind (fun b => b.filename)
assertEqual "Non-focused split keeps prior buffer" (some rightPath) rightBufName
def test : IO Unit := do
IO.println "Starting Expanded Keybind Tests..."
testMotions
testEditing
testOperators
testVisual
testCounted
testVimCompatMotions
testWorkgroupSwitch
testSearch
testCommandSubstitute
testCommandGlobal
testCursorDriftCustomTabStop
testCommandBloom
testUiCommands
testBufferExplorerCommand
testExplorerCommandAliases
testExplorerOpenUsesFocusedWindow
IO.println "All Expanded Keybind Tests passed!"
end Test.Keybinds