@defold-typescript/tstl-plugin 0.29.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,26 +1,26 @@
1
1
  import {
2
+ ANIMATION_ASSET_EXTENSIONS,
3
+ buildComponentAnimationIndex,
2
4
  buildConfigKeyIndex,
5
+ buildGuiFlipbookIndex,
3
6
  buildGuiNodeIndex,
4
7
  buildInputActionIndex,
5
8
  buildSceneComponentIndex,
6
9
  buildSceneObjectPathIndex,
7
- buildSpriteAnimationIndex,
8
10
  type ClassifiedSlot,
9
11
  componentIdOfSameObjectAddress,
10
12
  computeOutputRel,
11
- isAddressClass,
12
- isFragmentCaret,
13
- } from "@defold-typescript/transpiler";
14
- import { readBuildConfigFromHost } from "./build-config";
15
- import {
16
- ANIMATION_ASSET_EXTENSIONS,
17
13
  displayPathOf,
18
14
  GAME_PROJECT_DOCUMENT,
19
15
  GUI_EXTENSIONS,
20
16
  INPUT_BINDING_EXTENSIONS,
17
+ isAddressClass,
18
+ isFragmentCaret,
21
19
  PROJECT_EXTENSIONS,
22
- } from "./scene-documents";
23
- import type { SceneIndexCache } from "./scene-index-cache";
20
+ } from "@defold-typescript/transpiler";
21
+ import { readBuildConfigFromHost } from "./build-config";
22
+ import { type SceneIndexCache, sceneCollectionRolesOf } from "./scene-index-cache";
23
+ import { relativeUniverseFor } from "./scene-relative-addresses";
24
24
 
25
25
  // Keys of their own under the same cache the completion indexes use, so a
26
26
  // provenance map is thrown away with every index it was derived beside — and
@@ -29,7 +29,8 @@ const COMPONENT_PROVENANCE = "provenance:component-ids";
29
29
  const ACTION_PROVENANCE = "provenance:input-actions";
30
30
  const GUI_PROVENANCE = "provenance:gui-nodes";
31
31
  const PATH_PROVENANCE = "provenance:object-paths";
32
- const ANIMATION_PROVENANCE = "provenance:sprite-animations";
32
+ const ANIMATION_PROVENANCE = "provenance:component-animations";
33
+ const GUI_ANIMATION_PROVENANCE = "provenance:gui-flipbook";
33
34
 
34
35
  type Provenance = ReadonlyMap<string, readonly string[]>;
35
36
 
@@ -125,16 +126,48 @@ function nodeProvenance(cache: SceneIndexCache, fileName: string): Provenance {
125
126
  function pathProvenance(cache: SceneIndexCache): Provenance {
126
127
  return cache.derived(
127
128
  PATH_PROVENANCE,
128
- () => buildSceneObjectPathIndex(cache.documents().documents).declaredIn,
129
+ () =>
130
+ buildSceneObjectPathIndex(cache.documents().documents, sceneCollectionRolesOf(cache))
131
+ .declaredIn,
129
132
  );
130
133
  }
131
134
 
132
- // Scoped exactly the way the animation completion is: to the sprite component
133
- // the slot's sibling literal addresses on the one game object owning this
134
- // script, resolved through the same helper so the two cannot disagree about
135
- // which sprite is addressed. The tile set is reported only for an id it really
136
- // declares — a panel naming a file that does not carry the name is the
137
- // fabricated answer this surface exists to avoid.
135
+ // Scoped exactly the way the gui flipbook completion is: to the `.gui` scene
136
+ // naming this file's generated gui script. The index already records which
137
+ // named texture declared each id, so the answer is read straight off it — and
138
+ // an id no named texture carries is answered with silence rather than the
139
+ // scene that failed to declare it.
140
+ function guiAnimationProvenance(input: {
141
+ cache: SceneIndexCache;
142
+ fileName: string;
143
+ entryName: string;
144
+ }): readonly string[] {
145
+ const { cache, fileName, entryName } = input;
146
+ const index = cache.derived(GUI_ANIMATION_PROVENANCE, () =>
147
+ buildGuiFlipbookIndex({
148
+ scenes: cache.documents(GUI_EXTENSIONS).documents,
149
+ assets: cache.documents(ANIMATION_ASSET_EXTENSIONS).documents,
150
+ }),
151
+ );
152
+ const config = readBuildConfigFromHost(cache.host, cache.projectRoot);
153
+ const resource = computeOutputRel(
154
+ displayPathOf(cache.projectRoot, fileName),
155
+ config,
156
+ "gui-script",
157
+ );
158
+ const declarers = index.byScriptResource.get(resource)?.get(entryName);
159
+ return declarers === undefined ? [] : [...declarers].sort();
160
+ }
161
+
162
+ // Scoped exactly the way the animation completion is: to the sprite or model
163
+ // component the slot's sibling literal addresses on the one game object owning
164
+ // this script, resolved through the same helper so the two cannot disagree
165
+ // about which component is addressed. The declaring document — an atlas for a
166
+ // sprite, and for a model the animation set that lists the entry producing the
167
+ // id — is reported only for an id it really declares: the per-id map's key set
168
+ // *is* the component's id set, so a miss there already means this component
169
+ // does not carry the name, and a panel naming a file that does not carry it is
170
+ // the fabricated answer this surface exists to avoid.
138
171
  function animationProvenance(input: {
139
172
  slot: ClassifiedSlot;
140
173
  cache: SceneIndexCache;
@@ -142,19 +175,21 @@ function animationProvenance(input: {
142
175
  entryName: string;
143
176
  }): readonly string[] {
144
177
  const { slot, cache, fileName, entryName } = input;
178
+ if (slot.nodeParameter !== undefined) {
179
+ return guiAnimationProvenance({ cache, fileName, entryName });
180
+ }
145
181
  const component = componentIdOfSameObjectAddress(slot.addressText ?? "");
146
182
  if (component === undefined) return [];
147
183
  const index = cache.derived(ANIMATION_PROVENANCE, () =>
148
- buildSpriteAnimationIndex({
184
+ buildComponentAnimationIndex({
149
185
  scenes: cache.documents().documents,
150
186
  assets: cache.documents(ANIMATION_ASSET_EXTENSIONS).documents,
151
187
  }),
152
188
  );
153
189
  const config = readBuildConfigFromHost(cache.host, cache.projectRoot);
154
190
  const resource = computeOutputRel(displayPathOf(cache.projectRoot, fileName), config, "script");
155
- const tileSet = index.tileSetByScriptResource.get(resource)?.get(component);
156
- if (tileSet === undefined) return [];
157
- return index.byScriptResource.get(resource)?.get(component)?.has(entryName) ? [tileSet] : [];
191
+ const source = index.sourceByScriptResource.get(resource)?.get(component)?.get(entryName);
192
+ return source === undefined ? [] : [source];
158
193
  }
159
194
 
160
195
  // A config key is declared by the one file that answers a reader at runtime, and
@@ -177,6 +212,37 @@ function constantProvenance(
177
212
  return cache.resourcePaths(extensions).has(entryName) ? [entryName.replace(/^\//, "")] : [];
178
213
  }
179
214
 
215
+ /**
216
+ * The sentence a relative entry's panel carries, or `undefined` where the entry
217
+ * is not one — a fragment caret, a slot that names no address, or a name this
218
+ * file's relative universe does not offer.
219
+ *
220
+ * A relative address is valid from the object hosting the script and nowhere
221
+ * else, so the objects it resolves from *are* its provenance: naming a declaring
222
+ * file instead would answer a question the author is not asking. Where the entry
223
+ * resolves from only some of the contexts this script runs in, the others are
224
+ * named too — a sometimes-valid entry that read as an always-valid one is the
225
+ * wrong-suggestion class this panel exists to close.
226
+ */
227
+ export function resolveRelativeEntryProvenance(input: {
228
+ slot: ClassifiedSlot;
229
+ position: number;
230
+ cache: SceneIndexCache;
231
+ fileName: string;
232
+ entryName: string;
233
+ }): string | undefined {
234
+ const { slot, position, cache, fileName, entryName } = input;
235
+ if (!isAddressClass(slot.class) || isFragmentCaret(slot, position)) return undefined;
236
+ const universe = relativeUniverseFor(cache, fileName);
237
+ const resolvesFrom = universe.contextsByEntry.get(entryName);
238
+ if (resolvesFrom === undefined) return undefined;
239
+ const others = universe.contexts
240
+ .map((context) => context.object)
241
+ .filter((object) => !resolvesFrom.includes(object));
242
+ const relative = `Relative to ${resolvesFrom.join(", ")}`;
243
+ return others.length === 0 ? relative : `${relative}; not from ${others.join(", ")}`;
244
+ }
245
+
180
246
  // The display paths of the project files that declare `entryName` in the
181
247
  // universe this slot draws from, sorted, empty when the project does not name
182
248
  // one exactly. Every kind the completion path offers is answered here; silence
@@ -0,0 +1,145 @@
1
+ import {
2
+ buildSceneObjectPathIndex,
3
+ buildScriptNamingContexts,
4
+ computeOutputRel,
5
+ displayPathOf,
6
+ GUI_EXTENSIONS,
7
+ guiScriptResourcesOf,
8
+ type NamingContext,
9
+ relativeAddressesFrom,
10
+ type SceneObjectPathIndex,
11
+ } from "@defold-typescript/transpiler";
12
+ import { readBuildConfigFromHost } from "./build-config";
13
+ import { type SceneIndexCache, sceneCollectionRolesOf } from "./scene-index-cache";
14
+
15
+ // The relative forms the file being edited may write, and the objects each one
16
+ // resolves from. Unlike every other completion universe this is per-file by
17
+ // construction: a relative address continues the collection path of the object
18
+ // hosting the script, so it means one thing from there and nothing anywhere
19
+ // else.
20
+ export interface RelativeUniverse {
21
+ /** Every object hosting this file's generated script, sorted by object path. */
22
+ readonly contexts: readonly NamingContext[];
23
+ /** Relative game-object paths, valid from at least one context. */
24
+ readonly paths: ReadonlySet<string>;
25
+ /** Those paths joined to the components their object owns. */
26
+ readonly addresses: ReadonlySet<string>;
27
+ /** Entry name -> the sorted object paths it resolves from. */
28
+ readonly contextsByEntry: ReadonlyMap<string, readonly string[]>;
29
+ }
30
+
31
+ const CONTEXTS = "relative:naming-contexts";
32
+
33
+ interface ProjectContexts {
34
+ readonly index: SceneObjectPathIndex;
35
+ readonly byScriptResource: ReadonlyMap<string, readonly NamingContext[]>;
36
+ }
37
+
38
+ function projectContexts(cache: SceneIndexCache): ProjectContexts {
39
+ return cache.derived(CONTEXTS, () => {
40
+ const index = buildSceneObjectPathIndex(
41
+ cache.documents().documents,
42
+ sceneCollectionRolesOf(cache),
43
+ );
44
+ // A `.gui` names the gui script it drives, and that indirection is the only
45
+ // way a gui script reaches an object: the edge is read through the same
46
+ // shared builder the CLI's walk uses rather than a second reader.
47
+ const guiScripts = guiScriptResourcesOf(cache.documents(GUI_EXTENSIONS).documents);
48
+ return { index, byScriptResource: buildScriptNamingContexts(index, guiScripts) };
49
+ });
50
+ }
51
+
52
+ /**
53
+ * Every game-object path the project declares and the components each owns —
54
+ * the index a check scopes an absolute `#fragment` address against.
55
+ *
56
+ * Read off the walk `projectContexts` already caches rather than built again:
57
+ * the naming contexts are derived *from* this index, so the reachability path
58
+ * costs no second `.go`/`.collection` parse.
59
+ */
60
+ export function projectObjectIndex(cache: SceneIndexCache): SceneObjectPathIndex {
61
+ return projectContexts(cache).index;
62
+ }
63
+
64
+ /**
65
+ * The relative universe the file at `fileName` may write from, keyed per file
66
+ * under the same cache the absolute indexes live in.
67
+ *
68
+ * The file is mapped *forward* to the script resource a scene would name — the
69
+ * same direction `nodeEntries` takes, because an output path cannot say which
70
+ * include base produced it. Both script kinds are resolved and their contexts
71
+ * unioned: a `.ts` is named as a `.script` by a game object and as a
72
+ * `.gui_script` by a `.gui`, and nothing in the file itself says which.
73
+ */
74
+ export function relativeUniverseFor(cache: SceneIndexCache, fileName: string): RelativeUniverse {
75
+ return cache.derived(`relative-universe:${fileName}`, () => {
76
+ const { index, byScriptResource } = projectContexts(cache);
77
+ const config = readBuildConfigFromHost(cache.host, cache.projectRoot);
78
+ const rel = displayPathOf(cache.projectRoot, fileName);
79
+
80
+ const byObject = new Map<string, NamingContext>();
81
+ for (const kind of ["script", "gui-script"] as const) {
82
+ for (const context of byScriptResource.get(computeOutputRel(rel, config, kind)) ?? []) {
83
+ byObject.set(context.object, context);
84
+ }
85
+ }
86
+ const contexts = [...byObject.values()].sort((a, b) => a.object.localeCompare(b.object));
87
+
88
+ const paths = new Set<string>();
89
+ const addresses = new Set<string>();
90
+ const contextsByEntry = new Map<string, string[]>();
91
+ for (const context of contexts) {
92
+ const relative = relativeAddressesFrom(index, context);
93
+ for (const entry of relative.paths) {
94
+ paths.add(entry);
95
+ contextsByEntry.set(entry, [...(contextsByEntry.get(entry) ?? []), context.object]);
96
+ }
97
+ for (const entry of relative.addresses) {
98
+ addresses.add(entry);
99
+ contextsByEntry.set(entry, [...(contextsByEntry.get(entry) ?? []), context.object]);
100
+ }
101
+ }
102
+ for (const objects of contextsByEntry.values()) objects.sort();
103
+
104
+ return { contexts, paths, addresses, contextsByEntry };
105
+ });
106
+ }
107
+
108
+ /**
109
+ * The worlds the file's script runs in, as `checkCrossWorldAddresses` asks for
110
+ * them: the de-duplicated sockets of every naming context hosting it, with the
111
+ * bootstrap world carried as `undefined`.
112
+ *
113
+ * The same answer `scriptWorldsForBuild` gives the CLI, reached through the
114
+ * chain that resolver was modelled on — so an address the build calls foreign is
115
+ * foreign in the editor too. An empty answer is the honest unknown: a script no
116
+ * scene hosts runs in no world this project can prove, and the check reports
117
+ * nothing for it.
118
+ */
119
+ export function worldsFor(
120
+ cache: SceneIndexCache,
121
+ fileName: string,
122
+ ): readonly (string | undefined)[] {
123
+ const sockets = new Set<string | undefined>();
124
+ for (const context of relativeUniverseFor(cache, fileName).contexts) {
125
+ sockets.add(context.socket);
126
+ }
127
+ return [...sockets];
128
+ }
129
+
130
+ /**
131
+ * Whether a bare `/path` can resolve from where this file runs. A bare address
132
+ * continues the caller's own world, so one offered to a script running behind a
133
+ * proxy names an object that world cannot reach.
134
+ *
135
+ * A file with no naming context offers them: nothing is known about where it
136
+ * runs, and withholding every bare path would delete the completions the
137
+ * absolute universe has always provided. Socket-qualified keys are never
138
+ * touched — whether a slot accepts a foreign socket is a different question.
139
+ */
140
+ export function offersBareWorld(universe: RelativeUniverse): boolean {
141
+ return (
142
+ universe.contexts.length === 0 ||
143
+ universe.contexts.some((context) => context.socket === undefined)
144
+ );
145
+ }
@@ -1,18 +0,0 @@
1
- export interface SceneReadHost {
2
- readDirectory?(path: string, extensions?: readonly string[]): string[];
3
- readFile(path: string): string | undefined;
4
- }
5
- export declare const SCENE_EXTENSIONS: string[];
6
- export declare const GUI_EXTENSIONS: string[];
7
- export declare const INPUT_BINDING_EXTENSIONS: string[];
8
- export declare const ANIMATION_ASSET_EXTENSIONS: string[];
9
- export declare const PROJECT_EXTENSIONS: string[];
10
- export declare const GAME_PROJECT_DOCUMENT = "game.project";
11
- export declare function displayPathOf(projectRoot: string, filePath: string): string;
12
- export declare function isExcludedProjectPath(displayPath: string): boolean;
13
- export declare function readSceneDocuments(host: SceneReadHost, projectRoot: string, extensions?: readonly string[]): {
14
- documents: Map<string, string>;
15
- unreadable: string[];
16
- paths: string[];
17
- };
18
- export declare function listProjectResourcePaths(host: SceneReadHost, projectRoot: string, extensions: readonly string[]): Set<string>;
@@ -1,126 +0,0 @@
1
- import { isDefignoredPath } from "@defold-typescript/transpiler";
2
-
3
- // The narrow slice of `ts.server.ServerHost` this needs: the real host
4
- // satisfies it structurally, and a test fake is two methods rather than a whole
5
- // server. `readDirectory` is optional because a host may not provide it, and a
6
- // plugin that throws takes the editor's completions down with it.
7
- export interface SceneReadHost {
8
- readDirectory?(path: string, extensions?: readonly string[]): string[];
9
- readFile(path: string): string | undefined;
10
- }
11
-
12
- export const SCENE_EXTENSIONS = [".go", ".collection"];
13
-
14
- // The extension sets more than one reader asks the host for. They live here
15
- // rather than beside either caller because the index cache serves exactly the
16
- // sets it is handed: a second definition that drifted would walk — and watch —
17
- // a universe the other reader never invalidates.
18
- export const GUI_EXTENSIONS = [".gui"];
19
- export const INPUT_BINDING_EXTENSIONS = [".input_binding"];
20
-
21
- // The documents an animation id is declared by: an atlas or tile source names
22
- // the ids, and a `.sprite` is the hop a referenced component takes to reach one.
23
- export const ANIMATION_ASSET_EXTENSIONS = [".atlas", ".tilesource", ".sprite"];
24
-
25
- // The walk returns every `.project` the project holds, so the lookup is by
26
- // display path — a vendored `*.project` declares keys this project's readers
27
- // cannot resolve.
28
- export const PROJECT_EXTENSIONS = [".project"];
29
- export const GAME_PROJECT_DOCUMENT = "game.project";
30
-
31
- // The project-relative name a file is keyed by — the same name the editor
32
- // reports for a program file, so a scene's claim on a script can be looked up
33
- // against the file being edited.
34
- export function displayPathOf(projectRoot: string, filePath: string): string {
35
- const path = filePath.replace(/\\/g, "/");
36
- const root = projectRoot.replace(/\\/g, "/").replace(/\/+$/, "");
37
- return path.startsWith(`${root}/`) ? path.slice(root.length + 1) : path;
38
- }
39
-
40
- // What both walks exclude is what Defold does not load as a project resource:
41
- // `build/` output, plus the directories the scaffolded `.defignore` names
42
- // (`isDefignoredPath`, root-anchored because a `.defignore` line is a
43
- // root-relative path).
44
- //
45
- // Bob writes `_generated_*.go` copies of the project's scenes under `build/`;
46
- // reading them would report every id twice, against files no author edits. The
47
- // segment test runs on the project-relative path, so a project that itself
48
- // lives under a directory named `build` is not excluded wholesale — a different
49
- // rule from `.defignore`'s, because bob writes those copies below whatever
50
- // depth it is pointed at.
51
- function isBuildOutput(displayPath: string): boolean {
52
- return displayPath.split("/").includes("build");
53
- }
54
-
55
- // The single rule, exported because the index cache filters watch events with
56
- // it: a copy in the watcher could drift from what the walks exclude, and an
57
- // event the walk would have ignored is exactly the event that must not throw the
58
- // cache away — bob writes `_generated_*.go` throughout a build.
59
- export function isExcludedProjectPath(displayPath: string): boolean {
60
- return isBuildOutput(displayPath) || isDefignoredPath(displayPath);
61
- }
62
-
63
- // Read the project's own scene sources through the editor host — by default the
64
- // `.go`/`.collection` set the component-id universe is built from, or whatever
65
- // `extensions` names instead. The two universes stay disjoint: a `.gui` declares
66
- // node ids, not component ids, so folding it into the default would feed gui
67
- // text to `buildSceneComponentIndex`. Keys are project-relative display paths,
68
- // matching what `buildSceneComponentIndex` reports in `incomplete`, and every
69
- // file that could not be read is named in `unreadable` — a universe with a
70
- // silent hole cannot be reasoned about. `paths` names the host paths actually
71
- // read — what a watcher must be registered on, which cannot be reconstructed
72
- // from a display path for a file outside the project root.
73
- export function readSceneDocuments(
74
- host: SceneReadHost,
75
- projectRoot: string,
76
- extensions: readonly string[] = SCENE_EXTENSIONS,
77
- ): { documents: Map<string, string>; unreadable: string[]; paths: string[] } {
78
- const documents = new Map<string, string>();
79
- const unreadable: string[] = [];
80
- const paths: string[] = [];
81
-
82
- if (!host.readDirectory) {
83
- unreadable.push("the editor host cannot enumerate project files, so no scene source was read");
84
- return { documents, unreadable, paths };
85
- }
86
-
87
- for (const filePath of host.readDirectory(projectRoot, extensions)) {
88
- const displayPath = displayPathOf(projectRoot, filePath);
89
- if (isExcludedProjectPath(displayPath)) continue;
90
- const text = host.readFile(filePath);
91
- if (text === undefined) {
92
- unreadable.push(`${displayPath}: could not be read`);
93
- continue;
94
- }
95
- documents.set(displayPath, text);
96
- paths.push(filePath);
97
- }
98
-
99
- return { documents, unreadable, paths };
100
- }
101
-
102
- // The project's own files of the given kinds, as the `/`-prefixed
103
- // project-relative paths a Defold resource property names. Nothing is read: a
104
- // resource path is the whole suggestion, so the walk is the entire universe.
105
- //
106
- // The extension filter is re-applied rather than trusted. `readSceneDocuments`
107
- // can take the host at its word because a wrong document merely contributes no
108
- // ids, but here a wrong file *is* the suggestion — a host that ignored its
109
- // `extensions` argument would turn a `.font` slot into a project-wide file dump.
110
- export function listProjectResourcePaths(
111
- host: SceneReadHost,
112
- projectRoot: string,
113
- extensions: readonly string[],
114
- ): Set<string> {
115
- if (!host.readDirectory) return new Set();
116
-
117
- const paths: string[] = [];
118
- for (const filePath of host.readDirectory(projectRoot, extensions)) {
119
- const displayPath = displayPathOf(projectRoot, filePath);
120
- if (isExcludedProjectPath(displayPath)) continue;
121
- if (!extensions.some((extension) => displayPath.endsWith(extension))) continue;
122
- paths.push(`/${displayPath}`);
123
- }
124
-
125
- return new Set(paths.sort());
126
- }