@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,148 @@
1
+ /**
2
+ * Code actions on loc-key references in script. Gated per command id
3
+ * (initializationOptions.client.commands): where the client registers the
4
+ * command the action carries it, so the file write happens in the client that
5
+ * owns the editor UX. Where it does not, the create-key quick fix carries a
6
+ * real WorkspaceEdit instead and the two editor-command actions are omitted
7
+ * rather than shipped dead.
8
+ */
9
+ import {
10
+ CodeActionKind,
11
+ CreateFile,
12
+ TextDocumentEdit,
13
+ type CodeAction,
14
+ type Diagnostic,
15
+ type Range,
16
+ type WorkspaceEdit,
17
+ } from "vscode-languageserver/node";
18
+ import type { TextDocument } from "vscode-languageserver-textdocument";
19
+ import * as fs from "fs";
20
+ import * as path from "path";
21
+ import { URI } from "vscode-uri";
22
+ import type { ServerData } from "../serverData";
23
+ import { findLocKeyRefs, type LocKeyRef } from "@px-lsp/protocol/locRefs";
24
+ import { clientCommands } from "@px-lsp/protocol/protocol";
25
+ import { getLineText } from "../documents";
26
+ import { activeProfile } from "../games/active";
27
+ import { canRunCommand } from "../clientMode";
28
+
29
+ export function locKeyRefAt(lineText: string, character: number): LocKeyRef | null {
30
+ const refs = findLocKeyRefs(lineText);
31
+ return refs.find((r) => character >= r.start - 1 && character <= r.end + 1) ?? refs[0] ?? null;
32
+ }
33
+
34
+ /** Context for the plain-client WorkspaceEdit fallback. */
35
+ export interface LocEditContext {
36
+ locLanguage: string;
37
+ /** Workspace-mod root of a file, or null (vanilla/parent files get no edit). */
38
+ modRootOf: (fsPath: string) => string | null;
39
+ /** Mod-relative localization root(s) from the loc_key schema entries. */
40
+ locRoots: string[];
41
+ }
42
+
43
+ /**
44
+ * A WorkspaceEdit that appends `key: ""` to the server-managed loc file
45
+ * (creating it, BOM included, when absent). Only one writer is ever active
46
+ * per session: a client registering px.editLocalization owns its own
47
+ * zzz_*_edits file and never sees this fallback, so the two files can coexist
48
+ * without fighting.
49
+ */
50
+ export function locCreateEdit(key: string, docFsPath: string, ctx: LocEditContext): WorkspaceEdit | null {
51
+ const modRoot = ctx.modRootOf(docFsPath);
52
+ if (!modRoot) return null;
53
+ const lang = ctx.locLanguage;
54
+ const locRoot = ctx.locRoots[0] ?? "localization";
55
+ const target = path.join(modRoot, locRoot, lang, `zzz_px_lsp_edits_l_${lang}.yml`);
56
+ const uri = URI.file(target).toString();
57
+ const entryLine = ` ${key}: ""`;
58
+ let content: string | null = null;
59
+ try {
60
+ content = fs.readFileSync(target, "utf8");
61
+ } catch {
62
+ content = null;
63
+ }
64
+ if (content === null) {
65
+ return {
66
+ documentChanges: [
67
+ CreateFile.create(uri, { ignoreIfExists: true }),
68
+ TextDocumentEdit.create({ uri, version: null }, [
69
+ {
70
+ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
71
+ newText: "" + `l_${lang}:\n${entryLine}\n`,
72
+ },
73
+ ]),
74
+ ],
75
+ };
76
+ }
77
+ const endLine = content.split("\n").length;
78
+ const needsNewline = content.length > 0 && !content.endsWith("\n");
79
+ return {
80
+ documentChanges: [
81
+ TextDocumentEdit.create({ uri, version: null }, [
82
+ {
83
+ range: { start: { line: endLine, character: 0 }, end: { line: endLine, character: 0 } },
84
+ newText: `${needsNewline ? "\n" : ""}${entryLine}\n`,
85
+ },
86
+ ]),
87
+ ],
88
+ };
89
+ }
90
+
91
+ export function provideCodeActions(
92
+ data: ServerData,
93
+ document: TextDocument,
94
+ range: Range,
95
+ diagnostics: Diagnostic[],
96
+ locEditContext?: LocEditContext
97
+ ): CodeAction[] {
98
+ const actions: CodeAction[] = [];
99
+ const canEditLoc = canRunCommand(clientCommands.editLocalization);
100
+ const canOpenSideBySide = canRunCommand(clientCommands.openLocalizationSideBySide);
101
+
102
+ // Quick fix on missing-required-loc diagnostics: create the key in place.
103
+ for (const d of diagnostics) {
104
+ if (d.code !== "missing-required-loc") continue;
105
+ const key = (d.data as { key?: string } | undefined)?.key;
106
+ if (!key) continue;
107
+ const title = `${activeProfile().shortName}: Create localization key "${key}"`;
108
+ if (canEditLoc) {
109
+ actions.push({
110
+ title,
111
+ kind: CodeActionKind.QuickFix,
112
+ diagnostics: [d],
113
+ command: { command: clientCommands.editLocalization, title: "Create localization", arguments: [key] },
114
+ });
115
+ } else if (locEditContext) {
116
+ const edit = locCreateEdit(key, URI.parse(document.uri).fsPath, locEditContext);
117
+ if (edit) actions.push({ title, kind: CodeActionKind.QuickFix, diagnostics: [d], edit });
118
+ }
119
+ }
120
+
121
+ // The two editor-command actions exist only for clients that register their
122
+ // command; elsewhere they would render and silently do nothing.
123
+ if (!canEditLoc && !canOpenSideBySide) return actions;
124
+
125
+ const ref = locKeyRefAt(getLineText(document, range.start.line), range.start.character);
126
+ if (!ref) return actions;
127
+
128
+ if (canEditLoc) {
129
+ actions.push({
130
+ title: `${activeProfile().shortName}: Edit localization for "${ref.key}"`,
131
+ kind: CodeActionKind.QuickFix,
132
+ command: { command: clientCommands.editLocalization, title: "Edit localization", arguments: [ref.key] },
133
+ });
134
+ }
135
+
136
+ if (canOpenSideBySide && data.index.lookup(ref.key).some((d) => d.kind === "loc_key")) {
137
+ actions.push({
138
+ title: `${activeProfile().shortName}: Open localization side by side`,
139
+ kind: CodeActionKind.Empty,
140
+ command: {
141
+ command: clientCommands.openLocalizationSideBySide,
142
+ title: "Open localization side by side",
143
+ arguments: [ref.key],
144
+ },
145
+ });
146
+ }
147
+ return actions;
148
+ }
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Color swatches and the multi-format picker (issue #11), via the standard
3
+ * LSP documentColor / colorPresentation pair so the editor's native picker
4
+ * drives it.
5
+ *
6
+ * The recognised forms are the ones measured in the vanilla files of the two
7
+ * calibrated games (2026-08), not the habits older-engine tools assume:
8
+ * rgb { 174 169 166 } ints 0..255
9
+ * hsv { 0.6 0.5 0.7 } hue 0..1, NOT 0..360 (998 vanilla uses)
10
+ * hsv360 { 358 70 65 } hue 0..360, s and v 0..100 (179 vanilla uses)
11
+ * hex { 50779b } six hex digits, no 0x prefix
12
+ * { 0.9 0.8 0.2 1 } untagged floats 0..1, alpha optional (.gui, named colors)
13
+ * { 180 75 80 } untagged ints 0..255
14
+ * Untagged blocks are only colors when the key says so (`color`, `color1`,
15
+ * `map_color`, ...) or when they sit inside a `colors = { }` table
16
+ * (common/named_colors). Tagged blocks are colors wherever they appear.
17
+ *
18
+ * The untagged int/float split follows the engine: a component with a `.`
19
+ * or every component <= 1 means floats, otherwise 0..255. `{ 1 1 1 }` is
20
+ * therefore white, which is what every vanilla .gui relies on.
21
+ */
22
+ import type { Color, ColorInformation, ColorPresentation, Range } from "vscode-languageserver/node";
23
+ import type { TextDocument } from "vscode-languageserver-textdocument";
24
+ import {
25
+ walkStatements,
26
+ type AssignmentNode,
27
+ type BlockNode,
28
+ type Statement,
29
+ type ValueNode,
30
+ } from "../parser";
31
+ import { getParse } from "../parseCache";
32
+
33
+ /** `rgbf` is a tagged rgb block written with 0..1 components (vanilla named_colors
34
+ * does this); it is offered back in the same notation, never as 0..255. */
35
+ export type ColorFormat = "rgb" | "rgbf" | "hsv" | "hsv360" | "hex" | "float" | "int";
36
+
37
+ const TAGS: ReadonlySet<string> = new Set(["rgb", "hsv", "hsv360", "hex"]);
38
+
39
+ /** The format rides along in the ColorInformation so the presentation request
40
+ * can lead with the author's own notation; the LSP layer passes extra fields
41
+ * through as plain object data. */
42
+ export interface ColorSite extends ColorInformation {
43
+ format: ColorFormat;
44
+ /** The source spelled a fourth component, so presentations keep it. */
45
+ alpha: boolean;
46
+ }
47
+
48
+ interface Decoded {
49
+ color: Color;
50
+ format: ColorFormat;
51
+ alpha: boolean;
52
+ }
53
+
54
+ function numbers(block: BlockNode): number[] | null {
55
+ const out: number[] = [];
56
+ for (const s of block.statements) {
57
+ if (s.kind !== "value" || s.value.kind !== "scalar" || s.value.quoted) return null;
58
+ const n = Number(s.value.text);
59
+ if (!Number.isFinite(n)) return null;
60
+ out.push(n);
61
+ }
62
+ return out.length === 3 || out.length === 4 ? out : null;
63
+ }
64
+
65
+ function clamp01(n: number): number {
66
+ return Math.min(1, Math.max(0, n));
67
+ }
68
+
69
+ function hsvToRgb(h: number, s: number, v: number): [number, number, number] {
70
+ const i = Math.floor(h * 6);
71
+ const f = h * 6 - i;
72
+ const p = v * (1 - s);
73
+ const q = v * (1 - f * s);
74
+ const t = v * (1 - (1 - f) * s);
75
+ switch (((i % 6) + 6) % 6) {
76
+ case 0:
77
+ return [v, t, p];
78
+ case 1:
79
+ return [q, v, p];
80
+ case 2:
81
+ return [p, v, t];
82
+ case 3:
83
+ return [p, q, v];
84
+ case 4:
85
+ return [t, p, v];
86
+ default:
87
+ return [v, p, q];
88
+ }
89
+ }
90
+
91
+ function rgbToHsv(r: number, g: number, b: number): [number, number, number] {
92
+ const max = Math.max(r, g, b);
93
+ const min = Math.min(r, g, b);
94
+ const d = max - min;
95
+ const s = max === 0 ? 0 : d / max;
96
+ let h = 0;
97
+ if (d > 0) {
98
+ if (max === r) h = (g - b) / d + (g < b ? 6 : 0);
99
+ else if (max === g) h = (b - r) / d + 2;
100
+ else h = (r - g) / d + 4;
101
+ h /= 6;
102
+ }
103
+ return [h, s, max];
104
+ }
105
+
106
+ /** `{ 1 1 1 }` is white and `{ 255 0 0 }` is red: floats when any component
107
+ * carries a fraction or all of them fit in 0..1. */
108
+ function rgbComponents(c: number[], format: ColorFormat): Decoded {
109
+ const floats = c.some((n) => !Number.isInteger(n)) || c.every((n) => n <= 1);
110
+ const k = floats ? 1 : 255;
111
+ const alpha = c.length === 4;
112
+ return {
113
+ color: {
114
+ red: clamp01(c[0] / k),
115
+ green: clamp01(c[1] / k),
116
+ blue: clamp01(c[2] / k),
117
+ alpha: alpha ? clamp01(c[3] / k) : 1,
118
+ },
119
+ format: format === "rgb" ? (floats ? "rgbf" : "rgb") : floats ? "float" : "int",
120
+ alpha,
121
+ };
122
+ }
123
+
124
+ /** Decode one value node into a color, or null when it is not a color. */
125
+ function decode(value: ValueNode, keyIsColor: boolean): Decoded | null {
126
+ if (value.kind === "tagged-block") {
127
+ const tag = value.tag.text;
128
+ if (!TAGS.has(tag)) return null;
129
+ if (tag === "hex") {
130
+ const s = value.block.statements;
131
+ if (s.length !== 1 || s[0].kind !== "value" || s[0].value.kind !== "scalar" || s[0].value.quoted)
132
+ return null;
133
+ const m = /^(?:0x)?([0-9a-fA-F]{6})$/.exec(s[0].value.text);
134
+ if (!m) return null;
135
+ const n = parseInt(m[1], 16);
136
+ return {
137
+ color: { red: (n >> 16) / 255, green: ((n >> 8) & 255) / 255, blue: (n & 255) / 255, alpha: 1 },
138
+ format: "hex",
139
+ alpha: false,
140
+ };
141
+ }
142
+ const c = numbers(value.block);
143
+ if (!c) return null;
144
+ // Vanilla named_colors spells `rgb { 1 0.4 0.6 }`, so rgb follows the untagged split too.
145
+ if (tag === "rgb") return rgbComponents(c, "rgb");
146
+ const alpha = c.length === 4;
147
+ const format: ColorFormat = tag === "hsv360" ? "hsv360" : "hsv";
148
+ const [h, s, v] = format === "hsv360" ? [c[0] / 360, c[1] / 100, c[2] / 100] : [c[0], c[1], c[2]];
149
+ const [r, g, b] = hsvToRgb(((h % 1) + 1) % 1, clamp01(s), clamp01(v));
150
+ return { color: { red: r, green: g, blue: b, alpha: alpha ? clamp01(c[3]) : 1 }, format, alpha };
151
+ }
152
+ if (value.kind === "block" && keyIsColor) {
153
+ const c = numbers(value);
154
+ return c ? rgbComponents(c, "float") : null;
155
+ }
156
+ return null;
157
+ }
158
+
159
+ /** Portrait genes: `hair_color = { 32 235 66 229 }` is two palette coordinates
160
+ * (x y x y), not RGBA. 766 sites per game in dna_data, ethnicities and
161
+ * bookmark_portraits would otherwise show a meaningless swatch. */
162
+ const GENE_KEYS: ReadonlySet<string> = new Set(["hair_color", "skin_color", "eye_color"]);
163
+
164
+ function keyNamesColor(stmt: Statement, ancestors: readonly (AssignmentNode | BlockNode)[]): boolean {
165
+ if (stmt.kind === "assignment" && stmt.key.text.includes("color")) return !GENE_KEYS.has(stmt.key.text);
166
+ // common/named_colors: `colors = { english = { 0.8 0.2 0.2 } }`.
167
+ const parent = ancestors[ancestors.length - 2];
168
+ return parent?.kind === "assignment" && parent.key.text === "colors";
169
+ }
170
+
171
+ export function provideDocumentColors(document: TextDocument): ColorSite[] {
172
+ const { result, lineIndex } = getParse(document);
173
+ const out: ColorSite[] = [];
174
+ walkStatements(result.root, (stmt, ancestors) => {
175
+ const value = stmt.value;
176
+ if (!value) return;
177
+ const hit = decode(value, keyNamesColor(stmt, ancestors));
178
+ if (!hit) return;
179
+ const range: Range = {
180
+ start: lineIndex.positionAt(value.range.start),
181
+ end: lineIndex.positionAt(value.range.end),
182
+ };
183
+ out.push({ range, color: hit.color, format: hit.format, alpha: hit.alpha });
184
+ });
185
+ return out;
186
+ }
187
+
188
+ const FORMATS: readonly ColorFormat[] = ["rgb", "hsv", "hsv360", "hex", "float", "int"];
189
+
190
+ /**
191
+ * `alpha` is the SOURCE's fourth component (null = the source spelled none),
192
+ * not the picker's: the editor's opacity slider always exists and cannot be
193
+ * hidden, so presentations pin alpha to what the file already says. A color
194
+ * with alpha keeps exactly that alpha, a color without one never gains one —
195
+ * script colors are not meant to grow alpha channels from a stray drag.
196
+ */
197
+ export function renderColor(color: Color, format: ColorFormat, alpha: number | null): string {
198
+ const i = (n: number) => Math.round(n * 255);
199
+ const f = (n: number) => String(Math.round(n * 1000) / 1000);
200
+ const withAlpha = alpha !== null;
201
+ const { red: r, green: g, blue: b } = color;
202
+ const a = alpha ?? 1;
203
+ switch (format) {
204
+ case "rgb":
205
+ return `rgb { ${i(r)} ${i(g)} ${i(b)}${withAlpha ? ` ${i(a)}` : ""} }`;
206
+ case "rgbf":
207
+ return `rgb { ${f(r)} ${f(g)} ${f(b)}${withAlpha ? ` ${f(a)}` : ""} }`;
208
+ case "int":
209
+ return `{ ${i(r)} ${i(g)} ${i(b)}${withAlpha ? ` ${i(a)}` : ""} }`;
210
+ case "float":
211
+ return `{ ${f(r)} ${f(g)} ${f(b)}${withAlpha ? ` ${f(a)}` : ""} }`;
212
+ case "hex":
213
+ return `hex { ${((i(r) << 16) | (i(g) << 8) | i(b)).toString(16).padStart(6, "0")} }`;
214
+ case "hsv": {
215
+ const [h, s, v] = rgbToHsv(r, g, b);
216
+ return `hsv { ${f(h)} ${f(s)} ${f(v)}${withAlpha ? ` ${f(a)}` : ""} }`;
217
+ }
218
+ case "hsv360": {
219
+ const [h, s, v] = rgbToHsv(r, g, b);
220
+ return `hsv360 { ${Math.round(h * 360)} ${Math.round(s * 100)} ${Math.round(v * 100)}${withAlpha ? ` ${f(a)}` : ""} }`;
221
+ }
222
+ }
223
+ }
224
+
225
+ /**
226
+ * One presentation per format, the author's own notation first so a nudge in
227
+ * the picker never silently rewrites `hsv` as `rgb`. The editor cycles through
228
+ * the rest on click; that is the "multiple format" part of the feature.
229
+ */
230
+ export function provideColorPresentations(
231
+ document: TextDocument,
232
+ color: Color,
233
+ range: Range
234
+ ): ColorPresentation[] {
235
+ const site = provideDocumentColors(document).find(
236
+ (s) => s.range.start.line === range.start.line && s.range.start.character === range.start.character
237
+ );
238
+ const own = site?.format ?? "rgb";
239
+ // The source's own alpha, never the picker's: the opacity slider is inert.
240
+ const alpha = site?.alpha ? site.color.alpha : null;
241
+ return [own, ...FORMATS.filter((f) => f !== own)].map((format) => ({
242
+ label: renderColor(color, format, alpha),
243
+ }));
244
+ }