4V7W7A6YX2J57RZUANMZKMKFHIDFNTXRMDR6M57DC6M3B4AHOK6AC LSCUXT3235I5ZZPZM2MMGLECHU2LAX2TKLRHDULONDAS5JEWTB7QC GSOBCC5UYHCUY53WEYJ4WG5RUQACOQSBCLEGA5W6GRPHARU4ZWEAC 6W6OJAUT3KLOXXKYR4GA3MG4ACLO2T4KX3SGD2KZDX6MYEZYTPHAC GUD2J453E3UTRJZIEEUUJ3ZIGL72ETUXTO57XQIZ2Y3ZU3C2R55QC NBG3GUEJJMWS2FRJDU26KRQLA6O2YK77PDAPBMD3SKWLDTK23BVAC ZL7ZSOEOGP2E24UEFC5VVPN5GTL3EUL34FA7F47WWWFPHJH2RS2AC TKAAHADV62OQORAO7MYRRRHTGPMG3GKXU4Z4MFNLAO6DLTSDU3CAC 32SDOYNGPOAAMEQMZLZUE3XE6D5CFSZU6M3JDRERUVPAB4TLZOOAC WRBKZMYVNHRWT7TTUGTDJ3TMWZB32QYW5PCLKTTVAJ2YF6OI3LTAC XCVPXP4UBFU25NF6VQC3MEYHHH45RL2UXM5TA6O23NA64FPCCCJQC WBKXA5RVL5DSEJZ4OBDBCTNRT3Q4VFEVQNA344CET3BHY7CY7H5AC A6E5SE5JPWUJJYIUS4CF7NMB5GM2SIIUXUZASSOVD467YWI5SCQQC CNJGJCJZ6LRHHIRSRATSE3D3Z5OQGSSTDMXCPVXSKRQLYJFME6IAC DTAIE7PK6TZLNSBQGENS6RT7FFOTMAPSE6ZTPDADINGJF5NTHLWQC 25KGF2W2ORVH3Z7RIBDBRKILMM624MNHIMN5QXNMZOWQ5DOJUCZAC KJLCU4X5Z3CZM4AORMWIKOPBWPVTDMLK4LYH7UXBJFUGL7AUQCSQC 2CFNOXLKFVAGON3Z2KISH4LNIALNY4VTBILJLQIE45YUKQI6ZHPQC NVJSIX3GL5WPI3FQ6U7TQQE5T4EDEHSI6SDQM7TPJTGBKCPILJFAC 5SFTBD4FW6GQPKRUJBAII5R2EZPDYG6CH57BIJD4IVZBE6JZB2ZQC TIAIRD7L5GWH22TOLDAGCIDCJDZ53YWK3KHQOH3EIU7RZEOEP32AC /-- Count newlines in a byte array slice. -/def countNewlines (data : ByteArray) (start : Nat) (len : Nat) : Nat :=let endPos := start + lenlet rec loop (i : Nat) (count : Nat) : Nat :=if i >= endPos then countelseif data[i]! == 10 then loop (i + 1) (count + 1) -- 10 is \nelse loop (i + 1) countloop start 0/-- Count UTF-8 characters in a byte array range. -/def countChars (bytes : ByteArray) (start len : Nat) : Nat :=let stop := start + lenlet rec loop (i : Nat) (cnt : Nat) : Nat :=if i >= stop then cntelselet b := bytes[i]!if (b &&& 0xC0) != 0x80 thenloop (i + 1) (cnt + 1)elseloop (i + 1) cntloop start 0
/-- Build an index mapping display columns to UTF-8 byte offsets at character boundaries.The result includes the starting boundary (0,0) and the final boundary (totalWidth,totalBytes). -/def buildDisplayByteIndex (s : String) : Array (Nat × Nat) :=let rec loop (cs : List Char) (disp bytes : Nat) (acc : Array (Nat × Nat)) : Array (Nat × Nat) :=match cs with| [] => acc| c :: rest =>let w := charWidth clet b := c.toString.toUTF8.sizelet disp' := disp + wlet bytes' := bytes + bloop rest disp' bytes' (acc.push (disp', bytes'))loop s.toList 0 0 #[(0, 0)]
/-- Compute the next display column (advancing by one character). -/def nextDisplayCol (s : String) (col : Nat) : Nat :=let idx := displayColToCharIndex s collet startWidth := displayWidthAtCharIndex s idxif col < startWidth thenstartWidthelselet chars := s.toListif idx < chars.length thenlet w := charWidth (chars[idx]!)startWidth + welsecol/-- Compute the previous display column (moving back by one character). -/def prevDisplayCol (s : String) (col : Nat) : Nat :=if col == 0 then0elselet idx := displayColToCharIndex s collet startWidth := displayWidthAtCharIndex s idxif col > startWidth thenstartWidthelse if idx == 0 then0elsedisplayWidthAtCharIndex s (idx - 1)/-- Drop `width` display columns from the start of a substring. -/def dropByDisplayWidth (s : Substring.Raw) (width : Nat) : Substring.Raw :=let rec loop (i : String.Pos.Raw) (acc : Nat) : Substring.Raw :=if h : i.byteIdx < s.bsize thenlet c := s.get ilet w := charWidth cif acc + w > width thens.extract i ⟨s.bsize⟩elselet i' := s.next ihave := Nat.sub_lt_sub_left h (Substring.Raw.lt_next s i h)loop i' (acc + w)elses.extract i ⟨s.bsize⟩termination_by s.bsize - i.1loop 0 0
/-- Calculate the total visual width of a substring with tab-stop awareness. -/def substringWidthWithTabStop (s : Substring.Raw) (tabStop : Nat) : Nat :=let rec loop (i : String.Pos.Raw) (acc : Nat) : Nat :=if h : i.byteIdx < s.bsize thenlet c := s.get ilet i' := s.next ihave := Nat.sub_lt_sub_left h (Substring.Raw.lt_next s i h)let w := charWidthAt tabStop acc cloop i' (acc + w)elseacctermination_by s.bsize - i.1loop 0 0
def consumeWordBackward (tabStop : Nat) (buffer : FileBuffer) (row : Row) (col : Col) (wantKw : Bool) : Row × Col :=consumeWordBackwardCore tabStop buffer row col wantKw (wordMoveFuel buffer)
) #[]private def asciiLowerChar (c : Char) : Char :=let n := c.toNatif 65 <= n && n <= 90 thenChar.ofNat (n + 32)elsecprivate def asciiLower (s : String) : String :=String.map asciiLowerChar sprivate def startsWithAt (s p : Array Char) (idx : Nat) : Bool :=if idx + p.size > s.size thenfalseelseId.run dolet mut ok := truefor i in [0:p.size] doif s[idx + i]! != p[i]! thenok := falsereturn okprivate def findSubseqMatch (q target : Array Char) : Option (Nat × Nat × Nat) :=if q.isEmpty thensome (0, 0, 0)elseId.run dolet mut qi := 0let mut start := 0let mut «end» := 0let mut foundStart := falsefor i in [0:target.size] doif qi < q.size && target[i]! == q[qi]! thenif !foundStart thenstart := ifoundStart := trueqi := qi + 1if qi == q.size then«end» := i + 1if qi == q.size thenlet span := «end» - startsome (start, «end», span)elsenoneprivate def scoreCompletion (query : String) (item : CompletionItem) : Int :=if query.isEmpty then0elselet q := (asciiLower query).toList.toArraylet tStr := asciiLower item.labellet t := tStr.toList.toArraymatch findSubseqMatch q t with| none => -1000000| some (start, _, span) =>let isPrefix := startsWithAt t q 0let contiguous := span == q.sizelet prefixBonus : Int := if isPrefix then 1200 else 0let contiguousBonus : Int := if contiguous then 600 else 0let boundaryBonus : Int :=if start == 0 then200elselet prev := t[start - 1]!if prev == '.' || prev == '_' || prev == ' ' then 120 else 0let gapPenalty : Int := Int.ofNat (span - q.size) * 8let lenPenalty : Int := Int.ofNat t.sizeprefixBonus + contiguousBonus + boundaryBonus - gapPenalty - lenPenaltystructure RankedCompletion whereitem : CompletionItemscore : Intderiving Inhabitedprivate def betterRanked (a b : RankedCompletion) : Bool :=if a.score == b.score thena.item.label < b.item.labelelsea.score > b.scoreprivate def heapSwap (arr : Array RankedCompletion) (i j : Nat) : Array RankedCompletion :=let ai := arr[i]!let aj := arr[j]!(arr.set! i aj).set! j aiprivate def siftUp (heap : Array RankedCompletion) (idx : Nat) : Array RankedCompletion := Id.run dolet mut h := heaplet mut i := idxwhile i > 0 dolet p := (i - 1) / 2if betterRanked h[p]! h[i]! thenh := heapSwap h i pi := pelsei := 0return hprivate def siftDown (heap : Array RankedCompletion) (idx : Nat) : Array RankedCompletion := Id.run dolet mut h := heaplet mut i := idxlet n := h.sizelet mut keep := truewhile keep dolet l := 2 * i + 1if l >= n thenkeep := falseelselet r := l + 1let child :=if r < n && betterRanked h[l]! h[r]! then r else lif betterRanked h[i]! h[child]! thenh := heapSwap h i childi := childelsekeep := falsereturn hprivate def heapPushTopK (heap : Array RankedCompletion) (k : Nat) (x : RankedCompletion) : Array RankedCompletion :=if k == 0 then#[]else if heap.size < k thensiftUp (heap.push x) heap.sizeelse if betterRanked x heap[0]! thensiftDown (heap.set! 0 x) 0elseheapprivate def sortRankedDesc (arr : Array RankedCompletion) : Array RankedCompletion := Id.run dolet mut out := arrlet n := out.sizefor i in [1:n] dolet cur := out[i]!let mut j := iwhile j > 0 && betterRanked cur out[j - 1]! doout := out.set! j out[j - 1]!j := j - 1out := out.set! j curreturn outprivate def rankAndSelectCompletions (query : String) (items : Array CompletionItem) (k : Nat := 40) : Array CompletionItem :=let heap := items.foldl (fun h item =>let sc := scoreCompletion query itemif sc <= -1000000 thenhelseheapPushTopK h k { item := item, score := sc }
clearUiCompletion
let lineStr := ViE.getLineFromBuffer buf cursor.row |>.getD ""let startCol := completionPrefixStartDisplayCol lineStr state.config.tabStop cursor.col.vallet startIdx := ViE.Unicode.displayColToCharIndexWithTabStop lineStr state.config.tabStop startCollet endIdx := ViE.Unicode.displayColToCharIndexWithTabStop lineStr state.config.tabStop cursor.col.vallet chars := lineStr.toListlet query := String.ofList (chars.drop startIdx |>.take (endIdx - startIdx))
pure { state with completionPopup := none }
pure statedef acceptSelectedCompletion (state : EditorState) : IO EditorState := doclearUiCompletionlet applied := applySelectedCompletion stateif applied.mode == .insert thenrequestCompletionForActiveIfRunning appliedelsepure applied
let matchesCursor := cursor.row.val == popup.anchorRow && cursor.col.val == popup.anchorColif state.mode == .insert && matchesActive && matchesCursor then
let sameLine := cursor.row.val == popup.anchorRowif state.mode == .insert && matchesActive && sameLine then
syncCompletionPopupFromUi infoSynced
let completionRequested ←if infoSynced.mode == .insert && isLeanBuffer infoSynced.getActiveBuffer && textChanged thenrequestCompletionForActiveIfRunning infoSyncedelsepure infoSyncedsyncCompletionPopupFromUi completionRequested
match s.completionPopup with| some _ =>match k with| Key.ctrl 'n' | Key.down => pure <| ViE.Lsp.Lean.selectNextCompletion s| Key.ctrl 'p' | Key.up => pure <| ViE.Lsp.Lean.selectPrevCompletion s| Key.enter | Key.char '\t' => ViE.Lsp.Lean.acceptSelectedCompletion s| Key.esc => pure <| ViE.Lsp.Lean.clearCompletionPopup ((s.commitEdit.moveCursorLeft).setMode Mode.normal)| Key.backspace => pure <| ViE.Lsp.Lean.clearCompletionPopup (s.deleteBeforeCursor)| Key.char c => pure <| ViE.Lsp.Lean.clearCompletionPopup (s.insertChar c)| _ => pure <| ViE.Lsp.Lean.clearCompletionPopup s| none =>match k with| Key.ctrl 'n' => ViE.Lsp.Lean.requestCompletionForActiveIfRunning s| 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,
dolet handled ← ViE.Language.Key.handleInsertKey? s kmatch handled with| some s' => pure s'| none =>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 with completionPopup := none }| _ => pure s,
left := midelseright := mid - 1return left/-- Binary search to find child index containing the given line number -/def findChildForLine (imeta : ViE.InternalMeta) (lineIdx : UInt64) : Nat :=Id.run dolet arr := imeta.cumulativeLinesif arr.isEmpty then return 0if arr.size == 1 then return 0let mut left := 0let mut right := arr.size - 2 -- Last valid child indexwhile left < right dolet mid := (left + right + 1) / 2if arr[mid]! <= lineIdx then
/-- Add a trigram to the Bloom filter. -/def addTrigramToBloom (bloom : ByteArray) (b0 b1 b2 : UInt8) : ByteArray :=let h1 := hash1 b0 b1 b2let h2 := hash2 b0 b1 b2bloomSetBit (bloomSetBit bloom h1) h2
def leafBloomBitsWithCache (pieces : Array ViE.Piece) (pt : ViE.PieceTable) (leafOffset : Nat)(cache : Lean.RBMap Nat ByteArray compare) (order : Array Nat) (maxSize : Nat): (ByteArray × Lean.RBMap Nat ByteArray compare × Array Nat) :=match cache.find? leafOffset with| some bits => (bits, cache, order)| none =>let bits := (buildBloomForPieces pieces pt).bitslet (cache', order') := cacheInsert cache order maxSize leafOffset bits(bits, cache', order')
/-- Flatten a tree into pieces in document order (iterative to avoid deep recursion). -/def toPieces (t : ViE.PieceTree) : Array ViE.Piece := Id.run dolet mut out : Array ViE.Piece := #[]let mut stack : List ViE.PieceTree := [t]while !stack.isEmpty domatch stack with| [] => pure ()| node :: rest =>stack := restmatch node with| ViE.PieceTree.empty => pure ()| ViE.PieceTree.leaf pieces _ _ =>out := out ++ pieces| ViE.PieceTree.internal children _ _ _ =>let mut i := children.sizewhile i > 0 dolet j := i - 1stack := children[j]! :: stacki := jreturn out
def replaceFirstInLine (line : String) (old : String) (new : String) : String :=if old.isEmpty thenlineelselet parts := line.splitOn oldmatch parts with| [] => line| p :: rest =>if rest.isEmpty thenlineelsep ++ new ++ (String.intercalate old rest)def replaceAllInLine (line : String) (old : String) (new : String) : String :=if old.isEmpty thenlineelseString.intercalate new (line.splitOn old)
def loadPreviewBufferByteArray (filename : String) (maxBytes : Nat) : IO FileBuffer := dotrylet path := System.FilePath.mk filenameif ← path.pathExists thenif ← path.isDir thenreturn emptyBuffer (some filename) true falseelselet data ← IO.FS.withFile filename IO.FS.Mode.read fun handle =>handle.read (USize.ofNat maxBytes)return bufferFromData filename data true falseelsereturn emptyBuffer (some filename) true falsecatch _ =>return emptyBuffer (some filename) true false
/-- Convert byte offset to Row/Col (display column) using default tab stop. -/def getPointFromOffsetInBuffer (buffer : FileBuffer) (offset : Nat) : Point :=getPointFromOffsetInBufferWithTabStop buffer offset defaultConfig.tabStop
/-- Update FileBuffer from TextBuffer (compatibility function) -/def fileBufferUpdateFromTextBuffer (buf : FileBuffer) (newContent : TextBuffer) : FileBuffer :=let newBuf := fileBufferFromTextBuffer buf.id buf.filename newContent{ newBuf with dirty := true }
/-- Modify a line in FileBuffer using PieceTable operations -/def modifyLineInBuffer (buffer : FileBuffer) (row : Row) (f : String → String) : FileBuffer :=match buffer.table.getLineRange row.val with| some (startOffset, length) =>match getLineFromBuffer buffer row with| some oldLine =>let newLine := f oldLine-- Edit: Delete old line content, insert new content.-- We preserve the newline character if it exists (getLineRange excludes it).let table' := buffer.table.delete startOffset length startOffsetlet table'' := table'.insert startOffset newLine startOffset{ buffer withtable := table''dirty := true}| none => buffer -- Should check range first, but safe fallback| none => buffer -- Line not found
/-- Helper to update a specific line in the buffer. -/def modifyLine (buffer : TextBuffer) (row : Row) (f : String → String) : TextBuffer :=if h : row.val < buffer.size thenlet oldLine := buffer[row.val]let newLine := stringToLine (f (lineToString oldLine))buffer.set! row.val newLineelsebuffer/-- Set a line directly (String version for compatibility) -/def setLine (buffer : TextBuffer) (row : Row) (content : String) : TextBuffer :=buffer.set! row.val (stringToLine content)/-- Take first n elements from Array -/def arrayTake {α : Type _} (arr : Array α) (n : Nat) : Array α :=arr.extract 0 n
def testLeanCompletionPopupBehavior : IO Unit := doIO.println " Testing Lean Completion Popup..."let popup : CompletionPopup := {items := #[{ label := "foo", insertText := "foo" },{ label := "foobar", insertText := "foobar" }]selected := 0anchorRow := 0anchorCol := 2}let s0 : EditorState :=({ ViE.initialState with mode := .insert, completionPopup := some popup }).updateActiveBuffer fun b =>{ b with filename := some "Main.lean", table := ViE.PieceTable.fromString "ab" }let s0 := s0.setCursor { row := ⟨0⟩, col := ⟨2⟩ }
let s1 ← runKeys s0 [Key.char 'c']assertBuffer "completion popup keeps showing on char insert" s1 "abc"assertEqual "completion popup remains visible after char insert" true s1.completionPopup.isSomelet s2 ← runKeys s1 [Key.enter]assertBuffer "Enter inserts newline instead of accepting completion" s2 "abc\n"assertCursor "Enter moves cursor to next line head with completion popup" s2 1 0assertEqual "Enter closes completion popup" true s2.completionPopup.isNone