text editor inspired vim and yi
import ViE.State.Config
import ViE.State.Layout
import ViE.State.Movement
import ViE.Types
import ViE.Buffer.Content
import ViE.Unicode

namespace ViE

def EditorState.startVisualMode (s : EditorState) : EditorState :=
  let s' := s.clampCursor
  { s' with mode := .visual, selectionStart := some s'.getCursor, message := "-- VISUAL --" }

def EditorState.startVisualBlockMode (s : EditorState) : EditorState :=
  let s' := s.clampCursor
  { s' with mode := .visualBlock, selectionStart := some s'.getCursor, message := "-- VISUAL BLOCK --" }

def EditorState.exitVisualMode (s : EditorState) : EditorState :=
  { s with mode := .normal, selectionStart := none, message := "" }

def normalizeRange (p1 p2 : Point) : (Point × Point) :=
  if p1.row < p2.row || (p1.row == p2.row && p1.col <= p2.col) then (p1, p2) else (p2, p1)

def EditorState.isInSelection (s : EditorState) (row : Row) (col : Col) : Bool :=
  match s.selectionStart with
  | none => false
  | some startPt =>
    if s.mode == .visualBlock then
       let cursor := s.getCursor
       let minRow := min startPt.row cursor.row
       let maxRow := max startPt.row cursor.row
       let minCol := min startPt.col cursor.col
       let maxCol := max startPt.col cursor.col
       row >= minRow && row <= maxRow && col >= minCol && col <= maxCol
    else if s.mode == .visual then
       let (p1, p2) := normalizeRange startPt s.getCursor
       if row < p1.row || row > p2.row then false
       else if row > p1.row && row < p2.row then true
       else if p1.row == p2.row then col >= p1.col && col <= p2.col
       else if row == p1.row then col >= p1.col
       else if row == p2.row then col <= p2.col
       else false
    else
       false

def EditorState.getSelectedText (s : EditorState) : String :=
  match s.selectionStart with
  | none => ""
  | some startPt =>
    let tabStop := s.config.tabStop
    let buffer := s.getActiveBuffer
    if s.mode == .visualBlock then
      let cursor := s.getCursor
      let minRow := (min startPt.row cursor.row).val
      let maxRow := (max startPt.row cursor.row).val
      let minCol := (min startPt.col cursor.col).val
      let maxCol := (max startPt.col cursor.col).val

      let lines := (List.range (maxRow - minRow + 1)).map fun i =>
        let r := minRow + i
        let line := ViE.getLineFromBuffer buffer ⟨r⟩ |>.getD ""
        let sub := ViE.Unicode.dropByDisplayWidthWithTabStop line.toRawSubstring tabStop minCol
        ViE.Unicode.takeByDisplayWidthWithTabStop sub tabStop (maxCol - minCol + 1)
      String.intercalate "\n" lines
    else
      let (p1, p2) := normalizeRange startPt s.getCursor
      let startOff := ViE.getOffsetFromPointInBufferWithTabStop buffer p1.row p1.col tabStop |>.getD 0
      let lineStr := ViE.getLineFromBuffer buffer p2.row |>.getD ""
      let endCol := if p2.col.val < ViE.Unicode.stringWidthWithTabStop lineStr tabStop then
        ViE.Unicode.nextDisplayColWithTabStop lineStr tabStop p2.col.val
      else
        p2.col.val
      let endOff := ViE.getOffsetFromPointInBufferWithTabStop buffer p2.row ⟨endCol⟩ tabStop |>.getD buffer.table.tree.length
      PieceTree.getSubstring buffer.table.tree startOff (endOff - startOff) buffer.table

def EditorState.yankSelection (s : EditorState) : EditorState :=
  let tabStop := s.config.tabStop
  let text := s.getSelectedText
  let reg : Register :=
    if s.mode == .visualBlock then
      let lines := if text.isEmpty then [] else text.splitOn "\n"
      let width := lines.foldl (fun m l => max m (ViE.Unicode.stringWidthWithTabStop l tabStop)) 0
      {
        kind := .blockwise
        text := text
        blockLines := lines
        blockWidth := width
      }
    else
      {
        kind := .charwise
        text := text
        blockLines := []
        blockWidth := 0
      }
  { s.exitVisualMode with clipboard := some reg, message := s!"Yanked selection" }

def EditorState.deleteSelection (s : EditorState) : EditorState :=
  match s.selectionStart with
  | none => s
  | some startPt =>
    let tabStop := s.config.tabStop
    if s.mode == .visualBlock then
      let text := s.getSelectedText
      let lines := if text.isEmpty then [] else text.splitOn "\n"
      let width := lines.foldl (fun m l => max m (ViE.Unicode.stringWidthWithTabStop l tabStop)) 0
      let reg : Register := {
        kind := .blockwise
        text := text
        blockLines := lines
        blockWidth := width
      }
      let cursor := s.getCursor
      let minRow := (min startPt.row cursor.row).val
      let maxRow := (max startPt.row cursor.row).val
      let minCol := (min startPt.col cursor.col).val
      let maxCol := (max startPt.col cursor.col).val

      let s' := (List.range (maxRow - minRow + 1)).foldl (init := s) fun st i =>
        let r := minRow + i
        st.updateActiveBuffer fun buffer =>
          match buffer.table.getLineRange r with
          | some (lineStart, lineLen) =>
            let start := ViE.getOffsetFromPointInBufferWithTabStop buffer ⟨r⟩ ⟨minCol⟩ tabStop |>.getD (lineStart + lineLen)
            let endCol := maxCol + 1
            let endOff := ViE.getOffsetFromPointInBufferWithTabStop buffer ⟨r⟩ ⟨endCol⟩ tabStop |>.getD (lineStart + lineLen)
            let len := if endOff > start then endOff - start else 0
            if len > 0 then
              { buffer with table := buffer.table.delete start len start, dirty := true }
            else buffer
          | none => buffer
      { s'.exitVisualMode with clipboard := some reg } |>.setCursor { row := ⟨minRow⟩, col := ⟨minCol⟩ }
    else
      let text := s.getSelectedText
      let reg : Register := {
        kind := .charwise
        text := text
        blockLines := []
        blockWidth := 0
      }
      let (p1, p2) := normalizeRange startPt s.getCursor
      let s' := s.updateActiveBuffer fun buffer =>
        let startOff := ViE.getOffsetFromPointInBufferWithTabStop buffer p1.row p1.col tabStop |>.getD 0
        let lineStr := ViE.getLineFromBuffer buffer p2.row |>.getD ""
        let endCol := if p2.col.val < ViE.Unicode.stringWidthWithTabStop lineStr tabStop then
          ViE.Unicode.nextDisplayColWithTabStop lineStr tabStop p2.col.val
        else
          p2.col.val
        let endOff := ViE.getOffsetFromPointInBufferWithTabStop buffer p2.row ⟨endCol⟩ tabStop |>.getD buffer.table.tree.length
        { buffer with table := buffer.table.delete startOff (endOff - startOff) startOff, dirty := true }
      { s'.exitVisualMode with clipboard := some reg } |>.setCursor p1

end ViE