using fwt
using gfx
using petanque
using concurrent
using netColarUtils
**************************************************************************
** Recent
**************************************************************************
internal const class RecentCmd : Cmd
{
override const Str name := "Recent Files"
override Void invoke(Event event)
{
index := event.keyChar - '0'
items := frame.history.items
if(index>=0 && items.size > index)
frame.goto(items[index])
}
new make(|This| f) {f(this)}
}
internal const class MostRecentCmd : Cmd
{
override const Str name := "Last File"
override Void invoke(Event event)
{
if(frame.history.items.size > 1)
frame.goto(frame.history.items[1])
}
new make(|This| f) {f(this)}
}
**************************************************************************
** Prev/Next Mark
**************************************************************************
internal const class PrevMarkCmd : Cmd
{
override const Str name := "Prev Mark"
override Void invoke(Event event) { frame.curMark-- }
new make(|This| f) {f(this)}
}
internal const class NextMarkCmd : Cmd
{
override const Str name := "Next Mark"
override Void invoke(Event event) { frame.curMark++ }
new make(|This| f) {f(this)}
}
**************************************************************************
** GotoCmd
**************************************************************************
internal const class GotoCmd : Cmd
{
new make(|This| f) {f(this)}
override const Str name := "Goto"
override Void invoke(Event event)
{
// prompt field
font := ((Sys)Service.find(Sys#)).theme.font
prompt := Text
{
it.font = font
}
// table of matches
matches := GotoMatchModel { itemFont = font; width = 700 }
table := Table
{
it.headerVisible = false
it.model = matches
}
// check for current selection to initialize
selection := frame.curView?.curSelection ?: ""
prompt.text = selection
// If selection & single match, no need to prompt, just go straight there
if(! selection.isEmpty)
{
matches.items = findMatches(prompt.text.trim)
if(matches.items.size == 1)
{
frame.goto(matches.items.first)
return
}
}
// build dialog
Item? selected
ok := Dialog.ok
cancel := Dialog.cancel
dialog := Dialog(frame)
{
title = "Goto"
commands = [ok, cancel]
body = EdgePane
{
top = InsetPane(0, 0, 10, 0) { prompt, }
bottom = ConstraintPane
{
minw = maxw = matches.width+10
minh = maxh = 500
table,
}
}
}
prompt.onAction.add |e| { dialog.close(ok) }
prompt.onKeyDown.add |e|
{
if (e.key == Key.down)
{
e.consume
if (table.model.numRows > 0) table.selected = [0]
table.focus
}
}
prompt.onModify.add |e|
{
matches.items = findMatches(prompt.text.trim)
table.refreshAll
}
table
{
onAction.add |e|
{
selected = matches.items.getSafe(table.selected.first ?: -1)
dialog.close(ok)
}
onSelect.add |e|
{
selected = matches.items.getSafe(table.selected.first ?: -1)
}
}
// open dialog
if (dialog.open != Dialog.ok) return
// if we got actual selection from table use that
// otherwise assume top match from table
if (selected == null) selected = matches.items.first
if (selected == null) return
frame.goto(selected)
}
private Item[] findMatches(Str text)
{
acc := Item[,]
// integers are always line numbers
line := text.toInt(10, false)
file := frame.curFile
if (line != null && file != null)
return [FileItem.makeFile(file).setDis("Line $line").setLoc(ItemLoc{it.line = line-1})]
acc.addAll(frame.curSpace.findGotoMatches(text))
return acc
}
}
internal class GotoMatchModel : TableModel
{
Font? itemFont
Int width
Item[] items := Item[,]
new make(|This| f) {f(this)}
override Int numRows() { items.size }
override Int numCols() { 1 }
override Str header(Int col) { "" }
override Str text(Int col, Int row) { items[row].dis }
override Image? image(Int col, Int row) { items[row].icon }
override Font? font(Int col, Int row) { itemFont }
override Int? prefWidth(Int col) { width }
}
**************************************************************************
** FindCmd / Repace
**************************************************************************
const class FindCmd : Cmd
{
new make(|This| f) {f(this)}
override const Str name := "Find"
override Void invoke(Event event)
{
f := frame.curFile
if (f != null) find(f)
}
Void find(File file)
{
prompt := Text { }
path := Text { text = file.osPath }
matchCase := Button { mode = ButtonMode.check; text = "Match case"; selected = lastMatchCase.val }
replace := Button { mode = ButtonMode.check; text = "Replace (with preview)"; selected = false }
selection := frame.curView?.curSelection ?: ""
if (!selection.isEmpty && !selection.contains("\n"))
prompt.text = selection.trim
else
prompt.text = lastStr.val
pane := GridPane
{
numCols = 2
expandCol = 1
halignCells = Halign.fill
Label { text="Find" },
ConstraintPane { minw=300; maxw=300; add(prompt) },
Label { text="File" },
ConstraintPane { minw=300; maxw=300; add(path) },
matchCase,
replace,
}
dlg := Dialog(frame)
{
title = "Find"
body = pane
commands = [Dialog.ok, Dialog.cancel]
}
prompt.onAction.add |->| { dlg.close(Dialog.ok) }
if (Dialog.ok != dlg.open) return
// get and save text to search for
str := prompt.text
lastStr.val = str
lastMatchCase.val = matchCase.selected
// find all matches
matches := Item[,]
if (!matchCase.selected) str = str.lower
findMatches(matches, File.os(path.text), str, matchCase.selected)
if (matches.isEmpty) { Dialog.openInfo(frame, "No matches: $str.toCode"); return }
if(replace.selected)
{
// deal with replace
replaceAll(str, matches)
}
else
{
// show results in console
console.show(matches)
frame.goto(matches.first)
}
}
** Replace dialog & action on matches
Void replaceAll(Str search, Item[] items)
{
font := ((Sys)Service.find(Sys#)).theme.font
matches := GotoMatchModel { itemFont = font; width = 800;}
matches.items = items
table := Table
{
it.headerVisible = false
it.model = matches
multi = true
selected = (0 .. items.size-1).toList // select all
}
newText := Text {it.text = search}
dialog := Dialog(frame)
{
title = "Replace All"
commands = [ok, cancel]
body = EdgePane
{
top = InsetPane(0, 0, 10, 0)
{
GridPane
{
numCols = 2
Label{it.text = "Replace with:"},
newText,
},
}
bottom = ConstraintPane
{
minw = maxw = matches.width+10
minh = maxh = 500
table,
}
}
}
// open dialog
if (dialog.open != Dialog.ok) return
// do replace
selectedItems := items.findAll |item, index| {table.selected.contains(index)}
FileUtil.replaceAll(selectedItems, search, newText.text, "\n")
}
Void findMatches(FileItem[] matches, File f, Str str, Bool matchCase)
{
if(! f.exists) return
// recurse dirs
if (f.isDir)
{
if (f.name.startsWith(".")) return
if (f.name == "tmp" || f.name == "temp") return
f.list.each |x| { findMatches(matches, x, str, matchCase) }
return
}
if ( ! FileUtils.isTextFile(f, 10000000)) return
try
{
f.readAllLines.each |line, linei|
{
chars := matchCase ? line : line.lower
col := chars.index(str)
while (col != null)
{
span := Span(linei, col, linei, col+str.size)
dis := "$f.name(${linei+1}) [${col+1}-${col+1+str.size}]: $line.trim"
matches.add(FileItem.makeFile(f).setDis(dis).setLoc(
ItemLoc{it.line = linei; it.col = col; it.span = span}).setIcon(
Sys.cur.theme.iconMark))
col = chars.index(str, col+str.size)
}
}
}
catch(Err e){}
}
const AtomicRef lastStr := AtomicRef("")
const AtomicBool lastMatchCase:= AtomicBool(true)
}
**************************************************************************
** FindInSpaceCmd
**************************************************************************
internal const class FindInSpaceCmd : Cmd
{
new make(|This| f) {f(this)}
override const Str name := "Find in Space"
override Void invoke(Event event)
{
File? dir
cs := frame.curSpace
dir = cs.root
if (dir != null) ((FindCmd)Sys.cur.commands.find).find(dir)
}
}