@px-lsp/server 0.1.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/LICENSE +674 -0
- package/README.md +334 -0
- package/THIRD-PARTY-NOTICES.md +83 -0
- package/data/ck3/dataTypes.json +2195 -0
- package/data/ck3/data_types/data_types_common.txt +2040 -0
- package/data/ck3/data_types/data_types_gui.txt +5264 -0
- package/data/ck3/data_types/data_types_internalclausewitzgui.txt +14843 -0
- package/data/ck3/data_types/data_types_script.txt +4251 -0
- package/data/ck3/data_types/data_types_uncategorized.txt +109984 -0
- package/data/ck3/freqs.json +1 -0
- package/data/ck3/guiSchema.json +6344 -0
- package/data/ck3/script_docs/effects.log +16059 -0
- package/data/ck3/script_docs/event_targets.log +2098 -0
- package/data/ck3/script_docs/modifiers.log +2228 -0
- package/data/ck3/script_docs/on_actions.log +5275 -0
- package/data/ck3/script_docs/triggers.log +11991 -0
- package/data/ck3/structures.json +9743 -0
- package/data/ck3/wikidocs/ATTRIBUTION.md +18 -0
- package/data/ck3/wikidocs/Data_types.md +2568 -0
- package/data/ck3/wikidocs/Effects_list.md +1176 -0
- package/data/ck3/wikidocs/Scopes_list.md +341 -0
- package/data/ck3/wikidocs/Triggers_list.md +1097 -0
- package/data/eu5/data_types/data_types_common.txt +2087 -0
- package/data/eu5/data_types/data_types_gui.txt +6732 -0
- package/data/eu5/data_types/data_types_internalclausewitzgui.txt +19276 -0
- package/data/eu5/data_types/data_types_script.txt +5688 -0
- package/data/eu5/data_types/data_types_uncategorized.txt +135569 -0
- package/data/vic3/data_types/data_types_common.txt +2021 -0
- package/data/vic3/data_types/data_types_gui.txt +5592 -0
- package/data/vic3/data_types/data_types_internalclausewitzgui.txt +17304 -0
- package/data/vic3/data_types/data_types_script.txt +2817 -0
- package/data/vic3/data_types/data_types_uncategorized.txt +84354 -0
- package/data/vic3/freqs.json +1 -0
- package/data/vic3/guiSchema.json +5578 -0
- package/data/vic3/script_docs/effects.log +38135 -0
- package/data/vic3/script_docs/event_targets.log +2028 -0
- package/data/vic3/script_docs/modifiers.log +18954 -0
- package/data/vic3/script_docs/on_actions.log +1561 -0
- package/data/vic3/script_docs/triggers.log +15738 -0
- package/data/vic3/structures.json +10189 -0
- package/dist/server.js +63668 -0
- package/media/px-lsp.svg +12 -0
- package/package.json +50 -0
- package/src/clientMode.ts +60 -0
- package/src/coa/coa.ts +184 -0
- package/src/coa/coaParse.ts +267 -0
- package/src/context.ts +78 -0
- package/src/contextKeywords.ts +224 -0
- package/src/data/dataBindingMacros.ts +82 -0
- package/src/data/dataFnDocs.ts +152 -0
- package/src/data/dataFnUsage.ts +431 -0
- package/src/data/dataTypes.ts +279 -0
- package/src/data/defines.ts +123 -0
- package/src/data/docsParser.ts +453 -0
- package/src/data/keywordDocs.ts +98 -0
- package/src/data/modifierTemplates.ts +143 -0
- package/src/data/textFormatting.ts +165 -0
- package/src/data/wikiDocs.ts +187 -0
- package/src/dds/decoder.ts +1007 -0
- package/src/dds/encode.ts +235 -0
- package/src/dds/index.ts +58 -0
- package/src/dds/png.ts +96 -0
- package/src/dds/tga.ts +62 -0
- package/src/documents.ts +35 -0
- package/src/features/assetPaths.ts +169 -0
- package/src/features/codeActions.ts +148 -0
- package/src/features/colors.ts +244 -0
- package/src/features/completion.ts +961 -0
- package/src/features/datafunction.ts +729 -0
- package/src/features/definition.ts +84 -0
- package/src/features/diagnostics.ts +244 -0
- package/src/features/folding.ts +106 -0
- package/src/features/formatting.ts +60 -0
- package/src/features/guiLanguage.ts +366 -0
- package/src/features/guiNavigation.ts +140 -0
- package/src/features/guiTree.ts +97 -0
- package/src/features/hover.ts +817 -0
- package/src/features/hoverRender.ts +222 -0
- package/src/features/inlayHints.ts +147 -0
- package/src/features/locFormatting.ts +127 -0
- package/src/features/references.ts +70 -0
- package/src/features/rename.ts +135 -0
- package/src/features/scopeAt.ts +65 -0
- package/src/features/semanticTokens.ts +188 -0
- package/src/features/signatureHelp.ts +72 -0
- package/src/features/symbols.ts +241 -0
- package/src/features/textureHover.ts +143 -0
- package/src/features/workspaceSymbols.ts +69 -0
- package/src/games/active.ts +19 -0
- package/src/games/ck3/ambientScopes.ts +273 -0
- package/src/games/ck3/index.ts +38 -0
- package/src/games/ck3/meta.ts +28 -0
- package/src/games/ck3/modifierPlaceholders.ts +61 -0
- package/src/games/ck3/saveSchema.ts +134 -0
- package/src/games/ck3/scaffolds.ts +197 -0
- package/src/games/ck3/schema.ts +422 -0
- package/src/games/ck3/structures.ts +887 -0
- package/src/games/eu5/index.ts +75 -0
- package/src/games/eu5/meta.ts +44 -0
- package/src/games/eu5/scaffolds.ts +49 -0
- package/src/games/eu5/schema.generated.ts +1043 -0
- package/src/games/jomini/variables.ts +134 -0
- package/src/games/profile.ts +205 -0
- package/src/games/registry.ts +27 -0
- package/src/games/vic3/index.ts +52 -0
- package/src/games/vic3/meta.ts +55 -0
- package/src/games/vic3/saveSchema.ts +77 -0
- package/src/games/vic3/scaffolds.ts +135 -0
- package/src/games/vic3/schema.ts +650 -0
- package/src/games/vic3/structures.ts +33 -0
- package/src/gui/anchorSpec.ts +66 -0
- package/src/gui/declMarkers.ts +30 -0
- package/src/gui/fillGeometry.ts +101 -0
- package/src/gui/guiDefs.ts +386 -0
- package/src/gui/guiDependencies.ts +352 -0
- package/src/gui/guiLinks.ts +64 -0
- package/src/gui/layoutEngine.ts +1998 -0
- package/src/gui/layoutService.ts +221 -0
- package/src/gui/measuredMetrics.ts +21 -0
- package/src/gui/previewService.ts +89 -0
- package/src/gui/saveSchema.ts +220 -0
- package/src/gui/saveValues.ts +399 -0
- package/src/gui/saveZip.ts +60 -0
- package/src/gui/sourceEdit.ts +535 -0
- package/src/gui/sourceEditService.ts +439 -0
- package/src/gui/sourceModel.ts +603 -0
- package/src/gui/textResolve.ts +145 -0
- package/src/gui/textureInfo.ts +106 -0
- package/src/gui/vocabulary.ts +149 -0
- package/src/gui/widgetEdit.ts +52 -0
- package/src/gui/widgetInfo.ts +245 -0
- package/src/index/docComments.ts +103 -0
- package/src/index/extract.ts +252 -0
- package/src/index/indexer.ts +369 -0
- package/src/index/intern.ts +101 -0
- package/src/index/lazyRefs.ts +145 -0
- package/src/index/modOrigin.ts +69 -0
- package/src/index/references.ts +534 -0
- package/src/overview/dependencies.ts +240 -0
- package/src/overview/eventBanner.ts +95 -0
- package/src/overview/eventDetail.ts +482 -0
- package/src/overview/eventGraph.ts +617 -0
- package/src/overview/eventVocabulary.ts +214 -0
- package/src/overview/locCoverage.ts +138 -0
- package/src/overview/modOverview.ts +29 -0
- package/src/overview/overrides.ts +89 -0
- package/src/parseCache.ts +81 -0
- package/src/parser/cst.ts +257 -0
- package/src/parser/encoding.ts +106 -0
- package/src/parser/index.ts +7 -0
- package/src/parser/lexer.ts +245 -0
- package/src/parser/locParser.ts +276 -0
- package/src/parser/parser.ts +360 -0
- package/src/schema/freqs.ts +70 -0
- package/src/schema/loader.ts +113 -0
- package/src/schema/types.ts +142 -0
- package/src/scopes/inference.ts +478 -0
- package/src/scopes/model.ts +148 -0
- package/src/scopes/varTypes.ts +290 -0
- package/src/server.ts +1894 -0
- package/src/serverData.ts +98 -0
- package/src/structure.ts +56 -0
- package/src/wordAt.ts +49 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DDS encoder for the image → DDS converter: uncompressed A8R8G8B8 plus BC1
|
|
3
|
+
* (DXT1) and BC3 (DXT5) via classic range-fit block compression — compact,
|
|
4
|
+
* dependency-free, and good enough for GUI icons/illustrations. Round-trip
|
|
5
|
+
* tested against decoder.ts.
|
|
6
|
+
*
|
|
7
|
+
* No `vscode` imports: unit-tested in plain Node, used by the client host.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type DdsEncodeFormat = "bc1" | "bc3" | "bgra8";
|
|
11
|
+
|
|
12
|
+
const DDSD_CAPS = 0x1;
|
|
13
|
+
const DDSD_HEIGHT = 0x2;
|
|
14
|
+
const DDSD_WIDTH = 0x4;
|
|
15
|
+
const DDSD_PITCH = 0x8;
|
|
16
|
+
const DDSD_PIXELFORMAT = 0x1000;
|
|
17
|
+
const DDSD_LINEARSIZE = 0x80000;
|
|
18
|
+
const DDPF_ALPHAPIXELS = 0x1;
|
|
19
|
+
const DDPF_FOURCC = 0x4;
|
|
20
|
+
const DDPF_RGB = 0x40;
|
|
21
|
+
const DDSCAPS_TEXTURE = 0x1000;
|
|
22
|
+
|
|
23
|
+
function fourCC(s: string): number {
|
|
24
|
+
return (s.charCodeAt(0) | (s.charCodeAt(1) << 8) | (s.charCodeAt(2) << 16) | (s.charCodeAt(3) << 24)) >>> 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** True when any pixel has alpha < 255 — drives the "auto" format choice. */
|
|
28
|
+
export function hasTransparency(rgba: Uint8Array): boolean {
|
|
29
|
+
for (let i = 3; i < rgba.length; i += 4) if (rgba[i] !== 255) return true;
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Encode RGBA8 pixels as a DDS file (no mipmaps). */
|
|
34
|
+
export function encodeDds(
|
|
35
|
+
width: number,
|
|
36
|
+
height: number,
|
|
37
|
+
rgba: Uint8Array,
|
|
38
|
+
format: DdsEncodeFormat
|
|
39
|
+
): Uint8Array {
|
|
40
|
+
if (rgba.length < width * height * 4) throw new Error("pixel buffer too small");
|
|
41
|
+
const data =
|
|
42
|
+
format === "bgra8" ? encodeBgra(width, height, rgba) : encodeBlocks(width, height, rgba, format);
|
|
43
|
+
|
|
44
|
+
const header = new Uint8Array(128);
|
|
45
|
+
const dv = new DataView(header.buffer);
|
|
46
|
+
dv.setUint32(0, fourCC("DDS "), true);
|
|
47
|
+
dv.setUint32(4, 124, true); // dwSize
|
|
48
|
+
const compressed = format !== "bgra8";
|
|
49
|
+
dv.setUint32(
|
|
50
|
+
8,
|
|
51
|
+
DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT | (compressed ? DDSD_LINEARSIZE : DDSD_PITCH),
|
|
52
|
+
true
|
|
53
|
+
);
|
|
54
|
+
dv.setUint32(12, height, true);
|
|
55
|
+
dv.setUint32(16, width, true);
|
|
56
|
+
dv.setUint32(20, compressed ? data.length : width * 4, true); // linear size / pitch
|
|
57
|
+
// depth, mipmaps, 11 reserved dwords stay 0
|
|
58
|
+
const pf = 76; // pixel-format struct offset
|
|
59
|
+
dv.setUint32(pf, 32, true); // pf size
|
|
60
|
+
if (format === "bc1") {
|
|
61
|
+
dv.setUint32(pf + 4, DDPF_FOURCC, true);
|
|
62
|
+
dv.setUint32(pf + 8, fourCC("DXT1"), true);
|
|
63
|
+
} else if (format === "bc3") {
|
|
64
|
+
dv.setUint32(pf + 4, DDPF_FOURCC, true);
|
|
65
|
+
dv.setUint32(pf + 8, fourCC("DXT5"), true);
|
|
66
|
+
} else {
|
|
67
|
+
dv.setUint32(pf + 4, DDPF_RGB | DDPF_ALPHAPIXELS, true);
|
|
68
|
+
dv.setUint32(pf + 12, 32, true); // bit count
|
|
69
|
+
dv.setUint32(pf + 16, 0x00ff0000, true); // R
|
|
70
|
+
dv.setUint32(pf + 20, 0x0000ff00, true); // G
|
|
71
|
+
dv.setUint32(pf + 24, 0x000000ff, true); // B
|
|
72
|
+
dv.setUint32(pf + 28, 0xff000000, true); // A
|
|
73
|
+
}
|
|
74
|
+
dv.setUint32(108, DDSCAPS_TEXTURE, true);
|
|
75
|
+
|
|
76
|
+
const out = new Uint8Array(128 + data.length);
|
|
77
|
+
out.set(header, 0);
|
|
78
|
+
out.set(data, 128);
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function encodeBgra(width: number, height: number, rgba: Uint8Array): Uint8Array {
|
|
83
|
+
const out = new Uint8Array(width * height * 4);
|
|
84
|
+
for (let i = 0; i < width * height; i++) {
|
|
85
|
+
out[i * 4] = rgba[i * 4 + 2]; // B
|
|
86
|
+
out[i * 4 + 1] = rgba[i * 4 + 1]; // G
|
|
87
|
+
out[i * 4 + 2] = rgba[i * 4]; // R
|
|
88
|
+
out[i * 4 + 3] = rgba[i * 4 + 3]; // A
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function encodeBlocks(width: number, height: number, rgba: Uint8Array, format: "bc1" | "bc3"): Uint8Array {
|
|
94
|
+
const bw = Math.ceil(width / 4);
|
|
95
|
+
const bh = Math.ceil(height / 4);
|
|
96
|
+
const blockSize = format === "bc1" ? 8 : 16;
|
|
97
|
+
const out = new Uint8Array(bw * bh * blockSize);
|
|
98
|
+
const px = new Uint8Array(64); // 16 RGBA pixels
|
|
99
|
+
let offset = 0;
|
|
100
|
+
for (let by = 0; by < bh; by++) {
|
|
101
|
+
for (let bx = 0; bx < bw; bx++) {
|
|
102
|
+
// Gather the 4x4 block, clamping at edges.
|
|
103
|
+
for (let y = 0; y < 4; y++) {
|
|
104
|
+
const sy = Math.min(height - 1, by * 4 + y);
|
|
105
|
+
for (let x = 0; x < 4; x++) {
|
|
106
|
+
const sx = Math.min(width - 1, bx * 4 + x);
|
|
107
|
+
const so = (sy * width + sx) * 4;
|
|
108
|
+
const d = (y * 4 + x) * 4;
|
|
109
|
+
px[d] = rgba[so];
|
|
110
|
+
px[d + 1] = rgba[so + 1];
|
|
111
|
+
px[d + 2] = rgba[so + 2];
|
|
112
|
+
px[d + 3] = rgba[so + 3];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (format === "bc3") {
|
|
116
|
+
writeAlphaBlock(px, out, offset);
|
|
117
|
+
offset += 8;
|
|
118
|
+
}
|
|
119
|
+
writeColorBlock(px, out, offset);
|
|
120
|
+
offset += 8;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function to565(r: number, g: number, b: number): number {
|
|
127
|
+
return ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function from565(c: number): [number, number, number] {
|
|
131
|
+
const r = (c >> 11) & 0x1f;
|
|
132
|
+
const g = (c >> 5) & 0x3f;
|
|
133
|
+
const b = c & 0x1f;
|
|
134
|
+
return [(r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2)];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Range-fit BC1 color block (always 4-color mode: c0 > c1). Endpoints are the
|
|
139
|
+
* extremes of the block's principal luminance ordering; each pixel snaps to
|
|
140
|
+
* the nearest of the 4 palette entries.
|
|
141
|
+
*/
|
|
142
|
+
function writeColorBlock(px: Uint8Array, out: Uint8Array, offset: number): void {
|
|
143
|
+
// Find the two extreme pixels by projecting on the max-variance axis
|
|
144
|
+
// (approximated by luminance — adequate for icon/illustration content).
|
|
145
|
+
let minL = Infinity;
|
|
146
|
+
let maxL = -Infinity;
|
|
147
|
+
let minI = 0;
|
|
148
|
+
let maxI = 0;
|
|
149
|
+
for (let i = 0; i < 16; i++) {
|
|
150
|
+
const l = px[i * 4] * 3 + px[i * 4 + 1] * 6 + px[i * 4 + 2];
|
|
151
|
+
if (l < minL) {
|
|
152
|
+
minL = l;
|
|
153
|
+
minI = i;
|
|
154
|
+
}
|
|
155
|
+
if (l > maxL) {
|
|
156
|
+
maxL = l;
|
|
157
|
+
maxI = i;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
let c0 = to565(px[maxI * 4], px[maxI * 4 + 1], px[maxI * 4 + 2]);
|
|
161
|
+
let c1 = to565(px[minI * 4], px[minI * 4 + 1], px[minI * 4 + 2]);
|
|
162
|
+
if (c0 < c1) {
|
|
163
|
+
const t = c0;
|
|
164
|
+
c0 = c1;
|
|
165
|
+
c1 = t;
|
|
166
|
+
} else if (c0 === c1 && c1 > 0) {
|
|
167
|
+
c1 = c1 - 1; // keep 4-color mode; palette entry 0 still matches exactly
|
|
168
|
+
}
|
|
169
|
+
const [r0, g0, b0] = from565(c0);
|
|
170
|
+
const [r1, g1, b1] = from565(c1);
|
|
171
|
+
const palette = [
|
|
172
|
+
[r0, g0, b0],
|
|
173
|
+
[r1, g1, b1],
|
|
174
|
+
[(2 * r0 + r1 + 1) / 3, (2 * g0 + g1 + 1) / 3, (2 * b0 + b1 + 1) / 3],
|
|
175
|
+
[(r0 + 2 * r1 + 1) / 3, (g0 + 2 * g1 + 1) / 3, (b0 + 2 * b1 + 1) / 3],
|
|
176
|
+
];
|
|
177
|
+
let indices = 0;
|
|
178
|
+
for (let i = 0; i < 16; i++) {
|
|
179
|
+
let best = 0;
|
|
180
|
+
let bestD = Infinity;
|
|
181
|
+
for (let p = 0; p < 4; p++) {
|
|
182
|
+
const dr = px[i * 4] - palette[p][0];
|
|
183
|
+
const dg = px[i * 4 + 1] - palette[p][1];
|
|
184
|
+
const db = px[i * 4 + 2] - palette[p][2];
|
|
185
|
+
const d = dr * dr + dg * dg + db * db;
|
|
186
|
+
if (d < bestD) {
|
|
187
|
+
bestD = d;
|
|
188
|
+
best = p;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
indices |= best << (i * 2);
|
|
192
|
+
}
|
|
193
|
+
const dv = new DataView(out.buffer, out.byteOffset + offset, 8);
|
|
194
|
+
dv.setUint16(0, c0, true);
|
|
195
|
+
dv.setUint16(2, c1, true);
|
|
196
|
+
dv.setUint32(4, indices >>> 0, true);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** BC3 alpha block: a0 > a1 (8-level) range fit with 3-bit indices. */
|
|
200
|
+
function writeAlphaBlock(px: Uint8Array, out: Uint8Array, offset: number): void {
|
|
201
|
+
let a0 = 0;
|
|
202
|
+
let a1 = 255;
|
|
203
|
+
for (let i = 0; i < 16; i++) {
|
|
204
|
+
const a = px[i * 4 + 3];
|
|
205
|
+
if (a > a0) a0 = a;
|
|
206
|
+
if (a < a1) a1 = a;
|
|
207
|
+
}
|
|
208
|
+
if (a0 === a1) {
|
|
209
|
+
// Uniform alpha: endpoints equal would flip to the 6-level mode; nudge.
|
|
210
|
+
if (a0 < 255) a0 = a0 + 1;
|
|
211
|
+
else a1 = a1 - 1;
|
|
212
|
+
}
|
|
213
|
+
const palette = [a0, a1];
|
|
214
|
+
for (let i = 1; i <= 6; i++) palette.push(Math.round(((7 - i) * a0 + i * a1) / 7));
|
|
215
|
+
const idx: number[] = [];
|
|
216
|
+
for (let i = 0; i < 16; i++) {
|
|
217
|
+
const a = px[i * 4 + 3];
|
|
218
|
+
let best = 0;
|
|
219
|
+
let bestD = Infinity;
|
|
220
|
+
for (let p = 0; p < 8; p++) {
|
|
221
|
+
const d = Math.abs(a - palette[p]);
|
|
222
|
+
if (d < bestD) {
|
|
223
|
+
bestD = d;
|
|
224
|
+
best = p;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
idx.push(best);
|
|
228
|
+
}
|
|
229
|
+
out[offset] = a0;
|
|
230
|
+
out[offset + 1] = a1;
|
|
231
|
+
// Pack 16 × 3-bit indices little-endian into 6 bytes.
|
|
232
|
+
let bits = 0n;
|
|
233
|
+
for (let i = 15; i >= 0; i--) bits = (bits << 3n) | BigInt(idx[i]);
|
|
234
|
+
for (let i = 0; i < 6; i++) out[offset + 2 + i] = Number((bits >> BigInt(i * 8)) & 0xffn);
|
|
235
|
+
}
|
package/src/dds/index.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { decodeDds, type DecodedImage } from "./decoder";
|
|
2
|
+
import { encodePng } from "./png";
|
|
3
|
+
|
|
4
|
+
export { decodeDds, ddsFormatInfo } from "./decoder";
|
|
5
|
+
export { encodePng } from "./png";
|
|
6
|
+
export { decodeTga } from "./tga";
|
|
7
|
+
export { encodeDds, hasTransparency, type DdsEncodeFormat } from "./encode";
|
|
8
|
+
export type { DecodedImage } from "./decoder";
|
|
9
|
+
|
|
10
|
+
/** Nearest-neighbour downscale so max(width, height) <= maxDim. Returns the image unchanged if already small enough. */
|
|
11
|
+
export function downscale(img: DecodedImage, maxDim: number): DecodedImage {
|
|
12
|
+
const { width, height, pixels } = img;
|
|
13
|
+
const longest = Math.max(width, height);
|
|
14
|
+
if (longest <= maxDim || maxDim <= 0) return img;
|
|
15
|
+
|
|
16
|
+
const scale = maxDim / longest;
|
|
17
|
+
const dw = Math.max(1, Math.round(width * scale));
|
|
18
|
+
const dh = Math.max(1, Math.round(height * scale));
|
|
19
|
+
const out = new Uint8Array(dw * dh * 4);
|
|
20
|
+
for (let y = 0; y < dh; y++) {
|
|
21
|
+
const sy = Math.min(height - 1, Math.floor((y * height) / dh));
|
|
22
|
+
for (let x = 0; x < dw; x++) {
|
|
23
|
+
const sx = Math.min(width - 1, Math.floor((x * width) / dw));
|
|
24
|
+
const so = (sy * width + sx) * 4;
|
|
25
|
+
const dof = (y * dw + x) * 4;
|
|
26
|
+
out[dof] = pixels[so];
|
|
27
|
+
out[dof + 1] = pixels[so + 1];
|
|
28
|
+
out[dof + 2] = pixels[so + 2];
|
|
29
|
+
out[dof + 3] = pixels[so + 3];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { width: dw, height: dh, pixels: out };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Decode a DDS, downscale so max(w,h) <= maxDim, encode PNG, return a data URI —
|
|
37
|
+
* or null if even a shrunk preview can't fit the length budget.
|
|
38
|
+
*
|
|
39
|
+
* VS Code's hover markdown renderer hard-truncates any hover body longer than
|
|
40
|
+
* 100_000 chars (`preprocessMarkdownString` in markdownRenderer.ts: `if
|
|
41
|
+
* (value.length > 100_000) value = value.substr(0, 100_000) + '…'`). That cut
|
|
42
|
+
* lands mid-base64 inside ``, leaving an unterminated image
|
|
43
|
+
* link that markdown-it then renders as literal text — the raw base64 spilling
|
|
44
|
+
* into the popup. Smooth vanilla art deflates small (~55-80 KB URIs) and stays
|
|
45
|
+
* under the limit; noisy/photographic converter output can exceed it (a 1592x848
|
|
46
|
+
* DXT1 event scene measured ~108 KB). So we cap the URI at `maxUriLength` and
|
|
47
|
+
* progressively shrink maxDim until it fits, decoding only once.
|
|
48
|
+
*/
|
|
49
|
+
export function ddsToPngDataUri(buf: Uint8Array, maxDim = 256, maxUriLength = 90_000): string | null {
|
|
50
|
+
const decoded = decodeDds(buf);
|
|
51
|
+
for (let dim = maxDim; dim >= 32; dim = Math.floor(dim * 0.75)) {
|
|
52
|
+
const scaled = downscale(decoded, dim);
|
|
53
|
+
const png = encodePng(scaled.width, scaled.height, scaled.pixels);
|
|
54
|
+
const uri = `data:image/png;base64,${Buffer.from(png).toString("base64")}`;
|
|
55
|
+
if (uri.length <= maxUriLength) return uri;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
package/src/dds/png.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { deflateSync } from "node:zlib";
|
|
2
|
+
|
|
3
|
+
// CRC32 table (IEEE polynomial 0xEDB88320)
|
|
4
|
+
const CRC_TABLE: Uint32Array = (() => {
|
|
5
|
+
const table = new Uint32Array(256);
|
|
6
|
+
for (let n = 0; n < 256; n++) {
|
|
7
|
+
let c = n;
|
|
8
|
+
for (let k = 0; k < 8; k++) {
|
|
9
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
10
|
+
}
|
|
11
|
+
table[n] = c >>> 0;
|
|
12
|
+
}
|
|
13
|
+
return table;
|
|
14
|
+
})();
|
|
15
|
+
|
|
16
|
+
function crc32(bytes: Uint8Array): number {
|
|
17
|
+
let crc = 0xffffffff;
|
|
18
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
19
|
+
crc = CRC_TABLE[(crc ^ bytes[i]) & 0xff] ^ (crc >>> 8);
|
|
20
|
+
}
|
|
21
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function u32be(n: number): Uint8Array {
|
|
25
|
+
const b = new Uint8Array(4);
|
|
26
|
+
b[0] = (n >>> 24) & 0xff;
|
|
27
|
+
b[1] = (n >>> 16) & 0xff;
|
|
28
|
+
b[2] = (n >>> 8) & 0xff;
|
|
29
|
+
b[3] = n & 0xff;
|
|
30
|
+
return b;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function chunk(type: string, data: Uint8Array): Uint8Array {
|
|
34
|
+
const typeBytes = new Uint8Array(4);
|
|
35
|
+
for (let i = 0; i < 4; i++) typeBytes[i] = type.charCodeAt(i);
|
|
36
|
+
const body = new Uint8Array(typeBytes.length + data.length);
|
|
37
|
+
body.set(typeBytes, 0);
|
|
38
|
+
body.set(data, typeBytes.length);
|
|
39
|
+
const crc = crc32(body);
|
|
40
|
+
const out = new Uint8Array(4 + body.length + 4);
|
|
41
|
+
out.set(u32be(data.length), 0);
|
|
42
|
+
out.set(body, 4);
|
|
43
|
+
out.set(u32be(crc), 4 + body.length);
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Encode an 8-bit RGBA image to PNG bytes. No external dependencies:
|
|
49
|
+
* filter type 0 per scanline, zlib deflate via node:zlib, manual CRC32.
|
|
50
|
+
*/
|
|
51
|
+
export function encodePng(width: number, height: number, rgba: Uint8Array): Uint8Array {
|
|
52
|
+
if (width <= 0 || height <= 0) throw new Error("encodePng: invalid dimensions");
|
|
53
|
+
if (rgba.length < width * height * 4) throw new Error("encodePng: pixel buffer too small");
|
|
54
|
+
|
|
55
|
+
const sig = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
56
|
+
|
|
57
|
+
// IHDR: width, height, bit depth 8, color type 6 (RGBA), compression 0, filter 0, interlace 0
|
|
58
|
+
const ihdr = new Uint8Array(13);
|
|
59
|
+
ihdr.set(u32be(width), 0);
|
|
60
|
+
ihdr.set(u32be(height), 4);
|
|
61
|
+
ihdr[8] = 8;
|
|
62
|
+
ihdr[9] = 6;
|
|
63
|
+
ihdr[10] = 0;
|
|
64
|
+
ihdr[11] = 0;
|
|
65
|
+
ihdr[12] = 0;
|
|
66
|
+
|
|
67
|
+
// Raw image data with filter byte (0) prepended to each scanline
|
|
68
|
+
const stride = width * 4;
|
|
69
|
+
const raw = new Uint8Array((stride + 1) * height);
|
|
70
|
+
for (let y = 0; y < height; y++) {
|
|
71
|
+
const dst = y * (stride + 1);
|
|
72
|
+
raw[dst] = 0; // filter type none
|
|
73
|
+
raw.set(rgba.subarray(y * stride, y * stride + stride), dst + 1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const compressed = deflateSync(raw);
|
|
77
|
+
|
|
78
|
+
const ihdrChunk = chunk("IHDR", ihdr);
|
|
79
|
+
const idatChunk = chunk(
|
|
80
|
+
"IDAT",
|
|
81
|
+
new Uint8Array(compressed.buffer, compressed.byteOffset, compressed.byteLength)
|
|
82
|
+
);
|
|
83
|
+
const iendChunk = chunk("IEND", new Uint8Array(0));
|
|
84
|
+
|
|
85
|
+
const total = sig.length + ihdrChunk.length + idatChunk.length + iendChunk.length;
|
|
86
|
+
const out = new Uint8Array(total);
|
|
87
|
+
let off = 0;
|
|
88
|
+
out.set(sig, off);
|
|
89
|
+
off += sig.length;
|
|
90
|
+
out.set(ihdrChunk, off);
|
|
91
|
+
off += ihdrChunk.length;
|
|
92
|
+
out.set(idatChunk, off);
|
|
93
|
+
off += idatChunk.length;
|
|
94
|
+
out.set(iendChunk, off);
|
|
95
|
+
return out;
|
|
96
|
+
}
|
package/src/dds/tga.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Truevision TGA decoder: true-color 24/32-bit, raw (type 2) or RLE (type 10),
|
|
3
|
+
* either vertical origin. That is every .tga the games ship under gfx/ (the
|
|
4
|
+
* coat-of-arms patterns are 24-bit, raw or RLE, bottom-left origin). No
|
|
5
|
+
* color-mapped or grayscale images: nothing in the corpus uses them.
|
|
6
|
+
*/
|
|
7
|
+
import type { DecodedImage } from "./decoder";
|
|
8
|
+
|
|
9
|
+
export function decodeTga(buf: Uint8Array): DecodedImage {
|
|
10
|
+
if (buf.length < 18) throw new Error("truncated TGA header");
|
|
11
|
+
const idLength = buf[0];
|
|
12
|
+
const colorMapType = buf[1];
|
|
13
|
+
const imageType = buf[2];
|
|
14
|
+
const width = buf[12] | (buf[13] << 8);
|
|
15
|
+
const height = buf[14] | (buf[15] << 8);
|
|
16
|
+
const depth = buf[16];
|
|
17
|
+
const descriptor = buf[17];
|
|
18
|
+
if (colorMapType !== 0 || (imageType !== 2 && imageType !== 10)) {
|
|
19
|
+
throw new Error(`unsupported TGA image type ${imageType} (color map ${colorMapType})`);
|
|
20
|
+
}
|
|
21
|
+
if (depth !== 24 && depth !== 32) throw new Error(`unsupported TGA depth ${depth}`);
|
|
22
|
+
if (width === 0 || height === 0) throw new Error("empty TGA");
|
|
23
|
+
|
|
24
|
+
const bpp = depth / 4 / 2; // bytes per pixel: 3 or 4
|
|
25
|
+
const topDown = (descriptor & 0x20) !== 0;
|
|
26
|
+
const pixels = new Uint8Array(width * height * 4);
|
|
27
|
+
let src = 18 + idLength;
|
|
28
|
+
const count = width * height;
|
|
29
|
+
|
|
30
|
+
const put = (i: number): void => {
|
|
31
|
+
// Row i / width of the file maps to the bottom row first unless top-down.
|
|
32
|
+
const row = Math.floor(i / width);
|
|
33
|
+
const y = topDown ? row : height - 1 - row;
|
|
34
|
+
const o = (y * width + (i % width)) * 4;
|
|
35
|
+
pixels[o] = buf[src + 2];
|
|
36
|
+
pixels[o + 1] = buf[src + 1];
|
|
37
|
+
pixels[o + 2] = buf[src];
|
|
38
|
+
pixels[o + 3] = bpp === 4 ? buf[src + 3] : 255;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
if (imageType === 2) {
|
|
42
|
+
if (src + count * bpp > buf.length) throw new Error("truncated TGA data");
|
|
43
|
+
for (let i = 0; i < count; i++, src += bpp) put(i);
|
|
44
|
+
return { width, height, pixels };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let i = 0;
|
|
48
|
+
while (i < count) {
|
|
49
|
+
if (src >= buf.length) throw new Error("truncated TGA RLE data");
|
|
50
|
+
const packet = buf[src++];
|
|
51
|
+
const n = (packet & 0x7f) + 1;
|
|
52
|
+
if (packet & 0x80) {
|
|
53
|
+
if (src + bpp > buf.length) throw new Error("truncated TGA RLE data");
|
|
54
|
+
for (let k = 0; k < n && i < count; k++, i++) put(i);
|
|
55
|
+
src += bpp;
|
|
56
|
+
} else {
|
|
57
|
+
if (src + n * bpp > buf.length) throw new Error("truncated TGA RLE data");
|
|
58
|
+
for (let k = 0; k < n && i < count; k++, i++, src += bpp) put(i);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { width, height, pixels };
|
|
62
|
+
}
|
package/src/documents.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small helpers over `vscode-languageserver-textdocument` documents: the LSP
|
|
3
|
+
* TextDocument API has no lineAt(), so line text is sliced via ranges (offsets
|
|
4
|
+
* clamp to the line end).
|
|
5
|
+
*/
|
|
6
|
+
import type { TextDocument } from "vscode-languageserver-textdocument";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The non-script languages we serve. Everything else whose id starts with
|
|
10
|
+
* `paradox` is the script language.
|
|
11
|
+
*/
|
|
12
|
+
const NON_SCRIPT_LANGUAGES = new Set(["paradox-loc", "paradox-gui", "paradox-mod", "paradox-info"]);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Whether a document is jomini script, whatever the client called it.
|
|
16
|
+
*
|
|
17
|
+
* Clients may append a suffix to get a per-language icon and label per game
|
|
18
|
+
* (the VS Code extension sends `paradox-<game>`, neovim sends plain
|
|
19
|
+
* `paradox`). They are one language to the server, and naming the suffixes
|
|
20
|
+
* here would put game names in engine code, so the test is generic: it is
|
|
21
|
+
* script unless it is one of the four other Paradox languages.
|
|
22
|
+
*/
|
|
23
|
+
export function isScriptLanguage(languageId: string): boolean {
|
|
24
|
+
return languageId.startsWith("paradox") && !NON_SCRIPT_LANGUAGES.has(languageId);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getLineText(document: TextDocument, line: number): string {
|
|
28
|
+
if (line < 0 || line >= document.lineCount) return "";
|
|
29
|
+
const text = document.getText({
|
|
30
|
+
start: { line, character: 0 },
|
|
31
|
+
end: { line, character: Number.MAX_SAFE_INTEGER },
|
|
32
|
+
});
|
|
33
|
+
// The range clamp keeps the trailing newline out, but guard against CR.
|
|
34
|
+
return text.replace(/[\r\n]+$/, "");
|
|
35
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem-backed asset-path IntelliSense (user-reported gap): completion for
|
|
3
|
+
* quoted/unquoted asset paths (`texture = "gfx/interface/icons/..."`) drilled
|
|
4
|
+
* one directory segment at a time across the content roots, plus bare-filename
|
|
5
|
+
* `.dds` fields whose base directory the engine fixes per context
|
|
6
|
+
* (`icon = cultivation_realm_2.dds` in a trait → gfx/interface/icons/traits/).
|
|
7
|
+
*
|
|
8
|
+
* Roots are listed in the same shadowing order as textureHover.ts: modPath,
|
|
9
|
+
* then parentPaths, then gamePath. Listing is lazy — one fs.readdirSync per root
|
|
10
|
+
* per request, no upfront recursive scan.
|
|
11
|
+
*/
|
|
12
|
+
import { CompletionItemKind, type CompletionItem } from "vscode-languageserver/node";
|
|
13
|
+
import * as fs from "fs";
|
|
14
|
+
import * as path from "path";
|
|
15
|
+
import type { ParadoxSettings } from "@px-lsp/protocol/protocol";
|
|
16
|
+
import type { CompletionResult } from "./completion";
|
|
17
|
+
|
|
18
|
+
/** Top-level folders under a content root that hold referenceable assets. */
|
|
19
|
+
const ASSET_ROOTS = new Set(["gfx", "gui", "fonts", "sound", "music", "dlc"]);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Bare-filename `.dds` fields and the base directory the engine resolves them
|
|
23
|
+
* against, keyed by definition kind (the file's schema folder) then by key.
|
|
24
|
+
* Every entry was verified against vanilla: a real file uses the bare name AND
|
|
25
|
+
* the asset lives at the mapped directory (defines in
|
|
26
|
+
* common/defines/graphic/00_graphics.txt, cross-checked with the assets):
|
|
27
|
+
* - trait.icon → TRAIT_ICON_PATH (also documented in _traits.info)
|
|
28
|
+
* - death_reason.icon → DEATH_REASON_ICON_PATH
|
|
29
|
+
* - building.type_icon → BUILDING_TYPE_ICON_PATH
|
|
30
|
+
*/
|
|
31
|
+
const BARE_NAME_BASE_DIRS: Record<string, Record<string, string[]>> = {
|
|
32
|
+
trait: { icon: ["gfx/interface/icons/traits"] },
|
|
33
|
+
death_reason: { icon: ["gfx/interface/icons/death_reason"] },
|
|
34
|
+
building: { type_icon: ["gfx/interface/icons/building_types"] },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
interface Root {
|
|
38
|
+
root: string;
|
|
39
|
+
label: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Content roots in shadowing order (mod → parents → vanilla), non-null only. */
|
|
43
|
+
export function assetRoots(settings: ParadoxSettings): Root[] {
|
|
44
|
+
const roots: Root[] = [];
|
|
45
|
+
if (settings.modPath) roots.push({ root: settings.modPath, label: "mod" });
|
|
46
|
+
for (const p of settings.parentPaths ?? []) roots.push({ root: p, label: "parent" });
|
|
47
|
+
if (settings.gamePath) roots.push({ root: settings.gamePath, label: "vanilla" });
|
|
48
|
+
return roots;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Base dirs a bare `.dds` field resolves against, or null when the key is not mapped. */
|
|
52
|
+
export function bareNameBaseDirs(kind: string | null | undefined, key: string): string[] | null {
|
|
53
|
+
if (!kind) return null;
|
|
54
|
+
return BARE_NAME_BASE_DIRS[kind]?.[key] ?? null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The asset-path prefix being typed inside a value, or null when the value is
|
|
59
|
+
* not a directory-style asset path. Matches `key = "gfx/interface/ico`
|
|
60
|
+
* (quote optional — script paths are often unquoted). A path qualifies when its
|
|
61
|
+
* first segment is a known asset root; a slash-free partial qualifies when it is
|
|
62
|
+
* still a prefix of one (so `"g` can suggest gfx/gui), which keeps arbitrary
|
|
63
|
+
* quoted strings (`name = "PdxWidget"`) out.
|
|
64
|
+
*/
|
|
65
|
+
export function assetDirContext(linePrefix: string): string | null {
|
|
66
|
+
const m = /([A-Za-z_][A-Za-z0-9_]*)\s*=\s*"?([A-Za-z0-9_.\-/]*)$/.exec(linePrefix);
|
|
67
|
+
if (!m) return null;
|
|
68
|
+
const value = m[2];
|
|
69
|
+
// An empty value is ambiguous (could be a bare-name field): let the value-position
|
|
70
|
+
// handler decide, kick in only once a segment is being typed.
|
|
71
|
+
if (value === "") return null;
|
|
72
|
+
const first = value.split("/")[0].toLowerCase();
|
|
73
|
+
if (value.includes("/")) {
|
|
74
|
+
return ASSET_ROOTS.has(first) ? value : null;
|
|
75
|
+
}
|
|
76
|
+
// No slash yet: only offer when the partial can still grow into a root.
|
|
77
|
+
const low = value.toLowerCase();
|
|
78
|
+
for (const r of ASSET_ROOTS) if (r.startsWith(low)) return value;
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function startsWithFold(name: string, prefix: string): boolean {
|
|
83
|
+
return name.toLowerCase().startsWith(prefix.toLowerCase());
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Directory-segment completion for `pathPrefix` (e.g. `gfx/interface/ico`):
|
|
88
|
+
* children of the parent directory across roots, deduped by relative path in
|
|
89
|
+
* shadowing order. Directories get a trailing `/` and a retrigger command so the
|
|
90
|
+
* user drills down; files complete as-is. isIncomplete so the client re-queries
|
|
91
|
+
* per keystroke.
|
|
92
|
+
*/
|
|
93
|
+
export function provideAssetDirCompletion(settings: ParadoxSettings, pathPrefix: string): CompletionResult {
|
|
94
|
+
const slash = pathPrefix.lastIndexOf("/");
|
|
95
|
+
const parentRel = slash >= 0 ? pathPrefix.slice(0, slash) : "";
|
|
96
|
+
const partial = slash >= 0 ? pathPrefix.slice(slash + 1) : pathPrefix;
|
|
97
|
+
|
|
98
|
+
const seen = new Set<string>();
|
|
99
|
+
const items: CompletionItem[] = [];
|
|
100
|
+
for (const { root, label } of assetRoots(settings)) {
|
|
101
|
+
const dir = parentRel ? path.join(root, ...parentRel.split("/")) : root;
|
|
102
|
+
let entries: fs.Dirent[];
|
|
103
|
+
try {
|
|
104
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
105
|
+
} catch {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
for (const d of entries) {
|
|
109
|
+
const name = d.name;
|
|
110
|
+
if (partial && !startsWithFold(name, partial)) continue;
|
|
111
|
+
const isDir = d.isDirectory();
|
|
112
|
+
const relLow = `${parentRel}/${name}`.toLowerCase();
|
|
113
|
+
if (seen.has(relLow)) continue;
|
|
114
|
+
seen.add(relLow);
|
|
115
|
+
const item: CompletionItem = {
|
|
116
|
+
label: name,
|
|
117
|
+
kind: isDir ? CompletionItemKind.Folder : CompletionItemKind.File,
|
|
118
|
+
detail: label,
|
|
119
|
+
// Directories first, then files; alphabetical within each group.
|
|
120
|
+
sortText: `${isDir ? "0" : "1"}${name.toLowerCase()}`,
|
|
121
|
+
};
|
|
122
|
+
if (isDir) {
|
|
123
|
+
item.insertText = `${name}/`;
|
|
124
|
+
item.command = { command: "editor.action.triggerSuggest", title: "Suggest next segment" };
|
|
125
|
+
}
|
|
126
|
+
items.push(item);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return { isIncomplete: true, items };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* `*.dds` completion for a bare-filename field: files from the mapped base dirs
|
|
134
|
+
* across roots (mod-first), deduped by filename. Returns null when the key is
|
|
135
|
+
* not a mapped bare-name field.
|
|
136
|
+
*/
|
|
137
|
+
export function provideBareNameCompletion(
|
|
138
|
+
settings: ParadoxSettings,
|
|
139
|
+
kind: string | null | undefined,
|
|
140
|
+
key: string
|
|
141
|
+
): CompletionItem[] | null {
|
|
142
|
+
const dirs = bareNameBaseDirs(kind, key);
|
|
143
|
+
if (!dirs) return null;
|
|
144
|
+
const seen = new Set<string>();
|
|
145
|
+
const items: CompletionItem[] = [];
|
|
146
|
+
for (const { root, label } of assetRoots(settings)) {
|
|
147
|
+
for (const rel of dirs) {
|
|
148
|
+
let entries: string[];
|
|
149
|
+
try {
|
|
150
|
+
entries = fs.readdirSync(path.join(root, ...rel.split("/")));
|
|
151
|
+
} catch {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
for (const name of entries) {
|
|
155
|
+
if (!name.toLowerCase().endsWith(".dds")) continue;
|
|
156
|
+
const low = name.toLowerCase();
|
|
157
|
+
if (seen.has(low)) continue;
|
|
158
|
+
seen.add(low);
|
|
159
|
+
items.push({
|
|
160
|
+
label: name,
|
|
161
|
+
kind: CompletionItemKind.File,
|
|
162
|
+
detail: `${rel} (${label})`,
|
|
163
|
+
sortText: name.toLowerCase(),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return items;
|
|
169
|
+
}
|