import ViE.State.Config
import ViE.Types
import ViE.Buffer.Content
namespace ViE
/-- Get current workgroup -/
def EditorState.getCurrentWorkgroup (s : EditorState) : WorkgroupState :=
s.workgroups[s.currentGroup]!
/-- Update current workgroup -/
def EditorState.updateCurrentWorkgroup (s : EditorState) (f : WorkgroupState → WorkgroupState) : EditorState :=
{ s with workgroups := s.workgroups.set! s.currentGroup (f (s.getCurrentWorkgroup)) }
/-- Get current workspace within the active workgroup -/
def EditorState.getCurrentWorkspace (s : EditorState) : WorkspaceState :=
let wg := s.getCurrentWorkgroup
wg.workspaces[wg.currentWorkspace]!
/-- Update current workspace within the active workgroup -/
def EditorState.updateCurrentWorkspace (s : EditorState) (f : WorkspaceState → WorkspaceState) : EditorState :=
s.updateCurrentWorkgroup fun wg =>
if wg.currentWorkspace < wg.workspaces.size then
let ws := wg.workspaces[wg.currentWorkspace]!
let updated := f ws
{ wg with workspaces := wg.workspaces.set! wg.currentWorkspace updated }
else
wg
/-- Switch to workgroup n -/
def EditorState.switchToWorkgroup (s : EditorState) (n : Nat) : EditorState :=
if n < s.workgroups.size then
{ s with currentGroup := n }
else
s
/- Helper functions to access/modify the active view -/
def Layout.findView (l : Layout) (id : Nat) : Option ViewState :=
match l with
| .window wid v => if wid == id then some v else none
| .hsplit left right _ => (left.findView id).orElse (fun _ => right.findView id)
| .vsplit top bottom _ => (top.findView id).orElse (fun _ => bottom.findView id)
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
/-- Update a view in the layout tree. Returns (updated layout, found flag) -/
def Layout.updateView (l : Layout) (id : Nat) (f : ViewState → ViewState) : Layout :=
let rec update (l : Layout) : Layout × Bool :=
match l with
| .window wid v =>
if wid == id then (.window wid (f v), true) else (l, false)
| .hsplit left right ratio =>
let (newLeft, found) := update left
if found then
(.hsplit newLeft right ratio, true)
else
let (newRight, found') := update right
(.hsplit newLeft newRight ratio, found')
| .vsplit top bottom ratio =>
let (newTop, found) := update top
if found then
(.vsplit newTop bottom ratio, true)
else
let (newBottom, found') := update bottom
(.vsplit newTop newBottom ratio, found')
(update l).1
def Layout.remove (l : Layout) (id : Nat) : Option Layout :=
match l with
| .window wid _ => if wid == id then none else some l
| .hsplit left right ratio =>
match left.remove id with
| none => some right
| some newLeft =>
match right.remove id with
| none => some newLeft
| some newRight => some (.hsplit newLeft newRight ratio)
| .vsplit top bottom ratio =>
match top.remove id with
| none => some bottom
| some newTop =>
match bottom.remove id with
| none => some newTop
| some newBottom => some (.vsplit newTop newBottom ratio)
def EditorState.getActiveBuffer (s : EditorState) : FileBuffer :=
let ws := s.getCurrentWorkspace
match ws.layout.findView ws.activeWindowId with
| some v => ws.buffers.find? (fun b => b.id == v.bufferId) |>.getD initialBuffer
| none => initialBuffer
def EditorState.updateActiveBuffer (s : EditorState) (f : FileBuffer -> FileBuffer) : EditorState :=
let ws := s.getCurrentWorkspace
let view := ws.layout.findView ws.activeWindowId
let s' :=
s.updateCurrentWorkspace fun ws =>
match view with
| some v =>
let newBuffers := ws.buffers.map fun b =>
if b.id == v.bufferId then
let updated := FileBuffer.recomputeMissingEol (f b)
FileBuffer.clearCache updated
else b
{ ws with buffers := newBuffers }
| none => ws
match view, s'.searchState with
| some v, some ss =>
{
s' with
searchState := some {
ss with
lineCacheBufferId := some v.bufferId
lineMatches := Lean.RBMap.empty
lineOrder := #[]
}
}
| _, _ => s'
def EditorState.getScroll (s : EditorState) : Row × Col :=
let ws := s.getCurrentWorkspace
match ws.layout.findView ws.activeWindowId with
| some v => (v.scrollRow, v.scrollCol)
| none => (Row.zero, Col.zero)
def EditorState.updateActiveView (s : EditorState) (f : ViewState → ViewState) : EditorState :=
s.updateCurrentWorkspace fun ws =>
{ ws with layout := ws.layout.updateView ws.activeWindowId f }
def WorkspaceState.isFloatingWindow (ws : WorkspaceState) (windowId : Nat) : Bool :=
ws.floatingWindows.contains windowId
def WorkspaceState.setWindowFloating (ws : WorkspaceState) (windowId : Nat) (enabled : Bool) : WorkspaceState :=
if enabled then
if ws.floatingWindows.contains windowId then ws
else { ws with floatingWindows := windowId :: ws.floatingWindows }
else
{
ws with
floatingWindows := 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 bottom
let ids := idsOf ws.layout
{
ws with
floatingWindows := 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.getFloatingWindowIds
let rec loop (i : Nat) : Option Nat :=
if i >= ids.size then
none
else if ids[i]! == windowId then
some i
else
loop (i + 1)
loop 0
def 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 windowId
ws.setFloatingWindowOffset windowId (rowOff + dRow) (colOff + dCol)
structure FloatingWindowBorderFlags where
top : Bool := true
right : Bool := true
bottom : Bool := true
left : Bool := true
deriving Inhabited
def 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 then
none
else if ws.layoutAllFloating l then
-- Outermost subtree containing the target where all descendants are floating.
some l
else
match l with
| .hsplit left right _ => (go left).orElse (fun _ => go right)
| .vsplit top bottom _ => (go top).orElse (fun _ => go bottom)
| .window _ _ => none
go ws.layout
def computeFloatingWindowBounds (rows cols idx : Nat) : Option (Nat × Nat × Nat × Nat) :=
let availableRows := if rows > 1 then rows - 1 else rows
if availableRows < 6 || cols < 16 then
none
else
let hMax := availableRows - 2
let wMax := cols - 4
let h := min hMax (max 8 ((availableRows * 3) / 5))
let w := min wMax (max 24 ((cols * 3) / 5))
let topBase := (availableRows - h) / 2
let leftBase := (cols - w) / 2
let 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.getCurrentWorkspace
let floatingIds := ws.getFloatingWindowIds
let rec indexOf (ids : Array Nat) (target : Nat) (i : Nat := 0) : Option Nat :=
if i >= ids.size then none
else if ids[i]! == target then some i
else indexOf ids target (i + 1)
let splitByRatio (total : Nat) (ratio : Float) : Option (Nat × Nat) :=
if total < 2 then
none
else
let raw := (Float.ofNat total * ratio).toUInt64.toNat
let 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 bottom
let minFloatingIndexOf (l : Layout) : Option Nat :=
let ids := idsOfLayout l
let 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 best
loop ids none
let 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 => none
let baseBounds :=
match floatingSubtreeBounds with
| some b => some b
| none =>
match ws.floatingWindowIndex? windowId with
| none => none
| some idx => computeFloatingWindowBounds s.windowHeight s.windowWidth idx
let toNatNonNeg (v : Int) : Nat :=
if v < 0 then 0 else Int.toNat v
let applyOffset (b : Nat × Nat × Nat × Nat) : Nat × Nat × Nat × Nat :=
let (top, left, h, w) := b
let (rowOff, colOff) := ws.getFloatingWindowOffset windowId
let availableRows := if s.windowHeight > 1 then s.windowHeight - 1 else s.windowHeight
let maxTop := if availableRows > h then availableRows - h else 0
let maxLeft := if s.windowWidth > w then s.windowWidth - w else 0
let rawTop := Int.ofNat top + rowOff
let rawLeft := Int.ofNat left + colOff
let top' := min maxTop (toNatNonNeg rawTop)
let left' := min maxLeft (toNatNonNeg rawLeft)
(top', left', h, w)
baseBounds.map applyOffset
def EditorState.getFloatingWindowBorderFlags (s : EditorState) (windowId : Nat) : FloatingWindowBorderFlags :=
let ws := s.getCurrentWorkspace
let 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 bottomFlags
match 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.getActiveWindowBounds (s : EditorState) : Nat × Nat :=
let ws := s.getCurrentWorkspace
let rec findBounds (l : Layout) (h w : Nat) : Option (Nat × Nat) :=
match l with
| .window id _ => if id == ws.activeWindowId then some (h, w) else none
| .hsplit left right ratio =>
let leftW := (Float.ofNat w * ratio).toUInt64.toNat
(findBounds left h leftW).orElse (fun _ => findBounds right h (if w > leftW then w - leftW - 1 else 0))
| .vsplit top bottom ratio =>
let topH := (Float.ofNat h * ratio).toUInt64.toNat
(findBounds top topH w).orElse (fun _ => findBounds bottom (if h > topH then h - topH - 1 else 0) w)
-- Reserve 1 line for global status/command
let layoutH := if s.windowHeight > 0 then s.windowHeight - 1 else 0
(findBounds ws.layout layoutH s.windowWidth).getD (layoutH, s.windowWidth)
def EditorState.getActiveWindowScrollBounds (s : EditorState) : Nat × Nat :=
let ws := s.getCurrentWorkspace
let activeId := ws.activeWindowId
if ws.isFloatingWindow activeId then
match s.getFloatingWindowBounds activeId with
| some (_, _, h, w) =>
-- Floating window has a border, so scrollable area is smaller than outer bounds.
let borderFlags := s.getFloatingWindowBorderFlags activeId
let topBottomBorders := (if borderFlags.top then 1 else 0) + (if borderFlags.bottom then 1 else 0)
let contentRows := if h > topBottomBorders then h - topBottomBorders else 1
let h' := contentRows + 1
let 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.getActiveWindowBounds
else
s.getActiveWindowBounds
def getAllWindowBounds (l : Layout) (h w : Nat) : List (Nat × Nat × Nat × Nat × Nat) :=
let rec traverse (l : Layout) (r c h w : Nat) (acc : List (Nat × Nat × Nat × Nat × Nat)) : List (Nat × Nat × Nat × Nat × Nat) :=
match l with
| .window id _ => (id, r, c, h, w) :: acc
| .hsplit left right ratio =>
let leftW := (Float.ofNat w * ratio).toUInt64.toNat
let acc' := traverse left r c h leftW acc
traverse right r (c + leftW + 1) h (if w > leftW then w - leftW - 1 else 0) acc'
| .vsplit top bottom ratio =>
let topH := (Float.ofNat h * ratio).toUInt64.toNat
let acc' := traverse top r c topH w acc
traverse bottom (r + topH + 1) c (if h > topH then h - topH - 1 else 0) w acc'
traverse l 0 0 h w []
def getWindowIds (l : Layout) : List Nat :=
match l with
| .window id _ => [id]
| .hsplit left right _ => getWindowIds left ++ getWindowIds right
| .vsplit top bottom _ => getWindowIds top ++ getWindowIds bottom
end ViE