import ViE.State
import ViE.Types
import ViE.Buffer.Manager
import ViE.Buffer.Content
import ViE.Basic
import ViE.State.Config
import ViE.Buffer.LowIO
import ViE.Window.Actions
namespace ViE.Feature
open ViE
def previewMaxBytes : Nat := 65536
def updateExplorerState (state : EditorState) (bufId : Nat) (f : ExplorerState → ExplorerState) : EditorState :=
{ state with explorers := state.explorers.map (fun (id, ex) => if id == bufId then (id, f ex) else (id, ex)) }
private def isUsableExplorerTargetWindow (ws : WorkspaceState) (explorerWinId : Nat) (previewWinId : Option Nat)
(explorerBufIds : List Nat) (previewBufId : Option Nat) (wid : Nat) : Bool :=
if wid == explorerWinId then
false
else if previewWinId == some wid then
false
else
match ws.layout.findView wid with
| some v =>
let isExplorerBuf := explorerBufIds.contains v.bufferId
let isPreviewBuf := match previewBufId with | some pid => v.bufferId == pid | none => false
!isExplorerBuf && !isPreviewBuf
| none => false
def openActiveAsFloating (state : EditorState) : EditorState :=
state.updateCurrentWorkspace fun ws =>
(ws.setWindowFloating ws.activeWindowId true).pruneFloatingWindows
def openExplorerWindowsAsFloating (state : EditorState) (previewWindowId : Option Nat) : EditorState :=
state.updateCurrentWorkspace fun ws =>
let ws := ws.setWindowFloating ws.activeWindowId true
let ws :=
match previewWindowId with
| some wid =>
if ws.layout.findView wid |>.isSome then
ws.setWindowFloating wid true
else
ws
| none => ws
ws.pruneFloatingWindows
def placeExplorerInWindow (state : EditorState) (bufferId : Nat) (cursorRow : Nat) (reuseActive : Bool) : EditorState :=
if reuseActive then
state.updateActiveView fun v =>
{ v with bufferId := bufferId, cursor := {row := ⟨cursorRow⟩, col := 0}, scrollRow := 0, scrollCol := 0 }
else
let ws := state.getCurrentWorkspace
let newWinId := ws.nextWindowId
let s1 := ViE.Window.splitWindow state false
s1.updateCurrentWorkspace fun ws =>
{ ws with
activeWindowId := 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 := title
lines := #[initialText]
maxWidth := 48
cursorRow := 0
cursorCol := initialText.toList.length
}
{
state with
mode := .normal
floatingOverlay := some ov
floatingInputCommand := some commandPrefix
inputState := {
state.inputState with
commandBuffer := ""
countBuffer := ""
previousKey := none
pendingKeys := ""
}
message := ""
dirty := true
}
def fileExplorerNewFileLabel : String := "[New File]"
def fileExplorerNewDirectoryLabel : String := "[New Directory]"
def isFileExplorerActionEntry (entry : FileEntry) : Bool :=
entry.name == fileExplorerNewFileLabel || entry.name == fileExplorerNewDirectoryLabel
def fileExplorerActionEntries : List FileEntry := [
{
name := fileExplorerNewFileLabel
path := ""
isDirectory := false
},
{
name := fileExplorerNewDirectoryLabel
path := ""
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 then
loop tail (idx + 1)
else
some idx
loop entries 0
def defaultExplorerCursorRow (entries : List FileEntry) : Nat :=
match firstNonActionEntryIndex entries with
| some idx => 2 + idx
| none => 2
def ensureTrailingSlash (path : String) : String :=
if path.endsWith "/" then path else path ++ "/"
def getSelectedEntry (state : EditorState) (explorer : ExplorerState) : Option FileEntry :=
let cursor := state.getCursor
if cursor.row < 2 then
none
else
let idx := cursor.row - 2
if idx.val < explorer.entries.length then
some (explorer.entries[idx.val]!)
else
none
def getSelectedIndex (state : EditorState) (explorer : ExplorerState) : Option Nat :=
let cursor := state.getCursor
if cursor.row < 2 then
none
else
let idx := cursor.row - 2
if idx.val < explorer.entries.length then
some idx.val
else
none
def buildWorkspacePreviewLines (ws : WorkspaceState) : List String :=
let header := [s!"Workspace: {ws.name}", ""]
let entries := ws.buffers.map fun b =>
match b.filename with
| some name => name
| none => s!"[Untitled:{b.id}]"
if entries.isEmpty then
header ++ ["(no buffers)"]
else
header ++ entries
def buildWorkgroupPreviewLines (wg : WorkgroupState) : List String :=
let header := [s!"Workgroup: {wg.name}", ""]
let entries := wg.workspaces.toList.map (fun ws => ws.name)
if entries.isEmpty then
header ++ ["(no workspaces)"]
else
header ++ entries
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?
else
none
def 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.getCurrentWorkspace
if ws.buffers.any (fun b => b.id == bid) then
some bid
else
none
def 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}"
def ensurePreviewTextBuffer (state : EditorState) (explorer : ExplorerState) (title : String) (lines : List String) : (EditorState × Nat) :=
let content : TextBuffer := if lines.isEmpty then #[#[]] else lines.toArray.map stringToLine
let buildLeafBits := state.config.searchBloomBuildLeafBits
let buf := { ViE.Buffer.fileBufferFromTextBufferWithConfig 0 (some s!"preview://{title}") content buildLeafBits with dirty := false }
match explorer.previewBufferId with
| some pid =>
let ws := state.getCurrentWorkspace
if ws.buffers.any (fun b => b.id == pid) then
let updated := { buf with id := pid }
let s' := state.updateCurrentWorkspace fun ws =>
{ ws with buffers := ws.buffers.map (fun b => if b.id == pid then updated else b) }
(s', pid)
else
let (pid, s') := ViE.Buffer.addBuffer state buf
(s', pid)
| none =>
let (pid, s') := ViE.Buffer.addBuffer state buf
(s', pid)
def getPreviewWorkspace (state : EditorState) (explorer : ExplorerState) : WorkspaceState :=
let wg := state.getCurrentWorkgroup
match getSelectedIndex state explorer with
| some idx =>
if idx >= 2 then
let real := idx - 2
if real < wg.workspaces.size then
wg.workspaces[real]!
else
state.getCurrentWorkspace
else
state.getCurrentWorkspace
| none => state.getCurrentWorkspace
def getPreviewWorkgroup (state : EditorState) (explorer : ExplorerState) : WorkgroupState :=
match getSelectedIndex state explorer with
| some idx =>
if idx >= 2 then
let real := idx - 2
if real < state.workgroups.size then
state.workgroups[real]!
else
state.getCurrentWorkgroup
else
state.getCurrentWorkgroup
| none => state.getCurrentWorkgroup
def ensurePreviewBuffer (state : EditorState) (explorer : ExplorerState) (entry : FileEntry) : IO (EditorState × Nat) := do
let resolved := entry.path
let loaded ← ViE.loadPreviewBufferByteArrayWithConfig resolved previewMaxBytes state.config
let previewName := some s!"preview://{resolved}"
let previewBuf := { loaded with filename := previewName, dirty := false }
match explorer.previewBufferId with
| some pid =>
let ws := state.getCurrentWorkspace
if ws.buffers.any (fun b => b.id == pid) then
let updated := { previewBuf with id := pid }
let s' := state.updateCurrentWorkspace fun ws =>
{ ws with buffers := ws.buffers.map (fun b => if b.id == pid then updated else b) }
return (s', pid)
else
let (pid, s') := ViE.Buffer.addBuffer state previewBuf
return (s', pid)
| none =>
let (pid, s') := ViE.Buffer.addBuffer state previewBuf
return (s', pid)
def refreshExplorerPreview (state : EditorState) : IO EditorState := do
let buf := state.getActiveBuffer
let explorerOpt := state.explorers.find? (fun (id, _) => id == buf.id)
match explorerOpt with
| none => return state
| some (bufId, explorer) =>
match explorer.previewWindowId with
| none => return state
| some previewWinId =>
match explorer.mode with
| .files =>
match getSelectedEntry state explorer with
| none => return state
| some entry =>
if isFileExplorerActionEntry entry then
return state
else if entry.isDirectory then
return state
else
let (s1, previewBufId) ← ensurePreviewBuffer state explorer entry
let 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 previewBufferId := some previewBufId })
return s3
| .workspaces =>
let ws := getPreviewWorkspace state explorer
let lines := buildWorkspacePreviewLines ws
let (s1, previewBufId) := ensurePreviewTextBuffer state explorer ws.name lines
let 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 previewBufferId := some previewBufId })
return s3
| .workgroups =>
let wg := getPreviewWorkgroup state explorer
let lines := buildWorkgroupPreviewLines wg
let (s1, previewBufId) := ensurePreviewTextBuffer state explorer wg.name lines
let 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 previewBufferId := some previewBufId })
return s3
| .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
def openExplorerDefaultPreview (state : EditorState) (bufId : Nat) (explorer : ExplorerState) : IO EditorState := do
match explorer.previewWindowId with
| some _ => return state
| none =>
let ws := state.getCurrentWorkspace
let previewWinId := ws.nextWindowId
let s1 := ViE.Window.splitWindow state false
let (s2, previewBufId) ←
match explorer.mode with
| .files =>
let selected := getSelectedEntry s1 explorer
let entryOpt : Option FileEntry :=
match selected with
| some e =>
if isFileExplorerActionEntry e then
explorer.entries.find? (fun x => !isFileExplorerActionEntry x)
else
some e
| none =>
explorer.entries.find? (fun x => !isFileExplorerActionEntry x)
match entryOpt with
| some entry =>
if entry.isDirectory then
let content : TextBuffer := #[#[]]
let buildLeafBits := s1.config.searchBloomBuildLeafBits
let buf := { ViE.Buffer.fileBufferFromTextBufferWithConfig 0 (some "preview://") content buildLeafBits with dirty := false }
let (pid, s2) := ViE.Buffer.addBuffer s1 buf
pure (s2, pid)
else
ensurePreviewBuffer s1 explorer entry
| none =>
let content : TextBuffer := #[#[]]
let buildLeafBits := s1.config.searchBloomBuildLeafBits
let buf := { ViE.Buffer.fileBufferFromTextBufferWithConfig 0 (some "preview://") content buildLeafBits with dirty := false }
let (pid, s2) := ViE.Buffer.addBuffer s1 buf
pure (s2, pid)
| .workspaces =>
let ws := getPreviewWorkspace s1 explorer
let lines := buildWorkspacePreviewLines ws
let (s2, pid) := ensurePreviewTextBuffer s1 explorer ws.name lines
pure (s2, pid)
| .workgroups =>
let wg := getPreviewWorkgroup s1 explorer
let lines := buildWorkgroupPreviewLines wg
let (s2, pid) := ensurePreviewTextBuffer s1 explorer wg.name lines
pure (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.id
let previewBufId := (selectedBufferIdFromExplorer s1 explorer).getD fallbackBufId
pure (s1, previewBufId)
let s3 := s2.updateCurrentWorkspace fun ws =>
{ ws with layout := ws.layout.updateView previewWinId (fun v =>
{ v with bufferId := previewBufId, cursor := Point.zero, scrollRow := 0, scrollCol := 0 }) }
let s4 := updateExplorerState s3 bufId (fun ex =>
{
ex with
previewWindowId := some previewWinId
previewBufferId := if explorer.mode == .buffers then none else some previewBufId
})
return openExplorerWindowsAsFloating s4 (some previewWinId)
def toggleExplorerPreview (state : EditorState) : IO EditorState := do
let buf := state.getActiveBuffer
let explorerOpt := state.explorers.find? (fun (id, _) => id == buf.id)
match explorerOpt with
| none => return state
| some (bufId, explorer) =>
match explorer.previewWindowId with
| some previewWinId =>
let s1 := state.updateCurrentWorkspace fun ws =>
match ws.layout.remove previewWinId with
| some newLayout => ({ ws with layout := newLayout }).pruneFloatingWindows
| none => ws.pruneFloatingWindows
let s2 := updateExplorerState s1 bufId (fun ex => { ex with previewWindowId := none, previewBufferId := none })
return { s2 with message := "Preview closed" }
| none =>
match explorer.mode with
| .files =>
match getSelectedEntry state explorer with
| none => return { state with message := "Preview: no selection" }
| some entry =>
if entry.isDirectory then
return { state with message := "Preview: directory" }
else
let ws := state.getCurrentWorkspace
let previewWinId := ws.nextWindowId
let s1 := ViE.Window.splitWindow state false
let (s2, previewBufId) ← ensurePreviewBuffer s1 explorer entry
let s3 := s2.updateCurrentWorkspace fun ws =>
{ ws with layout := ws.layout.updateView previewWinId (fun v =>
{ v with bufferId := previewBufId, cursor := Point.zero, scrollRow := 0, scrollCol := 0 }) }
let s4 := updateExplorerState s3 bufId (fun ex => { ex with previewWindowId := some previewWinId, previewBufferId := some previewBufId })
let s5 := openExplorerWindowsAsFloating s4 (some previewWinId)
return { s5 with message := "Preview opened" }
| .workspaces =>
let ws := state.getCurrentWorkspace
let previewWinId := ws.nextWindowId
let s1 := ViE.Window.splitWindow state false
let previewWs := getPreviewWorkspace s1 explorer
let lines := buildWorkspacePreviewLines previewWs
let (s2, previewBufId) := ensurePreviewTextBuffer s1 explorer previewWs.name lines
let s3 := s2.updateCurrentWorkspace fun ws =>
{ ws with layout := ws.layout.updateView previewWinId (fun v =>
{ v with bufferId := previewBufId, cursor := Point.zero, scrollRow := 0, scrollCol := 0 }) }
let s4 := updateExplorerState s3 bufId (fun ex => { ex with previewWindowId := some previewWinId, previewBufferId := some previewBufId })
let s5 := openExplorerWindowsAsFloating s4 (some previewWinId)
return { s5 with message := "Preview opened" }
| .workgroups =>
let ws := state.getCurrentWorkspace
let previewWinId := ws.nextWindowId
let s1 := ViE.Window.splitWindow state false
let previewWg := getPreviewWorkgroup s1 explorer
let lines := buildWorkgroupPreviewLines previewWg
let (s2, previewBufId) := ensurePreviewTextBuffer s1 explorer previewWg.name lines
let s3 := s2.updateCurrentWorkspace fun ws =>
{ ws with layout := ws.layout.updateView previewWinId (fun v =>
{ v with bufferId := previewBufId, cursor := Point.zero, scrollRow := 0, scrollCol := 0 }) }
let s4 := updateExplorerState s3 bufId (fun ex => { ex with previewWindowId := some previewWinId, previewBufferId := some previewBufId })
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.getCurrentWorkspace
let previewWinId := ws.nextWindowId
let s1 := ViE.Window.splitWindow state false
let 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" }
/-- Open the file explorer at the specified path (or current workspace root) -/
def openExplorer (state : EditorState) (pathArg : String) : IO EditorState := do
let ws := state.getCurrentWorkspace
let path := if pathArg == "." || pathArg == "" then
ws.rootPath.getD "."
else if pathArg.startsWith "/" then
pathArg
else
match ws.rootPath with
| some root =>
if pathArg == root then root else root ++ "/" ++ pathArg
| none => pathArg
-- Read directory contents
try
let dirPath := System.FilePath.mk path
let entries := (← System.FilePath.readDir dirPath).toList
let mut navEntries : List FileEntry := []
-- Add parent directory entry if not at root
if path != "/" then
let parentPath := match dirPath.parent with
| some p => p.toString
| none => "/"
navEntries := navEntries ++ [{
name := ".."
path := parentPath
isDirectory := true
}]
-- Convert directory entries to FileEntry
for entry in entries do
let entryPath := entry.path.toString
let isDir ← entry.path.isDir
navEntries := navEntries ++ [{
name := entry.fileName
path := entryPath
isDirectory := isDir
}]
-- Sort: directories first, then files, alphabetically
let sortedEntries := navEntries.toArray.qsort fun a b =>
if a.isDirectory != b.isDirectory then
a.isDirectory -- directories first
else
a.name < b.name
let allEntries := fileExplorerActionEntries ++ sortedEntries.toList
-- Create explorer state
let reuseActive := state.explorers.any (fun (id, _) => id == state.getActiveBuffer.id)
let targetWindowId :=
if reuseActive then
match state.explorers.find? (fun (id, _) => id == state.getActiveBuffer.id) with
| some (_, ex) => ex.targetWindowId
| none => some ws.activeWindowId
else
some ws.activeWindowId
let explorerState : ExplorerState := {
currentPath := path
entries := allEntries
selectedIndex := 0
mode := .files
previewWindowId := none
previewBufferId := none
targetWindowId := targetWindowId
}
-- Generate buffer content
let header := [s!"Explorer: {path}", ""]
let mut content := header
let mut idx := 0
for entry in explorerState.entries do
let pref := if idx == 0 then "> " else " "
let isAction := isFileExplorerActionEntry entry
let icon :=
if isAction then ""
else if entry.isDirectory then state.config.dirIcon
else state.config.fileIcon
let suffix := if !isAction && entry.isDirectory then "/" else ""
content := content ++ [s!"{pref}{icon}{entry.name}{suffix}"]
idx := idx + 1
-- Create new buffer
let contentBuffer := content.toArray.map stringToLine
let (bufferId, s') := ViE.Buffer.createNewBuffer state contentBuffer (some s!"explorer://{path}")
-- Register explorer
let s'' := { s' with explorers := (bufferId, explorerState) :: s'.explorers }
-- Open explorer in a dedicated window by default; reuse active only when already in explorer.
let s''' := placeExplorerInWindow s'' bufferId (defaultExplorerCursorRow explorerState.entries) reuseActive
let s'''' := { s''' with message := s!"Explorer: {path}" }
let s''''' := openActiveAsFloating s''''
-- Auto-open preview by default for file explorer
return (← openExplorerDefaultPreview s''''' bufferId explorerState)
catch e =>
return { state with message := s!"Error reading directory: {e}" }
def openExplorerWithPreview (state : EditorState) (pathArg : String) (previewWindowId : Option Nat)
(previewBufferId : Option Nat) (targetWindowId : Option Nat) : IO EditorState := do
let ws := state.getCurrentWorkspace
let path := if pathArg == "." || pathArg == "" then
ws.rootPath.getD "."
else if pathArg.startsWith "/" then
pathArg
else
match ws.rootPath with
| some root =>
if pathArg == root then root else root ++ "/" ++ pathArg
| none => pathArg
try
let dirPath := System.FilePath.mk path
let entries := (← System.FilePath.readDir dirPath).toList
let mut navEntries : List FileEntry := []
if path != "/" then
let parentPath := match dirPath.parent with
| some p => p.toString
| none => "/"
navEntries := navEntries ++ [{
name := ".."
path := parentPath
isDirectory := true
}]
for entry in entries do
let entryPath := entry.path.toString
let isDir ← entry.path.isDir
navEntries := navEntries ++ [{
name := entry.fileName
path := entryPath
isDirectory := isDir
}]
let sortedEntries := navEntries.toArray.qsort fun a b =>
if a.isDirectory != b.isDirectory then
a.isDirectory
else
a.name < b.name
let allEntries := fileExplorerActionEntries ++ sortedEntries.toList
let explorerState : ExplorerState := {
currentPath := path
entries := allEntries
selectedIndex := 0
mode := .files
previewWindowId := previewWindowId
previewBufferId := previewBufferId
targetWindowId := targetWindowId
}
let header := [s!"Explorer: {path}", ""]
let mut content := header
let mut idx := 0
for entry in explorerState.entries do
let pref := if idx == 0 then "> " else " "
let isAction := isFileExplorerActionEntry entry
let icon :=
if isAction then ""
else if entry.isDirectory then state.config.dirIcon
else state.config.fileIcon
let suffix := if !isAction && entry.isDirectory then "/" else ""
content := content ++ [s!"{pref}{icon}{entry.name}{suffix}"]
idx := idx + 1
let contentBuffer := content.toArray.map stringToLine
let (bufferId, s') := ViE.Buffer.createNewBuffer state contentBuffer (some s!"explorer://{path}")
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 (defaultExplorerCursorRow explorerState.entries) reuseActive
let s'''' := { s''' with message := s!"Explorer: {path}" }
let s''''' := openActiveAsFloating s''''
match previewWindowId with
| some wid => refreshExplorerPreview (openExplorerWindowsAsFloating s''''' (some wid))
| none => openExplorerDefaultPreview s''''' bufferId explorerState
catch e =>
return { state with message := s!"Error reading directory: {e}" }
/-- Open the workspace explorer to list workspaces. -/
def openWorkspaceExplorer (state : EditorState) : IO EditorState := do
let wg := state.getCurrentWorkgroup
let mut entries : List FileEntry := []
entries := entries ++ [{
name := "[New Workspace]"
path := ""
isDirectory := false
}]
entries := entries ++ [{
name := "[Rename Workspace]"
path := ""
isDirectory := false
}]
for ws in wg.workspaces.toList do
let label :=
match ws.rootPath with
| some root => s!"{ws.name} — {root}"
| none => ws.name
entries := entries ++ [{
name := label
path := ws.rootPath.getD ""
isDirectory := true
}]
let explorerState : ExplorerState := {
currentPath := s!"workgroup:{wg.name}"
entries := entries
selectedIndex := 0
mode := .workspaces
previewWindowId := none
previewBufferId := none
targetWindowId := some state.getCurrentWorkspace.activeWindowId
}
let header := [s!"Workspace Explorer: {wg.name}", ""]
let mut content := header
let mut idx := 0
for entry in explorerState.entries do
let pref := if idx == 0 then "> " else " "
let mark := if idx == (wg.currentWorkspace + 1) then "* " else " "
let icon := if entry.isDirectory then state.config.dirIcon else state.config.fileIcon
content := content ++ [s!"{pref}{mark}{icon}{entry.name}"]
idx := idx + 1
let contentBuffer := content.toArray.map stringToLine
let (bufferId, s') := ViE.Buffer.createNewBuffer state contentBuffer (some "explorer://workgroup")
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 reuseActive
let s'''' := { s''' with message := s!"Workspace Explorer: {wg.name}" }
let s''''' := openActiveAsFloating s''''
return (← openExplorerDefaultPreview s''''' bufferId explorerState)
/-- Open the workgroup explorer to list workgroups. -/
def openWorkgroupExplorer (state : EditorState) : IO EditorState := do
let mut entries : List FileEntry := []
entries := entries ++ [{
name := "[New Workgroup]"
path := ""
isDirectory := false
}]
entries := entries ++ [{
name := "[Rename Workgroup]"
path := ""
isDirectory := false
}]
for wg in state.workgroups.toList do
entries := entries ++ [{
name := wg.name
path := ""
isDirectory := true
}]
let explorerState : ExplorerState := {
currentPath := "workgroups"
entries := entries
selectedIndex := 0
mode := .workgroups
previewWindowId := none
previewBufferId := none
targetWindowId := some state.getCurrentWorkspace.activeWindowId
}
let header := ["Workgroup Explorer", ""]
let mut content := header
let mut idx := 0
for entry in explorerState.entries do
let pref := if idx == explorerState.selectedIndex then "> " else " "
let mark := if idx == (state.currentGroup + 1) then "* " else " "
let icon := if entry.isDirectory then state.config.dirIcon else state.config.fileIcon
content := content ++ [s!"{pref}{mark}{icon}{entry.name}"]
idx := idx + 1
let contentBuffer := content.toArray.map stringToLine
let (bufferId, s') := ViE.Buffer.createNewBuffer state contentBuffer (some "explorer://workgroups")
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) reuseActive
let s'''' := { s''' with message := "Workgroup Explorer" }
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 := do
let ws := state.getCurrentWorkspace
let 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 bid
let candidates := ws.buffers.filter (fun b => !skipBufferId b.id)
if candidates.isEmpty then
return { state with message := "No open buffers" }
let sortedBuffers := (candidates.toArray.qsort fun a b => a.id < b.id).toList
let entries := sortedBuffers.map fun b =>
{
name := bufferExplorerLabel b
path := makeBufferEntryPath b.id
isDirectory := false
}
let activeBufferId := state.getActiveBuffer.id
let rec findSelectedIndex (rest : List FileEntry) (idx : Nat) : Nat :=
match rest with
| [] => 0
| entry :: tail =>
if parseBufferEntryId entry.path == some activeBufferId then
idx
else
findSelectedIndex tail (idx + 1)
let selectedIndex := findSelectedIndex entries 0
let explorerState : ExplorerState := {
currentPath := "buffers"
entries := entries
selectedIndex := selectedIndex
mode := .buffers
previewWindowId := none
previewBufferId := none
targetWindowId := some state.getCurrentWorkspace.activeWindowId
}
let header := [s!"Buffer Explorer: {ws.name}", ""]
let mut content := header
let mut idx := 0
for entry in explorerState.entries do
let 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 + 1
let contentBuffer := content.toArray.map stringToLine
let (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) reuseActive
let s'''' := { s''' with message := "Buffer Explorer" }
let s''''' := openActiveAsFloating s''''
return (← openExplorerDefaultPreview s''''' bufferId explorerState)
/-- Handle Enter key in explorer buffer -/
def handleExplorerEnter (state : EditorState) : IO EditorState := do
let buf := state.getActiveBuffer
-- Check if this is an explorer buffer
let explorerOpt := state.explorers.find? (fun (id, _) => id == buf.id)
match explorerOpt with
| none =>
match state.searchState with
| some _ => return ViE.findNextMatch state
| none => return state.moveCursorDown
| some (_, explorer) =>
-- Get current cursor row
let cursor := state.getCursor
if cursor.row < 2 then return state -- Header rows
-- Explorer header is 2 lines, so subtract 2
let selectedIdx := cursor.row - 2
if selectedIdx.val >= explorer.entries.length then
return state
match explorer.mode with
| .files =>
-- Get selected entry
let entry := explorer.entries[selectedIdx.val]!
if entry.name == fileExplorerNewFileLabel then
let cmdPrefix := s!"mkfile {ensureTrailingSlash explorer.currentPath}"
return openNameInputFloat state "New File" "" cmdPrefix
else if entry.name == fileExplorerNewDirectoryLabel then
let cmdPrefix := s!"mkdir {ensureTrailingSlash explorer.currentPath}"
return openNameInputFloat state "New Directory" "" cmdPrefix
else if entry.isDirectory then
-- Navigate to directory
openExplorerWithPreview state entry.path explorer.previewWindowId explorer.previewBufferId explorer.targetWindowId
else
let ws0 := state.getCurrentWorkspace
let explorerWinId := ws0.activeWindowId
let explorerBufIds := state.explorers.map (fun (id, _) => id)
let targetWinOpt :=
let preferred := explorer.targetWindowId
match preferred with
| some wid =>
if isUsableExplorerTargetWindow ws0 explorerWinId explorer.previewWindowId explorerBufIds explorer.previewBufferId wid then
some wid
else
let ids := ViE.Window.getWindowIds ws0.layout
ids.find? (fun w => isUsableExplorerTargetWindow ws0 explorerWinId explorer.previewWindowId explorerBufIds explorer.previewBufferId w)
| none =>
let ids := ViE.Window.getWindowIds ws0.layout
ids.find? (fun w => isUsableExplorerTargetWindow ws0 explorerWinId explorer.previewWindowId explorerBufIds explorer.previewBufferId w)
-- 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.pruneFloatingWindows
let 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
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 newLayout
let activeId :=
if ids.contains targetWinId then
targetWinId
else
match ids with
| h :: _ => h
| [] => ws.activeWindowId
({ ws with layout := newLayout, activeWindowId := activeId }).pruneFloatingWindows
| none =>
ws.pruneFloatingWindows
let s3 := ViE.Buffer.removeBuffer s2 buf.id
let opened ← ViE.Buffer.openBuffer s3 entry.path
return opened
| none =>
let opened ← ViE.Buffer.openBuffer s1 entry.path
let s2 := opened.updateCurrentWorkspace fun ws =>
(ws.setWindowFloating ws.activeWindowId false).pruneFloatingWindows
let s3 := ViE.Buffer.removeBuffer s2 buf.id
return 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.getCurrentWorkspace
if !ws0.buffers.any (fun b => b.id == targetBufId) then
return { state with message := s!"Buffer not found: {targetBufId}" }
else
let explorerWinId := ws0.activeWindowId
let explorerBufIds := state.explorers.map (fun (id, _) => id)
let targetWinOpt :=
let preferred := explorer.targetWindowId
let isTarget (wid : Nat) :=
isUsableExplorerTargetWindow ws0 explorerWinId explorer.previewWindowId explorerBufIds none wid
match preferred with
| some wid =>
if isTarget wid then
some wid
else
let ids := ViE.Window.getWindowIds ws0.layout
ids.find? isTarget
| none =>
let ids := ViE.Window.getWindowIds ws0.layout
ids.find? isTarget
let 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.pruneFloatingWindows
updateExplorerState 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 newLayout
let activeId :=
if ids.contains targetWinId then
targetWinId
else
match ids with
| h :: _ => h
| [] => ws.activeWindowId
({ ws with layout := newLayout, activeWindowId := activeId }).pruneFloatingWindows
| none =>
ws.pruneFloatingWindows
let s3 := ViE.Buffer.removeBuffer s2 buf.id
let 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).pruneFloatingWindows
let s4 := ViE.Buffer.removeBuffer s3 buf.id
return { s4 with message := s!"Switched to buffer: {targetLabel}" }
| .workgroups =>
let idx := selectedIdx.val
if idx == 0 then
return openNameInputFloat state "New Workgroup" "" "wg new "
else if idx == 1 then
let current := state.getCurrentWorkgroup.name
return openNameInputFloat state "Rename Workgroup" current "wg rename "
else
let realIdx := idx - 2
if realIdx < state.workgroups.size then
let s' := { state with currentGroup := realIdx }
let wg := s'.getCurrentWorkgroup
return { s' with message := s!"Switched workgroup: {wg.name}" }
else
return state
| .workspaces =>
let idx := selectedIdx.val
if idx == 0 then
return openNameInputFloat state "New Workspace" "" "ws new "
else if idx == 1 then
let current := state.getCurrentWorkspace.name
return openNameInputFloat state "Rename Workspace" current "ws rename "
else
let realIdx := idx - 2
let s' := state.updateCurrentWorkgroup fun wg =>
if realIdx < wg.workspaces.size then
{ wg with currentWorkspace := realIdx }
else
wg
let ws := s'.getCurrentWorkspace
return { s' with message := s!"Switched workspace: {ws.name}" }
end ViE.Feature