Files
sanspie 5eb1832907
ci-platforms / darwin amd64 smoke (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 15m28s
ci / release (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / checks (push) Successful in 7m47s
Complete Stage 12 for v0.12.0
2026-11-05 11:46:53 +03:00

166 lines
5.0 KiB
Go

package agentbridge
import (
"bytes"
"errors"
"os"
"path/filepath"
"strings"
"aego/app/agent"
"aego/editor/core/assetops"
"aego/project"
)
// scriptUndo is an applied script edit: the open buffer's revision after it, or the text on disk.
type scriptUndo struct {
rel string
rev uint64
open bool
created bool
before, after string
assets *assetops.Ops
}
func (b *Bridge) ScriptText(rel string) (string, bool) {
d := b.ctx.Docs.Script(rel)
if d == nil {
return "", false
}
return string(d.Code.Bytes()), true
}
// ApplyScript edits an open script in its buffer, one Undo step of the code editor that the user saves; a closed one on disk.
func (b *Bridge) ApplyScript(e agent.ScriptEdit, label string) error {
if strings.HasSuffix(e.Rel, ".meta") && b.ctx.Pipeline != nil {
if id, ok := b.ctx.Pipeline.Lookup(strings.TrimSuffix(e.Rel, ".meta")); ok {
owner, err := b.ctx.DocumentForAsset(id)
if err != nil {
return err
}
if owner.Metadata == nil || owner.Metadata.Path != e.Rel {
return errors.New(e.Rel + " has no editable metadata document")
}
if e.New {
return errors.New(e.Rel + " already belongs to an imported asset")
}
}
}
if e.New {
if ext := filepath.Ext(e.Rel); ext == ".aemat" || ext == ".aeshader" {
ops := &assetops.Ops{}
if _, err := ops.CreateFile(b.ctx, e.Rel, []byte(e.After)); err != nil {
return err
}
b.last = &undoRef{label: label, script: &scriptUndo{rel: e.Rel, created: true, after: e.After, assets: ops}}
return nil
}
abs := filepath.Join(b.ctx.Project.Root, filepath.FromSlash(e.Rel))
if _, err := os.Stat(abs); err == nil {
return errors.New(e.Rel + " exists; change it with script.edit")
}
if err := os.MkdirAll(filepath.Dir(abs), 0o755); err != nil {
return err
}
if err := project.WriteAtomic(abs, []byte(e.After)); err != nil {
return err
}
b.last = &undoRef{label: label, script: &scriptUndo{rel: e.Rel, created: true, after: e.After}}
b.Wrote([]string{e.Rel})
return nil
}
if d := b.ctx.Docs.Script(e.Rel); d != nil {
if d.ReadOnly || d.Conflict {
return errors.New(e.Rel + " is read-only or has an unresolved external change")
}
if string(d.Code.Bytes()) != e.Before {
return errors.New(e.Rel + " changed since the preview; read it again with script.read")
}
lo, oldHi, newHi := changed(e.Before, e.After)
d.Code.ReplaceRanges([][2]int{{lo, oldHi}}, [][]byte{[]byte(e.After[lo:newHi])})
b.last = &undoRef{label: label, script: &scriptUndo{rel: e.Rel, rev: d.Code.Rev(), open: true, before: e.Before, after: e.After}}
return nil
}
abs := filepath.Join(b.ctx.Project.Root, filepath.FromSlash(e.Rel))
disk, err := os.ReadFile(abs)
if err != nil {
return err
}
if string(disk) != e.Before {
return errors.New(e.Rel + " changed since the preview; read it again with script.read")
}
if err := project.WriteAtomic(abs, []byte(e.After)); err != nil {
return err
}
b.last = &undoRef{label: label, script: &scriptUndo{rel: e.Rel, before: e.Before, after: e.After}}
b.Wrote([]string{e.Rel})
return nil
}
// changed is the range of a that differs from b: a[lo:aHi] became b[lo:bHi].
func changed(a, b string) (lo, aHi, bHi int) {
for lo < len(a) && lo < len(b) && a[lo] == b[lo] {
lo++
}
aHi, bHi = len(a), len(b)
for aHi > lo && bHi > lo && a[aHi-1] == b[bHi-1] {
aHi--
bHi--
}
return lo, aHi, bHi
}
func (b *Bridge) canUndoScript(u *scriptUndo) bool {
if d := b.ctx.Docs.Script(u.rel); d != nil {
if d.ReadOnly || d.Conflict {
return false
}
if u.open {
return d.Code.Rev() == u.rev && d.Code.CanUndo()
}
return string(d.Code.Bytes()) == u.after
}
disk, err := os.ReadFile(filepath.Join(b.ctx.Project.Root, filepath.FromSlash(u.rel)))
return err == nil && bytes.Equal(disk, []byte(u.after))
}
func (b *Bridge) undoScript(u *scriptUndo) error {
if u.created {
if d := b.ctx.Docs.Script(u.rel); d != nil && d.Dirty() {
return errors.New(u.rel + " has unsaved changes; close it first")
}
if u.assets != nil {
return u.assets.Undo(b.ctx)
}
if err := os.Remove(filepath.Join(b.ctx.Project.Root, filepath.FromSlash(u.rel))); err != nil {
return err
}
b.Wrote([]string{u.rel})
return nil
}
if d := b.ctx.Docs.Script(u.rel); d != nil {
if d.ReadOnly || d.Conflict {
return errors.New(u.rel + " is read-only or has an unresolved external change")
}
if u.open {
if d.ExplicitSave && b.ctx.Pipeline != nil && strings.HasSuffix(u.rel, ".meta") {
if id, ok := b.ctx.Pipeline.Lookup(strings.TrimSuffix(u.rel, ".meta")); ok {
if owner := b.ctx.Docs.AssetDocument(id); owner != nil && owner.Metadata == d {
return b.ctx.UndoAsset(owner)
}
}
}
d.Code.Undo()
return nil
}
lo, oldHi, newHi := changed(u.after, u.before)
d.Code.ReplaceRanges([][2]int{{lo, oldHi}}, [][]byte{[]byte(u.before[lo:newHi])})
return nil
}
if err := project.WriteAtomic(filepath.Join(b.ctx.Project.Root, filepath.FromSlash(u.rel)), []byte(u.before)); err != nil {
return err
}
b.Wrote([]string{u.rel})
return nil
}