TKAAHADV62OQORAO7MYRRRHTGPMG3GKXU4Z4MFNLAO6DLTSDU3CAC SEIYD7YXZ5XI4W7UVF5O2UFLA6OCZZCOLV2XB46RKJ5EN6UQC4UAC GUD2J453E3UTRJZIEEUUJ3ZIGL72ETUXTO57XQIZ2Y3ZU3C2R55QC 6W6OJAUT3KLOXXKYR4GA3MG4ACLO2T4KX3SGD2KZDX6MYEZYTPHAC XCVPXP4UBFU25NF6VQC3MEYHHH45RL2UXM5TA6O23NA64FPCCCJQC YILSJ2SNU6DNZVSFL4MD6QSNLQHUNSBSTGGEQCFTWFTRR62IXBWAC CNJGJCJZ6LRHHIRSRATSE3D3Z5OQGSSTDMXCPVXSKRQLYJFME6IAC NBG3GUEJJMWS2FRJDU26KRQLA6O2YK77PDAPBMD3SKWLDTK23BVAC WRBKZMYVNHRWT7TTUGTDJ3TMWZB32QYW5PCLKTTVAJ2YF6OI3LTAC U45XPF3YKPXXRJ4MN24T2RV7GOL4FZKQSWX5P5I7WT4HC5XF4FHAC OHL2PRHBX74FZKWZU7IR6RSXA36PDZDROVI4AHTX4SO7VQFQ5RQQC KJLCU4X5Z3CZM4AORMWIKOPBWPVTDMLK4LYH7UXBJFUGL7AUQCSQC 5SFTBD4FW6GQPKRUJBAII5R2EZPDYG6CH57BIJD4IVZBE6JZB2ZQC 6UFPT4GPGYM3ORW35EIY5A7Z34WCTXRSWW45T625F2WPMLGR3NHAC O763V2GX3Z2JT5EOWNAVVGJ2SDDJ3ZMB63D267P2YXG5SIC6RGGAC { ws with layout := updateLayout ws.layout, nextWindowId := nextId + 1 }
let ws' := { ws with layout := updateLayout ws.layout, nextWindowId := nextId + 1 }let ws' :=if activeWasFloating then(ws'.setWindowFloating nextId true).setFloatingWindowOffset nextId activeRowOff activeColOffelsews'ws'.pruneFloatingWindows
def nudgeActiveFloatingWindow (state : EditorState) (dir : Direction) (step : Int := 1) : EditorState :=let ws := state.getCurrentWorkspacelet activeId := ws.activeWindowIdif !ws.isFloatingWindow activeId then{ state with message := "active window is not floating" }elsematch ws.findFloatingSubtree activeId with| none => { state with message := "floating window subtree not found" }| some subtree =>let ids := ViE.Window.getWindowIds subtreelet dRow : Int :=match dir with| .up => -step| .down => step| _ => 0let dCol : Int :=match dir with| .left => -step| .right => step| _ => 0let s' := state.updateCurrentWorkspace fun ws =>ids.foldl (fun acc wid => acc.adjustFloatingWindowOffset wid dRow dCol) ws{ s' with dirty := true, message := "floating window moved" }
let (h, w) := state.getActiveWindowBounds-- Active window height already excludes the global status line.let linesInView := if h > 0 then h else 1
let (h, w) := state.getActiveWindowScrollBoundslet linesInView := if h > 1 then h - 1 else 1
import ViE.Stateimport ViE.Terminalimport ViE.Configimport ViE.Unicodeimport ViE.Colornamespace ViE.UIopen ViE/-- Find all occurrences of a byte pattern within a byte array. -/partial def findAllMatchesBytes (haystack : ByteArray) (needle : ByteArray) : Array (Nat × Nat) :=let n := needle.sizelet h := haystack.sizeif n == 0 || h < n then#[]elselet rec matchesAt (i j : Nat) : Bool :=if j == n then trueelse if haystack[i + j]! == needle[j]! then matchesAt i (j + 1) else falselet rec loop (i : Nat) (acc : Array (Nat × Nat)) : Array (Nat × Nat) :=if i + n > h then accelselet acc' := if matchesAt i 0 then acc.push (i, i + n) else accloop (i + 1) acc'loop 0 #[]def overlapsByteRange (r : Nat × Nat) (byteStart byteEnd : Nat) : Bool :=let (s, e) := rbyteStart < e && byteEnd > sdef activeMatchRange (hitRanges : Array (Nat × Nat)) (cursorByte : Option Nat) : Option (Nat × Nat) :=match cursorByte with| none => none| some cb =>let rec loop (i : Nat) : Option (Nat × Nat) :=if i >= hitRanges.size thennoneelselet m := hitRanges[i]!let (s, e) := mif s <= cb && cb < e thensome melseloop (i + 1)loop 0def updateSearchLineCache (st : EditorState) (lineIdx : Row) (lineStr : String) (matchRanges : Array (Nat × Nat)) : EditorState :=match st.searchState with| none => st| some ss =>let lineMatches := ss.lineMatches.insert lineIdx (lineStr, matchRanges)let order :=if ss.lineOrder.contains lineIdx thenss.lineOrderelsess.lineOrder.push lineIdxlet (lineMatches, order) :=if order.size > ss.lineCacheMax thenlet dropCount := order.size - ss.lineCacheMaxlet evicted := order.extract 0 dropCountlet order := order.extract dropCount order.sizelet lineMatches := evicted.foldl (fun acc r => acc.erase r) lineMatches(lineMatches, order)else(lineMatches, order){ st with searchState := some { ss with lineMatches := lineMatches, lineOrder := order } }/-- 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. -/def takeVisual (s : Substring.Raw) (width : Nat) : String :=Unicode.takeByDisplayWidth s width/-- Render the current editor state to the terminal. -/def render (state : EditorState) : IO EditorState := doif !state.dirty then return state-- Start building the frame bufferlet mut buffer : Array String := #[]buffer := buffer.push Terminal.hideCursorStrbuffer := buffer.push Terminal.homeCursorStrlet (rows, cols) ← Terminal.getWindowSizelet ws := state.getCurrentWorkspacelet getBuffer (st : EditorState) (id : Nat) : FileBuffer :=let ws := st.getCurrentWorkspacews.buffers.find? (fun b => b.id == id) |>.getD initialFileBufferlet rec renderLayout (l : Layout) (st : EditorState) (r c h w : Nat) : (Array String × EditorState) := Id.run domatch l with| Layout.window id view =>let prefetchSearchMatches (st : EditorState) (buf : FileBuffer) (startRow endRow : Nat) : EditorState :=match st.searchState with| none => st| some ss =>if ss.pattern.isEmpty thenstelselet totalLines := FileBuffer.lineCount buflet rec loop (row : Nat) (acc : EditorState) : EditorState :=if row >= endRow thenaccelse if row >= totalLines thenaccelselet lineIdx : Row := ⟨row⟩let lineStr := getLineFromBuffer buf lineIdx |>.getD ""match acc.searchState with| none => acc| some ss2 =>match ss2.lineMatches.find? lineIdx with| some (cachedLine, _) =>if cachedLine == lineStr thenloop (row + 1) accelselet matchRanges := findAllMatchesBytes lineStr.toUTF8 ss2.pattern.toUTF8let acc' := updateSearchLineCache acc lineIdx lineStr matchRangesloop (row + 1) acc'| none =>let matchRanges := findAllMatchesBytes lineStr.toUTF8 ss2.pattern.toUTF8let acc' := updateSearchLineCache acc lineIdx lineStr matchRangesloop (row + 1) acc'loop startRow stlet workH := if h > 0 then h - 1 else 0let buf := getBuffer st view.bufferIdlet mut currentSt := stlet mut winBuf : Array String := #[]let prefetchMargin := 20let totalLines := FileBuffer.lineCount buflet startRow := if view.scrollRow.val > prefetchMargin then view.scrollRow.val - prefetchMargin else 0let endRow := min totalLines (view.scrollRow.val + workH + prefetchMargin)currentSt := prefetchSearchMatches currentSt buf startRow endRowfor i in [0:workH] dolet lineIdx : Row := ⟨view.scrollRow.val + i⟩let screenRow := r + i-- Position cursor for this linewinBuf := winBuf.push (Terminal.moveCursorStr screenRow c)if lineIdx.val < FileBuffer.lineCount buf then-- Check cachelet lnWidth := if st.config.showLineNumbers then 4 else 0let availableWidth := if w > lnWidth then w - lnWidth else 0let cachedLine := buf.cache.find lineIdxlet cachedRaw := buf.cache.findRaw lineIdxlet cachedIdx := buf.cache.findIndex lineIdxlet lineStr := cachedRaw.getD (getLineFromBuffer buf lineIdx |>.getD "")let lineIndex := cachedIdx.getD (ViE.Unicode.buildDisplayByteIndex lineStr)let (displayLine, nextSt) := match cachedLine with| some (s, cachedScrollCol, cachedWidth) =>if cachedScrollCol == view.scrollCol.val && cachedWidth == availableWidth then(s, currentSt)else-- Cache miss; fall through to recompute.match getLineFromBuffer buf lineIdx with| some lineStr =>let sub := ViE.Unicode.dropByDisplayWidth lineStr.toRawSubstring view.scrollCol.vallet dl := takeVisual sub availableWidthlet cache := match cachedRaw, cachedIdx with| some raw, some _ =>if raw == lineStr thenbuf.cacheelse(buf.cache.updateIndex lineIdx (ViE.Unicode.buildDisplayByteIndex lineStr))| _, _ =>(buf.cache.updateIndex lineIdx (ViE.Unicode.buildDisplayByteIndex lineStr))let cache := (cache.update lineIdx dl view.scrollCol.val availableWidth).updateRaw lineIdx lineStrlet updatedBuf := { buf with cache := cache }let s' := currentSt.updateCurrentWorkspace fun ws =>{ ws with buffers := ws.buffers.map (fun (b : FileBuffer) => if b.id == buf.id then updatedBuf else b) }(dl, s')| none => ("", currentSt)| none =>if let some lineStr := getLineFromBuffer buf lineIdx thenlet sub := ViE.Unicode.dropByDisplayWidth lineStr.toRawSubstring view.scrollCol.vallet dl := takeVisual sub availableWidthlet cache := match cachedRaw, cachedIdx with| some raw, some _ =>if raw == lineStr thenbuf.cacheelse(buf.cache.updateIndex lineIdx (ViE.Unicode.buildDisplayByteIndex lineStr))| _, _ =>(buf.cache.updateIndex lineIdx (ViE.Unicode.buildDisplayByteIndex lineStr))-- Update cache in currentStlet cache := (cache.update lineIdx dl view.scrollCol.val availableWidth).updateRaw lineIdx lineStrlet updatedBuf := { buf with cache := cache }let s' := currentSt.updateCurrentWorkspace fun ws =>{ ws with buffers := ws.buffers.map (fun (b : FileBuffer) => if b.id == buf.id then updatedBuf else b) }(dl, s')else ("", currentSt)currentSt := nextStif st.config.showLineNumbers thenwinBuf := winBuf.push s!"{leftPad (toString (lineIdx.val + 1)) 3} "let isVisual := st.mode == Mode.visual || st.mode == Mode.visualBlocklet selRange := if isVisual then st.selectionStart else nonelet (searchMatches, searchSt) :=match st.searchState with| some ss =>if ss.pattern.isEmpty then(#[], currentSt)elsematch ss.lineMatches.find? lineIdx with| some (cachedLine, cachedMatches) =>if cachedLine == lineStr then(cachedMatches, currentSt)elselet matchRanges := findAllMatchesBytes lineStr.toUTF8 ss.pattern.toUTF8let st' := updateSearchLineCache currentSt lineIdx lineStr matchRanges(matchRanges, st')| none =>let matchRanges := findAllMatchesBytes lineStr.toUTF8 ss.pattern.toUTF8let st' := updateSearchLineCache currentSt lineIdx lineStr matchRanges(matchRanges, st')| none => (#[], currentSt)currentSt := searchStlet needsStyled := selRange.isSome || !searchMatches.isEmptyif !needsStyled thenwinBuf := winBuf.push displayLineelselet mut styleActive := falselet chars := displayLine.toListlet mut dispIdx := 0let cursorByte :=if lineIdx == view.cursor.row thensome (ViE.Unicode.displayColToByteOffsetFromIndex lineIndex view.cursor.col.val)elsenonelet activeMatch := activeMatchRange searchMatches cursorBytefor ch in chars dolet w := Unicode.charWidth chlet colVal := view.scrollCol.val + dispIdxlet byteStart := ViE.Unicode.displayColToByteOffsetFromIndex lineIndex colVallet byteEnd := ViE.Unicode.displayColToByteOffsetFromIndex lineIndex (colVal + w)let isSelected :=st.isInSelection lineIdx ⟨colVal⟩ ||(w > 1 && st.isInSelection lineIdx ⟨colVal + 1⟩)let isMatched :=searchMatches.any (fun m => overlapsByteRange m byteStart byteEnd)let isActiveMatched :=match activeMatch with| some m => overlapsByteRange m byteStart byteEnd| none => falseif isSelected thenif !styleActive thenwinBuf := winBuf.push "\x1b[7m" -- Inverse videostyleActive := trueelse if isMatched thenif !styleActive thenif isActiveMatched thenwinBuf := winBuf.push st.config.searchHighlightCursorStyleelsewinBuf := winBuf.push st.config.searchHighlightStylestyleActive := trueelse if styleActive thenwinBuf := winBuf.push "\x1b[0m"styleActive := falsewinBuf := winBuf.push ch.toStringdispIdx := dispIdx + wif styleActive then winBuf := winBuf.push "\x1b[0m"elsewinBuf := winBuf.push st.config.emptyLineMarker-- Clear the rest of the linewinBuf := winBuf.push Terminal.clearLineStrlet statusRow := r + workHwinBuf := winBuf.push (Terminal.moveCursorStr statusRow c)let fileName := buf.filename.getD "[No Name]"let modeStr := if id == currentSt.getCurrentWorkspace.activeWindowId then s!"-- {st.mode} --" else "--"let eolMark := if buf.missingEol then " [noeol]" else ""let statusStr := s!"{modeStr} {fileName}{eolMark} [W:{id} B:{view.bufferId}] [{st.getCurrentWorkgroup.name}] {st.getCurrentWorkspace.name}"winBuf := winBuf.push st.config.statusBarStylewinBuf := winBuf.push statusStr.trimAscii.toStringwinBuf := winBuf.push Terminal.clearLineStrwinBuf := winBuf.push st.config.resetStyle(winBuf, currentSt)| Layout.hsplit left right ratio =>let leftW := (Float.ofNat w * ratio).toUInt64.toNatlet (leftStr, st') := renderLayout left st r c h leftW-- Draw vertical separatorlet mut sepStr : Array String := #[]if w > leftW thenlet sepCol := c + leftWfor i in [0:h] dosepStr := sepStr.push (Terminal.moveCursorStr (r + i) sepCol)sepStr := sepStr.push st.config.vSplitStrlet (rightStr, st'') := renderLayout right st' r (c + leftW + 1) h (if w > leftW then w - leftW - 1 else 0)(leftStr ++ sepStr ++ rightStr, st'')| Layout.vsplit top bottom ratio =>let topH := (Float.ofNat h * ratio).toUInt64.toNatlet (topStr, st') := renderLayout top st r c topH w-- Draw horizontal separatorlet mut sepStr : Array String := #[]if h > topH thenlet sepRow := r + topHsepStr := sepStr.push (Terminal.moveCursorStr sepRow c)for _ in [0:w] dosepStr := sepStr.push st.config.hSplitStrlet (bottomStr, st'') := renderLayout bottom st' (r + topH + 1) c (if h > topH then h - topH - 1 else 0) w(topStr ++ sepStr ++ bottomStr, st'')let layoutH := rows - 1let (layoutStr, state) := renderLayout ws.layout state 0 0 layoutH colsbuffer := buffer ++ layoutStr-- Global Status / Command Linebuffer := buffer.push (Terminal.moveCursorStr (rows - 1) 0)let statusRight :=if state.mode == .command then s!":{state.inputState.commandBuffer}"else if state.mode == .searchForward then s!"/{state.inputState.commandBuffer}"else if state.mode == .searchBackward then s!"?{state.inputState.commandBuffer}"else state.messagebuffer := buffer.push statusRightbuffer := buffer.push 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 == ws.activeWindowId then-- let buf := getBuffer state 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 then-- let rIdx : Row := ⟨view.scrollRow.val + visualRow⟩if view.cursor.col.val < view.scrollCol.val thennoneelselet visualCol := view.cursor.col.val - view.scrollCol.valif (visualCol + colOffset) < w thensome (r + visualRow, c + visualCol + colOffset)else 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.push (match getCursorPos ws.layout 0 0 layoutH cols with| some (pr, pc) => Terminal.moveCursorStr pr pc| none => "" -- Cursor hidden or out of view)if state.mode == .command thenbuffer := buffer.push (Terminal.moveCursorStr (rows - 1) (1 + state.inputState.commandBuffer.length))if state.mode == .searchForward || state.mode == .searchBackward thenbuffer := buffer.push (Terminal.moveCursorStr (rows - 1) (1 + state.inputState.commandBuffer.length))if state.mode == .visual || state.mode == .visualBlock thenbuffer := buffer.push Terminal.hideCursorStrelsebuffer := buffer.push Terminal.showCursorStr-- Output everythinglet finalStr := String.intercalate "" buffer.toListIO.print finalStr(← IO.getStdout).flushreturn stateend ViE.UI
import ViE.UI.Primitivesimport ViE.UI.Searchimport ViE.UI.Overlayimport ViE.UI.Windowimport ViE.UI.Render
import ViE.UI.Primitivesimport ViE.UI.Searchimport ViE.Terminalnamespace ViE.UIopen ViEdef renderWindow (state : EditorState) (windowId : Nat) (view : ViewState) (rect : Rect) : (Array String × EditorState) := Id.run dolet r := rect.rowlet c := rect.collet h := rect.heightlet w := rect.widthlet workH := if h > 0 then h - 1 else 0let buf0 := getWorkspaceBuffer state view.bufferIdlet mut currentSt := statelet mut winBuf : Array String := #[]let prefetchMargin := 20let totalLines := FileBuffer.lineCount buf0let startRow := if view.scrollRow.val > prefetchMargin then view.scrollRow.val - prefetchMargin else 0let endRow := min totalLines (view.scrollRow.val + workH + prefetchMargin)currentSt := prefetchSearchLineMatches currentSt buf0 startRow endRowfor i in [0:workH] dolet lineIdx : Row := ⟨view.scrollRow.val + i⟩let screenRow := r + iwinBuf := winBuf.push (Terminal.moveCursorStr screenRow c)let buf := getWorkspaceBuffer currentSt view.bufferIdif lineIdx.val < FileBuffer.lineCount buf thenlet lnWidth := if state.config.showLineNumbers then 4 else 0let availableWidth := if w > lnWidth then w - lnWidth else 0let cachedLine := buf.cache.find lineIdxlet cachedRaw := buf.cache.findRaw lineIdxlet cachedIdx := buf.cache.findIndex lineIdxlet lineStr := cachedRaw.getD (getLineFromBuffer buf lineIdx |>.getD "")let lineIndex := cachedIdx.getD (ViE.Unicode.buildDisplayByteIndex lineStr)let (displayLine, nextSt) :=match cachedLine with| some (s, cachedScrollCol, cachedWidth) =>if cachedScrollCol == view.scrollCol.val && cachedWidth == availableWidth then(s, currentSt)elsematch getLineFromBuffer buf lineIdx with| some lineStr =>let sub := ViE.Unicode.dropByDisplayWidth lineStr.toRawSubstring view.scrollCol.vallet dl := takeVisual sub availableWidthlet cache := match cachedRaw, cachedIdx with| some raw, some _ =>if raw == lineStr thenbuf.cacheelse(buf.cache.updateIndex lineIdx (ViE.Unicode.buildDisplayByteIndex lineStr))| _, _ =>(buf.cache.updateIndex lineIdx (ViE.Unicode.buildDisplayByteIndex lineStr))let cache := (cache.update lineIdx dl view.scrollCol.val availableWidth).updateRaw lineIdx lineStrlet updatedBuf := { buf with cache := cache }let s' := currentSt.updateCurrentWorkspace fun ws =>{ ws with buffers := ws.buffers.map (fun (b : FileBuffer) => if b.id == buf.id then updatedBuf else b) }(dl, s')| none => ("", currentSt)| none =>if let some lineStr := getLineFromBuffer buf lineIdx thenlet sub := ViE.Unicode.dropByDisplayWidth lineStr.toRawSubstring view.scrollCol.vallet dl := takeVisual sub availableWidthlet cache := match cachedRaw, cachedIdx with| some raw, some _ =>if raw == lineStr thenbuf.cacheelse(buf.cache.updateIndex lineIdx (ViE.Unicode.buildDisplayByteIndex lineStr))| _, _ =>(buf.cache.updateIndex lineIdx (ViE.Unicode.buildDisplayByteIndex lineStr))let cache := (cache.update lineIdx dl view.scrollCol.val availableWidth).updateRaw lineIdx lineStrlet updatedBuf := { buf with cache := cache }let s' := currentSt.updateCurrentWorkspace fun ws =>{ ws with buffers := ws.buffers.map (fun (b : FileBuffer) => if b.id == buf.id then updatedBuf else b) }(dl, s')else("", currentSt)currentSt := nextStif state.config.showLineNumbers thenwinBuf := winBuf.push s!"{leftPad (toString (lineIdx.val + 1)) 3} "let isVisual := state.mode == Mode.visual || state.mode == Mode.visualBlocklet selRange := if isVisual then state.selectionStart else nonelet (searchMatches, searchSt) := getLineSearchMatches currentSt buf.id lineIdx lineStrcurrentSt := searchStlet needsStyled := selRange.isSome || !searchMatches.isEmptyif !needsStyled thenwinBuf := winBuf.push displayLineelselet mut styleActive := falselet chars := displayLine.toListlet mut dispIdx := 0let cursorByte :=if lineIdx == view.cursor.row thensome (ViE.Unicode.displayColToByteOffsetFromIndex lineIndex view.cursor.col.val)elsenonelet activeMatch := activeMatchRange searchMatches cursorBytefor ch in chars dolet chW := Unicode.charWidth chlet colVal := view.scrollCol.val + dispIdxlet byteStart := ViE.Unicode.displayColToByteOffsetFromIndex lineIndex colVallet byteEnd := ViE.Unicode.displayColToByteOffsetFromIndex lineIndex (colVal + chW)let isSelected :=state.isInSelection lineIdx ⟨colVal⟩ ||(chW > 1 && state.isInSelection lineIdx ⟨colVal + 1⟩)let isMatched :=searchMatches.any (fun m => overlapsByteRange m byteStart byteEnd)let isActiveMatched :=match activeMatch with| some m => overlapsByteRange m byteStart byteEnd| none => falseif isSelected thenif !styleActive thenwinBuf := winBuf.push "\x1b[7m"styleActive := trueelse if isMatched thenif !styleActive thenif isActiveMatched thenwinBuf := winBuf.push state.config.searchHighlightCursorStyleelsewinBuf := winBuf.push state.config.searchHighlightStylestyleActive := trueelse if styleActive thenwinBuf := winBuf.push "\x1b[0m"styleActive := falsewinBuf := winBuf.push ch.toStringdispIdx := dispIdx + chWif styleActive thenwinBuf := winBuf.push "\x1b[0m"elsewinBuf := winBuf.push state.config.emptyLineMarkerwinBuf := winBuf.push Terminal.clearLineStrlet statusRow := r + workHlet statusBuf := getWorkspaceBuffer currentSt view.bufferIdwinBuf := winBuf.push (Terminal.moveCursorStr statusRow c)let fileName := statusBuf.filename.getD "[No Name]"let modeStr := if windowId == currentSt.getCurrentWorkspace.activeWindowId then s!"-- {state.mode} --" else "--"let eolMark := if statusBuf.missingEol then " [noeol]" else ""let statusStr := s!"{modeStr} {fileName}{eolMark} [W:{windowId} B:{view.bufferId}] [{state.getCurrentWorkgroup.name}] {state.getCurrentWorkspace.name}"winBuf := winBuf.push state.config.statusBarStylewinBuf := winBuf.push statusStr.trimAscii.toStringwinBuf := winBuf.push Terminal.clearLineStrwinBuf := winBuf.push state.config.resetStyle(winBuf, currentSt)def renderFloatingWindow(st : EditorState)(view : ViewState)(rect : Rect)(borderFlags : FloatingWindowBorderFlags): (Array String × Option (Nat × Nat)) := Id.run dolet top := rect.rowlet left := rect.collet h := rect.heightlet w := rect.widthlet topBorderH := if borderFlags.top then 1 else 0let rightBorderW := if borderFlags.right then 1 else 0let bottomBorderH := if borderFlags.bottom then 1 else 0let leftBorderW := if borderFlags.left then 1 else 0let topBottomBorderH := topBorderH + bottomBorderHlet sideBorderW := leftBorderW + rightBorderWif h < topBottomBorderH + 1 || w < sideBorderW + 1 thenreturn (#[], none)let buf := getWorkspaceBuffer st view.bufferIdlet innerW := if w > sideBorderW then w - sideBorderW else 1let innerH := if h > topBottomBorderH then h - topBottomBorderH else 1let lnWidth := if st.config.showLineNumbers then 4 else 0let textW := if innerW > lnWidth then innerW - lnWidth else 1let repeatToken (token : String) (n : Nat) : String :=String.intercalate "" (List.replicate n token)let hBorder := if st.config.hSplitStr.isEmpty then "-" else st.config.hSplitStrlet vBorder := if st.config.vSplitStr.isEmpty then "|" else st.config.vSplitStrlet leftBorder := if borderFlags.left then vBorder else ""let rightBorder := if borderFlags.right then vBorder else ""let border := repeatToken hBorder wlet mut out : Array String := #[]out := out.push st.config.resetStyleif borderFlags.top thenout := out.push (Terminal.moveCursorStr top left)out := out.push borderlet isVisual := st.mode == Mode.visual || st.mode == Mode.visualBlocklet hasSelection := isVisual && st.selectionStart.isSomefor i in [0:innerH] dolet lineIdx : Row := ⟨view.scrollRow.val + i⟩let raw := if lineIdx.val < FileBuffer.lineCount buf then getLineFromBuffer buf lineIdx |>.getD "" else ""let sub := ViE.Unicode.dropByDisplayWidth raw.toRawSubstring view.scrollCol.vallet plainShown := takeVisual sub textWlet shownW := Unicode.stringWidth plainShownlet shown :=if hasSelection thenId.run dolet mut styled : Array String := #[]let mut styleActive := falselet mut dispCol := view.scrollCol.valfor ch in plainShown.toList dolet chW := Unicode.charWidth chlet selected :=st.isInSelection lineIdx ⟨dispCol⟩ ||(chW > 1 && st.isInSelection lineIdx ⟨dispCol + 1⟩)if selected thenif !styleActive thenstyled := styled.push "\x1b[7m"styleActive := trueelse if styleActive thenstyled := styled.push st.config.resetStylestyleActive := falsestyled := styled.push ch.toStringdispCol := dispCol + chWif styleActive thenstyled := styled.push st.config.resetStylereturn String.intercalate "" styled.toListelseplainShownlet padW := if shownW < textW then textW - shownW else 0let pad := "".pushn ' ' padWlet withNo :=if st.config.showLineNumbers thens!"{leftPad (toString (lineIdx.val + 1)) 3} {shown}{pad}"elses!"{shown}{pad}"out := out.push (Terminal.moveCursorStr (top + topBorderH + i) left)out := out.push s!"{leftBorder}{withNo}{rightBorder}"if borderFlags.bottom thenout := out.push (Terminal.moveCursorStr (top + h - 1) left)out := out.push borderout := out.push st.config.resetStylelet cursorPos : Option (Nat × Nat) :=if view.cursor.row.val < view.scrollRow.val thennoneelselet visRow := view.cursor.row.val - view.scrollRow.valif visRow < innerH thenlet visCol := if view.cursor.col.val >= view.scrollCol.val then view.cursor.col.val - view.scrollCol.val else 0let colOff := if st.config.showLineNumbers then 4 else 0let c := min (innerW - 1) (colOff + visCol)some (top + topBorderH + visRow, left + leftBorderW + c)elsenonereturn (out, cursorPos)end ViE.UI
import ViE.UI.Primitivesnamespace ViE.UIopen ViE/-- Find all occurrences of a byte pattern within a byte array. -/partial def findAllMatchesBytes (haystack : ByteArray) (needle : ByteArray) : Array (Nat × Nat) :=let n := needle.sizelet h := haystack.sizeif n == 0 || h < n then#[]elselet rec matchesAt (i j : Nat) : Bool :=if j == n then trueelse if haystack[i + j]! == needle[j]! then matchesAt i (j + 1) else falselet rec loop (i : Nat) (acc : Array (Nat × Nat)) : Array (Nat × Nat) :=if i + n > h then accelselet acc' := if matchesAt i 0 then acc.push (i, i + n) else accloop (i + 1) acc'loop 0 #[]def overlapsByteRange (r : Nat × Nat) (byteStart byteEnd : Nat) : Bool :=let (s, e) := rbyteStart < e && byteEnd > sdef activeMatchRange (hitRanges : Array (Nat × Nat)) (cursorByte : Option Nat) : Option (Nat × Nat) :=match cursorByte with| none => none| some cb =>let rec loop (i : Nat) : Option (Nat × Nat) :=if i >= hitRanges.size thennoneelselet m := hitRanges[i]!let (s, e) := mif s <= cb && cb < e thensome melseloop (i + 1)loop 0def updateSearchLineCache (st : EditorState) (lineIdx : Row) (lineStr : String) (matchRanges : Array (Nat × Nat)) : EditorState :=match st.searchState with| none => st| some ss =>let lineMatches := ss.lineMatches.insert lineIdx (lineStr, matchRanges)let order :=if ss.lineOrder.contains lineIdx thenss.lineOrderelsess.lineOrder.push lineIdxlet (lineMatches, order) :=if order.size > ss.lineCacheMax thenlet dropCount := order.size - ss.lineCacheMaxlet evicted := order.extract 0 dropCountlet order := order.extract dropCount order.sizelet lineMatches := evicted.foldl (fun acc r => acc.erase r) lineMatches(lineMatches, order)else(lineMatches, order){ st with searchState := some { ss with lineMatches := lineMatches, lineOrder := order } }def ensureSearchLineCacheForBuffer (st : EditorState) (bufferId : Nat) : EditorState :=match st.searchState with| none => st| some ss =>if ss.lineCacheBufferId == some bufferId thenstelse{st withsearchState := some {ss withlineCacheBufferId := some bufferIdlineMatches := Lean.RBMap.emptylineOrder := #[]}}def getLineSearchMatches(st : EditorState)(bufferId : Nat)(lineIdx : Row)(lineStr : String): (Array (Nat × Nat) × EditorState) :=let st := ensureSearchLineCacheForBuffer st bufferIdmatch st.searchState with| none => (#[], st)| some ss =>if ss.pattern.isEmpty then(#[], st)elsematch ss.lineMatches.find? lineIdx with| some (cachedLine, cachedMatches) =>if cachedLine == lineStr then(cachedMatches, st)elselet matchRanges := findAllMatchesBytes lineStr.toUTF8 ss.pattern.toUTF8let st' := updateSearchLineCache st lineIdx lineStr matchRanges(matchRanges, st')| none =>let matchRanges := findAllMatchesBytes lineStr.toUTF8 ss.pattern.toUTF8let st' := updateSearchLineCache st lineIdx lineStr matchRanges(matchRanges, st')def prefetchSearchLineMatches(st : EditorState)(buf : FileBuffer)(startRow endRow : Nat): EditorState :=let rec loop (row : Nat) (acc : EditorState) : EditorState :=if row >= endRow thenaccelse if row >= FileBuffer.lineCount buf thenaccelselet lineIdx : Row := ⟨row⟩let lineStr := getLineFromBuffer buf lineIdx |>.getD ""let (_, acc') := getLineSearchMatches acc buf.id lineIdx lineStrloop (row + 1) acc'loop startRow stend ViE.UI
import ViE.UI.Windowimport ViE.UI.Overlayimport ViE.Terminalnamespace ViE.UIopen ViE/-- Render the current editor state to the terminal. -/def render (state : EditorState) : IO EditorState := doif !state.dirty then return state-- Start building the frame bufferlet mut buffer : Array String := #[]buffer := buffer.push Terminal.hideCursorStrlet (rows, cols) ← Terminal.getWindowSizelet ws := state.getCurrentWorkspacelet rec renderLayout (l : Layout) (st : EditorState) (r c h w : Nat) : (Array String × EditorState) := Id.run domatch l with| Layout.window id view =>if st.getCurrentWorkspace.isFloatingWindow id thenlet mut blankBuf : Array String := #[]for i in [0:h] doblankBuf := blankBuf.push (Terminal.moveCursorStr (r + i) c)blankBuf := blankBuf.push ("".pushn ' ' w)return (blankBuf, st)renderWindow st id view { row := r, col := c, height := h, width := w }| Layout.hsplit left right ratio =>let leftW := (Float.ofNat w * ratio).toUInt64.toNatlet (leftStr, st') := renderLayout left st r c h leftW-- Draw vertical separatorlet mut sepStr : Array String := #[]if w > leftW thenlet sepCol := c + leftWfor i in [0:h] dosepStr := sepStr.push (Terminal.moveCursorStr (r + i) sepCol)sepStr := sepStr.push st.config.vSplitStrlet (rightStr, st'') := renderLayout right st' r (c + leftW + 1) h (if w > leftW then w - leftW - 1 else 0)(leftStr ++ sepStr ++ rightStr, st'')| Layout.vsplit top bottom ratio =>let topH := (Float.ofNat h * ratio).toUInt64.toNatlet (topStr, st') := renderLayout top st r c topH w-- Draw horizontal separatorlet mut sepStr : Array String := #[]if h > topH thenlet sepRow := r + topHsepStr := sepStr.push (Terminal.moveCursorStr sepRow c)for _ in [0:w] dosepStr := sepStr.push st.config.hSplitStrlet (bottomStr, st'') := renderLayout bottom st' (r + topH + 1) c (if h > topH then h - topH - 1 else 0) w(topStr ++ sepStr ++ bottomStr, st'')let layoutH := rows - 1let baseLayout :=ws.getFloatingWindowIds.foldl (fun acc wid =>match acc with| some l => l.remove wid| none => none) (some ws.layout)if baseLayout.isNone thenbuffer := buffer.push Terminal.clearScreenStrbuffer := buffer.push Terminal.homeCursorStrlet (layoutStr, stateAfterLayout) :=match baseLayout with| some l => renderLayout l state 0 0 layoutH cols| none => (#[], state)buffer := buffer ++ layoutStrlet wsAfterLayout := stateAfterLayout.getCurrentWorkspacelet floatingViews := wsAfterLayout.getFloatingWindowIds.foldl (fun acc wid =>match wsAfterLayout.layout.findView wid with| some v => acc.push (wid, v)| none => acc) #[]let mut activeFloatingCursorPos : Option (Nat × Nat) := nonefor i in [0:floatingViews.size] dolet (wid, view) := floatingViews[i]!match stateAfterLayout.getFloatingWindowBounds wid with| some (top, left, h, w) =>let borderFlags := stateAfterLayout.getFloatingWindowBorderFlags widlet (winBuf, cursorPos) := renderFloatingWindow stateAfterLayout view { row := top, col := left, height := h, width := w } borderFlagsbuffer := buffer ++ winBufif wid == wsAfterLayout.activeWindowId thenactiveFloatingCursorPos := cursorPos| none => pure ()-- Global Status / Command Linebuffer := buffer.push (Terminal.moveCursorStr (rows - 1) 0)let statusRight := renderStatusBar stateAfterLayoutbuffer := buffer.push statusRightbuffer := buffer.push Terminal.clearLineStrlet overlayToRender := stateAfterLayout.floatingOverlay.orElse (fun _ => messageOverlayForState stateAfterLayout)if let some overlay := overlayToRender thenbuffer := buffer ++ renderFloatingOverlay rows cols overlay-- Set Physical Cursorlet rec getCursorPos (l : Layout) (r c h w : Nat) : Option (Nat × Nat) :=match l with| Layout.window id view =>if id == wsAfterLayout.activeWindowId thenlet workH := if h > 0 then h - 1 else 0let colOffset := if stateAfterLayout.config.showLineNumbers then 4 else 0let visualRow := view.cursor.row.val - view.scrollRow.valif visualRow >= 0 && visualRow < workH thenif view.cursor.col.val < view.scrollCol.val thennoneelselet visualCol := view.cursor.col.val - view.scrollCol.valif (visualCol + colOffset) < w thensome (r + visualRow, c + visualCol + colOffset)else 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)let overlayCursorPos : Option (Nat × Nat) :=match stateAfterLayout.floatingOverlay with| none => none| some overlay =>match computeFloatingOverlayLayout rows cols overlay with| none => none| some layout =>let lines := if overlay.lines.isEmpty then #[""] else overlay.lineslet maxRow := lines.size - 1let rowIdx := min overlay.cursorRow maxRowlet line := lines[rowIdx]!let lineW := Unicode.stringWidth linelet colIdx := min overlay.cursorCol lineWlet visRow := min rowIdx (layout.contentRows - 1)let visCol := min colIdx layout.innerWidthsome (layout.top + 1 + layout.titleRows + visRow, layout.left + 2 + visCol)if stateAfterLayout.floatingOverlay.isSome && stateAfterLayout.mode != .command &&stateAfterLayout.mode != .searchForward && stateAfterLayout.mode != .searchBackward thenbuffer := buffer.push (match overlayCursorPos with| some (pr, pc) => Terminal.moveCursorStr pr pc| none => "")else if wsAfterLayout.isFloatingWindow wsAfterLayout.activeWindowId &&stateAfterLayout.mode != .command && stateAfterLayout.mode != .searchForward && stateAfterLayout.mode != .searchBackward thenbuffer := buffer.push (match activeFloatingCursorPos with| some (pr, pc) => Terminal.moveCursorStr pr pc| none => "")elsebuffer := buffer.push (match getCursorPos (baseLayout.getD wsAfterLayout.layout) 0 0 layoutH cols with| some (pr, pc) => Terminal.moveCursorStr pr pc| none => "")if stateAfterLayout.mode == .command thenbuffer := buffer.push (Terminal.moveCursorStr (rows - 1) (1 + stateAfterLayout.inputState.commandBuffer.length))if stateAfterLayout.mode == .searchForward || stateAfterLayout.mode == .searchBackward thenbuffer := buffer.push (Terminal.moveCursorStr (rows - 1) (1 + stateAfterLayout.inputState.commandBuffer.length))if stateAfterLayout.mode == .visual || stateAfterLayout.mode == .visualBlock thenbuffer := buffer.push Terminal.hideCursorStrelsebuffer := buffer.push Terminal.showCursorStr-- Output everythinglet finalStr := String.intercalate "" buffer.toListIO.print finalStr(← IO.getStdout).flushreturn stateAfterLayoutend ViE.UI
import ViE.Stateimport ViE.Unicodenamespace 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. -/def takeVisual (s : Substring.Raw) (width : Nat) : String :=Unicode.takeByDisplayWidth s widthdef rightPadVisual (s : String) (width : Nat) : String :=let w := Unicode.stringWidth sif w >= width thenselses ++ "".pushn ' ' (width - w)structure Rect whererow : Natcol : Natheight : Natwidth : Natderiving Inhabiteddef getWorkspaceBuffer (st : EditorState) (id : Nat) : FileBuffer :=let ws := st.getCurrentWorkspacews.buffers.find? (fun b => b.id == id) |>.getD initialFileBufferend ViE.UI
import ViE.UI.Primitivesimport ViE.Terminalimport ViE.Colornamespace ViE.UIopen ViEdef shouldRenderMessageAsFloat (msg : String) : Bool :=let m := msg.trimAscii.toStringif m.isEmpty thenfalseelsem.startsWith "Error" ||m.startsWith "Cannot" ||m.startsWith "Invalid" ||m.startsWith "Unknown" ||m.startsWith "No " ||m.startsWith "Empty " ||m.startsWith "Usage:" ||m.startsWith "failed" ||m.startsWith "Failed" ||m.contains "not found"def renderStatusBar (state : EditorState) : String :=let plainMessage := state.message.trimAscii.toStringlet floatMessage := shouldRenderMessageAsFloat plainMessageif state.mode == .command thens!":{state.inputState.commandBuffer}"else if state.mode == .searchForward thens!"/{state.inputState.commandBuffer}"else if state.mode == .searchBackward thens!"?{state.inputState.commandBuffer}"else if floatMessage then""elseplainMessagedef messageOverlayForState (state : EditorState) : Option FloatingOverlay :=let plainMessage := state.message.trimAscii.toStringlet floatMessage := shouldRenderMessageAsFloat plainMessageif state.floatingOverlay.isNone &&state.mode != .command &&state.mode != .searchForward &&state.mode != .searchBackward &&floatMessage thenif plainMessage.isEmpty thennoneelsesome {title := "Message"lines := (plainMessage.splitOn "\n").toArraymaxWidth := 0cursorRow := 0cursorCol := 0}elsenonestructure FloatingOverlayLayout wheretop : Natleft : NatinnerWidth : NattitleRows : NatcontentRows : Natderiving Inhabiteddef computeFloatingOverlayLayout (rows cols : Nat) (overlay : FloatingOverlay) : Option FloatingOverlayLayout := Id.run dolet availableRows := if rows > 1 then rows - 1 else rowsif cols < 8 || availableRows < 4 thenreturn nonelet lines := if overlay.lines.isEmpty then #[""] else overlay.lineslet titleText := if overlay.title.isEmpty then "" else s!"[{overlay.title}]"let titleRows := if titleText.isEmpty then 0 else 1let naturalWidthContent := lines.foldl (fun acc ln => max acc (Unicode.stringWidth ln)) 0let naturalWidth := max naturalWidthContent (Unicode.stringWidth titleText)let maxInnerWidth := if cols > 8 then cols - 8 else 1let targetWidth :=if overlay.maxWidth > 0 thenoverlay.maxWidthelsenaturalWidthlet innerWidth := max 1 (min targetWidth maxInnerWidth)let maxContentRows :=if availableRows > titleRows + 2 thenavailableRows - titleRows - 2else0if maxContentRows == 0 thenreturn nonelet naturalRows := lines.sizelet targetRows := naturalRowslet contentRows := max 1 (min targetRows maxContentRows)let boxHeight := contentRows + titleRows + 2let boxWidth := innerWidth + 4let top := (availableRows - boxHeight) / 2let left := (cols - boxWidth) / 2return some {top := topleft := leftinnerWidth := innerWidthtitleRows := titleRowscontentRows := contentRows}def renderFloatingOverlay (rows cols : Nat) (overlay : FloatingOverlay) : Array String := Id.run dolet some layout := computeFloatingOverlayLayout rows cols overlay | return #[]let lines := if overlay.lines.isEmpty then #[""] else overlay.lineslet titleText := if overlay.title.isEmpty then "" else s!"[{overlay.title}]"let top := layout.toplet left := layout.leftlet innerWidth := layout.innerWidthlet titleRows := layout.titleRowslet contentRows := layout.contentRowslet border := "+" ++ "".pushn '-' (innerWidth + 2) ++ "+"let style := (ViE.Color.toBg .brightBlack) ++ (ViE.Color.toFg .white)let mut out : Array String := #[]out := out.push (Terminal.moveCursorStr top left)out := out.push styleout := out.push borderif titleRows == 1 thenout := out.push (Terminal.moveCursorStr (top + 1) left)let clippedTitle := Unicode.takeByDisplayWidth titleText.toRawSubstring innerWidthout := out.push s!"| {rightPadVisual clippedTitle innerWidth} |"for i in [0:contentRows] dolet raw := lines[i]?.getD ""let clipped := Unicode.takeByDisplayWidth raw.toRawSubstring innerWidthlet row := top + 1 + titleRows + iout := out.push (Terminal.moveCursorStr row left)out := out.push s!"| {rightPadVisual clipped innerWidth} |"let bottom := top + contentRows + titleRows + 1out := out.push (Terminal.moveCursorStr bottom left)out := out.push borderout := out.push ViE.Color.resetreturn outend ViE.UI
def Layout.findWindowIdByBufferId (l : Layout) (bufferId : Nat) : Option Nat :=match l with| .window wid v => if v.bufferId == bufferId then some wid else none| .hsplit left right _ => (left.findWindowIdByBufferId bufferId).orElse (fun _ => right.findWindowIdByBufferId bufferId)| .vsplit top bottom _ => (top.findWindowIdByBufferId bufferId).orElse (fun _ => bottom.findWindowIdByBufferId bufferId)def Layout.containsWindow (l : Layout) (windowId : Nat) : Bool :=match l with| .window wid _ => wid == windowId| .hsplit left right _ => left.containsWindow windowId || right.containsWindow windowId| .vsplit top bottom _ => top.containsWindow windowId || bottom.containsWindow windowId
s.updateCurrentWorkspace fun ws =>let view := ws.layout.findView ws.activeWindowIdmatch view with| some v =>let newBuffers := ws.buffers.map fun b =>if b.id == v.bufferId thenlet updated := FileBuffer.recomputeMissingEol (f b)FileBuffer.clearCache updatedelse b{ ws with buffers := newBuffers }| none => ws
let ws := s.getCurrentWorkspacelet view := ws.layout.findView ws.activeWindowIdlet s' :=s.updateCurrentWorkspace fun ws =>match view with| some v =>let newBuffers := ws.buffers.map fun b =>if b.id == v.bufferId thenlet updated := FileBuffer.recomputeMissingEol (f b)FileBuffer.clearCache updatedelse b{ ws with buffers := newBuffers }| none => wsmatch view, s'.searchState with| some v, some ss =>{s' withsearchState := some {ss withlineCacheBufferId := some v.bufferIdlineMatches := Lean.RBMap.emptylineOrder := #[]}}| _, _ => s'
def WorkspaceState.isFloatingWindow (ws : WorkspaceState) (windowId : Nat) : Bool :=ws.floatingWindows.contains windowIddef WorkspaceState.setWindowFloating (ws : WorkspaceState) (windowId : Nat) (enabled : Bool) : WorkspaceState :=if enabled thenif ws.floatingWindows.contains windowId then wselse { ws with floatingWindows := windowId :: ws.floatingWindows }else{ws withfloatingWindows := ws.floatingWindows.filter (fun wid => wid != windowId)floatingWindowOffsets := ws.floatingWindowOffsets.filter (fun (wid, _, _) => wid != windowId)}def WorkspaceState.toggleWindowFloating (ws : WorkspaceState) (windowId : Nat) : WorkspaceState :=ws.setWindowFloating windowId (!ws.isFloatingWindow windowId)def WorkspaceState.pruneFloatingWindows (ws : WorkspaceState) : WorkspaceState :=let rec idsOf (l : Layout) : List Nat :=match l with| .window id _ => [id]| .hsplit left right _ => idsOf left ++ idsOf right| .vsplit top bottom _ => idsOf top ++ idsOf bottomlet ids := idsOf ws.layout{ws withfloatingWindows := ws.floatingWindows.filter (fun wid => ids.contains wid)floatingWindowOffsets := ws.floatingWindowOffsets.filter (fun (wid, _, _) => ids.contains wid)}def WorkspaceState.getFloatingWindowIds (ws : WorkspaceState) : Array Nat :=ws.floatingWindows.foldl (fun acc wid =>match ws.layout.findView wid with| some _ => acc.push wid| none => acc) #[]def WorkspaceState.floatingWindowIndex? (ws : WorkspaceState) (windowId : Nat) : Option Nat :=let ids := ws.getFloatingWindowIdslet rec loop (i : Nat) : Option Nat :=if i >= ids.size thennoneelse if ids[i]! == windowId thensome ielseloop (i + 1)loop 0def WorkspaceState.getFloatingWindowOffset (ws : WorkspaceState) (windowId : Nat) : Int × Int :=match ws.floatingWindowOffsets.find? (fun (wid, _, _) => wid == windowId) with| some (_, rowOff, colOff) => (rowOff, colOff)| none => (0, 0)def WorkspaceState.setFloatingWindowOffset (ws : WorkspaceState) (windowId : Nat) (rowOff colOff : Int) : WorkspaceState :=let rest := ws.floatingWindowOffsets.filter (fun (wid, _, _) => wid != windowId){ ws with floatingWindowOffsets := (windowId, rowOff, colOff) :: rest }def WorkspaceState.adjustFloatingWindowOffset (ws : WorkspaceState) (windowId : Nat) (dRow dCol : Int) : WorkspaceState :=let (rowOff, colOff) := ws.getFloatingWindowOffset windowIdws.setFloatingWindowOffset windowId (rowOff + dRow) (colOff + dCol)structure FloatingWindowBorderFlags wheretop : Bool := trueright : Bool := truebottom : Bool := trueleft : Bool := truederiving Inhabiteddef WorkspaceState.layoutAllFloating (ws : WorkspaceState) (l : Layout) : Bool :=match l with| .window id _ => ws.isFloatingWindow id| .hsplit left right _ => ws.layoutAllFloating left && ws.layoutAllFloating right| .vsplit top bottom _ => ws.layoutAllFloating top && ws.layoutAllFloating bottom
def WorkspaceState.findFloatingSubtree (ws : WorkspaceState) (windowId : Nat) : Option Layout :=let rec go (l : Layout) : Option Layout :=if !l.containsWindow windowId thennoneelse if ws.layoutAllFloating l then-- Outermost subtree containing the target where all descendants are floating.some lelsematch l with| .hsplit left right _ => (go left).orElse (fun _ => go right)| .vsplit top bottom _ => (go top).orElse (fun _ => go bottom)| .window _ _ => nonego ws.layout
def computeFloatingWindowBounds (rows cols idx : Nat) : Option (Nat × Nat × Nat × Nat) :=let availableRows := if rows > 1 then rows - 1 else rowsif availableRows < 6 || cols < 16 thennoneelselet hMax := availableRows - 2let wMax := cols - 4let h := min hMax (max 8 ((availableRows * 3) / 5))let w := min wMax (max 24 ((cols * 3) / 5))let topBase := (availableRows - h) / 2let leftBase := (cols - w) / 2let top := min (availableRows - h) (topBase + idx)let left := min (cols - w) (leftBase + (2 * idx))some (top, left, h, w)def EditorState.getFloatingWindowBounds (s : EditorState) (windowId : Nat) : Option (Nat × Nat × Nat × Nat) :=let ws := s.getCurrentWorkspacelet floatingIds := ws.getFloatingWindowIdslet rec indexOf (ids : Array Nat) (target : Nat) (i : Nat := 0) : Option Nat :=if i >= ids.size then noneelse if ids[i]! == target then some ielse indexOf ids target (i + 1)let splitByRatio (total : Nat) (ratio : Float) : Option (Nat × Nat) :=if total < 2 thennoneelselet raw := (Float.ofNat total * ratio).toUInt64.toNatlet first := max 1 (min (total - 1) raw)some (first, total - first)let rec idsOfLayout (l : Layout) : List Nat :=match l with| .window id _ => [id]| .hsplit left right _ => idsOfLayout left ++ idsOfLayout right| .vsplit top bottom _ => idsOfLayout top ++ idsOfLayout bottomlet minFloatingIndexOf (l : Layout) : Option Nat :=let ids := idsOfLayout llet rec loop (xs : List Nat) (best : Option Nat) : Option Nat :=match xs with| [] => best| id :: rest =>match indexOf floatingIds id, best with| some i, none => loop rest (some i)| some i, some b => loop rest (some (min i b))| none, _ => loop rest bestloop ids nonelet rec locateInFloatingSubtree(l : Layout) (top left h w : Nat): Option (Nat × Nat × Nat × Nat) :=match l with| .window id _ =>if id == windowId then some (top, left, h, w) else none| .hsplit leftL rightL ratio =>match splitByRatio w ratio with| none => none| some (leftW, rightW) =>(locateInFloatingSubtree leftL top left h leftW).orElse(fun _ => locateInFloatingSubtree rightL top (left + leftW) h rightW)| .vsplit topL bottomL ratio =>match splitByRatio h ratio with| none => none| some (topH, bottomH) =>(locateInFloatingSubtree topL top left topH w).orElse(fun _ => locateInFloatingSubtree bottomL (top + topH) left bottomH w)let floatingSubtreeBounds : Option (Nat × Nat × Nat × Nat) :=match ws.findFloatingSubtree windowId with| none => none| some subtree =>match minFloatingIndexOf subtree with| none => none| some groupIdx =>match computeFloatingWindowBounds s.windowHeight s.windowWidth groupIdx with| some (top, left, h, w) =>locateInFloatingSubtree subtree top left h w| none => nonelet baseBounds :=match floatingSubtreeBounds with| some b => some b| none =>match ws.floatingWindowIndex? windowId with| none => none| some idx => computeFloatingWindowBounds s.windowHeight s.windowWidth idxlet toNatNonNeg (v : Int) : Nat :=if v < 0 then 0 else Int.toNat vlet applyOffset (b : Nat × Nat × Nat × Nat) : Nat × Nat × Nat × Nat :=let (top, left, h, w) := blet (rowOff, colOff) := ws.getFloatingWindowOffset windowIdlet availableRows := if s.windowHeight > 1 then s.windowHeight - 1 else s.windowHeightlet maxTop := if availableRows > h then availableRows - h else 0let maxLeft := if s.windowWidth > w then s.windowWidth - w else 0let rawTop := Int.ofNat top + rowOfflet rawLeft := Int.ofNat left + colOfflet top' := min maxTop (toNatNonNeg rawTop)let left' := min maxLeft (toNatNonNeg rawLeft)(top', left', h, w)baseBounds.map applyOffsetdef EditorState.getFloatingWindowBorderFlags (s : EditorState) (windowId : Nat) : FloatingWindowBorderFlags :=let ws := s.getCurrentWorkspacelet rec locateFlags(l : Layout)(acc : FloatingWindowBorderFlags): Option FloatingWindowBorderFlags :=match l with| .window id _ =>if id == windowId then some acc else none| .hsplit left right _ =>let leftFlags := { acc with right := false }match locateFlags left leftFlags with| some f => some f| none =>let rightFlags := { acc with left := true }locateFlags right rightFlags| .vsplit top bottom _ =>let topFlags := { acc with bottom := false }match locateFlags top topFlags with| some f => some f| none =>let bottomFlags := { acc with top := true }locateFlags bottom bottomFlagsmatch ws.findFloatingSubtree windowId with| some subtree =>(locateFlags subtree { top := true, right := true, bottom := true, left := true }).getD{ top := true, right := true, bottom := true, left := true }| none => { top := true, right := true, bottom := true, left := true }
def EditorState.getActiveWindowScrollBounds (s : EditorState) : Nat × Nat :=let ws := s.getCurrentWorkspacelet activeId := ws.activeWindowIdif ws.isFloatingWindow activeId thenmatch s.getFloatingWindowBounds activeId with| some (_, _, h, w) =>-- Floating window has a border, so scrollable area is smaller than outer bounds.let borderFlags := s.getFloatingWindowBorderFlags activeIdlet topBottomBorders := (if borderFlags.top then 1 else 0) + (if borderFlags.bottom then 1 else 0)let contentRows := if h > topBottomBorders then h - topBottomBorders else 1let h' := contentRows + 1let sideBorders := (if borderFlags.left then 1 else 0) + (if borderFlags.right then 1 else 0)let w' := if w > sideBorders then w - sideBorders else 1(h', w')| none =>s.getActiveWindowBoundselses.getActiveWindowBounds
def arrayInsertAt (arr : Array α) (idx : Nat) (x : α) : Array α :=let i := min idx arr.size(arr.toList.take i ++ [x] ++ arr.toList.drop i).toArraydef arrayEraseAt (arr : Array α) (idx : Nat) : Array α :=if idx >= arr.size then arrelse(arr.toList.take idx ++ arr.toList.drop (idx + 1)).toArraydef overlayLineLen (line : String) : Nat :=line.toList.lengthdef overlayNormalize (ov : FloatingOverlay) : FloatingOverlay :=if ov.lines.isEmpty then{ ov with lines := #[""], cursorRow := 0, cursorCol := 0 }elselet row := min ov.cursorRow (ov.lines.size - 1)let col := min ov.cursorCol (overlayLineLen (ov.lines[row]!)){ ov with cursorRow := row, cursorCol := col }def updateOverlay (s : EditorState) (f : FloatingOverlay → FloatingOverlay) : EditorState :=match s.floatingOverlay with| none => s| some ov =>{ s withfloatingOverlay := some (overlayNormalize (f ov))dirty := true}def overlayMoveLeft (s : EditorState) : EditorState :=updateOverlay s (fun ov =>if ov.cursorCol > 0 then { ov with cursorCol := ov.cursorCol - 1 } else ov)def overlayMoveRight (s : EditorState) : EditorState :=updateOverlay s (fun ov =>let ov := overlayNormalize ovlet len := overlayLineLen (ov.lines[ov.cursorRow]!)if ov.cursorCol < len then { ov with cursorCol := ov.cursorCol + 1 } else ov)def overlayMoveUp (s : EditorState) : EditorState :=updateOverlay s (fun ov =>if ov.cursorRow > 0 then { ov with cursorRow := ov.cursorRow - 1 } else ov)def overlayMoveDown (s : EditorState) : EditorState :=updateOverlay s (fun ov =>let ov := overlayNormalize ovif ov.cursorRow + 1 < ov.lines.size then { ov with cursorRow := ov.cursorRow + 1 } else ov)def overlayMoveLineStart (s : EditorState) : EditorState :=updateOverlay s (fun ov => { ov with cursorCol := 0 })def overlayMoveLineEnd (s : EditorState) : EditorState :=updateOverlay s (fun ov =>let ov := overlayNormalize ov{ ov with cursorCol := overlayLineLen (ov.lines[ov.cursorRow]!) })def overlayInsertChar (s : EditorState) (c : Char) : EditorState :=updateOverlay s (fun ov =>let ov := overlayNormalize ovlet row := ov.cursorRowlet col := ov.cursorCollet line := ov.lines[row]!let chars := line.toListlet newLine := String.ofList (chars.take col ++ [c] ++ chars.drop col){ ov with lines := ov.lines.set! row newLine, cursorCol := col + 1 })def overlayBackspace (s : EditorState) : EditorState :=updateOverlay s (fun ov =>let ov := overlayNormalize ovlet row := ov.cursorRowlet col := ov.cursorCollet line := ov.lines[row]!if col > 0 thenlet chars := line.toListlet newLine := String.ofList (chars.take (col - 1) ++ chars.drop col){ ov with lines := ov.lines.set! row newLine, cursorCol := col - 1 }else if row > 0 thenlet prev := ov.lines[row - 1]!let merged := prev ++ linelet lines := (ov.lines.set! (row - 1) merged)let lines := arrayEraseAt lines row{ ov with lines := lines, cursorRow := row - 1, cursorCol := overlayLineLen prev }elseov)def overlayDeleteCharAt (s : EditorState) : EditorState :=updateOverlay s (fun ov =>let ov := overlayNormalize ovlet row := ov.cursorRowlet col := ov.cursorCollet line := ov.lines[row]!let len := overlayLineLen lineif col < len thenlet chars := line.toListlet newLine := String.ofList (chars.take col ++ chars.drop (col + 1)){ ov with lines := ov.lines.set! row newLine }else if row + 1 < ov.lines.size thenlet next := ov.lines[row + 1]!let merged := line ++ nextlet lines := (ov.lines.set! row merged)let lines := arrayEraseAt lines (row + 1){ ov with lines := lines }elseov)def overlayInsertNewline (s : EditorState) : EditorState :=updateOverlay s (fun ov =>let ov := overlayNormalize ovlet row := ov.cursorRowlet col := ov.cursorCollet line := ov.lines[row]!let chars := line.toListlet left := String.ofList (chars.take col)let right := String.ofList (chars.drop col)let lines := (ov.lines.set! row left)let lines := arrayInsertAt lines (row + 1) right{ ov with lines := lines, cursorRow := row + 1, cursorCol := 0 })def overlayOpenLineBelow (s : EditorState) : EditorState :=updateOverlay s (fun ov =>let ov := overlayNormalize ovlet row := ov.cursorRow + 1let lines := arrayInsertAt ov.lines row ""{ ov with lines := lines, cursorRow := row, cursorCol := 0 })def overlayOpenLineAbove (s : EditorState) : EditorState :=updateOverlay s (fun ov =>let ov := overlayNormalize ovlet row := ov.cursorRowlet lines := arrayInsertAt ov.lines row ""{ ov with lines := lines, cursorRow := row, cursorCol := 0 })def shouldRenderMessageAsFloat (msg : String) : Bool :=let m := msg.trimAscii.toStringif m.isEmpty thenfalseelsem.startsWith "Error" ||m.startsWith "Cannot" ||m.startsWith "Invalid" ||m.startsWith "Unknown" ||m.startsWith "No " ||m.startsWith "Empty " ||m.startsWith "Usage:" ||m.startsWith "failed" ||m.startsWith "Failed" ||m.contains "not found"
let submitFloatingInput (state : EditorState) : IO EditorState := domatch state.floatingInputCommand, state.floatingOverlay with| some cmdPrefix, some ov =>let input := (ov.lines[0]?.getD "").trimAscii.toStringlet cmd := s!"{cmdPrefix}{input}"let pre := {state withfloatingOverlay := nonefloatingInputCommand := nonemode := .normalinputState := {state.inputState withcommandBuffer := cmdcountBuffer := ""previousKey := nonependingKeys := ""}}let out ← ViE.Command.executeCommand commands prereturn {out withmode := .normalinputState := {out.inputState withcommandBuffer := ""countBuffer := ""previousKey := nonependingKeys := ""}dirty := true}| _, _ =>return {state withfloatingOverlay := nonefloatingInputCommand := nonedirty := true}
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 => "/"ViE.Feature.openExplorerWithPreview s parentPath explorer.previewWindowId explorer.previewBufferId| none => pure $ clearInput (EditorState.moveCursorLeftN s s.getCount)else
let closeOverlay : EditorState :={ s with floatingOverlay := none, floatingInputCommand := none, dirty := true, message := "floating overlay cleared" }if s.floatingOverlay.isSome thenif s.floatingInputCommand.isSome thenmatch k with| Key.esc => pure { s with floatingOverlay := none, floatingInputCommand := none, mode := .normal, dirty := true, message := "" }| Key.enter => submitFloatingInput s| Key.char 'q' => pure { s with floatingOverlay := none, floatingInputCommand := none, mode := .normal, dirty := true, message := "" }| Key.left => pure (overlayMoveLeft s)| Key.right => pure (overlayMoveRight s)| Key.up => pure (overlayMoveUp s)| Key.down => pure (overlayMoveDown s)| Key.backspace => pure (overlayBackspace s)| Key.char c => pure (overlayInsertChar s c)| _ => pure selsematch k with| Key.esc => pure closeOverlay| Key.enter => pure closeOverlay| Key.char 'q' => pure closeOverlay| Key.char ':' => pure $ s.setMode Mode.command| Key.char 'i' => pure { s with mode := .insert, dirty := true }| Key.char 'a' => pure { (overlayMoveRight s) with mode := .insert }| Key.char 'h' => pure (overlayMoveLeft s)| Key.char 'j' => pure (overlayMoveDown s)| Key.char 'k' => pure (overlayMoveUp s)| Key.char 'l' => pure (overlayMoveRight s)| Key.char '0' => pure (overlayMoveLineStart s)| Key.char '$' => pure (overlayMoveLineEnd s)| Key.char 'x' => pure (overlayDeleteCharAt s)| Key.char 'o' => pure { (overlayOpenLineBelow s) with mode := .insert }| Key.char 'O' => pure { (overlayOpenLineAbove s) with mode := .insert }| _ => pure selseif shouldRenderMessageAsFloat s.message thenmatch k with| Key.esc => pure { s with message := "", dirty := true }| Key.enter => pure { s with message := "", dirty := true }| _ => pure selsematch k with| Key.esc =>pure { s with inputState := { s.inputState with countBuffer := "", previousKey := none } }| Key.char 'h' =>-- Check if in explorer bufferlet buf := s.getActiveBufferlet isExplorer := s.explorers.any (fun (id, _) => id == buf.id)if isExplorer thenlet explorerOpt := s.explorers.find? (fun (id, _) => id == buf.id)match explorerOpt with| some (_, explorer) =>if explorer.mode == .files thenlet parentPath := match (System.FilePath.mk explorer.currentPath).parent with| some p => p.toString| none => "/"ViE.Feature.openExplorerWithPreview s parentPath explorer.previewWindowId explorer.previewBufferIdelsehandleMotion s EditorState.moveCursorLeft| none => pure $ clearInput (EditorState.moveCursorLeftN s s.getCount)else
| Key.char 'l' => handleMotion s EditorState.moveCursorRight| Key.enter => ViE.Feature.handleExplorerEnter s| Key.char 'i' => pure $ s.setMode Mode.insert| Key.char 'a' =>
| Key.char 'l' => handleMotion s EditorState.moveCursorRight| Key.enter => ViE.Feature.handleExplorerEnter s| Key.char 'i' => pure $ s.setMode Mode.insert| Key.char 'a' =>
| 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' =>
| 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' =>
| Key.char 'b' => handleMotion s EditorState.moveWordBackward| Key.char 'e' => handleMotion s EditorState.moveWordEnd| Key.char 'x' => pure $ clearInput (EditorState.deleteCharAfterCursor s)| Key.char 'n' => pure $ clearInput (ViE.findNextMatch s)| Key.char 'N' =>
| Key.char 'b' => handleMotion s EditorState.moveWordBackward| Key.char 'e' => handleMotion s EditorState.moveWordEnd| Key.char 'x' => pure $ clearInput (EditorState.deleteCharAfterCursor s)| Key.char 'n' => pure $ clearInput (ViE.findNextMatch s)| Key.char 'N' =>
| Key.char '|' =>let n := s.getCountpure $ clearInput (EditorState.jumpToColumn s n)| Key.char 'c' =>match s.inputState.previousKey with| some 'c' =>-- cc: delete line and enter insert mode-- For now, approximate with delete line. Ideally should preserve line as empty.pure $ (s.deleteCurrentLine).setMode Mode.insert| _ => pure { s with inputState := { s.inputState with previousKey := some 'c' } }| Key.char 'd' =>match s.inputState.previousKey with| some 'd' => pure $ s.deleteCurrentLine| _ => pure { s with inputState := { s.inputState with previousKey := some 'd' } }| Key.char 'u' => pure $ s.undo| Key.ctrl 'r' => pure $ s.redo| Key.ctrl 'w' =>
| Key.char '|' =>let n := s.getCountpure $ clearInput (EditorState.jumpToColumn s n)| Key.char 'c' =>match s.inputState.previousKey with| some 'c' =>-- cc: delete line and enter insert mode-- For now, approximate with delete line. Ideally should preserve line as empty.pure $ (s.deleteCurrentLine).setMode Mode.insert| _ => pure { s with inputState := { s.inputState with previousKey := some 'c' } }| Key.char 'd' =>match s.inputState.previousKey with| some 'd' => pure $ s.deleteCurrentLine| _ => pure { s with inputState := { s.inputState with previousKey := some 'd' } }| Key.char 'u' => pure $ s.undo| Key.ctrl 'r' => pure $ s.redo| Key.ctrl 'l' => pure { s with dirty := true, message := "redraw" }| Key.ctrl 'w' =>
| '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
| 'H' =>if activeIsFloating thenpure $ nudgeActiveFloatingWindow s' .leftelsepure $ resizeWindow s' .left 0.05| 'J' =>if activeIsFloating thenpure $ nudgeActiveFloatingWindow s' .downelsepure $ resizeWindow s' .down 0.05| 'K' =>if activeIsFloating thenpure $ nudgeActiveFloatingWindow s' .upelsepure $ resizeWindow s' .up 0.05| 'L' =>if activeIsFloating thenpure $ nudgeActiveFloatingWindow s' .rightelsepure $ resizeWindow s' .right 0.05
insert := fun s k => match k with| Key.esc => pure $ (s.commitEdit.moveCursorLeft).setMode Mode.normal| Key.backspace => pure $ s.deleteBeforeCursor| Key.char c => pure $ s.insertChar c| Key.enter => pure s.insertNewline| _ => pure s,
insert := fun s k =>if s.floatingOverlay.isSome thenif s.floatingInputCommand.isSome thenlet submitFloatingInput (state : EditorState) : IO EditorState := domatch state.floatingInputCommand, state.floatingOverlay with| some cmdPrefix, some ov =>let input := (ov.lines[0]?.getD "").trimAscii.toStringlet cmd := s!"{cmdPrefix}{input}"let pre := {state withfloatingOverlay := nonefloatingInputCommand := nonemode := .normalinputState := {state.inputState withcommandBuffer := cmdcountBuffer := ""previousKey := nonependingKeys := ""}}let out ← ViE.Command.executeCommand commands prereturn {out withmode := .normalinputState := {out.inputState withcommandBuffer := ""countBuffer := ""previousKey := nonependingKeys := ""}dirty := true}| _, _ =>return {state withfloatingOverlay := nonefloatingInputCommand := nonemode := .normaldirty := true}match k with| Key.esc => pure { s with floatingOverlay := none, floatingInputCommand := none, mode := .normal, dirty := true, message := "" }| Key.backspace => pure (overlayBackspace s)| Key.char c => pure (overlayInsertChar s c)| Key.enter => submitFloatingInput s| Key.left => pure (overlayMoveLeft s)| Key.right => pure (overlayMoveRight s)| Key.up => pure (overlayMoveUp s)| Key.down => pure (overlayMoveDown s)| _ => pure selsematch k with| Key.esc => pure { s with mode := .normal, dirty := true }| Key.backspace => pure (overlayBackspace s)| Key.char c => pure (overlayInsertChar s c)| Key.enter => pure (overlayInsertNewline s)| Key.left => pure (overlayMoveLeft s)| Key.right => pure (overlayMoveRight s)| Key.up => pure (overlayMoveUp s)| Key.down => pure (overlayMoveDown s)| _ => pure selsematch k with| Key.esc => pure $ (s.commitEdit.moveCursorLeft).setMode Mode.normal| Key.backspace => pure $ s.deleteBeforeCursor| Key.char c => pure $ s.insertChar c| Key.enter => pure s.insertNewline| _ => pure s,
def flushEscSequence (seq : String) : List Key :=seq.toList.map (fun ch => if ch == '\x1b' then Key.esc else Key.char ch)def isAsciiInRange (ch : Char) (lo hi : Nat) : Bool :=let n := ch.toNatlo <= n && n <= hidef isCsiParamChar (ch : Char) : Bool :=ch.isDigit || ch == ';' || ch == '?' || ch == ':' || ch == '<' || ch == '=' || ch == '>'def isCsiFinalChar (ch : Char) : Bool :=isAsciiInRange ch 0x40 0x7edef decodeCsiFinal (ch : Char) : Key :=match ch with| 'A' => Key.up| 'B' => Key.down| 'C' => Key.right| 'D' => Key.left| 'H' => Key.unknown 'H'| 'F' => Key.unknown 'F'| _ => Key.unknown chdef withPending (state : EditorState) (pending : String) (currentTime : Nat) : EditorState :={ state with inputState := { state.inputState with pendingKeys := pending, lastInputTime := currentTime } }def clearPending (state : EditorState) : EditorState :={ state with inputState := { state.inputState with pendingKeys := "" } }
-- 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?
(withPending state pending currentTime, [])
-- Treat ESC + <char> as Alt combination when it's a single character and not CSI.match pending.toList with| ['\x1b', ch] =>if ch != '[' then({ state with inputState := { state.inputState with pendingKeys := "" } }, [Key.alt ch])else({ state with inputState := { state.inputState with pendingKeys := pending, lastInputTime := currentTime } }, [])| _ =>-- 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)
match pending.toList with| ['\x1b', ch] =>if ch == '[' then(withPending state pending currentTime, [])else(clearPending state, [Key.alt ch])| _ =>if pending.startsWith "\x1b[" thenlet chars := pending.toListlet last := chars.getLastD '\x00'if isCsiFinalChar last then(clearPending state, [decodeCsiFinal last])else if isCsiParamChar last && pending.length <= 12 then(withPending state pending currentTime, [])else if pending.length > 12 then(clearPending state, flushEscSequence pending)else(clearPending state, flushEscSequence pending)else(clearPending state, flushEscSequence pending)
if 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)
if currentTime - state.inputState.lastInputTime > 50 thenlet keys := flushEscSequence state.inputState.pendingKeys(clearPending state, keys)
-- 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?| _ =>-- 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])
ViE.Key.parseKey state c currentTime
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, [])
ViE.Key.checkTimeout state currentTime
def cmdRedraw (_ : List String) (state : EditorState) : IO EditorState :=let s1 := state.updateCurrentWorkspace fun ws =>{ ws with buffers := ws.buffers.map FileBuffer.clearCache }let s2 :=match s1.searchState with| some ss =>{ s1 withsearchState := some {ss withlineMatches := Lean.RBMap.emptylineOrder := #[]}}| none => s1return { s2 with dirty := true, message := "redraw" }def floatUsage : String :="Usage: :float [--title <title>|--title=<title>] [--width <cols>|--width=<cols>] <text> | :float clear"def parsePositiveNat (s : String) : Option Nat :=match s.toNat? with| some n => if n > 0 then some n else none| none => nonepartial def parseFloatArgs(args : List String)(title : String)(maxWidth : Nat)(textRev : List String): Except String (String × Nat × List String) :=match args with| [] =>.ok (title, maxWidth, textRev.reverse)| "--" :: rest =>.ok (title, maxWidth, textRev.reverse ++ rest)| "--title" :: t :: rest =>parseFloatArgs rest t maxWidth textRev| "--title" :: [] =>.error floatUsage| "--width" :: w :: rest =>match parsePositiveNat w with| some n => parseFloatArgs rest title n textRev| none => .error s!"Invalid float width: {w}"| "--width" :: [] =>.error floatUsage| tok :: rest =>if tok.startsWith "--title=" thenparseFloatArgs rest (tok.drop 8 |>.toString) maxWidth textRevelse if tok.startsWith "--width=" thenlet w := (tok.drop 8).toStringmatch parsePositiveNat w with| some n => parseFloatArgs rest title n textRev| none => .error s!"Invalid float width: {w}"elseparseFloatArgs rest title maxWidth (tok :: textRev)def cmdFloat (args : List String) (state : EditorState) : IO EditorState := doif args == ["clear"] thenreturn { state with floatingOverlay := none, floatingInputCommand := none, dirty := true, message := "floating overlay cleared" }match parseFloatArgs args "Float" 0 [] with| .error msg =>return { state with message := msg }| .ok (titleRaw, maxWidth, textTokens) =>let raw := (String.intercalate " " textTokens).trimAscii.toStringif raw.isEmpty thenreturn { state with message := floatUsage }elselet title := titleRaw.trimAscii.toStringlet lines := (raw.splitOn "\\n").toArraylet lines := if lines.isEmpty then #[""] else lineslet lastRow := lines.size - 1let lastCol := (lines[lastRow]!.toList.length)let overlay : FloatingOverlay := {title := if title.isEmpty then "Float" else titlelines := linesmaxWidth := maxWidthcursorRow := lastRowcursorCol := lastCol}return { state with floatingOverlay := some overlay, floatingInputCommand := none, dirty := true, message := "floating overlay shown" }def cmdNoFloat (_ : List String) (state : EditorState) : IO EditorState :=return { state with floatingOverlay := none, floatingInputCommand := none, dirty := true, message := "floating overlay cleared" }def cmdFloatWin (args : List String) (state : EditorState) : IO EditorState := dolet usage := "Usage: :floatwin [toggle|on|off|clear]"let activeId := state.getCurrentWorkspace.activeWindowIdlet s' := state.updateCurrentWorkspace fun ws =>let ws := ws.pruneFloatingWindowsmatch args with| [] => ws.toggleWindowFloating activeId| ["toggle"] => ws.toggleWindowFloating activeId| ["on"] => ws.setWindowFloating activeId true| ["off"] => ws.setWindowFloating activeId false| ["clear"] => { ws with floatingWindows := [], floatingWindowOffsets := [] }| _ => wslet ws' := s'.getCurrentWorkspacelet isFloating := ws'.isFloatingWindow activeIdlet msg :=match args with| _ :: _ =>if args == ["clear"] then "floating windows cleared"else if args == ["on"] then s!"window {activeId} floating on"else if args == ["off"] then s!"window {activeId} floating off"else if args == ["toggle"] thenif isFloating then s!"window {activeId} floating on" else s!"window {activeId} floating off"else usage| [] =>if isFloating then s!"window {activeId} floating on" else s!"window {activeId} floating off"return { s' with dirty := true, message := msg }
let name := s!"Group {state.workgroups.size}"let nextBufId := state.workgroups.foldl (fun m g =>g.workspaces.foldl (fun m2 ws => max m2 ws.nextBufferId) m) 0let newWg := createEmptyWorkgroup name nextBufIdlet newState := { state withworkgroups := state.workgroups.push newWg,currentGroup := state.workgroups.size -- switch to new one using the size before push}return { newState with message := s!"Created workgroup: {name}" }
if state.mode == .command thenreturn ViE.Feature.openNameInputFloat state "New Workgroup" "" "wg new "elselet name := s!"Group {state.workgroups.size}"let nextBufId := state.workgroups.foldl (fun m g =>g.workspaces.foldl (fun m2 ws => max m2 ws.nextBufferId) m) 0let newWg := createEmptyWorkgroup name nextBufIdlet newState := { state withworkgroups := state.workgroups.push newWg,currentGroup := state.workgroups.size -- switch to new one using the size before push}return { newState with message := s!"Created workgroup: {name}" }
let nextBufId := state.workgroups.foldl (fun m g =>g.workspaces.foldl (fun m2 ws => max m2 ws.nextBufferId) m) 0let newWg := createEmptyWorkgroup name nextBufIdlet newState := { state withworkgroups := state.workgroups.push newWg,currentGroup := state.workgroups.size}return { newState with message := s!"Created workgroup: {name}" }
let trimmed := name.trimAscii.toStringif trimmed.isEmpty thenreturn { state with message := "Workgroup name cannot be empty" }elselet nextBufId := state.workgroups.foldl (fun m g =>g.workspaces.foldl (fun m2 ws => max m2 ws.nextBufferId) m) 0let newWg := createEmptyWorkgroup trimmed nextBufIdlet newState := { state withworkgroups := state.workgroups.push newWg,currentGroup := state.workgroups.size}return { newState with message := s!"Created workgroup: {trimmed}" }
let name := s!"Workspace {wg.workspaces.size}"let newWs := makeWorkspaceState name none maxBufIdlet newState := state.updateCurrentWorkgroup fun wg =>{ wg withworkspaces := wg.workspaces.push newWs,currentWorkspace := wg.workspaces.size}return { newState with message := s!"Created workspace: {name}" }
if state.mode == .command thenreturn ViE.Feature.openNameInputFloat state "New Workspace" "" "ws new "elselet name := s!"Workspace {wg.workspaces.size}"let newWs := makeWorkspaceState name none maxBufIdlet newState := state.updateCurrentWorkgroup fun wg =>{ wg withworkspaces := wg.workspaces.push newWs,currentWorkspace := wg.workspaces.size}return { newState with message := s!"Created workspace: {name}" }
let newWs := makeWorkspaceState name none maxBufIdlet newState := state.updateCurrentWorkgroup fun wg =>{ wg withworkspaces := wg.workspaces.push newWs,currentWorkspace := wg.workspaces.size}return { newState with message := s!"Created workspace: {name}" }
let trimmed := name.trimAscii.toStringif trimmed.isEmpty thenreturn { state with message := "Workspace name cannot be empty" }elselet newWs := makeWorkspaceState trimmed none maxBufIdlet newState := state.updateCurrentWorkgroup fun wg =>{ wg withworkspaces := wg.workspaces.push newWs,currentWorkspace := wg.workspaces.size}return { newState with message := s!"Created workspace: {trimmed}" }
let newWs := makeWorkspaceState name (some absPath) maxBufIdlet newState := state.updateCurrentWorkgroup fun wg =>{ wg withworkspaces := wg.workspaces.push newWs,currentWorkspace := wg.workspaces.size}return { newState with message := s!"Created workspace: {name} ({absPath})" }
let newWs := makeWorkspaceState trimmed (some absPath) maxBufIdlet newState := state.updateCurrentWorkgroup fun wg =>{ wg withworkspaces := wg.workspaces.push newWs,currentWorkspace := wg.workspaces.size}return { newState with message := s!"Created workspace: {trimmed} ({absPath})" }
def refreshActiveFileExplorer (state : EditorState) : IO EditorState := dolet buf := state.getActiveBuffermatch state.explorers.find? (fun (id, _) => id == buf.id) with| none => return state| some (_, explorer) =>if explorer.mode == .files thenViE.Feature.openExplorerWithPreview state explorer.currentPath explorer.previewWindowId explorer.previewBufferIdelsereturn statedef resolveCommandPath (state : EditorState) (path : String) : String :=if path.startsWith "/" then path else state.getCurrentWorkspace.resolvePath path
def cmdMkfile (args : List String) (state : EditorState) : IO EditorState := domatch args with| [rawPath] =>let path := (resolveCommandPath state rawPath).trimAscii.toStringlet leaf := (System.FilePath.fileName path).getD ""if path.isEmpty || leaf.isEmpty thenreturn { state with message := "File name cannot be empty" }elselet fp := System.FilePath.mk pathtryif ← fp.pathExists thenreturn { state with message := s!"File already exists: {path}" }elsematch fp.parent with| some p => IO.FS.createDirAll p| none => pure ()IO.FS.writeFile path ""let refreshed ← refreshActiveFileExplorer statereturn { refreshed with message := s!"Created file: {path}" }catch e =>return { state with message := s!"Error creating file: {e}" }| _ =>return { state with message := "Usage: :mkfile <path>" }def cmdMkdir (args : List String) (state : EditorState) : IO EditorState := domatch args with| [rawPath] =>let path := (resolveCommandPath state rawPath).trimAscii.toStringlet leaf := (System.FilePath.fileName path).getD ""if path.isEmpty || leaf.isEmpty thenreturn { state with message := "Directory name cannot be empty" }elselet fp := System.FilePath.mk pathtryif ← fp.pathExists thenif ← fp.isDir thenreturn { state with message := s!"Directory already exists: {path}" }elsereturn { state with message := s!"File already exists: {path}" }elseIO.FS.createDirAll pathlet refreshed ← refreshActiveFileExplorer statereturn { refreshed with message := s!"Created directory: {path}" }catch e =>return { state with message := s!"Error creating directory: {e}" }| _ =>return { state with message := "Usage: :mkdir <path>" }def cmdEx (args : List String) (state : EditorState) : IO EditorState :=match args with| [] =>ViE.Feature.openExplorer state (state.getCurrentWorkspace.rootPath.getD ".")| ["list"] =>ViE.Feature.openExplorer state (state.getCurrentWorkspace.rootPath.getD ".")| ["list", path] =>ViE.Feature.openExplorer state path| [path] =>ViE.Feature.openExplorer state path| _ =>return { state with message := "Usage: :ex [list [path]]" }
let path := match args with| [] => state.getCurrentWorkspace.rootPath.getD "."| [p] => p| _ => "."ViE.Feature.openExplorer state path
cmdEx args statedef cmdBuf (args : List String) (state : EditorState) : IO EditorState :=match args with| [] => ViE.Feature.openBufferExplorer state| ["list"] => ViE.Feature.openBufferExplorer state| ["ls"] => ViE.Feature.openBufferExplorer state| _ => return { state with message := "Usage: :buf [list|ls]" }def cmdBuffers (args : List String) (state : EditorState) : IO EditorState :=match args with| [] => ViE.Feature.openBufferExplorer state| _ => return { state with message := "Usage: :buffers" }
def cmdWgExplorer (_ : List String) (state : EditorState) : IO EditorState :=ViE.Feature.openWorkgroupExplorer state
def cmdWgExplorer (args : List String) (state : EditorState) : IO EditorState :=match args with| [] => cmdWg ["list"] state| _ => return { state with message := "Usage: :wgex" }
def openActiveAsFloating (state : EditorState) : EditorState :=state.updateCurrentWorkspace fun ws =>(ws.setWindowFloating ws.activeWindowId true).pruneFloatingWindowsdef openExplorerWindowsAsFloating (state : EditorState) (previewWindowId : Option Nat) : EditorState :=state.updateCurrentWorkspace fun ws =>let ws := ws.setWindowFloating ws.activeWindowId truelet ws :=match previewWindowId with| some wid =>if ws.layout.findView wid |>.isSome thenws.setWindowFloating wid trueelsews| none => wsws.pruneFloatingWindowsdef placeExplorerInWindow (state : EditorState) (bufferId : Nat) (cursorRow : Nat) (reuseActive : Bool) : EditorState :=if reuseActive thenstate.updateActiveView fun v =>{ v with bufferId := bufferId, cursor := {row := ⟨cursorRow⟩, col := 0}, scrollRow := 0, scrollCol := 0 }elselet ws := state.getCurrentWorkspacelet newWinId := ws.nextWindowIdlet s1 := ViE.Window.splitWindow state falses1.updateCurrentWorkspace fun ws =>{ ws withactiveWindowId := newWinId,layout := ws.layout.updateView newWinId (fun v =>{ v with bufferId := bufferId, cursor := {row := ⟨cursorRow⟩, col := 0}, scrollRow := 0, scrollCol := 0 })}def openNameInputFloat (state : EditorState) (title : String) (initialText : String) (commandPrefix : String) : EditorState :=let ov : FloatingOverlay := {title := titlelines := #[initialText]maxWidth := 48cursorRow := 0cursorCol := initialText.toList.length}{state withmode := .normalfloatingOverlay := some ovfloatingInputCommand := some commandPrefixinputState := {state.inputState withcommandBuffer := ""countBuffer := ""previousKey := nonependingKeys := ""}message := ""dirty := true}def fileExplorerNewFileLabel : String := "[New File]"def fileExplorerNewDirectoryLabel : String := "[New Directory]"def isFileExplorerActionEntry (entry : FileEntry) : Bool :=entry.name == fileExplorerNewFileLabel || entry.name == fileExplorerNewDirectoryLabeldef fileExplorerActionEntries : List FileEntry := [{name := fileExplorerNewFileLabelpath := ""isDirectory := false},{name := fileExplorerNewDirectoryLabelpath := ""isDirectory := false}]def firstNonActionEntryIndex (entries : List FileEntry) : Option Nat :=let rec loop (rest : List FileEntry) (idx : Nat) : Option Nat :=match rest with| [] => none| e :: tail =>if isFileExplorerActionEntry e thenloop tail (idx + 1)elsesome idxloop entries 0def defaultExplorerCursorRow (entries : List FileEntry) : Nat :=match firstNonActionEntryIndex entries with| some idx => 2 + idx| none => 2def ensureTrailingSlash (path : String) : String :=if path.endsWith "/" then path else path ++ "/"
def bufferEntryPrefix : String := "buffer://"def makeBufferEntryPath (bufferId : Nat) : String :=s!"{bufferEntryPrefix}{bufferId}"def parseBufferEntryId (path : String) : Option Nat :=if path.startsWith bufferEntryPrefix then(path.drop bufferEntryPrefix.length).toString.toNat?elsenonedef selectedBufferIdFromExplorer (state : EditorState) (explorer : ExplorerState) : Option Nat :=match getSelectedEntry state explorer with| none => none| some entry =>match parseBufferEntryId entry.path with| none => none| some bid =>let ws := state.getCurrentWorkspaceif ws.buffers.any (fun b => b.id == bid) thensome bidelsenonedef bufferExplorerLabel (buf : FileBuffer) : String :=let name := buf.filename.getD s!"[No Name:{buf.id}]"let dirty := if buf.dirty then " [+]" else ""s!"[{buf.id}] {name}{dirty}"
| .buffers =>match selectedBufferIdFromExplorer state explorer with| none => return state| some previewBufId =>let s1 := state.updateCurrentWorkspace fun ws =>{ ws with layout := ws.layout.updateView previewWinId (fun v =>{ v with bufferId := previewBufId, cursor := Point.zero, scrollRow := 0, scrollCol := 0 }) }let s2 := updateExplorerState s1 bufId (fun ex => { ex with previewBufferId := none })return s2
let entryOpt := getSelectedEntry s1 explorer
let selected := getSelectedEntry s1 explorerlet entryOpt : Option FileEntry :=match selected with| some e =>if isFileExplorerActionEntry e thenexplorer.entries.find? (fun x => !isFileExplorerActionEntry x)elsesome e| none =>explorer.entries.find? (fun x => !isFileExplorerActionEntry x)
if entry.isDirectory then
if entry.isDirectory thenlet content : TextBuffer := #[#[]]let buildLeafBits := s1.config.searchBloomBuildLeafBitslet buf := { ViE.Buffer.fileBufferFromTextBufferWithConfig 0 (some "preview://") content buildLeafBits with dirty := false }let (pid, s2) := ViE.Buffer.addBuffer s1 bufpure (s2, pid)elseensurePreviewBuffer s1 explorer entry| none =>
elseensurePreviewBuffer s1 explorer entry| none =>let content : TextBuffer := #[#[]]let buildLeafBits := s1.config.searchBloomBuildLeafBitslet buf := { ViE.Buffer.fileBufferFromTextBufferWithConfig 0 (some "preview://") content buildLeafBits with dirty := false }let (pid, s2) := ViE.Buffer.addBuffer s1 bufpure (s2, pid)
| .buffers =>let fallbackBufId :=match explorer.entries.find? (fun e => (parseBufferEntryId e.path).isSome) with| some e => (parseBufferEntryId e.path).getD s1.getActiveBuffer.id| none => s1.getActiveBuffer.idlet previewBufId := (selectedBufferIdFromExplorer s1 explorer).getD fallbackBufIdpure (s1, previewBufId)
{ ex with previewWindowId := some previewWinId, previewBufferId := some previewBufId })return s4
{ex withpreviewWindowId := some previewWinIdpreviewBufferId := if explorer.mode == .buffers then none else some previewBufId})return openExplorerWindowsAsFloating s4 (some previewWinId)
return { s4 with message := "Preview opened" }
let s5 := openExplorerWindowsAsFloating s4 (some previewWinId)return { s5 with message := "Preview opened" }| .buffers =>match selectedBufferIdFromExplorer state explorer with| none =>return { state with message := "Preview: no buffer selection" }| some previewBufId =>let ws := state.getCurrentWorkspacelet previewWinId := ws.nextWindowIdlet s1 := ViE.Window.splitWindow state falselet s2 := s1.updateCurrentWorkspace fun ws =>{ ws with layout := ws.layout.updateView previewWinId (fun v =>{ v with bufferId := previewBufId, cursor := Point.zero, scrollRow := 0, scrollCol := 0 }) }let s3 := updateExplorerState s2 bufId (fun ex => { ex with previewWindowId := some previewWinId, previewBufferId := none })let s4 := openExplorerWindowsAsFloating s3 (some previewWinId)return { s4 with message := "Preview opened" }
let icon := if entry.isDirectory then state.config.dirIcon else state.config.fileIconlet suffix := if entry.isDirectory then "/" else ""
let isAction := isFileExplorerActionEntry entrylet icon :=if isAction then ""else if entry.isDirectory then state.config.dirIconelse state.config.fileIconlet suffix := if !isAction && entry.isDirectory then "/" else ""
-- Switch to explorer bufferlet s''' := s''.updateActiveView fun v => { v with bufferId := bufferId, cursor := {row := 2, col := 0}, scrollRow := 0, scrollCol := 0 }
-- Open explorer in a dedicated window by default; reuse active only when already in explorer.let reuseActive := state.explorers.any (fun (id, _) => id == state.getActiveBuffer.id)let s''' := placeExplorerInWindow s'' bufferId (defaultExplorerCursorRow explorerState.entries) reuseActive
let icon := if entry.isDirectory then state.config.dirIcon else state.config.fileIconlet suffix := if entry.isDirectory then "/" else ""
let isAction := isFileExplorerActionEntry entrylet icon :=if isAction then ""else if entry.isDirectory then state.config.dirIconelse state.config.fileIconlet suffix := if !isAction && entry.isDirectory then "/" else ""
let s''' := s''.updateActiveView fun v => { v with bufferId := bufferId, cursor := {row := 2, col := 0}, scrollRow := 0, scrollCol := 0 }
let reuseActive := state.explorers.any (fun (id, _) => id == state.getActiveBuffer.id)let s''' := placeExplorerInWindow s'' bufferId (defaultExplorerCursorRow explorerState.entries) reuseActive
| some _ => refreshExplorerPreview s''''| none => openExplorerDefaultPreview s'''' bufferId explorerState
| some wid => refreshExplorerPreview (openExplorerWindowsAsFloating s''''' (some wid))| none => openExplorerDefaultPreview s''''' bufferId explorerState
let s''' := s''.updateActiveView fun v => { v with bufferId := bufferId, cursor := {row := 2, col := 0}, scrollRow := 0, scrollCol := 0 }
let reuseActive := state.explorers.any (fun (id, _) => id == state.getActiveBuffer.id)let s''' := placeExplorerInWindow s'' bufferId 2 reuseActive
let s''' := s''.updateActiveView fun v =>{ v with bufferId := bufferId, cursor := {row := ⟨2 + explorerState.selectedIndex⟩, col := 0}, scrollRow := 0, scrollCol := 0 }
let reuseActive := state.explorers.any (fun (id, _) => id == state.getActiveBuffer.id)let s''' := placeExplorerInWindow s'' bufferId (2 + explorerState.selectedIndex) reuseActive
return (← openExplorerDefaultPreview s'''' bufferId explorerState)
let s''''' := openActiveAsFloating s''''return (← openExplorerDefaultPreview s''''' bufferId explorerState)/-- Open the buffer explorer to list currently opened workspace buffers. -/def openBufferExplorer (state : EditorState) : IO EditorState := dolet ws := state.getCurrentWorkspacelet explorerBufIds := state.explorers.map (fun (id, _) => id)let previewBufIds := state.explorers.filterMap (fun (_, ex) => ex.previewBufferId)let skipBufferId (bid : Nat) : Bool :=explorerBufIds.contains bid || previewBufIds.contains bidlet candidates := ws.buffers.filter (fun b => !skipBufferId b.id)if candidates.isEmpty thenreturn { state with message := "No open buffers" }let sortedBuffers := (candidates.toArray.qsort fun a b => a.id < b.id).toListlet entries := sortedBuffers.map fun b =>{name := bufferExplorerLabel bpath := makeBufferEntryPath b.idisDirectory := false}let activeBufferId := state.getActiveBuffer.idlet rec findSelectedIndex (rest : List FileEntry) (idx : Nat) : Nat :=match rest with| [] => 0| entry :: tail =>if parseBufferEntryId entry.path == some activeBufferId thenidxelsefindSelectedIndex tail (idx + 1)let selectedIndex := findSelectedIndex entries 0let explorerState : ExplorerState := {currentPath := "buffers"entries := entriesselectedIndex := selectedIndexmode := .bufferspreviewWindowId := nonepreviewBufferId := none}let header := [s!"Buffer Explorer: {ws.name}", ""]let mut content := headerlet mut idx := 0for entry in explorerState.entries dolet pref := if idx == explorerState.selectedIndex then "> " else " "let mark :=if parseBufferEntryId entry.path == some activeBufferId then"* "else" "content := content ++ [s!"{pref}{mark}{state.config.fileIcon}{entry.name}"]idx := idx + 1let contentBuffer := content.toArray.map stringToLinelet (bufferId, s') := ViE.Buffer.createNewBuffer state contentBuffer (some "explorer://buffers")let s'' := { s' with explorers := (bufferId, explorerState) :: s'.explorers }let reuseActive := state.explorers.any (fun (id, _) => id == state.getActiveBuffer.id)let s''' := placeExplorerInWindow s'' bufferId (2 + explorerState.selectedIndex) reuseActivelet s'''' := { s''' with message := "Buffer Explorer" }let s''''' := openActiveAsFloating s''''return (← openExplorerDefaultPreview s''''' bufferId explorerState)
if entry.isDirectory then
if entry.name == fileExplorerNewFileLabel thenlet cmdPrefix := s!"mkfile {ensureTrailingSlash explorer.currentPath}"return openNameInputFloat state "New File" "" cmdPrefixelse if entry.name == fileExplorerNewDirectoryLabel thenlet cmdPrefix := s!"mkdir {ensureTrailingSlash explorer.currentPath}"return openNameInputFloat state "New Directory" "" cmdPrefixelse if entry.isDirectory then
-- Close preview window if it exists
let ws0 := state.getCurrentWorkspacelet explorerWinId := ws0.activeWindowIdlet explorerBufIds := state.explorers.map (fun (id, _) => id)let targetWinOpt :=let ids := ViE.Window.getWindowIds ws0.layoutids.find? fun wid =>if wid == explorerWinId thenfalseelse if explorer.previewWindowId == some wid thenfalseelsematch ws0.layout.findView wid with| some v =>let isExplorerBuf := explorerBufIds.contains v.bufferIdlet isPreviewBuf :=match explorer.previewBufferId with| some pid => v.bufferId == pid| none => false!isExplorerBuf && !isPreviewBuf| none => false-- Close preview and unregister explorer metadata.
let s1 := state.updateCurrentWorkspace fun ws =>match ws.layout.remove previewId with| some newLayout => { ws with layout := newLayout }| none => wslet s2 := updateExplorerState s1 buf.id (fun ex => { ex with previewWindowId := none, previewBufferId := none })match explorer.previewBufferId with| some previewBufId => ViE.Buffer.removeBuffer s2 previewBufId| none => s2-- Open fileViE.Buffer.openBuffer s1 entry.path
let s1 := state.updateCurrentWorkspace fun ws =>match ws.layout.remove previewId with| some newLayout => ({ ws with layout := newLayout }).pruneFloatingWindows| none => ws.pruneFloatingWindowslet s2 := updateExplorerState s1 buf.id (fun ex => { ex with previewWindowId := none, previewBufferId := none })match explorer.previewBufferId with| some previewBufId => ViE.Buffer.removeBuffer s2 previewBufId| none => s2let s1 := { s1 with explorers := s1.explorers.filter (fun (id, _) => id != buf.id) }match targetWinOpt with| some targetWinId =>let s2 := s1.updateCurrentWorkspace fun ws =>match ws.layout.remove explorerWinId with| some newLayout =>let ids := ViE.Window.getWindowIds newLayoutlet activeId :=if ids.contains targetWinId thentargetWinIdelsematch ids with| h :: _ => h| [] => ws.activeWindowId({ ws with layout := newLayout, activeWindowId := activeId }).pruneFloatingWindows| none =>ws.pruneFloatingWindowslet s3 := ViE.Buffer.removeBuffer s2 buf.idlet opened ← ViE.Buffer.openBuffer s3 entry.pathreturn opened| none =>let opened ← ViE.Buffer.openBuffer s1 entry.pathlet s2 := opened.updateCurrentWorkspace fun ws =>(ws.setWindowFloating ws.activeWindowId false).pruneFloatingWindowslet s3 := ViE.Buffer.removeBuffer s2 buf.idreturn s3| .buffers =>let entry := explorer.entries[selectedIdx.val]!match parseBufferEntryId entry.path with| none =>return { state with message := "Invalid buffer entry" }| some targetBufId =>let ws0 := state.getCurrentWorkspaceif !ws0.buffers.any (fun b => b.id == targetBufId) thenreturn { state with message := s!"Buffer not found: {targetBufId}" }elselet explorerWinId := ws0.activeWindowIdlet explorerBufIds := state.explorers.map (fun (id, _) => id)let targetWinOpt :=let ids := ViE.Window.getWindowIds ws0.layoutids.find? fun wid =>if wid == explorerWinId thenfalseelse if explorer.previewWindowId == some wid thenfalseelsematch ws0.layout.findView wid with| some v =>let isExplorerBuf := explorerBufIds.contains v.bufferId!isExplorerBuf| none => falselet targetLabel :=match ws0.buffers.find? (fun b => b.id == targetBufId) with| some b => b.filename.getD s!"[No Name:{targetBufId}]"| none => s!"[No Name:{targetBufId}]"-- Close preview and unregister explorer metadata.let s1 :=match explorer.previewWindowId with| none => state| some previewId =>let s1 := state.updateCurrentWorkspace fun ws =>match ws.layout.remove previewId with| some newLayout => ({ ws with layout := newLayout }).pruneFloatingWindows| none => ws.pruneFloatingWindowsupdateExplorerState s1 buf.id (fun ex => { ex with previewWindowId := none, previewBufferId := none })let s1 := { s1 with explorers := s1.explorers.filter (fun (id, _) => id != buf.id) }match targetWinOpt with| some targetWinId =>let s2 := s1.updateCurrentWorkspace fun ws =>match ws.layout.remove explorerWinId with| some newLayout =>let ids := ViE.Window.getWindowIds newLayoutlet activeId :=if ids.contains targetWinId thentargetWinIdelsematch ids with| h :: _ => h| [] => ws.activeWindowId({ ws with layout := newLayout, activeWindowId := activeId }).pruneFloatingWindows| none =>ws.pruneFloatingWindowslet s3 := ViE.Buffer.removeBuffer s2 buf.idlet s4 := s3.updateActiveView fun v =>{ v with bufferId := targetBufId, cursor := Point.zero, scrollRow := 0, scrollCol := 0 }return { s4 with message := s!"Switched to buffer: {targetLabel}" }| none =>let s2 := s1.updateActiveView fun v =>{ v with bufferId := targetBufId, cursor := Point.zero, scrollRow := 0, scrollCol := 0 }let s3 := s2.updateCurrentWorkspace fun ws =>(ws.setWindowFloating ws.activeWindowId false).pruneFloatingWindowslet s4 := ViE.Buffer.removeBuffer s3 buf.idreturn { s4 with message := s!"Switched to buffer: {targetLabel}" }
let s' := { state withmode := .commandinputState := { state.inputState with commandBuffer := "wg new ", countBuffer := "", previousKey := none, pendingKeys := "" }}return { s' with message := "Workgroup name:" }
return openNameInputFloat state "New Workgroup" "" "wg new "
let s' := { state withmode := .commandinputState := { state.inputState with commandBuffer := s!"wg rename {current}", countBuffer := "", previousKey := none, pendingKeys := "" }}return { s' with message := "Workgroup rename:" }
return openNameInputFloat state "Rename Workgroup" current "wg rename "
let s' := { state withmode := .commandinputState := { state.inputState with commandBuffer := "ws new ", countBuffer := "", previousKey := none, pendingKeys := "" }}return { s' with message := "Workspace name:" }
return openNameInputFloat state "New Workspace" "" "ws new "
let s' := { state withmode := .commandinputState := { state.inputState with commandBuffer := s!"ws rename {current}", countBuffer := "", previousKey := none, pendingKeys := "" }}return { s' with message := "Workspace rename:" }
return openNameInputFloat state "Rename Workspace" current "ws rename "
assertEqual "New workspace enters command mode" Mode.command s8b.modeassertEqual "New workspace command prompt" "ws new " s8b.inputState.commandBuffer
assertEqual "New workspace opens floating input" true s8b.floatingOverlay.isSomeassertEqual "New workspace floating command prefix" (some "ws new ") s8b.floatingInputCommand
assertEqual "Rename workspace enters command mode" Mode.command s8d.modeassertEqual "Rename workspace command prompt" true (s8d.inputState.commandBuffer.startsWith "ws rename ")
assertEqual "Rename workspace opens floating input" true s8d.floatingOverlay.isSomeassertEqual "Rename workspace floating command prefix" (some "ws rename ") s8d.floatingInputCommand
assertEqual "Workspace explorer preview window is floating" true (wsPrev.isFloatingWindow previewWinId)let pairSideBySide :=match s12c.getFloatingWindowBounds wsPrev.activeWindowId, s12c.getFloatingWindowBounds previewWinId with| some (et, el, eh, ew), some (pt, pl, ph, pw) =>et == pt && eh == ph && ((el + ew <= pl) || (pl + pw <= el))| _, _ => falseassertEqual "Workspace explorer/preview floating pair is side-by-side" true pairSideBySide
assertEqual "Workgroup explorer preview window is floating" true (wsPrev.isFloatingWindow previewWinId)let pairSideBySide :=match s2.getFloatingWindowBounds wsPrev.activeWindowId, s2.getFloatingWindowBounds previewWinId with| some (et, el, eh, ew), some (pt, pl, ph, pw) =>et == pt && eh == ph && ((el + ew <= pl) || (pl + pw <= el))| _, _ => falseassertEqual "Workgroup explorer/preview floating pair is side-by-side" true pairSideBySide
assertEqual "New workgroup enters command mode" Mode.command s4.modeassertEqual "New workgroup command prompt" "wg new " s4.inputState.commandBuffer
assertEqual "New workgroup opens floating input" true s4.floatingOverlay.isSomeassertEqual "New workgroup floating command prefix" (some "wg new ") s4.floatingInputCommand
assertEqual "Rename workgroup enters command mode" Mode.command s4b.modeassertEqual "Rename workgroup command prompt" true (s4b.inputState.commandBuffer.startsWith "wg rename ")
assertEqual "Rename workgroup opens floating input" true s4b.floatingOverlay.isSomeassertEqual "Rename workgroup floating command prefix" (some "wg rename ") s4b.floatingInputCommand
import Test.TreeStatsdef main (args : List String) : IO Unit :=Test.TreeStats.main args
if i % 1000 == 0 thenIO.println s!"Iteration {i}..."
return ptpartial def countNodes (t : PieceTree) : Nat :=match t with| .empty => 0| .leaf _ _ _ => 1| .internal cs _ _ => 1 + cs.foldl (fun acc c => acc + countNodes c) 0partial def countLeaves (t : PieceTree) : Nat :=match t with| .empty => 0| .leaf _ _ _ => 1| .internal cs _ _ => cs.foldl (fun acc c => acc + countLeaves c) 0partial def maxLeafPieces (t : PieceTree) : Nat :=match t with| .empty => 0| .leaf ps _ _ => ps.size| .internal cs _ _ => cs.foldl (fun acc c => max acc (maxLeafPieces c)) 0partial def maxInternalChildren (t : PieceTree) : Nat :=match t with| .empty => 0| .leaf _ _ _ => 0| .internal cs _ _ =>cs.foldl (fun acc c => max acc (maxInternalChildren c)) cs.sizedef parseIterations (args : List String) : Nat :=let args :=match args with| "--" :: rest => rest| _ => argsmatch args with| a :: _ =>match a.toNat? with| some n => n| none => 5000| [] => 5000
let h := PieceTree.height treelet nodes := countNodes treelet leaves := countLeaves treelet maxLeaf := maxLeafPieces treelet maxChildren := maxInternalChildren treeIO.println s!"iterations={iterations}"IO.println s!"bytes={PieceTree.length tree} lines={PieceTree.lineBreaks tree} height={h}"IO.println s!"nodes={nodes} leaves={leaves} maxLeafPieces={maxLeaf} maxChildren={maxChildren}"
| .empty => IO.println "Tree is empty"| .leaf ps _ => IO.println s!"Tree is a single leaf with {ps.size} pieces"| .internal cs s =>IO.println s!"Tree is internal with {cs.size} children at root"IO.println s!"Tree height: {s.height}"
| .empty => IO.println "root=empty"| .leaf ps _ _ => IO.println s!"root=leaf pieces={ps.size}"| .internal cs _ _ => IO.println s!"root=internal children={cs.size}"
-- Check if children are also flatlet mut maxChildSize := 0for c in cs domatch c with| .internal cs2 _ => maxChildSize := max maxChildSize cs2.size| .leaf ps _ => maxChildSize := max maxChildSize ps.size| .empty => continueIO.println s!"Max child size at level 1: {maxChildSize}"
let endT := ← IO.monoMsNowIO.println s!"Time to getLineRange for last line: {endT - start}ms"
let stop := ← IO.monoMsNowIO.println s!"getLineRange(last)={stop - start}ms"def test : IO Unit := doIO.println "Starting TreeStats Test..."let iterations := 3000let pt := buildWorkload iterationslet t := pt.treelet bytes := PieceTree.length tlet textBytes := pt.toString.toUTF8.sizelet isNonEmpty :=match t with| .empty => false| _ => trueassertEqual "tree length equals rendered bytes" textBytes bytesassert "tree is non-empty" isNonEmptyassert "tree height is at least 2 after workload" (PieceTree.height t >= 2)assert "leaf piece count stays within node capacity" (maxLeafPieces t <= NodeCapacity)assert "internal children stay within node capacity" (maxInternalChildren t <= NodeCapacity)assert "line range for last inserted line exists" ((pt.getLineRange (iterations - 1)).isSome)IO.println "TreeStats Test passed!"def main (args : List String) : IO Unit := dolet iterations := parseIterations argslet pt := buildWorkload iterationsreport pt iterationsend Test.TreeStats
let s0 := ViE.startOrUpdateSearch ViE.initialState "abc" .forward falselet (m1, s1) := ViE.UI.getLineSearchMatches s0 s0.getActiveBuffer.id 0 "abc abc"assertEqual "Line search cache computes matches" true (m1.size > 0)let cachedBeforeSwitch :=match s1.searchState with| some ss => ss.lineMatches.contains 0| none => falseassertEqual "Line search cache populated" true cachedBeforeSwitchlet s2 := ViE.UI.ensureSearchLineCacheForBuffer s1 9999let cacheClearedOnSwitch :=match s2.searchState with| some ss => ss.lineMatches.isEmpty && ss.lineCacheBufferId == some 9999| none => falseassertEqual "Line search cache resets on buffer switch" true cacheClearedOnSwitchlet t0 := 1000let (p0, k0) := ViE.parseKey ViE.initialState '\x1b' t0let (p1, k1) := ViE.parseKey p0 '[' (t0 + 1)let (p2, k2) := ViE.parseKey p1 '1' (t0 + 2)let (p3, k3) := ViE.parseKey p2 ';' (t0 + 3)let (p4, k4) := ViE.parseKey p3 '2' (t0 + 4)let (p5, k5) := ViE.parseKey p4 'X' (t0 + 5)assertEqual "Unknown CSI emits no keys while pending" ([] : List Key) (k0 ++ k1 ++ k2 ++ k3 ++ k4)assertEqual "Unknown CSI final emits Key.unknown" [Key.unknown 'X'] k5assertEqual "Unknown CSI clears pending state" "" p5.inputState.pendingKeys
let chars : List Char := ['\x1b', '[', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3']let (pLong, emitted) := chars.foldl(fun (acc : EditorState × List Key) ch =>let (st, out) := acclet (st', keys) := ViE.parseKey st ch (t0 + 20)(st', out ++ keys))(ViE.initialState, [])assertEqual "Overlong unknown CSI flushes emitted keys" true (!emitted.isEmpty)assertEqual "Overlong unknown CSI clears pending state" "" pLong.inputState.pendingKeys
def testUiCommands : IO Unit := doIO.println " Testing UI Commands..."let s0 := ViE.initialStatelet s1 ← runKeys s0 ([Key.char ':'] ++ keys "float hello" ++ [Key.enter])assertEqual ":float shows overlay" true s1.floatingOverlay.isSomelet s2 ← runKeys s1 [Key.esc]assertEqual "Esc closes overlay" false s2.floatingOverlay.isSomelet s3 ← runKeys s0 ([Key.char ':'] ++ keys "float alpha\\nbeta" ++ [Key.enter])match s3.floatingOverlay with| some overlay =>assertEqual ":float parses newline escape (line count)" 2 overlay.lines.sizeassertEqual ":float parses newline escape (line 1)" "alpha" overlay.lines[0]!assertEqual ":float parses newline escape (line 2)" "beta" overlay.lines[1]!| none =>assertEqual ":float newline overlay exists" true falselet s4 ← runKeys s3 ([Key.char ':'] ++ keys "nofloat" ++ [Key.enter])assertEqual ":nofloat clears overlay" false s4.floatingOverlay.isSomelet s5 ← runKeys s0 ([Key.char ':'] ++ keys "redraw" ++ [Key.enter])assertEqual ":redraw sets message" "redraw" s5.messageassertEqual ":redraw marks dirty" true s5.dirtylet s6 ← runKeys s0 ([Key.char ':'] ++ keys "redraw!" ++ [Key.enter])assertEqual ":redraw! alias works" "redraw" s6.messagelet s7 ← runKeys s0 [Key.ctrl 'l']assertEqual "Ctrl-l redraw marks dirty" true s7.dirtylet s8 ← runKeys s0 ([Key.char ':'] ++ keys "float --title Note --width 32 hello world" ++ [Key.enter])match s8.floatingOverlay with| some overlay =>assertEqual ":float --title sets title" "Note" overlay.titleassertEqual ":float --width sets width" 32 overlay.maxWidthassertEqual ":float with options keeps text" "hello world" overlay.lines[0]!| none =>assertEqual ":float with options shows overlay" true falselet s9 ← runKeys s0 ([Key.char ':'] ++ keys "float --title=Panel --width=28 hi" ++ [Key.enter])match s9.floatingOverlay with| some overlay =>assertEqual ":float --title= sets title" "Panel" overlay.titleassertEqual ":float --width= sets width" 28 overlay.maxWidth| none =>assertEqual ":float with inline options shows overlay" true falselet s10 ← runKeys s0 ([Key.char ':'] ++ keys "float --width nope hi" ++ [Key.enter])assertEqual ":float invalid width message" "Invalid float width: nope" s10.messagelet s11 ← runKeys s0 ([Key.char 'i'] ++ keys "abc" ++ [Key.esc] ++ [Key.char ':'] ++ keys "float guard" ++ [Key.enter])let s12 ← runKeys s11 [Key.char 'i']assertEqual "floating overlay enters insert mode" Mode.insert s12.modelet s13 ← runKeys s12 (keys "X" ++ [Key.enter] ++ keys "Y")match s13.floatingOverlay with| some overlay =>assertEqual "floating overlay writes text" "guardX" overlay.lines[0]!assertEqual "floating overlay writes next line" "Y" overlay.lines[1]!| none =>assertEqual "floating overlay remains open while editing" true falselet s14 ← runKeys s13 [Key.esc]assertEqual "Esc exits floating overlay insert mode" Mode.normal s14.modelet s15 ← runKeys s14 [Key.enter]assertEqual "Enter closes floating overlay" false s15.floatingOverlay.isSomelet sMsg0 := { s0 with message := "Error: sample message", dirty := true }let sMsg1 ← runKeys sMsg0 [Key.enter]assertEqual "Enter closes message float" "" sMsg1.messagelet sMsg2 := { s0 with message := "Cannot write preview buffer", dirty := true }let sMsg3 ← runKeys sMsg2 [Key.esc]assertEqual "Esc closes message float" "" sMsg3.messagelet sPrompt0 ← runKeys s0 ([Key.char ':'] ++ keys "ws list" ++ [Key.enter])let sPrompt1 := sPrompt0.updateActiveView fun v => { v with cursor := { row := ⟨2⟩, col := 0 } }let sPrompt2 ← runKeys sPrompt1 [Key.enter]assertEqual "Workspace explorer New opens floating input" true sPrompt2.floatingOverlay.isSomeassertEqual "Workspace explorer New sets floating command prefix" (some "ws new ") sPrompt2.floatingInputCommandlet sPrompt3 ← runKeys sPrompt2 (keys "TmpWS" ++ [Key.enter])assertEqual "Workspace explorer floating input submits with Enter" "TmpWS" sPrompt3.getCurrentWorkspace.nameassertEqual "Workspace explorer floating input closes after submit" false sPrompt3.floatingOverlay.isSomelet sWsCmd0 ← runKeys s0 ([Key.char ':'] ++ keys "ws new" ++ [Key.enter])assertEqual ":ws new opens floating input" true sWsCmd0.floatingOverlay.isSomeassertEqual ":ws new floating command prefix" (some "ws new ") sWsCmd0.floatingInputCommandlet sWsCmd1 ← runKeys sWsCmd0 (keys "CmdWorkspace" ++ [Key.enter])assertEqual ":ws new floating input submits name" "CmdWorkspace" sWsCmd1.getCurrentWorkspace.namelet sWgCmd0 ← runKeys s0 ([Key.char ':'] ++ keys "wg new" ++ [Key.enter])assertEqual ":wg new opens floating input" true sWgCmd0.floatingOverlay.isSomeassertEqual ":wg new floating command prefix" (some "wg new ") sWgCmd0.floatingInputCommandlet sWgCmd1 ← runKeys sWgCmd0 (keys "CmdGroup" ++ [Key.enter])assertEqual ":wg new floating input submits name" "CmdGroup" sWgCmd1.getCurrentWorkgroup.namelet stamp ← IO.monoMsNowlet tmpRoot := s!"/tmp/vie-explorer-create-{stamp}"IO.FS.createDirAll tmpRootlet sExp0 := { s0 with windowHeight := 30, windowWidth := 100 }let sExp1 ← runKeys sExp0 ([Key.char ':'] ++ keys s!"ex list {tmpRoot}" ++ [Key.enter])let explorerOpt := sExp1.explorers.find? (fun (id, _) => id == sExp1.getActiveBuffer.id)match explorerOpt with| none =>assertEqual "Explorer create test has active explorer" true false| some (_, explorer) =>let newFileIdx := (findEntryIndex explorer.entries "[New File]").getD 0let sExp2 := sExp1.updateActiveView fun v => { v with cursor := { row := ⟨2 + newFileIdx⟩, col := 0 } }let sExp3 ← runKeys sExp2 [Key.enter]assertEqual "Explorer [New File] opens floating input" true sExp3.floatingOverlay.isSomeassertEqual "Explorer [New File] sets command prefix" (some s!"mkfile {tmpRoot}/") sExp3.floatingInputCommandlet sExp4 ← runKeys sExp3 (keys "alpha.txt" ++ [Key.enter])let fileCreated ← (System.FilePath.mk s!"{tmpRoot}/alpha.txt").pathExistsassertEqual "Explorer [New File] creates file" true fileCreatedlet explorerOpt2 := sExp4.explorers.find? (fun (id, _) => id == sExp4.getActiveBuffer.id)match explorerOpt2 with| none =>assertEqual "Explorer refreshed after file create" true false| some (_, explorer2) =>assertEqual "Explorer list includes created file" true (findEntryIndex explorer2.entries "alpha.txt").isSomelet newDirIdx := (findEntryIndex explorer.entries "[New Directory]").getD 1let sExp5 := sExp4.updateActiveView fun v => { v with cursor := { row := ⟨2 + newDirIdx⟩, col := 0 } }let sExp6 ← runKeys sExp5 [Key.enter]assertEqual "Explorer [New Directory] opens floating input" true sExp6.floatingOverlay.isSomeassertEqual "Explorer [New Directory] sets command prefix" (some s!"mkdir {tmpRoot}/") sExp6.floatingInputCommandlet sExp7 ← runKeys sExp6 (keys "subdir" ++ [Key.enter])let createdDirPath := System.FilePath.mk s!"{tmpRoot}/subdir"let dirExists ← createdDirPath.pathExistslet dirIsDir ← if dirExists then createdDirPath.isDir else pure falseassertEqual "Explorer [New Directory] creates directory" true dirIsDirlet explorerOpt3 := sExp7.explorers.find? (fun (id, _) => id == sExp7.getActiveBuffer.id)match explorerOpt3 with| none =>assertEqual "Explorer refreshed after directory create" true false| some (_, explorer3) =>assertEqual "Explorer list includes created directory" true (findEntryIndex explorer3.entries "subdir").isSomelet s16 ← runKeys s0 ([Key.char ':'] ++ keys "vs" ++ [Key.enter])let ws16 := s16.getCurrentWorkspaceassertEqual "split creates second window" true (ws16.nextWindowId >= 2)let s17 ← runKeys s16 ([Key.char ':'] ++ keys "floatwin on" ++ [Key.enter])let ws17 := s17.getCurrentWorkspaceassertEqual ":floatwin on marks active window floating" true (ws17.isFloatingWindow ws17.activeWindowId)let s18 ← runKeys s17 ([Key.char ':'] ++ keys "floatwin off" ++ [Key.enter])let ws18 := s18.getCurrentWorkspaceassertEqual ":floatwin off clears floating flag" false (ws18.isFloatingWindow ws18.activeWindowId)let s19 ← runKeys s18 ([Key.char ':'] ++ keys "floatwin" ++ [Key.enter])let ws19 := s19.getCurrentWorkspaceassertEqual ":floatwin toggles floating flag" true (ws19.isFloatingWindow ws19.activeWindowId)let sFloatMove0 := { s0 with windowHeight := 24, windowWidth := 80 }let sFloatMove1 ← runKeys sFloatMove0 ([Key.char ':'] ++ keys "floatwin on" ++ [Key.enter])let floatMoveId := sFloatMove1.getCurrentWorkspace.activeWindowIdlet beforeFloatMove := sFloatMove1.getFloatingWindowBounds floatMoveIdlet sFloatMove2 ← runKeys sFloatMove1 [Key.alt 'L']let afterFloatMove := sFloatMove2.getFloatingWindowBounds floatMoveIdmatch beforeFloatMove, afterFloatMove with| some (_, left0, _, _), some (_, left1, _, _) =>assertEqual "Alt-Shift-l moves floating window right" (left0 + 1) left1| _, _ =>assertEqual "Alt-Shift-l moves floating window right" true falselet sFloatMoveGrp0 := { s0 with windowHeight := 24, windowWidth := 80 }let sFloatMoveGrp1 ← runKeys sFloatMoveGrp0 ([Key.char ':'] ++ keys "floatwin on" ++ [Key.enter])let sFloatMoveGrp2 ← runKeys sFloatMoveGrp1 ([Key.char ':'] ++ keys "vsplit" ++ [Key.enter])let moveGrpIds := sFloatMoveGrp2.getCurrentWorkspace.getFloatingWindowIds.toListlet beforeGrp := moveGrpIds.filterMap (fun wid => (sFloatMoveGrp2.getFloatingWindowBounds wid).map (fun b => (wid, b)))let sFloatMoveGrp3 ← runKeys sFloatMoveGrp2 [Key.alt 'J']let afterGrp := moveGrpIds.filterMap (fun wid => (sFloatMoveGrp3.getFloatingWindowBounds wid).map (fun b => (wid, b)))let movedAllDown :=moveGrpIds.all (fun wid =>match beforeGrp.find? (fun (id, _) => id == wid), afterGrp.find? (fun (id, _) => id == wid) with| some (_, (top0, _, _, _)), some (_, (top1, _, _, _)) => top1 == top0 + 1| _, _ => false)assertEqual "Alt-Shift-j moves all panes in active floating subtree down" true movedAllDownlet sFloatVs0 := { s0 with windowHeight := 24, windowWidth := 80 }let sFloatVs1 ← runKeys sFloatVs0 ([Key.char ':'] ++ keys "floatwin on" ++ [Key.enter])let sFloatVs2 ← runKeys sFloatVs1 ([Key.char ':'] ++ keys "vsplit" ++ [Key.enter])let wsFloatVs2 := sFloatVs2.getCurrentWorkspacelet floatingIdsVs := wsFloatVs2.getFloatingWindowIds.toListassertEqual "vsplit in floating window keeps new pane floating" true (floatingIdsVs.length >= 2)match floatingIdsVs with| a :: b :: _ =>let sideBySide :=match sFloatVs2.getFloatingWindowBounds a, sFloatVs2.getFloatingWindowBounds b with| some (t1, l1, h1, w1), some (t2, l2, h2, w2) =>t1 == t2 && h1 == h2 && ((l1 + w1 == l2) || (l2 + w2 == l1))| _, _ => falseassertEqual "vsplit in floating window keeps pair side-by-side" true sideBySide| _ =>assertEqual "vsplit in floating window keeps pair side-by-side" true falselet sFloatSp0 := { s0 with windowHeight := 24, windowWidth := 80 }let sFloatSp1 ← runKeys sFloatSp0 ([Key.char ':'] ++ keys "floatwin on" ++ [Key.enter])let sFloatSp2 ← runKeys sFloatSp1 ([Key.char ':'] ++ keys "split" ++ [Key.enter])let wsFloatSp2 := sFloatSp2.getCurrentWorkspacelet floatingIdsSp := wsFloatSp2.getFloatingWindowIds.toListassertEqual "split in floating window keeps new pane floating" true (floatingIdsSp.length >= 2)match floatingIdsSp with| a :: b :: _ =>let stacked :=match sFloatSp2.getFloatingWindowBounds a, sFloatSp2.getFloatingWindowBounds b with| some (t1, l1, h1, w1), some (t2, l2, h2, w2) =>l1 == l2 && w1 == w2 && ((t1 + h1 == t2) || (t2 + h2 == t1))| _, _ => falseassertEqual "split in floating window keeps pair stacked" true stacked| _ =>assertEqual "split in floating window keeps pair stacked" true falselet sFloatMix0 := { s0 with windowHeight := 24, windowWidth := 80 }let sFloatMix1 ← runKeys sFloatMix0 ([Key.char ':'] ++ keys "floatwin on" ++ [Key.enter])let sFloatMix2 ← runKeys sFloatMix1 ([Key.char ':'] ++ keys "vsplit" ++ [Key.enter])let sFloatMix3 ← runKeys sFloatMix2 ([Key.char ':'] ++ keys "split" ++ [Key.enter])let wsFloatMix3 := sFloatMix3.getCurrentWorkspacelet floatingIdsMix := wsFloatMix3.getFloatingWindowIds.toListassertEqual "vsplit then split in floating window keeps three panes floating" 3 floatingIdsMix.lengthlet boundsMix := floatingIdsMix.filterMap (fun wid => sFloatMix3.getFloatingWindowBounds wid)assertEqual "vsplit then split in floating window resolves bounds for all panes" 3 boundsMix.lengthlet overlaps (a b : Nat × Nat × Nat × Nat) : Bool :=let (ta, la, ha, wa) := alet (tb, lb, hb, wb) := bla < lb + wb && lb < la + wa && ta < tb + hb && tb < ta + halet rec hasOverlap (xs : List (Nat × Nat × Nat × Nat)) : Bool :=match xs with| [] => false| x :: rest => rest.any (fun y => overlaps x y) || hasOverlap restassertEqual "vsplit then split in floating window panes do not overlap" false (hasOverlap boundsMix)
let sFloatDeep0 := { s0 with windowHeight := 24, windowWidth := 80 }let sFloatDeep1 ← runKeys sFloatDeep0 ([Key.char ':'] ++ keys "floatwin on" ++ [Key.enter])let sFloatDeep2 ← runKeys sFloatDeep1 ([Key.char ':'] ++ keys "vs" ++ [Key.enter])let sFloatDeep3 ← runKeys sFloatDeep2 ([Key.char ':'] ++ keys "hs" ++ [Key.enter])let sFloatDeep4 ← runKeys sFloatDeep3 ([Key.char ':'] ++ keys "vs" ++ [Key.enter])let wsFloatDeep4 := sFloatDeep4.getCurrentWorkspacelet floatingIdsDeep := wsFloatDeep4.getFloatingWindowIds.toListassertEqual "vs -> hs -> vs in floating window creates four panes" 4 floatingIdsDeep.lengthlet boundsDeep := floatingIdsDeep.filterMap (fun wid => sFloatDeep4.getFloatingWindowBounds wid)assertEqual "vs -> hs -> vs in floating window resolves bounds for all panes" 4 boundsDeep.lengthassertEqual "vs -> hs -> vs in floating window panes do not overlap" false (hasOverlap boundsDeep)let sideBySideTouch (a b : Nat × Nat × Nat × Nat) : Bool :=let (t1, l1, h1, w1) := alet (t2, l2, h2, w2) := b((l1 + w1 == l2) || (l2 + w2 == l1)) && (t1 < t2 + h2 && t2 < t1 + h1)let stackedTouch (a b : Nat × Nat × Nat × Nat) : Bool :=let (t1, l1, h1, w1) := alet (t2, l2, h2, w2) := b((t1 + h1 == t2) || (t2 + h2 == t1)) && (l1 < l2 + w2 && l2 < l1 + w1)let rec hasPairWith(pred : (Nat × Nat × Nat × Nat) → (Nat × Nat × Nat × Nat) → Bool)(xs : List (Nat × Nat × Nat × Nat)) : Bool :=match xs with| [] => false| x :: rest => rest.any (fun y => pred x y) || hasPairWith pred restassertEqual "vs -> hs -> vs in floating window keeps horizontal adjacency" true (hasPairWith sideBySideTouch boundsDeep)assertEqual "vs -> hs -> vs in floating window keeps vertical adjacency" true (hasPairWith stackedTouch boundsDeep)let longLine := "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"let longText := String.intercalate "\n" [longLine, longLine, longLine, longLine, longLine, longLine, longLine, longLine, longLine, longLine, longLine, longLine] ++ "\n"let s20 := { s0 with windowHeight := 14, windowWidth := 40 }let s21 ← runKeys s20 ([Key.char 'i'] ++ keys longText ++ [Key.esc] ++ keys "gg0")let s22 ← runKeys s21 ([Key.char ':'] ++ keys "floatwin on" ++ [Key.enter])let s23 ← runKeys s22 [Key.char '1', Key.char '0', Key.char 'j']let (sRow23, _) := s23.getScrollassertEqual "floating window vertical scroll follows cursor" true (sRow23.val > 0)let s24 ← runKeys s23 [Key.char '3', Key.char '5', Key.char 'l']let (_, sCol24) := s24.getScrollassertEqual "floating window horizontal scroll follows cursor" true (sCol24.val > 0)let s25 ← runKeys s0 ([Key.char 'i'] ++ keys "abcd\n" ++ [Key.esc] ++ keys "gg0")let s26 ← runKeys s25 ([Key.char ':'] ++ keys "floatwin on" ++ [Key.enter])let s27 ← runKeys s26 [Key.char 'v', Key.char 'l', Key.char 'd']assertBuffer "visual mode edits active floating window buffer" s27 "cd\n"let s28 ← runKeys s0 ([Key.char 'i'] ++ keys "abcd\nefgh\n" ++ [Key.esc] ++ keys "gg0")let s29 ← runKeys s28 ([Key.char ':'] ++ keys "floatwin on" ++ [Key.enter])let s30 ← runKeys s29 [Key.char 'l', Key.char 'V', Key.char 'l', Key.char 'j', Key.char 'd']assertBuffer "visual block edits active floating window buffer" s30 "ad\neh\n"def testBufferExplorerCommand : IO Unit := doIO.println " Testing Buffer Explorer..."let s0 := ViE.initialStatelet stamp ← IO.monoMsNowlet tmpRoot := s!"/tmp/vie-buffer-explorer-{stamp}"IO.FS.createDirAll tmpRootlet bufAPath := s!"{tmpRoot}/bufA.txt"let bufBPath := s!"{tmpRoot}/bufB.txt"IO.FS.writeFile bufAPath "alpha\n"IO.FS.writeFile bufBPath "beta\n"let sBuf0 := { s0 with windowHeight := 30, windowWidth := 100 }let sBuf1 ← runKeys sBuf0 ([Key.char ':'] ++ keys s!"e {bufAPath}" ++ [Key.enter])let sBuf2 ← runKeys sBuf1 ([Key.char ':'] ++ keys s!"e {bufBPath}" ++ [Key.enter])let targetBufIdOpt := sBuf2.getCurrentWorkspace.buffers.find? (fun b => b.filename == some bufAPath) |>.map (fun b => b.id)assertEqual "Buffer explorer target buffer exists" true targetBufIdOpt.isSomelet targetBufId := targetBufIdOpt.getD 0let sBuf3 ← runKeys sBuf2 ([Key.char ':'] ++ keys "buf list" ++ [Key.enter])assertEqual ":buf list opens buffer explorer" true ((sBuf3.getActiveBuffer.filename.getD "").startsWith "explorer://buffers")let explorerBufId := sBuf3.getActiveBuffer.idlet explorerBufOpt := sBuf3.explorers.find? (fun (id, _) => id == explorerBufId)match explorerBufOpt with| none =>assertEqual "Buffer explorer registered" true false| some (_, explorerBuf) =>let targetPath := s!"buffer://{targetBufId}"let targetIdxOpt := findEntryIndexByPath explorerBuf.entries targetPathassertEqual "Buffer explorer contains target buffer entry" true targetIdxOpt.isSomelet targetIdx := targetIdxOpt.getD 0let sBuf4 := sBuf3.updateActiveView fun v => { v with cursor := { row := ⟨2 + targetIdx⟩, col := 0 } }let sBuf5 ← runKeys sBuf4 [Key.enter]assertEqual "Buffer explorer Enter switches active buffer" (some bufAPath) sBuf5.getActiveBuffer.filenameassertEqual "Buffer explorer closes after selection" false (sBuf5.explorers.any (fun (id, _) => id == explorerBufId))let sBufCompat ← runKeys sBuf2 ([Key.char ':'] ++ keys "buffers" ++ [Key.enter])assertEqual ":buffers alias opens buffer explorer" true ((sBufCompat.getActiveBuffer.filename.getD "").startsWith "explorer://buffers")let sBufAlias ← runKeys sBuf2 ([Key.char ':'] ++ keys "ls" ++ [Key.enter])assertEqual ":ls alias opens buffer explorer" true ((sBufAlias.getActiveBuffer.filename.getD "").startsWith "explorer://buffers")def testExplorerCommandAliases : IO Unit := doIO.println " Testing Explorer Command Names..."let s0 := ViE.initialStatelet stamp ← IO.monoMsNowlet tmpRoot := s!"/tmp/vie-ex-alias-{stamp}"IO.FS.createDirAll tmpRootIO.FS.writeFile s!"{tmpRoot}/x.txt" "x\n"let s1 ← runKeys s0 ([Key.char ':'] ++ keys s!"ex list {tmpRoot}" ++ [Key.enter])assertEqual ":ex list opens file explorer" true ((s1.getActiveBuffer.filename.getD "").startsWith "explorer://")let s2 ← runKeys s0 ([Key.char ':'] ++ keys s!"ee {tmpRoot}" ++ [Key.enter])assertEqual ":ee alias opens file explorer" true ((s2.getActiveBuffer.filename.getD "").startsWith "explorer://")let s3 ← runKeys s0 ([Key.char ':'] ++ keys "wgex" ++ [Key.enter])assertEqual ":wgex alias opens workgroup explorer" (some "explorer://workgroups") s3.getActiveBuffer.filename
let ws1 := s1.getCurrentWorkspaceassert "File explorer opens in floating window" (ws1.isFloatingWindow ws1.activeWindowId)let baseBufId := s0.getActiveBuffer.idlet baseWinId :=(ViE.Window.getWindowIds ws1.layout).find? (fun wid =>if wid == ws1.activeWindowId thenfalseelsematch ws1.layout.findView wid with| some v => v.bufferId == baseBufId| none => false)let baseWindowStillExists :=(ViE.Window.getWindowIds ws1.layout).any (fun wid =>if wid == ws1.activeWindowId thenfalseelsematch ws1.layout.findView wid with| some v => v.bufferId == baseBufId| none => false)assert "Opening explorer keeps original buffer window intact" baseWindowStillExists
let previewFloating :=match exOpen.previewWindowId with| some wid => s1.getCurrentWorkspace.isFloatingWindow wid| none => falseassert "Preview window opens in floating window" previewFloatinglet pairSideBySide :=match exOpen.previewWindowId with| some wid =>let ws1 := s1.getCurrentWorkspacematch s1.getFloatingWindowBounds ws1.activeWindowId, s1.getFloatingWindowBounds wid with| some (et, el, eh, ew), some (pt, pl, ph, pw) =>et == pt && eh == ph && ((el + ew <= pl) || (pl + pw <= el))| _, _ => false| none => falseassert "Explorer/preview floating pair is side-by-side" pairSideBySide
let ws5b := s5b.getCurrentWorkspaceassert "Opening file from floating explorer leaves non-floating window" (!ws5b.isFloatingWindow ws5b.activeWindowId)match baseWinId with| some wid =>assert "Opening file from explorer uses original buffer window" (ws5b.activeWindowId == wid)| none =>throw (IO.userError "Base window id not found after explorer open")
- `:ee [path]`: Open file explorer- `:wgex`: Open workgroup explorer
- `:ex list [path]`: Open file explorer- `:ee [path]`: Alias for `:ex list [path]`- `:buf list`: Open buffer explorer for switching among opened buffers- `:buffers`, `:ls`: Aliases for `:buf list`- `:wgex`: Alias for `:wg list`
- `:redraw`, `:redraw!`: Force redraw and clear render caches- `:float [--title <title>|--title=<title>] [--width <cols>|--width=<cols>] <text>`: Show centered floating overlay (`\\n` to split lines)- `:float` default size: width/height follows message content size (clamped to current screen buffer)- `:float clear`, `:nofloat`: Close floating overlay- `:floatwin [toggle|on|off|clear]`: Toggle active window as floating window (buffer is the same workspace buffer)- floating overlay keys:- `NORMAL`: `i/a/o/O/h/j/k/l/0/$/x` edit/move, `Esc`/`Enter`/`q` close- `INSERT`: text input, `Backspace`, `Enter` newline, `Esc` back to overlay `NORMAL`