Files

330 lines
7.7 KiB
Go

package scripteditor
import (
"path"
"sort"
"strconv"
"strings"
"aego/core/mathx"
"aego/editor/core/context"
"aego/editor/core/services"
"aego/platform/input"
"aego/script/workspace"
"aego/ui/core"
"aego/ui/widgets"
)
const (
FindID = "find"
findDelay = 0.25
findLimit = 2000
)
type findRow struct {
file bool
path string
count int
match int
}
type FindPanel struct {
host *Host
query string
repl string
opts workspace.GrepOptions
title string
usages bool
matches []workspace.Match
rows []findRow
files int
collapsed map[string]bool
truncated bool
err string
dirty bool
editedAt float64
gen uint64
running bool
focus bool
scroll float32
sel int
}
func newFindPanel(h *Host) *FindPanel {
return &FindPanel{host: h, collapsed: map[string]bool{}, sel: -1}
}
func (f *FindPanel) ID() string {
return FindID
}
func (f *FindPanel) Title() string {
if f.usages {
return "Usages"
}
return "Find"
}
func (f *FindPanel) start(q string) {
f.usages, f.title = false, ""
if q != "" {
f.query = q
}
f.focus = true
f.changed(-1)
}
func (f *FindPanel) changed(now float64) {
f.usages = false
f.dirty = true
f.editedAt = now
}
func (f *FindPanel) showUsages(title string, ms []workspace.Match) {
f.gen++
f.usages, f.title = true, title
f.running, f.dirty, f.truncated, f.err = false, false, false, ""
f.matches = ms
f.scroll, f.sel = 0, -1
f.build()
}
func (f *FindPanel) tick(now float64) {
if !f.dirty || f.usages || (f.editedAt >= 0 && now-f.editedAt < findDelay) {
return
}
f.dirty = false
f.gen++
gen := f.gen
if f.query == "" {
f.matches, f.truncated, f.err, f.running = nil, false, "", false
f.build()
return
}
q, o, ws := f.query, f.opts, f.host.ws
f.running = true
d := f.host.d
d.Submit(func() {
ms, trunc, err := ws.Grep(q, o, findLimit)
d.OnMain(func() {
if gen != f.gen {
return
}
f.running = false
f.matches, f.truncated, f.err = ms, trunc, ""
if err != nil {
f.err = err.Error()
}
f.scroll, f.sel = 0, -1
f.build()
})
})
}
func (f *FindPanel) build() {
sort.SliceStable(f.matches, func(i, j int) bool {
a, b := f.matches[i], f.matches[j]
if a.Path != b.Path {
return a.Path < b.Path
}
if a.Line != b.Line {
return a.Line < b.Line
}
return a.Col < b.Col
})
f.rows = f.rows[:0]
f.files = 0
for i := 0; i < len(f.matches); {
j := i
for j < len(f.matches) && f.matches[j].Path == f.matches[i].Path {
j++
}
p := f.matches[i].Path
f.rows = append(f.rows, findRow{file: true, path: p, count: j - i})
f.files++
if !f.collapsed[p] {
for k := i; k < j; k++ {
f.rows = append(f.rows, findRow{path: p, match: k})
}
}
i = j
}
if f.sel >= len(f.rows) {
f.sel = -1
}
}
func (f *FindPanel) status() string {
switch {
case f.err != "":
return f.err
case f.running:
return "Searching…"
case f.usages:
return f.title + ": " + strconv.Itoa(len(f.matches)) + " usage(s) in " + strconv.Itoa(f.files) + " file(s)"
case f.query == "":
return "Type to search in all scripts"
}
s := strconv.Itoa(len(f.matches)) + " match(es) in " + strconv.Itoa(f.files) + " file(s)"
if f.truncated {
s += ", showing the first " + strconv.Itoa(findLimit)
}
return s
}
func (f *FindPanel) Draw(fr *services.Frame, r mathx.Rect) {
ui := fr.UI
th := &ui.Theme
bar := ui.Cursor().Next(0)
qid := ui.ID("query")
if f.focus {
ui.SetFocus(qid)
ui.EditBuffer(qid, f.query).SelectAll()
f.focus = false
}
enter := ui.Focus() == qid && (ui.KeyPressed(input.KeyEnter) || ui.KeyPressed(input.KeyKPEnter))
tw := float32(34)
fw := max(80, bar.W-3*(tw+4))
if changed, _ := widgets.TextFieldRect(ui, qid, mathx.Rect{X: bar.X, Y: bar.Y, W: fw, H: bar.H}, &f.query, true); changed {
f.changed(ui.Time)
}
if enter {
f.changed(-1)
ui.SetFocus(qid)
}
toggles := [...]struct {
label string
tip string
on *bool
}{
{"Aa", "Match case", &f.opts.Case},
{"W", "Whole word", &f.opts.Word},
{".*", "Regular expression", &f.opts.Regexp},
}
x := bar.X + fw + 4
for i, t := range toggles {
id := core.IDSeq(qid, 10+i)
if widgets.ToggleButton(ui, id, mathx.Rect{X: x, Y: bar.Y, W: tw, H: bar.H}, t.label, t.on) {
f.changed(-1)
}
widgets.Tooltip(ui, id, t.tip)
x += tw + 4
}
rrow := ui.Cursor().Next(0)
bw := float32(100)
widgets.TextFieldRect(ui, ui.ID("repl"), mathx.Rect{X: rrow.X, Y: rrow.Y, W: max(60, rrow.W-bw-4), H: rrow.H}, &f.repl, true)
if widgets.ButtonRect(ui, ui.ID("replall"), mathx.Rect{X: rrow.X + rrow.W - bw, Y: rrow.Y, W: bw, H: rrow.H}, "Replace All") {
f.replaceAll()
}
col := th.TextDim
if f.err != "" {
col = th.Error
}
sr := ui.Cursor().Next(0)
ui.PushClip(sr)
ui.Text(f.status(), mathx.Vec2{X: sr.X, Y: ui.TextY(sr)}, col)
ui.PopClip()
top := sr.Y + sr.H + th.Spacing
list := mathx.Rect{X: r.X, Y: top, W: r.W, H: max(0, r.Y+r.H-top)}
widgets.VirtualList(ui, ui.ID("list"), list, len(f.rows), th.RowHeight, &f.scroll, func(i int, rr mathx.Rect) {
f.drawRow(ui, i, rr)
})
}
func (f *FindPanel) drawRow(ui *core.Context, i int, rr mathx.Rect) {
th := &ui.Theme
row := f.rows[i]
if row.file {
id := core.IDOf("find.file/" + row.path)
if ui.Pressed(id, rr) {
f.collapsed[row.path] = !f.collapsed[row.path]
f.build()
}
if ui.Hot() == id {
ui.Rect(rr, th.WidgetHot)
}
widgets.Chevron(ui, mathx.Rect{X: rr.X, Y: rr.Y, W: th.Indent, H: rr.H}, !f.collapsed[row.path], th.TextDim)
x := rr.X + th.Indent + 4
name := path.Base(row.path)
ui.Text(name, mathx.Vec2{X: x, Y: ui.TextY(rr)}, th.Text)
x += ui.TextSize(name).X + 10
ui.Text(path.Dir(row.path)+" ("+strconv.Itoa(row.count)+")", mathx.Vec2{X: x, Y: ui.TextY(rr)}, th.TextDim)
return
}
m := f.matches[row.match]
id := core.IDSeq(ui.ID("match"), i)
if ui.Pressed(id, rr) {
f.sel = i
f.host.Goto(context.At(m.Path, m.Line, m.Col))
}
switch {
case f.sel == i:
ui.Rect(rr, th.SelectionDim)
case ui.Hot() == id:
ui.Rect(rr, th.WidgetHot)
}
x := rr.X + th.Indent*2
pos := strconv.Itoa(m.Line) + ":" + strconv.Itoa(m.Col)
ui.Text(pos, mathx.Vec2{X: x, Y: ui.TextY(rr)}, th.TextDim)
x += max(ui.TextSize(pos).X+10, 56)
text := strings.TrimLeft(m.Text, " \t")
off := max(0, m.Offset-(len(m.Text)-len(text)))
end := min(len(text), off+m.Len)
off = min(off, end)
ui.PushClip(mathx.Rect{X: x, Y: rr.Y, W: max(0, rr.X+rr.W-x), H: rr.H})
hx := x + ui.TextSize(text[:off]).X
ui.Rect(mathx.Rect{X: hx, Y: rr.Y + 2, W: ui.TextSize(text[off:end]).X, H: rr.H - 4}, th.Code.Occurrence)
ui.Text(text, mathx.Vec2{X: x, Y: ui.TextY(rr)}, th.Text)
ui.PopClip()
}
func (f *FindPanel) replaceAll() {
if f.usages || f.query == "" || len(f.matches) == 0 {
return
}
re, err := workspace.Pattern(f.query, f.opts)
if err != nil {
f.host.d.Notices.Notify(context.LevelError, err.Error())
return
}
var paths []string
seen := map[string]bool{}
for _, m := range f.matches {
if !seen[m.Path] {
seen[m.Path] = true
paths = append(paths, m.Path)
}
}
total, files := 0, 0
for _, p := range paths {
d, err := f.host.docs().OpenScript(p)
if err != nil {
f.host.d.Notices.Notify(context.LevelError, err.Error())
continue
}
c := d.Code
src := c.Bytes()
var rs [][2]int
var texts [][]byte
for _, m := range re.FindAllSubmatchIndex(src, -1) {
if m[1] == m[0] {
continue
}
t := []byte(f.repl)
if f.opts.Regexp {
t = re.Expand(nil, []byte(f.repl), src, m)
}
rs = append(rs, [2]int{m[0], m[1]})
texts = append(texts, t)
}
if n := c.ReplaceRanges(rs, texts); n > 0 {
total += n
files++
f.host.ws.SetOverlay(d.Path, c.Bytes())
}
}
f.host.lang.Invalidate()
f.host.d.Notices.Notify(context.LevelInfo, "Replaced "+strconv.Itoa(total)+" occurrence(s) in "+strconv.Itoa(files)+" file(s), Save All writes them")
f.changed(-1)
}