6W6OJAUT3KLOXXKYR4GA3MG4ACLO2T4KX3SGD2KZDX6MYEZYTPHAC leanprover/lean4:v4.27.0
name = "vie"version = "0.1.0"defaultTargets = ["vie"][[lean_lib]]name = "ViE"[[lean_exe]]name = "vie"root = "Main"
{"version": "1.1.0","packagesDir": ".lake/packages","packages": [],"name": "editor","lakeDir": ".lake"}
-- This module serves as the root of the `Editor` library.-- Import modules here that should be built as part of the library.import ViE.Basicimport ViE.Typesimport ViE.Bufferimport ViE.Actionsimport ViE.Commandimport ViE.Keyimport ViE.Stateimport ViE.Terminalimport ViE.UIimport ViE.IOimport ViE.Configimport ViE.Appimport ViE.Loaderimport ViE.Workspace
import ViE.Typesnamespace ViE/-- Resolve a file path relative to workspace -/def Workspace.resolvePath (ws : Workspace) (path : String) : String :=if path.startsWith "/" then-- Absolute path, use as-ispathelse-- Relative path, resolve from workspacematch ws.rootPath with| some root => root ++ "/" ++ path| none => path -- No workspace, use as-isend ViE
import ViE.Typesimport ViE.Buffernamespace ViE.Windowopen ViEdef getAllWindowBounds (l : Layout) (h w : Nat) : List (Nat × Nat × Nat × Nat × Nat) :=let rec traverse (l : Layout) (r c h w : Nat) (acc : List (Nat × Nat × Nat × Nat × Nat)) : List (Nat × Nat × Nat × Nat × Nat) :=match l with| Layout.window id _ => (id, r, c, h, w) :: acc| Layout.hsplit left right ratio =>let leftW := (Float.ofNat w * ratio).toUInt64.toNatlet acc' := traverse left r c h leftW acctraverse right r (c + leftW + 1) h (if w > leftW then w - leftW - 1 else 0) acc'| Layout.vsplit top bottom ratio =>let topH := (Float.ofNat h * ratio).toUInt64.toNatlet acc' := traverse top r c topH w acctraverse bottom (r + topH + 1) c (if h > topH then h - topH - 1 else 0) w acc'traverse l 0 0 h w []def getWindowIds (l : Layout) : List Nat :=match l with| Layout.window id _ => [id]| Layout.hsplit left right _ => getWindowIds left ++ getWindowIds right| Layout.vsplit top bottom _ => getWindowIds top ++ getWindowIds bottom
import ViE.Stateimport ViE.Typesimport ViE.Window.Analysisnamespace ViE.Windowopen ViEdef switchWindow (state : EditorState) (dir : Direction) : EditorState :=let wg := state.getCurrentWorkgrouplet bounds := ViE.Window.getAllWindowBounds wg.layout (if state.windowHeight > 0 then state.windowHeight - 1 else 0) state.windowWidthlet current := bounds.find? (fun (id, _, _, _, _) => id == wg.activeWindowId)match current with| none => state| some (_, r, c, h, w) =>-- Find candidates with orthogonal overlaplet candidates := bounds.filter fun (id, r', c', h', w') =>id != wg.activeWindowId &&match dir with| .left => c' < c && (r' < r + h && r' + h' > r)| .right => c' >= c + w && (r' < r + h && r' + h' > r)| .up => r' < r && (c' < c + w && c' + w' > c)| .down => r' >= r + h && (c' < c + w && c' + w' > c)-- Filter again for adjacency/overlaplet best := candidates.foldl (fun acc p =>let (_, r', c', _, _) := pmatch acc with| none => some p| some (_, br, bc, _, _) =>match dir with| .left => if c' > bc then some p else some acc.get!| .right => if c' < bc then some p else some acc.get!| .up => if r' > br then some p else some acc.get!| .down => if r' < br then some p else some acc.get!) nonematch best with| some (id, _, _, _, _) => state.updateCurrentWorkgroup fun wg => { wg with activeWindowId := id }| none => statedef closeActiveWindow (state : EditorState) : EditorState :=let wg := state.getCurrentWorkgroupmatch wg.layout.remove wg.activeWindowId with| none => { state with shouldQuit := true }| some newLayout =>state.updateCurrentWorkgroup fun wg =>let newIds := ViE.Window.getWindowIds newLayoutlet newActiveId := match newIds with| [] => 0 -- Should not happen if layout valid| h :: _ => h{ wg with layout := newLayout, activeWindowId := newActiveId }def splitWindow (state : EditorState) (direction : Bool) : EditorState :=-- direction: true = vertical (top/bottom), false = horizontal (left/right)let s' := state.updateCurrentWorkgroup fun wg =>let activeId := wg.activeWindowIdlet nextId := wg.nextWindowIdlet currentView := wg.layout.findView activeId |>.getD initialViewlet newView := currentViewlet rec updateLayout (l : Layout) : Layout :=match l with| Layout.window wid v =>if wid == activeId thenif direction then Layout.vsplit (Layout.window wid v) (Layout.window nextId newView) 0.5else Layout.hsplit (Layout.window wid v) (Layout.window nextId newView) 0.5else Layout.window wid v| Layout.hsplit left right r => Layout.hsplit (updateLayout left) (updateLayout right) r| Layout.vsplit top bottom r => Layout.vsplit (updateLayout top) (updateLayout bottom) r{ wg with layout := updateLayout wg.layout, nextWindowId := nextId + 1 }{ s' with message := "Split window" }/-- Result of resize operation: None (not found), Some (layout, handled) -/abbrev ResizeResult := Option (Layout × Bool)def resizeWindow (state : EditorState) (dir : Direction) (amount : Float) : EditorState :=state.updateCurrentWorkgroup fun wg =>let activeId := wg.activeWindowIdlet rec traverse (l : Layout) : ResizeResult :=match l with| Layout.window wid v =>if wid == activeId then some (Layout.window wid v, false) else none| Layout.hsplit left right r =>match traverse left with| some (newLeft, handled) =>if handled then some (Layout.hsplit newLeft right r, true)else-- We are in left child, unhandled.match dir with| .left | .right => -- Horizontal resizelet delta := if dir == .right then amount else -amountlet newR := max 0.05 (min 0.95 (r + delta))some (Layout.hsplit newLeft right newR, true)| _ => some (Layout.hsplit newLeft right r, false)| none =>match traverse right with| some (newRight, handled) =>if handled then some (Layout.hsplit left newRight r, true)else-- We are in right child, unhandled.match dir with| .left | .right => -- Horizontal resize-- Consistent direction: Right (l) -> Increase ratio (move split right). Left (h) -> Decrease ratio (move split left).let delta := if dir == .right then amount else -amountlet newR := max 0.05 (min 0.95 (r + delta))some (Layout.hsplit left newRight newR, true)| _ => some (Layout.hsplit left newRight r, false)| none => none| Layout.vsplit top bottom r =>match traverse top with| some (newTop, handled) =>if handled then some (Layout.vsplit newTop bottom r, true)else-- Top childmatch dir with| .up | .down => -- Vertical resize-- Top child: Down (j) -> Increase ratio (move split down). Up (k) -> Decrease ratio (move split up).let delta := if dir == .down then amount else -amountlet newR := max 0.05 (min 0.95 (r + delta))some (Layout.vsplit newTop bottom newR, true)| _ => some (Layout.vsplit newTop bottom r, false)| none =>match traverse bottom with| some (newBottom, handled) =>if handled then some (Layout.vsplit top newBottom r, true)else-- Bottom childmatch dir with| .up | .down => -- Vertical resize-- Bottom child: Down (j) -> Increase ratio (move split down). Up (k) -> Decrease ratio (move split up).let delta := if dir == .down then amount else -amountlet newR := max 0.05 (min 0.95 (r + delta))some (Layout.vsplit top newBottom newR, true)| _ => some (Layout.vsplit top newBottom r, false)| none => nonematch traverse wg.layout with| some (newLayout, _) => { wg with layout := newLayout }| none => wgdef cycleWindow (state : EditorState) : EditorState :=state.updateCurrentWorkgroup fun wg =>let ids := ViE.Window.getWindowIds wg.layoutif ids.isEmpty then wgelselet current := wg.activeWindowId-- Manual index search to avoid import issues if anylet rec findNext (list : List Nat) (sawCurrent : Bool) : Option Nat :=match list with| [] => if sawCurrent then ids.head? else none| id :: rest =>if sawCurrent then some idelse if id == current then findNext rest trueelse findNext rest falsematch findNext ids false with| some nextId => { wg with activeWindowId := nextId }| none => { wg with activeWindowId := match ids with | [] => wg.activeWindowId | h :: _ => h }def enforceScroll (state : EditorState) : EditorState :=let (h, w) := state.getActiveWindowBounds-- Active window height. Reserve 1 line for status bar?-- UI logic reserves 1 for status bar in split rendering.let linesInView := if h > 1 then h - 1 else 1-- Vertical Scrolllet (sRow, sCol) := state.getScrolllet cursor := state.getCursorlet state :=if cursor.row.val < sRow.val thenstate.setScroll cursor.row sColelse if cursor.row.val >= sRow.val + linesInView thenlet newScrollRow : Row := ⟨sRow.val + linesInView - 1⟩state.setScroll newScrollRow sColelsestate-- Refresh scroll after potential updatelet (sRow, sCol) := state.getScroll-- Horizontal Scrolllet lnWidth := if state.config.showLineNumbers then 4 else 0let colsInView := if w > lnWidth then w - lnWidth else 1let state :=if cursor.col.val < sCol.val thenstate.setScroll sRow cursor.colelse if cursor.col.val >= sCol.val + colsInView thenlet newScrollCol : Col := ⟨sCol.val + colsInView - 1⟩state.setScroll sRow newScrollColelsestatestateend ViE.Window
namespace ViE.Unicode/-- Ranges of characters that have a visual width of 2 (Fullwidth/Wide).Based on EastAsianWidth.txt (Wide or Fullwidth). -/def wideRanges : List (Nat × Nat) := [(0x1100, 0x115F), -- Hangul Jamo(0x2329, 0x232A), -- Angle brackets(0x2E80, 0x303E), -- CJK Radicals / Punctuation(0x3040, 0x3247), -- Hiragana, Katakana, Enclosed(0x3250, 0x4DBF), -- Enclosed CJK / CJK Extension A(0x4E00, 0xA4C6), -- CJK Unified Ideographs / Yi(0xA960, 0xA97C), -- Hangul Jamo Extended-A(0xAC00, 0xD7A3), -- Hangul Syllables(0xF900, 0xFAFF), -- CJK Compatibility Ideographs(0xFE10, 0xFE19), -- Vertical forms(0xFE30, 0xFE6F), -- CJK Compatibility Forms(0xFF01, 0xFF60), -- Fullwidth Forms(0xFFE0, 0xFFE6), -- Fullwidth currency/signs(0x1F300, 0x1F64F), -- Miscellaneous Symbols and Pictographs (Emojis)(0x1F900, 0x1F9FF) -- Supplemental Symbols and Pictographs (some, generic range)]/-- Check if a character code is in a wide range. -/def isWide (c : Nat) : Bool :=wideRanges.any fun (start, stop) => c >= start && c <= stop/-- Get the visual width of a character.Returns 2 for wide characters, 0 for null/combining (simplified), 1 for others. -/def charWidth (c : Char) : Nat :=let n := c.toNatif n == 0 then 0 -- Null is 0 width? Or usually displayed as something?-- Usually invalid in text, but let's say 0.else if n < 0x20 then 2 -- Control characters shown as ^Aelse if n == 0x7F then 2 -- DEL shown as ^?else if isWide n then 2else 1/-- Get the display string for a character.- Control characters (0x00-0x1F) -> "^A" notation.- 0x7F -> "^?"- Others -> character itself as string. -/def getDisplayString (c : Char) : String :=let n := c.toNatif n < 0x20 then-- 0x01 is ^A (A is 0x41). 0x01 + 0x40 = 0x41.-- 0x00 is ^@.let base := (n + 0x40).toUInt8"^" ++ (Char.ofNat base.toNat).toStringelse if n == 0x7F then"^?"elsec.toString/-- Calculate the total visual width of a string. -/def stringWidth (s : String) : Nat :=s.foldl (fun acc c => acc + charWidth c) 0/-- Calculate the total visual width of a substring. -/def substringWidth (s : Substring.Raw) : Nat :=s.foldl (fun acc c => acc + charWidth c) 0/-- Get the byte length of a UTF-8 sequence starting with `b`.Returns 0 if `b` is a continuation byte or invalid start. -/def utf8ByteLength (b : UInt8) : Nat :=if b < 0x80 then 1else if (b &&& 0xE0) == 0xC0 then 2else if (b &&& 0xF0) == 0xE0 then 3else if (b &&& 0xF8) == 0xF0 then 4else 0 -- Invalid or continuation/-- Count newlines in a byte array slice efficiently -/def countNewlines (data : ByteArray) (start : Nat) (len : Nat) : Nat :=let endPos := start + lenlet rec loop (i : Nat) (count : Nat) : Nat :=if i >= endPos then countelseif data[i]! == 10 then loop (i + 1) (count + 1) -- 10 is \nelse loop (i + 1) countloop start 0/-- Count UTF-8 characters in a byte array range. -/def countChars (bytes : ByteArray) (start len : Nat) : Nat :=let stop := start + lenlet rec loop (i : Nat) (cnt : Nat) : Nat :=if i >= stop then cntelselet b := bytes[i]!if (b &&& 0xC0) != 0x80 thenloop (i + 1) (cnt + 1)elseloop (i + 1) cntloop start 0end ViE.Unicode
import ViE.Stateimport ViE.Terminalimport ViE.Configimport ViE.Unicodeimport ViE.Colornamespace ViE.UIopen ViE/-- Pad a string on the left with spaces until it reaches the given width. -/def leftPad (s : String) (width : Nat) : String :=if s.length >= width then selse "".pushn ' ' (width - s.length) ++ s/-- Take characters from substring until visual width limit is reached. -/partial def takeVisual (s : Substring.Raw) (width : Nat) : String :=let rec loop (sub : Substring.Raw) (currW : Nat) (acc : String) : String :=if sub.isEmpty then accelselet c := sub.frontlet w := Unicode.charWidth cif currW + w <= width thenloop (sub.drop 1) (currW + w) (acc ++ Unicode.getDisplayString c)elseaccloop s 0 ""/-- Render the current editor state to the terminal. -/def render (state : EditorState) : IO Unit := do-- Start building the frame bufferlet mut buffer := ""buffer := buffer ++ Terminal.hideCursorStr ++ Terminal.homeCursorStrlet (rows, cols) ← Terminal.getWindowSizelet wg := state.getCurrentWorkgrouplet getBuffer (id : Nat) : FileBuffer :=wg.buffers.find? (fun b => b.id == id) |>.getD initialBufferlet rec renderLayout (l : Layout) (r c h w : Nat) : String := Id.run domatch l with| Layout.window id view =>let workH := if h > 0 then h - 1 else 0let buf := getBuffer view.bufferIdlet mut winBuf := ""for i in [0:workH] dolet lineIdx := view.scrollRow + ilet screenRow := r + i-- Position cursor for this linewinBuf := winBuf ++ Terminal.moveCursorStr screenRow cif lineIdx < FileBuffer.lineCount buf thenif let some lineStr := getLineFromBuffer buf lineIdx thenlet lnWidth := if state.config.showLineNumbers then 4 else 0let availableWidth := if w > lnWidth then w - lnWidth else 0let sub := lineStr.toRawSubstring.drop view.scrollCol.vallet displayLine := takeVisual sub availableWidthif state.config.showLineNumbers thenwinBuf := winBuf ++ s!"{leftPad (toString (lineIdx + 1)) 3} "let isVisual := state.mode == .visual || state.mode == .visualBlocklet selRange := if isVisual then state.selectionStart else nonematch selRange with| none => winBuf := winBuf ++ displayLine| some _ =>let mut styleActive := false-- For highlighting, simpler to just iterate over result string or re-calculate-- This visual highlighting logic implies `displayLine` is the raw chars roughly.-- But `takeVisual` expands tabs/controls!-- Logic below assumes 1:1 char mapping which might be flawed if `takeVisual` changes string (e.g. ^A).-- Leaving visual highlight logic "as is" but it iterates `displayLine.toList`.-- `displayLine` is a String, so `toList` is fine (though allocs). Visual mode perf is secondary to normal nav.let chars := displayLine.toListlet mut idx := 0for c in chars dolet col := view.scrollCol.val + idxlet isSelected := state.isInSelection lineIdx colif isSelected && !styleActive thenwinBuf := winBuf ++ "\x1b[7m" -- Inverse videostyleActive := trueelse if !isSelected && styleActive thenwinBuf := winBuf ++ "\x1b[0m"styleActive := falsewinBuf := winBuf ++ c.toStringidx := idx + 1if styleActive then winBuf := winBuf ++ "\x1b[0m"elsewinBuf := winBuf ++ state.config.emptyLineMarker-- Clear the rest of the line to ensure clean overwritewinBuf := winBuf ++ Terminal.clearLineStrlet statusRow := r + workHwinBuf := winBuf ++ Terminal.moveCursorStr statusRow clet fileName := buf.filename.getD "[No Name]"let modeStr := if id == wg.activeWindowId then s!"-- {state.mode} --" else "--"let eolMark := if buf.missingEol then " [noeol]" else ""let statusStr := s!"{modeStr} {fileName}{eolMark} [W:{id} B:{view.bufferId}] [WG:{state.currentGroup}] {state.workspace.displayName}"-- Ensure status line also clears restwinBuf := winBuf ++ state.config.statusBarStyle ++ statusStr.trimAscii ++ Terminal.clearLineStr ++ state.config.resetStyle-- Cursor drawing is separate (at end of frame)winBuf| Layout.hsplit left right ratio =>let leftW := (Float.ofNat w * ratio).toUInt64.toNatlet leftStr := renderLayout left r c h leftW-- Draw vertical separatorlet mut sepStr := ""if w > leftW thenlet sepCol := c + leftWfor i in [0:h] dosepStr := sepStr ++ Terminal.moveCursorStr (r + i) sepCol ++ state.config.vSplitStrlet rightStr := renderLayout right r (c + leftW + 1) h (if w > leftW then w - leftW - 1 else 0)leftStr ++ sepStr ++ rightStr| Layout.vsplit top bottom ratio =>let topH := (Float.ofNat h * ratio).toUInt64.toNatlet topStr := renderLayout top r c topH w-- Draw horizontal separatorlet mut sepStr := ""if h > topH thenlet sepRow := r + topHsepStr := sepStr ++ Terminal.moveCursorStr sepRow cfor _ in [0:w] dosepStr := sepStr ++ state.config.hSplitStrlet bottomStr := renderLayout bottom (r + topH + 1) c (if h > topH then h - topH - 1 else 0) wtopStr ++ sepStr ++ bottomStrlet layoutH := rows - 1buffer := buffer ++ renderLayout wg.layout 0 0 layoutH cols-- Global Status / Command Linebuffer := buffer ++ Terminal.moveCursorStr (rows - 1) 0let statusRight :=if state.mode == .command then s!":{state.inputState.commandBuffer}"else state.messagebuffer := buffer ++ statusRight ++ Terminal.clearLineStr-- Set Physical Cursorlet rec getCursorPos (l : Layout) (r c h w : Nat) : Option (Nat × Nat) :=match l with| Layout.window id view =>if id == wg.activeWindowId thenlet buf := getBuffer view.bufferIdlet workH := if h > 0 then h - 1 else 0let colOffset := if state.config.showLineNumbers then 4 else 0let visualRow := view.cursor.row.val - view.scrollRow.valif visualRow >= 0 && visualRow < workH thenif let some currentLineStr := getLineFromBuffer buf view.cursor.row.val thenlet preCursor := (currentLineStr.toRawSubstring.drop view.scrollCol.val).take (view.cursor.col.val - view.scrollCol.val)let visualCol := ViE.Unicode.substringWidth preCursorif (visualCol + colOffset) < w thensome (r + visualRow, c + visualCol + colOffset)else noneelse noneelse noneelse none| Layout.hsplit left right ratio =>let leftW := (Float.ofNat w * ratio).toUInt64.toNat(getCursorPos left r c h leftW).orElse (fun _ => getCursorPos right r (c + leftW + 1) h (if w > leftW then w - leftW - 1 else 0))| Layout.vsplit top bottom ratio =>let topH := (Float.ofNat h * ratio).toUInt64.toNat(getCursorPos top r c topH w).orElse (fun _ => getCursorPos bottom (r + topH + 1) c (if h > topH then h - topH - 1 else 0) w)buffer := buffer ++match getCursorPos wg.layout 0 0 layoutH cols with| some (pr, pc) => Terminal.moveCursorStr pr pc| none => "" -- Cursor hidden or out of viewif state.mode == .command thenbuffer := buffer ++ Terminal.moveCursorStr (rows - 1) (1 + state.inputState.commandBuffer.length)if state.mode == .visual || state.mode == .visualBlock thenbuffer := buffer ++ Terminal.hideCursorStrelsebuffer := buffer ++ Terminal.showCursorStr-- Output everythingIO.print buffer(← IO.getStdout).flushend ViE.UI
import ViE.Data.PieceTablenamespace ViEopen ViE.PieceTableinductive Mode where| normal| insert| command| visual| visualBlockderiving Repr, BEq, Inhabitedinstance : ToString Mode wheretoString| .normal => "NORMAL"| .insert => "INSERT"| .command => "COMMAND"| .visual => "VISUAL"| .visualBlock => "VISUAL BLOCK"/-- Workspace configuration -/structure Workspace whererootPath : Option String := nonedisplayName : String := "Untitled"deriving Repr/-- Row index (0-based) with dimensional safety -/structure Row whereval : Natderiving Repr, BEq, Ord/-- Column index (0-based) with dimensional safety -/structure Col whereval : Natderiving Repr, BEq, Ord/-- Cursor position with dimensional type safety -/structure Point whererow : Rowcol : Colderiving Repr, BEq-- Row helper functionsnamespace Rowdef zero : Row := ⟨0⟩def succ (r : Row) : Row := ⟨r.val + 1⟩def pred (r : Row) : Row := ⟨r.val.pred⟩@[simp] def add (r : Row) (n : Nat) : Row := ⟨r.val + n⟩@[simp] def sub (r : Row) (n : Nat) : Row := ⟨r.val - n⟩instance : OfNat Row n := ⟨⟨n⟩⟩instance : LT Row := ⟨fun a b => a.val < b.val⟩instance : LE Row := ⟨fun a b => a.val ≤ b.val⟩instance (a b : Row) : Decidable (a < b) := inferInstanceAs (Decidable (a.val < b.val))instance (a b : Row) : Decidable (a ≤ b) := inferInstanceAs (Decidable (a.val ≤ b.val))instance : Min Row := ⟨fun a b => if a.val ≤ b.val then a else b⟩instance : Max Row := ⟨fun a b => if a.val ≥ b.val then a else b⟩instance : HAdd Row Nat Row := ⟨fun r n => ⟨r.val + n⟩⟩instance : HAdd Row Nat Nat := ⟨fun r n => r.val + n⟩instance : HSub Row Row Nat := ⟨fun a b => a.val - b.val⟩instance : HSub Row Nat Row := ⟨fun r n => ⟨r.val - n⟩⟩instance : HSub Row Nat Nat := ⟨fun r n => r.val - n⟩instance : ToString Row := ⟨fun r => toString r.val⟩end Row-- Col helper functionsnamespace Coldef zero : Col := ⟨0⟩def succ (c : Col) : Col := ⟨c.val + 1⟩def pred (c : Col) : Col := ⟨c.val.pred⟩@[simp] def add (c : Col) (n : Nat) : Col := ⟨c.val + n⟩@[simp] def sub (c : Col) (n : Nat) : Col := ⟨c.val - n⟩instance : OfNat Col n := ⟨⟨n⟩⟩instance : LT Col := ⟨fun a b => a.val < b.val⟩instance : LE Col := ⟨fun a b => a.val ≤ b.val⟩instance (a b : Col) : Decidable (a < b) := inferInstanceAs (Decidable (a.val < b.val))instance (a b : Col) : Decidable (a ≤ b) := inferInstanceAs (Decidable (a.val ≤ b.val))instance : Min Col := ⟨fun a b => if a.val ≤ b.val then a else b⟩instance : Max Col := ⟨fun a b => if a.val ≥ b.val then a else b⟩instance : HAdd Col Nat Col := ⟨fun c n => ⟨c.val + n⟩⟩instance : HAdd Col Nat Nat := ⟨fun c n => c.val + n⟩instance : HSub Col Nat Col := ⟨fun c n => ⟨c.val - n⟩⟩instance : HSub Col Col Nat := ⟨fun a b => a.val - b.val⟩instance : ToString Col := ⟨fun c => toString c.val⟩end Col-- Point helper functionsnamespace Pointdef zero : Point := ⟨Row.zero, Col.zero⟩def make (row col : Nat) : Point := ⟨⟨row⟩, ⟨col⟩⟩end Point-- Inhabited instancesinstance : Inhabited Row := ⟨Row.zero⟩instance : Inhabited Col := ⟨Col.zero⟩instance : Inhabited Point := ⟨Point.zero⟩/-- A line is an array of characters for O(1) indexing -/abbrev Line := Array Char/-- A text buffer is an array of lines (2D character array) -/abbrev TextBuffer := Array Linestructure EditorConfig whereshowLineNumbers : BoolvSplitStr : StringhSplitStr : StringemptyLineMarker : StringstatusBarStyle : StringresetStyle : StringfileIcon : StringdirIcon : Stringderiving Repr, Inhabitedinductive Key where| char (c : Char)| ctrl (c : Char)| alt (c : Char)| esc| backspace| enter| left | right | up | down| unknown (c : Char)deriving Repr, BEq, Inhabitedstructure FileBuffer whereid : Natfilename : Option Stringdirty : Booltable : PieceTablemissingEol : Boolderiving Inhabited-- Manual Repr instanceinstance : Repr FileBuffer wherereprPrec buf _ :=s!"FileBuffer(id={buf.id}, file={buf.filename}, dirty={buf.dirty}, lines={PieceTree.lineBreaks buf.table.tree + 1})"def initialFileBuffer : FileBuffer := {id := 0filename := nonedirty := falsetable := PieceTable.fromString ""missingEol := false}namespace FileBufferdef lineCount (buf : FileBuffer) : Nat :=PieceTree.lineBreaks buf.table.tree + 1end FileBufferstructure ViewState wherebufferId : Natcursor : PointscrollRow : RowscrollCol : Colderiving Repr, Inhabited/-- File entry for explorer -/structure FileEntry wherename : Stringpath : StringisDirectory : Boolderiving Repr, Inhabited/-- Explorer state for file navigation -/structure ExplorerState wherecurrentPath : Stringentries : List FileEntryselectedIndex : Natderiving Repr, Inhabitedinductive Layout where| window (id : Nat) (view : ViewState)| hsplit (left right : Layout) (ratio : Float)| vsplit (top bottom : Layout) (ratio : Float)deriving Repr, Inhabited/-- Temporary input state that doesn't need to persist -/structure InputState wherepreviousKey : Option CharcountBuffer : StringcommandBuffer : StringpendingKeys : StringlastInputTime : Natderiving Repr, Inhabited/-- State for a single workgroup (independent buffer set and layout) -/structure WorkgroupState wherebuffers : List FileBuffernextBufferId : Natlayout : LayoutactiveWindowId : NatnextWindowId : Natderiving Repr, Inhabitedstructure EditorState wheremode : Mode-- Workgroup managementworkgroups : Array WorkgroupStatecurrentGroup : Nat-- Global Statemessage : StringshouldQuit : Boolconfig : EditorConfigworkspace : Workspaceclipboard : Option TextBuffer -- Yank bufferselectionStart : Option Point -- Visual mode anchorexplorers : List (Nat × ExplorerState) -- BufferId × Explorer state-- Temporary input stateinputState : InputState-- UI dimensions (updated frequently)windowHeight : NatwindowWidth : Nat-- Rendering optimizationdirty : Bool/-- Key mapping function type. -/structure KeyMap wherenormal : EditorState → Key → IO EditorStateinsert : EditorState → Key → IO EditorStatecommand : EditorState → Key → IO EditorStatevisual : EditorState → Key → IO EditorStatevisualBlock : EditorState → Key → IO EditorState/-- Command action type: arguments -> state -> IO new state -/abbrev CommandAction := List String → EditorState → IO EditorState/-- Map of command names to actions -/abbrev CommandMap := List (String × CommandAction)/-- The complete user configuration. -/structure Config wheresettings : EditorConfigbindings : KeyMapcommands : CommandMapinductive Direction where| left| right| up| downderiving Repr, BEq, Inhabitedstructure SessionState wherefiles : List StringactiveFileIndex : Natcursors : List (Nat × Nat) -- Row, Col per filederiving Repr, Inhabitedend ViE
import ViE.Unicodenamespace ViE.Terminal/-- Enable raw mode using stty.This relies on the availability of the `stty` command in the environment. -/def enableRawMode : IO Unit := do-- We ignore the output.-- Explicitly target /dev/tty because IO.Process might redirect stdin-- min 0 time 0 for non-blocking readlet _ ← IO.Process.run { cmd := "stty", args := #["-F", "/dev/tty", "raw", "-echo", "min", "0", "time", "0"] }return ()/-- Disable raw mode using stty. -/def disableRawMode : IO Unit := dolet _ ← IO.Process.run { cmd := "stty", args := #["-F", "/dev/tty", "sane"] }return ()/-- Clear the screen and position cursor at top-left. -/def clearScreen : IO Unit := doIO.print "\x1b[2J"IO.print "\x1b[H"/-- Get window size (rows, cols) using stty size. -/def getWindowSize : IO (Nat × Nat) := dolet out ← IO.Process.run { cmd := "stty", args := #["-F", "/dev/tty", "size"] }let parts := out.trimAscii.toString.splitOn " "match parts with| [rows, cols] =>let r := rows.toNat!let c := cols.toNat!return (r, c)| _ => return (24, 80) -- Fallback/-- Move cursor to specific row and column (0-indexed). -/def moveCursor (row col : Nat) : IO Unit := do-- ANSI escape codes are 1-indexed.IO.print s!"\x1b[{row + 1};{col + 1}H"def moveCursorStr (row col : Nat) : String :=s!"\x1b[{row + 1};{col + 1}H"/-- Hide cursor. -/def hideCursor : IO Unit := doIO.print "\x1b[?25l"def hideCursorStr : String := "\x1b[?25l"/-- Show cursor. -/def showCursor : IO Unit := doIO.print "\x1b[?25h"def showCursorStr : String := "\x1b[?25h"/-- Clear from cursor to end of line. -/def clearLineStr : String := "\x1b[K"/-- Home cursor. -/def homeCursorStr : String := "\x1b[H"/-- Read a single byte from stdin.Returns none on EOF or if no input available. -/def readKey : IO (Option Char) := dolet stdin ← IO.getStdinlet b1_arr ← stdin.read 1if b1_arr.size == 0 thenreturn nonelet b1 := b1_arr[0]!let len := ViE.Unicode.utf8ByteLength b1if len == 1 thenreturn some (Char.ofNat b1.toNat)else if len == 0 then-- Invalid start byte, ignorereturn noneelse-- Read remaining bytes (blocking for completion of valid UTF-8 char)-- If we got the first byte of a multi-byte sequence, the rest should be there immediately.let rest ← stdin.read (len - 1).toUSizeif rest.size != len - 1 then return nonelet b1n := b1.toNatif len == 2 thenlet b2 := rest[0]!.toNatlet u := ((b1n &&& 0x1F) <<< 6) ||| (b2 &&& 0x3F)return some (Char.ofNat u)else if len == 3 thenlet b2 := rest[0]!.toNatlet b3 := rest[1]!.toNatlet u := ((b1n &&& 0x0F) <<< 12) ||| ((b2 &&& 0x3F) <<< 6) ||| (b3 &&& 0x3F)return some (Char.ofNat u)else if len == 4 thenlet b2 := rest[0]!.toNatlet b3 := rest[1]!.toNatlet b4 := rest[2]!.toNatlet u := ((b1n &&& 0x07) <<< 18) ||| ((b2 &&& 0x3F) <<< 12) ||| ((b3 &&& 0x3F) <<< 6) ||| (b4 &&& 0x3F)return some (Char.ofNat u)elsereturn noneend ViE.Terminal
import ViE.State.Configimport ViE.State.Editimport ViE.State.Layoutimport ViE.State.Movementimport ViE.State.Visual
import ViE.State.Configimport ViE.State.Layoutimport ViE.State.Movementimport ViE.Typesimport ViE.Buffer.Contentnamespace ViEdef EditorState.startVisualMode (s : EditorState) : EditorState :=let s' := s.clampCursor{ s' with mode := .visual, selectionStart := some s'.getCursor, message := "-- VISUAL --" }def EditorState.startVisualBlockMode (s : EditorState) : EditorState :=let s' := s.clampCursor{ s' with mode := .visualBlock, selectionStart := some s'.getCursor, message := "-- VISUAL BLOCK --" }def EditorState.exitVisualMode (s : EditorState) : EditorState :={ s with mode := .normal, selectionStart := none, message := "" }def normalizeRange (p1 p2 : Point) : (Point × Point) :=if p1.row < p2.row || (p1.row == p2.row && p1.col <= p2.col) then (p1, p2) else (p2, p1)def EditorState.isInSelection (s : EditorState) (row col : Nat) : Bool :=match s.selectionStart with| none => false| some startPt =>if s.mode == .visualBlock thenlet cursor := s.getCursorlet minRow := min startPt.row cursor.rowlet maxRow := max startPt.row cursor.rowlet minCol := min startPt.col cursor.collet maxCol := max startPt.col cursor.colrow >= minRow.val && row <= maxRow.val && col >= minCol.val && col <= maxCol.valelse if s.mode == .visual thenlet (p1, p2) := normalizeRange startPt s.getCursorif row < p1.row.val || row > p2.row.val then falseelse if row > p1.row.val && row < p2.row.val then trueelse if p1.row.val == p2.row.val then col >= p1.col.val && col <= p2.col.valelse if row == p1.row.val then col >= p1.col.valelse if row == p2.row.val then col <= p2.col.valelse falseelsefalsedef EditorState.getSelectedText (s : EditorState) : TextBuffer :=match s.selectionStart with| none => #[]| some startPt =>if s.mode == .visualBlock thenlet cursor := s.getCursorlet minRow := min startPt.row cursor.rowlet maxRow := max startPt.row cursor.rowlet minCol := min startPt.col cursor.collet maxCol := max startPt.col cursor.collet content := s.activeBufferContent(List.range (maxRow.val - minRow.val + 1)).toArray.map fun i =>let row := minRow.val + ilet line := getLine content row |>.getD ""let chars := line.toListstringToLine (String.ofList (chars.drop minCol.val |>.take (maxCol.val - minCol.val + 1)))elselet (p1, p2) := normalizeRange startPt s.getCursorlet content := s.activeBufferContentif p1.row == p2.row thenlet line := getLine content p1.row.val |>.getD ""let chars := line.toList#[stringToLine (String.ofList (chars.drop p1.col.val |>.take (p2.col.val - p1.col.val + 1)))]elselet firstLine := getLine content p1.row.val |>.getD ""let lastLine := getLine content p2.row.val |>.getD ""let midLines := arrayDrop (arrayTake content p2.row.val) (p1.row.val + 1)let firstPart := stringToLine (String.ofList (firstLine.toList.drop p1.col.val))let lastPart := stringToLine (String.ofList (lastLine.toList.take (p2.col.val + 1)))arrayConcat [#[firstPart], midLines, #[lastPart]]def EditorState.yankSelection (s : EditorState) : EditorState :=let text := s.getSelectedText{ s.exitVisualMode with clipboard := some text, message := s!"Yanked {text.size} lines" }def EditorState.deleteSelection (s : EditorState) : EditorState :=match s.selectionStart with| none => s| some startPt =>if s.mode == .visualBlock thenlet cursor := s.getCursorlet minRow := min startPt.row cursor.rowlet maxRow := max startPt.row cursor.rowlet minCol := min startPt.col cursor.collet maxCol := max startPt.col cursor.collet s' := s.updateActiveBufferContent fun buffer =>buffer.mapIdx fun i lineArr =>let line := lineToString lineArrif i >= minRow.val && i <= maxRow.val thenlet chars := line.toListif minCol.val < chars.length thenlet before := chars.take minCol.vallet after := chars.drop (maxCol.val + 1)stringToLine (String.ofList (before ++ after))elselineArrelselineArr(s'.setCursor (Point.make minRow.val minCol.val)).exitVisualModeelselet (p1, p2) := normalizeRange startPt s.getCursorlet s' := s.updateActiveBufferContent fun buffer =>if p1.row == p2.row thenmodifyLine buffer p1.row.val fun line =>let chars := line.toListlet before := chars.take p1.col.vallet after := chars.drop (p2.col.val + 1)String.ofList (before ++ after)elselet beforeLines := arrayTake buffer p1.row.vallet afterLines := arrayDrop buffer (p2.row.val + 1)let firstLine := getLine buffer p1.row.val |>.getD ""let lastLine := getLine buffer p2.row.val |>.getD ""let newJoinedLine := stringToLine (String.ofList (firstLine.toList.take p1.col.val ++ lastLine.toList.drop (p2.col.val + 1)))arrayConcat [beforeLines, #[newJoinedLine], afterLines](s'.setCursor p1).exitVisualModeend ViE
import ViE.State.Configimport ViE.State.Layoutimport ViE.Typesimport ViE.Buffer.Contentnamespace ViEdef EditorState.getCursor (s : EditorState) : Point :=let wg := s.getCurrentWorkgroupmatch wg.layout.findView wg.activeWindowId with| some view => view.cursor| none => Point.zerodef 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 EditorState.moveCursorLeft (s : EditorState) : EditorState :=let cursor := s.getCursor-- let currentLine := getLine s.activeBufferContent cursor.row.val |>.getD ""if cursor.col.val > 0 thens.setCursor (Point.make cursor.row.val (cursor.col.val - 1))elsesdef EditorState.moveCursorRight (s : EditorState) : EditorState :=let cursor := s.getCursorlet buffer := s.getActiveBufferlet limit := getLineLengthFromBuffer buffer cursor.row.val |>.getD 0if cursor.col.val < limit thens.setCursor (Point.make cursor.row.val (cursor.col.val + 1))elsesdef EditorState.moveCursorUp (s : EditorState) : EditorState :=let cursor := s.getCursorif cursor.row.val > 0 thenlet newRow := cursor.row.val - 1let lineLen := getLineLengthFromBuffer s.getActiveBuffer newRow |>.getD 0s.setCursor (Point.make newRow (min cursor.col.val lineLen))elsesdef EditorState.moveCursorDown (s : EditorState) : EditorState :=let cursor := s.getCursorlet buffer := s.getActiveBufferif cursor.row.val < buffer.lineCount - 1 thenlet newRow := cursor.row.val + 1let lineLen := getLineLengthFromBuffer buffer newRow |>.getD 0s.setCursor (Point.make newRow (min cursor.col.val lineLen))elsesdef 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) sdef EditorState.moveCursorRightN (s : EditorState) (n : Nat) : EditorState :=n.repeat (fun s => s.moveCursorRight) sdef EditorState.moveCursorUpN (s : EditorState) (n : Nat) : EditorState :=n.repeat (fun s => s.moveCursorUp) sdef EditorState.moveCursorDownN (s : EditorState) (n : Nat) : EditorState :=n.repeat (fun s => s.moveCursorDown) sdef EditorState.getCount (s : EditorState) : Nat :=match s.inputState.countBuffer.toNat? with| some n => n| none => 1def EditorState.jumpToColumn (s : EditorState) (col : Nat) : EditorState :=let cursor := s.getCursorlet len := getLineLengthFromBuffer s.getActiveBuffer cursor.row.val |>.getD 0let newCol := min (col - 1) len -- 1-indexed to 0-indexed, clampedlet s' := s.setCursor (Point.make cursor.row.val newCol){ s' with inputState := { s'.inputState with countBuffer := "" } }def EditorState.jumpToLine (s : EditorState) (line : Nat) : EditorState :=let buffer := s.getActiveBufferlet maxLine := buffer.lineCountlet newRow := if line == 0 then 0 else min (line - 1) (maxLine - 1)let lineLen := getLineLengthFromBuffer buffer newRow |>.getD 0let cursor := s.getCursorlet newCol := min cursor.col.val lineLenlet s' := s.setCursor (Point.make newRow newCol){ s' with inputState := { s'.inputState with countBuffer := "" } }def EditorState.moveToLineStart (s : EditorState) : EditorState :=let cursor := s.getCursors.setCursor { cursor with col := 0 }def EditorState.moveToLineEnd (s : EditorState) : EditorState :=let cursor := s.getCursorlet len := getLineLengthFromBuffer s.getActiveBuffer cursor.row.val |>.getD 0s.setCursor (Point.make cursor.row.val len)def EditorState.clampCursor (s : EditorState) : EditorState :=let cursor := s.getCursorlet len := getLineLengthFromBuffer s.getActiveBuffer cursor.row.val |>.getD 0if cursor.col.val > len thens.setCursor (Point.make cursor.row.val len)elsesend ViE
import ViE.State.Configimport ViE.Typesimport ViE.Buffer.Contentnamespace ViE/-- Get current workgroup -/def EditorState.getCurrentWorkgroup (s : EditorState) : WorkgroupState :=s.workgroups[s.currentGroup]!/-- Update current workgroup -/def EditorState.updateCurrentWorkgroup (s : EditorState) (f : WorkgroupState → WorkgroupState) : EditorState :={ s with workgroups := s.workgroups.set! s.currentGroup (f (s.getCurrentWorkgroup)) }/-- Switch to workgroup n -/def EditorState.switchToWorkgroup (s : EditorState) (n : Nat) : EditorState :=if n < 10 then{ s with currentGroup := n }elses/- Helper functions to access/modify the active view -/def Layout.findView (l : Layout) (id : Nat) : Option ViewState :=match l with| .window wid v => if wid == id then some v else none| .hsplit left right _ => (left.findView id).orElse (fun _ => right.findView id)| .vsplit top bottom _ => (top.findView id).orElse (fun _ => bottom.findView id)/-- Update a view in the layout tree. Returns (updated layout, found flag) -/def Layout.updateView (l : Layout) (id : Nat) (f : ViewState → ViewState) : Layout :=let rec update (l : Layout) : Layout × Bool :=match l with| .window wid v =>if wid == id then (.window wid (f v), true) else (l, false)| .hsplit left right ratio =>let (newLeft, found) := update leftif found then(.hsplit newLeft right ratio, true)elselet (newRight, found') := update right(.hsplit newLeft newRight ratio, found')| .vsplit top bottom ratio =>let (newTop, found) := update topif found then(.vsplit newTop bottom ratio, true)elselet (newBottom, found') := update bottom(.vsplit newTop newBottom ratio, found')(update l).1def Layout.remove (l : Layout) (id : Nat) : Option Layout :=match l with| .window wid _ => if wid == id then none else some l| .hsplit left right ratio =>match left.remove id with| none => some right| some newLeft =>match right.remove id with| none => some newLeft| some newRight => some (.hsplit newLeft newRight ratio)| .vsplit top bottom ratio =>match top.remove id with| none => some bottom| some newTop =>match bottom.remove id with| none => some newTop| some newBottom => some (.vsplit newTop newBottom ratio)def EditorState.getActiveBuffer (s : EditorState) : FileBuffer :=let wg := s.getCurrentWorkgroupmatch wg.layout.findView wg.activeWindowId with| some v => wg.buffers.find? (fun b => b.id == v.bufferId) |>.getD initialBuffer| none => initialBufferdef EditorState.activeBufferContent (s : EditorState) : TextBuffer :=ViE.Buffer.fileBufferToTextBuffer s.getActiveBufferdef EditorState.updateActiveBuffer (s : EditorState) (f : FileBuffer -> FileBuffer) : EditorState :=s.updateCurrentWorkgroup fun wg =>let view := wg.layout.findView wg.activeWindowIdmatch view with| some v =>let newBuffers := wg.buffers.map fun b => if b.id == v.bufferId then f b else b{ wg with buffers := newBuffers }| none => wgdef EditorState.updateActiveBufferContent (s : EditorState) (f : TextBuffer → TextBuffer) : EditorState :=s.updateActiveBuffer fun b => ViE.Buffer.fileBufferUpdateFromTextBuffer b (f (ViE.Buffer.fileBufferToTextBuffer b))def EditorState.getScroll (s : EditorState) : Row × Col :=let wg := s.getCurrentWorkgroupmatch wg.layout.findView wg.activeWindowId with| some v => (v.scrollRow, v.scrollCol)| none => (Row.zero, Col.zero)def EditorState.updateActiveView (s : EditorState) (f : ViewState → ViewState) : EditorState :=s.updateCurrentWorkgroup fun wg =>{ wg with layout := wg.layout.updateView wg.activeWindowId f }def EditorState.getActiveWindowBounds (s : EditorState) : Nat × Nat :=let wg := s.getCurrentWorkgrouplet rec findBounds (l : Layout) (h w : Nat) : Option (Nat × Nat) :=match l with| .window id _ => if id == wg.activeWindowId then some (h, w) else none| .hsplit left right ratio =>let leftW := (Float.ofNat w * ratio).toUInt64.toNat(findBounds left h leftW).orElse (fun _ => findBounds right h (if w > leftW then w - leftW - 1 else 0))| .vsplit top bottom ratio =>let topH := (Float.ofNat h * ratio).toUInt64.toNat(findBounds top topH w).orElse (fun _ => findBounds bottom (if h > topH then h - topH - 1 else 0) w)-- Reserve 1 line for global status/commandlet layoutH := if s.windowHeight > 0 then s.windowHeight - 1 else 0(findBounds wg.layout layoutH s.windowWidth).getD (layoutH, s.windowWidth)def getAllWindowBounds (l : Layout) (h w : Nat) : List (Nat × Nat × Nat × Nat × Nat) :=let rec traverse (l : Layout) (r c h w : Nat) (acc : List (Nat × Nat × Nat × Nat × Nat)) : List (Nat × Nat × Nat × Nat × Nat) :=match l with| .window id _ => (id, r, c, h, w) :: acc| .hsplit left right ratio =>let leftW := (Float.ofNat w * ratio).toUInt64.toNatlet acc' := traverse left r c h leftW acctraverse right r (c + leftW + 1) h (if w > leftW then w - leftW - 1 else 0) acc'| .vsplit top bottom ratio =>let topH := (Float.ofNat h * ratio).toUInt64.toNatlet acc' := traverse top r c topH w acctraverse bottom (r + topH + 1) c (if h > topH then h - topH - 1 else 0) w acc'traverse l 0 0 h w []def getWindowIds (l : Layout) : List Nat :=match l with| .window id _ => [id]| .hsplit left right _ => getWindowIds left ++ getWindowIds right| .vsplit top bottom _ => getWindowIds top ++ getWindowIds bottomend ViE
import ViE.State.Configimport ViE.State.Layoutimport ViE.State.Movementimport ViE.Typesimport ViE.Buffer.Contentnamespace ViEdef EditorState.setMode (s : EditorState) (m : Mode) : EditorState :={ s with mode := m }def EditorState.insertChar (s : EditorState) (c : Char) : EditorState :=let cursor := s.getCursorlet row := cursor.row.vallet col := cursor.col.vallet s' := s.updateActiveBuffer fun buffer =>modifyLineInBuffer buffer row fun line =>let chars := line.toListlet newChars := (chars.take col) ++ [c] ++ (chars.drop col)String.ofList newCharss'.setCursor (Point.make row (col + 1))def EditorState.insertNewline (s : EditorState) : EditorState :=let cursor := s.getCursorlet row := cursor.row.vallet col := cursor.col.vallet currentLine := getLine s.activeBufferContent row |>.getD ""let chars := currentLine.toListlet beforeSplit := String.ofList (chars.take col)let afterSplit := String.ofList (chars.drop col)let s' := s.updateActiveBufferContent fun buffer =>let beforeLines := arrayTake buffer rowlet afterLines := arrayDrop buffer (row + 1)arrayConcat [beforeLines, #[stringToLine beforeSplit, stringToLine afterSplit], afterLines]s'.setCursor (Point.make (row + 1) 0)def EditorState.deleteBeforeCursor (s : EditorState) : EditorState :=let cursor := s.getCursorlet row := cursor.row.vallet col := cursor.col.valif col > 0 then-- Delete char before cursorlet s' := s.updateActiveBufferContent fun buffer =>modifyLine buffer row fun line =>let chars := line.toListlet newChars := (chars.take (col - 1)) ++ (chars.drop col)String.ofList newCharss'.setCursor (Point.make row (col - 1))else if row > 0 then-- At start of line: join with previous linelet prevRow := row - 1let prevLine := getLine s.activeBufferContent prevRow |>.getD ""let currentLine := getLine s.activeBufferContent row |>.getD ""let mergedLine := prevLine ++ currentLinelet newCol := prevLine.lengthlet s' := s.updateActiveBufferContent fun buffer =>let beforeLines := arrayTake buffer prevRowlet afterLines := arrayDrop buffer (row + 1)arrayConcat [beforeLines, #[stringToLine mergedLine], afterLines]s'.setCursor (Point.make prevRow newCol)elsesdef deleteLine (buffer : TextBuffer) (row : Nat) : TextBuffer :=let before := arrayTake buffer rowlet after := arrayDrop buffer (row + 1)if before.isEmpty && after.isEmpty thenemptyTextBuffer -- Minimum one empty lineelsebefore ++ afterdef EditorState.deleteCurrentLine (s : EditorState) : EditorState :=let cursor := s.getCursorlet s' := s.updateActiveBufferContent fun (buffer : TextBuffer) => deleteLine buffer cursor.row.vallet newBuffer := s'.activeBufferContentlet newRow := min cursor.row.val (newBuffer.size.pred)let currentLineLen := (getLine newBuffer newRow).map String.length |>.getD 0let newCol := min cursor.col.val currentLineLenlet s'' := s'.setCursor (Point.make newRow newCol){ s'' withinputState := { s''.inputState with previousKey := none },message := "Deleted line"}def EditorState.insertLineBelow (s : EditorState) : EditorState :=let cursor := s.getCursorlet s' := s.updateActiveBufferContent fun buffer =>let before := arrayTake buffer (cursor.row.val + 1)let after := arrayDrop buffer (cursor.row.val + 1)arrayConcat [before, #[#[]], after]s'.setCursor (Point.make (cursor.row.val + 1) 0)def EditorState.insertLineAbove (s : EditorState) : EditorState :=let cursor := s.getCursorlet s' := s.updateActiveBufferContent fun buffer =>let before := arrayTake buffer cursor.row.vallet after := arrayDrop buffer cursor.row.valarrayConcat [before, #[#[]], after]s'.setCursor (Point.make cursor.row.val 0)def EditorState.yankCurrentLine (s : EditorState) : EditorState :=let cursor := s.getCursorlet line := getLine s.activeBufferContent cursor.row.val |>.getD ""{ s with clipboard := some #[stringToLine line], message := "Yanked 1 line" }def EditorState.pasteBelow (s : EditorState) : EditorState :=match s.clipboard with| none => { s with message := "Clipboard empty" }| some lines =>let cursor := s.getCursorlet s' := s.updateActiveBufferContent fun buffer =>let before := arrayTake buffer (cursor.row.val + 1)let after := arrayDrop buffer (cursor.row.val + 1)arrayConcat [before, lines, after]s'.setCursor (Point.make (cursor.row.val + lines.size) 0)def EditorState.pasteAbove (s : EditorState) : EditorState :=match s.clipboard with| none => { s with message := "Clipboard empty" }| some lines =>let cursor := s.getCursorlet s' := s.updateActiveBufferContent fun buffer =>let before := arrayTake buffer cursor.row.vallet after := arrayDrop buffer cursor.row.valarrayConcat [before, lines, after]s'.setCursor (Point.make cursor.row.val 0)end ViE
import ViE.Typesimport ViE.Basicimport ViE.Colorimport ViE.Workspaceimport ViE.Data.PieceTablenamespace ViEopen ViE.PieceTabledef defaultStatus := (ViE.Color.toBg <|(ViE.Color.fromHex "333333").getD.brightBlack) ++ (ViE.Color.toFg .white)def defaultWorkspace : Workspace := {rootPath := nonedisplayName := "ViE"}def defaultConfig : EditorConfig := {showLineNumbers := falsevSplitStr := "│"hSplitStr := "─"emptyLineMarker := "~"statusBarStyle := defaultStatusresetStyle := ViE.Color.resetfileIcon := "📄 "dirIcon := "📁 "}def initialInputState : InputState := {previousKey := nonecountBuffer := ""commandBuffer := ""pendingKeys := ""lastInputTime := 0}def initialBuffer : FileBuffer := {id := 0filename := nonedirty := falsetable := PieceTable.fromString ""missingEol := false}def initialView : ViewState := {bufferId := 0cursor := Point.zeroscrollRow := Row.zeroscrollCol := Col.zero}def initialWorkgroupState : WorkgroupState := {buffers := [initialBuffer]nextBufferId := 1layout := .window 0 initialViewactiveWindowId := 0nextWindowId := 1}def createEmptyWorkgroup (nextBufId : Nat) : WorkgroupState := {buffers := [{ initialBuffer with id := nextBufId }]nextBufferId := nextBufId + 1layout := .window 0 initialViewactiveWindowId := 0nextWindowId := 1}/-- Helper to create an array of initial workgroup states -/def makeInitialWorkgroups : Array WorkgroupState :=Array.mk (List.replicate 10 initialWorkgroupState)def initialState : EditorState := {mode := .normal-- Initialize 10 workgroupsworkgroups := makeInitialWorkgroupscurrentGroup := 0message := "Welcome to Lean Editor"shouldQuit := falseconfig := defaultConfigworkspace := defaultWorkspaceclipboard := noneselectionStart := noneexplorers := []inputState := initialInputStatewindowHeight := 0windowWidth := 0dirty := true}end ViE
namespace ViEinductive Mode where| normal| insert| command| visual| visualBlockderiving Repr, BEq, Inhabitedinstance : ToString Mode wheretoString| .normal => "NORMAL"| .insert => "INSERT"| .command => "COMMAND"| .visual => "VISUAL"| .visualBlock => "VISUAL BLOCK"end ViE
namespace ViE.Loaderdef getUserConfigDir : IO String := dolet xdgConfig ← IO.getEnv "XDG_CONFIG_HOME"match xdgConfig with| some dir => return dir ++ "/vie"| none =>let home ← IO.getEnv "HOME"match home with| some h => return h ++ "/.config/vie"| none => return "./config"def getCustomBinaryPath : IO String := dolet dir ← getUserConfigDirreturn dir ++ "/.lake/build/bin/vie"def buildConfig : IO Bool := dolet dir ← getUserConfigDirtrylet out ← IO.Process.output {cmd := "lake"args := #["build"]cwd := dir}return out.exitCode == 0catch _ =>return false/-- Restart the editor by spawning a new process and waiting for it. -/def restartEditor (args : List String) : IO Unit := dolet binPath ← getCustomBinaryPath-- Spawn the new process. It will take over the terminal.let child ← IO.Process.spawn {cmd := binPathargs := args.toArray}-- Wait for the new process to finish instead of exiting.-- This prevents the shell prompt from flickering or appearing.let _ ← child.waitIO.Process.exit 0end ViE.Loader
import ViE.Key.Dispatchimport ViE.Key.Mapimport ViE.Key.Inputimport ViE.Key.Handler
import ViE.Stateimport ViE.Typesimport ViE.Key.Handlerimport ViE.Window.Actionsimport ViE.Command.Explorerimport ViE.Command.Dispatchimport ViE.Basicnamespace ViE.Keyopen ViEopen ViE.Windowopen ViE.Featureopen ViE.Key/-- Default key bindings. -/def makeKeyMap (commands : CommandMap) : KeyMap := {normal := fun s k => match k with| Key.char 'h' =>-- Check if in explorer bufferlet buf := s.getActiveBufferlet isExplorer := s.explorers.any (fun (id, _) => id == buf.id)if isExplorer then-- Navigate to parent directorylet explorerOpt := s.explorers.find? (fun (id, _) => id == buf.id)match explorerOpt with| some (_, explorer) =>let parentPath := match (System.FilePath.mk explorer.currentPath).parent with| some p => p.toString| none => "/"let s' := { s with inputState := { s.inputState with commandBuffer := s!"ee {parentPath}" } }ViE.Command.executeCommand commands s'| none => pure $ clearInput (EditorState.moveCursorLeftN s s.getCount)elsepure $ 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.enter => ViE.Feature.handleExplorerEnter s| Key.char 'i' => pure $ s.setMode Mode.insert| Key.char 'a' =>let s' := EditorState.moveCursorRight spure $ s'.setMode Mode.insert| Key.char 'A' =>let s' := EditorState.moveToLineEnd spure $ s'.setMode Mode.insert| Key.char ':' => pure $ s.setMode Mode.command| Key.char 'q' => pure $ s.setMode Mode.command| 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.startVisualBlockMode s| Key.char '0' =>if s.inputState.countBuffer.isEmpty then pure $ clearInput (EditorState.moveToLineStart s)elsepure { s with inputState := { s.inputState with countBuffer := s.inputState.countBuffer.push '0' } }| Key.char '$' => pure $ clearInput (EditorState.moveToLineEnd s)| Key.char 'p' => 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.getCountpure $ clearInput (EditorState.jumpToColumn s n)| Key.char 'd' =>match s.inputState.previousKey with| some 'd' => pure $ s.deleteCurrentLine| _ => pure { s with inputState := { s.inputState with previousKey := some 'd' } }| 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 commandlet 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 commandslet s' := { s with inputState := { s.inputState with previousKey := none } }match c with| 't' =>let next := (s.currentGroup + 1) % 10let s'' := s.switchToWorkgroup nextpure { s'' with message := s!"Switched to workgroup {next}" }| 'T' =>let prev := if s.currentGroup == 0 then 9 else s.currentGroup - 1let s'' := s.switchToWorkgroup prevpure { s'' with message := s!"Switched to workgroup {prev}" }| _ => pure s'elsematch c with| 'g' =>match s.inputState.previousKey with| some 'g' =>-- gg implementationlet n := s.getCountlet line := if n > 0 then n else 1pure $ clearInput (EditorState.jumpToLine s line)| _ => 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.lineCountpure $ clearInput (EditorState.jumpToLine s line)| _ =>if c.isDigit thenpure { s with inputState := { s.inputState with countBuffer := s.inputState.countBuffer.push c } }elsepure { s with inputState := { s.inputState with countBuffer := "", previousKey := none } }| Key.alt c =>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| 'H' => pure $ resizeWindow s' .left 0.05| 'J' => pure $ resizeWindow s' .down 0.05| 'K' => pure $ resizeWindow s' .up 0.05| 'L' => pure $ resizeWindow s' .right 0.05| _ => pure s'| _ => pure { s with inputState := { s.inputState with countBuffer := "", previousKey := none } },insert := fun s k => match k with| Key.esc => pure $ s.setMode Mode.normal| Key.backspace => pure $ s.deleteBeforeCursor| Key.char c => pure $ s.insertChar c| Key.enter => pure s.insertNewline| _ => 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 '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.lineCountpure $ clearInput (EditorState.jumpToLine s line)| 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.getCountlet line := if n > 0 then n else 1pure $ clearInput (EditorState.jumpToLine s line)| _ => pure { s with inputState := { s.inputState with previousKey := some 'g' } }| _ =>if c.isDigit thenpure { s with inputState := { s.inputState with countBuffer := s.inputState.countBuffer.push c } }elsepure 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.lineCountpure $ clearInput (EditorState.jumpToLine s line)| 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.getCountlet line := if n > 0 then n else 1pure $ clearInput (EditorState.jumpToLine s line)| _ => pure { s with inputState := { s.inputState with previousKey := some 'g' } }| _ =>if c.isDigit thenpure { s with inputState := { s.inputState with countBuffer := s.inputState.countBuffer.push c } }elsepure s| _ => pure s}end ViE.Key
import ViE.Stateimport ViE.Typesnamespace ViE.Key/-- Parse a character into a list of keys, handling escape sequences statefully. -/def parseKey (state : EditorState) (c : Char) (currentTime : Nat) : (EditorState × List Key) :=-- If we have pending keys (escape sequence in progress)if state.inputState.pendingKeys.length > 0 thenlet pending := state.inputState.pendingKeys.push c-- Check for known sequencesmatch pending with| "\x1b" =>({ state with inputState := { state.inputState with pendingKeys := pending, lastInputTime := currentTime } }, [])| "\x1b[" =>-- CSI sequence starter, keep waiting({ state with inputState := { state.inputState with pendingKeys := pending, lastInputTime := currentTime } }, [])| "\x1b[A" => ({ state with inputState := { state.inputState with pendingKeys := "" } }, [Key.up])| "\x1b[B" => ({ state with inputState := { state.inputState with pendingKeys := "" } }, [Key.down])| "\x1b[C" => ({ state with inputState := { state.inputState with pendingKeys := "" } }, [Key.right])| "\x1b[D" => ({ state with inputState := { state.inputState with pendingKeys := "" } }, [Key.left])| "\x1b[H" => ({ state with inputState := { state.inputState with pendingKeys := "" } }, [Key.unknown 'H']) -- Home?| "\x1b[F" => ({ state with inputState := { state.inputState with pendingKeys := "" } }, [Key.unknown 'F']) -- End?| _ =>-- Unknown sequence or incomplete?-- If length is long enough (e.g. 3 chars) and no match, flush as individual keys?-- Or check if it starts with valid prefix.if pending.startsWith "\x1b[" && pending.length < 5 then({ state with inputState := { state.inputState with pendingKeys := pending, lastInputTime := currentTime } }, [])else-- Flush everything as individual characters-- (Note: this simplifies, strictly we should map char-by-char)let keys := pending.toList.map (fun x => if x == '\x1b' then Key.esc else Key.char x)({ state with inputState := { state.inputState with pendingKeys := "" } }, keys)else-- No pending keysif c == '\x1b' then-- Start escape sequence({ state with inputState := { state.inputState with pendingKeys := "\x1b", lastInputTime := currentTime } }, [])else-- Normal character (using logic similar to old fromChar)let k := match c with| '\x01' => Key.ctrl 'a'| '\x02' => Key.ctrl 'b'| '\x03' => Key.ctrl 'c'| '\x04' => Key.ctrl 'd'| '\x05' => Key.ctrl 'e'| '\x06' => Key.ctrl 'f'| '\x07' => Key.ctrl 'g'| '\x08' => Key.ctrl 'h'| '\x09' => Key.ctrl 'i'| '\x0b' => Key.ctrl 'k'| '\x0c' => Key.ctrl 'l'| '\x0e' => Key.ctrl 'n'| '\x0f' => Key.ctrl 'o'| '\x10' => Key.ctrl 'p'| '\x11' => Key.ctrl 'q'| '\x12' => Key.ctrl 'r'| '\x13' => Key.ctrl 's'| '\x14' => Key.ctrl 't'| '\x15' => Key.ctrl 'u'| '\x16' => Key.ctrl 'v'| '\x17' => Key.ctrl 'w'| '\x18' => Key.ctrl 'x'| '\x19' => Key.ctrl 'y'| '\x1a' => Key.ctrl 'z'| '\x7f' => Key.backspace| '\x0a' | '\x0d' => Key.enter| _ => Key.char c(state, [k])/-- Check for escape sequence timeout. -/def checkTimeout (state : EditorState) (currentTime : Nat) : (EditorState × List Key) :=if state.inputState.pendingKeys.length > 0 thenif currentTime - state.inputState.lastInputTime > 50 then -- 50ms timeout-- Timeout! Flush pending keys as is.let keys := state.inputState.pendingKeys.toList.map (fun x => if x == '\x1b' then Key.esc else Key.char x)({ state with inputState := { state.inputState with pendingKeys := "" } }, keys)else(state, [])else(state, [])end ViE.Key
import ViE.Stateimport ViE.Typesimport ViE.Command.Dispatchnamespace ViE.Keyopen ViEdef handleCommandInput (commands : CommandMap) (state : EditorState) (k : Key) : IO EditorState := domatch k with| Key.esc => return { state with mode := .normal, message := "", inputState := { state.inputState with commandBuffer := "" } }| Key.enter =>let newState ← ViE.Command.executeCommand commands stateif newState.shouldQuit thenreturn newStateelsereturn { newState with mode := .normal, inputState := { newState.inputState with commandBuffer := "" } }| Key.backspace =>if state.inputState.commandBuffer.length > 0 thenlet newCmd := state.inputState.commandBuffer.dropEnd 1return { state with inputState := { state.inputState with commandBuffer := newCmd.toString } }elsereturn state| Key.char c => return { state with inputState := { state.inputState with commandBuffer := state.inputState.commandBuffer.push c } }| _ => return statedef clearInput (s : EditorState) : EditorState :={ s with inputState := { s.inputState with countBuffer := "", previousKey := none } }end ViE.Key
import ViE.Stateimport ViE.Typesimport ViE.Window.Actionsimport ViE.Key.Mapnamespace ViE.Keyopen ViEdef update (config : Config) (state : EditorState) (k : Key) : IO EditorState := dolet newState ← match state.mode with| .normal => config.bindings.normal state k| .insert => config.bindings.insert state k| .command => config.bindings.command state k| .visual => config.bindings.visual state k| .visualBlock => config.bindings.visualBlock state kreturn ViE.Window.enforceScroll newStateend ViE.Key
import ViE.Stateimport ViE.Basicimport ViE.Buffer.LowIOnamespace ViEopen ViE.PieceTable/-- Save buffer to file with error handling using atomic write. -/def saveBuffer (state : EditorState) (filename : String) : IO EditorState := dolet tempFilename := filename ++ ".tmp"trylet buffer := state.getActiveBuffer-- Get content from PieceTablelet content := PieceTable.toString buffer.table-- Ensure POSIX compliance (end with newline)-- If missingEol is true, we preserve the "no newline at EOF" state (unless user added one).-- If missingEol is false, we enforce a trailing newline.let finalContent :=if buffer.missingEol thencontentelseif !content.isEmpty && content.back != '\n' thencontent ++ "\n"elsecontent-- Write to temporary file firstIO.FS.writeFile tempFilename finalContent-- Rename temp file to target file (atomic on POSIX)IO.FS.rename tempFilename filename-- After save, reload as clean mmaplet newBuffer ← loadBufferByteArray filenamelet newBuffer := { newBuffer with id := buffer.id, dirty := false }-- Update the buffer in the current workgrouplet state' := state.updateActiveBuffer fun _ => newBufferreturn { state' with message := s!"\"{filename}\" saved" }catch e =>-- Try to clean up temp file if it exists (ignoring errors)tryIO.FS.removeFile tempFilenamecatch _ => pure ()-- Keep dirty flag on error to prevent data lossreturn { state with message := s!"Error saving \"{filename}\": {e}" }end ViE
import ViE.Data.PieceTable.Pieceimport ViE.Data.PieceTable.Typesimport ViE.Data.PieceTable.Treenamespace ViE.PieceTable/-- Chunk size for splitting large files (16KB) -/def PieceTable.ChunkSize : Nat := 1024 * 16/-- Construct from bytes -/def PieceTable.fromByteArray (bytes : ByteArray) : PieceTable :=if bytes.size == 0 then{ original := bytes, add := ByteArray.empty, tree := PieceTree.empty }else-- Split bytes into chunks to avoid monolithic pieceslet totalSize := bytes.sizelet rec splitChunks (start : Nat) (acc : Array Piece) : Array Piece :=if start >= totalSize then accelselet len := min PieceTable.ChunkSize (totalSize - start)let lines := ViE.Unicode.countNewlines bytes start lenlet chars := ViE.Unicode.countChars bytes start lenlet piece := { source := .original, start := start, length := len, lineBreaks := lines, charCount := chars }splitChunks (start + len) (acc.push piece)termination_by totalSize - startdecreasing_bysimp_wfrw [Nat.sub_add_eq]have h : start < totalSize := Nat.lt_of_not_le (by assumption)apply Nat.sub_lt_self· have h1 : 0 < totalSize - start := Nat.sub_pos_of_lt happly Nat.lt_min.mprconstructor. show 0 < PieceTable.ChunkSizeunfold PieceTable.ChunkSizeexact Nat.zero_lt_succ _· assumption· apply Nat.min_le_rightlet pieces := splitChunks 0 #[]let tree := PieceTree.fromPieces pieces{ original := bytes, add := ByteArray.empty, tree := tree }/-- Construct from initial string -/def PieceTable.fromString (s : String) : PieceTable :=PieceTable.fromByteArray s.toUTF8/-- Convert tree to string -/def PieceTable.toString (pt : PieceTable) : String :=PieceTree.getSubstring pt.tree 0 pt.tree.length pt/-- Insert text at offset -/def PieceTable.insert (pt : PieceTable) (offset : Nat) (text : String) : PieceTable :=if text.isEmpty then ptelselet (pt', newPiece) := PieceTableHelper.appendText pt textlet (l, r) := PieceTree.split pt.tree offset pt'let mid := PieceTree.mkLeaf #[newPiece]let newTree := PieceTree.concat (PieceTree.concat l mid) r{ pt' with tree := newTree }/-- Delete range [offset, offset + length) -/def PieceTable.delete (pt : PieceTable) (offset : Nat) (length : Nat) : PieceTable :=if length == 0 then ptelselet newTree := PieceTree.delete pt.tree offset length pt{ pt with tree := newTree }/-- Get line from buffer -/def PieceTable.getLine (pt : PieceTable) (lineIdx : Nat) : Option String :=PieceTree.getLine pt.tree lineIdx pt/-- Get line range from buffer -/def PieceTable.getLineRange (pt : PieceTable) (lineIdx : Nat) : Option (Nat × Nat) :=PieceTree.getLineRange pt.tree lineIdx pt/-- Get line char length from buffer -/def PieceTable.getLineLength (pt : PieceTable) (lineIdx : Nat) : Option Nat :=PieceTree.getLineLength pt.tree lineIdx ptend ViE.PieceTable
import ViE.Data.PieceTable.Piecenamespace ViE.PieceTable/-- A node in the B+ Piece Tree -/inductive PieceTree where| empty| leaf (pieces : Array Piece) (stats : Stats)| internal (children : Array PieceTree) (stats : Stats)deriving Repr, Inhabitedstructure PieceTable whereoriginal : ByteArrayadd : ByteArraytree : PieceTreederiving Inhabitedend ViE.PieceTable
import ViE.Data.PieceTable.Pieceimport ViE.Data.PieceTable.Typesimport ViE.Unicodenamespace ViE.PieceTablenamespace PieceTableHelper/-- Get the buffer data corresponding to a source -/def getBuffer (pt : PieceTable) (source : BufferSource) : ByteArray :=match source with| .original => pt.original| .add => pt.add/-- Append text to add buffer -/def appendText (pt : PieceTable) (text : String) : (PieceTable × Piece) :=let bytes := text.toUTF8let start := pt.add.sizelet len := bytes.sizelet newAdd := pt.add ++ byteslet newLines := ViE.Unicode.countNewlines bytes 0 lenlet newChars := ViE.Unicode.countChars bytes 0 lenlet piece := { source := .add, start := start, length := len, lineBreaks := newLines, charCount := newChars }({ pt with add := newAdd }, piece)/-- Split a piece into two at a given relative offset. -/def splitPiece (pt : PieceTable) (p : Piece) (offset : Nat) : (Piece × Piece) :=let buf := getBuffer pt p.sourcelet leftLen := offsetlet rightLen := p.length - offsetlet leftLines := ViE.Unicode.countNewlines buf p.start leftLenlet rightLines := p.lineBreaks - leftLineslet leftChars := ViE.Unicode.countChars buf p.start leftLenlet rightChars := p.charCount - leftCharslet leftP := { p with length := leftLen, lineBreaks := leftLines, charCount := leftChars }let rightP := { p with start := p.start + leftLen, length := rightLen, lineBreaks := rightLines, charCount := rightChars }(leftP, rightP)end PieceTableHelpernamespace PieceTree/-- Get stats of a tree -/def stats : PieceTree → Stats| empty => Stats.empty| leaf _ s => s| internal _ s => sdef length (t : PieceTree) : Nat := t.stats.bytesdef lineBreaks (t : PieceTree) : Nat := t.stats.linesdef height (t : PieceTree) : Nat := t.stats.height/-- Helper to construct a leaf node from pieces -/def mkLeaf (pieces : Array Piece) : PieceTree :=let stats := pieces.foldl (fun s p =>{ s with bytes := s.bytes + p.length, lines := s.lines + p.lineBreaks, chars := s.chars + p.charCount }){ bytes := 0, lines := 0, chars := 0, height := 1 }leaf pieces stats/-- Helper to construct an internal node from children -/def mkInternal (children : Array PieceTree) : PieceTree :=if children.isEmpty then emptyelselet firstHeight := (children[0]!).heightlet h := firstHeight + 1let stats := children.foldl (fun s c =>let cs := c.stats{ s with bytes := s.bytes + cs.bytes, lines := s.lines + cs.lines, chars := s.chars + cs.chars }){ bytes := 0, lines := 0, chars := 0, height := h }internal children stats/-- Concatenate two trees -/partial def concat (l : PieceTree) (r : PieceTree) : PieceTree :=match l, r with| empty, _ => r| _, empty => l| leaf ps1 _, leaf ps2 _ =>let ps := ps1 ++ ps2if ps.size <= NodeCapacity thenmkLeaf pselse-- Split into two leaveslet mid := ps.size / 2let l := mkLeaf (ps.extract 0 mid)let r := mkLeaf (ps.extract mid ps.size)mkInternal #[l, r]| internal cs1 _, internal cs2 _ =>if l.height == r.height thenlet cs := cs1 ++ cs2if cs.size <= NodeCapacity thenmkInternal cselselet mid := cs.size / 2let l := mkInternal (cs.extract 0 mid)let r := mkInternal (cs.extract mid cs.size)mkInternal #[l, r]else if l.height > r.height then-- Append r to the last child of llet lastIdx := cs1.size - 1let lastChild := cs1[lastIdx]!let newLast := concat lastChild rif newLast.height == lastChild.height thenmkInternal (cs1.set! lastIdx newLast)elsematch newLast with| internal subChildren _ =>let newCs := cs1.pop ++ subChildrenif newCs.size <= NodeCapacity then mkInternal newCselselet mid := newCs.size / 2mkInternal #[mkInternal (newCs.extract 0 mid), mkInternal (newCs.extract mid newCs.size)]| _ => mkInternal (cs1.push newLast)else -- l.height < r.heightlet firstChild := cs2[0]!let newFirst := concat l firstChildif newFirst.height == firstChild.height thenmkInternal (cs2.set! 0 newFirst)elsematch newFirst with| internal subChildren _ =>let newCs := subChildren ++ cs2.extract 1 cs2.sizeif newCs.size <= NodeCapacity then mkInternal newCselselet mid := newCs.size / 2mkInternal #[mkInternal (newCs.extract 0 mid), mkInternal (newCs.extract mid newCs.size)]| _ => mkInternal ((#[newFirst] ++ cs2))| leaf _ _, internal _ _ =>let firstChild := match r with | internal cs _ => cs[0]! | _ => emptylet newFirst := concat l firstChildmatch r with| internal cs2 _ =>if newFirst.height == firstChild.height thenmkInternal (cs2.set! 0 newFirst)elsematch newFirst with| internal subChildren _ =>let newCs := subChildren ++ cs2.extract 1 cs2.sizeif newCs.size <= NodeCapacity then mkInternal newCselselet mid := newCs.size / 2mkInternal #[mkInternal (newCs.extract 0 mid), mkInternal (newCs.extract mid newCs.size)]| _ => mkInternal ((#[newFirst] ++ cs2))| _ => empty| internal _ _, leaf _ _ =>let cs1 := match l with | internal cs _ => cs | _ => #[]let lastIdx := cs1.size - 1let lastChild := cs1[lastIdx]!let newLast := concat lastChild rif newLast.height == lastChild.height thenmkInternal (cs1.set! lastIdx newLast)elsematch newLast with| internal subChildren _ =>let newCs := cs1.pop ++ subChildrenif newCs.size <= NodeCapacity then mkInternal newCselselet mid := newCs.size / 2mkInternal #[mkInternal (newCs.extract 0 mid), mkInternal (newCs.extract mid newCs.size)]| _ => mkInternal (cs1.push newLast)/-- Split a leaf node at a specific piece index and internal offset -/def splitLeaf (pieces : Array Piece) (stats : Stats) (pt : PieceTable) (offset : Nat) : (PieceTree × PieceTree) :=let rec findPiece (i : Nat) (currentOff : Nat) : (Nat × Nat) :=if i >= pieces.size then (pieces.size, currentOff)elselet p := pieces[i]!if currentOff + p.length > offset then (i, currentOff)else findPiece (i + 1) (currentOff + p.length)let (idx, pStart) := findPiece 0 0if idx >= pieces.size then(leaf pieces stats, empty)elselet p := pieces[idx]!let splitOff := offset - pStartif splitOff == 0 thenlet leftArr := pieces.extract 0 idxlet rightArr := pieces.extract idx pieces.size(mkLeaf leftArr, mkLeaf rightArr)else if splitOff == p.length thenlet leftArr := pieces.extract 0 (idx + 1)let rightArr := pieces.extract (idx + 1) pieces.size(mkLeaf leftArr, mkLeaf rightArr)elselet (p1, p2) := PieceTableHelper.splitPiece pt p splitOfflet leftPieces := (pieces.extract 0 idx).push p1let rightPieces := (#[p2]).append (pieces.extract (idx + 1) pieces.size)(mkLeaf leftPieces, mkLeaf rightPieces)/-- Split the tree at a given character offset. -/partial def split (t : PieceTree) (offset : Nat) (pt : PieceTable) : (PieceTree × PieceTree) :=match t with| empty => (empty, empty)| leaf pieces s => splitLeaf pieces s pt offset| internal children curStats =>if offset == 0 then (empty, t)else if offset >= curStats.bytes then (t, empty)elselet rec scan (i : Nat) (accOff : Nat) : (PieceTree × PieceTree) :=if i >= children.size then (t, empty)elselet c := children[i]!if accOff + c.length > offset thenlet (cLeft, cRight) := split c (offset - accOff) ptlet leftChildren := (children.extract 0 i).push cLeftlet rightChildren := (#[cRight]).append (children.extract (i + 1) children.size)let leftFiltered := leftChildren.filter (fun c => c.length > 0)let rightFiltered := rightChildren.filter (fun c => c.length > 0)(mkInternal leftFiltered, mkInternal rightFiltered)elsescan (i + 1) (accOff + c.length)scan 0 0/-- Delete range [offset, offset + length) -/def delete (t : PieceTree) (offset : Nat) (length : Nat) (pt : PieceTable) : PieceTree :=if length == 0 then telselet (l, r) := split t offset ptlet (_, r') := split r length ptconcat l r'/-- Find offset of the N-th newline (0-indexed). -/partial def findNthNewline (t : PieceTree) (n : Nat) (pt : PieceTable) : Nat :=match t with| empty => 0| leaf pieces _ =>let rec scanPieces (i : Nat) (remainingN : Nat) (accOffset : Nat) : Nat :=if i >= pieces.size then accOffsetelselet p := pieces[i]!if remainingN < p.lineBreaks thenlet buf := PieceTableHelper.getBuffer pt p.sourcelet rec scan (j : Nat) (cnt : Nat) : Nat :=if j >= p.length then jelseif buf[p.start + j]! == 10 thenif cnt == remainingN then jelse scan (j + 1) (cnt + 1)else scan (j + 1) cntaccOffset + (scan 0 0)elsescanPieces (i + 1) (remainingN - p.lineBreaks) (accOffset + p.length)scanPieces 0 n 0| internal children _ =>let rec scanChildren (i : Nat) (remainingN : Nat) (accOffset : Nat) : Nat :=if i >= children.size then accOffsetelselet child := children[i]!let childLines := child.lineBreaksif remainingN < childLines thenaccOffset + findNthNewline child remainingN ptelsescanChildren (i + 1) (remainingN - childLines) (accOffset + child.length)scanChildren 0 n 0/-- Get substring of tree from start to end offset -/partial def getSubstring (t : PieceTree) (startOff endOff : Nat) (pt : PieceTable) : String :=if startOff >= endOff then ""elselet rec traverse (t : PieceTree) (currentOff : Nat) : String :=let tLen := t.lengthif currentOff + tLen <= startOff || currentOff >= endOff then ""elsematch t with| empty => ""| leaf pieces _ =>let rec scan (i : Nat) (off : Nat) (acc : String) : String :=if i >= pieces.size then accelselet p := pieces[i]!let pStart := offlet pEnd := off + p.lengthlet readStart := max startOff pStartlet readEnd := min endOff pEndif readStart < readEnd thenlet buf := PieceTableHelper.getBuffer pt p.sourcelet sliceStart := p.start + (readStart - pStart)let sliceEnd := p.start + (readEnd - pStart)let str := String.fromUTF8! (buf.extract sliceStart sliceEnd)scan (i + 1) pEnd (acc ++ str)elsescan (i + 1) pEnd accscan 0 currentOff ""| internal children _ =>let rec scanInternal (i : Nat) (off : Nat) (acc : String) : String :=if i >= children.size then accelselet c := children[i]!let s := traverse c offscanInternal (i + 1) (off + c.length) (acc ++ s)scanInternal 0 currentOff ""traverse t 0/-- Get total string length of a range (counting chars, not bytes, without allocation) -/partial def countCharsInRange (t : PieceTree) (startOff endOff : Nat) (pt : PieceTable) : Nat :=if startOff >= endOff then 0elselet rec traverse (t : PieceTree) (currentOff : Nat) : Nat :=let tLen := t.lengthif currentOff + tLen <= startOff || currentOff >= endOff then 0else if currentOff >= startOff && currentOff + tLen <= endOff thent.stats.charselsematch t with| empty => 0| leaf pieces _ =>let rec scan (i : Nat) (off : Nat) (acc : Nat) : Nat :=if i >= pieces.size then accelselet p := pieces[i]!let pStart := offlet pEnd := off + p.lengthif pEnd <= startOff || pStart >= endOff thenscan (i + 1) pEnd accelse if pStart >= startOff && pEnd <= endOff thenscan (i + 1) pEnd (acc + p.charCount)elselet readStart := max startOff pStartlet readEnd := min endOff pEndlet buf := PieceTableHelper.getBuffer pt p.sourcelet sliceStart := p.start + (readStart - pStart)let sliceLen := readEnd - readStartlet cnt := ViE.Unicode.countChars buf sliceStart sliceLenscan (i + 1) pEnd (acc + cnt)scan 0 currentOff 0| internal children _ =>let rec scanInternal (i : Nat) (off : Nat) (acc : Nat) : Nat :=if i >= children.size then accelselet c := children[i]!let s := traverse c offscanInternal (i + 1) (off + c.length) (acc + s)scanInternal 0 currentOff 0traverse t 0/-- Get line char length (0-indexed). Excludes newline at end of line. -/def getLineLength (t : PieceTree) (lineIdx : Nat) (pt : PieceTable) : Option Nat :=let totalLines := t.lineBreaksif lineIdx > totalLines then noneelselet startOffset := if lineIdx == 0 then 0 else findNthNewline t (lineIdx - 1) pt + 1let endOffset :=if lineIdx == totalLines thent.lengthelsefindNthNewline t lineIdx ptsome (countCharsInRange t startOffset endOffset pt)def getLine (t : PieceTree) (lineIdx : Nat) (pt : PieceTable) : Option String :=let totalLines := t.lineBreaksif lineIdx > totalLines then noneelselet startOffset := if lineIdx == 0 then 0 else findNthNewline t (lineIdx - 1) pt + 1let endOffset :=if lineIdx == totalLines thent.lengthelsefindNthNewline t lineIdx ptsome (getSubstring t startOffset endOffset pt)/-- Get line range (start, length) -/def getLineRange (t : PieceTree) (lineIdx : Nat) (pt : PieceTable) : Option (Nat × Nat) :=let totalLines := t.lineBreaksif lineIdx > totalLines then noneelselet startOffset := if lineIdx == 0 then 0 else findNthNewline t (lineIdx - 1) pt + 1let endOffset :=if lineIdx == totalLines thent.lengthelsefindNthNewline t lineIdx ptsome (startOffset, endOffset - startOffset)/-- Build a tree from a list of pieces (Bottom-Up construction) -/partial def fromPieces (pieces : Array Piece) : PieceTree :=if pieces.isEmpty then emptyelse-- Step 1: Create Leaveslet rec mkLeaves (i : Nat) (acc : Array PieceTree) : Array PieceTree :=if i >= pieces.size then accelselet chunk := pieces.extract i (i + NodeCapacity)let leaf := mkLeaf chunkmkLeaves (i + NodeCapacity) (acc.push leaf)let leaves := mkLeaves 0 #[]-- Step 2: Build Internal Nodes recursivelylet rec buildLevel (nodes : Array PieceTree) : PieceTree :=if nodes.size == 1 thennodes[0]!elselet rec mkNextLevel (i : Nat) (acc : Array PieceTree) : Array PieceTree :=if i >= nodes.size then accelselet chunk := nodes.extract i (i + NodeCapacity)let node := mkInternal chunkmkNextLevel (i + NodeCapacity) (acc.push node)buildLevel (mkNextLevel 0 #[])buildLevel leavesend PieceTreeend ViE.PieceTable
namespace ViE.PieceTable/-- Source of a piece: either the original file (read-only) or the add buffer (append-only) -/inductive BufferSource where| original| addderiving Repr, BEq, Inhabited/-- A piece descriptor pointing to a range in a source buffer -/structure Piece wheresource : BufferSourcestart : Natlength : NatlineBreaks : NatcharCount : Natderiving Repr, BEq, Inhabited/-- Aggregated statistics for a node -/structure Stats wherebytes : Natlines : Natchars : Natheight : Natderiving Repr, Inhabited, BEqdef Stats.empty : Stats := { bytes := 0, lines := 0, chars := 0, height := 0 }def Stats.ofPiece (p : Piece) : Stats :={ bytes := p.length, lines := p.lineBreaks, chars := p.charCount, height := 1 }instance : Add Stats whereadd a b := {bytes := a.bytes + b.bytes,lines := a.lines + b.lines,chars := a.chars + b.chars,height := max a.height b.height}/-- B+ Tree Node Capacity (Branching Factor) -/def NodeCapacity : Nat := 32end ViE.PieceTable
namespace ViE/-- Source of a piece: either the original file (read-only) or the add buffer (append-only) -/inductive BufferSource where| original| addderiving Repr, BEq, Inhabited/-- A piece descriptor pointing to a range in a source buffer -/structure Piece wheresource : BufferSourcestart : Natlength : NatlineBreaks : NatcharCount : Natderiving Repr, BEq, Inhabited/-- Aggregated statistics for a node -/structure Stats wherebytes : Natlines : Natchars : Natheight : Natderiving Repr, Inhabited, BEqdef Stats.empty : Stats := { bytes := 0, lines := 0, chars := 0, height := 0 }def Stats.ofPiece (p : Piece) : Stats :={ bytes := p.length, lines := p.lineBreaks, chars := p.charCount, height := 1 }instance : Add Stats whereadd a b := {bytes := a.bytes + b.bytes,lines := a.lines + b.lines,chars := a.chars + b.chars,height := max a.height b.height}/-- B+ Tree Node Capacity (Branching Factor) -/def NodeCapacity : Nat := 32end ViE
import ViE.Statenamespace ViE/-- Parse a character into a list of keys, handling escape sequences statefully. -/def parseKey (state : EditorState) (c : Char) (currentTime : Nat) : (EditorState × List Key) :=-- If we have pending keys (escape sequence in progress)if state.inputState.pendingKeys.length > 0 thenlet pending := state.inputState.pendingKeys.push c-- Check for known sequencesmatch pending with| "\x1b" =>-- This case is only reached if we pushed a char and pending became exactly "\x1b",-- which is impossible if start condition was "pending.length > 0" (meaning pending was already "\x1b")-- UNLESS previous pending was empty? No, then it goes to else block.-- Wait, if pending was "\x1b" and we pushed nothing? No, we always push c.-- So this case is indeed impossible for Escape+Char.({ state with inputState := { state.inputState with pendingKeys := pending, lastInputTime := currentTime } }, [])| "\x1b[" =>-- CSI sequence starter, keep waiting({ state with inputState := { state.inputState with pendingKeys := pending, lastInputTime := currentTime } }, [])| "\x1b[A" => ({ state with inputState := { state.inputState with pendingKeys := "" } }, [Key.up])| "\x1b[B" => ({ state with inputState := { state.inputState with pendingKeys := "" } }, [Key.down])| "\x1b[C" => ({ state with inputState := { state.inputState with pendingKeys := "" } }, [Key.right])| "\x1b[D" => ({ state with inputState := { state.inputState with pendingKeys := "" } }, [Key.left])| "\x1b[H" => ({ state with inputState := { state.inputState with pendingKeys := "" } }, [Key.unknown 'H']) -- Home?| "\x1b[F" => ({ state with inputState := { state.inputState with pendingKeys := "" } }, [Key.unknown 'F']) -- End?| _ =>-- Check for Alt+Char: Escape followed by a single charif pending.length == 2 && pending.startsWith "\x1b" thenmatch pending.toList with| [_, c] => ({ state with inputState := { state.inputState with pendingKeys := "" } }, [Key.alt c])| _ => -- Should be impossible given length checklet keys := pending.toList.map (fun x => if x == '\x1b' then Key.esc else Key.char x)({ state with inputState := { state.inputState with pendingKeys := "" } }, keys)-- Check if it looks like a CSI sequenceelse if pending.startsWith "\x1b[" && pending.length < 5 then({ state with inputState := { state.inputState with pendingKeys := pending, lastInputTime := currentTime } }, [])else-- Flush everything as individual characterslet keys := pending.toList.map (fun x => if x == '\x1b' then Key.esc else Key.char x)({ state with inputState := { state.inputState with pendingKeys := "" } }, keys)else-- No pending keysif c == '\x1b' then-- Start escape sequence({ state with inputState := { state.inputState with pendingKeys := "\x1b", lastInputTime := currentTime } }, [])else-- Normal character (using logic similar to old fromChar)let k := match c with| '\x01' => Key.ctrl 'a'| '\x02' => Key.ctrl 'b'| '\x03' => Key.ctrl 'c'| '\x04' => Key.ctrl 'd'| '\x05' => Key.ctrl 'e'| '\x06' => Key.ctrl 'f'| '\x07' => Key.ctrl 'g'| '\x08' => Key.ctrl 'h'| '\x09' => Key.ctrl 'i'| '\x0b' => Key.ctrl 'k'| '\x0c' => Key.ctrl 'l'| '\x0e' => Key.ctrl 'n'| '\x0f' => Key.ctrl 'o'| '\x10' => Key.ctrl 'p'| '\x11' => Key.ctrl 'q'| '\x12' => Key.ctrl 'r'| '\x13' => Key.ctrl 's'| '\x14' => Key.ctrl 't'| '\x15' => Key.ctrl 'u'| '\x16' => Key.ctrl 'v'| '\x17' => Key.ctrl 'w'| '\x18' => Key.ctrl 'x'| '\x19' => Key.ctrl 'y'| '\x1a' => Key.ctrl 'z'| '\x7f' => Key.backspace| '\x0a' | '\x0d' => Key.enter| _ => Key.char c(state, [k])/-- Check for escape sequence timeout. -/def checkTimeout (state : EditorState) (currentTime : Nat) : (EditorState × List Key) :=if state.inputState.pendingKeys.length > 0 thenif currentTime - state.inputState.lastInputTime > 50 then -- 50ms timeout-- Timeout! Flush pending keys as is.let keys := state.inputState.pendingKeys.toList.map (fun x => if x == '\x1b' then Key.esc else Key.char x)({ state with inputState := { state.inputState with pendingKeys := "" } }, keys)else(state, [])else(state, [])def enforceScroll (state : EditorState) : EditorState :=let (h, w) := state.getActiveWindowBounds-- Active window height. Reserve 1 line for status bar?-- UI logic reserves 1 for status bar in split rendering.let linesInView := if h > 1 then h - 1 else 1-- Vertical Scrolllet (sRow, sCol) := state.getScrolllet cursor := state.getCursorlet state :=if cursor.row.val < sRow.val thenstate.setScroll cursor.row sColelse if cursor.row.val >= sRow.val + linesInView thenlet newScrollRow : Row := ⟨cursor.row.val - linesInView + 1⟩state.setScroll newScrollRow sColelsestate-- Refresh scroll after potential updatelet (sRow, sCol) := state.getScroll-- Horizontal Scrolllet lnWidth := if state.config.showLineNumbers then 4 else 0let colsInView := if w > lnWidth then w - lnWidth else 1let state :=if cursor.col.val < sCol.val thenstate.setScroll sRow cursor.colelse if cursor.col.val >= sCol.val + colsInView thenlet newScrollCol : Col := ⟨cursor.col.val - colsInView + 1⟩state.setScroll sRow newScrollColelsestatestatedef update (config : Config) (state : EditorState) (k : Key) : IO EditorState := dolet newState ← match state.mode with| .normal => config.bindings.normal state k| .insert => config.bindings.insert state k| .command => config.bindings.command state k| .visual => config.bindings.visual state k| .visualBlock => config.bindings.visualBlock state kreturn enforceScroll newStateend ViE
import ViE.Command.Dispatchimport ViE.Command.Implimport ViE.Command.Parser
namespace ViE.Command/-- Parse filename argument from command args.Returns the filename from args, or the current buffer's filename if no args provided.Returns error message if multiple args or no filename available. -/def parseFilenameArg (args : List String) (currentFilename : Option String) : Except String String :=match args with| [fname] => .ok fname| [] => match currentFilename with| some fname => .ok fname| none => .error "No file name"| _ => .error "Too many arguments"end ViE.Command
import ViE.Stateimport ViE.Typesimport ViE.Window.Actionsimport ViE.Buffer.Managerimport ViE.Command.Explorerimport ViE.Loaderimport ViE.Checkpointimport ViE.Command.Parsernamespace ViE.Commandopen ViE.Windowopen ViE.Bufferopen ViE.Featuredef cmdEdit (args : List String) (state : EditorState) : IO EditorState := domatch args with| [fname] =>-- Resolve path relative to workspacelet resolvedPath := state.workspace.resolvePath fnameViE.Buffer.openBuffer state resolvedPath| [] => return { state with message := "No file name" }| _ => return { state with message := "Too many arguments" }def cmdWrite (args : List String) (state : EditorState) : IO EditorState :=match ViE.Command.parseFilenameArg args state.getActiveBuffer.filename with| .ok fname =>ViE.saveBuffer state fname| .error msg =>return { state with message := msg }def cmdQuit (_ : List String) (state : EditorState) : IO EditorState :=return (ViE.Window.closeActiveWindow state)def cmdQuitForce (_ : List String) (state : EditorState) : IO EditorState :=return { state with shouldQuit := true }def cmdWriteQuit (args : List String) (state : EditorState) : IO EditorState := domatch ViE.Command.parseFilenameArg args state.getActiveBuffer.filename with| .ok fname =>let state' ← ViE.saveBuffer state fnamereturn (ViE.Window.closeActiveWindow state')| .error msg =>return { state with message := msg }def cmdSplit (_ : List String) (state : EditorState) : IO EditorState :=return ViE.Window.splitWindow state truedef cmdVSplit (_ : List String) (state : EditorState) : IO EditorState :=return ViE.Window.splitWindow state falsedef cmdSet (args : List String) (state : EditorState) : IO EditorState :=match args with| ["number"] => return { state with config := { state.config with showLineNumbers := true }, message := "number set" }| ["nonumber"] => return { state with config := { state.config with showLineNumbers := false }, message := "number unset" }| _ => return { state with message := s!"Unknown setting or args: {args}" }def cmdWincmd (args : List String) (state : EditorState) : IO EditorState :=match args with| ["w"] => return ViE.Window.cycleWindow state| ["h"] => return ViE.Window.switchWindow state .left| ["j"] => return ViE.Window.switchWindow state .down| ["k"] => return ViE.Window.switchWindow state .up| ["l"] => return ViE.Window.switchWindow state .right| ["s"] => return ViE.Window.splitWindow state true| ["v"] => return ViE.Window.splitWindow state false| ["q"] => return (ViE.Window.closeActiveWindow state)| _ => return { state with message := s!"Unknown wincmd: {args}" }def cmdWinLeft (_ : List String) (state : EditorState) : IO EditorState := return ViE.Window.switchWindow state .leftdef cmdWinDown (_ : List String) (state : EditorState) : IO EditorState := return ViE.Window.switchWindow state .downdef cmdWinUp (_ : List String) (state : EditorState) : IO EditorState := return ViE.Window.switchWindow state .updef cmdWinRight (_ : List String) (state : EditorState) : IO EditorState := return ViE.Window.switchWindow state .rightdef cmdWinCycle (_ : List String) (state : EditorState) : IO EditorState := return ViE.Window.cycleWindow statedef cmdCd (args : List String) (state : EditorState) : IO EditorState :=match args with| [path] =>-- Change workspacelet absPath := if path.startsWith "/" then pathelse match state.workspace.rootPath with| some root => root ++ "/" ++ path| none => pathreturn { state withworkspace := { rootPath := some absPath },message := s!"Workspace: {absPath}"}| [] =>-- Clear workspacereturn { state withworkspace := defaultWorkspace,message := "Workspace cleared"}| _ => return { state with message := "Usage: :cd [path]" }def cmdPwd (_ : List String) (state : EditorState) : IO EditorState :=match state.workspace.rootPath with| some path => return { state with message := path }| none => return { state with message := "No workspace set" }def cmdWg (args : List String) (state : EditorState) : IO EditorState :=match args with| [n] =>match n.toNat? with| some num =>if num < 10 thenlet s' := state.switchToWorkgroup numreturn { s' with message := s!"Switched to workgroup {num}" }elsereturn { state with message := "Workgroup number must be 0-9" }| none => return { state with message := "Invalid workgroup number" }| [] => return { state with message := s!"Current workgroup: {state.currentGroup}" }| _ => return { state with message := "Usage: :wg [0-9]" }def cmdExplorer (args : List String) (state : EditorState) : IO EditorState :=let path := match args with| [] => state.workspace.rootPath.getD "."| [p] => p| _ => "."ViE.Feature.openExplorer state pathdef cmdReload (_ : List String) (state : EditorState) : IO EditorState := dolet success ← ViE.Loader.buildConfigif success then-- Save sessionViE.Checkpoint.saveSession state-- RestartViE.Loader.restartEditor []return { state with shouldQuit := true }elsereturn { state with message := "Configuration build failed" }def defaultCommandMap : CommandMap := [("e", cmdEdit),("w", cmdWrite),("q", cmdQuit),("q!", cmdQuitForce),("qa", cmdQuitForce),("qa!", cmdQuitForce),("wq", cmdWriteQuit),("sp", cmdSplit),("split", cmdSplit),("hs", cmdSplit),("hsplit", cmdSplit),("vs", cmdVSplit),("vsplit", cmdVSplit),("set", cmdSet),("wincmd", cmdWincmd),("wh", cmdWinLeft),("wj", cmdWinDown),("wk", cmdWinUp),("wl", cmdWinRight),("wc", cmdWinCycle),("cd", cmdCd),("pwd", cmdPwd),("wg", cmdWg),("ee", cmdExplorer),("reload", cmdReload),("refresh", cmdReload)]end ViE.Command
import ViE.Stateimport ViE.Typesimport ViE.Buffer.Managerimport ViE.Basicnamespace ViE.Featureopen ViE/-- Open the file explorer at the specified path (or current workspace root) -/def openExplorer (state : EditorState) (pathArg : String) : IO EditorState := dolet path := if pathArg == "." || pathArg == "" then state.workspace.rootPath.getD "."else if pathArg.startsWith "/" then pathArgelse match state.workspace.rootPath with| some root => root ++ "/" ++ pathArg| none => pathArg-- Read directory contentstrylet dirPath := System.FilePath.mk pathlet entries := (← System.FilePath.readDir dirPath).toListlet mut fileEntries : List FileEntry := []-- Add parent directory entry if not at rootif path != "/" thenlet parentPath := match dirPath.parent with| some p => p.toString| none => "/"fileEntries := fileEntries ++ [{name := ".."path := parentPathisDirectory := true}]-- Convert directory entries to FileEntryfor entry in entries dolet entryPath := entry.path.toStringlet isDir ← entry.path.isDirfileEntries := fileEntries ++ [{name := entry.fileNamepath := entryPathisDirectory := isDir}]-- Sort: directories first, then files, alphabeticallylet sortedEntries := fileEntries.toArray.qsort fun a b =>if a.isDirectory != b.isDirectory thena.isDirectory -- directories firstelsea.name < b.name-- Create explorer statelet explorerState : ExplorerState := {currentPath := pathentries := sortedEntries.toListselectedIndex := 0}-- Generate buffer contentlet header := [s!"Explorer: {path}", ""]let mut content := headerlet mut idx := 0for entry in explorerState.entries dolet pref := if idx == 0 then "> " else " "let icon := if entry.isDirectory then state.config.dirIcon else state.config.fileIconlet suffix := if entry.isDirectory then "/" else ""content := content ++ [s!"{pref}{icon}{entry.name}{suffix}"]idx := idx + 1-- Create new bufferlet contentBuffer := content.toArray.map stringToLinelet (bufferId, s') := ViE.Buffer.createNewBuffer state contentBuffer (some s!"explorer://{path}")-- Register explorerlet s'' := { s' with explorers := (bufferId, explorerState) :: s'.explorers }-- Switch to explorer bufferlet s''' := s''.updateActiveView fun v => { v with bufferId := bufferId, cursor := {row := 2, col := 0}, scrollRow := 0, scrollCol := 0 }return { s''' with message := s!"Explorer: {path}" }catch e =>return { state with message := s!"Error reading directory: {e}" }/-- Handle Enter key in explorer buffer -/def handleExplorerEnter (state : EditorState) : IO EditorState := dolet buf := state.getActiveBuffer-- Check if this is an explorer bufferlet explorerOpt := state.explorers.find? (fun (id, _) => id == buf.id)match explorerOpt with| none => return state.insertNewline -- Not an explorer, normal behavior| some (_, explorer) =>-- Get current cursor rowlet cursor := state.getCursor-- Explorer header is 2 lines, so subtract 2let selectedIdx := cursor.row - 2if selectedIdx < 0 || selectedIdx >= explorer.entries.length thenreturn state-- Get selected entrylet entry := explorer.entries[selectedIdx]!if entry.isDirectory then-- Navigate to directoryopenExplorer state entry.pathelse-- Open fileViE.Buffer.openBuffer state entry.pathend ViE.Feature
import ViE.Stateimport ViE.IOimport ViE.Loaderimport ViE.Checkpointimport ViE.Typesimport ViE.Buffer.Contentimport ViE.Window.Actionsimport ViE.Window.Analysisimport ViE.Buffer.Managerimport ViE.Command.Explorerimport ViE.Command.Parseropen ViE.Windowopen ViE.Bufferopen ViE.Featureopen ViE.Commandnamespace ViE.Commanddef executeCommand (commands : CommandMap) (state : EditorState) : IO EditorState := dolet fullCmd := state.inputState.commandBufferlet parts := fullCmd.splitOn " "match parts with| [] => return state| cmd :: args =>match commands.lookup cmd with| some action => action args state| none => return { state with message := s!"Unknown command: {fullCmd}" }end ViE.Command
namespace ViE.Color/-- Represents terminal colors. -/inductive Color| black | red | green | yellow | blue | magenta | cyan | white| brightBlack | brightRed | brightGreen | brightYellow | brightBlue | brightMagenta | brightCyan | brightWhite| rgb (r g b : UInt8)| grayscale (level : UInt8) -- 0-23 (maps to 232-255)| defaultderiving Repr, BEq, Inhabited/-- Convert Color to FG ANSI string -/def toFg (c : Color) : String :=match c with| .black => "\x1b[30m"| .red => "\x1b[31m"| .green => "\x1b[32m"| .yellow => "\x1b[33m"| .blue => "\x1b[34m"| .magenta => "\x1b[35m"| .cyan => "\x1b[36m"| .white => "\x1b[37m"| .brightBlack => "\x1b[90m"| .brightRed => "\x1b[91m"| .brightGreen => "\x1b[92m"| .brightYellow => "\x1b[93m"| .brightBlue => "\x1b[94m"| .brightMagenta => "\x1b[95m"| .brightCyan => "\x1b[96m"| .brightWhite => "\x1b[97m"| .rgb r g b => s!"\x1b[38;2;{r};{g};{b}m"| .grayscale level =>let idx := 232 + (if level > 23 then 23 else level)s!"\x1b[38;5;{idx}m"| .default => "\x1b[39m"/-- Convert Color to BG ANSI string -/def toBg (c : Color) : String :=match c with| .black => "\x1b[40m"| .red => "\x1b[41m"| .green => "\x1b[42m"| .yellow => "\x1b[43m"| .blue => "\x1b[44m"| .magenta => "\x1b[45m"| .cyan => "\x1b[46m"| .white => "\x1b[47m"| .brightBlack => "\x1b[100m"| .brightRed => "\x1b[101m"| .brightGreen => "\x1b[102m"| .brightYellow => "\x1b[103m"| .brightBlue => "\x1b[104m"| .brightMagenta => "\x1b[105m"| .brightCyan => "\x1b[106m"| .brightWhite => "\x1b[107m"| .rgb r g b => s!"\x1b[48;2;{r};{g};{b}m"| .grayscale level =>let idx := 232 + (if level > 23 then 23 else level)s!"\x1b[48;5;{idx}m"| .default => "\x1b[49m"def reset : String := "\x1b[0m"/-- Helper to parse hex string like "#RRGGBB" or "RRGGBB". -/def fromHex (hex : String) : Option Color :=let s := if hex.startsWith "#" then hex.drop 1 else hexif s.positions.count != 6 then noneelselet rStr := s.take 2let gStr := (s.drop 2).take 2let bStr := (s.drop 4).take 2match (String.toNat? ("0x" ++ rStr)), (String.toNat? ("0x" ++ gStr)), (String.toNat? ("0x" ++ bStr)) with| some r, some g, some b =>if r < 256 && g < 256 && b < 256 thensome (.rgb r.toUInt8 g.toUInt8 b.toUInt8)else none| _, _, _ => noneend ViE.Color
import ViE.Stateimport ViE.IOnamespace ViE.Checkpointdef sessionFile : String := "/tmp/editor_session.tmp"/-- Save current session state to a temporary file. -/def saveSession (state : EditorState) : IO Unit := dolet wg := state.getCurrentWorkgrouplet buffers := wg.buffers.filter (fun b => b.filename.isSome)let activeBuffer := state.getActiveBufferlet activeIdx := buffers.findIdx? (fun b => b.id == activeBuffer.id) |>.getD 0-- Create content:-- FILE1-- ROW COL-- ...-- --ACTIVE---- INDEXlet mut content := ""for b in buffers docontent := content ++ b.filename.get! ++ "\n"if b.id == activeBuffer.id thenlet cursor := state.getCursorcontent := content ++ s!"{cursor.row} {cursor.col}\n"elsecontent := content ++ "0 0\n"content := content ++ "--ACTIVE--\n"content := content ++ s!"{activeIdx}\n"IO.FS.writeFile sessionFile content/-- Load session state from temporary file. -/def loadSession : IO (Option (List String × Nat × List (Nat × Nat))) := doif ← System.FilePath.pathExists sessionFile thenlet content ← IO.FS.readFile sessionFile-- Clean up the file immediately so next run doesn't pick it up if not intendedIO.FS.removeFile sessionFilelet lines := content.splitOn "\n"let rec parseFiles (lines : List String) (accFiles : List String) (accCursors : List (Nat × Nat)) : Option (List String × Nat × List (Nat × Nat)) :=match lines with| [] => none| "--ACTIVE--" :: idxStr :: _ =>let idx := idxStr.toNat!some (accFiles.reverse, idx, accCursors.reverse)| fname :: coords :: rest =>let parts := coords.splitOn " "match parts with| [r, c] =>let cursor := (r.toNat!, c.toNat!)parseFiles rest (fname :: accFiles) (cursor :: accCursors)| _ => none -- Parse error| _ => nonereturn parseFiles lines [] []elsereturn noneend ViE.Checkpoint
import ViE.Buffer.Managerimport ViE.Buffer.Content
import ViE.Stateimport ViE.Typesimport ViE.Buffer.Contentimport ViE.Buffer.LowIOnamespace ViE.Bufferopen ViEdef createNewBuffer (state : EditorState) (content : TextBuffer) (filename : Option String) : (Nat × EditorState) :=let s' := state.updateCurrentWorkgroup fun wg =>let id := wg.nextBufferIdlet buf : FileBuffer := ViE.Buffer.fileBufferFromTextBuffer id filename content{ wg with buffers := buf :: wg.buffers, nextBufferId := id + 1 }let wg := s'.getCurrentWorkgroup(wg.nextBufferId - 1, s')def findBufferByFilename (state : EditorState) (fname : String) : Option Nat :=let wg := state.getCurrentWorkgroupwg.buffers.find? (fun b => b.filename == some fname) |>.map (fun b => b.id)/-- Open a file buffer in the active window. Loads from disk if necessary. -/def openBuffer (state : EditorState) (filename : String) : IO EditorState := dolet resolvedPath := state.workspace.resolvePath filenamematch findBufferByFilename state resolvedPath with| some bid =>-- Switch to existing bufferlet s' := state.updateActiveView fun v => { v with bufferId := bid, cursor := {row:=0, col:=0}, scrollRow:=0, scrollCol:=0 }return { s' with message := s!"Switched to \"{filename}\"" }| none =>trylet loadedBuf ← ViE.loadBufferByteArray resolvedPath-- Assign new ID and add to workgrouplet s' := state.updateCurrentWorkgroup fun wg =>let id := wg.nextBufferIdlet buf := { loadedBuf with id := id }{ wg with buffers := buf :: wg.buffers, nextBufferId := id + 1 }let wg := s'.getCurrentWorkgrouplet bid := wg.nextBufferId - 1let s'' := s'.updateActiveView fun v => { v with bufferId := bid, cursor := {row:=0, col:=0}, scrollRow:=0, scrollCol:=0 }return { s'' with message := s!"Loaded \"{filename}\"" }catch e =>return { state with message := s!"Error loading file: {e}" }end ViE.Buffer
import ViE.Typesimport ViE.Data.PieceTablenamespace ViEopen ViE.PieceTable/-- Load buffer from file using PieceTable -/def loadBufferByteArray (filename : String) : IO FileBuffer := dotrylet path := System.FilePath.mk filenameif ← path.pathExists thenif ← path.isDir then-- Directory: return empty bufferreturn {id := 0filename := some filenamedirty := falsetable := PieceTable.fromString ""missingEol := false}else-- Read file as ByteArraylet data ← IO.FS.readBinFile filename-- Check if file ends with newline (POSIX compliance)let missingEol := data.size > 0 && data[data.size - 1]! != 10let table := PieceTable.fromByteArray datareturn {id := 0filename := some filenamedirty := falsetable := tablemissingEol := missingEol}else-- File doesn't exist, return empty bufferreturn {id := 0filename := some filenamedirty := falsetable := PieceTable.fromString ""missingEol := false}catch _ =>-- On error, return empty bufferreturn {id := 0filename := some filenamedirty := falsetable := PieceTable.fromString ""missingEol := false}end ViE
import ViE.Typesimport ViE.Basicimport ViE.Data.PieceTablenamespace ViEopen ViE.PieceTable/-- Get a line from FileBuffer (delegates to PieceTable) -/def getLineFromBuffer (buffer : FileBuffer) (n : Nat) : Option String :=buffer.table.getLine n/-- Get line length from FileBuffer (delegates to PieceTable) -/def getLineLengthFromBuffer (buffer : FileBuffer) (n : Nat) : Option Nat :=buffer.table.getLineLength nnamespace Buffer/-- Convert FileBuffer to TextBuffer (compatibility function) -/def fileBufferToTextBuffer (buf : FileBuffer) : TextBuffer :=let lineCount := FileBuffer.lineCount bufif lineCount > 0 then-- Map each line index to its string content(List.range lineCount).toArray.map fun i =>match getLineFromBuffer buf i with| some str => stringToLine str| none => #[]else-- Empty buffer - return single empty line#[#[]]/-- Create/Update FileBuffer from TextBuffer (flattening is expensive, avoided if possible) -/def fileBufferFromTextBuffer (id : Nat) (filename : Option String) (content : TextBuffer) : FileBuffer :=let fullString := String.intercalate "\n" (content.toList.map lineToString)let table := PieceTable.fromString fullString{id := idfilename := filenamedirty := true -- Assume dirty if created from manual contenttable := tablemissingEol := false -- Default}/-- Update FileBuffer from TextBuffer (compatibility function) -/def fileBufferUpdateFromTextBuffer (buf : FileBuffer) (newContent : TextBuffer) : FileBuffer :=let newBuf := fileBufferFromTextBuffer buf.id buf.filename newContent{ newBuf with dirty := true }end Buffer/-- Modify a line in FileBuffer using PieceTable operations -/def modifyLineInBuffer (buffer : FileBuffer) (row : Nat) (f : String → String) : FileBuffer :=match buffer.table.getLineRange row with| some (startOffset, length) =>match buffer.table.getLine row with| some oldLine =>let newLine := f oldLine-- Edit: Delete old line content, insert new content.-- We preserve the newline character if it exists (getLineRange excludes it).let table' := buffer.table.delete startOffset lengthlet table'' := table'.insert startOffset newLine{ buffer withtable := table''dirty := true}| none => buffer -- Should check range first, but safe fallback| none => buffer -- Line not foundend ViE
import ViE.Typesnamespace ViE/-- Convert String to Line (Array Char) -/def stringToLine (s : String) : Line :=s.toList.toArray/-- Convert Line (Array Char) to String -/def lineToString (line : Line) : String :=String.ofList line.toList/-- Empty buffer with one empty line -/def emptyTextBuffer : TextBuffer :=#[#[]]/-- Helper to safely get a line as String. -/def getLine (buffer : TextBuffer) (n : Nat) : Option String :=if h : n < buffer.size thensome (lineToString buffer[n])elsenone/-- Helper to update a specific line in the buffer. -/def modifyLine (buffer : TextBuffer) (row : Nat) (f : String → String) : TextBuffer :=if h : row < buffer.size thenlet oldLine := buffer[row]let newLine := stringToLine (f (lineToString oldLine))buffer.set! row newLineelsebuffer/-- Set a line directly (String version for compatibility) -/def setLine (buffer : TextBuffer) (row : Nat) (content : String) : TextBuffer :=buffer.set! row (stringToLine content)/-- Take first n elements from Array -/def arrayTake {α : Type _} (arr : Array α) (n : Nat) : Array α :=arr.extract 0 n/-- Drop first n elements from Array -/def arrayDrop {α : Type _} (arr : Array α) (n : Nat) : Array α :=arr.extract n arr.size/-- Concatenate multiple arrays -/def arrayConcat {α : Type _} (arrays : List (Array α)) : Array α :=arrays.foldl (· ++ ·) #[]end ViE
import ViE.Basicimport ViE.Stateimport ViE.Terminalimport ViE.UIimport ViE.Actionsimport ViE.IOimport ViE.Configimport ViE.Checkpointimport ViE.Loadernamespace ViEopen ViE.PieceTable/-- Main event loop. -/partial def loop (config : Config) (state : EditorState) : IO Unit := do-- Only render if state is dirtyif state.dirty thenViE.UI.render state-- Reset dirty flag after render (or if it was already clean, keep it clean)let state := { state with dirty := false }let currentTime ← IO.monoMsNowlet c ← ViE.Terminal.readKey-- Update window sizelet (rows, cols) ← ViE.Terminal.getWindowSizelet state := { state with windowHeight := rows, windowWidth := cols }-- Handle the case where readKey returns None (no input available)match c with| none =>-- Check for timeoutlet (stateAfterTimeout, keys) := ViE.checkTimeout state currentTimeif keys.isEmpty then-- No input and no timeout events, sleep briefly to avoid busy loopIO.sleep 10 -- 10msloop config stateAfterTimeoutelse-- Process timeout keys (e.g. flushed Esc)let mut finalState := stateAfterTimeoutfor key in keys dofinalState ← ViE.update config finalState keyif finalState.shouldQuit thenreturn ()loop config { finalState with dirty := true }| some ch =>-- Parse the character into keys using the new parseKey functionlet (stateAfterParse, keys) := ViE.parseKey state ch currentTime-- Process all returned keyslet mut finalState := stateAfterParsefor key in keys dofinalState ← ViE.update config finalState keyif finalState.shouldQuit thenreturn ()loop config { finalState with dirty := true }/-- Entry point for the editor.Accepts a configuration and command line arguments. -/def start (config : Config) (args : List String) : IO Unit := do-- Check for checkpoint sessionlet checkpoint ← ViE.Checkpoint.loadSession-- Determine initial args (files to open)-- If we have a checkpoint and no args, use the checkpoint files.-- Otherwise use provided args.let (startArgs, _) := match checkpoint with| some (files, activeIdx, cursors) =>if args.isEmpty then(files, some (activeIdx, cursors))else(args, none)| none => (args, none)let filename := startArgs.head?-- Check if the argument is a directorylet (workspacePath, actualFilename) ← match filename with| some path =>let filePath := System.FilePath.mk pathif ← filePath.pathExists thenif ← filePath.isDir then-- It's a directory, use as workspacepure (some path, none)else-- It's a filepure (none, some path)else-- Path doesn't exist, treat as new filepure (none, some path)| none => pure (none, none)-- Load buffer if file existslet initialBuffer ← match actualFilename with| some fname => loadBufferByteArray fname| none => pure {id := 0filename := actualFilenamedirty := falsetable := PieceTable.fromString ""missingEol := false}-- Check if initial load had an errorlet firstLine := getLineFromBuffer initialBuffer 0 |>.getD ""let hasError := firstLine.startsWith "Error loading file:"let workspace := match workspacePath with| some path => { rootPath := some path }| none => ViE.defaultWorkspacelet state := { ViE.initialState withconfig := config.settings,workspace := workspace,message := if hasError then firstLineelse match actualFilename with| some f => s!"\"{f}\" [Read]"| none => match workspacePath with| some ws => s!"Workspace: {ws}"| none => "New File"}-- Update the first workgroup with the initial bufferlet state := state.updateCurrentWorkgroup fun wg =>{ wg with buffers := [initialBuffer] }ViE.Terminal.enableRawModetryloop config statefinallyViE.Terminal.disableRawModeViE.Terminal.clearScreenIO.println "Bye!"end ViE
import ViE.Stateimport ViE.Commandimport ViE.Keyimport ViE.IOimport ViE.Configimport ViE.Basicimport ViE.Loaderimport ViE.Checkpointimport ViE.Typesimport ViE.Window.Actionsimport ViE.Buffer.Managernamespace ViE/-- Insert a character at the cursor position. -/def insertAtCursor (state : EditorState) (c : Char) : EditorState :=state.insertChar c/-- Parse filename argument from command args.Returns the filename from args, or the current buffer's filename if no args provided.Returns error message if multiple args or no filename available. -/def parseFilenameArg (args : List String) (currentFilename : Option String) : Except String String :=match args with| [fname] => .ok fname| [] => match currentFilename with| some fname => .ok fname| none => .error "No file name"| _ => .error "Too many arguments"def executeCommand (commands : CommandMap) (state : EditorState) : IO EditorState := dolet fullCmd := state.inputState.commandBufferlet parts := fullCmd.splitOn " "match parts with| [] => return state| cmd :: args =>match commands.lookup cmd with| some action => action args state| none => return { state with message := s!"Unknown command: {fullCmd}" }/-- Handle Enter key in explorer buffer -/def handleExplorerEnter (commands : CommandMap) (state : EditorState) : IO EditorState := dolet buf := state.getActiveBuffer-- Check if this is an explorer bufferlet explorerOpt := state.explorers.find? (fun (id, _) => id == buf.id)match explorerOpt with| none => return state.insertNewline -- Not an explorer, normal behavior| some (_, explorer) =>-- Get current cursor rowlet cursor := state.getCursor-- Explorer header is 2 lines, so subtract 2let selectedIdx := cursor.row - 2if selectedIdx < 0 || selectedIdx >= explorer.entries.length thenreturn state-- Get selected entrylet entry := explorer.entries[selectedIdx]!if entry.isDirectory then-- Navigate to directorylet s' := { state with inputState := { state.inputState with commandBuffer := s!"ee {entry.path}" } }executeCommand commands s'else-- Open filelet s' := { state with inputState := { state.inputState with commandBuffer := s!"e {entry.path}" } }executeCommand commands s'def handleCommandInput (commands : CommandMap) (state : EditorState) (k : Key) : IO EditorState := domatch k with| Key.esc => return { state with mode := .normal, message := "", inputState := { state.inputState with commandBuffer := "" } }| Key.enter =>let newState ← executeCommand commands stateif newState.shouldQuit thenreturn newStateelsereturn { newState with mode := .normal, inputState := { newState.inputState with commandBuffer := "" } }| Key.backspace =>if state.inputState.commandBuffer.length > 0 thenlet newCmd := state.inputState.commandBuffer.dropEnd 1return { state with inputState := { state.inputState with commandBuffer := newCmd.toString } }elsereturn state| Key.char c => return { state with inputState := { state.inputState with commandBuffer := state.inputState.commandBuffer.push c } }| _ => return statedef clearInput (s : EditorState) : EditorState :={ s with inputState := { s.inputState with countBuffer := "", previousKey := none } }end ViE
# vi'e### A Vim-like Extensible text editor(very WIP)### installationclone this source and place or symlink this source under the '~/.config/vie/' directory.`lake build` and `lake exe editor` or add PATH to the '<this source directory>/.lake/build/bin/' directory.## Features- Basic text editing- Line numbers- Horizontal scrolling- Vertical scrolling- Modes- Command mode- Insert mode- Normal mode- Visual mode- VisualBlock mode- Split window buffer- Render UTF-8 characters (partial support)- Workgroups- Hot reload(recompile and reload) configuration- place this source and Main.lean in '~/.config/editor/' and :reload## Keybindings### Normal Mode- `h`: Move cursor left- `j`: Move cursor down- `k`: Move cursor up- `l`: Move cursor right- `i`: Enter Insert Mode- `:`; Enter Command Mode- `q`: Enter Command Mode (alias)- `d`: Delete current line (press twice `dd`)- `v`: Enter Visual Mode- `V`: Enter VisualBlock Mode- `gg`: Move to top of file- `G`: Move to bottom of file- `|`: Jump to column (preceded by number)- `[number]`: Type a number to set count for `|` or `G` command- `Enter`: Move cursor down### Insert Mode- `Esc`: Return to Normal Mode- `Backspace`: Delete character before cursor- `Enter`: Insert newline### Command Mode- `Esc`: Return to Normal Mode- `Enter`: Execute command### Visual/VisualBlock Mode- `h/j/k/l`: select more- `Esc`: Return to Normal Mode## Commands- `:w`: Save file- `:q`: Quit window- `:q!`: force quit- `:wq`: Save and quit- `:set number`: Show line numbers- `:set nonumber`: Hide line numbers- `:vs`, `:hs` vertical and horizontal window splitting- `:wincmd h/j/k/l` change focus select window- `:wh`, `:wj`, `:wk`, `:wl` change focus select window (alias)- `:wincmd w`, `:wc` cyclic selection- `:wg <n>` switch to workgroup <n>- `:reload`: reload configuration
import ViEimport ViE.Coloropen ViE.Colordef customConfig : ViE.EditorConfig := {ViE.defaultConfig withshowLineNumbers := truestatusBarStyle := (toBg Color.brightBlack) ++ (toFg Color.white)fileIcon := "File: "dirIcon := "Dir : "}def customKeyMap (commands : ViE.CommandMap) : ViE.KeyMap := {ViE.Key.makeKeyMap commands withnormal := fun s k => match k with-- Example custom binding: 'X' to quit| ViE.Key.char 'X' => pure { s with shouldQuit := true }| _ => (ViE.Key.makeKeyMap commands).normal s kvisual := fun s k => match k with| ViE.Key.char 'o' => pure (ViE.Window.cycleWindow s)| _ => (ViE.Key.makeKeyMap commands).visual s k}def myCustomCommand (_ : List String) (state : ViE.EditorState) : IO ViE.EditorState := doreturn { state with message := "Executed custom command!" }def main (args : List String) : IO Unit := dolet myCommands : ViE.CommandMap := [("mycmd", myCustomCommand),("o", ViE.Command.cmdWinCycle)]let commands := ViE.Command.defaultCommandMap ++ myCommandslet config : ViE.Config := {settings := customConfigbindings := customKeyMap commandscommands := commands}ViE.start config args
Copyright 2026 Yuki OtsukaRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
.git.DS_Store.lake