NVJSIX3GL5WPI3FQ6U7TQQE5T4EDEHSI6SDQM7TPJTGBKCPILJFAC ZL7ZSOEOGP2E24UEFC5VVPN5GTL3EUL34FA7F47WWWFPHJH2RS2AC 6W6OJAUT3KLOXXKYR4GA3MG4ACLO2T4KX3SGD2KZDX6MYEZYTPHAC U45XPF3YKPXXRJ4MN24T2RV7GOL4FZKQSWX5P5I7WT4HC5XF4FHAC TKAAHADV62OQORAO7MYRRRHTGPMG3GKXU4Z4MFNLAO6DLTSDU3CAC WRBKZMYVNHRWT7TTUGTDJ3TMWZB32QYW5PCLKTTVAJ2YF6OI3LTAC NBG3GUEJJMWS2FRJDU26KRQLA6O2YK77PDAPBMD3SKWLDTK23BVAC CNJGJCJZ6LRHHIRSRATSE3D3Z5OQGSSTDMXCPVXSKRQLYJFME6IAC 5SFTBD4FW6GQPKRUJBAII5R2EZPDYG6CH57BIJD4IVZBE6JZB2ZQC SEIYD7YXZ5XI4W7UVF5O2UFLA6OCZZCOLV2XB46RKJ5EN6UQC4UAC XCVPXP4UBFU25NF6VQC3MEYHHH45RL2UXM5TA6O23NA64FPCCCJQC def EditorState.searchWordUnderCursor (s : EditorState) (direction : SearchDirection) : EditorState :=match s.wordUnderCursor with| none => { s with message := "No keyword under cursor" }| some word =>let s1 := s.pushJumpPointlet cursor := s1.getCursorlet cursorOffset :=ViE.getOffsetFromPointInBufferWithTabStop s1.getActiveBuffer cursor.row cursor.col s1.config.tabStop |>.getD 0let s2 := ViE.startOrUpdateSearch s1 word direction falselet s3 :=match s2.searchState with| none => s2| some st => { s2 with searchState := some { st with lastMatch := some cursorOffset } }ViE.findNextMatch s3 (some direction)
def jumpListLimit : Nat := 256def visibleContentRows (s : EditorState) : Nat :=let (h, _) := s.getActiveWindowScrollBoundsif h > 1 then h - 1 else 1def maxScrollableTop (buffer : FileBuffer) (rowsInView : Nat) : Nat :=if buffer.lineCount > rowsInView then buffer.lineCount - rowsInView else 0def clampPointToActiveBuffer (s : EditorState) (p : Point) : Point :=let buffer := s.getActiveBufferif buffer.lineCount == 0 thenPoint.zeroelselet rowVal := min p.row.val (buffer.lineCount - 1)let tabStop := s.config.tabStoplet lineLen := lineDisplayWidth tabStop buffer ⟨rowVal⟩let colVal := min p.col.val lineLen{ row := ⟨rowVal⟩, col := ⟨colVal⟩ }def EditorState.pushJumpPoint (s : EditorState) : EditorState :=let p := s.getCursormatch s.jumpBack with| head :: _ =>if head == p thenselse{ s with jumpBack := (p :: s.jumpBack).take jumpListLimit, jumpForward := [] }| [] =>{ s with jumpBack := [p], jumpForward := [] }def EditorState.jumpBackInList (s : EditorState) : EditorState :=match s.jumpBack with| [] => { s with message := "Jump list empty" }| p :: rest =>let current := s.getCursorlet target := clampPointToActiveBuffer s plet s' := s.setCursor target{ s' withjumpBack := restjumpForward := (current :: s.jumpForward).take jumpListLimit}def EditorState.jumpForwardInList (s : EditorState) : EditorState :=match s.jumpForward with| [] => { s with message := "Jump list empty" }| p :: rest =>let current := s.getCursorlet target := clampPointToActiveBuffer s plet s' := s.setCursor target{ s' withjumpForward := restjumpBack := (current :: s.jumpBack).take jumpListLimit}def EditorState.scrollDownRows (s : EditorState) (rows : Nat) : EditorState :=let buffer := s.getActiveBufferif buffer.lineCount == 0 thenselselet tabStop := s.config.tabStoplet (sRow, sCol) := s.getScrolllet cursor := s.getCursorlet rowsInView := visibleContentRows slet maxTop := maxScrollableTop buffer rowsInViewlet newTop := min (sRow.val + rows) maxToplet visRow := if cursor.row.val >= sRow.val then cursor.row.val - sRow.val else 0let newRowVal := min (newTop + visRow) (buffer.lineCount - 1)let newLen := lineDisplayWidth tabStop buffer ⟨newRowVal⟩let newColVal := min cursor.col.val newLen(s.setScroll ⟨newTop⟩ sCol).setCursor { row := ⟨newRowVal⟩, col := ⟨newColVal⟩ }def EditorState.scrollUpRows (s : EditorState) (rows : Nat) : EditorState :=let buffer := s.getActiveBufferif buffer.lineCount == 0 thenselselet tabStop := s.config.tabStoplet (sRow, sCol) := s.getScrolllet cursor := s.getCursorlet newTop := if rows > sRow.val then 0 else sRow.val - rowslet visRow := if cursor.row.val >= sRow.val then cursor.row.val - sRow.val else 0let newRowVal := min (newTop + visRow) (buffer.lineCount - 1)let newLen := lineDisplayWidth tabStop buffer ⟨newRowVal⟩let newColVal := min cursor.col.val newLen(s.setScroll ⟨newTop⟩ sCol).setCursor { row := ⟨newRowVal⟩, col := ⟨newColVal⟩ }def halfPageRows (s : EditorState) : Nat :=max 1 ((visibleContentRows s) / 2)def fullPageRows (s : EditorState) : Nat :=max 1 (visibleContentRows s)def EditorState.scrollHalfPageDown (s : EditorState) (count : Nat := 0) : EditorState :=let rows := if count == 0 then halfPageRows s else counts.scrollDownRows rowsdef EditorState.scrollHalfPageUp (s : EditorState) (count : Nat := 0) : EditorState :=let rows := if count == 0 then halfPageRows s else counts.scrollUpRows rowsdef EditorState.scrollPageDown (s : EditorState) (count : Nat := 0) : EditorState :=let rows := if count == 0 then fullPageRows s else counts.scrollDownRows rowsdef EditorState.scrollPageUp (s : EditorState) (count : Nat := 0) : EditorState :=let rows := if count == 0 then fullPageRows s else counts.scrollUpRows rowsdef EditorState.scrollWindowDown (s : EditorState) (count : Nat := 1) : EditorState :=let rows := if count == 0 then 1 else counts.scrollDownRows rowsdef EditorState.scrollWindowUp (s : EditorState) (count : Nat := 1) : EditorState :=let rows := if count == 0 then 1 else counts.scrollUpRows rowsdef firstNonBlankColWithTabStop (line : String) (tabStop : Nat) : Nat :=let rec loop (cs : List Char) (col : Nat) : Nat :=match cs with| [] => 0| c :: rest =>if c == ' ' || c == '\t' thenloop rest (col + ViE.Unicode.charWidthAt tabStop col c)elsecolloop line.toList 0def EditorState.moveToScreenTop (s : EditorState) (count : Nat := 0) : EditorState :=let buffer := s.getActiveBufferif buffer.lineCount == 0 thenselselet n := if count == 0 then 1 else countlet rowsInView := visibleContentRows slet offset := min (n - 1) (rowsInView - 1)let (sRow, _) := s.getScrolllet rowVal := min (sRow.val + offset) (buffer.lineCount - 1)let tabStop := s.config.tabStoplet lineStr := lineString buffer ⟨rowVal⟩let colVal := firstNonBlankColWithTabStop lineStr tabStops.setCursor { row := ⟨rowVal⟩, col := ⟨colVal⟩ }def EditorState.moveToScreenMiddle (s : EditorState) : EditorState :=let buffer := s.getActiveBufferif buffer.lineCount == 0 thenselselet rowsInView := visibleContentRows slet offset := rowsInView / 2let (sRow, _) := s.getScrolllet rowVal := min (sRow.val + offset) (buffer.lineCount - 1)let tabStop := s.config.tabStoplet lineStr := lineString buffer ⟨rowVal⟩let colVal := firstNonBlankColWithTabStop lineStr tabStops.setCursor { row := ⟨rowVal⟩, col := ⟨colVal⟩ }def EditorState.moveToScreenBottom (s : EditorState) (count : Nat := 0) : EditorState :=let buffer := s.getActiveBufferif buffer.lineCount == 0 thenselselet n := if count == 0 then 1 else countlet rowsInView := visibleContentRows slet fromBottom := min (n - 1) (rowsInView - 1)let offset := (rowsInView - 1) - fromBottomlet (sRow, _) := s.getScrolllet rowVal := min (sRow.val + offset) (buffer.lineCount - 1)let tabStop := s.config.tabStoplet lineStr := lineString buffer ⟨rowVal⟩let colVal := firstNonBlankColWithTabStop lineStr tabStops.setCursor { row := ⟨rowVal⟩, col := ⟨colVal⟩ }private def findCharForwardOnLineCore(tabStop : Nat) (lineStr : String) (lineLen : Nat) (col : Nat) (target : Char) (fuel : Nat) : Option Nat :=match fuel with| 0 => none| fuel + 1 =>if col >= lineLen thennoneelselet c := charAtDisplayCol tabStop lineStr ⟨col⟩if c == target thensome colelselet next := ViE.Unicode.nextDisplayColWithTabStop lineStr tabStop colif next <= col then none else findCharForwardOnLineCore tabStop lineStr lineLen next target fuelprivate def findCharBackwardOnLineCore(tabStop : Nat) (lineStr : String) (col : Nat) (target : Char) (fuel : Nat) : Option Nat :=match fuel with| 0 => none| fuel + 1 =>let c := charAtDisplayCol tabStop lineStr ⟨col⟩if c == target thensome colelse if col == 0 thennoneelselet prev := ViE.Unicode.prevDisplayColWithTabStop lineStr tabStop colif prev >= col then none else findCharBackwardOnLineCore tabStop lineStr prev target fueldef EditorState.findCharMotion (s : EditorState) (target : Char) (forward : Bool) (till : Bool)(count : Nat := 1) (updateLast : Bool := true) : EditorState :=let buffer := s.getActiveBufferlet cursor := s.getCursorlet tabStop := s.config.tabStoplet lineStr := lineString buffer cursor.rowlet lineLen := lineDisplayWidth tabStop buffer cursor.rowif lineLen == 0 thenselselet n := if count == 0 then 1 else countlet rec findNth (fromCol : Nat) (remaining : Nat) (fuel : Nat) : Option Nat :=match remaining with| 0 => some fromCol| rem + 1 =>if fuel == 0 thennoneelselet found :=if forward thenlet start := ViE.Unicode.nextDisplayColWithTabStop lineStr tabStop fromColif start >= lineLen then noneelse findCharForwardOnLineCore tabStop lineStr lineLen start target (lineLen + 1)elseif fromCol == 0 then noneelselet start := ViE.Unicode.prevDisplayColWithTabStop lineStr tabStop fromColif start >= lineLen then noneelse findCharBackwardOnLineCore tabStop lineStr start target (lineLen + 1)match found with| none => none| some hit =>if rem == 0 thensome hitelsefindNth hit rem fuel.predtermination_by remaining + fuelmatch findNth cursor.col.val n (lineLen + n + 1) with| none => { s with message := s!"Pattern not found: {target}" }| some hit =>let hitCol :=if till thenif forward thenViE.Unicode.prevDisplayColWithTabStop lineStr tabStop hitelseViE.Unicode.nextDisplayColWithTabStop lineStr tabStop hitelsehitlet hitCol := min hitCol lineLenlet s' := s.setCursor { cursor with col := ⟨hitCol⟩ }if updateLast then{s' withinputState := {s'.inputState withlastFindChar := some targetlastFindForward := forwardlastFindTill := till}}elses'
def EditorState.repeatFindChar (s : EditorState) (reverse : Bool := false) : EditorState :=match s.inputState.lastFindChar with| none => { s with message := "No previous find command" }| some target =>let baseForward := s.inputState.lastFindForwardlet forward := if reverse then !baseForward else baseForwardlet count :=if s.inputState.lastFindTill && !reverse then 2 else 1s.findCharMotion target forward s.inputState.lastFindTill count false
def EditorState.wordUnderCursor (s : EditorState) : Option String :=let buffer := s.getActiveBufferlet cursor := s.getCursorlet tabStop := s.config.tabStoplet lineStr := lineString buffer cursor.rowlet lineLen := lineDisplayWidth tabStop buffer cursor.rowlet isWordChar (c : Char) : Bool := c.isAlphanum || c == '_'if lineLen == 0 || cursor.col.val >= lineLen thennoneelselet curChar := charAtDisplayCol tabStop lineStr cursor.colif !isWordChar curChar thennoneelselet rec expandLeft (col : Col) (fuel : Nat) : Col :=match fuel with| 0 => col| fuel + 1 =>if col.val == 0 thencolelselet prev := prevCol tabStop buffer cursor.row collet c := charAtDisplayCol tabStop lineStr previf isWordChar c then expandLeft prev fuel else collet rec expandRightExclusive (col : Col) (fuel : Nat) : Nat :=match fuel with| 0 => col.val| fuel + 1 =>let next := nextCol tabStop buffer cursor.row colif next.val >= lineLen thenlineLenelselet c := charAtDisplayCol tabStop lineStr nextif isWordChar c thenexpandRightExclusive next fuelelsenext.vallet start := expandLeft cursor.col (lineLen + 1)let endExclusive := expandRightExclusive cursor.col (lineLen + 1)if endExclusive <= start.val thennoneelselet sub := ViE.Unicode.dropByDisplayWidthWithTabStop lineStr.toRawSubstring tabStop start.valsome (ViE.Unicode.takeByDisplayWidthWithTabStop sub tabStop (endExclusive - start.val))def EditorState.jumpMatchingBracket (s : EditorState) : EditorState :=let buffer := s.getActiveBufferlet cursor := s.getCursorlet tabStop := s.config.tabStoplet lineLen := lineDisplayWidth tabStop buffer cursor.rowif lineLen == 0 || cursor.col.val >= lineLen then{ s with message := "No bracket under cursor" }elselet here := charAtDisplayCol tabStop (lineString buffer cursor.row) cursor.collet info : Option (UInt8 × UInt8 × Bool) :=match here with| '(' => some (UInt8.ofNat 40, UInt8.ofNat 41, true)| '[' => some (UInt8.ofNat 91, UInt8.ofNat 93, true)| '{' => some (UInt8.ofNat 123, UInt8.ofNat 125, true)| ')' => some (UInt8.ofNat 40, UInt8.ofNat 41, false)| ']' => some (UInt8.ofNat 91, UInt8.ofNat 93, false)| '}' => some (UInt8.ofNat 123, UInt8.ofNat 125, false)| _ => nonematch info with| none => { s with message := "No bracket under cursor" }| some (openB, closeB, forward) =>let off := ViE.getOffsetFromPointInBufferWithTabStop buffer cursor.row cursor.col tabStop |>.getD 0let bytes := buffer.table.toString.toUTF8if off >= bytes.size then{ s with message := "No bracket match" }elselet rec scanForward (i : Nat) (depth : Nat) (fuel : Nat) : Option Nat :=match fuel with| 0 => none| fuel + 1 =>if i >= bytes.size thennoneelselet b := bytes[i]!let depth :=if b == openB then depth + 1else if b == closeB then depth - 1else depthif depth == 0 then some i else scanForward (i + 1) depth fueltermination_by fuellet rec scanBackward (i : Nat) (depth : Nat) (fuel : Nat) : Option Nat :=match fuel with| 0 => none| fuel + 1 =>if i == 0 thennoneelselet j := i - 1let b := bytes[j]!let depth :=if b == closeB then depth + 1else if b == openB then depth - 1else depthif depth == 0 then some j else scanBackward j depth fueltermination_by fuellet hit :=if forward thenscanForward (off + 1) 1 (bytes.size + 1)elsescanBackward off 1 (bytes.size + 1)match hit with| none => { s with message := "No bracket match" }| some j =>let p := ViE.getPointFromOffsetInBufferWithTabStop buffer j tabStops.setCursor p
match ViE.getOffsetFromPointInBufferWithTabStop buffer cursor.row.val prevC.val tabStop,ViE.getOffsetFromPointInBufferWithTabStop buffer cursor.row.val cursor.col.val tabStop with
match ViE.getOffsetFromPointInBufferWithTabStop buffer cursor.row prevC tabStop,ViE.getOffsetFromPointInBufferWithTabStop buffer cursor.row cursor.col tabStop with
match ViE.getOffsetFromPointInBufferWithTabStop buffer cursor.row.val cursor.col.val tabStop,ViE.getOffsetFromPointInBufferWithTabStop buffer cursor.row.val nextC.val tabStop with
match ViE.getOffsetFromPointInBufferWithTabStop buffer cursor.row cursor.col tabStop,ViE.getOffsetFromPointInBufferWithTabStop buffer cursor.row nextC tabStop with
match ViE.getOffsetFromPointInBufferWithTabStop buffer cursor.row.val cursor.col.val tabStop,ViE.getOffsetFromPointInBufferWithTabStop buffer cursor.row.val nextC.val tabStop with
match ViE.getOffsetFromPointInBufferWithTabStop buffer cursor.row cursor.col tabStop,ViE.getOffsetFromPointInBufferWithTabStop buffer cursor.row nextC tabStop with
match ViE.getOffsetFromPointInBufferWithTabStop buffer p1.row.val p1.col.val tabStop,ViE.getOffsetFromPointInBufferWithTabStop buffer p2.row.val p2.col.val tabStop with
match ViE.getOffsetFromPointInBufferWithTabStop buffer p1.row p1.col tabStop,ViE.getOffsetFromPointInBufferWithTabStop buffer p2.row p2.col tabStop with
if s.inputState.previousKey == some 'f' thenmatch 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'.getCountpure $ 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' thenmatch 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'.getCountpure $ clearInput (EditorState.findCharMotion s' c true true n true)| _ =>pure { s with inputState := { s.inputState with previousKey := none, countBuffer := "" } }else
| Key.char 'H' =>let n := rawCount spure $ clearInput ((s.pushJumpPoint).moveToScreenTop n)| Key.char 'M' =>pure $ clearInput ((s.pushJumpPoint).moveToScreenMiddle)| Key.char 'L' =>let n := rawCount spure $ 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.ctrl 'd' =>let n := rawCount spure $ clearInput (EditorState.scrollHalfPageDown s n)| Key.ctrl 'u' =>let n := rawCount spure $ clearInput (EditorState.scrollHalfPageUp s n)| Key.ctrl 'f' =>let n := rawCount spure $ clearInput (EditorState.scrollPageDown s n)| Key.ctrl 'b' =>let n := rawCount spure $ clearInput (EditorState.scrollPageUp s n)| Key.ctrl 'e' =>let n := rawCount spure $ clearInput (EditorState.scrollWindowDown s n)| Key.ctrl 'y' =>let n := rawCount spure $ clearInput (EditorState.scrollWindowUp s n)| Key.ctrl 'o' =>pure $ clearInput (EditorState.jumpBackInList s)| Key.char '\t' =>pure $ clearInput (EditorState.jumpForwardInList s)
def collectMatchesInLine (pt : PieceTable) (row : Nat) (pattern : ByteArray) : Array (Nat × Nat) :=match pt.getLineRange row with
def collectMatchesInLine (pt : PieceTable) (row : Row) (pattern : ByteArray) : Array (Nat × Nat) :=match pt.getLineRange row.val with
def getOffsetFromPointInBufferWithTabStop (buffer : FileBuffer) (row col tabStop : Nat) : Option Nat :=match buffer.table.getLineRange row with
def getOffsetFromPointInBufferWithTabStop (buffer : FileBuffer) (row : Row) (col : Col) (tabStop : Nat) : Option Nat :=match buffer.table.getLineRange row.val with
let line := getLineFromBuffer buffer ⟨row⟩ |>.getD ""let byteOff := ViE.Unicode.displayColToByteOffsetWithTabStop line tabStop col
let line := getLineFromBuffer buffer row |>.getD ""let byteOff := ViE.Unicode.displayColToByteOffsetWithTabStop line tabStop col.val
/-- Convert byte offset to Row/Col (display column) with configurable tab stop. -/def getPointFromOffsetInBufferWithTabStop (buffer : FileBuffer) (offset tabStop : Nat) : Point :=let (rowNat, _) := buffer.table.getPointFromOffset offsetmatch buffer.table.getLineRange rowNat with| some (startOff, len) =>let rel := if offset >= startOff then min (offset - startOff) len else 0let sub := ViE.PieceTree.getSubstring buffer.table.tree startOff rel buffer.tablelet col := ViE.Unicode.stringWidthWithTabStop sub tabStopPoint.fromNat rowNat col| none =>Point.fromNat rowNat 0
let sInc0 := ViE.startOrUpdateSearch ViE.initialState "abc" .forward falselet sInc1 := {sInc0 withmode := .searchForwardinputState := { sInc0.inputState with commandBuffer := "" }}let sInc2 := ViE.runIncrementalSearch sInc1let incCleared :=match sInc2.searchState with| none => true| some _ => falseassertEqual "Incremental search with empty pattern clears highlight state" true incCleared
def testVimCompatMotions : IO Unit := doIO.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 0let sCu ← runKeys sCd [Key.ctrl 'u']assertCursor "Ctrl-u scrolls half page up" sCu 0 0let sCf ← runKeys s1 [Key.ctrl 'f']assertCursor "Ctrl-f scrolls one page down" sCf 10 0let sCb ← runKeys sCf [Key.ctrl 'b']assertCursor "Ctrl-b scrolls one page up" sCb 0 0let sCe ← runKeys s1 [Key.ctrl 'e']let (ceRow, _) := sCe.getScrollassertEqual "Ctrl-e scrolls window down by one line" 1 ceRow.vallet sCy ← runKeys sCe [Key.ctrl 'y']let (cyRow, _) := sCy.getScrollassertEqual "Ctrl-y scrolls window up by one line" 0 cyRow.vallet sH ← runKeys sCf [Key.char 'H']assertCursor "H moves to top line of screen" sH 10 2let sM ← runKeys sCf [Key.char 'M']assertCursor "M moves to middle line of screen" sM 15 2let sL ← runKeys sCf [Key.char 'L']assertCursor "L moves to bottom line of screen" sL 19 2let 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 3let sF2 ← runKeys sF1 [Key.char ';']assertCursor "; repeats last f motion" sF2 0 6let sF3 ← runKeys sF2 [Key.char ',']assertCursor ", reverses last f motion" sF3 0 3let sT1 ← runKeys sF3 [Key.char 't', Key.char 'a']assertCursor "ta moves before target" sT1 0 5let 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 6let sPct2 ← runKeys sPct1 [Key.ctrl 'o']assertCursor "Ctrl-o jumps back in jump list" sPct2 0 0let sPct3 ← runKeys sPct2 [Key.char '\t']assertCursor "Ctrl-i (Tab) jumps forward in jump list" sPct3 0 6let sStar0 := ViE.initialStatelet 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 0let sStar3 ← runKeys sStar2 [Key.char '#']assertCursor "# searches previous word under cursor" sStar3 0 0