@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.
Files changed (163) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +334 -0
  3. package/THIRD-PARTY-NOTICES.md +83 -0
  4. package/data/ck3/dataTypes.json +2195 -0
  5. package/data/ck3/data_types/data_types_common.txt +2040 -0
  6. package/data/ck3/data_types/data_types_gui.txt +5264 -0
  7. package/data/ck3/data_types/data_types_internalclausewitzgui.txt +14843 -0
  8. package/data/ck3/data_types/data_types_script.txt +4251 -0
  9. package/data/ck3/data_types/data_types_uncategorized.txt +109984 -0
  10. package/data/ck3/freqs.json +1 -0
  11. package/data/ck3/guiSchema.json +6344 -0
  12. package/data/ck3/script_docs/effects.log +16059 -0
  13. package/data/ck3/script_docs/event_targets.log +2098 -0
  14. package/data/ck3/script_docs/modifiers.log +2228 -0
  15. package/data/ck3/script_docs/on_actions.log +5275 -0
  16. package/data/ck3/script_docs/triggers.log +11991 -0
  17. package/data/ck3/structures.json +9743 -0
  18. package/data/ck3/wikidocs/ATTRIBUTION.md +18 -0
  19. package/data/ck3/wikidocs/Data_types.md +2568 -0
  20. package/data/ck3/wikidocs/Effects_list.md +1176 -0
  21. package/data/ck3/wikidocs/Scopes_list.md +341 -0
  22. package/data/ck3/wikidocs/Triggers_list.md +1097 -0
  23. package/data/eu5/data_types/data_types_common.txt +2087 -0
  24. package/data/eu5/data_types/data_types_gui.txt +6732 -0
  25. package/data/eu5/data_types/data_types_internalclausewitzgui.txt +19276 -0
  26. package/data/eu5/data_types/data_types_script.txt +5688 -0
  27. package/data/eu5/data_types/data_types_uncategorized.txt +135569 -0
  28. package/data/vic3/data_types/data_types_common.txt +2021 -0
  29. package/data/vic3/data_types/data_types_gui.txt +5592 -0
  30. package/data/vic3/data_types/data_types_internalclausewitzgui.txt +17304 -0
  31. package/data/vic3/data_types/data_types_script.txt +2817 -0
  32. package/data/vic3/data_types/data_types_uncategorized.txt +84354 -0
  33. package/data/vic3/freqs.json +1 -0
  34. package/data/vic3/guiSchema.json +5578 -0
  35. package/data/vic3/script_docs/effects.log +38135 -0
  36. package/data/vic3/script_docs/event_targets.log +2028 -0
  37. package/data/vic3/script_docs/modifiers.log +18954 -0
  38. package/data/vic3/script_docs/on_actions.log +1561 -0
  39. package/data/vic3/script_docs/triggers.log +15738 -0
  40. package/data/vic3/structures.json +10189 -0
  41. package/dist/server.js +63668 -0
  42. package/media/px-lsp.svg +12 -0
  43. package/package.json +50 -0
  44. package/src/clientMode.ts +60 -0
  45. package/src/coa/coa.ts +184 -0
  46. package/src/coa/coaParse.ts +267 -0
  47. package/src/context.ts +78 -0
  48. package/src/contextKeywords.ts +224 -0
  49. package/src/data/dataBindingMacros.ts +82 -0
  50. package/src/data/dataFnDocs.ts +152 -0
  51. package/src/data/dataFnUsage.ts +431 -0
  52. package/src/data/dataTypes.ts +279 -0
  53. package/src/data/defines.ts +123 -0
  54. package/src/data/docsParser.ts +453 -0
  55. package/src/data/keywordDocs.ts +98 -0
  56. package/src/data/modifierTemplates.ts +143 -0
  57. package/src/data/textFormatting.ts +165 -0
  58. package/src/data/wikiDocs.ts +187 -0
  59. package/src/dds/decoder.ts +1007 -0
  60. package/src/dds/encode.ts +235 -0
  61. package/src/dds/index.ts +58 -0
  62. package/src/dds/png.ts +96 -0
  63. package/src/dds/tga.ts +62 -0
  64. package/src/documents.ts +35 -0
  65. package/src/features/assetPaths.ts +169 -0
  66. package/src/features/codeActions.ts +148 -0
  67. package/src/features/colors.ts +244 -0
  68. package/src/features/completion.ts +961 -0
  69. package/src/features/datafunction.ts +729 -0
  70. package/src/features/definition.ts +84 -0
  71. package/src/features/diagnostics.ts +244 -0
  72. package/src/features/folding.ts +106 -0
  73. package/src/features/formatting.ts +60 -0
  74. package/src/features/guiLanguage.ts +366 -0
  75. package/src/features/guiNavigation.ts +140 -0
  76. package/src/features/guiTree.ts +97 -0
  77. package/src/features/hover.ts +817 -0
  78. package/src/features/hoverRender.ts +222 -0
  79. package/src/features/inlayHints.ts +147 -0
  80. package/src/features/locFormatting.ts +127 -0
  81. package/src/features/references.ts +70 -0
  82. package/src/features/rename.ts +135 -0
  83. package/src/features/scopeAt.ts +65 -0
  84. package/src/features/semanticTokens.ts +188 -0
  85. package/src/features/signatureHelp.ts +72 -0
  86. package/src/features/symbols.ts +241 -0
  87. package/src/features/textureHover.ts +143 -0
  88. package/src/features/workspaceSymbols.ts +69 -0
  89. package/src/games/active.ts +19 -0
  90. package/src/games/ck3/ambientScopes.ts +273 -0
  91. package/src/games/ck3/index.ts +38 -0
  92. package/src/games/ck3/meta.ts +28 -0
  93. package/src/games/ck3/modifierPlaceholders.ts +61 -0
  94. package/src/games/ck3/saveSchema.ts +134 -0
  95. package/src/games/ck3/scaffolds.ts +197 -0
  96. package/src/games/ck3/schema.ts +422 -0
  97. package/src/games/ck3/structures.ts +887 -0
  98. package/src/games/eu5/index.ts +75 -0
  99. package/src/games/eu5/meta.ts +44 -0
  100. package/src/games/eu5/scaffolds.ts +49 -0
  101. package/src/games/eu5/schema.generated.ts +1043 -0
  102. package/src/games/jomini/variables.ts +134 -0
  103. package/src/games/profile.ts +205 -0
  104. package/src/games/registry.ts +27 -0
  105. package/src/games/vic3/index.ts +52 -0
  106. package/src/games/vic3/meta.ts +55 -0
  107. package/src/games/vic3/saveSchema.ts +77 -0
  108. package/src/games/vic3/scaffolds.ts +135 -0
  109. package/src/games/vic3/schema.ts +650 -0
  110. package/src/games/vic3/structures.ts +33 -0
  111. package/src/gui/anchorSpec.ts +66 -0
  112. package/src/gui/declMarkers.ts +30 -0
  113. package/src/gui/fillGeometry.ts +101 -0
  114. package/src/gui/guiDefs.ts +386 -0
  115. package/src/gui/guiDependencies.ts +352 -0
  116. package/src/gui/guiLinks.ts +64 -0
  117. package/src/gui/layoutEngine.ts +1998 -0
  118. package/src/gui/layoutService.ts +221 -0
  119. package/src/gui/measuredMetrics.ts +21 -0
  120. package/src/gui/previewService.ts +89 -0
  121. package/src/gui/saveSchema.ts +220 -0
  122. package/src/gui/saveValues.ts +399 -0
  123. package/src/gui/saveZip.ts +60 -0
  124. package/src/gui/sourceEdit.ts +535 -0
  125. package/src/gui/sourceEditService.ts +439 -0
  126. package/src/gui/sourceModel.ts +603 -0
  127. package/src/gui/textResolve.ts +145 -0
  128. package/src/gui/textureInfo.ts +106 -0
  129. package/src/gui/vocabulary.ts +149 -0
  130. package/src/gui/widgetEdit.ts +52 -0
  131. package/src/gui/widgetInfo.ts +245 -0
  132. package/src/index/docComments.ts +103 -0
  133. package/src/index/extract.ts +252 -0
  134. package/src/index/indexer.ts +369 -0
  135. package/src/index/intern.ts +101 -0
  136. package/src/index/lazyRefs.ts +145 -0
  137. package/src/index/modOrigin.ts +69 -0
  138. package/src/index/references.ts +534 -0
  139. package/src/overview/dependencies.ts +240 -0
  140. package/src/overview/eventBanner.ts +95 -0
  141. package/src/overview/eventDetail.ts +482 -0
  142. package/src/overview/eventGraph.ts +617 -0
  143. package/src/overview/eventVocabulary.ts +214 -0
  144. package/src/overview/locCoverage.ts +138 -0
  145. package/src/overview/modOverview.ts +29 -0
  146. package/src/overview/overrides.ts +89 -0
  147. package/src/parseCache.ts +81 -0
  148. package/src/parser/cst.ts +257 -0
  149. package/src/parser/encoding.ts +106 -0
  150. package/src/parser/index.ts +7 -0
  151. package/src/parser/lexer.ts +245 -0
  152. package/src/parser/locParser.ts +276 -0
  153. package/src/parser/parser.ts +360 -0
  154. package/src/schema/freqs.ts +70 -0
  155. package/src/schema/loader.ts +113 -0
  156. package/src/schema/types.ts +142 -0
  157. package/src/scopes/inference.ts +478 -0
  158. package/src/scopes/model.ts +148 -0
  159. package/src/scopes/varTypes.ts +290 -0
  160. package/src/server.ts +1894 -0
  161. package/src/serverData.ts +98 -0
  162. package/src/structure.ts +56 -0
  163. package/src/wordAt.ts +49 -0
@@ -0,0 +1,145 @@
1
+ /**
2
+ * What a textbox SHOWS, as far as a static preview can know it.
3
+ *
4
+ * A `text = "..."` value is one of: a localization key, a literal, or a mix of
5
+ * literal text and `[datafunction]` expressions the running game evaluates.
6
+ * The preview resolves what is knowable (a key through the loc index,
7
+ * `Localize('key')`, `Concept('key', 'text')`, a value the modder typed into
8
+ * the per-mod preview table) and shows the rest honestly: the last segment of
9
+ * the chain (`GetName`) marked as unresolved, never an invented value. The
10
+ * result is segmented so a client can style and explain each part.
11
+ *
12
+ * Pure: the loc index is an injected lookup. No vscode, no fs.
13
+ */
14
+ import type { GuiTextSegment } from "@px-lsp/protocol/protocol";
15
+
16
+ export interface TextResolvers {
17
+ /** The configured language's value for a loc key, or undefined. */
18
+ loc: (key: string) => string | undefined;
19
+ /** Modder-supplied preview text per exact `[...]` source (without brackets). */
20
+ previewValues?: Record<string, string>;
21
+ }
22
+
23
+ export interface ResolvedText {
24
+ /** What is measured and drawn. */
25
+ text: string;
26
+ /** Absent when the text is a plain literal with nothing to explain. */
27
+ segments?: GuiTextSegment[];
28
+ }
29
+
30
+ /** A loc key: one word of key characters, nothing a literal sentence would have. */
31
+ const LOC_KEY = /^[A-Za-z0-9_][A-Za-z0-9_.\-']*$/;
32
+
33
+ /**
34
+ * Loc formatting the game does not draw: `#bold text#!`, `#R text#!`, `§Ytext§!`,
35
+ * `@icon!` icon references. Removed for measurement; the preview has no glyphs for them.
36
+ */
37
+ function stripFormatting(s: string): string {
38
+ return s
39
+ .replace(/#!/g, "")
40
+ .replace(/#[A-Za-z_][A-Za-z0-9_;:]*\s?/g, "")
41
+ .replace(/§!/g, "")
42
+ .replace(/§[A-Za-z0-9]/g, "")
43
+ .replace(/@[A-Za-z0-9_]+!/g, "")
44
+ .replace(/\\n/g, "\n");
45
+ }
46
+
47
+ /** Split `a [Fn] b [[literal]` into literal and datafunction pieces. */
48
+ function tokenize(s: string): { literal?: string; fn?: string }[] {
49
+ const out: { literal?: string; fn?: string }[] = [];
50
+ let lit = "";
51
+ for (let i = 0; i < s.length; i++) {
52
+ const c = s[i];
53
+ if (c === "[" && s[i + 1] === "[") {
54
+ lit += "[";
55
+ i++;
56
+ continue;
57
+ }
58
+ if (c === "[") {
59
+ let depth = 1;
60
+ let j = i + 1;
61
+ for (; j < s.length && depth > 0; j++) {
62
+ if (s[j] === "[") depth++;
63
+ else if (s[j] === "]") depth--;
64
+ }
65
+ if (depth !== 0) {
66
+ lit += s.slice(i);
67
+ break;
68
+ }
69
+ if (lit) out.push({ literal: lit });
70
+ lit = "";
71
+ out.push({ fn: s.slice(i + 1, j - 1) });
72
+ i = j - 1;
73
+ continue;
74
+ }
75
+ lit += c;
76
+ }
77
+ if (lit) out.push({ literal: lit });
78
+ return out;
79
+ }
80
+
81
+ /** `Localize('key')` / `Concept('key','shown')` / `Concept('key')`: the loc the chain stands for. */
82
+ function locOfCall(fn: string, loc: TextResolvers["loc"]): string | undefined {
83
+ const m = /^(Localize|Concept)\s*\(\s*'([^']*)'(?:\s*,\s*'([^']*)')?\s*\)$/.exec(fn.trim());
84
+ if (!m) return undefined;
85
+ if (m[1] === "Concept" && m[3] !== undefined) return loc(m[3]) ?? m[3];
86
+ return loc(m[2]);
87
+ }
88
+
89
+ /** `GetPlayer.GetName` -> `GetName`; `Concept('x','y')` -> `Concept`; strips a `Get` prefix and arguments. */
90
+ function chipFor(fn: string): string {
91
+ // `|0`, `|%`: format specifiers after the chain, not part of the name.
92
+ const chain = fn.split("|")[0];
93
+ const last = chain.split(".").pop() ?? chain;
94
+ const name = last.replace(/\(.*$/, "").trim() || fn;
95
+ return name.replace(/^Get(?=[A-Z])/, "") || name;
96
+ }
97
+
98
+ function resolveFn(fn: string, r: TextResolvers): GuiTextSegment {
99
+ const override = r.previewValues?.[fn] ?? r.previewValues?.[`[${fn}]`];
100
+ if (override !== undefined) return { text: override, kind: "datafn", source: fn, resolved: true };
101
+ const viaLoc = locOfCall(fn, r.loc);
102
+ if (viaLoc !== undefined)
103
+ return { text: stripFormatting(viaLoc), kind: "datafn", source: fn, resolved: true };
104
+ return { text: chipFor(fn), kind: "datafn", source: fn, resolved: false };
105
+ }
106
+
107
+ /** Resolve one string's datafunctions into segments (no loc-key lookup of the whole). */
108
+ function resolveMixed(s: string, r: TextResolvers): GuiTextSegment[] {
109
+ return tokenize(s).map((t) =>
110
+ t.fn !== undefined
111
+ ? resolveFn(t.fn, r)
112
+ : {
113
+ text: stripFormatting(t.literal ?? ""),
114
+ kind: "literal" as const,
115
+ source: t.literal ?? "",
116
+ resolved: true,
117
+ }
118
+ );
119
+ }
120
+
121
+ export function resolveGuiText(raw: string, r: TextResolvers): ResolvedText {
122
+ const trimmed = raw.trim();
123
+ if (!trimmed) return { text: "" };
124
+ if (LOC_KEY.test(trimmed)) {
125
+ const value = r.loc(trimmed);
126
+ if (value !== undefined) {
127
+ // The value itself may hold datafunctions; those resolve one level deep.
128
+ const inner = resolveMixed(value, r);
129
+ const text = inner.map((s) => s.text).join("");
130
+ const segments: GuiTextSegment[] =
131
+ inner.length === 1 && inner[0].kind === "literal"
132
+ ? [{ text, kind: "loc", source: trimmed, resolved: true }]
133
+ : inner.map((s) => (s.kind === "literal" ? { ...s, kind: "loc", source: trimmed } : s));
134
+ return { text, segments };
135
+ }
136
+ // Not in the index: a key nobody localized yet, or a literal word. Shown as is, flagged.
137
+ if (trimmed.includes("_") || trimmed.includes(".")) {
138
+ return { text: trimmed, segments: [{ text: trimmed, kind: "loc", source: trimmed, resolved: false }] };
139
+ }
140
+ return { text: trimmed };
141
+ }
142
+ const segments = resolveMixed(trimmed, r);
143
+ const text = segments.map((s) => s.text).join("");
144
+ return segments.every((s) => s.kind === "literal") ? { text } : { text, segments };
145
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Frame-sheet facts for the inspector: where a widget's texture file is, how
3
+ * big the sheet is, and which cell of it the widget draws.
4
+ *
5
+ * The sheet's pixel size is the only thing that needs the file, and a DDS
6
+ * carries it in its 128-byte header, so this reads exactly that prefix and
7
+ * never decodes: an inspector row must not cost a 4096x4096 BC7 decode.
8
+ *
9
+ * The grid comes from `framesize = { w h }` plus `frame` (Studio §L, L22),
10
+ * which is what the vanilla trees carry: the default profile's gui tree has
11
+ * 111 files with a `framesize` and neither harvested vanilla tree (nor either
12
+ * harvested guiSchema.json) contains a `noofframes`, so no second spelling is
13
+ * invented here. The cell math itself is computeFrameCell's, shared with the renderer.
14
+ *
15
+ * No `vscode` imports: unit-tested in plain Node.
16
+ */
17
+ import * as fs from "fs";
18
+ import * as path from "path";
19
+ import type { GuiTextureInfo } from "@px-lsp/protocol/protocol";
20
+ import { ddsFormatInfo } from "../dds/decoder";
21
+ import { computeFrameCell } from "./fillGeometry";
22
+
23
+ /** Roots a mod-relative texture path is resolved against. */
24
+ export interface TextureRoots {
25
+ gamePath: string | null;
26
+ modPath: string | null;
27
+ /** Parent/dependency mods in load order, base first. */
28
+ parentPaths?: string[];
29
+ /** Engine (jomini) roots, below the game. */
30
+ engineRoots?: string[];
31
+ }
32
+
33
+ /**
34
+ * A DDS header is 128 bytes, 148 with the DX10 extension. Read a round 256 so
35
+ * a header read is one syscall and never depends on the file's total size.
36
+ */
37
+ const HEADER_BYTES = 256;
38
+
39
+ /**
40
+ * First root that has the file. Load order for a plain asset is last-in-wins,
41
+ * so the mod is tried first, then parent mods from the last loaded back, then
42
+ * the game, then the engine folder.
43
+ */
44
+ export function resolveTextureFile(rel: string, roots: TextureRoots): string | null {
45
+ const parents = [...(roots.parentPaths ?? [])].reverse();
46
+ const order = [roots.modPath, ...parents, roots.gamePath, ...(roots.engineRoots ?? [])];
47
+ for (const root of order) {
48
+ if (!root) continue;
49
+ const abs = path.join(root, rel);
50
+ try {
51
+ if (fs.statSync(abs).isFile()) return abs;
52
+ } catch {
53
+ /* not under this root */
54
+ }
55
+ }
56
+ return null;
57
+ }
58
+
59
+ /** Width/height from the file's header alone; null when it is not a readable DDS. */
60
+ export function readTextureSize(file: string): { width: number; height: number } | null {
61
+ let fd: number;
62
+ try {
63
+ fd = fs.openSync(file, "r");
64
+ } catch {
65
+ return null;
66
+ }
67
+ try {
68
+ const buf = Buffer.alloc(HEADER_BYTES);
69
+ const read = fs.readSync(fd, buf, 0, HEADER_BYTES, 0);
70
+ const info = ddsFormatInfo(new Uint8Array(buf.subarray(0, read)));
71
+ if (!info || info.width <= 0 || info.height <= 0) return null;
72
+ return { width: info.width, height: info.height };
73
+ } catch {
74
+ return null;
75
+ } finally {
76
+ fs.closeSync(fd);
77
+ }
78
+ }
79
+
80
+ /** What the inspector shows for one fill: the path, the sheet, the current cell. */
81
+ export function describeTexture(
82
+ fill: { texture: string; framesize?: [number, number]; frame?: number },
83
+ source: "fill" | "background",
84
+ roots?: TextureRoots
85
+ ): GuiTextureInfo {
86
+ const info: GuiTextureInfo = { path: fill.texture, source };
87
+ if (fill.framesize) {
88
+ info.framesize = fill.framesize;
89
+ info.frame = fill.frame ?? 1;
90
+ }
91
+ const file = roots ? resolveTextureFile(fill.texture, roots) : null;
92
+ if (!file) return info;
93
+ info.file = file;
94
+ const size = readTextureSize(file);
95
+ if (!size) return info;
96
+ info.width = size.width;
97
+ info.height = size.height;
98
+ if (!fill.framesize) return info;
99
+ const [fw, fh] = fill.framesize;
100
+ if (fw <= 0 || fh <= 0) return info;
101
+ info.columns = Math.max(1, Math.floor(size.width / fw));
102
+ info.rows = Math.max(1, Math.floor(size.height / fh));
103
+ const cell = computeFrameCell(fill.framesize, info.frame ?? 1, size.width, size.height);
104
+ info.cell = { x: cell.sx, y: cell.sy, w: cell.sw, h: cell.sh };
105
+ return info;
106
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * paradox/guiVocabulary backend: which widgets a designer palette may offer.
3
+ *
4
+ * Every name comes from something the project already harvested, never from a
5
+ * list typed here: the bundled `data/<game>/guiSchema.json` (600+ widget types
6
+ * with their vanilla usage counts, built by `scripts/build-gui-schema.ts`) plus
7
+ * the requested document's own `template` / `type` declarations. A palette that
8
+ * offered a name from memory would write a widget the game does not know, which
9
+ * is exactly the failure AGENTS.md's one design idea exists to prevent.
10
+ *
11
+ * The same harvest answers the other half of a designer's vocabulary: which
12
+ * PROPERTIES a widget type carries, for an inspector that offers to add one.
13
+ * Ranked by vanilla usage, scoped to the types this document names, with the
14
+ * tree-wide ranking as the fallback for a type the harvest does not know.
15
+ *
16
+ * `container` is derived the same way: a type is one when the vanilla tree ever
17
+ * wrote a WIDGET block inside it (the harvest counts child keys among a type's
18
+ * props), with the engine's own attribute-block set excluded so `size` and
19
+ * `background` do not make everything a container.
20
+ *
21
+ * No `vscode` imports: unit-tested in plain Node.
22
+ */
23
+ import type { GuiVocabularyEntry, GuiVocabularyResult } from "@px-lsp/protocol/protocol";
24
+ import { parseScript, type Statement } from "../parser";
25
+ import { collectGuiDefsParsed } from "./guiDefs";
26
+ import { PROPERTY_BLOCKS } from "./layoutEngine";
27
+
28
+ /** The slice of `guiSchema.json` a palette needs. */
29
+ interface GuiSchemaTypes {
30
+ types?: Record<string, { count: number; props?: Record<string, number> }>;
31
+ globalProps?: Record<string, number>;
32
+ }
33
+
34
+ /**
35
+ * How many harvested types a palette gets. The tail is single-use vanilla
36
+ * types; `total` reports what was left out rather than pretending the list is
37
+ * everything. A UI budget, not a measurement.
38
+ */
39
+ export const VOCABULARY_LIMIT = 300;
40
+
41
+ /**
42
+ * How many property names one type offers, and how long the tree-wide fallback
43
+ * ranking is. Both are UI budgets: the harvest keeps up to 100 properties per
44
+ * type and 200 overall, and a completion list nobody scrolls past the twentieth
45
+ * row of does not need to carry the tail across the wire on every layout.
46
+ */
47
+ export const TYPE_PROPERTY_LIMIT = 60;
48
+ export const COMMON_PROPERTY_LIMIT = 80;
49
+
50
+ export function computeGuiVocabulary(text: string, schema: unknown): GuiVocabularyResult {
51
+ const types = (schema as GuiSchemaTypes | undefined)?.types ?? {};
52
+ const known = new Set(Object.keys(types));
53
+ const entries: GuiVocabularyEntry[] = [];
54
+
55
+ // The document's own declarations first, and never capped: they are the ones
56
+ // its author reaches for, and no harvest can know them.
57
+ const statements = parseScript(text).root.statements;
58
+ const own = collectGuiDefsParsed(statements);
59
+ for (const [name, def] of own.types) {
60
+ entries.push({ name, kind: "type", local: true, base: def.base, container: true });
61
+ }
62
+ for (const [name] of own.templates) {
63
+ entries.push({ name, kind: "template", local: true });
64
+ }
65
+ const declared = new Set(entries.map((e) => e.name));
66
+
67
+ const harvested = [...known]
68
+ .filter((name) => !declared.has(name))
69
+ .sort((a, b) => types[b].count - types[a].count || a.localeCompare(b));
70
+ for (const name of harvested.slice(0, VOCABULARY_LIMIT)) {
71
+ entries.push({
72
+ name,
73
+ kind: "builtin",
74
+ count: types[name].count,
75
+ container: holdsWidgets(types[name].props, known),
76
+ });
77
+ }
78
+ return {
79
+ entries,
80
+ total: declared.size + harvested.length,
81
+ properties: propertiesFor(statements, own.types, types),
82
+ commonProperties: rank((schema as GuiSchemaTypes | undefined)?.globalProps, COMMON_PROPERTY_LIMIT),
83
+ };
84
+ }
85
+
86
+ /**
87
+ * The property names the harvest saw on the widget types THIS DOCUMENT names,
88
+ * which is what an inspector's add-property row completes from. Scoped to the
89
+ * document rather than sent whole because the harvest holds 556 types and an
90
+ * open panel re-asks after every layout; the types a file actually writes are a
91
+ * couple of dozen, and `commonProperties` covers the rest.
92
+ *
93
+ * "Names" is the union of the keys it writes blocks under and the bases of its
94
+ * own `type X = base` declarations: a derived type's properties live under its
95
+ * base in the harvest, which is also where the widget's type chain ends.
96
+ */
97
+ function propertiesFor(
98
+ statements: readonly Statement[],
99
+ localTypes: ReadonlyMap<string, { base: string }>,
100
+ types: Record<string, { count: number; props?: Record<string, number> }>
101
+ ): Record<string, string[]> {
102
+ const named = new Set<string>();
103
+ collectBlockKeys(statements, named);
104
+ for (const def of localTypes.values()) named.add(def.base.toLowerCase());
105
+
106
+ const out: Record<string, string[]> = {};
107
+ for (const name of named) {
108
+ const props = types[name]?.props;
109
+ if (props) out[name] = rank(props, TYPE_PROPERTY_LIMIT);
110
+ }
111
+ return out;
112
+ }
113
+
114
+ /** Every key the document writes a block under, lowercased, at any depth. */
115
+ function collectBlockKeys(statements: readonly Statement[], into: Set<string>): void {
116
+ for (const stmt of statements) {
117
+ if (stmt.kind === "value") {
118
+ if (stmt.value.kind === "block") collectBlockKeys(stmt.value.statements, into);
119
+ continue;
120
+ }
121
+ const value = stmt.value;
122
+ if (!value) continue;
123
+ const block = value.kind === "block" ? value : value.kind === "tagged-block" ? value.block : null;
124
+ if (!block) continue;
125
+ if (!stmt.key.quoted) into.add(stmt.key.text.toLowerCase());
126
+ collectBlockKeys(block.statements, into);
127
+ }
128
+ }
129
+
130
+ /** Usage counts to names, most used first, capped. */
131
+ function rank(counts: Record<string, number> | undefined, limit: number): string[] {
132
+ return Object.entries(counts ?? {})
133
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
134
+ .slice(0, limit)
135
+ .map(([name]) => name);
136
+ }
137
+
138
+ /**
139
+ * A type the vanilla tree ever wrote another widget inside. The harvest counts
140
+ * child widget keys among a type's props, so the test is "does any prop name a
141
+ * known widget type", with the engine's attribute blocks (`size`, `background`,
142
+ * `state`, …) taken out — those are data, not children.
143
+ */
144
+ function holdsWidgets(props: Record<string, number> | undefined, known: ReadonlySet<string>): boolean {
145
+ for (const prop of Object.keys(props ?? {})) {
146
+ if (!PROPERTY_BLOCKS.has(prop) && known.has(prop)) return true;
147
+ }
148
+ return false;
149
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * paradox/guiWidgetEdit backend: DEPRECATED, kept for hosts already wired to
3
+ * it. It is a thin alias over the `paradox/guiSourceEdit` core
4
+ * (`sourceEditService.ts`): the same span model, the same refusal guards, the
5
+ * same edits, narrowed to the one gesture this request can express (set
6
+ * `position` or `size` on the widget whose statement starts on a given line).
7
+ *
8
+ * What the narrow shape costs the caller: it returns ONE edit or null, so a
9
+ * refusal arrives as a bare null with its reason dropped, and a batch has to be
10
+ * several round trips. New hosts should send `paradox/guiSourceEdit` with a
11
+ * `setProperties` op instead.
12
+ */
13
+ import type { GuiDefs } from "./guiDefs";
14
+ import { emptyGuiDefs } from "./guiDefs";
15
+ import { computeGuiSourceEdit } from "./sourceEditService";
16
+
17
+ export interface WidgetTextEdit {
18
+ /** UTF-16 offsets into the request's text. */
19
+ start: number;
20
+ end: number;
21
+ newText: string;
22
+ }
23
+
24
+ const PAIR_PROPERTIES = new Set(["position", "size"]);
25
+
26
+ export function computeGuiWidgetEdit(
27
+ text: string,
28
+ line: number,
29
+ property: string,
30
+ values: [number, number],
31
+ defs: GuiDefs = emptyGuiDefs()
32
+ ): WidgetTextEdit | null {
33
+ if (!PAIR_PROPERTIES.has(property)) return null;
34
+ const result = computeGuiSourceEdit(
35
+ text,
36
+ {
37
+ kind: "setProperties",
38
+ line,
39
+ properties: [{ key: property, value: `{ ${fmt(values[0])} ${fmt(values[1])} }` }],
40
+ },
41
+ defs
42
+ );
43
+ // One edit is all this shape can carry, and a batch of one is what it asks
44
+ // for; a refusal (and its reason) collapses to null.
45
+ const edits = result?.edits;
46
+ return edits?.length === 1 ? edits[0] : null;
47
+ }
48
+
49
+ function fmt(v: number): string {
50
+ const rounded = Math.round(v);
51
+ return Math.abs(v - rounded) < 0.005 ? String(rounded) : v.toFixed(1);
52
+ }