@defold-typescript/tstl-plugin 0.24.0 → 0.25.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/build-config.d.ts +3 -0
- package/dist/index.js +524 -1
- package/dist/scene-completions.d.ts +19 -0
- package/dist/scene-documents.d.ts +18 -0
- package/dist/scene-index-cache.d.ts +15 -0
- package/dist/scene-provenance.d.ts +9 -0
- package/package.json +3 -2
- package/src/build-config.ts +24 -0
- package/src/index.ts +349 -1
- package/src/scene-completions.ts +114 -0
- package/src/scene-documents.ts +126 -0
- package/src/scene-index-cache.ts +168 -0
- package/src/scene-provenance.ts +215 -0
package/dist/index.js
CHANGED
|
@@ -1,5 +1,472 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import {
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import {
|
|
5
|
+
buildConfigKeyIndex as buildConfigKeyIndex2,
|
|
6
|
+
buildGuiNodeIndex as buildGuiNodeIndex2,
|
|
7
|
+
buildInputActionIndex as buildInputActionIndex2,
|
|
8
|
+
buildSceneComponentIndex as buildSceneComponentIndex2,
|
|
9
|
+
buildSceneObjectPathIndex as buildSceneObjectPathIndex2,
|
|
10
|
+
buildSpriteAnimationIndex as buildSpriteAnimationIndex2,
|
|
11
|
+
componentIdOfSameObjectAddress as componentIdOfSameObjectAddress2,
|
|
12
|
+
computeOutputRel as computeOutputRel2,
|
|
13
|
+
getProgramDiagnostics,
|
|
14
|
+
isAddressClass as isAddressClass2,
|
|
15
|
+
isFragmentCaret as isFragmentCaret2,
|
|
16
|
+
resolveClassifiedSlotAtPosition
|
|
17
|
+
} from "@defold-typescript/transpiler";
|
|
18
|
+
|
|
19
|
+
// src/build-config.ts
|
|
20
|
+
import { DEFAULT_INCLUDE, parseBuildConfig } from "@defold-typescript/transpiler";
|
|
21
|
+
var ALONGSIDE = { outDir: undefined, include: DEFAULT_INCLUDE };
|
|
22
|
+
function readBuildConfigFromHost(host, projectRoot) {
|
|
23
|
+
const root = projectRoot.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
24
|
+
const text = host.readFile(`${root}/tsconfig.json`);
|
|
25
|
+
if (text === undefined) {
|
|
26
|
+
return ALONGSIDE;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
return parseBuildConfig(text);
|
|
30
|
+
} catch {
|
|
31
|
+
return ALONGSIDE;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/scene-completions.ts
|
|
36
|
+
var CONTRIBUTED_ENTRY_KIND = "string";
|
|
37
|
+
function keyAboveAll(baseEntries) {
|
|
38
|
+
return `${baseEntries.reduce((greatest, base) => base.sortText > greatest ? base.sortText : greatest, "zz")}0`;
|
|
39
|
+
}
|
|
40
|
+
var DEFOLD_COMPLETION_SOURCE = "defold-typescript/scene";
|
|
41
|
+
function entriesFor(ids, offeredName, baseEntries, replacementSpan) {
|
|
42
|
+
const offered = new Set(baseEntries.map((base) => base.name));
|
|
43
|
+
const sortText = keyAboveAll(baseEntries);
|
|
44
|
+
return [...ids].sort().filter((id) => !offered.has(offeredName(id))).map((id) => ({
|
|
45
|
+
name: id,
|
|
46
|
+
kind: CONTRIBUTED_ENTRY_KIND,
|
|
47
|
+
kindModifiers: "",
|
|
48
|
+
sortText,
|
|
49
|
+
replacementSpan,
|
|
50
|
+
source: DEFOLD_COMPLETION_SOURCE
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
function buildSceneCompletionEntries(input) {
|
|
54
|
+
const { slot, ids, baseEntries } = input;
|
|
55
|
+
if (slot.fragmentStart === -1)
|
|
56
|
+
return [];
|
|
57
|
+
const fragmentOffset = slot.fragmentStart - slot.textStart;
|
|
58
|
+
const prefix = slot.text.slice(0, fragmentOffset);
|
|
59
|
+
return entriesFor(ids, (id) => `${prefix}${id}`, baseEntries, {
|
|
60
|
+
start: slot.fragmentStart,
|
|
61
|
+
length: slot.text.length - fragmentOffset
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function buildAddressPathCompletionEntries(input) {
|
|
65
|
+
const { slot, paths, baseEntries } = input;
|
|
66
|
+
const length = slot.fragmentStart === -1 ? slot.text.length : slot.fragmentStart - slot.textStart - 1;
|
|
67
|
+
const suffix = slot.text.slice(length);
|
|
68
|
+
return entriesFor(paths, (path) => `${path}${suffix}`, baseEntries, {
|
|
69
|
+
start: slot.textStart,
|
|
70
|
+
length
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function buildWholeLiteralCompletionEntries(input) {
|
|
74
|
+
const { slot, ids, baseEntries } = input;
|
|
75
|
+
return entriesFor(ids, (id) => id, baseEntries, {
|
|
76
|
+
start: slot.textStart,
|
|
77
|
+
length: slot.text.length
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/scene-documents.ts
|
|
82
|
+
import { isDefignoredPath } from "@defold-typescript/transpiler";
|
|
83
|
+
var SCENE_EXTENSIONS = [".go", ".collection"];
|
|
84
|
+
var GUI_EXTENSIONS = [".gui"];
|
|
85
|
+
var INPUT_BINDING_EXTENSIONS = [".input_binding"];
|
|
86
|
+
var ANIMATION_ASSET_EXTENSIONS = [".atlas", ".tilesource", ".sprite"];
|
|
87
|
+
var PROJECT_EXTENSIONS = [".project"];
|
|
88
|
+
var GAME_PROJECT_DOCUMENT = "game.project";
|
|
89
|
+
function displayPathOf(projectRoot, filePath) {
|
|
90
|
+
const path = filePath.replace(/\\/g, "/");
|
|
91
|
+
const root = projectRoot.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
92
|
+
return path.startsWith(`${root}/`) ? path.slice(root.length + 1) : path;
|
|
93
|
+
}
|
|
94
|
+
function isBuildOutput(displayPath) {
|
|
95
|
+
return displayPath.split("/").includes("build");
|
|
96
|
+
}
|
|
97
|
+
function isExcludedProjectPath(displayPath) {
|
|
98
|
+
return isBuildOutput(displayPath) || isDefignoredPath(displayPath);
|
|
99
|
+
}
|
|
100
|
+
function readSceneDocuments(host, projectRoot, extensions = SCENE_EXTENSIONS) {
|
|
101
|
+
const documents = new Map;
|
|
102
|
+
const unreadable = [];
|
|
103
|
+
const paths = [];
|
|
104
|
+
if (!host.readDirectory) {
|
|
105
|
+
unreadable.push("the editor host cannot enumerate project files, so no scene source was read");
|
|
106
|
+
return { documents, unreadable, paths };
|
|
107
|
+
}
|
|
108
|
+
for (const filePath of host.readDirectory(projectRoot, extensions)) {
|
|
109
|
+
const displayPath = displayPathOf(projectRoot, filePath);
|
|
110
|
+
if (isExcludedProjectPath(displayPath))
|
|
111
|
+
continue;
|
|
112
|
+
const text = host.readFile(filePath);
|
|
113
|
+
if (text === undefined) {
|
|
114
|
+
unreadable.push(`${displayPath}: could not be read`);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
documents.set(displayPath, text);
|
|
118
|
+
paths.push(filePath);
|
|
119
|
+
}
|
|
120
|
+
return { documents, unreadable, paths };
|
|
121
|
+
}
|
|
122
|
+
function listProjectResourcePaths(host, projectRoot, extensions) {
|
|
123
|
+
if (!host.readDirectory)
|
|
124
|
+
return new Set;
|
|
125
|
+
const paths = [];
|
|
126
|
+
for (const filePath of host.readDirectory(projectRoot, extensions)) {
|
|
127
|
+
const displayPath = displayPathOf(projectRoot, filePath);
|
|
128
|
+
if (isExcludedProjectPath(displayPath))
|
|
129
|
+
continue;
|
|
130
|
+
if (!extensions.some((extension) => displayPath.endsWith(extension)))
|
|
131
|
+
continue;
|
|
132
|
+
paths.push(`/${displayPath}`);
|
|
133
|
+
}
|
|
134
|
+
return new Set(paths.sort());
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/scene-index-cache.ts
|
|
138
|
+
function cacheKey(extensions) {
|
|
139
|
+
return [...extensions].sort().join("|");
|
|
140
|
+
}
|
|
141
|
+
function createSceneIndexCache(host, projectRoot) {
|
|
142
|
+
const watchDirectory = host.watchDirectory;
|
|
143
|
+
const watchFile = host.watchFile;
|
|
144
|
+
const enabled = typeof watchDirectory === "function" && typeof watchFile === "function";
|
|
145
|
+
const documentCache = new Map;
|
|
146
|
+
const resourceCache = new Map;
|
|
147
|
+
const derivedCache = new Map;
|
|
148
|
+
const fileWatchers = new Map;
|
|
149
|
+
const servedExtensions = new Set;
|
|
150
|
+
let directoryWatcher;
|
|
151
|
+
let disposed = false;
|
|
152
|
+
function closeFileWatchers() {
|
|
153
|
+
for (const watcher of fileWatchers.values())
|
|
154
|
+
watcher.close();
|
|
155
|
+
fileWatchers.clear();
|
|
156
|
+
}
|
|
157
|
+
function invalidate() {
|
|
158
|
+
documentCache.clear();
|
|
159
|
+
resourceCache.clear();
|
|
160
|
+
derivedCache.clear();
|
|
161
|
+
closeFileWatchers();
|
|
162
|
+
}
|
|
163
|
+
function affectsCache(hostPath) {
|
|
164
|
+
const displayPath = displayPathOf(projectRoot, hostPath);
|
|
165
|
+
if (isExcludedProjectPath(displayPath))
|
|
166
|
+
return false;
|
|
167
|
+
for (const extension of servedExtensions) {
|
|
168
|
+
if (displayPath.endsWith(extension))
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
function onEvent(hostPath) {
|
|
174
|
+
if (disposed)
|
|
175
|
+
return;
|
|
176
|
+
if (affectsCache(hostPath))
|
|
177
|
+
invalidate();
|
|
178
|
+
}
|
|
179
|
+
function active() {
|
|
180
|
+
if (!enabled || disposed)
|
|
181
|
+
return false;
|
|
182
|
+
if (!directoryWatcher && watchDirectory) {
|
|
183
|
+
directoryWatcher = watchDirectory(projectRoot, onEvent, true);
|
|
184
|
+
}
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
function serve(extensions) {
|
|
188
|
+
for (const extension of extensions)
|
|
189
|
+
servedExtensions.add(extension);
|
|
190
|
+
}
|
|
191
|
+
function watchReadFiles(paths) {
|
|
192
|
+
if (!watchFile)
|
|
193
|
+
return;
|
|
194
|
+
for (const path of paths) {
|
|
195
|
+
if (fileWatchers.has(path))
|
|
196
|
+
continue;
|
|
197
|
+
fileWatchers.set(path, watchFile(path, (fileName) => onEvent(fileName)));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
host,
|
|
202
|
+
projectRoot,
|
|
203
|
+
documents(extensions = SCENE_EXTENSIONS) {
|
|
204
|
+
if (!active())
|
|
205
|
+
return readSceneDocuments(host, projectRoot, extensions);
|
|
206
|
+
const key = cacheKey(extensions);
|
|
207
|
+
const cached = documentCache.get(key);
|
|
208
|
+
if (cached)
|
|
209
|
+
return cached;
|
|
210
|
+
const result = readSceneDocuments(host, projectRoot, extensions);
|
|
211
|
+
documentCache.set(key, result);
|
|
212
|
+
serve(extensions);
|
|
213
|
+
watchReadFiles(result.paths);
|
|
214
|
+
return result;
|
|
215
|
+
},
|
|
216
|
+
resourcePaths(extensions) {
|
|
217
|
+
if (!active())
|
|
218
|
+
return listProjectResourcePaths(host, projectRoot, extensions);
|
|
219
|
+
const key = cacheKey(extensions);
|
|
220
|
+
const cached = resourceCache.get(key);
|
|
221
|
+
if (cached)
|
|
222
|
+
return cached;
|
|
223
|
+
const result = listProjectResourcePaths(host, projectRoot, extensions);
|
|
224
|
+
resourceCache.set(key, result);
|
|
225
|
+
serve(extensions);
|
|
226
|
+
return result;
|
|
227
|
+
},
|
|
228
|
+
derived(key, compute) {
|
|
229
|
+
if (!active())
|
|
230
|
+
return compute();
|
|
231
|
+
if (derivedCache.has(key))
|
|
232
|
+
return derivedCache.get(key);
|
|
233
|
+
const value = compute();
|
|
234
|
+
derivedCache.set(key, value);
|
|
235
|
+
return value;
|
|
236
|
+
},
|
|
237
|
+
dispose() {
|
|
238
|
+
if (disposed)
|
|
239
|
+
return;
|
|
240
|
+
disposed = true;
|
|
241
|
+
directoryWatcher?.close();
|
|
242
|
+
directoryWatcher = undefined;
|
|
243
|
+
closeFileWatchers();
|
|
244
|
+
documentCache.clear();
|
|
245
|
+
resourceCache.clear();
|
|
246
|
+
derivedCache.clear();
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/scene-provenance.ts
|
|
252
|
+
import {
|
|
253
|
+
buildConfigKeyIndex,
|
|
254
|
+
buildGuiNodeIndex,
|
|
255
|
+
buildInputActionIndex,
|
|
256
|
+
buildSceneComponentIndex,
|
|
257
|
+
buildSceneObjectPathIndex,
|
|
258
|
+
buildSpriteAnimationIndex,
|
|
259
|
+
componentIdOfSameObjectAddress,
|
|
260
|
+
computeOutputRel,
|
|
261
|
+
isAddressClass,
|
|
262
|
+
isFragmentCaret
|
|
263
|
+
} from "@defold-typescript/transpiler";
|
|
264
|
+
var COMPONENT_PROVENANCE = "provenance:component-ids";
|
|
265
|
+
var ACTION_PROVENANCE = "provenance:input-actions";
|
|
266
|
+
var GUI_PROVENANCE = "provenance:gui-nodes";
|
|
267
|
+
var PATH_PROVENANCE = "provenance:object-paths";
|
|
268
|
+
var ANIMATION_PROVENANCE = "provenance:sprite-animations";
|
|
269
|
+
function byDeclaringDocument(documents, idsOf) {
|
|
270
|
+
const declarers = new Map;
|
|
271
|
+
for (const [displayPath, text] of documents) {
|
|
272
|
+
for (const id of idsOf(new Map([[displayPath, text]]))) {
|
|
273
|
+
const paths = declarers.get(id);
|
|
274
|
+
if (paths) {
|
|
275
|
+
paths.push(displayPath);
|
|
276
|
+
} else {
|
|
277
|
+
declarers.set(id, [displayPath]);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
for (const paths of declarers.values())
|
|
282
|
+
paths.sort();
|
|
283
|
+
return declarers;
|
|
284
|
+
}
|
|
285
|
+
function byClaimedScript(documents) {
|
|
286
|
+
const byResource = new Map;
|
|
287
|
+
for (const [displayPath, text] of documents) {
|
|
288
|
+
const index = buildGuiNodeIndex(new Map([[displayPath, text]]));
|
|
289
|
+
for (const [resource, ids] of index.byScriptResource) {
|
|
290
|
+
let declarers = byResource.get(resource);
|
|
291
|
+
if (!declarers) {
|
|
292
|
+
declarers = new Map;
|
|
293
|
+
byResource.set(resource, declarers);
|
|
294
|
+
}
|
|
295
|
+
for (const id of ids) {
|
|
296
|
+
const paths = declarers.get(id);
|
|
297
|
+
if (paths) {
|
|
298
|
+
paths.push(displayPath);
|
|
299
|
+
} else {
|
|
300
|
+
declarers.set(id, [displayPath]);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
for (const declarers of byResource.values()) {
|
|
306
|
+
for (const paths of declarers.values())
|
|
307
|
+
paths.sort();
|
|
308
|
+
}
|
|
309
|
+
return byResource;
|
|
310
|
+
}
|
|
311
|
+
function componentProvenance(cache) {
|
|
312
|
+
return cache.derived(COMPONENT_PROVENANCE, () => byDeclaringDocument(cache.documents().documents, (narrowed) => buildSceneComponentIndex(narrowed).ids));
|
|
313
|
+
}
|
|
314
|
+
function actionProvenance(cache) {
|
|
315
|
+
return cache.derived(ACTION_PROVENANCE, () => byDeclaringDocument(cache.documents(INPUT_BINDING_EXTENSIONS).documents, (narrowed) => buildInputActionIndex(narrowed)));
|
|
316
|
+
}
|
|
317
|
+
function nodeProvenance(cache, fileName) {
|
|
318
|
+
const byResource = cache.derived(GUI_PROVENANCE, () => byClaimedScript(cache.documents(GUI_EXTENSIONS).documents));
|
|
319
|
+
const config = readBuildConfigFromHost(cache.host, cache.projectRoot);
|
|
320
|
+
const resource = computeOutputRel(displayPathOf(cache.projectRoot, fileName), config, "gui-script");
|
|
321
|
+
return byResource.get(resource) ?? new Map;
|
|
322
|
+
}
|
|
323
|
+
function pathProvenance(cache) {
|
|
324
|
+
return cache.derived(PATH_PROVENANCE, () => buildSceneObjectPathIndex(cache.documents().documents).declaredIn);
|
|
325
|
+
}
|
|
326
|
+
function animationProvenance(input) {
|
|
327
|
+
const { slot, cache, fileName, entryName } = input;
|
|
328
|
+
const component = componentIdOfSameObjectAddress(slot.addressText ?? "");
|
|
329
|
+
if (component === undefined)
|
|
330
|
+
return [];
|
|
331
|
+
const index = cache.derived(ANIMATION_PROVENANCE, () => buildSpriteAnimationIndex({
|
|
332
|
+
scenes: cache.documents().documents,
|
|
333
|
+
assets: cache.documents(ANIMATION_ASSET_EXTENSIONS).documents
|
|
334
|
+
}));
|
|
335
|
+
const config = readBuildConfigFromHost(cache.host, cache.projectRoot);
|
|
336
|
+
const resource = computeOutputRel(displayPathOf(cache.projectRoot, fileName), config, "script");
|
|
337
|
+
const tileSet = index.tileSetByScriptResource.get(resource)?.get(component);
|
|
338
|
+
if (tileSet === undefined)
|
|
339
|
+
return [];
|
|
340
|
+
return index.byScriptResource.get(resource)?.get(component)?.has(entryName) ? [tileSet] : [];
|
|
341
|
+
}
|
|
342
|
+
function constantProvenance(slot, cache, entryName) {
|
|
343
|
+
if (slot.class === "config-key") {
|
|
344
|
+
const text = cache.documents(PROJECT_EXTENSIONS).documents.get(GAME_PROJECT_DOCUMENT);
|
|
345
|
+
if (text === undefined)
|
|
346
|
+
return [];
|
|
347
|
+
const keys = cache.derived("config-keys", () => buildConfigKeyIndex(text));
|
|
348
|
+
return keys.has(entryName) ? [GAME_PROJECT_DOCUMENT] : [];
|
|
349
|
+
}
|
|
350
|
+
const extensions = slot.resourceExtensions;
|
|
351
|
+
if (extensions === undefined || extensions.length === 0)
|
|
352
|
+
return [];
|
|
353
|
+
return cache.resourcePaths(extensions).has(entryName) ? [entryName.replace(/^\//, "")] : [];
|
|
354
|
+
}
|
|
355
|
+
function resolveEntryProvenance(input) {
|
|
356
|
+
const { slot, position, cache, fileName, entryName } = input;
|
|
357
|
+
if (isAddressClass(slot.class)) {
|
|
358
|
+
if (!isFragmentCaret(slot, position)) {
|
|
359
|
+
return pathProvenance(cache).get(entryName) ?? [];
|
|
360
|
+
}
|
|
361
|
+
return componentProvenance(cache).get(entryName) ?? [];
|
|
362
|
+
}
|
|
363
|
+
if (slot.class === "gui-node") {
|
|
364
|
+
return nodeProvenance(cache, fileName).get(entryName) ?? [];
|
|
365
|
+
}
|
|
366
|
+
if (slot.class === "action-id") {
|
|
367
|
+
return actionProvenance(cache).get(entryName) ?? [];
|
|
368
|
+
}
|
|
369
|
+
if (slot.class === "animation") {
|
|
370
|
+
return animationProvenance({ slot, cache, fileName, entryName });
|
|
371
|
+
}
|
|
372
|
+
if (slot.class === "config-key" || slot.class === "resource-path") {
|
|
373
|
+
return constantProvenance(slot, cache, entryName);
|
|
374
|
+
}
|
|
375
|
+
return [];
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// src/index.ts
|
|
379
|
+
var requireFromHere = createRequire(import.meta.url);
|
|
380
|
+
var urlParameterTable;
|
|
381
|
+
function loadUrlParameterTable() {
|
|
382
|
+
if (!urlParameterTable) {
|
|
383
|
+
try {
|
|
384
|
+
const path = requireFromHere.resolve("@defold-typescript/types/url-parameters.json");
|
|
385
|
+
urlParameterTable = JSON.parse(readFileSync(path, "utf8"));
|
|
386
|
+
} catch {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return urlParameterTable;
|
|
391
|
+
}
|
|
392
|
+
function componentEntries(slot, position, cache, baseEntries) {
|
|
393
|
+
if (!isFragmentCaret2(slot, position)) {
|
|
394
|
+
return [];
|
|
395
|
+
}
|
|
396
|
+
return buildSceneCompletionEntries({
|
|
397
|
+
slot,
|
|
398
|
+
ids: cache.derived("component-ids", () => buildSceneComponentIndex2(cache.documents().documents).ids),
|
|
399
|
+
baseEntries
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
function objectPathEntries(slot, position, cache, baseEntries) {
|
|
403
|
+
if (isFragmentCaret2(slot, position)) {
|
|
404
|
+
return [];
|
|
405
|
+
}
|
|
406
|
+
return buildAddressPathCompletionEntries({
|
|
407
|
+
slot,
|
|
408
|
+
paths: cache.derived("object-paths", () => buildSceneObjectPathIndex2(cache.documents().documents).paths),
|
|
409
|
+
baseEntries
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
function nodeEntries(slot, cache, fileName, baseEntries) {
|
|
413
|
+
const { projectRoot } = cache;
|
|
414
|
+
const index = cache.derived("gui-nodes", () => buildGuiNodeIndex2(cache.documents(GUI_EXTENSIONS).documents));
|
|
415
|
+
const config = readBuildConfigFromHost(cache.host, projectRoot);
|
|
416
|
+
const resource = computeOutputRel2(displayPathOf(projectRoot, fileName), config, "gui-script");
|
|
417
|
+
const ids = index.byScriptResource.get(resource);
|
|
418
|
+
return ids === undefined ? [] : buildWholeLiteralCompletionEntries({ slot, ids, baseEntries });
|
|
419
|
+
}
|
|
420
|
+
function animationEntries(slot, cache, fileName, baseEntries) {
|
|
421
|
+
const component = componentIdOfSameObjectAddress2(slot.addressText ?? "");
|
|
422
|
+
if (component === undefined) {
|
|
423
|
+
return [];
|
|
424
|
+
}
|
|
425
|
+
const { projectRoot } = cache;
|
|
426
|
+
const index = cache.derived("sprite-animations", () => buildSpriteAnimationIndex2({
|
|
427
|
+
scenes: cache.documents().documents,
|
|
428
|
+
assets: cache.documents(ANIMATION_ASSET_EXTENSIONS).documents
|
|
429
|
+
}));
|
|
430
|
+
const config = readBuildConfigFromHost(cache.host, projectRoot);
|
|
431
|
+
const resource = computeOutputRel2(displayPathOf(projectRoot, fileName), config, "script");
|
|
432
|
+
const ids = index.byScriptResource.get(resource)?.get(component);
|
|
433
|
+
return ids === undefined ? [] : buildWholeLiteralCompletionEntries({ slot, ids, baseEntries });
|
|
434
|
+
}
|
|
435
|
+
function resourceEntries(slot, cache, baseEntries) {
|
|
436
|
+
const extensions = slot.resourceExtensions;
|
|
437
|
+
if (extensions === undefined || extensions.length === 0) {
|
|
438
|
+
return [];
|
|
439
|
+
}
|
|
440
|
+
const ids = cache.resourcePaths(extensions);
|
|
441
|
+
return buildWholeLiteralCompletionEntries({ slot, ids, baseEntries });
|
|
442
|
+
}
|
|
443
|
+
function configKeyEntries(slot, cache, baseEntries) {
|
|
444
|
+
const text = cache.documents(PROJECT_EXTENSIONS).documents.get(GAME_PROJECT_DOCUMENT);
|
|
445
|
+
if (text === undefined) {
|
|
446
|
+
return [];
|
|
447
|
+
}
|
|
448
|
+
const ids = cache.derived("config-keys", () => buildConfigKeyIndex2(text));
|
|
449
|
+
return buildWholeLiteralCompletionEntries({ slot, ids, baseEntries });
|
|
450
|
+
}
|
|
451
|
+
function actionIdEntries(slot, cache, baseEntries) {
|
|
452
|
+
const ids = cache.derived("input-actions", () => buildInputActionIndex2(cache.documents(INPUT_BINDING_EXTENSIONS).documents));
|
|
453
|
+
return buildWholeLiteralCompletionEntries({ slot, ids, baseEntries });
|
|
454
|
+
}
|
|
455
|
+
function provenancePanel(entryName, declaredIn) {
|
|
456
|
+
return {
|
|
457
|
+
name: entryName,
|
|
458
|
+
kind: CONTRIBUTED_ENTRY_KIND,
|
|
459
|
+
kindModifiers: "",
|
|
460
|
+
displayParts: [{ kind: "stringLiteral", text: JSON.stringify(entryName) }],
|
|
461
|
+
documentation: [
|
|
462
|
+
{
|
|
463
|
+
kind: "text",
|
|
464
|
+
text: `Declared in ${declaredIn.join(", ")}`
|
|
465
|
+
}
|
|
466
|
+
],
|
|
467
|
+
source: [{ kind: "text", text: DEFOLD_COMPLETION_SOURCE }]
|
|
468
|
+
};
|
|
469
|
+
}
|
|
3
470
|
function init(modules) {
|
|
4
471
|
const ts = modules.typescript;
|
|
5
472
|
function create(info) {
|
|
@@ -13,6 +480,30 @@ function init(modules) {
|
|
|
13
480
|
writable[key] = (...args) => fn.apply(base, args);
|
|
14
481
|
}
|
|
15
482
|
}
|
|
483
|
+
const serverHost = info.serverHost;
|
|
484
|
+
const cache = serverHost?.readDirectory ? createSceneIndexCache(serverHost, info.project.getCurrentDirectory()) : undefined;
|
|
485
|
+
proxy.dispose = () => {
|
|
486
|
+
cache?.dispose();
|
|
487
|
+
const disposeBase = base.dispose;
|
|
488
|
+
disposeBase?.call(base);
|
|
489
|
+
};
|
|
490
|
+
proxy.getCompletionEntryDetails = (fileName, position, entryName, formatOptions, source, preferences, data) => {
|
|
491
|
+
const forward = () => base.getCompletionEntryDetails?.(fileName, position, entryName, formatOptions, source, preferences, data);
|
|
492
|
+
if (source !== DEFOLD_COMPLETION_SOURCE || !cache) {
|
|
493
|
+
return forward();
|
|
494
|
+
}
|
|
495
|
+
const program = base.getProgram();
|
|
496
|
+
const table = loadUrlParameterTable();
|
|
497
|
+
if (!program || !table) {
|
|
498
|
+
return forward();
|
|
499
|
+
}
|
|
500
|
+
const slot = resolveClassifiedSlotAtPosition({ program, table, fileName, position });
|
|
501
|
+
if (!slot) {
|
|
502
|
+
return forward();
|
|
503
|
+
}
|
|
504
|
+
const declaredIn = resolveEntryProvenance({ slot, position, cache, fileName, entryName });
|
|
505
|
+
return declaredIn.length === 0 ? forward() : provenancePanel(entryName, declaredIn);
|
|
506
|
+
};
|
|
16
507
|
proxy.getSemanticDiagnostics = (fileName) => {
|
|
17
508
|
const prior = base.getSemanticDiagnostics(fileName);
|
|
18
509
|
const program = base.getProgram();
|
|
@@ -22,6 +513,38 @@ function init(modules) {
|
|
|
22
513
|
const transpiler = getProgramDiagnostics(program, program.getSourceFile(fileName)).map((diagnostic) => ({ ...diagnostic, category: ts.DiagnosticCategory.Suggestion }));
|
|
23
514
|
return [...prior, ...transpiler];
|
|
24
515
|
};
|
|
516
|
+
proxy.getCompletionsAtPosition = (fileName, position, options, formattingSettings) => {
|
|
517
|
+
const prior = base.getCompletionsAtPosition(fileName, position, options, formattingSettings);
|
|
518
|
+
const program = base.getProgram();
|
|
519
|
+
const table = loadUrlParameterTable();
|
|
520
|
+
if (!program || !table) {
|
|
521
|
+
return prior;
|
|
522
|
+
}
|
|
523
|
+
const slot = resolveClassifiedSlotAtPosition({ program, table, fileName, position });
|
|
524
|
+
if (!slot) {
|
|
525
|
+
return prior;
|
|
526
|
+
}
|
|
527
|
+
if (!cache) {
|
|
528
|
+
return prior;
|
|
529
|
+
}
|
|
530
|
+
const baseEntries = prior?.entries ?? [];
|
|
531
|
+
const entries = isAddressClass2(slot.class) ? [
|
|
532
|
+
...componentEntries(slot, position, cache, baseEntries),
|
|
533
|
+
...objectPathEntries(slot, position, cache, baseEntries)
|
|
534
|
+
] : 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) : [];
|
|
535
|
+
if (entries.length === 0) {
|
|
536
|
+
return prior;
|
|
537
|
+
}
|
|
538
|
+
if (!prior) {
|
|
539
|
+
return {
|
|
540
|
+
isGlobalCompletion: false,
|
|
541
|
+
isMemberCompletion: false,
|
|
542
|
+
isNewIdentifierLocation: false,
|
|
543
|
+
entries
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
return { ...prior, entries: [...prior.entries, ...entries] };
|
|
547
|
+
};
|
|
25
548
|
return proxy;
|
|
26
549
|
}
|
|
27
550
|
return { create };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ClassifiedSlot } from "@defold-typescript/transpiler";
|
|
2
|
+
import type ts from "typescript";
|
|
3
|
+
export declare const CONTRIBUTED_ENTRY_KIND: ts.ScriptElementKind;
|
|
4
|
+
export declare const DEFOLD_COMPLETION_SOURCE = "defold-typescript/scene";
|
|
5
|
+
export declare function buildSceneCompletionEntries(input: {
|
|
6
|
+
slot: ClassifiedSlot;
|
|
7
|
+
ids: ReadonlySet<string>;
|
|
8
|
+
baseEntries: readonly ts.CompletionEntry[];
|
|
9
|
+
}): ts.CompletionEntry[];
|
|
10
|
+
export declare function buildAddressPathCompletionEntries(input: {
|
|
11
|
+
slot: ClassifiedSlot;
|
|
12
|
+
paths: ReadonlySet<string>;
|
|
13
|
+
baseEntries: readonly ts.CompletionEntry[];
|
|
14
|
+
}): ts.CompletionEntry[];
|
|
15
|
+
export declare function buildWholeLiteralCompletionEntries(input: {
|
|
16
|
+
slot: ClassifiedSlot;
|
|
17
|
+
ids: ReadonlySet<string>;
|
|
18
|
+
baseEntries: readonly ts.CompletionEntry[];
|
|
19
|
+
}): ts.CompletionEntry[];
|
|
@@ -0,0 +1,18 @@
|
|
|
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>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type * as ts from "typescript";
|
|
2
|
+
import { readSceneDocuments, type SceneReadHost } from "./scene-documents";
|
|
3
|
+
export interface SceneWatchHost extends SceneReadHost {
|
|
4
|
+
watchFile?(path: string, callback: ts.FileWatcherCallback, pollingInterval?: number): ts.FileWatcher;
|
|
5
|
+
watchDirectory?(path: string, callback: ts.DirectoryWatcherCallback, recursive?: boolean): ts.FileWatcher;
|
|
6
|
+
}
|
|
7
|
+
export interface SceneIndexCache {
|
|
8
|
+
readonly host: SceneWatchHost;
|
|
9
|
+
readonly projectRoot: string;
|
|
10
|
+
documents(extensions?: readonly string[]): ReturnType<typeof readSceneDocuments>;
|
|
11
|
+
resourcePaths(extensions: readonly string[]): Set<string>;
|
|
12
|
+
derived<T>(key: string, compute: () => T): T;
|
|
13
|
+
dispose(): void;
|
|
14
|
+
}
|
|
15
|
+
export declare function createSceneIndexCache(host: SceneWatchHost, projectRoot: string): SceneIndexCache;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ClassifiedSlot } from "@defold-typescript/transpiler";
|
|
2
|
+
import type { SceneIndexCache } from "./scene-index-cache";
|
|
3
|
+
export declare function resolveEntryProvenance(input: {
|
|
4
|
+
slot: ClassifiedSlot;
|
|
5
|
+
position: number;
|
|
6
|
+
cache: SceneIndexCache;
|
|
7
|
+
fileName: string;
|
|
8
|
+
entryName: string;
|
|
9
|
+
}): readonly string[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defold-typescript/tstl-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "TypeScript language-service plugin surfacing Defold transpiler diagnostics live in the editor.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
"test": "bun test"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@defold-typescript/transpiler": "0.
|
|
38
|
+
"@defold-typescript/transpiler": "0.25.0",
|
|
39
|
+
"@defold-typescript/types": "0.25.0"
|
|
39
40
|
},
|
|
40
41
|
"peerDependencies": {
|
|
41
42
|
"typescript": ">=5.0.0 <7"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type BuildConfig, DEFAULT_INCLUDE, parseBuildConfig } from "@defold-typescript/transpiler";
|
|
2
|
+
import type { SceneReadHost } from "./scene-documents";
|
|
3
|
+
|
|
4
|
+
// Alongside output is what the build itself does with no `outDir`, so it is also
|
|
5
|
+
// the honest answer when the config cannot be read: a wrong guess would offer
|
|
6
|
+
// completions for a resource no scene names, and a throw here would take the
|
|
7
|
+
// editor's own completions down with it.
|
|
8
|
+
const ALONGSIDE: BuildConfig = { outDir: undefined, include: DEFAULT_INCLUDE };
|
|
9
|
+
|
|
10
|
+
// The project's `tsconfig.json` as the build reads it, fetched through the
|
|
11
|
+
// editor host rather than the filesystem so the plugin sees the same unsaved
|
|
12
|
+
// state the editor does.
|
|
13
|
+
export function readBuildConfigFromHost(host: SceneReadHost, projectRoot: string): BuildConfig {
|
|
14
|
+
const root = projectRoot.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
15
|
+
const text = host.readFile(`${root}/tsconfig.json`);
|
|
16
|
+
if (text === undefined) {
|
|
17
|
+
return ALONGSIDE;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
return parseBuildConfig(text);
|
|
21
|
+
} catch {
|
|
22
|
+
return ALONGSIDE;
|
|
23
|
+
}
|
|
24
|
+
}
|