@defold-typescript/tstl-plugin 0.30.0 → 0.32.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.
- package/dist/completion-harness.d.ts +11 -0
- package/dist/index.js +232 -52
- package/dist/scene-diagnostics.d.ts +29 -0
- package/dist/scene-index-cache.d.ts +3 -1
- package/dist/scene-provenance.d.ts +20 -1
- package/dist/scene-relative-addresses.d.ts +55 -0
- package/package.json +3 -3
- package/src/index.ts +97 -41
- package/src/scene-diagnostics.ts +92 -0
- package/src/scene-index-cache.ts +48 -0
- package/src/scene-provenance.ts +82 -14
- package/src/scene-relative-addresses.ts +145 -0
|
@@ -5,8 +5,14 @@ export type PluginFactory = (modules: {
|
|
|
5
5
|
create(info: ts.server.PluginCreateInfo): ts.LanguageService;
|
|
6
6
|
};
|
|
7
7
|
export declare const SCENE_DOCUMENTS: Record<string, string>;
|
|
8
|
+
export declare const BOOTSTRAP_MAIN = "[bootstrap]\nmain_collection = /main.collectionc\n";
|
|
9
|
+
export declare function scriptObject(id: string, resource: string): string;
|
|
10
|
+
export declare function proxyObject(collection: string): string;
|
|
11
|
+
export declare const TWO_WORLD_DOCUMENTS: Record<string, string>;
|
|
8
12
|
export interface ProxyHost {
|
|
9
13
|
documents: Record<string, string>;
|
|
14
|
+
/** Listed by `readDirectory`, but answered `undefined` by `readFile`. */
|
|
15
|
+
unreadable: Set<string>;
|
|
10
16
|
directoryReads: string[][];
|
|
11
17
|
openWatchers: number;
|
|
12
18
|
fireDirectory(hostPath: string): void;
|
|
@@ -20,7 +26,12 @@ export interface CompletionSetup {
|
|
|
20
26
|
export declare function completionSetup(options: {
|
|
21
27
|
source: string;
|
|
22
28
|
base: ts.WithMetadata<ts.CompletionInfo> | undefined;
|
|
29
|
+
/** Further program files beside the edited one, so a check can be scoped to it. */
|
|
30
|
+
sources?: Record<string, string>;
|
|
23
31
|
documents?: Record<string, string>;
|
|
32
|
+
/** Scene sources the host lists but cannot read, so a walk composes a real hole. */
|
|
33
|
+
unreadable?: readonly string[];
|
|
34
|
+
baseDiagnostics?: ts.Diagnostic[];
|
|
24
35
|
serverHost?: boolean;
|
|
25
36
|
watch?: boolean;
|
|
26
37
|
baseDispose?: boolean;
|
package/dist/index.js
CHANGED
|
@@ -3,22 +3,22 @@ import { readFileSync } from "node:fs";
|
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import {
|
|
5
5
|
ANIMATION_ASSET_EXTENSIONS as ANIMATION_ASSET_EXTENSIONS2,
|
|
6
|
+
buildComponentAnimationIndex as buildComponentAnimationIndex2,
|
|
6
7
|
buildConfigKeyIndex as buildConfigKeyIndex2,
|
|
8
|
+
buildGuiFlipbookIndex as buildGuiFlipbookIndex2,
|
|
7
9
|
buildGuiNodeIndex as buildGuiNodeIndex2,
|
|
8
10
|
buildInputActionIndex as buildInputActionIndex2,
|
|
9
|
-
|
|
10
|
-
buildSceneObjectPathIndex as buildSceneObjectPathIndex2,
|
|
11
|
-
buildSpriteAnimationIndex as buildSpriteAnimationIndex2,
|
|
11
|
+
buildSceneObjectPathIndex as buildSceneObjectPathIndex3,
|
|
12
12
|
componentIdOfSameObjectAddress as componentIdOfSameObjectAddress2,
|
|
13
|
-
computeOutputRel as
|
|
14
|
-
displayPathOf as
|
|
15
|
-
GAME_PROJECT_DOCUMENT as
|
|
16
|
-
GUI_EXTENSIONS as
|
|
13
|
+
computeOutputRel as computeOutputRel3,
|
|
14
|
+
displayPathOf as displayPathOf4,
|
|
15
|
+
GAME_PROJECT_DOCUMENT as GAME_PROJECT_DOCUMENT3,
|
|
16
|
+
GUI_EXTENSIONS as GUI_EXTENSIONS3,
|
|
17
17
|
getProgramDiagnostics,
|
|
18
18
|
INPUT_BINDING_EXTENSIONS as INPUT_BINDING_EXTENSIONS2,
|
|
19
19
|
isAddressClass as isAddressClass2,
|
|
20
20
|
isFragmentCaret as isFragmentCaret2,
|
|
21
|
-
PROJECT_EXTENSIONS as
|
|
21
|
+
PROJECT_EXTENSIONS as PROJECT_EXTENSIONS3,
|
|
22
22
|
resolveClassifiedSlotAtPosition
|
|
23
23
|
} from "@defold-typescript/transpiler";
|
|
24
24
|
|
|
@@ -87,11 +87,23 @@ function buildWholeLiteralCompletionEntries(input) {
|
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
// src/scene-diagnostics.ts
|
|
91
|
+
import {
|
|
92
|
+
checkCrossWorldAddresses,
|
|
93
|
+
checkUrlFragmentReachability
|
|
94
|
+
} from "@defold-typescript/transpiler";
|
|
95
|
+
|
|
90
96
|
// src/scene-index-cache.ts
|
|
91
97
|
import {
|
|
98
|
+
buildSceneCollectionRoles,
|
|
99
|
+
buildSceneComponentIndex,
|
|
100
|
+
COLLECTION_REFERENCE_EXTENSIONS,
|
|
92
101
|
displayPathOf,
|
|
102
|
+
GAME_PROJECT_DOCUMENT,
|
|
93
103
|
isExcludedProjectPath,
|
|
104
|
+
LIBRARY_DEPENDENCY_ROOT,
|
|
94
105
|
listProjectResourcePaths,
|
|
106
|
+
PROJECT_EXTENSIONS,
|
|
95
107
|
readSceneDocuments,
|
|
96
108
|
SCENE_EXTENSIONS
|
|
97
109
|
} from "@defold-typescript/transpiler";
|
|
@@ -122,6 +134,9 @@ function createSceneIndexCache(host, projectRoot) {
|
|
|
122
134
|
}
|
|
123
135
|
function affectsCache(hostPath) {
|
|
124
136
|
const displayPath = displayPathOf(projectRoot, hostPath);
|
|
137
|
+
if (displayPath === LIBRARY_DEPENDENCY_ROOT || displayPath.startsWith(`${LIBRARY_DEPENDENCY_ROOT}/`)) {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
125
140
|
if (isExcludedProjectPath(displayPath))
|
|
126
141
|
return false;
|
|
127
142
|
for (const extension of servedExtensions) {
|
|
@@ -207,31 +222,145 @@ function createSceneIndexCache(host, projectRoot) {
|
|
|
207
222
|
}
|
|
208
223
|
};
|
|
209
224
|
}
|
|
225
|
+
function sceneComponentIndexOf(cache) {
|
|
226
|
+
return cache.derived("component-index", () => buildSceneComponentIndex(cache.documents().documents));
|
|
227
|
+
}
|
|
228
|
+
function sceneCollectionRolesOf(cache) {
|
|
229
|
+
return cache.derived("collection-roles", () => buildSceneCollectionRoles({
|
|
230
|
+
documents: cache.documents().documents,
|
|
231
|
+
references: cache.documents(COLLECTION_REFERENCE_EXTENSIONS).documents,
|
|
232
|
+
gameProject: cache.documents(PROJECT_EXTENSIONS).documents.get(GAME_PROJECT_DOCUMENT)
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/scene-relative-addresses.ts
|
|
237
|
+
import {
|
|
238
|
+
buildSceneObjectPathIndex,
|
|
239
|
+
buildScriptNamingContexts,
|
|
240
|
+
computeOutputRel,
|
|
241
|
+
displayPathOf as displayPathOf2,
|
|
242
|
+
GUI_EXTENSIONS,
|
|
243
|
+
guiScriptResourcesOf,
|
|
244
|
+
relativeAddressesFrom
|
|
245
|
+
} from "@defold-typescript/transpiler";
|
|
246
|
+
var CONTEXTS = "relative:naming-contexts";
|
|
247
|
+
function projectContexts(cache) {
|
|
248
|
+
return cache.derived(CONTEXTS, () => {
|
|
249
|
+
const index = buildSceneObjectPathIndex(cache.documents().documents, sceneCollectionRolesOf(cache));
|
|
250
|
+
const guiScripts = guiScriptResourcesOf(cache.documents(GUI_EXTENSIONS).documents);
|
|
251
|
+
return { index, byScriptResource: buildScriptNamingContexts(index, guiScripts) };
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
function projectObjectIndex(cache) {
|
|
255
|
+
return projectContexts(cache).index;
|
|
256
|
+
}
|
|
257
|
+
function relativeUniverseFor(cache, fileName) {
|
|
258
|
+
return cache.derived(`relative-universe:${fileName}`, () => {
|
|
259
|
+
const { index, byScriptResource } = projectContexts(cache);
|
|
260
|
+
const config = readBuildConfigFromHost(cache.host, cache.projectRoot);
|
|
261
|
+
const rel = displayPathOf2(cache.projectRoot, fileName);
|
|
262
|
+
const byObject = new Map;
|
|
263
|
+
for (const kind of ["script", "gui-script"]) {
|
|
264
|
+
for (const context of byScriptResource.get(computeOutputRel(rel, config, kind)) ?? []) {
|
|
265
|
+
byObject.set(context.object, context);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const contexts = [...byObject.values()].sort((a, b) => a.object.localeCompare(b.object));
|
|
269
|
+
const paths = new Set;
|
|
270
|
+
const addresses = new Set;
|
|
271
|
+
const contextsByEntry = new Map;
|
|
272
|
+
for (const context of contexts) {
|
|
273
|
+
const relative = relativeAddressesFrom(index, context);
|
|
274
|
+
for (const entry of relative.paths) {
|
|
275
|
+
paths.add(entry);
|
|
276
|
+
contextsByEntry.set(entry, [...contextsByEntry.get(entry) ?? [], context.object]);
|
|
277
|
+
}
|
|
278
|
+
for (const entry of relative.addresses) {
|
|
279
|
+
addresses.add(entry);
|
|
280
|
+
contextsByEntry.set(entry, [...contextsByEntry.get(entry) ?? [], context.object]);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
for (const objects of contextsByEntry.values())
|
|
284
|
+
objects.sort();
|
|
285
|
+
return { contexts, paths, addresses, contextsByEntry };
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
function worldsFor(cache, fileName) {
|
|
289
|
+
const sockets = new Set;
|
|
290
|
+
for (const context of relativeUniverseFor(cache, fileName).contexts) {
|
|
291
|
+
sockets.add(context.socket);
|
|
292
|
+
}
|
|
293
|
+
return [...sockets];
|
|
294
|
+
}
|
|
295
|
+
function offersBareWorld(universe) {
|
|
296
|
+
return universe.contexts.length === 0 || universe.contexts.some((context) => context.socket === undefined);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/scene-diagnostics.ts
|
|
300
|
+
var DEFOLD_DIAGNOSTIC_SOURCE = "defold-typescript";
|
|
301
|
+
var UNREACHABLE_FRAGMENT_CODE = 9000101;
|
|
302
|
+
var FOREIGN_SOCKET_CODE = 9000102;
|
|
303
|
+
function suggestion(typescript, file, finding, code) {
|
|
304
|
+
return {
|
|
305
|
+
file,
|
|
306
|
+
start: finding.start,
|
|
307
|
+
length: finding.length,
|
|
308
|
+
messageText: finding.message,
|
|
309
|
+
category: typescript.DiagnosticCategory.Suggestion,
|
|
310
|
+
source: DEFOLD_DIAGNOSTIC_SOURCE,
|
|
311
|
+
code
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
function sceneReachabilityDiagnostics(input) {
|
|
315
|
+
const { ts: typescript, program, table, cache, fileName } = input;
|
|
316
|
+
const sourceFile = program.getSourceFile(fileName);
|
|
317
|
+
if (!sourceFile) {
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
const index = sceneComponentIndexOf(cache);
|
|
321
|
+
const report = checkUrlFragmentReachability({
|
|
322
|
+
program,
|
|
323
|
+
table,
|
|
324
|
+
index: { ids: index.ids, incomplete: [...index.incomplete, ...cache.documents().unreadable] },
|
|
325
|
+
objects: projectObjectIndex(cache),
|
|
326
|
+
sourceFiles: [sourceFile]
|
|
327
|
+
});
|
|
328
|
+
const fragments = report.kind === "suppressed" ? [] : report.findings.map((finding) => suggestion(typescript, sourceFile, finding, UNREACHABLE_FRAGMENT_CODE));
|
|
329
|
+
const crossWorld = checkCrossWorldAddresses({
|
|
330
|
+
program,
|
|
331
|
+
table,
|
|
332
|
+
worldsOf: (name) => worldsFor(cache, name),
|
|
333
|
+
sourceFiles: [sourceFile]
|
|
334
|
+
}).map((finding) => suggestion(typescript, sourceFile, finding, FOREIGN_SOCKET_CODE));
|
|
335
|
+
return [...fragments, ...crossWorld];
|
|
336
|
+
}
|
|
210
337
|
|
|
211
338
|
// src/scene-provenance.ts
|
|
212
339
|
import {
|
|
213
340
|
ANIMATION_ASSET_EXTENSIONS,
|
|
341
|
+
buildComponentAnimationIndex,
|
|
214
342
|
buildConfigKeyIndex,
|
|
343
|
+
buildGuiFlipbookIndex,
|
|
215
344
|
buildGuiNodeIndex,
|
|
216
345
|
buildInputActionIndex,
|
|
217
|
-
buildSceneComponentIndex,
|
|
218
|
-
buildSceneObjectPathIndex,
|
|
219
|
-
buildSpriteAnimationIndex,
|
|
346
|
+
buildSceneComponentIndex as buildSceneComponentIndex2,
|
|
347
|
+
buildSceneObjectPathIndex as buildSceneObjectPathIndex2,
|
|
220
348
|
componentIdOfSameObjectAddress,
|
|
221
|
-
computeOutputRel,
|
|
222
|
-
displayPathOf as
|
|
223
|
-
GAME_PROJECT_DOCUMENT,
|
|
224
|
-
GUI_EXTENSIONS,
|
|
349
|
+
computeOutputRel as computeOutputRel2,
|
|
350
|
+
displayPathOf as displayPathOf3,
|
|
351
|
+
GAME_PROJECT_DOCUMENT as GAME_PROJECT_DOCUMENT2,
|
|
352
|
+
GUI_EXTENSIONS as GUI_EXTENSIONS2,
|
|
225
353
|
INPUT_BINDING_EXTENSIONS,
|
|
226
354
|
isAddressClass,
|
|
227
355
|
isFragmentCaret,
|
|
228
|
-
PROJECT_EXTENSIONS
|
|
356
|
+
PROJECT_EXTENSIONS as PROJECT_EXTENSIONS2
|
|
229
357
|
} from "@defold-typescript/transpiler";
|
|
230
358
|
var COMPONENT_PROVENANCE = "provenance:component-ids";
|
|
231
359
|
var ACTION_PROVENANCE = "provenance:input-actions";
|
|
232
360
|
var GUI_PROVENANCE = "provenance:gui-nodes";
|
|
233
361
|
var PATH_PROVENANCE = "provenance:object-paths";
|
|
234
|
-
var ANIMATION_PROVENANCE = "provenance:
|
|
362
|
+
var ANIMATION_PROVENANCE = "provenance:component-animations";
|
|
363
|
+
var GUI_ANIMATION_PROVENANCE = "provenance:gui-flipbook";
|
|
235
364
|
function byDeclaringDocument(documents, idsOf) {
|
|
236
365
|
const declarers = new Map;
|
|
237
366
|
for (const [displayPath, text] of documents) {
|
|
@@ -275,49 +404,73 @@ function byClaimedScript(documents) {
|
|
|
275
404
|
return byResource;
|
|
276
405
|
}
|
|
277
406
|
function componentProvenance(cache) {
|
|
278
|
-
return cache.derived(COMPONENT_PROVENANCE, () => byDeclaringDocument(cache.documents().documents, (narrowed) =>
|
|
407
|
+
return cache.derived(COMPONENT_PROVENANCE, () => byDeclaringDocument(cache.documents().documents, (narrowed) => buildSceneComponentIndex2(narrowed).ids));
|
|
279
408
|
}
|
|
280
409
|
function actionProvenance(cache) {
|
|
281
410
|
return cache.derived(ACTION_PROVENANCE, () => byDeclaringDocument(cache.documents(INPUT_BINDING_EXTENSIONS).documents, (narrowed) => buildInputActionIndex(narrowed)));
|
|
282
411
|
}
|
|
283
412
|
function nodeProvenance(cache, fileName) {
|
|
284
|
-
const byResource = cache.derived(GUI_PROVENANCE, () => byClaimedScript(cache.documents(
|
|
413
|
+
const byResource = cache.derived(GUI_PROVENANCE, () => byClaimedScript(cache.documents(GUI_EXTENSIONS2).documents));
|
|
285
414
|
const config = readBuildConfigFromHost(cache.host, cache.projectRoot);
|
|
286
|
-
const resource =
|
|
415
|
+
const resource = computeOutputRel2(displayPathOf3(cache.projectRoot, fileName), config, "gui-script");
|
|
287
416
|
return byResource.get(resource) ?? new Map;
|
|
288
417
|
}
|
|
289
418
|
function pathProvenance(cache) {
|
|
290
|
-
return cache.derived(PATH_PROVENANCE, () =>
|
|
419
|
+
return cache.derived(PATH_PROVENANCE, () => buildSceneObjectPathIndex2(cache.documents().documents, sceneCollectionRolesOf(cache)).declaredIn);
|
|
420
|
+
}
|
|
421
|
+
function guiAnimationProvenance(input) {
|
|
422
|
+
const { cache, fileName, entryName } = input;
|
|
423
|
+
const index = cache.derived(GUI_ANIMATION_PROVENANCE, () => buildGuiFlipbookIndex({
|
|
424
|
+
scenes: cache.documents(GUI_EXTENSIONS2).documents,
|
|
425
|
+
assets: cache.documents(ANIMATION_ASSET_EXTENSIONS).documents
|
|
426
|
+
}));
|
|
427
|
+
const config = readBuildConfigFromHost(cache.host, cache.projectRoot);
|
|
428
|
+
const resource = computeOutputRel2(displayPathOf3(cache.projectRoot, fileName), config, "gui-script");
|
|
429
|
+
const declarers = index.byScriptResource.get(resource)?.get(entryName);
|
|
430
|
+
return declarers === undefined ? [] : [...declarers].sort();
|
|
291
431
|
}
|
|
292
432
|
function animationProvenance(input) {
|
|
293
433
|
const { slot, cache, fileName, entryName } = input;
|
|
434
|
+
if (slot.nodeParameter !== undefined) {
|
|
435
|
+
return guiAnimationProvenance({ cache, fileName, entryName });
|
|
436
|
+
}
|
|
294
437
|
const component = componentIdOfSameObjectAddress(slot.addressText ?? "");
|
|
295
438
|
if (component === undefined)
|
|
296
439
|
return [];
|
|
297
|
-
const index = cache.derived(ANIMATION_PROVENANCE, () =>
|
|
440
|
+
const index = cache.derived(ANIMATION_PROVENANCE, () => buildComponentAnimationIndex({
|
|
298
441
|
scenes: cache.documents().documents,
|
|
299
442
|
assets: cache.documents(ANIMATION_ASSET_EXTENSIONS).documents
|
|
300
443
|
}));
|
|
301
444
|
const config = readBuildConfigFromHost(cache.host, cache.projectRoot);
|
|
302
|
-
const resource =
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
return [];
|
|
306
|
-
return index.byScriptResource.get(resource)?.get(component)?.has(entryName) ? [tileSet] : [];
|
|
445
|
+
const resource = computeOutputRel2(displayPathOf3(cache.projectRoot, fileName), config, "script");
|
|
446
|
+
const source = index.sourceByScriptResource.get(resource)?.get(component)?.get(entryName);
|
|
447
|
+
return source === undefined ? [] : [source];
|
|
307
448
|
}
|
|
308
449
|
function constantProvenance(slot, cache, entryName) {
|
|
309
450
|
if (slot.class === "config-key") {
|
|
310
|
-
const text = cache.documents(
|
|
451
|
+
const text = cache.documents(PROJECT_EXTENSIONS2).documents.get(GAME_PROJECT_DOCUMENT2);
|
|
311
452
|
if (text === undefined)
|
|
312
453
|
return [];
|
|
313
454
|
const keys = cache.derived("config-keys", () => buildConfigKeyIndex(text));
|
|
314
|
-
return keys.has(entryName) ? [
|
|
455
|
+
return keys.has(entryName) ? [GAME_PROJECT_DOCUMENT2] : [];
|
|
315
456
|
}
|
|
316
457
|
const extensions = slot.resourceExtensions;
|
|
317
458
|
if (extensions === undefined || extensions.length === 0)
|
|
318
459
|
return [];
|
|
319
460
|
return cache.resourcePaths(extensions).has(entryName) ? [entryName.replace(/^\//, "")] : [];
|
|
320
461
|
}
|
|
462
|
+
function resolveRelativeEntryProvenance(input) {
|
|
463
|
+
const { slot, position, cache, fileName, entryName } = input;
|
|
464
|
+
if (!isAddressClass(slot.class) || isFragmentCaret(slot, position))
|
|
465
|
+
return;
|
|
466
|
+
const universe = relativeUniverseFor(cache, fileName);
|
|
467
|
+
const resolvesFrom = universe.contextsByEntry.get(entryName);
|
|
468
|
+
if (resolvesFrom === undefined)
|
|
469
|
+
return;
|
|
470
|
+
const others = universe.contexts.map((context) => context.object).filter((object) => !resolvesFrom.includes(object));
|
|
471
|
+
const relative = `Relative to ${resolvesFrom.join(", ")}`;
|
|
472
|
+
return others.length === 0 ? relative : `${relative}; not from ${others.join(", ")}`;
|
|
473
|
+
}
|
|
321
474
|
function resolveEntryProvenance(input) {
|
|
322
475
|
const { slot, position, cache, fileName, entryName } = input;
|
|
323
476
|
if (isAddressClass(slot.class)) {
|
|
@@ -361,40 +514,60 @@ function componentEntries(slot, position, cache, baseEntries) {
|
|
|
361
514
|
}
|
|
362
515
|
return buildSceneCompletionEntries({
|
|
363
516
|
slot,
|
|
364
|
-
ids:
|
|
517
|
+
ids: sceneComponentIndexOf(cache).ids,
|
|
365
518
|
baseEntries
|
|
366
519
|
});
|
|
367
520
|
}
|
|
368
|
-
function objectPathEntries(slot, position, cache, baseEntries) {
|
|
521
|
+
function objectPathEntries(slot, position, cache, fileName, baseEntries) {
|
|
369
522
|
if (isFragmentCaret2(slot, position)) {
|
|
370
523
|
return [];
|
|
371
524
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
525
|
+
const universe = relativeUniverseFor(cache, fileName);
|
|
526
|
+
const bare = offersBareWorld(universe);
|
|
527
|
+
const absolute = cache.derived("object-paths", () => buildSceneObjectPathIndex3(cache.documents().documents, sceneCollectionRolesOf(cache)).paths);
|
|
528
|
+
const paths = new Set;
|
|
529
|
+
for (const key of absolute) {
|
|
530
|
+
if (bare || !key.startsWith("/"))
|
|
531
|
+
paths.add(key);
|
|
532
|
+
}
|
|
533
|
+
for (const key of universe.paths)
|
|
534
|
+
paths.add(key);
|
|
535
|
+
const entries = buildAddressPathCompletionEntries({ slot, paths, baseEntries });
|
|
536
|
+
if (slot.fragmentStart === -1) {
|
|
537
|
+
entries.push(...buildWholeLiteralCompletionEntries({ slot, ids: universe.addresses, baseEntries }));
|
|
538
|
+
}
|
|
539
|
+
return entries;
|
|
377
540
|
}
|
|
378
541
|
function nodeEntries(slot, cache, fileName, baseEntries) {
|
|
379
542
|
const { projectRoot } = cache;
|
|
380
|
-
const index = cache.derived("gui-nodes", () => buildGuiNodeIndex2(cache.documents(
|
|
543
|
+
const index = cache.derived("gui-nodes", () => buildGuiNodeIndex2(cache.documents(GUI_EXTENSIONS3).documents));
|
|
381
544
|
const config = readBuildConfigFromHost(cache.host, projectRoot);
|
|
382
|
-
const resource =
|
|
545
|
+
const resource = computeOutputRel3(displayPathOf4(projectRoot, fileName), config, "gui-script");
|
|
383
546
|
const ids = index.byScriptResource.get(resource);
|
|
384
547
|
return ids === undefined ? [] : buildWholeLiteralCompletionEntries({ slot, ids, baseEntries });
|
|
385
548
|
}
|
|
386
549
|
function animationEntries(slot, cache, fileName, baseEntries) {
|
|
550
|
+
const { projectRoot } = cache;
|
|
551
|
+
if (slot.nodeParameter !== undefined) {
|
|
552
|
+
const index = cache.derived("gui-flipbook", () => buildGuiFlipbookIndex2({
|
|
553
|
+
scenes: cache.documents(GUI_EXTENSIONS3).documents,
|
|
554
|
+
assets: cache.documents(ANIMATION_ASSET_EXTENSIONS2).documents
|
|
555
|
+
}));
|
|
556
|
+
const config = readBuildConfigFromHost(cache.host, projectRoot);
|
|
557
|
+
const resource = computeOutputRel3(displayPathOf4(projectRoot, fileName), config, "gui-script");
|
|
558
|
+
const declared = index.byScriptResource.get(resource);
|
|
559
|
+
return declared === undefined ? [] : buildWholeLiteralCompletionEntries({ slot, ids: new Set(declared.keys()), baseEntries });
|
|
560
|
+
}
|
|
387
561
|
const component = componentIdOfSameObjectAddress2(slot.addressText ?? "");
|
|
388
562
|
if (component === undefined) {
|
|
389
563
|
return [];
|
|
390
564
|
}
|
|
391
|
-
const
|
|
392
|
-
const index = cache.derived("sprite-animations", () => buildSpriteAnimationIndex2({
|
|
565
|
+
const index = cache.derived("component-animations", () => buildComponentAnimationIndex2({
|
|
393
566
|
scenes: cache.documents().documents,
|
|
394
567
|
assets: cache.documents(ANIMATION_ASSET_EXTENSIONS2).documents
|
|
395
568
|
}));
|
|
396
569
|
const config = readBuildConfigFromHost(cache.host, projectRoot);
|
|
397
|
-
const resource =
|
|
570
|
+
const resource = computeOutputRel3(displayPathOf4(projectRoot, fileName), config, "script");
|
|
398
571
|
const ids = index.byScriptResource.get(resource)?.get(component);
|
|
399
572
|
return ids === undefined ? [] : buildWholeLiteralCompletionEntries({ slot, ids, baseEntries });
|
|
400
573
|
}
|
|
@@ -407,7 +580,7 @@ function resourceEntries(slot, cache, baseEntries) {
|
|
|
407
580
|
return buildWholeLiteralCompletionEntries({ slot, ids, baseEntries });
|
|
408
581
|
}
|
|
409
582
|
function configKeyEntries(slot, cache, baseEntries) {
|
|
410
|
-
const text = cache.documents(
|
|
583
|
+
const text = cache.documents(PROJECT_EXTENSIONS3).documents.get(GAME_PROJECT_DOCUMENT3);
|
|
411
584
|
if (text === undefined) {
|
|
412
585
|
return [];
|
|
413
586
|
}
|
|
@@ -418,18 +591,13 @@ function actionIdEntries(slot, cache, baseEntries) {
|
|
|
418
591
|
const ids = cache.derived("input-actions", () => buildInputActionIndex2(cache.documents(INPUT_BINDING_EXTENSIONS2).documents));
|
|
419
592
|
return buildWholeLiteralCompletionEntries({ slot, ids, baseEntries });
|
|
420
593
|
}
|
|
421
|
-
function provenancePanel(entryName,
|
|
594
|
+
function provenancePanel(entryName, documentation) {
|
|
422
595
|
return {
|
|
423
596
|
name: entryName,
|
|
424
597
|
kind: CONTRIBUTED_ENTRY_KIND,
|
|
425
598
|
kindModifiers: "",
|
|
426
599
|
displayParts: [{ kind: "stringLiteral", text: JSON.stringify(entryName) }],
|
|
427
|
-
documentation: [
|
|
428
|
-
{
|
|
429
|
-
kind: "text",
|
|
430
|
-
text: `Declared in ${declaredIn.join(", ")}`
|
|
431
|
-
}
|
|
432
|
-
],
|
|
600
|
+
documentation: [{ kind: "text", text: documentation }],
|
|
433
601
|
source: [{ kind: "text", text: DEFOLD_COMPLETION_SOURCE }]
|
|
434
602
|
};
|
|
435
603
|
}
|
|
@@ -467,8 +635,18 @@ function init(modules) {
|
|
|
467
635
|
if (!slot) {
|
|
468
636
|
return forward();
|
|
469
637
|
}
|
|
638
|
+
const relative = resolveRelativeEntryProvenance({
|
|
639
|
+
slot,
|
|
640
|
+
position,
|
|
641
|
+
cache,
|
|
642
|
+
fileName,
|
|
643
|
+
entryName
|
|
644
|
+
});
|
|
645
|
+
if (relative !== undefined) {
|
|
646
|
+
return provenancePanel(entryName, relative);
|
|
647
|
+
}
|
|
470
648
|
const declaredIn = resolveEntryProvenance({ slot, position, cache, fileName, entryName });
|
|
471
|
-
return declaredIn.length === 0 ? forward() : provenancePanel(entryName, declaredIn);
|
|
649
|
+
return declaredIn.length === 0 ? forward() : provenancePanel(entryName, `Declared in ${declaredIn.join(", ")}`);
|
|
472
650
|
};
|
|
473
651
|
proxy.getSemanticDiagnostics = (fileName) => {
|
|
474
652
|
const prior = base.getSemanticDiagnostics(fileName);
|
|
@@ -477,7 +655,9 @@ function init(modules) {
|
|
|
477
655
|
return prior;
|
|
478
656
|
}
|
|
479
657
|
const transpiler = getProgramDiagnostics(program, program.getSourceFile(fileName)).map((diagnostic) => ({ ...diagnostic, category: ts.DiagnosticCategory.Suggestion }));
|
|
480
|
-
|
|
658
|
+
const table = loadUrlParameterTable();
|
|
659
|
+
const scene = cache && table ? sceneReachabilityDiagnostics({ ts, program, table, cache, fileName }) : [];
|
|
660
|
+
return [...prior, ...transpiler, ...scene];
|
|
481
661
|
};
|
|
482
662
|
proxy.getCompletionsAtPosition = (fileName, position, options, formattingSettings) => {
|
|
483
663
|
const prior = base.getCompletionsAtPosition(fileName, position, options, formattingSettings);
|
|
@@ -496,7 +676,7 @@ function init(modules) {
|
|
|
496
676
|
const baseEntries = prior?.entries ?? [];
|
|
497
677
|
const entries = isAddressClass2(slot.class) ? [
|
|
498
678
|
...componentEntries(slot, position, cache, baseEntries),
|
|
499
|
-
...objectPathEntries(slot, position, cache, baseEntries)
|
|
679
|
+
...objectPathEntries(slot, position, cache, fileName, baseEntries)
|
|
500
680
|
] : slot.class === "gui-node" ? nodeEntries(slot, cache, fileName, baseEntries) : slot.class === "animation" ? animationEntries(slot, cache, fileName, baseEntries) : slot.class === "resource-path" ? resourceEntries(slot, cache, baseEntries) : slot.class === "config-key" ? configKeyEntries(slot, cache, baseEntries) : slot.class === "action-id" ? actionIdEntries(slot, cache, baseEntries) : [];
|
|
501
681
|
if (entries.length === 0) {
|
|
502
682
|
return prior;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { UrlParameterTable } from "@defold-typescript/types";
|
|
2
|
+
import type * as ts from "typescript";
|
|
3
|
+
import { type SceneIndexCache } from "./scene-index-cache";
|
|
4
|
+
/** The `source` an editor renders beside the message, and `--json` never sees. */
|
|
5
|
+
export declare const DEFOLD_DIAGNOSTIC_SOURCE = "defold-typescript";
|
|
6
|
+
/**
|
|
7
|
+
* The address findings the editor surfaces on the file being edited, as
|
|
8
|
+
* advisory suggestions.
|
|
9
|
+
*
|
|
10
|
+
* Every path that cannot produce one answers with an empty array. A suppressed
|
|
11
|
+
* reachability report is the important case: there is no file span on which a
|
|
12
|
+
* "the check did not run" notice could honestly sit, and a speculative squiggle
|
|
13
|
+
* is worse than none — the build says so in prose instead, where a whole-project
|
|
14
|
+
* line has somewhere to go.
|
|
15
|
+
*
|
|
16
|
+
* The *whole* hole is one index, composed the way `sceneIndexForBuild` composes
|
|
17
|
+
* it: what the parse could not read plus what the walk could not reach. The
|
|
18
|
+
* completion path deliberately reads a partial universe — a suggestion claims
|
|
19
|
+
* nothing about what is absent — but a finding does, so a component declared
|
|
20
|
+
* only by an unresolved dependency suppresses the check rather than reporting
|
|
21
|
+
* the address as unreachable.
|
|
22
|
+
*/
|
|
23
|
+
export declare function sceneReachabilityDiagnostics(input: {
|
|
24
|
+
ts: typeof import("typescript");
|
|
25
|
+
program: ts.Program;
|
|
26
|
+
table: UrlParameterTable;
|
|
27
|
+
cache: SceneIndexCache;
|
|
28
|
+
fileName: string;
|
|
29
|
+
}): ts.Diagnostic[];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readSceneDocuments, type SceneReadHost } from "@defold-typescript/transpiler";
|
|
1
|
+
import { readSceneDocuments, type SceneCollectionRoles, type SceneComponentIndex, type SceneReadHost } from "@defold-typescript/transpiler";
|
|
2
2
|
import type * as ts from "typescript";
|
|
3
3
|
export interface SceneWatchHost extends SceneReadHost {
|
|
4
4
|
watchFile?(path: string, callback: ts.FileWatcherCallback, pollingInterval?: number): ts.FileWatcher;
|
|
@@ -13,3 +13,5 @@ export interface SceneIndexCache {
|
|
|
13
13
|
dispose(): void;
|
|
14
14
|
}
|
|
15
15
|
export declare function createSceneIndexCache(host: SceneWatchHost, projectRoot: string): SceneIndexCache;
|
|
16
|
+
export declare function sceneComponentIndexOf(cache: SceneIndexCache): SceneComponentIndex;
|
|
17
|
+
export declare function sceneCollectionRolesOf(cache: SceneIndexCache): SceneCollectionRoles;
|
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
import { type ClassifiedSlot } from "@defold-typescript/transpiler";
|
|
2
|
-
import type
|
|
2
|
+
import { type SceneIndexCache } from "./scene-index-cache";
|
|
3
|
+
/**
|
|
4
|
+
* The sentence a relative entry's panel carries, or `undefined` where the entry
|
|
5
|
+
* is not one — a fragment caret, a slot that names no address, or a name this
|
|
6
|
+
* file's relative universe does not offer.
|
|
7
|
+
*
|
|
8
|
+
* A relative address is valid from the object hosting the script and nowhere
|
|
9
|
+
* else, so the objects it resolves from *are* its provenance: naming a declaring
|
|
10
|
+
* file instead would answer a question the author is not asking. Where the entry
|
|
11
|
+
* resolves from only some of the contexts this script runs in, the others are
|
|
12
|
+
* named too — a sometimes-valid entry that read as an always-valid one is the
|
|
13
|
+
* wrong-suggestion class this panel exists to close.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveRelativeEntryProvenance(input: {
|
|
16
|
+
slot: ClassifiedSlot;
|
|
17
|
+
position: number;
|
|
18
|
+
cache: SceneIndexCache;
|
|
19
|
+
fileName: string;
|
|
20
|
+
entryName: string;
|
|
21
|
+
}): string | undefined;
|
|
3
22
|
export declare function resolveEntryProvenance(input: {
|
|
4
23
|
slot: ClassifiedSlot;
|
|
5
24
|
position: number;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type NamingContext, type SceneObjectPathIndex } from "@defold-typescript/transpiler";
|
|
2
|
+
import { type SceneIndexCache } from "./scene-index-cache";
|
|
3
|
+
export interface RelativeUniverse {
|
|
4
|
+
/** Every object hosting this file's generated script, sorted by object path. */
|
|
5
|
+
readonly contexts: readonly NamingContext[];
|
|
6
|
+
/** Relative game-object paths, valid from at least one context. */
|
|
7
|
+
readonly paths: ReadonlySet<string>;
|
|
8
|
+
/** Those paths joined to the components their object owns. */
|
|
9
|
+
readonly addresses: ReadonlySet<string>;
|
|
10
|
+
/** Entry name -> the sorted object paths it resolves from. */
|
|
11
|
+
readonly contextsByEntry: ReadonlyMap<string, readonly string[]>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Every game-object path the project declares and the components each owns —
|
|
15
|
+
* the index a check scopes an absolute `#fragment` address against.
|
|
16
|
+
*
|
|
17
|
+
* Read off the walk `projectContexts` already caches rather than built again:
|
|
18
|
+
* the naming contexts are derived *from* this index, so the reachability path
|
|
19
|
+
* costs no second `.go`/`.collection` parse.
|
|
20
|
+
*/
|
|
21
|
+
export declare function projectObjectIndex(cache: SceneIndexCache): SceneObjectPathIndex;
|
|
22
|
+
/**
|
|
23
|
+
* The relative universe the file at `fileName` may write from, keyed per file
|
|
24
|
+
* under the same cache the absolute indexes live in.
|
|
25
|
+
*
|
|
26
|
+
* The file is mapped *forward* to the script resource a scene would name — the
|
|
27
|
+
* same direction `nodeEntries` takes, because an output path cannot say which
|
|
28
|
+
* include base produced it. Both script kinds are resolved and their contexts
|
|
29
|
+
* unioned: a `.ts` is named as a `.script` by a game object and as a
|
|
30
|
+
* `.gui_script` by a `.gui`, and nothing in the file itself says which.
|
|
31
|
+
*/
|
|
32
|
+
export declare function relativeUniverseFor(cache: SceneIndexCache, fileName: string): RelativeUniverse;
|
|
33
|
+
/**
|
|
34
|
+
* The worlds the file's script runs in, as `checkCrossWorldAddresses` asks for
|
|
35
|
+
* them: the de-duplicated sockets of every naming context hosting it, with the
|
|
36
|
+
* bootstrap world carried as `undefined`.
|
|
37
|
+
*
|
|
38
|
+
* The same answer `scriptWorldsForBuild` gives the CLI, reached through the
|
|
39
|
+
* chain that resolver was modelled on — so an address the build calls foreign is
|
|
40
|
+
* foreign in the editor too. An empty answer is the honest unknown: a script no
|
|
41
|
+
* scene hosts runs in no world this project can prove, and the check reports
|
|
42
|
+
* nothing for it.
|
|
43
|
+
*/
|
|
44
|
+
export declare function worldsFor(cache: SceneIndexCache, fileName: string): readonly (string | undefined)[];
|
|
45
|
+
/**
|
|
46
|
+
* Whether a bare `/path` can resolve from where this file runs. A bare address
|
|
47
|
+
* continues the caller's own world, so one offered to a script running behind a
|
|
48
|
+
* proxy names an object that world cannot reach.
|
|
49
|
+
*
|
|
50
|
+
* A file with no naming context offers them: nothing is known about where it
|
|
51
|
+
* runs, and withholding every bare path would delete the completions the
|
|
52
|
+
* absolute universe has always provided. Socket-qualified keys are never
|
|
53
|
+
* touched — whether a slot accepts a foreign socket is a different question.
|
|
54
|
+
*/
|
|
55
|
+
export declare function offersBareWorld(universe: RelativeUniverse): boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defold-typescript/tstl-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "TypeScript language-service plugin surfacing Defold transpiler diagnostics live in the editor.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"test": "bun test"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@defold-typescript/transpiler": "0.
|
|
41
|
-
"@defold-typescript/types": "0.
|
|
40
|
+
"@defold-typescript/transpiler": "0.32.0",
|
|
41
|
+
"@defold-typescript/types": "0.32.0"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"typescript": ">=5.0.0 <7"
|
package/src/index.ts
CHANGED
|
@@ -2,12 +2,12 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import {
|
|
4
4
|
ANIMATION_ASSET_EXTENSIONS,
|
|
5
|
+
buildComponentAnimationIndex,
|
|
5
6
|
buildConfigKeyIndex,
|
|
7
|
+
buildGuiFlipbookIndex,
|
|
6
8
|
buildGuiNodeIndex,
|
|
7
9
|
buildInputActionIndex,
|
|
8
|
-
buildSceneComponentIndex,
|
|
9
10
|
buildSceneObjectPathIndex,
|
|
10
|
-
buildSpriteAnimationIndex,
|
|
11
11
|
type ClassifiedSlot,
|
|
12
12
|
componentIdOfSameObjectAddress,
|
|
13
13
|
computeOutputRel,
|
|
@@ -31,12 +31,16 @@ import {
|
|
|
31
31
|
CONTRIBUTED_ENTRY_KIND,
|
|
32
32
|
DEFOLD_COMPLETION_SOURCE,
|
|
33
33
|
} from "./scene-completions";
|
|
34
|
+
import { sceneReachabilityDiagnostics } from "./scene-diagnostics";
|
|
34
35
|
import {
|
|
35
36
|
createSceneIndexCache,
|
|
36
37
|
type SceneIndexCache,
|
|
37
38
|
type SceneWatchHost,
|
|
39
|
+
sceneCollectionRolesOf,
|
|
40
|
+
sceneComponentIndexOf,
|
|
38
41
|
} from "./scene-index-cache";
|
|
39
|
-
import { resolveEntryProvenance } from "./scene-provenance";
|
|
42
|
+
import { resolveEntryProvenance, resolveRelativeEntryProvenance } from "./scene-provenance";
|
|
43
|
+
import { offersBareWorld, relativeUniverseFor } from "./scene-relative-addresses";
|
|
40
44
|
|
|
41
45
|
const requireFromHere = createRequire(import.meta.url);
|
|
42
46
|
|
|
@@ -76,35 +80,50 @@ function componentEntries(
|
|
|
76
80
|
// suggestion claims nothing about what is absent.
|
|
77
81
|
return buildSceneCompletionEntries({
|
|
78
82
|
slot,
|
|
79
|
-
ids: cache.
|
|
80
|
-
"component-ids",
|
|
81
|
-
() => buildSceneComponentIndex(cache.documents().documents).ids,
|
|
82
|
-
),
|
|
83
|
+
ids: sceneComponentIndexOf(cache).ids,
|
|
83
84
|
baseEntries,
|
|
84
85
|
});
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
// The exact complement of `componentEntries`' guard, so precisely one of the two
|
|
88
|
-
// universes answers any caret in an address.
|
|
89
|
-
// universe, and for the same reason: what a path resolves to
|
|
90
|
-
// on the collection that was loaded, which the file being
|
|
89
|
+
// universes answers any caret in an address. The absolute half is project-wide
|
|
90
|
+
// like the component universe, and for the same reason: what a path resolves to
|
|
91
|
+
// at runtime depends on the collection that was loaded, which the file being
|
|
92
|
+
// edited does not say. The relative half cannot be — a relative address
|
|
93
|
+
// continues the collection path of the object hosting *this* script — so it is
|
|
94
|
+
// read per file and merged in, and the caller's own world decides whether a bare
|
|
95
|
+
// absolute path can resolve at all.
|
|
91
96
|
function objectPathEntries(
|
|
92
97
|
slot: ClassifiedSlot,
|
|
93
98
|
position: number,
|
|
94
99
|
cache: SceneIndexCache,
|
|
100
|
+
fileName: string,
|
|
95
101
|
baseEntries: readonly ts.CompletionEntry[],
|
|
96
102
|
): ts.CompletionEntry[] {
|
|
97
103
|
if (isFragmentCaret(slot, position)) {
|
|
98
104
|
return [];
|
|
99
105
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
const universe = relativeUniverseFor(cache, fileName);
|
|
107
|
+
const bare = offersBareWorld(universe);
|
|
108
|
+
const absolute = cache.derived(
|
|
109
|
+
"object-paths",
|
|
110
|
+
() =>
|
|
111
|
+
buildSceneObjectPathIndex(cache.documents().documents, sceneCollectionRolesOf(cache)).paths,
|
|
112
|
+
);
|
|
113
|
+
const paths = new Set<string>();
|
|
114
|
+
for (const key of absolute) {
|
|
115
|
+
if (bare || !key.startsWith("/")) paths.add(key);
|
|
116
|
+
}
|
|
117
|
+
for (const key of universe.paths) paths.add(key);
|
|
118
|
+
const entries = buildAddressPathCompletionEntries({ slot, paths, baseEntries });
|
|
119
|
+
// A join is the whole literal, so accepting one where the author already typed
|
|
120
|
+
// a `#` would silently rewrite the fragment they are holding.
|
|
121
|
+
if (slot.fragmentStart === -1) {
|
|
122
|
+
entries.push(
|
|
123
|
+
...buildWholeLiteralCompletionEntries({ slot, ids: universe.addresses, baseEntries }),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return entries;
|
|
108
127
|
}
|
|
109
128
|
|
|
110
129
|
// No caret guard: the span is the whole literal, so an entry is well-formed
|
|
@@ -131,24 +150,46 @@ function nodeEntries(
|
|
|
131
150
|
}
|
|
132
151
|
|
|
133
152
|
// Also no caret guard, and the same forward output-path mapping — but scoped
|
|
134
|
-
// one step further than a node id
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
153
|
+
// one step further than a node id, in whichever of the two ways the slot's own
|
|
154
|
+
// entry declares. Every unresolved link returns nothing rather than a
|
|
155
|
+
// project-wide guess, because an animation id the resolved atlas does not
|
|
156
|
+
// declare is a runtime crash.
|
|
157
|
+
//
|
|
158
|
+
// The branch is the table's own discriminator rather than a second arm in the
|
|
159
|
+
// class chain: an `animation` entry names either an address companion or a node
|
|
160
|
+
// companion, and the drift guard is what keeps it to exactly one.
|
|
139
161
|
function animationEntries(
|
|
140
162
|
slot: ClassifiedSlot,
|
|
141
163
|
cache: SceneIndexCache,
|
|
142
164
|
fileName: string,
|
|
143
165
|
baseEntries: readonly ts.CompletionEntry[],
|
|
144
166
|
): ts.CompletionEntry[] {
|
|
167
|
+
const { projectRoot } = cache;
|
|
168
|
+
// Scoped to the `.gui` scene naming this file's generated gui script, never
|
|
169
|
+
// to the node the call addresses: `gui.set_texture` retargets a node at
|
|
170
|
+
// runtime, so the scene's textures are the honest universe.
|
|
171
|
+
if (slot.nodeParameter !== undefined) {
|
|
172
|
+
const index = cache.derived("gui-flipbook", () =>
|
|
173
|
+
buildGuiFlipbookIndex({
|
|
174
|
+
scenes: cache.documents(GUI_EXTENSIONS).documents,
|
|
175
|
+
assets: cache.documents(ANIMATION_ASSET_EXTENSIONS).documents,
|
|
176
|
+
}),
|
|
177
|
+
);
|
|
178
|
+
const config = readBuildConfigFromHost(cache.host, projectRoot);
|
|
179
|
+
const resource = computeOutputRel(displayPathOf(projectRoot, fileName), config, "gui-script");
|
|
180
|
+
const declared = index.byScriptResource.get(resource);
|
|
181
|
+
return declared === undefined
|
|
182
|
+
? []
|
|
183
|
+
: buildWholeLiteralCompletionEntries({ slot, ids: new Set(declared.keys()), baseEntries });
|
|
184
|
+
}
|
|
185
|
+
// Scoped to the sprite or model component the slot's *sibling* literal
|
|
186
|
+
// addresses on the one game object that owns this script.
|
|
145
187
|
const component = componentIdOfSameObjectAddress(slot.addressText ?? "");
|
|
146
188
|
if (component === undefined) {
|
|
147
189
|
return [];
|
|
148
190
|
}
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
buildSpriteAnimationIndex({
|
|
191
|
+
const index = cache.derived("component-animations", () =>
|
|
192
|
+
buildComponentAnimationIndex({
|
|
152
193
|
scenes: cache.documents().documents,
|
|
153
194
|
assets: cache.documents(ANIMATION_ASSET_EXTENSIONS).documents,
|
|
154
195
|
}),
|
|
@@ -210,23 +251,17 @@ function actionIdEntries(
|
|
|
210
251
|
}
|
|
211
252
|
|
|
212
253
|
// The panel a claimed request is answered with. `documentation` rather than
|
|
213
|
-
// `displayParts` carries the
|
|
214
|
-
// body of the panel, which is where a list of files reads as
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
): ts.CompletionEntryDetails {
|
|
254
|
+
// `displayParts` carries the sentence because a host renders the former as the
|
|
255
|
+
// body of the panel, which is where a list of files — or of objects — reads as
|
|
256
|
+
// one. The sentence is the caller's, since an absolute entry is answered with
|
|
257
|
+
// the files declaring it and a relative one with the objects it resolves from.
|
|
258
|
+
function provenancePanel(entryName: string, documentation: string): ts.CompletionEntryDetails {
|
|
219
259
|
return {
|
|
220
260
|
name: entryName,
|
|
221
261
|
kind: CONTRIBUTED_ENTRY_KIND,
|
|
222
262
|
kindModifiers: "",
|
|
223
263
|
displayParts: [{ kind: "stringLiteral", text: JSON.stringify(entryName) }],
|
|
224
|
-
documentation: [
|
|
225
|
-
{
|
|
226
|
-
kind: "text",
|
|
227
|
-
text: `Declared in ${declaredIn.join(", ")}`,
|
|
228
|
-
},
|
|
229
|
-
],
|
|
264
|
+
documentation: [{ kind: "text", text: documentation }],
|
|
230
265
|
source: [{ kind: "text", text: DEFOLD_COMPLETION_SOURCE }],
|
|
231
266
|
};
|
|
232
267
|
}
|
|
@@ -304,8 +339,23 @@ export default function init(modules: { typescript: typeof import("typescript")
|
|
|
304
339
|
if (!slot) {
|
|
305
340
|
return forward();
|
|
306
341
|
}
|
|
342
|
+
// Tried first because the two universes are disjoint by construction and
|
|
343
|
+
// only this one knows which object an entry is relative to; a name it does
|
|
344
|
+
// not carry falls straight through to the declaring-file answer.
|
|
345
|
+
const relative = resolveRelativeEntryProvenance({
|
|
346
|
+
slot,
|
|
347
|
+
position,
|
|
348
|
+
cache,
|
|
349
|
+
fileName,
|
|
350
|
+
entryName,
|
|
351
|
+
});
|
|
352
|
+
if (relative !== undefined) {
|
|
353
|
+
return provenancePanel(entryName, relative);
|
|
354
|
+
}
|
|
307
355
|
const declaredIn = resolveEntryProvenance({ slot, position, cache, fileName, entryName });
|
|
308
|
-
return declaredIn.length === 0
|
|
356
|
+
return declaredIn.length === 0
|
|
357
|
+
? forward()
|
|
358
|
+
: provenancePanel(entryName, `Declared in ${declaredIn.join(", ")}`);
|
|
309
359
|
};
|
|
310
360
|
|
|
311
361
|
proxy.getSemanticDiagnostics = (fileName: string): ts.Diagnostic[] => {
|
|
@@ -319,7 +369,13 @@ export default function init(modules: { typescript: typeof import("typescript")
|
|
|
319
369
|
const transpiler = getProgramDiagnostics(program, program.getSourceFile(fileName)).map(
|
|
320
370
|
(diagnostic) => ({ ...diagnostic, category: ts.DiagnosticCategory.Suggestion }),
|
|
321
371
|
);
|
|
322
|
-
|
|
372
|
+
// The same advisory posture, and the same absent-cache early return the
|
|
373
|
+
// completion path takes: a host that cannot enumerate files has no scene
|
|
374
|
+
// universe, and an empty one would report every address as unreachable.
|
|
375
|
+
const table = loadUrlParameterTable();
|
|
376
|
+
const scene =
|
|
377
|
+
cache && table ? sceneReachabilityDiagnostics({ ts, program, table, cache, fileName }) : [];
|
|
378
|
+
return [...prior, ...transpiler, ...scene];
|
|
323
379
|
};
|
|
324
380
|
|
|
325
381
|
// Strictly additive: every path that cannot produce a suggestion returns the
|
|
@@ -354,7 +410,7 @@ export default function init(modules: { typescript: typeof import("typescript")
|
|
|
354
410
|
const entries = isAddressClass(slot.class)
|
|
355
411
|
? [
|
|
356
412
|
...componentEntries(slot, position, cache, baseEntries),
|
|
357
|
-
...objectPathEntries(slot, position, cache, baseEntries),
|
|
413
|
+
...objectPathEntries(slot, position, cache, fileName, baseEntries),
|
|
358
414
|
]
|
|
359
415
|
: slot.class === "gui-node"
|
|
360
416
|
? nodeEntries(slot, cache, fileName, baseEntries)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import {
|
|
2
|
+
checkCrossWorldAddresses,
|
|
3
|
+
checkUrlFragmentReachability,
|
|
4
|
+
} from "@defold-typescript/transpiler";
|
|
5
|
+
import type { UrlParameterTable } from "@defold-typescript/types";
|
|
6
|
+
import type * as ts from "typescript";
|
|
7
|
+
import { type SceneIndexCache, sceneComponentIndexOf } from "./scene-index-cache";
|
|
8
|
+
import { projectObjectIndex, worldsFor } from "./scene-relative-addresses";
|
|
9
|
+
|
|
10
|
+
/** The `source` an editor renders beside the message, and `--json` never sees. */
|
|
11
|
+
export const DEFOLD_DIAGNOSTIC_SOURCE = "defold-typescript";
|
|
12
|
+
|
|
13
|
+
// A range neither TypeScript (< 100000) nor tstl occupies, so a host filtering
|
|
14
|
+
// by code can tell the two address findings apart from each other and from
|
|
15
|
+
// everything else the language service reports.
|
|
16
|
+
const UNREACHABLE_FRAGMENT_CODE = 9_000_101;
|
|
17
|
+
const FOREIGN_SOCKET_CODE = 9_000_102;
|
|
18
|
+
|
|
19
|
+
function suggestion(
|
|
20
|
+
typescript: typeof import("typescript"),
|
|
21
|
+
file: ts.SourceFile,
|
|
22
|
+
finding: { readonly start: number; readonly length: number; readonly message: string },
|
|
23
|
+
code: number,
|
|
24
|
+
): ts.Diagnostic {
|
|
25
|
+
return {
|
|
26
|
+
file,
|
|
27
|
+
start: finding.start,
|
|
28
|
+
length: finding.length,
|
|
29
|
+
messageText: finding.message,
|
|
30
|
+
category: typescript.DiagnosticCategory.Suggestion,
|
|
31
|
+
source: DEFOLD_DIAGNOSTIC_SOURCE,
|
|
32
|
+
code,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The address findings the editor surfaces on the file being edited, as
|
|
38
|
+
* advisory suggestions.
|
|
39
|
+
*
|
|
40
|
+
* Every path that cannot produce one answers with an empty array. A suppressed
|
|
41
|
+
* reachability report is the important case: there is no file span on which a
|
|
42
|
+
* "the check did not run" notice could honestly sit, and a speculative squiggle
|
|
43
|
+
* is worse than none — the build says so in prose instead, where a whole-project
|
|
44
|
+
* line has somewhere to go.
|
|
45
|
+
*
|
|
46
|
+
* The *whole* hole is one index, composed the way `sceneIndexForBuild` composes
|
|
47
|
+
* it: what the parse could not read plus what the walk could not reach. The
|
|
48
|
+
* completion path deliberately reads a partial universe — a suggestion claims
|
|
49
|
+
* nothing about what is absent — but a finding does, so a component declared
|
|
50
|
+
* only by an unresolved dependency suppresses the check rather than reporting
|
|
51
|
+
* the address as unreachable.
|
|
52
|
+
*/
|
|
53
|
+
export function sceneReachabilityDiagnostics(input: {
|
|
54
|
+
ts: typeof import("typescript");
|
|
55
|
+
program: ts.Program;
|
|
56
|
+
table: UrlParameterTable;
|
|
57
|
+
cache: SceneIndexCache;
|
|
58
|
+
fileName: string;
|
|
59
|
+
}): ts.Diagnostic[] {
|
|
60
|
+
const { ts: typescript, program, table, cache, fileName } = input;
|
|
61
|
+
const sourceFile = program.getSourceFile(fileName);
|
|
62
|
+
if (!sourceFile) {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
const index = sceneComponentIndexOf(cache);
|
|
66
|
+
const report = checkUrlFragmentReachability({
|
|
67
|
+
program,
|
|
68
|
+
table,
|
|
69
|
+
index: { ids: index.ids, incomplete: [...index.incomplete, ...cache.documents().unreadable] },
|
|
70
|
+
objects: projectObjectIndex(cache),
|
|
71
|
+
sourceFiles: [sourceFile],
|
|
72
|
+
});
|
|
73
|
+
const fragments =
|
|
74
|
+
report.kind === "suppressed"
|
|
75
|
+
? []
|
|
76
|
+
: report.findings.map((finding) =>
|
|
77
|
+
suggestion(typescript, sourceFile, finding, UNREACHABLE_FRAGMENT_CODE),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// Outside the branch above, never inside it: the two honesty rules are
|
|
81
|
+
// independent — one suppresses index-wide on an incomplete component universe,
|
|
82
|
+
// the other withholds per file on an unresolvable world — which is the reason
|
|
83
|
+
// the transpiler kept them as two exports rather than one.
|
|
84
|
+
const crossWorld = checkCrossWorldAddresses({
|
|
85
|
+
program,
|
|
86
|
+
table,
|
|
87
|
+
worldsOf: (name) => worldsFor(cache, name),
|
|
88
|
+
sourceFiles: [sourceFile],
|
|
89
|
+
}).map((finding) => suggestion(typescript, sourceFile, finding, FOREIGN_SOCKET_CODE));
|
|
90
|
+
|
|
91
|
+
return [...fragments, ...crossWorld];
|
|
92
|
+
}
|
package/src/scene-index-cache.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
|
+
buildSceneCollectionRoles,
|
|
3
|
+
buildSceneComponentIndex,
|
|
4
|
+
COLLECTION_REFERENCE_EXTENSIONS,
|
|
2
5
|
displayPathOf,
|
|
6
|
+
GAME_PROJECT_DOCUMENT,
|
|
3
7
|
isExcludedProjectPath,
|
|
8
|
+
LIBRARY_DEPENDENCY_ROOT,
|
|
4
9
|
listProjectResourcePaths,
|
|
10
|
+
PROJECT_EXTENSIONS,
|
|
5
11
|
readSceneDocuments,
|
|
6
12
|
SCENE_EXTENSIONS,
|
|
13
|
+
type SceneCollectionRoles,
|
|
14
|
+
type SceneComponentIndex,
|
|
7
15
|
type SceneReadHost,
|
|
8
16
|
} from "@defold-typescript/transpiler";
|
|
9
17
|
import type * as ts from "typescript";
|
|
@@ -81,6 +89,19 @@ export function createSceneIndexCache(host: SceneWatchHost, projectRoot: string)
|
|
|
81
89
|
|
|
82
90
|
function affectsCache(hostPath: string): boolean {
|
|
83
91
|
const displayPath = displayPathOf(projectRoot, hostPath);
|
|
92
|
+
// `resolve` rewrites this surface wholesale, and `.defold-types/` is
|
|
93
|
+
// `.defignore`d — so the exclusion test below would throw the event away and
|
|
94
|
+
// leave the editor offering ids from a dependency the author just removed.
|
|
95
|
+
// The manifest counts too: it alone changes when a dependency is dropped.
|
|
96
|
+
// The root itself, not only its descendants: removing the last dependency
|
|
97
|
+
// deletes the whole directory and writes nothing inside it, so that event is
|
|
98
|
+
// the only one a watcher reports.
|
|
99
|
+
if (
|
|
100
|
+
displayPath === LIBRARY_DEPENDENCY_ROOT ||
|
|
101
|
+
displayPath.startsWith(`${LIBRARY_DEPENDENCY_ROOT}/`)
|
|
102
|
+
) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
84
105
|
if (isExcludedProjectPath(displayPath)) return false;
|
|
85
106
|
for (const extension of servedExtensions) {
|
|
86
107
|
if (displayPath.endsWith(extension)) return true;
|
|
@@ -166,3 +187,30 @@ export function createSceneIndexCache(host: SceneWatchHost, projectRoot: string)
|
|
|
166
187
|
},
|
|
167
188
|
};
|
|
168
189
|
}
|
|
190
|
+
|
|
191
|
+
// The component-id universe, whole — ids *and* the walk's own record of what it
|
|
192
|
+
// could not prove — read through the cache so one walk serves both readers. The
|
|
193
|
+
// two disagree about what to do with the hole rather than about how to build it:
|
|
194
|
+
// a completion suggests from a partial universe, a finding must not, so the
|
|
195
|
+
// index is shared here and each caller applies its own rule.
|
|
196
|
+
export function sceneComponentIndexOf(cache: SceneIndexCache): SceneComponentIndex {
|
|
197
|
+
return cache.derived("component-index", () =>
|
|
198
|
+
buildSceneComponentIndex(cache.documents().documents),
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// The world each collection is, read through the cache so the completion path
|
|
203
|
+
// and the provenance path share one classification — and so the walk over the
|
|
204
|
+
// proxy/factory documents and `game.project` is invalidated by the same
|
|
205
|
+
// watchers the scene walk is. Both call sites go through here rather than
|
|
206
|
+
// building roles of their own, which is what would let one of them drift back
|
|
207
|
+
// to offering every collection's paths bare.
|
|
208
|
+
export function sceneCollectionRolesOf(cache: SceneIndexCache): SceneCollectionRoles {
|
|
209
|
+
return cache.derived("collection-roles", () =>
|
|
210
|
+
buildSceneCollectionRoles({
|
|
211
|
+
documents: cache.documents().documents,
|
|
212
|
+
references: cache.documents(COLLECTION_REFERENCE_EXTENSIONS).documents,
|
|
213
|
+
gameProject: cache.documents(PROJECT_EXTENSIONS).documents.get(GAME_PROJECT_DOCUMENT),
|
|
214
|
+
}),
|
|
215
|
+
);
|
|
216
|
+
}
|
package/src/scene-provenance.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ANIMATION_ASSET_EXTENSIONS,
|
|
3
|
+
buildComponentAnimationIndex,
|
|
3
4
|
buildConfigKeyIndex,
|
|
5
|
+
buildGuiFlipbookIndex,
|
|
4
6
|
buildGuiNodeIndex,
|
|
5
7
|
buildInputActionIndex,
|
|
6
8
|
buildSceneComponentIndex,
|
|
7
9
|
buildSceneObjectPathIndex,
|
|
8
|
-
buildSpriteAnimationIndex,
|
|
9
10
|
type ClassifiedSlot,
|
|
10
11
|
componentIdOfSameObjectAddress,
|
|
11
12
|
computeOutputRel,
|
|
@@ -18,7 +19,8 @@ import {
|
|
|
18
19
|
PROJECT_EXTENSIONS,
|
|
19
20
|
} from "@defold-typescript/transpiler";
|
|
20
21
|
import { readBuildConfigFromHost } from "./build-config";
|
|
21
|
-
import type
|
|
22
|
+
import { type SceneIndexCache, sceneCollectionRolesOf } from "./scene-index-cache";
|
|
23
|
+
import { relativeUniverseFor } from "./scene-relative-addresses";
|
|
22
24
|
|
|
23
25
|
// Keys of their own under the same cache the completion indexes use, so a
|
|
24
26
|
// provenance map is thrown away with every index it was derived beside — and
|
|
@@ -27,7 +29,8 @@ const COMPONENT_PROVENANCE = "provenance:component-ids";
|
|
|
27
29
|
const ACTION_PROVENANCE = "provenance:input-actions";
|
|
28
30
|
const GUI_PROVENANCE = "provenance:gui-nodes";
|
|
29
31
|
const PATH_PROVENANCE = "provenance:object-paths";
|
|
30
|
-
const ANIMATION_PROVENANCE = "provenance:
|
|
32
|
+
const ANIMATION_PROVENANCE = "provenance:component-animations";
|
|
33
|
+
const GUI_ANIMATION_PROVENANCE = "provenance:gui-flipbook";
|
|
31
34
|
|
|
32
35
|
type Provenance = ReadonlyMap<string, readonly string[]>;
|
|
33
36
|
|
|
@@ -123,16 +126,48 @@ function nodeProvenance(cache: SceneIndexCache, fileName: string): Provenance {
|
|
|
123
126
|
function pathProvenance(cache: SceneIndexCache): Provenance {
|
|
124
127
|
return cache.derived(
|
|
125
128
|
PATH_PROVENANCE,
|
|
126
|
-
() =>
|
|
129
|
+
() =>
|
|
130
|
+
buildSceneObjectPathIndex(cache.documents().documents, sceneCollectionRolesOf(cache))
|
|
131
|
+
.declaredIn,
|
|
127
132
|
);
|
|
128
133
|
}
|
|
129
134
|
|
|
130
|
-
// Scoped exactly the way the
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
|
|
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.
|
|
136
171
|
function animationProvenance(input: {
|
|
137
172
|
slot: ClassifiedSlot;
|
|
138
173
|
cache: SceneIndexCache;
|
|
@@ -140,19 +175,21 @@ function animationProvenance(input: {
|
|
|
140
175
|
entryName: string;
|
|
141
176
|
}): readonly string[] {
|
|
142
177
|
const { slot, cache, fileName, entryName } = input;
|
|
178
|
+
if (slot.nodeParameter !== undefined) {
|
|
179
|
+
return guiAnimationProvenance({ cache, fileName, entryName });
|
|
180
|
+
}
|
|
143
181
|
const component = componentIdOfSameObjectAddress(slot.addressText ?? "");
|
|
144
182
|
if (component === undefined) return [];
|
|
145
183
|
const index = cache.derived(ANIMATION_PROVENANCE, () =>
|
|
146
|
-
|
|
184
|
+
buildComponentAnimationIndex({
|
|
147
185
|
scenes: cache.documents().documents,
|
|
148
186
|
assets: cache.documents(ANIMATION_ASSET_EXTENSIONS).documents,
|
|
149
187
|
}),
|
|
150
188
|
);
|
|
151
189
|
const config = readBuildConfigFromHost(cache.host, cache.projectRoot);
|
|
152
190
|
const resource = computeOutputRel(displayPathOf(cache.projectRoot, fileName), config, "script");
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
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];
|
|
156
193
|
}
|
|
157
194
|
|
|
158
195
|
// A config key is declared by the one file that answers a reader at runtime, and
|
|
@@ -175,6 +212,37 @@ function constantProvenance(
|
|
|
175
212
|
return cache.resourcePaths(extensions).has(entryName) ? [entryName.replace(/^\//, "")] : [];
|
|
176
213
|
}
|
|
177
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
|
+
|
|
178
246
|
// The display paths of the project files that declare `entryName` in the
|
|
179
247
|
// universe this slot draws from, sorted, empty when the project does not name
|
|
180
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
|
+
}
|