Files

334 lines
8.5 KiB
Go

package references
import (
"path"
"sort"
"strconv"
"strings"
"aego/asset"
"aego/asset/pipeline"
"aego/core/ids"
"aego/core/mathx"
"aego/editor/core/context"
"aego/editor/core/refs"
"aego/editor/core/services"
"aego/editor/core/tasks"
"aego/ui/core"
"aego/ui/widgets"
)
const (
ID = "references"
MissingID = "missing_references"
CommandFind = "assets.find_references"
)
type Deps struct {
Refs *refs.Service
Pipeline *pipeline.Pipeline
Docs services.Documents
Selection services.Selection
Nav services.Navigation
Notices services.Notices
Tasks *tasks.Queue
Delete func([]ids.AssetID)
Show func(id ids.AssetID)
ShowHierarchy func()
}
func kindIcon(k asset.ArtifactKind, project bool) string {
switch {
case project:
return "cat.settings"
case k == asset.ArtScene:
return "file.scene"
case k == asset.ArtPrefab:
return "file.prefab"
case k == asset.ArtData:
return "file.data"
case k == asset.ArtTable:
return "file.table"
case k == asset.ArtPreset:
return "file.preset"
}
return "file.unknown"
}
func openPlace(d Deps, r refs.Ref, pl refs.Place) {
switch {
case r.Project:
d.Notices.Notify(context.LevelInfo, "The startup scene is set in Project Settings")
case r.Kind == asset.ArtScene || r.Kind == asset.ArtPrefab:
if _, err := d.Docs.Documents().Open(r.Path); err != nil {
d.Notices.Notify(context.LevelError, err.Error())
return
}
if !pl.Node.IsZero() {
d.Selection.Select(pl.Node)
if d.ShowHierarchy != nil {
d.ShowHierarchy()
}
}
default:
d.Nav.Open(context.Location{Path: r.Path, Offset: -1})
}
}
func pickAsset(ui *core.Context, pid core.ID, pl *pipeline.Pipeline, kind asset.ArtifactKind, q *string, skip ids.AssetID) (ids.AssetID, bool) {
if !ui.BeginPopup(pid, 360) {
return ids.AssetID{}, false
}
widgets.SearchField(ui, core.IDSeq(pid, 1), ui.Cursor().Next(0), q)
needle := strings.ToLower(strings.TrimSpace(*q))
list := pl.Assets()
sort.Slice(list, func(i, j int) bool { return list[i].Path < list[j].Path })
var got ids.AssetID
picked, n := false, 0
for _, a := range list {
if n >= 200 {
break
}
if a.ID == skip || (kind != "" && a.Kind != kind) {
continue
}
if needle != "" && !strings.Contains(strings.ToLower(a.Path), needle) {
continue
}
n++
if widgets.Selectable(ui, core.IDSeq(pid, 10+n), a.Path, false) {
got, picked = a.ID, true
ui.ClosePopup(pid)
}
}
if n == 0 {
widgets.LabelDim(ui, "No matching assets")
}
ui.EndPopup()
return got, picked
}
func runReplace(d Deps, from, to ids.AssetID, label string, done func()) {
job, err := d.Refs.Begin(from, to)
if err != nil {
d.Notices.Notify(context.LevelError, err.Error())
return
}
finish := func(err error) {
if err == nil {
err = d.Refs.Finish(job)
}
switch {
case err != nil:
d.Notices.Notify(context.LevelError, err.Error()+" (originals in "+job.Backup+")")
default:
n := len(job.Changed()) + len(job.Docs)
msg := label + ": " + strconv.Itoa(n) + " file(s) updated"
if len(job.Changed()) > 0 {
msg += ", originals in " + job.Backup
}
d.Notices.Notify(context.LevelInfo, msg)
}
if done != nil {
done()
}
}
if len(job.Files) == 0 {
finish(nil)
return
}
if d.Tasks == nil {
finish(job.Rewrite())
return
}
d.Tasks.Work(tasks.Spec{Title: label, Kind: "refs", Detail: strings.Join(job.Files, ", ")}, func(*tasks.Task) error {
return job.Rewrite()
}).Then(func(t *tasks.Task) { finish(t.Err()) })
}
type row struct {
hit int
place int
}
type Panel struct {
d Deps
target ids.AssetID
kind asset.ArtifactKind
hits []refs.Hit
rows []row
deleting []ids.AssetID
pick string
scroll float32
}
func New(d Deps) *Panel {
return &Panel{d: d}
}
func (p *Panel) ID() string {
return ID
}
func (p *Panel) Title() string {
return "References"
}
func (p *Panel) Icon() string {
return "panel.references"
}
func (p *Panel) Show(id ids.AssetID, deleting []ids.AssetID) {
p.target, p.deleting, p.scroll = id, deleting, 0
p.kind = ""
if e, ok := p.d.Pipeline.Entry(id); ok {
p.kind = e.Kind
} else if g, ok := p.d.Pipeline.Grave(id); ok {
p.kind = g.Kind
}
p.refresh()
}
func (p *Panel) refresh() {
p.hits = p.d.Refs.Find(p.target)
p.rows = p.rows[:0]
for i, h := range p.hits {
p.rows = append(p.rows, row{hit: i, place: -1})
for j := range h.Places {
p.rows = append(p.rows, row{hit: i, place: j})
}
}
}
func (p *Panel) name() string {
if rel, ok := p.d.Pipeline.PathOf(p.target); ok {
return rel
}
if g, ok := p.d.Pipeline.Grave(p.target); ok {
return g.Path + " (deleted)"
}
return p.target.String()
}
func (p *Panel) Draw(f *services.Frame, r mathx.Rect) {
ui := f.UI
th := &ui.Theme
if p.target.IsZero() {
widgets.LabelWrapped(ui, "Select an asset and press "+"Shift+F12"+" or use Find References in its menu.", true)
return
}
head := ui.Cursor().Next(0)
rw := ui.TextSize("Refresh").X + 16
ui.PushClip(mathx.Rect{X: head.X, Y: head.Y, W: head.W - rw - 4, H: head.H})
ui.Text(path.Base(p.name()), mathx.Vec2{X: head.X, Y: ui.TextY(head)}, th.Text)
ui.Text(path.Dir(p.name()), mathx.Vec2{X: head.X + ui.TextSize(path.Base(p.name())).X + 10, Y: ui.TextY(head)}, th.TextDim)
ui.PopClip()
if widgets.ButtonRect(ui, ui.ID("refresh"), mathx.Rect{X: head.X + head.W - rw, Y: head.Y, W: rw, H: head.H}, "Refresh") {
p.refresh()
}
n := len(p.hits)
if n == 0 {
widgets.LabelDim(ui, "Nothing references this asset")
} else {
widgets.LabelDim(ui, strconv.Itoa(n)+" file(s) reference it")
}
foot := th.RowHeight + th.Padding
top := ui.Cursor().Y
list := mathx.Rect{X: r.X, Y: top, W: r.W, H: max(0, r.Y+r.H-top-foot)}
widgets.VirtualList(ui, ui.ID("rows"), list, len(p.rows), th.RowHeight, &p.scroll, func(i int, rr mathx.Rect) {
p.drawRow(ui, i, rr)
})
ui.Cursor().Y = list.Y + list.H + 4
p.footer(ui)
}
func (p *Panel) drawRow(ui *core.Context, i int, rr mathx.Rect) {
th := &ui.Theme
rw := p.rows[i]
h := &p.hits[rw.hit]
id := core.IDSeq(ui.ID("row"), i)
if ui.Clicked(id, rr) {
pl := refs.Place{}
if rw.place >= 0 {
pl = h.Places[rw.place]
}
openPlace(p.d, h.Ref, pl)
}
if ui.Hot() == id {
ui.Rect(rr, th.WidgetHot)
}
ui.PushClip(rr)
if rw.place < 0 {
widgets.IconAt(ui, mathx.Rect{X: rr.X + 2, Y: rr.Y + (rr.H-16)/2, W: 16, H: 16}, kindIcon(h.Ref.Kind, h.Ref.Project), th.TextDim)
text := h.Ref.Path
if h.Open {
text += " (open)"
}
ui.Text(text, mathx.Vec2{X: rr.X + 24, Y: ui.TextY(rr)}, th.Text)
if h.Err != nil {
ui.TextIn(rr, h.Err.Error(), th.Error, core.AlignRight)
}
} else {
ui.Text(h.Places[rw.place].String(), mathx.Vec2{X: rr.X + 24 + th.Indent, Y: ui.TextY(rr)}, th.TextDim)
}
ui.PopClip()
}
func (p *Panel) footer(ui *core.Context) {
row := ui.Cursor().Next(0)
x := row.X
btn := func(id, label string, enabled bool) (bool, mathx.Rect) {
w := ui.TextSize(label).X + 18
r := mathx.Rect{X: x, Y: row.Y, W: w, H: row.H}
x += w + 6
if !enabled {
ui.TextIn(r, label, ui.Theme.TextDisabled, core.AlignCenter)
return false, r
}
return widgets.ButtonRect(ui, ui.ID(id), r, label), r
}
has := len(p.hits) > 0
pid := ui.ID("pick")
if ok, r := btn("replace", "Replace with…", has); ok {
p.pick = ""
ui.CloseAllPopups()
ui.OpenPopup(pid, mathx.Vec2{X: r.X, Y: r.Y - 300})
}
if ok, _ := btn("clear", "Clear References", has); ok {
target := p.target
p.d.Notices.Ask(context.Question{
Title: "Clear references",
Message: "Every reference to " + p.name() + " becomes None. Files that are not open are rewritten on disk; their originals go to .aego/recovery.",
Buttons: []string{"Clear", "Cancel"},
Then: func(c int) {
if c == 0 {
runReplace(p.d, target, ids.AssetID{}, "Clear references to "+path.Base(p.name()), p.refresh)
}
},
})
}
if len(p.deleting) > 0 && p.d.Delete != nil {
label := "Delete " + strconv.Itoa(len(p.deleting)) + " asset(s)"
if ok, _ := btn("delete", label, true); ok {
list := p.deleting
p.deleting = nil
p.d.Delete(list)
p.refresh()
}
}
if to, ok := pickAsset(ui, pid, p.d.Pipeline, p.kind, &p.pick, p.target); ok {
target := p.target
rel, _ := p.d.Pipeline.PathOf(to)
p.d.Notices.Ask(context.Question{
Title: "Replace references",
Message: "Replace every reference to " + p.name() + " with " + rel + ". Files that are not open are rewritten on disk; their originals go to .aego/recovery.",
Buttons: []string{"Replace", "Cancel"},
Then: func(c int) {
if c == 0 {
runReplace(p.d, target, to, "Replace references to "+path.Base(p.name()), p.refresh)
}
},
})
}
}