@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,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
|
+
}
|