Files

246 lines
5.6 KiB
Go

package vcsdiff
import (
"aego/core/ids"
"path"
"strconv"
"aego/asset/pipeline"
"aego/core/mathx"
"aego/editor/core/context"
"aego/editor/core/scenediff"
"aego/editor/core/services"
"aego/editor/core/tasks"
"aego/editor/core/vcs"
"aego/ui/core"
"aego/ui/widgets"
)
const ID = "structural_diff"
type Deps struct {
VCS vcs.Provider
Pipeline *pipeline.Pipeline
Docs services.Documents
Selection services.Selection
Notices services.Notices
Tasks *tasks.Queue
ShowHierarchy func()
}
type Panel struct {
d Deps
rel string
revs []vcs.Commit
rev int
res scenediff.Result
err string
loading *tasks.Task
scroll float32
}
func New(d Deps) *Panel {
return &Panel{d: d}
}
func (p *Panel) ID() string {
return ID
}
func (p *Panel) Title() string {
return "Diff"
}
func (p *Panel) Icon() string {
return "panel.diff"
}
func Diffable(rel string) bool {
switch path.Ext(rel) {
case ".aescene", ".aeprefab", ".aedata", ".aetable", ".aepreset":
return true
}
return false
}
func (p *Panel) Show(rel string) {
p.rel, p.rev, p.revs, p.scroll = rel, 0, nil, 0
p.load()
}
func (p *Panel) revision() string {
if p.rev > 0 && p.rev <= len(p.revs) {
return p.revs[p.rev-1].Hash
}
return "HEAD"
}
func (p *Panel) load() {
if p.d.VCS == nil || p.rel == "" || p.loading != nil {
return
}
rel, rev, abs, prov := p.rel, p.revision(), p.d.Pipeline.Abs(p.rel), p.d.VCS
needLog := p.revs == nil
var res scenediff.Result
var log []vcs.Commit
var failed string
p.err = ""
p.loading = p.d.Tasks.Work(tasks.Spec{Title: "Diff " + path.Base(rel) + " with " + shortRev(rev), Kind: "vcs"}, func(*tasks.Task) error {
if needLog {
log, _ = prov.Log(rel, 50)
}
before, err := prov.Show(rev, rel)
if err != nil {
before = nil
failed = "not in " + shortRev(rev) + ", shown as new"
}
after, err := vcs.ReadWorking(abs)
if err != nil {
return err
}
res, err = scenediff.Compute(before, after, rel)
return err
}).Then(func(t *tasks.Task) {
p.loading = nil
if t.Err() != nil {
p.err = t.Err().Error()
return
}
if needLog {
p.revs = log
}
p.res = res
if failed != "" {
p.err = failed
}
})
}
func shortRev(r string) string {
if len(r) > 8 {
return r[:8]
}
return r
}
func (p *Panel) Draw(f *services.Frame, r mathx.Rect) {
ui := f.UI
th := &ui.Theme
if p.d.VCS == nil {
widgets.LabelWrapped(ui, "The project is not inside a git repository", true)
return
}
if p.rel == "" {
widgets.LabelWrapped(ui, "Use Compare with HEAD in the menu of a scene, prefab or data asset", true)
return
}
head := ui.Cursor().Next(0)
names := []string{"HEAD"}
for _, c := range p.revs {
names = append(names, c.Short+" "+c.Subject)
}
cw := min(260, head.W*0.45)
ui.PushClip(mathx.Rect{X: head.X, Y: head.Y, W: head.W - cw - 6, H: head.H})
ui.Text(p.rel+" · working copy vs", mathx.Vec2{X: head.X, Y: ui.TextY(head)}, th.Text)
ui.PopClip()
if widgets.Combo(ui, ui.ID("rev"), mathx.Rect{X: head.X + head.W - cw, Y: head.Y, W: cw, H: head.H}, &p.rev, names, false) {
p.load()
}
if d := p.d.Docs.Documents().Find(p.rel); d != nil && d.Dirty() {
widgets.LabelDim(ui, "Unsaved changes in the open document are not included")
}
if p.loading != nil {
widgets.LabelDim(ui, "Loading…")
return
}
if p.err != "" {
widgets.LabelDim(ui, p.err)
}
if p.res.Empty() {
widgets.LabelDim(ui, "No differences")
return
}
widgets.LabelDim(ui, strconv.Itoa(len(p.res.Changes))+" change(s)")
top := ui.Cursor().Y
list := mathx.Rect{X: r.X, Y: top, W: r.W, H: max(0, r.Y+r.H-top)}
widgets.VirtualList(ui, ui.ID("rows"), list, len(p.res.Changes), th.RowHeight+2, &p.scroll, func(i int, rr mathx.Rect) {
p.row(ui, i, rr)
})
}
func kindColor(th *core.Theme, k scenediff.Kind) mathx.Color {
switch k {
case scenediff.NodeAdded, scenediff.CompAdded, scenediff.FieldAdded:
return th.AxisY
case scenediff.NodeRemoved, scenediff.CompRemoved, scenediff.FieldRemoved:
return th.Error
}
return th.Warn
}
func (p *Panel) row(ui *core.Context, i int, rr mathx.Rect) {
th := &ui.Theme
c := p.res.Changes[i]
id := core.IDSeq(ui.ID("row"), i)
if ui.Clicked(id, rr) && p.res.Scene && c.Node != "" {
p.reveal(c.Node)
}
if ui.Hot() == id {
ui.Rect(rr, th.WidgetHot)
}
ui.Rect(mathx.Rect{X: rr.X, Y: rr.Y + 2, W: 3, H: rr.H - 4}, kindColor(th, c.Kind))
where := c.Path
if c.Owner != "" {
if where != "" {
where += " · "
}
where += c.Owner
}
if c.Field != "" {
where += " · " + c.Field
}
text := c.Kind.String()
switch {
case c.Before != "" && c.After != "":
text += ": " + c.Before + " → " + c.After
case c.After != "":
text += ": " + c.After
case c.Before != "":
text += ": " + c.Before
}
ww := min(rr.W*0.45, ui.TextSize(where).X+12)
ui.PushClip(mathx.Rect{X: rr.X + 8, Y: rr.Y, W: ww, H: rr.H})
ui.Text(where, mathx.Vec2{X: rr.X + 8, Y: ui.TextY(rr)}, th.Text)
ui.PopClip()
ui.PushClip(mathx.Rect{X: rr.X + 12 + ww, Y: rr.Y, W: max(0, rr.W-ww-12), H: rr.H})
ui.Text(text, mathx.Vec2{X: rr.X + 12 + ww, Y: ui.TextY(rr)}, th.TextDim)
ui.PopClip()
ui.MouseOver(id, rr)
widgets.Tooltip(ui, id, where+"\n"+text)
}
func (p *Panel) reveal(node string) {
id, err := parseNode(node)
if err != nil {
return
}
d, err := p.d.Docs.Documents().Open(p.rel)
if err != nil {
p.d.Notices.Notify(context.LevelError, err.Error())
return
}
s := d.Scene()
if h := s.Handle(id); h.IsZero() || !s.IsAlive(h) {
p.d.Notices.Notify(context.LevelInfo, "The node is not in the working copy")
return
}
p.d.Selection.Select(id)
if p.d.ShowHierarchy != nil {
p.d.ShowHierarchy()
}
}
func parseNode(s string) (ids.NodeID, error) {
return ids.ParseNodeID(s)
}