Files

821 lines
19 KiB
Go

package scripteditor
import (
"strconv"
"strings"
"aego/core/errs"
"aego/core/ids"
"aego/editor/core/command"
"aego/editor/core/context"
"aego/editor/core/document"
"aego/editor/core/services"
edws "aego/editor/core/workspace"
"aego/platform/input"
"aego/scene"
"aego/scene/scriptcomp"
"aego/script/diag"
"aego/script/langsvc"
"aego/script/workspace"
"aego/ui/core"
"aego/ui/dock"
)
const (
Scope = "code"
historyMax = 64
countsEvery = 1.0
refreshEvery = 1.0
AutosaveDelay = 1.0
saveRetry = 5.0
)
const (
CommandFindInScripts = "code.find_in_scripts"
CommandBack = "code.back"
CommandForward = "code.forward"
CommandInlineErrors = "code.inline_errors"
CommandProblems = "code.problems"
CommandInlayHints = "code.inlay_hints"
CommandMinimap = "code.minimap"
CommandSplitRight = "code.split_right"
CommandSplitDown = "code.split_down"
CommandAutosave = "file.autosave"
CommandImplementation = "code.implementation"
CommandRainbow = "code.rainbow_brackets"
CommandHighlightBlock = "code.highlight_block"
)
func PanelID(rel string) string {
return "script:" + rel
}
type spot struct {
path string
off int
}
type Deps struct {
Docs services.Documents
Selection services.Selection
Commands *command.Registry
Notices services.Notices
Nav services.Navigation
Problems services.Problems
Scripts context.ScriptHost
Viewport *services.Viewport
Now func() float64
Submit func(func())
OnMain func(func())
}
type autosaveMark struct {
rev uint64
at float64
}
type Host struct {
lang *langsvc.Service
ws *workspace.Workspace
d Deps
dock *edws.Workspace
panels map[string]*Panel
clones map[string]*Panel
nextClone int
active *Panel
find *FindPanel
queue []func()
checkAt float64
refreshAt float64
refreshing bool
back []spot
fwd []spot
live []context.ScriptProblem
liveRev uint64
liveSnap *workspace.Snapshot
symSnap *workspace.Snapshot
syms []context.SearchItem
counts map[string]int
countsAt float64
edits map[*document.ScriptDocument]autosaveMark
zen string
Inline bool
Hints bool
Minimap bool
Rainbow bool
Block bool
Autosave bool
Virtual func(path string) (*document.ScriptDocument, error)
}
func NewHost(lang *langsvc.Service) *Host {
h := &Host{
lang: lang,
ws: lang.Workspace(),
panels: map[string]*Panel{},
clones: map[string]*Panel{},
counts: map[string]int{},
edits: map[*document.ScriptDocument]autosaveMark{},
Inline: true,
Hints: true,
Minimap: true,
Rainbow: true,
Block: true,
Autosave: true,
}
h.find = newFindPanel(h)
h.Virtual = h.virtual
return h
}
func (h *Host) docs() *document.Manager {
return h.d.Docs.Documents()
}
func (h *Host) Attach(d Deps, w *edws.Workspace) {
h.d, h.dock = d, w
if err := w.Register(edws.Desc{Panel: h.find, Near: edws.Console, Order: 240}); err != nil {
h.d.Notices.Notify(context.LevelError, err.Error())
}
h.registerCommands()
h.registerZen()
for _, rec := range h.docs().FindScriptRecovery() {
doc, err := h.docs().RestoreScriptRecovery(rec)
if err != nil {
h.d.Notices.Notify(context.LevelWarn, err.Error())
continue
}
h.show(doc)
h.d.Notices.Notify(context.LevelInfo, "Restored unsaved changes of "+doc.Path)
}
}
func (h *Host) toggle(v *bool) func() error {
return func() error {
*v = !*v
return nil
}
}
func (h *Host) registerCommands() {
hasActive := func() bool { return h.active != nil }
list := []command.Desc{
{ID: CommandFindInScripts, Name: "Find in Scripts", Category: "Scripts", Order: 100,
Shortcut: command.Key(input.KeyF, command.ModPrimary|command.ModShift),
Run: func() error {
h.FindInScripts(h.query())
return nil
}},
{ID: CommandBack, Name: "Back", Category: "Scripts", Order: 200,
Shortcut: command.Key(input.KeyLeft, command.ModPrimary|command.ModAlt),
Enabled: func() bool { return len(h.back) > 0 },
Run: func() error {
h.Back()
return nil
}},
{ID: CommandForward, Name: "Forward", Category: "Scripts", Order: 210,
Shortcut: command.Key(input.KeyRight, command.ModPrimary|command.ModAlt),
Enabled: func() bool { return len(h.fwd) > 0 },
Run: func() error {
h.Forward()
return nil
}},
{ID: CommandImplementation, Name: "Go to Implementation", Category: "Scripts", Order: 220,
Shortcut: command.Key(input.KeyB, command.ModPrimary|command.ModAlt), Enabled: hasActive,
Run: func() error {
p := h.active
h.queue = append(h.queue, func() {
if h.owns(p) {
p.implementation(h.d.Viewport.UI)
}
})
return nil
}},
{ID: CommandProblems, Name: "Problems", Category: "Scripts", Order: 300,
Run: func() error {
h.d.Nav.Open(context.ProblemsAt(""))
return nil
}},
{ID: CommandSplitRight, Name: "Split Right", Category: "Scripts", Order: 350, Enabled: hasActive,
Run: func() error {
h.split(h.active, false)
return nil
}},
{ID: CommandSplitDown, Name: "Split Down", Category: "Scripts", Order: 360, Enabled: hasActive,
Run: func() error {
h.split(h.active, true)
return nil
}},
{ID: CommandInlineErrors, Name: "Inline Error Messages", Category: "Scripts", Order: 400,
Checked: func() bool { return h.Inline }, Run: h.toggle(&h.Inline)},
{ID: CommandInlayHints, Name: "Parameter Name Hints", Category: "Scripts", Order: 410,
Checked: func() bool { return h.Hints }, Run: h.toggle(&h.Hints)},
{ID: CommandMinimap, Name: "Minimap", Category: "Scripts", Order: 420,
Checked: func() bool { return h.Minimap }, Run: h.toggle(&h.Minimap)},
{ID: CommandRainbow, Name: "Rainbow Brackets", Category: "Scripts", Order: 430,
Checked: func() bool { return h.Rainbow }, Run: h.toggle(&h.Rainbow)},
{ID: CommandHighlightBlock, Name: "Highlight Current Block", Category: "Scripts", Order: 440,
Checked: func() bool { return h.Block }, Run: h.toggle(&h.Block)},
{ID: CommandAutosave, Name: "Autosave Scripts", Category: "File", Order: 250,
Checked: func() bool { return h.Autosave }, Run: h.toggle(&h.Autosave)},
}
for _, d := range list {
if err := h.d.Commands.Register(d); err != nil {
h.d.Notices.Notify(context.LevelError, err.Error())
}
}
}
func (h *Host) Open(rel string, line int) {
h.Goto(context.At(rel, line, 1))
}
func (h *Host) OpenAt(rel string, off int) {
h.Goto(context.AtOffset(rel, off))
}
func (h *Host) Goto(l context.Location) {
h.queue = append(h.queue, func() { h.goTo(l, true) })
}
func (h *Host) document(p string) (*document.ScriptDocument, error) {
if pn := h.panels[p]; pn != nil {
return pn.doc, nil
}
if strings.HasPrefix(p, context.StubPrefix) || strings.HasPrefix(p, context.GoPrefix) {
if h.Virtual == nil {
return nil, errs.New(errs.EditorDocument, "library sources are not available")
}
return h.Virtual(p)
}
return h.docs().OpenScript(document.Clean(p))
}
func (h *Host) goTo(l context.Location, record bool) {
if h.dock == nil {
return
}
d, err := h.document(l.Path)
if err != nil {
h.d.Notices.Notify(context.LevelError, err.Error())
return
}
if record {
h.mark()
}
p := h.show(d)
c := d.Code
p.restoreSel()
jump := true
switch {
case l.Offset >= 0:
c.SetCursor(l.Offset, false)
case l.Line > 0:
c.SetCursor(c.Offset(l.Line-1, max(l.Col-1, 0)), false)
default:
jump = false
}
p.saveSel()
p.wantFocus = true
if jump {
p.reveal = true
p.flashLine, p.flashAt = c.Line(c.Cursor()), h.d.Now()
}
h.active = p
}
func (h *Host) byID(id string) *Panel {
if p := h.clones[id]; p != nil {
return p
}
for _, p := range h.panels {
if p.ID() == id {
return p
}
}
return nil
}
func (h *Host) query() string {
if h.owns(h.active) {
return h.active.selectionQuery()
}
return ""
}
func (h *Host) owns(p *Panel) bool {
return p != nil && (h.panels[p.doc.Path] == p || h.clones[p.ID()] == p)
}
func (h *Host) current() (spot, bool) {
p := h.active
if !h.owns(p) {
return spot{}, false
}
return spot{path: p.doc.Path, off: p.cursor()}, true
}
func (h *Host) mark() {
s, ok := h.current()
if !ok {
return
}
if n := len(h.back); n > 0 && h.back[n-1] == s {
return
}
h.back = append(h.back, s)
if len(h.back) > historyMax {
h.back = append(h.back[:0], h.back[1:]...)
}
h.fwd = h.fwd[:0]
}
func (h *Host) step(from, to *[]spot) {
n := len(*from)
if n == 0 {
return
}
s := (*from)[n-1]
*from = (*from)[:n-1]
if cur, ok := h.current(); ok {
*to = append(*to, cur)
}
h.goTo(context.AtOffset(s.path, s.off), false)
}
func (h *Host) Back() {
h.queue = append(h.queue, func() { h.step(&h.back, &h.fwd) })
}
func (h *Host) Forward() {
h.queue = append(h.queue, func() { h.step(&h.fwd, &h.back) })
}
func (h *Host) show(d *document.ScriptDocument) *Panel {
if p := h.panels[d.Path]; p != nil {
h.dock.Show(p.ID())
return p
}
p := newPanel(h, d)
if err := h.dock.Register(edws.Desc{Panel: p, Hidden: true}); err != nil {
h.d.Notices.Notify(context.LevelError, err.Error())
return p
}
h.panels[d.Path] = p
if target := h.dock.Tree.FindPanel(edws.Scene); target != 0 {
h.dock.Tree.DockPanel(p.ID(), target, dock.SideCenter)
}
h.dock.Show(p.ID())
return p
}
func (h *Host) split(p *Panel, down bool) {
if p == nil {
return
}
h.queue = append(h.queue, func() {
if !h.owns(p) {
return
}
h.nextClone++
c := newPanel(h, p.doc)
c.clone, c.idx = true, h.nextClone
c.fid = core.IDOf("code/" + c.ID())
c.own, c.ownRev, c.ownSet = p.own, p.ownRev, p.ownSet
c.scrollY = p.scrollY
if err := h.dock.Register(edws.Desc{Panel: c, Hidden: true}); err != nil {
h.d.Notices.Notify(context.LevelError, err.Error())
return
}
h.clones[c.ID()] = c
side := dock.SideRight
if down {
side = dock.SideBottom
}
h.dock.Tree.DockPanel(c.ID(), h.dock.Tree.FindPanel(p.ID()), side)
h.dock.Show(c.ID())
c.wantFocus = true
})
}
func (h *Host) requestClose(p *Panel) {
h.queue = append(h.queue, func() {
if p.clone || p.doc.ReadOnly || !p.doc.Dirty() {
h.close(p)
return
}
h.d.Notices.Ask(context.Question{
Title: "Unsaved script",
Message: p.doc.Path + " has unsaved changes.",
Buttons: []string{"Save", "Discard", "Cancel"},
Then: func(choice int) {
switch choice {
case 0:
if err := h.docs().SaveScript(p.doc); err != nil {
h.d.Notices.Notify(context.LevelError, err.Error())
return
}
h.queue = append(h.queue, func() { h.close(p) })
case 1:
h.queue = append(h.queue, func() { h.close(p) })
}
},
})
})
}
func (h *Host) drop(p *Panel) {
h.dock.Unregister(p.ID())
p.dispose()
if h.active == p {
h.active = nil
}
}
func (h *Host) close(p *Panel) {
if p.clone {
if h.clones[p.ID()] == p {
delete(h.clones, p.ID())
h.drop(p)
}
return
}
if h.panels[p.doc.Path] != p {
return
}
for id, c := range h.clones {
if c.doc == p.doc {
delete(h.clones, id)
h.drop(c)
}
}
delete(h.panels, p.doc.Path)
delete(h.edits, p.doc)
h.drop(p)
if !p.doc.ReadOnly {
h.docs().CloseScript(p.doc)
h.ws.ClearOverlay(p.doc.Path)
h.lang.Invalidate()
}
if len(h.panels) == 0 {
h.live, h.liveSnap = nil, nil
h.liveRev++
}
}
func (h *Host) eachView(d *document.ScriptDocument, fn func(*Panel)) {
if p := h.panels[d.Path]; p != nil {
fn(p)
}
for _, c := range h.clones {
if c.doc == d {
fn(c)
}
}
}
func (h *Host) Tick(now float64) {
if h.dock == nil {
return
}
q := h.queue
h.queue = nil
for _, fn := range q {
fn()
}
h.find.tick(now)
h.refreshCounts(now)
h.refresh(now)
h.autosave(now)
if now < h.checkAt {
return
}
h.checkAt = now + 1
reloaded, conflicts := h.docs().CheckExternalScripts()
for _, d := range reloaded {
h.eachView(d, func(p *Panel) { p.resync() })
h.d.Notices.Notify(context.LevelInfo, "Reloaded "+d.Path)
}
for _, d := range conflicts {
h.eachView(d, func(p *Panel) { p.conflict = true })
}
}
func (h *Host) refresh(now float64) {
if h.refreshing || now < h.refreshAt {
return
}
h.refreshAt = now + refreshEvery
if s := h.lang.Latest(); s != nil && s.Gen == h.ws.Version() {
return
}
h.refreshing = true
h.d.Submit(func() {
h.lang.Refresh()
h.d.OnMain(func() { h.refreshing = false })
})
}
func (h *Host) buildStatus() (string, bool) {
if h.d.Scripts == nil {
return "", false
}
gen, failed := h.d.Scripts.Status()
n := 0
ps, _ := h.d.Scripts.Problems()
for _, pr := range ps {
if pr.Error {
n++
}
}
s := "Scripts: build " + strconv.FormatUint(gen, 10)
switch {
case n > 0:
return s + " · " + strconv.Itoa(n) + " errors", true
case failed:
return s + " · latest failed", true
}
return s + " · ok", false
}
func (h *Host) ensure() {
if h.lang.Latest() == nil {
h.lang.Refresh()
}
}
func (h *Host) blocked(d *document.ScriptDocument) bool {
p := h.panels[d.Path]
return d.ReadOnly || (p != nil && p.conflict)
}
func (h *Host) saveDoc(d *document.ScriptDocument, now float64) bool {
if err := h.docs().SaveScript(d); err != nil {
h.d.Notices.Notify(context.LevelError, "autosave failed: "+err.Error())
h.edits[d] = autosaveMark{rev: d.Code.Rev(), at: now + saveRetry}
return false
}
delete(h.edits, d)
return true
}
func (h *Host) autosave(now float64) {
if !h.Autosave {
return
}
for _, d := range h.docs().Scripts() {
if !d.Dirty() || h.blocked(d) {
delete(h.edits, d)
continue
}
rev := d.Code.Rev()
m, ok := h.edits[d]
if !ok || m.rev != rev {
h.edits[d] = autosaveMark{rev: rev, at: now}
continue
}
if now-m.at >= AutosaveDelay {
h.saveDoc(d, now)
}
}
}
func (h *Host) FlushAutosave() int {
if h.dock == nil || !h.Autosave {
return 0
}
now, n := h.d.Now(), 0
for _, d := range h.docs().Scripts() {
if d.Dirty() && !h.blocked(d) && h.saveDoc(d, now) {
n++
}
}
return n
}
func (h *Host) Caret(id string) (int, int, bool) {
p := h.byID(id)
if p == nil {
return 0, 0, false
}
l, c := p.doc.Code.LineCol(p.cursor())
return l + 1, c + 1, true
}
func (h *Host) setLive(snap *workspace.Snapshot) {
if snap == nil || snap == h.liveSnap || (h.liveSnap != nil && snap.Gen < h.liveSnap.Gen) {
return
}
h.liveSnap = snap
h.live = h.live[:0]
sm := snap.Sources
for _, d := range snap.Diagnostics {
if d.Severity == diag.Info || !d.Primary.Valid() {
continue
}
line, col := sm.LineCol(d.Primary.File, d.Primary.Start)
h.live = append(h.live, context.ScriptProblem{
Package: snap.Packages[d.Primary.File],
Path: sm.Path(d.Primary.File),
Line: line,
Col: col,
Code: d.Code,
Error: d.Severity == diag.Error,
Message: d.Message,
Help: diag.HelpText(d.Code),
})
}
h.liveRev++
}
func (h *Host) LiveProblems() ([]context.ScriptProblem, uint64) {
if len(h.panels) == 0 || h.liveSnap == nil {
return nil, 0
}
return h.live, h.liveRev
}
func (h *Host) toContext(l langsvc.Location, snap *workspace.Snapshot) context.Location {
if p := h.panels[l.Path]; p != nil && snap != nil && p.snap == snap {
if s, _, ok := p.doc.Code.MapSpan(p.snapRev, int(l.Span.Start), int(l.Span.End)); ok {
return context.AtOffset(l.Path, s)
}
}
if snap != nil && l.Span.Valid() {
line, col := snap.Sources.LineCol(l.Span.File, l.Span.Start)
return context.At(l.Path, line, col)
}
return context.AtOffset(l.Path, int(l.Span.Start))
}
func (h *Host) Declaration(typeName, field string) (context.Location, bool) {
h.ensure()
l, ok := h.lang.ScriptDeclaration(typeName, field)
if !ok {
return context.Location{}, false
}
return h.toContext(l, h.lang.Latest()), true
}
func (h *Host) FindUsages(typeName, field string) {
h.ensure()
locs, snap := h.lang.ScriptUsages(typeName, field)
title := typeName[strings.LastIndexByte(typeName, '.')+1:]
if field != "" {
title += "." + field
}
h.showUsages(title, locs, snap)
}
func (h *Host) showUsages(title string, locs []langsvc.Location, snap *workspace.Snapshot) {
var ms []workspace.Match
if snap != nil {
sm := snap.Sources
for _, l := range locs {
line, col := sm.LineCol(l.Span.File, l.Span.Start)
ls := sm.Offset(l.Span.File, line, 1)
ms = append(ms, workspace.Match{
Path: l.Path,
Line: line,
Col: col,
Offset: int(l.Span.Start - ls),
Len: int(l.Span.Len()),
Text: sm.Line(l.Span.File, line),
})
}
}
h.find.showUsages("Usages of "+title, ms)
h.dock.Show(FindID)
}
func (h *Host) FindInScripts(q string) {
h.find.start(q)
h.dock.Show(FindID)
}
func (h *Host) Symbols() []context.SearchItem {
h.ensure()
snap := h.lang.Latest()
if snap == nil || snap == h.symSnap {
return h.syms
}
h.symSnap = snap
h.syms = h.syms[:0]
for _, s := range h.lang.WorkspaceSymbols() {
detail := s.Path
if s.Container != "" {
detail = s.Container + " · " + s.Path
}
h.syms = append(h.syms, context.SearchItem{
Label: s.Name,
Detail: detail,
Kind: s.Kind.String(),
Loc: context.At(s.Path, s.Line, s.Col),
})
}
return h.syms
}
func (h *Host) refreshCounts(now float64) {
if now < h.countsAt {
return
}
h.countsAt = now + countsEvery
clear(h.counts)
for _, d := range h.docs().Docs() {
scene.Store[scriptcomp.ScriptComponent](d.Scene()).EachAll(func(_ scene.NodeHandle, sc *scriptcomp.ScriptComponent) {
for i := range sc.Attachments {
h.counts[sc.Attachments[i].TypeName]++
}
})
}
}
func attached(d *document.SceneDocument, full string) []ids.NodeID {
s := d.Scene()
var out []ids.NodeID
scene.Store[scriptcomp.ScriptComponent](s).EachAll(func(n scene.NodeHandle, sc *scriptcomp.ScriptComponent) {
if sc.Index(full) >= 0 && s.IsAlive(n) {
out = append(out, s.ID(n))
}
})
return out
}
func (h *Host) selectAttached(full string) {
docs := h.docs()
var target *document.SceneDocument
var nodes []ids.NodeID
if d := docs.Edit(); d != nil {
if n := attached(d, full); len(n) > 0 {
target, nodes = d, n
}
}
if target == nil {
for _, d := range docs.Docs() {
if n := attached(d, full); len(n) > 0 {
target, nodes = d, n
break
}
}
}
if target == nil {
h.d.Notices.Notify(context.LevelInfo, "No open scene uses "+full)
return
}
docs.SetActive(target.ID)
h.d.Selection.Select(nodes...)
h.dock.Show(edws.Hierarchy)
}
func (h *Host) Renamed(oldRel, newRel string) {
moved := func(p string) (string, bool) {
switch {
case p == oldRel:
return newRel, true
case strings.HasPrefix(p, oldRel+"/"):
return newRel + strings.TrimPrefix(p, oldRel), true
}
return "", false
}
type move struct {
from, to string
p *Panel
}
var list []move
for path, pn := range h.panels {
if np, ok := moved(path); ok {
list = append(list, move{path, np, pn})
}
}
for _, mv := range list {
delete(h.panels, mv.from)
h.panels[mv.to] = mv.p
h.ws.ClearOverlay(mv.from)
mv.p.resync()
}
for _, s := range [][]spot{h.back, h.fwd} {
for i := range s {
if np, ok := moved(s[i].path); ok {
s[i].path = np
}
}
}
if len(list) > 0 {
h.lang.Invalidate()
}
}
func (h *Host) GotoSymbol(stub, symbol string) {
h.queue = append(h.queue, func() {
h.ensure()
line := 1
if st, ok := h.lang.Stub(stub); ok {
if l, ok := st.Lines[symbol]; ok {
line = l + 1
}
}
h.goTo(context.At(stub, line, 1), true)
})
}