Documentation: engine/docs assembles one registry from the script
bindings, the schema, module docs and module-owned script paths; problems
are reported as warnings at build time. Module registries record doc
sources and script module owners; diag.Codes lists every diagnostic code.
Project bootstrap: project.defaultSpace ("2d"/"3d"), EnsureDirs creates
the default folders when the editor opens a project (reported in the
console), prefabs/ is a pipeline root. Native builds drop -trimpath so
binding locations resolve inside native/.
Editor files: create scene/prefab/script from menus, move/rename/duplicate
/delete with undo, script package retargeting on move, open scenes and
script panels follow renames, Save As / Rename Scene prompts, scene
picker, startup scene. Scoped shortcuts fall back to the global command
when disabled; Save As is Ctrl+Shift+S, Save All Ctrl+Alt+S.
Script editor: Go to Implementation (stub → native/ source), host
definitions always resolve to stubs, bracket match/mismatch states,
rainbow brackets (persisted), mono code font (ui-mono atlases, per-command
fonts in draw.List, PushFont/PopFont restored by Mark/Restore).
Tests: langsvc definition/implementation, bracket states, dispatch
fallback, script renames, font stack, editor file-op integration, golden
mono glyph and bracket references; depcheck rule for engine/docs.
Docs pipeline: engine, render2d and script/engine ship their own res/docs;
keyword hover text comes from the registry through manifest.Keywords;
project script symbols join the registry; aego docs export|check|build|
serve, --tool docs in native binaries, aego check --explain; app/docstool
and the tools/gendocs module (goldmark) build the static site.
Playground: app/playground + cmd/aego-playground (js/wasm, 3.3 MB gzip)
compile, highlight and run scripts in the browser; run blocks in the
language docs become examples. engine/builtin/headless is the module set
without render3d; workspace.Config.FS; StubFor/StubPaths; backends and the
script importer drop out of js builds.
VM: one frame pointer in the interpreter loop, direct host calls, panic
recovery once per run instead of per host call (Update2k -19%,
Schedule10k -24% against the previous head). bench-build/compare/profile
run pre-built, pinned, interleaved binaries.
Editor: structural bracket levels and current-block highlight, indent
guides, occurrence highlighting with write marks, multi-format clipboard
(copy/cut/paste assets, paste images as sprites or textures). gl33 probes
a renderable depth format and reports framebuffer failures by stage.
7 E:
- project templates empty/2d/2.5d/3d, app/projects as the single creation path (D148)
- launcher and recent projects (D149)
- orthographic Camera3D and Scene View (D150)
- reference states in the Inspector, ids.Short (D151)
- sceneio remaps node references when loading without KeepIDs (D152)
- exported ComponentRef<T> (D153)
- aego icons via module.Audit (D146)
229 lines
6.4 KiB
Go
229 lines
6.4 KiB
Go
package langsvc
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"aego/script/abi"
|
|
"aego/script/ast"
|
|
"aego/script/checker"
|
|
"aego/script/resolver"
|
|
"aego/script/types"
|
|
"aego/script/workspace"
|
|
)
|
|
|
|
func paramsText(sg *checker.ScriptSignal) string {
|
|
parts := make([]string, len(sg.Params))
|
|
for i, p := range sg.Params {
|
|
parts[i] = sg.Names[i] + " " + typeString(p)
|
|
}
|
|
return "(" + strings.Join(parts, ", ") + ")"
|
|
}
|
|
|
|
func signalDecl(sg *checker.ScriptSignal) string {
|
|
return "signal " + sg.Name + paramsText(sg)
|
|
}
|
|
|
|
func lastElem(pkg string) string {
|
|
return pkg[strings.LastIndexByte(pkg, '/')+1:]
|
|
}
|
|
|
|
func shortScript(full string) string {
|
|
return full[strings.LastIndexByte(full, '.')+1:]
|
|
}
|
|
|
|
func signalOf(a *checker.Analysis, x ast.Expr) *checker.ScriptSignal {
|
|
switch e := x.(type) {
|
|
case *ast.Ident:
|
|
if o := object(a, e); o != nil && o.Kind == resolver.ObjSignal {
|
|
sg, _ := o.Data.(*checker.ScriptSignal)
|
|
return sg
|
|
}
|
|
case *ast.SelectorExpr:
|
|
if sel := a.Selections[e]; sel != nil && sel.Kind == checker.SelSignal {
|
|
return sel.Signal
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func scriptAt(a *checker.Analysis, sd *ast.ScriptDecl) *checker.ScriptType {
|
|
if sd == nil {
|
|
return nil
|
|
}
|
|
for _, st := range a.Scripts {
|
|
if st.Decl == sd {
|
|
return st
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func sameParams(f *checker.Func, sg *checker.ScriptSignal) bool {
|
|
if len(f.Params) != len(sg.Params) {
|
|
return false
|
|
}
|
|
for i, p := range f.Params {
|
|
if p.Type != sg.Params[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func handlerCandidate(st *checker.ScriptType, f *checker.Func) bool {
|
|
return !f.Decl.Coroutine && !st.IsLifecycle(f) && f.Result == types.Void
|
|
}
|
|
|
|
func startItems(c *collector, snap *workspace.Snapshot, f *ast.File, off uint32) {
|
|
_, sd := enclosing(f, off)
|
|
st := scriptAt(snap.Analysis, sd)
|
|
if st == nil {
|
|
return
|
|
}
|
|
for _, fn := range st.Funcs {
|
|
if fn.Decl.Coroutine {
|
|
c.add(Item{Label: fn.Name, Detail: funcSig(fn), Doc: fn.Decl.Doc, Kind: ItemMethod, Insert: fn.Name + "("})
|
|
}
|
|
}
|
|
}
|
|
|
|
func yieldItems(c *collector, snap *workspace.Snapshot) {
|
|
for _, y := range []Item{
|
|
{Label: "wait", Detail: "yield wait(seconds float)", Insert: "wait(${1:seconds})"},
|
|
{Label: "frames", Detail: "yield frames(n int)", Insert: "frames(${1:n})"},
|
|
{Label: "fixed", Detail: "yield fixed(n int)", Insert: "fixed(${1:n})"},
|
|
} {
|
|
y.Kind = ItemSnippet
|
|
y.Doc = builtinText(snap, "yield "+y.Label)
|
|
c.add(y)
|
|
}
|
|
}
|
|
|
|
func signalMembers(c *collector, snap *workspace.Snapshot, sg *checker.ScriptSignal) {
|
|
c.add(Item{Label: "Emit", Detail: "Emit" + paramsText(sg), Doc: builtinText(snap, "Emit"), Kind: ItemMethod, Insert: "Emit("})
|
|
c.add(Item{Label: "Connect", Detail: "Connect(handler func" + paramsText(sg) + ") Connection", Doc: builtinText(snap, "Connect"), Kind: ItemMethod, Insert: "Connect("})
|
|
}
|
|
|
|
func scriptRefMembers(c *collector, snap *workspace.Snapshot, t *types.Type, pkg string) {
|
|
st := snap.Analysis.ScriptByName(t.Script)
|
|
if st == nil {
|
|
return
|
|
}
|
|
visible := func(name string) bool { return st.Pkg == pkg || abi.Exported(name) }
|
|
for _, f := range st.Fields {
|
|
if visible(f.Name) {
|
|
c.add(Item{Label: f.Name, Detail: scriptField(f), Doc: f.Decl.Doc, Kind: ItemField})
|
|
}
|
|
}
|
|
for _, sg := range st.Signals {
|
|
if visible(sg.Name) {
|
|
c.add(Item{Label: sg.Name, Detail: signalDecl(sg), Doc: sg.Decl.Doc, Kind: ItemSignal})
|
|
}
|
|
}
|
|
for _, fn := range st.Funcs {
|
|
if visible(fn.Name) && !st.IsLifecycle(fn) && !fn.Decl.Coroutine {
|
|
c.add(Item{Label: fn.Name, Detail: funcSig(fn), Doc: fn.Decl.Doc, Kind: ItemMethod, Insert: fn.Name + "("})
|
|
}
|
|
}
|
|
}
|
|
|
|
func handlerItems(c *collector, snap *workspace.Snapshot, f *ast.File, src []byte, dot, off uint32) {
|
|
end := dot
|
|
for end > 0 && (src[end-1] == ' ' || src[end-1] == '\t') {
|
|
end--
|
|
}
|
|
a := snap.Analysis
|
|
x := exprEndingAt(f, end)
|
|
if x == nil {
|
|
return
|
|
}
|
|
sg := signalOf(a, x)
|
|
_, sd := enclosing(f, off)
|
|
st := scriptAt(a, sd)
|
|
if sg == nil || st == nil {
|
|
return
|
|
}
|
|
for _, fn := range st.Funcs {
|
|
if !handlerCandidate(st, fn) {
|
|
continue
|
|
}
|
|
it := Item{Label: fn.Name, Detail: funcSig(fn), Doc: fn.Decl.Doc, Kind: ItemMethod}
|
|
c.rank = rankLocal
|
|
if !sameParams(fn, sg) {
|
|
c.rank = rankScript
|
|
it.Detail += " · does not match " + signalDecl(sg)
|
|
}
|
|
c.add(it)
|
|
}
|
|
}
|
|
|
|
func signalNamed(a *checker.Analysis, pkg, name string) *checker.ScriptSignal {
|
|
var found *checker.ScriptSignal
|
|
for _, st := range a.Scripts {
|
|
sg := st.Signal(name)
|
|
if sg == nil {
|
|
continue
|
|
}
|
|
if st.Pkg == pkg {
|
|
return sg
|
|
}
|
|
if found == nil {
|
|
found = sg
|
|
}
|
|
}
|
|
return found
|
|
}
|
|
|
|
func scriptMethodNamed(a *checker.Analysis, pkg, name string) *checker.Func {
|
|
var found *checker.Func
|
|
for _, st := range a.Scripts {
|
|
f := st.Methods[name]
|
|
if f == nil || st.IsLifecycle(f) || f.Decl.Coroutine {
|
|
continue
|
|
}
|
|
if st.Pkg == pkg {
|
|
return f
|
|
}
|
|
if found == nil {
|
|
found = f
|
|
}
|
|
}
|
|
return found
|
|
}
|
|
|
|
func signalSignature(snap *workspace.Snapshot, sg *checker.ScriptSignal, method string) Signature {
|
|
var sb sigBuilder
|
|
sb.b.WriteString("func (signal " + sg.Name + ") " + method + "(")
|
|
if method == "Emit" {
|
|
for i, p := range sg.Params {
|
|
sb.param(sg.Names[i] + " " + typeString(p))
|
|
}
|
|
sb.b.WriteByte(')')
|
|
} else {
|
|
sb.param("handler func" + paramsText(sg))
|
|
sb.b.WriteString(") Connection")
|
|
}
|
|
return Signature{Label: sb.b.String(), Params: sb.params, Doc: builtinText(snap, method)}
|
|
}
|
|
|
|
func asyncCallHover(snap *workspace.Snapshot, k *checker.Call) (Hover, bool) {
|
|
switch k.Kind {
|
|
case checker.CallEmit:
|
|
return Hover{Signature: "func (signal " + k.Signal.Name + ") Emit" + paramsText(k.Signal), Doc: builtinText(snap, "Emit")}, true
|
|
case checker.CallConnect:
|
|
return Hover{Signature: "func (signal " + k.Signal.Name + ") Connect(handler func" + paramsText(k.Signal) + ") Connection", Doc: builtinText(snap, "Connect")}, true
|
|
case checker.CallDisconnect:
|
|
return Hover{Signature: "func (Connection) Disconnect()", Doc: builtinText(snap, "Disconnect")}, true
|
|
case checker.CallConnected:
|
|
return Hover{Signature: "func (Connection) Connected() bool", Doc: builtinText(snap, "Connected")}, true
|
|
case checker.CallCoStop:
|
|
return Hover{Signature: "func (Coroutine) Stop()", Doc: builtinText(snap, "Stop")}, true
|
|
case checker.CallCoRunning:
|
|
return Hover{Signature: "func (Coroutine) Running() bool", Doc: builtinText(snap, "Running")}, true
|
|
case checker.CallGetScript:
|
|
s := shortScript(k.Target)
|
|
return Hover{Signature: "func (NodeRef) GetScript<" + s + ">() ScriptRef<" + s + ">", Doc: builtinText(snap, "GetScript")}, true
|
|
}
|
|
return Hover{}, false
|
|
}
|