TIAIRD7L5GWH22TOLDAGCIDCJDZ53YWK3KHQOH3EIU7RZEOEP32AC TKAAHADV62OQORAO7MYRRRHTGPMG3GKXU4Z4MFNLAO6DLTSDU3CAC XCVPXP4UBFU25NF6VQC3MEYHHH45RL2UXM5TA6O23NA64FPCCCJQC 6W6OJAUT3KLOXXKYR4GA3MG4ACLO2T4KX3SGD2KZDX6MYEZYTPHAC 5SFTBD4FW6GQPKRUJBAII5R2EZPDYG6CH57BIJD4IVZBE6JZB2ZQC WRBKZMYVNHRWT7TTUGTDJ3TMWZB32QYW5PCLKTTVAJ2YF6OI3LTAC KJLCU4X5Z3CZM4AORMWIKOPBWPVTDMLK4LYH7UXBJFUGL7AUQCSQC CNJGJCJZ6LRHHIRSRATSE3D3Z5OQGSSTDMXCPVXSKRQLYJFME6IAC U45XPF3YKPXXRJ4MN24T2RV7GOL4FZKQSWX5P5I7WT4HC5XF4FHAC GUD2J453E3UTRJZIEEUUJ3ZIGL72ETUXTO57XQIZ2Y3ZU3C2R55QC SEIYD7YXZ5XI4W7UVF5O2UFLA6OCZZCOLV2XB46RKJ5EN6UQC4UAC /-- Build line-start offsets by walking the piece tree in document order. -/private def buildLineIndex (pt : PieceTable) : Array Nat := Id.run dolet capacity := PieceTree.lineBreaks pt.tree + 1let mut starts : Array Nat := (Array.mkEmpty capacity).push 0let mut docOff := 0let mut stack : List PieceTree := [pt.tree]while !stack.isEmpty domatch stack with| [] => pure ()| node :: rest =>stack := restmatch node with| PieceTree.empty => pure ()| PieceTree.leaf pieces _ _ =>for p in pieces dolet buf := PieceTableHelper.getBuffer pt p.sourcelet stop := p.start + p.lengthlet mut i := p.startwhile i < stop doif buf[i]! == 10 thenstarts := starts.push (docOff + 1)i := i + 1docOff := docOff + 1| PieceTree.internal children _ _ =>let mut i := children.sizewhile i > 0 dolet j := i - 1stack := children[j]! :: stacki := jreturn startsprivate def withLineIndexCache (pt : PieceTable) : PieceTable :={ pt with lineIndexCache := some (buildLineIndex pt) }
{ original := bytes, addBuffers := #[], tree := PieceTree.empty, bloomBuildLeafBits := buildLeafBits,undoStack := [], redoStack := [], undoLimit := 100, lastInsert := none }
withLineIndexCache {original := bytes, addBuffers := #[], tree := PieceTree.empty, bloomBuildLeafBits := buildLeafBits,undoStack := [], redoStack := [], undoLimit := 100, lastInsert := none}
{ original := bytes, addBuffers := #[], tree := tree, bloomBuildLeafBits := buildLeafBits,undoStack := [], redoStack := [], undoLimit := 100, lastInsert := none }
withLineIndexCache {original := bytes, addBuffers := #[], tree := tree, bloomBuildLeafBits := buildLeafBits,undoStack := [], redoStack := [], undoLimit := 100, lastInsert := none}
(pt.undoStack, pt.undoStackCount, some { docOffset := offset + text.toUTF8.size, bufferIdx := pt.addBuffers.size, bufferOffset := text.toUTF8.size })
(pt.undoStack, pt.undoStackCount, some { docOffset := offset + text.toUTF8.size, bufferIdx := appendMeta.bufferIdx, bufferOffset := appendMeta.bufferEnd })
(finalStack, finalCount, some { docOffset := offset + text.toUTF8.size, bufferIdx := pt.addBuffers.size, bufferOffset := text.toUTF8.size })
(finalStack, finalCount, some { docOffset := offset + text.toUTF8.size, bufferIdx := appendMeta.bufferIdx, bufferOffset := appendMeta.bufferEnd })
(finalStack, finalCount, some { docOffset := offset + text.toUTF8.size, bufferIdx := pt.addBuffers.size, bufferOffset := text.toUTF8.size })
(finalStack, finalCount, some { docOffset := offset + text.toUTF8.size, bufferIdx := appendMeta.bufferIdx, bufferOffset := appendMeta.bufferEnd })
PieceTree.getLineRange pt.tree lineIdx pt
let idx := pt.lineIndexmatch idx[lineIdx]? with| none => none| some startOff =>let totalLen := pt.tree.lengthmatch idx[lineIdx + 1]? with| some nextStart =>let len := if nextStart > startOff then (nextStart - startOff - 1) else 0some (startOff, len)| none =>some (startOff, totalLen - startOff)/-- Get line from buffer -/def PieceTable.getLine (pt : PieceTable) (lineIdx : Nat) : Option String :=match PieceTable.getLineRange pt lineIdx with| some (off, len) => some (PieceTree.getSubstring pt.tree off len pt)| none => none
/-- Append text to add buffer, splitting into chunks if necessary -/def appendText (pt : ViE.PieceTable) (text : String) : (ViE.PieceTable × Array ViE.Piece) :=
/-- Append text to add buffer, splitting into chunks if necessary. Reuses tail add buffer when possible. -/def appendText (pt : ViE.PieceTable) (text : String) : (ViE.PieceTable × Array ViE.Piece × AppendMeta) :=
let bufferIdx := pt.addBuffers.sizelet newAddBuffers := pt.addBuffers.push bytes
let (bufferIdx, startOff, newAddBuffers) :=match pt.addBuffers.back? with| some tail =>if tail.size + totalSize <= ViE.AddBufferReuseLimit thenlet idx := pt.addBuffers.size - 1let startOff := tail.sizelet merged := tail ++ byteslet bufs := pt.addBuffers.set! idx merged(idx, startOff, bufs)elselet idx := pt.addBuffers.size(idx, 0, pt.addBuffers.push bytes)| none =>(0, 0, #[bytes])
private def isUsableExplorerTargetWindow (ws : WorkspaceState) (explorerWinId : Nat) (previewWinId : Option Nat)(explorerBufIds : List Nat) (previewBufId : Option Nat) (wid : Nat) : Bool :=if wid == explorerWinId thenfalseelse if previewWinId == some wid thenfalseelsematch ws.layout.findView wid with| some v =>let isExplorerBuf := explorerBufIds.contains v.bufferIdlet isPreviewBuf := match previewBufId with | some pid => v.bufferId == pid | none => false!isExplorerBuf && !isPreviewBuf| none => false
let reuseActive := state.explorers.any (fun (id, _) => id == state.getActiveBuffer.id)let targetWindowId :=if reuseActive thenmatch state.explorers.find? (fun (id, _) => id == state.getActiveBuffer.id) with| some (_, ex) => ex.targetWindowId| none => some ws.activeWindowIdelsesome ws.activeWindowId
def openExplorerWithPreview (state : EditorState) (pathArg : String) (previewWindowId : Option Nat) (previewBufferId : Option Nat) : IO EditorState := do
def openExplorerWithPreview (state : EditorState) (pathArg : String) (previewWindowId : Option Nat)(previewBufferId : Option Nat) (targetWindowId : Option Nat) : IO EditorState := do
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
let preferred := explorer.targetWindowIdmatch preferred with| some wid =>if isUsableExplorerTargetWindow ws0 explorerWinId explorer.previewWindowId explorerBufIds explorer.previewBufferId wid thensome widelselet ids := ViE.Window.getWindowIds ws0.layoutids.find? (fun w => isUsableExplorerTargetWindow ws0 explorerWinId explorer.previewWindowId explorerBufIds explorer.previewBufferId w)| none =>let ids := ViE.Window.getWindowIds ws0.layoutids.find? (fun w => isUsableExplorerTargetWindow ws0 explorerWinId explorer.previewWindowId explorerBufIds explorer.previewBufferId w)
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 => false
let preferred := explorer.targetWindowIdlet isTarget (wid : Nat) :=isUsableExplorerTargetWindow ws0 explorerWinId explorer.previewWindowId explorerBufIds none widmatch preferred with| some wid =>if isTarget wid thensome widelselet ids := ViE.Window.getWindowIds ws0.layoutids.find? isTarget| none =>let ids := ViE.Window.getWindowIds ws0.layoutids.find? isTarget
let s9 := ViE.initialStatelet s10 ← runKeys s9 ([Key.char 'i'] ++ keys "a\nfoo1\nb\nfoo2\nc" ++ [Key.esc] ++ [Key.char 'g', Key.char 'g', Key.char 'j', Key.char 'j', Key.char 'j', Key.char '0'])assertCursor ":g/pat/ d precondition cursor on deletable line" s10 3 0let s11 ← runKeys s10 ([Key.char ':'] ++ keys "g/foo/ d" ++ [Key.enter])assertBuffer ":g/pat/ d keeps expected text when cursor line is deleted" s11 "a\nb\nc"assertCursor ":g/pat/ d clamps cursor when current line is deleted" s11 2 0def testCursorDriftCustomTabStop : IO Unit := doIO.println " Testing Cursor Drift (custom tabStop)..."let cfg8 := { ViE.defaultConfig with tabStop := 8 }let s0 := { ViE.initialState with config := cfg8 }let s1 ← runKeys s0 ([Key.char 'i', Key.char '\t'] ++ keys "foo\n" ++ [Key.esc])let s2 ← runKeys s1 [Key.char 'g', Key.char 'g', Key.char '0', Key.char 'l']assertCursor "custom tabStop: l moves to first char after tab" s2 0 8let s3 ← runKeys s2 ([Key.char ':'] ++ keys "s/foo/bar/" ++ [Key.enter])assertBuffer "custom tabStop: :s updates current line" s3 "\tbar\n"let s4 ← runKeys s3 [Key.char 'u']assertBuffer "custom tabStop: undo restores text after :s" s4 "\tfoo\n"assertCursor "custom tabStop: undo restores cursor col after :s" s4 0 8let s5 ← runKeys s4 [Key.ctrl 'r']assertBuffer "custom tabStop: redo reapplies :s" s5 "\tbar\n"assertCursor "custom tabStop: redo restores cursor col after :s" s5 0 8
def testExplorerOpenUsesFocusedWindow : IO Unit := doIO.println " Testing Explorer Open Target Window..."let s0 := { ViE.initialState with windowHeight := 30, windowWidth := 100 }let stamp ← IO.monoMsNowlet tmpRoot := s!"/tmp/vie-ex-target-{stamp}"IO.FS.createDirAll tmpRootlet leftPath := s!"{tmpRoot}/left.txt"let rightPath := s!"{tmpRoot}/right.txt"let targetPath := s!"{tmpRoot}/target.txt"IO.FS.writeFile leftPath "left\n"IO.FS.writeFile rightPath "right\n"IO.FS.writeFile targetPath "target\n"let s1 ← runKeys s0 ([Key.char ':'] ++ keys s!"e {leftPath}" ++ [Key.enter])let s2 ← runKeys s1 ([Key.char ':'] ++ keys "vsplit" ++ [Key.enter])let rightWinId := s2.getCurrentWorkspace.activeWindowIdlet s3 ← runKeys s2 ([Key.char ':'] ++ keys s!"e {rightPath}" ++ [Key.enter])let s4 ← runKeys s3 ([Key.char ':'] ++ keys "wincmd w" ++ [Key.enter])let targetWinId := s4.getCurrentWorkspace.activeWindowIdassertEqual "wincmd w changes active target window" true (targetWinId != rightWinId)let s5 ← runKeys s4 ([Key.char ':'] ++ keys s!"ex list {tmpRoot}" ++ [Key.enter])let explorerBufId := s5.getActiveBuffer.idlet explorerOpt := s5.explorers.find? (fun (id, _) => id == explorerBufId)match explorerOpt with| none =>assertEqual "File explorer registered" true false| some (_, explorer) =>let targetIdxOpt := findEntryIndex explorer.entries "target.txt"assertEqual "Explorer contains target file" true targetIdxOpt.isSomelet targetIdx := targetIdxOpt.getD 0let s6 := s5.updateActiveView fun v => { v with cursor := { row := ⟨2 + targetIdx⟩, col := 0 } }let s7 ← runKeys s6 [Key.enter]assertEqual "Explorer Enter uses focused window from before open" targetWinId s7.getCurrentWorkspace.activeWindowIdassertEqual "Selected file opens in focused window" (some targetPath) s7.getActiveBuffer.filenamelet ws7 := s7.getCurrentWorkspacematch ws7.layout.findView rightWinId with| none =>assertEqual "Other split window still exists" true false| some rightView =>let rightBufName := ws7.buffers.find? (fun b => b.id == rightView.bufferId) |>.bind (fun b => b.filename)assertEqual "Non-focused split keeps prior buffer" (some rightPath) rightBufName
def testBuildRestoredWorkspaceCustomTabStop : IO Unit := doIO.println "Starting Restored Workspace Custom TabStop Test..."let stamp <- IO.monoMsNowlet root := s!"/tmp/vie-checkpoint-tab-{stamp}"IO.FS.createDirAll rootlet f1 := s!"{root}/tab.txt"IO.FS.writeFile f1 "\talpha\n"let settings := { ViE.defaultConfig with tabStop := 8 }let ws <- ViE.buildRestoredWorkspace settings (some root) [f1] 0 [(0, 8)]let active := ws.layout.findView ws.activeWindowId |>.getD ViE.initialViewassertEqual "Restored custom tabStop cursor row" 0 active.cursor.row.valassertEqual "Restored custom tabStop cursor col" 8 active.cursor.col.val