import ViE.State.Config
import ViE.State.Layout
import ViE.Types
import ViE.Buffer.Content
import ViE.Unicode
namespace ViE
def EditorState.getCursor (s : EditorState) : Point :=
let ws := s.getCurrentWorkspace
match ws.layout.findView ws.activeWindowId with
| some view => view.cursor
| none => Point.zero
def EditorState.setCursor (s : EditorState) (p : Point) : EditorState :=
s.updateActiveView fun v => { v with cursor := p }
def EditorState.setScroll (s : EditorState) (row : Row) (col : Col) : EditorState :=
s.updateActiveView fun v => { v with scrollRow := row, scrollCol := col }
def lineString (buffer : FileBuffer) (row : Row) : String :=
ViE.getLineFromBuffer buffer row |>.getD ""
def lineDisplayWidth (tabStop : Nat) (buffer : FileBuffer) (row : Row) : Nat :=
ViE.getLineLengthFromBufferWithTabStop buffer row tabStop |>.getD 0
def charAtDisplayCol (tabStop : Nat) (lineStr : String) (col : Col) : Char :=
let idx := ViE.Unicode.displayColToCharIndexWithTabStop lineStr tabStop col.val
List.getD lineStr.toList idx ' '
def nextCol (tabStop : Nat) (buffer : FileBuffer) (row : Row) (col : Col) : Col :=
let lineStr := lineString buffer row
⟨ViE.Unicode.nextDisplayColWithTabStop lineStr tabStop col.val⟩
def prevCol (tabStop : Nat) (buffer : FileBuffer) (row : Row) (col : Col) : Col :=
let lineStr := lineString buffer row
⟨ViE.Unicode.prevDisplayColWithTabStop lineStr tabStop col.val⟩
def EditorState.moveCursorLeft (s : EditorState) : EditorState :=
let tabStop := s.config.tabStop
let cursor := s.getCursor
if cursor.col.val > 0 then
let newCol := prevCol tabStop s.getActiveBuffer cursor.row cursor.col
s.setCursor { cursor with col := newCol }
else
s
def EditorState.moveCursorRight (s : EditorState) : EditorState :=
let tabStop := s.config.tabStop
let cursor := s.getCursor
let buffer := s.getActiveBuffer
let limit := lineDisplayWidth tabStop buffer cursor.row
let newCol := nextCol tabStop buffer cursor.row cursor.col
if cursor.col.val < limit && newCol.val != cursor.col.val then
s.setCursor { cursor with col := newCol }
else
s
def EditorState.moveCursorUp (s : EditorState) : EditorState :=
let tabStop := s.config.tabStop
let cursor := s.getCursor
if cursor.row.val > 0 then
let newRow : Row := ⟨cursor.row.val - 1⟩
let lineLen := lineDisplayWidth tabStop s.getActiveBuffer newRow
s.setCursor { cursor with row := newRow, col := ⟨min cursor.col.val lineLen⟩ }
else
s
def EditorState.moveCursorDown (s : EditorState) : EditorState :=
let tabStop := s.config.tabStop
let cursor := s.getCursor
let buffer := s.getActiveBuffer
if cursor.row.val < buffer.lineCount - 1 then
let newRow : Row := ⟨cursor.row.val + 1⟩
let lineLen := lineDisplayWidth tabStop buffer newRow
s.setCursor { cursor with row := newRow, col := ⟨min cursor.col.val lineLen⟩ }
else
s
def EditorState.clearInputState (s : EditorState) : EditorState :=
{ s with inputState := { s.inputState with countBuffer := "", previousKey := none } }
def EditorState.moveCursorLeftN (s : EditorState) (n : Nat) : EditorState :=
n.repeat (fun s => s.moveCursorLeft) s
def EditorState.moveCursorRightN (s : EditorState) (n : Nat) : EditorState :=
n.repeat (fun s => s.moveCursorRight) s
def EditorState.moveCursorUpN (s : EditorState) (n : Nat) : EditorState :=
n.repeat (fun s => s.moveCursorUp) s
def EditorState.moveCursorDownN (s : EditorState) (n : Nat) : EditorState :=
n.repeat (fun s => s.moveCursorDown) s
def EditorState.getCount (s : EditorState) : Nat :=
match s.inputState.countBuffer.toNat? with
| some n => n
| none => 1
def EditorState.jumpToColumn (s : EditorState) (col : Col) : EditorState :=
let tabStop := s.config.tabStop
let cursor := s.getCursor
let len := lineDisplayWidth tabStop s.getActiveBuffer cursor.row
let newCol : Col := ⟨min (col.val - 1) len⟩ -- 1-indexed to 0-indexed, clamped
let s' := s.setCursor { cursor with col := newCol }
{ s' with inputState := { s'.inputState with countBuffer := "" } }
def EditorState.jumpToLine (s : EditorState) (line : Nat) : EditorState :=
let tabStop := s.config.tabStop
let buffer := s.getActiveBuffer
let maxLine := buffer.lineCount
let newRowVal := if line == 0 then 0 else min (line - 1) (maxLine - 1)
let newRow : Row := ⟨newRowVal⟩
let lineLen := lineDisplayWidth tabStop buffer newRow
let cursor := s.getCursor
let newCol : Col := ⟨min cursor.col.val lineLen⟩
let s' := s.setCursor { cursor with row := newRow, col := newCol }
{ s' with inputState := { s'.inputState with countBuffer := "" } }
def EditorState.moveToLineStart (s : EditorState) : EditorState :=
let cursor := s.getCursor
s.setCursor { cursor with col := 0 }
def EditorState.moveToLineEnd (s : EditorState) : EditorState :=
let tabStop := s.config.tabStop
let cursor := s.getCursor
let len := lineDisplayWidth tabStop s.getActiveBuffer cursor.row
s.setCursor { cursor with col := ⟨len⟩ }
def EditorState.clampCursor (s : EditorState) : EditorState :=
let tabStop := s.config.tabStop
let cursor := s.getCursor
let len := lineDisplayWidth tabStop s.getActiveBuffer cursor.row
match s.mode with
| Mode.insert =>
if cursor.col.val > len then
s.setCursor { cursor with col := ⟨len⟩ }
else
s
| _ => -- Normal, Visual etc.
if len == 0 then
s.setCursor { cursor with col := ⟨0⟩ }
else if cursor.col.val >= len then
s.setCursor { cursor with col := ⟨len - 1⟩ }
else
s
def jumpListLimit : Nat := 256
def visibleContentRows (s : EditorState) : Nat :=
let (h, _) := s.getActiveWindowScrollBounds
if h > 1 then h - 1 else 1
def maxScrollableTop (buffer : FileBuffer) (rowsInView : Nat) : Nat :=
if buffer.lineCount > rowsInView then buffer.lineCount - rowsInView else 0
def clampPointToActiveBuffer (s : EditorState) (p : Point) : Point :=
let buffer := s.getActiveBuffer
if buffer.lineCount == 0 then
Point.zero
else
let rowVal := min p.row.val (buffer.lineCount - 1)
let tabStop := s.config.tabStop
let 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.getCursor
match s.jumpBack with
| head :: _ =>
if head == p then
s
else
{ 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.getCursor
let target := clampPointToActiveBuffer s p
let s' := s.setCursor target
{ s' with
jumpBack := rest
jumpForward := (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.getCursor
let target := clampPointToActiveBuffer s p
let s' := s.setCursor target
{ s' with
jumpForward := rest
jumpBack := (current :: s.jumpBack).take jumpListLimit
}
def EditorState.scrollDownRows (s : EditorState) (rows : Nat) : EditorState :=
let buffer := s.getActiveBuffer
if buffer.lineCount == 0 then
s
else
let tabStop := s.config.tabStop
let (sRow, sCol) := s.getScroll
let cursor := s.getCursor
let rowsInView := visibleContentRows s
let maxTop := maxScrollableTop buffer rowsInView
let newTop := min (sRow.val + rows) maxTop
let visRow := if cursor.row.val >= sRow.val then cursor.row.val - sRow.val else 0
let 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.getActiveBuffer
if buffer.lineCount == 0 then
s
else
let tabStop := s.config.tabStop
let (sRow, sCol) := s.getScroll
let cursor := s.getCursor
let newTop := if rows > sRow.val then 0 else sRow.val - rows
let visRow := if cursor.row.val >= sRow.val then cursor.row.val - sRow.val else 0
let 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 count
s.scrollDownRows rows
def EditorState.scrollHalfPageUp (s : EditorState) (count : Nat := 0) : EditorState :=
let rows := if count == 0 then halfPageRows s else count
s.scrollUpRows rows
def EditorState.scrollPageDown (s : EditorState) (count : Nat := 0) : EditorState :=
let rows := if count == 0 then fullPageRows s else count
s.scrollDownRows rows
def EditorState.scrollPageUp (s : EditorState) (count : Nat := 0) : EditorState :=
let rows := if count == 0 then fullPageRows s else count
s.scrollUpRows rows
def EditorState.scrollWindowDown (s : EditorState) (count : Nat := 1) : EditorState :=
let rows := if count == 0 then 1 else count
s.scrollDownRows rows
def EditorState.scrollWindowUp (s : EditorState) (count : Nat := 1) : EditorState :=
let rows := if count == 0 then 1 else count
s.scrollUpRows rows
def 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' then
loop rest (col + ViE.Unicode.charWidthAt tabStop col c)
else
col
loop line.toList 0
def EditorState.moveToScreenTop (s : EditorState) (count : Nat := 0) : EditorState :=
let buffer := s.getActiveBuffer
if buffer.lineCount == 0 then
s
else
let n := if count == 0 then 1 else count
let rowsInView := visibleContentRows s
let offset := min (n - 1) (rowsInView - 1)
let (sRow, _) := s.getScroll
let rowVal := min (sRow.val + offset) (buffer.lineCount - 1)
let tabStop := s.config.tabStop
let lineStr := lineString buffer ⟨rowVal⟩
let colVal := firstNonBlankColWithTabStop lineStr tabStop
s.setCursor { row := ⟨rowVal⟩, col := ⟨colVal⟩ }
def EditorState.moveToScreenMiddle (s : EditorState) : EditorState :=
let buffer := s.getActiveBuffer
if buffer.lineCount == 0 then
s
else
let rowsInView := visibleContentRows s
let offset := rowsInView / 2
let (sRow, _) := s.getScroll
let rowVal := min (sRow.val + offset) (buffer.lineCount - 1)
let tabStop := s.config.tabStop
let lineStr := lineString buffer ⟨rowVal⟩
let colVal := firstNonBlankColWithTabStop lineStr tabStop
s.setCursor { row := ⟨rowVal⟩, col := ⟨colVal⟩ }
def EditorState.moveToScreenBottom (s : EditorState) (count : Nat := 0) : EditorState :=
let buffer := s.getActiveBuffer
if buffer.lineCount == 0 then
s
else
let n := if count == 0 then 1 else count
let rowsInView := visibleContentRows s
let fromBottom := min (n - 1) (rowsInView - 1)
let offset := (rowsInView - 1) - fromBottom
let (sRow, _) := s.getScroll
let rowVal := min (sRow.val + offset) (buffer.lineCount - 1)
let tabStop := s.config.tabStop
let lineStr := lineString buffer ⟨rowVal⟩
let colVal := firstNonBlankColWithTabStop lineStr tabStop
s.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 then
none
else
let c := charAtDisplayCol tabStop lineStr ⟨col⟩
if c == target then
some col
else
let next := ViE.Unicode.nextDisplayColWithTabStop lineStr tabStop col
if next <= col then none else findCharForwardOnLineCore tabStop lineStr lineLen next target fuel
private 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 then
some col
else if col == 0 then
none
else
let prev := ViE.Unicode.prevDisplayColWithTabStop lineStr tabStop col
if prev >= col then none else findCharBackwardOnLineCore tabStop lineStr prev target fuel
def EditorState.findCharMotion (s : EditorState) (target : Char) (forward : Bool) (till : Bool)
(count : Nat := 1) (updateLast : Bool := true) : EditorState :=
let buffer := s.getActiveBuffer
let cursor := s.getCursor
let tabStop := s.config.tabStop
let lineStr := lineString buffer cursor.row
let lineLen := lineDisplayWidth tabStop buffer cursor.row
if lineLen == 0 then
s
else
let n := if count == 0 then 1 else count
let rec findNth (fromCol : Nat) (remaining : Nat) (fuel : Nat) : Option Nat :=
match remaining with
| 0 => some fromCol
| rem + 1 =>
if fuel == 0 then
none
else
let found :=
if forward then
let start := ViE.Unicode.nextDisplayColWithTabStop lineStr tabStop fromCol
if start >= lineLen then none
else findCharForwardOnLineCore tabStop lineStr lineLen start target (lineLen + 1)
else
if fromCol == 0 then none
else
let start := ViE.Unicode.prevDisplayColWithTabStop lineStr tabStop fromCol
if start >= lineLen then none
else findCharBackwardOnLineCore tabStop lineStr start target (lineLen + 1)
match found with
| none => none
| some hit =>
if rem == 0 then
some hit
else
findNth hit rem fuel.pred
termination_by remaining + fuel
match findNth cursor.col.val n (lineLen + n + 1) with
| none => { s with message := s!"Pattern not found: {target}" }
| some hit =>
let hitCol :=
if till then
if forward then
ViE.Unicode.prevDisplayColWithTabStop lineStr tabStop hit
else
ViE.Unicode.nextDisplayColWithTabStop lineStr tabStop hit
else
hit
let hitCol := min hitCol lineLen
let s' := s.setCursor { cursor with col := ⟨hitCol⟩ }
if updateLast then
{
s' with
inputState := {
s'.inputState with
lastFindChar := some target
lastFindForward := forward
lastFindTill := till
}
}
else
s'
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.lastFindForward
let forward := if reverse then !baseForward else baseForward
let count :=
if s.inputState.lastFindTill && !reverse then 2 else 1
s.findCharMotion target forward s.inputState.lastFindTill count false
def EditorState.wordUnderCursor (s : EditorState) : Option String :=
let buffer := s.getActiveBuffer
let cursor := s.getCursor
let tabStop := s.config.tabStop
let lineStr := lineString buffer cursor.row
let lineLen := lineDisplayWidth tabStop buffer cursor.row
let isWordChar (c : Char) : Bool := c.isAlphanum || c == '_'
if lineLen == 0 || cursor.col.val >= lineLen then
none
else
let curChar := charAtDisplayCol tabStop lineStr cursor.col
if !isWordChar curChar then
none
else
let rec expandLeft (col : Col) (fuel : Nat) : Col :=
match fuel with
| 0 => col
| fuel + 1 =>
if col.val == 0 then
col
else
let prev := prevCol tabStop buffer cursor.row col
let c := charAtDisplayCol tabStop lineStr prev
if isWordChar c then expandLeft prev fuel else col
let rec expandRightExclusive (col : Col) (fuel : Nat) : Nat :=
match fuel with
| 0 => col.val
| fuel + 1 =>
let next := nextCol tabStop buffer cursor.row col
if next.val >= lineLen then
lineLen
else
let c := charAtDisplayCol tabStop lineStr next
if isWordChar c then
expandRightExclusive next fuel
else
next.val
let start := expandLeft cursor.col (lineLen + 1)
let endExclusive := expandRightExclusive cursor.col (lineLen + 1)
if endExclusive <= start.val then
none
else
let sub := ViE.Unicode.dropByDisplayWidthWithTabStop lineStr.toRawSubstring tabStop start.val
some (ViE.Unicode.takeByDisplayWidthWithTabStop sub tabStop (endExclusive - start.val))
def EditorState.jumpMatchingBracket (s : EditorState) : EditorState :=
let buffer := s.getActiveBuffer
let cursor := s.getCursor
let tabStop := s.config.tabStop
let lineLen := lineDisplayWidth tabStop buffer cursor.row
if lineLen == 0 || cursor.col.val >= lineLen then
{ s with message := "No bracket under cursor" }
else
let here := charAtDisplayCol tabStop (lineString buffer cursor.row) cursor.col
let 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)
| _ => none
match 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 0
let bytes := buffer.table.toString.toUTF8
if off >= bytes.size then
{ s with message := "No bracket match" }
else
let rec scanForward (i : Nat) (depth : Nat) (fuel : Nat) : Option Nat :=
match fuel with
| 0 => none
| fuel + 1 =>
if i >= bytes.size then
none
else
let b := bytes[i]!
let depth :=
if b == openB then depth + 1
else if b == closeB then depth - 1
else depth
if depth == 0 then some i else scanForward (i + 1) depth fuel
termination_by fuel
let rec scanBackward (i : Nat) (depth : Nat) (fuel : Nat) : Option Nat :=
match fuel with
| 0 => none
| fuel + 1 =>
if i == 0 then
none
else
let j := i - 1
let b := bytes[j]!
let depth :=
if b == closeB then depth + 1
else if b == openB then depth - 1
else depth
if depth == 0 then some j else scanBackward j depth fuel
termination_by fuel
let hit :=
if forward then
scanForward (off + 1) 1 (bytes.size + 1)
else
scanBackward off 1 (bytes.size + 1)
match hit with
| none => { s with message := "No bracket match" }
| some j =>
let p := ViE.getPointFromOffsetInBufferWithTabStop buffer j tabStop
s.setCursor p
def isKeyword (c : Char) : Bool :=
c.isAlphanum || c == '_'
/-- Upper bound used to guarantee termination for word-movement scans. -/
def wordMoveFuel (buffer : FileBuffer) : Nat :=
max 1 (buffer.table.length + FileBuffer.lineCount buffer + 1)
mutual
def findNextForwardCore (tabStop : Nat) (buffer : FileBuffer) (row : Row) (col : Col) (started : Bool) (fuel : Nat) : Row × Col :=
match fuel with
| 0 => (row, col)
| fuel + 1 =>
let lineLen := lineDisplayWidth tabStop buffer row
if lineLen == 0 then
(row, 0) -- Stop at empty line
else if col.val >= lineLen then
-- End of line, wrap to next line
if row.val + 1 < FileBuffer.lineCount buffer then
let nextRow := row.succ
let nextLen := lineDisplayWidth tabStop buffer nextRow
if nextLen == 0 then
(nextRow, 0) -- Stop at empty line
else
findNextForwardCore tabStop buffer nextRow 0 true fuel
else
(row, col) -- End of file
else
let lineStr := lineString buffer row
let c := charAtDisplayCol tabStop lineStr col
let isKw := isKeyword c
let isSpace := c.isWhitespace
if !started then
if isSpace then
findNextForwardCore tabStop buffer row (nextCol tabStop buffer row col) true fuel
else
findNextWordEndForwardCore tabStop buffer row (nextCol tabStop buffer row col) isKw fuel
else
if isSpace then
findNextForwardCore tabStop buffer row (nextCol tabStop buffer row col) started fuel
else
(row, col) -- Found start of next word
def findNextWordEndForwardCore (tabStop : Nat) (buffer : FileBuffer) (row : Row) (col : Col) (wasTv : Bool) (fuel : Nat) : Row × Col :=
match fuel with
| 0 => (row, col)
| fuel + 1 =>
let lineLen := lineDisplayWidth tabStop buffer row
if col.val >= lineLen then
if row.val + 1 < FileBuffer.lineCount buffer then
findNextForwardCore tabStop buffer row.succ 0 true fuel
else
(row, col)
else
let lineStr := lineString buffer row
let c := charAtDisplayCol tabStop lineStr col
let isKw := isKeyword c
let isSpace := c.isWhitespace
if isSpace then
findNextForwardCore tabStop buffer row (nextCol tabStop buffer row col) true fuel
else if isKw != wasTv then
(row, col)
else
findNextWordEndForwardCore tabStop buffer row (nextCol tabStop buffer row col) wasTv fuel
end
def findNextForward (tabStop : Nat) (buffer : FileBuffer) (row : Row) (col : Col) (started : Bool) : Row × Col :=
findNextForwardCore tabStop buffer row col started (wordMoveFuel buffer)
def findNextWordEndForward (tabStop : Nat) (buffer : FileBuffer) (row : Row) (col : Col) (wasTv : Bool) : Row × Col :=
findNextWordEndForwardCore tabStop buffer row col wasTv (wordMoveFuel buffer)
/-- Move forward to start of next word (w) -/
def EditorState.moveWordForward (s : EditorState) : EditorState :=
let tabStop := s.config.tabStop
let buffer := s.getActiveBuffer
let cursor := s.getCursor
let currLineLen := lineDisplayWidth tabStop buffer cursor.row
let (r, c) := if currLineLen == 0 then
if cursor.row.val + 1 < FileBuffer.lineCount buffer then
findNextForward tabStop buffer cursor.row.succ 0 true
else
(cursor.row, 0)
else
let lineStr := lineString buffer cursor.row
if cursor.col.val < currLineLen then
let ch := charAtDisplayCol tabStop lineStr cursor.col
let isKw := isKeyword ch
let isSpace := ch.isWhitespace
if isSpace then
findNextForward tabStop buffer cursor.row (nextCol tabStop buffer cursor.row cursor.col) true
else
findNextWordEndForward tabStop buffer cursor.row (nextCol tabStop buffer cursor.row cursor.col) isKw
else
if cursor.row.val + 1 < FileBuffer.lineCount buffer then
findNextForward tabStop buffer cursor.row.succ 0 true
else
(cursor.row, ⟨currLineLen⟩)
let finalLineLen := lineDisplayWidth tabStop buffer r
let finalColVal := if r.val + 1 >= FileBuffer.lineCount buffer && c.val >= finalLineLen && finalLineLen > 0 && s.mode != Mode.insert then
finalLineLen - 1
else
min c.val finalLineLen
s.setCursor { row := r, col := ⟨finalColVal⟩ }
mutual
def findPrevStartCore (tabStop : Nat) (buffer : FileBuffer) (row : Row) (col : Col) (fuel : Nat) : Row × Col :=
match fuel with
| 0 => (row, col)
| fuel + 1 =>
let lineStr := lineString buffer row
let c := charAtDisplayCol tabStop lineStr col
if c.isWhitespace then
if col.val == 0 then
if row.val > 0 then
let prevRow := row.pred
let prevLen := lineDisplayWidth tabStop buffer prevRow
if prevLen > 0 then findPrevStartCore tabStop buffer prevRow ⟨prevLen - 1⟩ fuel else (prevRow, 0)
else (0, 0)
else
findPrevStartCore tabStop buffer row (prevCol tabStop buffer row col) fuel
else
let isKw := isKeyword c
consumeWordBackwardCore tabStop buffer row col isKw fuel
def consumeWordBackwardCore (tabStop : Nat) (buffer : FileBuffer) (row : Row) (col : Col) (wantKw : Bool) (fuel : Nat) : Row × Col :=
match fuel with
| 0 => (row, col)
| fuel + 1 =>
let lineStr := lineString buffer row
let c := charAtDisplayCol tabStop lineStr col
let isKw := isKeyword c
if c.isWhitespace || isKw != wantKw then
(row, nextCol tabStop buffer row col)
else
if col.val == 0 then (row, 0)
else consumeWordBackwardCore tabStop buffer row (prevCol tabStop buffer row col) wantKw fuel
end
def findPrevStart (tabStop : Nat) (buffer : FileBuffer) (row : Row) (col : Col) : Row × Col :=
findPrevStartCore tabStop buffer row col (wordMoveFuel buffer)
def consumeWordBackward (tabStop : Nat) (buffer : FileBuffer) (row : Row) (col : Col) (wantKw : Bool) : Row × Col :=
consumeWordBackwardCore tabStop buffer row col wantKw (wordMoveFuel buffer)
/-- Move backward to start of previous word (b) -/
def EditorState.moveWordBackward (s : EditorState) : EditorState :=
let tabStop := s.config.tabStop
let buffer := s.getActiveBuffer
let cursor := s.getCursor
if cursor.row.val == 0 && cursor.col.val == 0 then s
else
let lineLen := lineDisplayWidth tabStop buffer cursor.row
let startCol : Col := if cursor.col.val >= lineLen then ⟨lineLen⟩ else cursor.col
let (r, c) :=
if startCol.val == 0 then
if cursor.row.val > 0 then
let prevRow := cursor.row.pred
let prevLen := lineDisplayWidth tabStop buffer prevRow
if prevLen > 0 then
findPrevStart tabStop buffer prevRow ⟨prevLen - 1⟩
else
(prevRow, 0)
else (0, 0)
else
findPrevStart tabStop buffer cursor.row (prevCol tabStop buffer cursor.row startCol)
s.setCursor { row := r, col := c }
mutual
def findNextEndCore (tabStop : Nat) (buffer : FileBuffer) (row : Row) (col : Col) (fuel : Nat) : Row × Col :=
match fuel with
| 0 => (row, col)
| fuel + 1 =>
let lineLen := lineDisplayWidth tabStop buffer row
if col.val >= lineLen then
if row.val + 1 < FileBuffer.lineCount buffer then
findNextEndCore tabStop buffer row.succ 0 fuel
else
(row, col)
else
let lineStr := lineString buffer row
let c := charAtDisplayCol tabStop lineStr col
if c.isWhitespace then
findNextEndCore tabStop buffer row (nextCol tabStop buffer row col) fuel
else
let isKw := isKeyword c
consumeWordToEndCore tabStop buffer row col isKw fuel
def consumeWordToEndCore (tabStop : Nat) (buffer : FileBuffer) (row : Row) (col : Col) (wantKw : Bool) (fuel : Nat) : Row × Col :=
match fuel with
| 0 => (row, col)
| fuel + 1 =>
let lineLen := lineDisplayWidth tabStop buffer row
let nextColVal := (nextCol tabStop buffer row col).val
if nextColVal >= lineLen then
(row, col)
else
let lineStr := lineString buffer row
let nextC := charAtDisplayCol tabStop lineStr (nextCol tabStop buffer row col)
let nextKw := isKeyword nextC
if nextC.isWhitespace || nextKw != wantKw then
(row, col)
else
consumeWordToEndCore tabStop buffer row (nextCol tabStop buffer row col) wantKw fuel
end
def findNextEnd (tabStop : Nat) (buffer : FileBuffer) (row : Row) (col : Col) : Row × Col :=
findNextEndCore tabStop buffer row col (wordMoveFuel buffer)
def consumeWordToEnd (tabStop : Nat) (buffer : FileBuffer) (row : Row) (col : Col) (wantKw : Bool) : Row × Col :=
consumeWordToEndCore tabStop buffer row col wantKw (wordMoveFuel buffer)
/-- Move to end of word (e) -/
def EditorState.moveWordEnd (s : EditorState) : EditorState :=
let tabStop := s.config.tabStop
let buffer := s.getActiveBuffer
let cursor := s.getCursor
let currLineLen := lineDisplayWidth tabStop buffer cursor.row
let startCol : Col := nextCol tabStop buffer cursor.row cursor.col
let (r, c) :=
if startCol.val >= currLineLen then
if cursor.row.val + 1 < FileBuffer.lineCount buffer then
findNextEnd tabStop buffer cursor.row.succ 0
else
(cursor.row, ⟨currLineLen⟩)
else
findNextEnd tabStop buffer cursor.row startCol
s.setCursor { row := r, col := c }
end ViE