@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
package/media/px-lsp.svg
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
|
|
2
|
+
<rect width="128" height="128" rx="26" fill="#17161A" />
|
|
3
|
+
<g transform="translate(29.559 60.595) scale(0.023192 -0.023192)" fill="#F2EDE3" fill-rule="evenodd">
|
|
4
|
+
<path transform="translate(0 0)" d="M0 0L0 1466L700 1466Q1010 1466 1140 1362Q1270 1258 1270 1040Q1270 822 1140 719Q1010 616 700 616L453 616L453 0ZM453 946L690 946Q900 946 900 1040Q900 1136 690 1136L453 1136Z" />
|
|
5
|
+
<path transform="translate(1470 0)" d="M0 1466L560 1466L750 1169.7L940 1466L1500 1466L1030 733L1500 0L940 0L750 296.3L560 0L0 0L470 733Z" />
|
|
6
|
+
</g>
|
|
7
|
+
<g transform="translate(19.007 100.595) scale(0.023192 -0.023192)" fill="#C8952F" fill-rule="evenodd">
|
|
8
|
+
<path transform="translate(0 0)" d="M0 0L0 1466L453 1466L453 362L1080 362L1080 0Z" />
|
|
9
|
+
<path transform="translate(1280 0)" d="M1077.1 1220.2L1053.8 1257.2L1026.6 1292.4L995.8 1325.4L961.6 1356.0L924.2 1384.0L884.0 1409.1L841.3 1431.1L796.3 1449.9L749.5 1465.3L701.3 1477.2L652.0 1485.4L602.0 1490.0L551.6 1490.9L501.4 1488.0L451.8 1481.5L403.0 1471.3L355.4 1457.5L309.6 1440.3L265.8 1419.8L224.3 1396.1L185.6 1369.5L149.8 1340.1L117.4 1308.2L88.5 1274.0L63.4 1237.8L42.2 1199.9L25.2 1160.6L12.5 1120.2L4.2 1079.0L0.3 1037.3L0.9 995.6L6.0 954.0L15.5 913.0L29.3 872.9L47.4 833.9L69.6 796.4L95.8 760.7L125.7 727.1L159.0 695.8L195.6 667.1L235.1 641.3L277.2 618.4L321.6 598.8L367.9 582.5L415.8 569.6L464.9 560.4L514.7 554.9L565.0 553.0L565.0 903.0L545.9 903.5L526.9 904.9L508.2 907.2L490.0 910.5L472.4 914.6L455.5 919.6L439.4 925.4L424.4 932.0L410.5 939.2L397.8 947.2L386.5 955.7L376.5 964.8L368.1 974.3L361.2 984.2L355.9 994.3L352.3 1004.8L350.3 1015.3L350.1 1025.9L351.6 1036.5L354.8 1046.9L359.6 1057.2L366.1 1067.1L374.1 1076.8L383.7 1085.9L394.7 1094.6L407.0 1102.7L420.6 1110.2L435.4 1116.9L451.1 1122.9L467.8 1128.1L485.3 1132.5L503.3 1136.0L521.9 1138.6L540.8 1140.2L559.9 1141.0L579.1 1140.7L598.1 1139.6L616.9 1137.5L635.2 1134.5L653.0 1130.6L670.1 1125.8L686.4 1120.2L701.7 1113.9L715.9 1106.8L728.9 1099.0L740.7 1090.6L751.0 1081.7L759.9 1072.3ZM565.0 903.0L615.3 901.1L665.1 895.6L714.2 886.4L762.1 873.5L808.4 857.2L852.8 837.6L894.9 814.7L934.4 788.9L971.0 760.2L1004.3 728.9L1034.2 695.3L1060.4 659.6L1082.6 622.1L1100.7 583.1L1114.5 543.0L1124.0 502.0L1129.1 460.4L1129.7 418.7L1125.8 377.0L1117.5 335.8L1104.8 295.4L1087.8 256.1L1066.6 218.2L1041.5 182.0L1012.6 147.8L980.2 115.9L944.4 86.5L905.7 59.9L864.2 36.2L820.4 15.7L774.6 -1.5L727.0 -15.3L678.2 -25.5L628.6 -32.0L578.4 -34.9L528.0 -34.0L478.0 -29.4L428.7 -21.2L380.5 -9.3L333.7 6.1L288.7 24.9L246.0 46.9L205.8 72.0L168.4 100.0L134.2 130.6L103.4 163.6L76.2 198.8L52.9 235.8L370.1 383.7L379.0 374.3L389.3 365.4L401.1 357.0L414.1 349.2L428.3 342.1L443.6 335.8L459.9 330.2L477.0 325.4L494.8 321.5L513.1 318.5L531.9 316.4L550.9 315.3L570.1 315.0L589.2 315.8L608.1 317.4L626.7 320.0L644.7 323.5L662.2 327.9L678.9 333.1L694.6 339.1L709.4 345.8L723.0 353.3L735.3 361.4L746.3 370.1L755.9 379.2L763.9 388.9L770.4 398.8L775.2 409.1L778.4 419.5L779.9 430.1L779.7 440.7L777.7 451.2L774.1 461.7L768.8 471.8L761.9 481.7L753.5 491.2L743.5 500.3L732.2 508.8L719.5 516.8L705.6 524.0L690.6 530.6L674.5 536.4L657.6 541.4L640.0 545.5L621.8 548.8L603.1 551.1L584.1 552.5L565.0 553.0Z" />
|
|
10
|
+
<path transform="translate(2610 0)" d="M0 0L0 1466L700 1466Q1010 1466 1140 1362Q1270 1258 1270 1040Q1270 822 1140 719Q1010 616 700 616L453 616L453 0ZM453 946L690 946Q900 946 900 1040Q900 1136 690 1136L453 1136Z" />
|
|
11
|
+
</g>
|
|
12
|
+
</svg>
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@px-lsp/server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "GPL-3.0-or-later",
|
|
5
|
+
"description": "Language server for Paradox script (Crusader Kings III first), usable over node-ipc or stdio from any LSP client.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"lsp",
|
|
8
|
+
"language-server",
|
|
9
|
+
"paradox",
|
|
10
|
+
"crusader-kings-3",
|
|
11
|
+
"ck3",
|
|
12
|
+
"modding"
|
|
13
|
+
],
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/JDeffner/paradox-modding-toolkit.git",
|
|
17
|
+
"directory": "packages/server"
|
|
18
|
+
},
|
|
19
|
+
"bin": {
|
|
20
|
+
"px-lsp": "./dist/server.js"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"data",
|
|
25
|
+
"media",
|
|
26
|
+
"src",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE",
|
|
29
|
+
"THIRD-PARTY-NOTICES.md"
|
|
30
|
+
],
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"exports": {
|
|
35
|
+
"./package.json": "./package.json",
|
|
36
|
+
"./dds": "./src/dds/index.ts",
|
|
37
|
+
"./parser": "./src/parser/index.ts",
|
|
38
|
+
"./*": "./src/*.ts"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"vscode-languageserver": "^10.1.0",
|
|
42
|
+
"vscode-languageserver-textdocument": "^1.0.12",
|
|
43
|
+
"vscode-uri": "^3.1.0",
|
|
44
|
+
"@px-lsp/protocol": "0.1.0"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"compile": "esbuild src/server.ts --bundle --outfile=dist/server.js --format=cjs --platform=node --target=node18 --banner:js=\"#!/usr/bin/env node\"",
|
|
48
|
+
"watch": "esbuild src/server.ts --bundle --outfile=dist/server.js --format=cjs --platform=node --target=node18 --watch"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolved client capabilities (docs/PROTOCOL.md §Initialization). A client
|
|
3
|
+
* declares what it implements in `initializationOptions.client`; every gate
|
|
4
|
+
* site here asks a semantic question (`hoverHtml()`, `canRunCommand(id)`)
|
|
5
|
+
* instead of "is this the VSCode client", so a client can take the rich hover
|
|
6
|
+
* markup without the commands, or one command without the others.
|
|
7
|
+
*
|
|
8
|
+
* Defaults to fully capable so the pure-render unit tests exercise the rich
|
|
9
|
+
* output; server.ts installs the resolved value at initialize.
|
|
10
|
+
*/
|
|
11
|
+
import { allClientCommandIds, type ParadoxInitOptions } from "@px-lsp/protocol/protocol";
|
|
12
|
+
|
|
13
|
+
export interface ClientCapabilities {
|
|
14
|
+
/** Client renders the sanitized `<span style="color:var(--*)">` hover markup. */
|
|
15
|
+
hoverHtml: boolean;
|
|
16
|
+
/** Command ids the client registers; anything else must not be emitted. */
|
|
17
|
+
commands: ReadonlySet<string>;
|
|
18
|
+
/** Client watches the mod tree itself and pushes paradox/modFileChanged. */
|
|
19
|
+
ownFileWatcher: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Read the capabilities off initializationOptions. The deprecated
|
|
24
|
+
* `clientCommands: true` boolean is an alias for "everything on" (what the
|
|
25
|
+
* VSCode extension sent before the object existed); the object wins when both
|
|
26
|
+
* are present.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveClientCapabilities(init: Partial<ParadoxInitOptions>): ClientCapabilities {
|
|
29
|
+
if (init.client) {
|
|
30
|
+
return {
|
|
31
|
+
hoverHtml: init.client.hoverHtml === true,
|
|
32
|
+
commands: new Set(init.client.commands ?? []),
|
|
33
|
+
ownFileWatcher: init.client.ownFileWatcher === true,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (init.clientCommands === true) {
|
|
37
|
+
return { hoverHtml: true, commands: new Set(allClientCommandIds), ownFileWatcher: true };
|
|
38
|
+
}
|
|
39
|
+
return { hoverHtml: false, commands: new Set(), ownFileWatcher: false };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let caps: ClientCapabilities = resolveClientCapabilities({ clientCommands: true });
|
|
43
|
+
|
|
44
|
+
export function setClientCapabilities(value: ClientCapabilities): void {
|
|
45
|
+
caps = value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function clientCapabilities(): ClientCapabilities {
|
|
49
|
+
return caps;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Whether hover markdown may carry the sanitized colored spans. */
|
|
53
|
+
export function hoverHtml(): boolean {
|
|
54
|
+
return caps.hoverHtml;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Whether the client registers `id`, so a `command:` link or command action reaches it. */
|
|
58
|
+
export function canRunCommand(id: string): boolean {
|
|
59
|
+
return caps.commands.has(id);
|
|
60
|
+
}
|
package/src/coa/coa.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coat-of-arms model for the flag builder (every game whose profile opts in
|
|
3
|
+
* shares the format): the types, color resolution and the writer. Parsing lives in
|
|
4
|
+
* coaParse.ts so this module (which the webview app bundles) pulls in no
|
|
5
|
+
* parser and no game profile.
|
|
6
|
+
*
|
|
7
|
+
* The model mirrors what the game reads, no more:
|
|
8
|
+
*
|
|
9
|
+
* NAME = {
|
|
10
|
+
* pattern = "pattern_solid.tga"
|
|
11
|
+
* color1 = "red" # named | color3 (reference) | rgb { } | hsv { } | hsv360 { }
|
|
12
|
+
* colored_emblem = { # | textured_emblem | sub
|
|
13
|
+
* texture = "ce_circle.dds" # sub: parent = "NAME"
|
|
14
|
+
* mask = { 1 } # colored_emblem only: pattern color slot 1..3
|
|
15
|
+
* color1 = color2
|
|
16
|
+
* instance = { rotation = 0 scale = { 1 1 } position = { 0.5 0.5 } }
|
|
17
|
+
* } # sub instances: offset = { } scale = { }
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* No vscode imports; runs in the extension host, the webview app and tests.
|
|
21
|
+
*/
|
|
22
|
+
export type Rgb = [number, number, number];
|
|
23
|
+
|
|
24
|
+
export type CoaColor =
|
|
25
|
+
| { name: string; kind: "named"; value: string }
|
|
26
|
+
| { name: string; kind: "ref"; value: string }
|
|
27
|
+
| { name: string; kind: "rgb"; value: Rgb }
|
|
28
|
+
| { name: string; kind: "hsv360"; value: [number, number, number] };
|
|
29
|
+
|
|
30
|
+
export interface CoaInstance {
|
|
31
|
+
rotation: number;
|
|
32
|
+
scale: [number, number];
|
|
33
|
+
position: [number, number];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CoaSubInstance {
|
|
37
|
+
offset: [number, number];
|
|
38
|
+
scale: [number, number];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type CoaLayer =
|
|
42
|
+
| { kind: "colored_emblem"; texture: string; mask: number; colors: CoaColor[]; instances: CoaInstance[] }
|
|
43
|
+
| { kind: "textured_emblem"; texture: string; instances: CoaInstance[] }
|
|
44
|
+
| { kind: "sub"; parent: string; instances: CoaSubInstance[] };
|
|
45
|
+
|
|
46
|
+
export interface CoaFlag {
|
|
47
|
+
name: string;
|
|
48
|
+
pattern: string;
|
|
49
|
+
colors: CoaColor[];
|
|
50
|
+
layers: CoaLayer[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The slots a flag or a layer may fill, in the order the game names them. */
|
|
54
|
+
export const COLOR_SLOTS = [
|
|
55
|
+
"color1",
|
|
56
|
+
"color2",
|
|
57
|
+
"color3",
|
|
58
|
+
"color4",
|
|
59
|
+
"color5",
|
|
60
|
+
"color6",
|
|
61
|
+
"color7",
|
|
62
|
+
"color8",
|
|
63
|
+
"color9",
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
/** Placeholder colors a pattern texture is painted with; slot N maps to color N. */
|
|
67
|
+
export const PATTERN_SOURCE_COLORS: Rgb[] = [
|
|
68
|
+
[255, 0, 0],
|
|
69
|
+
[255, 255, 0],
|
|
70
|
+
[255, 255, 255],
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
/** Placeholder colors of a colored emblem (red/green identify the slot, blue is shading). */
|
|
74
|
+
export const EMBLEM_SOURCE_COLORS: Rgb[] = [
|
|
75
|
+
[0, 0, 128],
|
|
76
|
+
[0, 255, 128],
|
|
77
|
+
[255, 0, 128],
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
export const DEFAULT_INSTANCE: CoaInstance = { rotation: 0, scale: [1, 1], position: [0.5, 0.5] };
|
|
81
|
+
export const DEFAULT_SUB_INSTANCE: CoaSubInstance = { offset: [0, 0], scale: [1, 1] };
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Colors
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
export function hsv360ToRgb(h: number, s: number, v: number): Rgb {
|
|
88
|
+
const hh = (((h % 360) + 360) % 360) / 60;
|
|
89
|
+
const ss = Math.min(1, Math.max(0, s / 100));
|
|
90
|
+
const vv = Math.min(1, Math.max(0, v / 100));
|
|
91
|
+
const c = vv * ss;
|
|
92
|
+
const x = c * (1 - Math.abs((hh % 2) - 1));
|
|
93
|
+
const m = vv - c;
|
|
94
|
+
const sector = Math.floor(hh);
|
|
95
|
+
const [r, g, b] =
|
|
96
|
+
sector === 0
|
|
97
|
+
? [c, x, 0]
|
|
98
|
+
: sector === 1
|
|
99
|
+
? [x, c, 0]
|
|
100
|
+
: sector === 2
|
|
101
|
+
? [0, c, x]
|
|
102
|
+
: sector === 3
|
|
103
|
+
? [0, x, c]
|
|
104
|
+
: sector === 4
|
|
105
|
+
? [x, 0, c]
|
|
106
|
+
: [c, 0, x];
|
|
107
|
+
return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255)];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The rgb a color resolves to: a literal, a named color from the game's
|
|
112
|
+
* tables, or a reference (`color1 = color3`) into the flag's own colors. A
|
|
113
|
+
* reference to a reference is followed once (the game does not chain them).
|
|
114
|
+
*/
|
|
115
|
+
export function colorToRgb(color: CoaColor, named: Record<string, Rgb>, flagColors: CoaColor[]): Rgb | null {
|
|
116
|
+
switch (color.kind) {
|
|
117
|
+
case "rgb":
|
|
118
|
+
return color.value;
|
|
119
|
+
case "hsv360":
|
|
120
|
+
return hsv360ToRgb(color.value[0], color.value[1], color.value[2]);
|
|
121
|
+
case "named":
|
|
122
|
+
return named[color.value] ?? null;
|
|
123
|
+
case "ref": {
|
|
124
|
+
const base = flagColors.find((c) => c.name === color.value);
|
|
125
|
+
if (!base || base.kind === "ref") return null;
|
|
126
|
+
return colorToRgb(base, named, []);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Writing
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
const fmt = (n: number): string =>
|
|
136
|
+
Number.isInteger(n) ? String(n) : n.toFixed(3).replace(/0+$/, "").replace(/\.$/, "");
|
|
137
|
+
|
|
138
|
+
function writeColor(c: CoaColor): string {
|
|
139
|
+
switch (c.kind) {
|
|
140
|
+
case "named":
|
|
141
|
+
return `${c.name} = "${c.value}"`;
|
|
142
|
+
case "ref":
|
|
143
|
+
return `${c.name} = ${c.value}`;
|
|
144
|
+
case "rgb":
|
|
145
|
+
return `${c.name} = rgb { ${c.value.join(" ")} }`;
|
|
146
|
+
case "hsv360":
|
|
147
|
+
return `${c.name} = hsv360 { ${c.value.map((v) => Math.round(v)).join(" ")} }`;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The flag as the game reads it: tabs, one attribute per line, no trailing newline. */
|
|
152
|
+
export function writeFlag(flag: CoaFlag): string {
|
|
153
|
+
const lines: string[] = [`${flag.name || "NONE"} = {`];
|
|
154
|
+
if (flag.pattern) lines.push(`\tpattern = "${flag.pattern}"`);
|
|
155
|
+
for (const c of flag.colors) lines.push(`\t${writeColor(c)}`);
|
|
156
|
+
for (const layer of flag.layers) {
|
|
157
|
+
lines.push("");
|
|
158
|
+
lines.push(`\t${layer.kind} = {`);
|
|
159
|
+
if (layer.kind === "sub") {
|
|
160
|
+
lines.push(`\t\tparent = "${layer.parent}"`);
|
|
161
|
+
for (const i of layer.instances) {
|
|
162
|
+
lines.push(
|
|
163
|
+
`\t\tinstance = { offset = { ${fmt(i.offset[0])} ${fmt(i.offset[1])} } scale = { ${fmt(i.scale[0])} ${fmt(i.scale[1])} } }`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
lines.push(`\t\ttexture = "${layer.texture}"`);
|
|
168
|
+
if (layer.kind === "colored_emblem") {
|
|
169
|
+
if (layer.mask > 0) lines.push(`\t\tmask = { ${layer.mask} }`);
|
|
170
|
+
for (const c of layer.colors) lines.push(`\t\t${writeColor(c)}`);
|
|
171
|
+
}
|
|
172
|
+
for (const i of layer.instances) {
|
|
173
|
+
const parts = [];
|
|
174
|
+
if (i.rotation !== 0) parts.push(`rotation = ${fmt(i.rotation)}`);
|
|
175
|
+
parts.push(`scale = { ${fmt(i.scale[0])} ${fmt(i.scale[1])} }`);
|
|
176
|
+
parts.push(`position = { ${fmt(i.position[0])} ${fmt(i.position[1])} }`);
|
|
177
|
+
lines.push(`\t\tinstance = { ${parts.join(" ")} }`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
lines.push("\t}");
|
|
181
|
+
}
|
|
182
|
+
lines.push("}");
|
|
183
|
+
return lines.join("\n");
|
|
184
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coat-of-arms parsing: a `common/coat_of_arms/coat_of_arms/*.txt` file into
|
|
3
|
+
* CoaFlags, `common/named_colors/*.txt` into rgb, and the in-place replace
|
|
4
|
+
* of one flag in a file. Numbers may be `@vars` defined at the top of the
|
|
5
|
+
* same file, including `@[ expr ]` arithmetic; both are resolved here so the
|
|
6
|
+
* renderer only sees numbers. `template = { }` blocks are skipped like the
|
|
7
|
+
* game's own tool does.
|
|
8
|
+
*
|
|
9
|
+
* Host and test side only: the tolerant parser brings the game profiles with
|
|
10
|
+
* it, which the webview app must not bundle.
|
|
11
|
+
*/
|
|
12
|
+
import { parseScript } from "../parser/parser";
|
|
13
|
+
import type { BlockNode, Statement, ValueNode } from "../parser/cst";
|
|
14
|
+
import {
|
|
15
|
+
COLOR_SLOTS,
|
|
16
|
+
colorToRgb,
|
|
17
|
+
DEFAULT_INSTANCE,
|
|
18
|
+
DEFAULT_SUB_INSTANCE,
|
|
19
|
+
type CoaColor,
|
|
20
|
+
type CoaFlag,
|
|
21
|
+
type CoaInstance,
|
|
22
|
+
type CoaLayer,
|
|
23
|
+
type CoaSubInstance,
|
|
24
|
+
type Rgb,
|
|
25
|
+
} from "./coa";
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// @variables
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
type Vars = Map<string, number>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Evaluate `@[ expr ]` / `@name` / a plain number. Only `+ - * /`, parentheses
|
|
35
|
+
* and earlier `@vars` are understood, which is all the coa files use; anything
|
|
36
|
+
* else is NaN and the caller keeps its default.
|
|
37
|
+
*/
|
|
38
|
+
export function resolveNumber(text: string, vars: Vars): number {
|
|
39
|
+
const t = text.trim();
|
|
40
|
+
if (t.startsWith("@[") && t.endsWith("]")) return evalExpr(t.slice(2, -1), vars);
|
|
41
|
+
if (t.startsWith("@")) return vars.get(t) ?? NaN;
|
|
42
|
+
return Number(t);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function evalExpr(src: string, vars: Vars): number {
|
|
46
|
+
// Tokens: numbers, @names, operators, parens.
|
|
47
|
+
const tokens = src.match(/\d+(?:\.\d+)?|@[\w.]+|[-+*/()]/g);
|
|
48
|
+
if (!tokens || tokens.join("") !== src.replace(/\s+/g, "")) return NaN;
|
|
49
|
+
let i = 0;
|
|
50
|
+
const primary = (): number => {
|
|
51
|
+
const tok = tokens[i++];
|
|
52
|
+
if (tok === undefined) return NaN;
|
|
53
|
+
if (tok === "(") {
|
|
54
|
+
const v = sum();
|
|
55
|
+
return tokens[i++] === ")" ? v : NaN;
|
|
56
|
+
}
|
|
57
|
+
if (tok === "-") return -primary();
|
|
58
|
+
if (tok.startsWith("@")) return vars.get(tok) ?? NaN;
|
|
59
|
+
return Number(tok);
|
|
60
|
+
};
|
|
61
|
+
const product = (): number => {
|
|
62
|
+
let v = primary();
|
|
63
|
+
while (tokens[i] === "*" || tokens[i] === "/") {
|
|
64
|
+
const op = tokens[i++];
|
|
65
|
+
const r = primary();
|
|
66
|
+
v = op === "*" ? v * r : v / r;
|
|
67
|
+
}
|
|
68
|
+
return v;
|
|
69
|
+
};
|
|
70
|
+
const sum = (): number => {
|
|
71
|
+
let v = product();
|
|
72
|
+
while (tokens[i] === "+" || tokens[i] === "-") {
|
|
73
|
+
const op = tokens[i++];
|
|
74
|
+
const r = product();
|
|
75
|
+
v = op === "+" ? v + r : v - r;
|
|
76
|
+
}
|
|
77
|
+
return v;
|
|
78
|
+
};
|
|
79
|
+
const v = sum();
|
|
80
|
+
return i === tokens.length ? v : NaN;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Parsing
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
function scalarText(v: ValueNode | null): string | null {
|
|
88
|
+
return v && v.kind === "scalar" ? v.text : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function numbers(block: BlockNode, vars: Vars): number[] {
|
|
92
|
+
const out: number[] = [];
|
|
93
|
+
for (const s of block.statements) {
|
|
94
|
+
if (s.kind === "value" && s.value.kind === "scalar") out.push(resolveNumber(s.value.text, vars));
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function pair(block: BlockNode, vars: Vars, fallback: [number, number]): [number, number] {
|
|
100
|
+
const n = numbers(block, vars);
|
|
101
|
+
if (n.length !== 2 || n.some((x) => !Number.isFinite(x))) return fallback;
|
|
102
|
+
return [n[0], n[1]];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseColor(name: string, value: ValueNode, vars: Vars): CoaColor | null {
|
|
106
|
+
if (value.kind === "scalar") {
|
|
107
|
+
if (!value.quoted && COLOR_SLOTS.includes(value.text)) return { name, kind: "ref", value: value.text };
|
|
108
|
+
return { name, kind: "named", value: value.text };
|
|
109
|
+
}
|
|
110
|
+
const tag = value.kind === "tagged-block" ? value.tag.text : "";
|
|
111
|
+
const block = value.kind === "tagged-block" ? value.block : value;
|
|
112
|
+
const n = numbers(block, vars);
|
|
113
|
+
if (n.length !== 3 || n.some((x) => !Number.isFinite(x))) return null;
|
|
114
|
+
if (tag === "hsv") return { name, kind: "hsv360", value: [n[0] * 360, n[1] * 100, n[2] * 100] };
|
|
115
|
+
if (tag === "hsv360") return { name, kind: "hsv360", value: [n[0], n[1], n[2]] };
|
|
116
|
+
// rgb, or untyped: floats 0..1 or bytes 0..255
|
|
117
|
+
const rgb = n.every((x) => x <= 1) ? n.map((x) => Math.round(x * 255)) : n.map((x) => Math.round(x));
|
|
118
|
+
return { name, kind: "rgb", value: [rgb[0], rgb[1], rgb[2]] };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function parseColors(statements: Statement[], vars: Vars): CoaColor[] {
|
|
122
|
+
const out: CoaColor[] = [];
|
|
123
|
+
for (const s of statements) {
|
|
124
|
+
if (s.kind !== "assignment" || !s.value || !COLOR_SLOTS.includes(s.key.text)) continue;
|
|
125
|
+
const c = parseColor(s.key.text, s.value, vars);
|
|
126
|
+
if (c) out.push(c);
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function parseInstances(statements: Statement[], vars: Vars): CoaInstance[] {
|
|
132
|
+
const out: CoaInstance[] = [];
|
|
133
|
+
for (const s of statements) {
|
|
134
|
+
if (s.kind !== "assignment" || s.key.text !== "instance" || s.value?.kind !== "block") continue;
|
|
135
|
+
const inst: CoaInstance = { ...DEFAULT_INSTANCE };
|
|
136
|
+
for (const a of s.value.statements) {
|
|
137
|
+
if (a.kind !== "assignment" || !a.value) continue;
|
|
138
|
+
if (a.key.text === "rotation" && a.value.kind === "scalar") {
|
|
139
|
+
const r = resolveNumber(a.value.text, vars);
|
|
140
|
+
if (Number.isFinite(r)) inst.rotation = r;
|
|
141
|
+
} else if (a.key.text === "scale" && a.value.kind === "block") inst.scale = pair(a.value, vars, [1, 1]);
|
|
142
|
+
else if (a.key.text === "position" && a.value.kind === "block")
|
|
143
|
+
inst.position = pair(a.value, vars, [0.5, 0.5]);
|
|
144
|
+
}
|
|
145
|
+
out.push(inst);
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function parseSubInstances(statements: Statement[], vars: Vars): CoaSubInstance[] {
|
|
151
|
+
const out: CoaSubInstance[] = [];
|
|
152
|
+
for (const s of statements) {
|
|
153
|
+
if (s.kind !== "assignment" || s.key.text !== "instance" || s.value?.kind !== "block") continue;
|
|
154
|
+
const inst: CoaSubInstance = { ...DEFAULT_SUB_INSTANCE };
|
|
155
|
+
for (const a of s.value.statements) {
|
|
156
|
+
if (a.kind !== "assignment" || a.value?.kind !== "block") continue;
|
|
157
|
+
if (a.key.text === "scale") inst.scale = pair(a.value, vars, [1, 1]);
|
|
158
|
+
else if (a.key.text === "offset") inst.offset = pair(a.value, vars, [0, 0]);
|
|
159
|
+
}
|
|
160
|
+
out.push(inst);
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function parseLayer(kind: string, block: BlockNode, vars: Vars): CoaLayer | null {
|
|
166
|
+
let texture = "";
|
|
167
|
+
let parent = "";
|
|
168
|
+
let mask = 0;
|
|
169
|
+
for (const s of block.statements) {
|
|
170
|
+
if (s.kind !== "assignment" || !s.value) continue;
|
|
171
|
+
const text = scalarText(s.value);
|
|
172
|
+
if (s.key.text === "texture" && text !== null) texture = text;
|
|
173
|
+
else if (s.key.text === "parent" && text !== null) parent = text;
|
|
174
|
+
else if (s.key.text === "mask" && s.value.kind === "block") {
|
|
175
|
+
const n = numbers(s.value, vars);
|
|
176
|
+
if (n.length === 1 && Number.isFinite(n[0])) mask = n[0];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
switch (kind) {
|
|
180
|
+
case "colored_emblem":
|
|
181
|
+
return {
|
|
182
|
+
kind,
|
|
183
|
+
texture,
|
|
184
|
+
mask,
|
|
185
|
+
colors: parseColors(block.statements, vars),
|
|
186
|
+
instances: parseInstances(block.statements, vars),
|
|
187
|
+
};
|
|
188
|
+
case "textured_emblem":
|
|
189
|
+
return { kind, texture, instances: parseInstances(block.statements, vars) };
|
|
190
|
+
case "sub":
|
|
191
|
+
return { kind, parent, instances: parseSubInstances(block.statements, vars) };
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function parseFlag(name: string, block: BlockNode, vars: Vars = new Map()): CoaFlag {
|
|
197
|
+
const flag: CoaFlag = { name, pattern: "", colors: parseColors(block.statements, vars), layers: [] };
|
|
198
|
+
for (const s of block.statements) {
|
|
199
|
+
if (s.kind !== "assignment" || !s.value) continue;
|
|
200
|
+
if (s.key.text === "pattern") flag.pattern = scalarText(s.value) ?? "";
|
|
201
|
+
else if (s.value.kind === "block") {
|
|
202
|
+
const layer = parseLayer(s.key.text, s.value, vars);
|
|
203
|
+
if (layer) flag.layers.push(layer);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return flag;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Every flag in one coa file. `template` blocks are skipped (they are not
|
|
211
|
+
* flags) and so is anything that is not `name = { }`.
|
|
212
|
+
*/
|
|
213
|
+
export function parseCoaFile(text: string): CoaFlag[] {
|
|
214
|
+
const { root } = parseScript(text);
|
|
215
|
+
const vars: Vars = new Map();
|
|
216
|
+
const flags: CoaFlag[] = [];
|
|
217
|
+
for (const s of root.statements) {
|
|
218
|
+
if (s.kind !== "assignment" || !s.value) continue;
|
|
219
|
+
if (s.key.text.startsWith("@")) {
|
|
220
|
+
const text = scalarText(s.value);
|
|
221
|
+
if (text !== null) {
|
|
222
|
+
const n = resolveNumber(text, vars);
|
|
223
|
+
if (Number.isFinite(n)) vars.set(s.key.text, n);
|
|
224
|
+
}
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (s.key.text === "template" || s.value.kind !== "block") continue;
|
|
228
|
+
flags.push(parseFlag(s.key.text, s.value, vars));
|
|
229
|
+
}
|
|
230
|
+
return flags;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** `colors = { name = hsv360 { } ... }` entries of a named_colors file, as rgb bytes. */
|
|
234
|
+
export function parseNamedColors(text: string): Record<string, Rgb> {
|
|
235
|
+
const { root } = parseScript(text);
|
|
236
|
+
const out: Record<string, Rgb> = {};
|
|
237
|
+
for (const s of root.statements) {
|
|
238
|
+
if (s.kind !== "assignment" || s.key.text !== "colors" || s.value?.kind !== "block") continue;
|
|
239
|
+
for (const c of s.value.statements) {
|
|
240
|
+
if (c.kind !== "assignment" || !c.value || c.value.kind === "scalar") continue;
|
|
241
|
+
const color = parseColor(c.key.text, c.value, new Map());
|
|
242
|
+
if (color) {
|
|
243
|
+
const rgb = colorToRgb(color, {}, []);
|
|
244
|
+
if (rgb) out[c.key.text] = rgb;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return out;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Replace the top-level `name = { }` block in a coa file with `script`, or
|
|
253
|
+
* append the script when the file has no such block. The rest of the file is
|
|
254
|
+
* untouched (line endings, comments, other flags).
|
|
255
|
+
*/
|
|
256
|
+
export function upsertFlagInFile(text: string, name: string, script: string): string {
|
|
257
|
+
const { root } = parseScript(text);
|
|
258
|
+
const eol = text.includes("\r\n") ? "\r\n" : "\n";
|
|
259
|
+
const body = script.replace(/\n/g, eol);
|
|
260
|
+
for (const s of root.statements) {
|
|
261
|
+
if (s.kind === "assignment" && s.key.text === name && s.value?.kind === "block") {
|
|
262
|
+
return text.slice(0, s.range.start) + body + text.slice(s.range.end);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const sep = text.length === 0 || text.endsWith("\n") ? "" : eol;
|
|
266
|
+
return text + sep + eol + body + eol;
|
|
267
|
+
}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Block-context detection: from a cursor offset, walk the CST path upward to
|
|
3
|
+
* the nearest block keyword that decides trigger vs effect grammar.
|
|
4
|
+
*
|
|
5
|
+
* This is deliberately not scope-chain inference (that is the scope engine's
|
|
6
|
+
* job, Phase 3); it only answers "am I inside a trigger block, an effect
|
|
7
|
+
* block, or don't know". Built on the tolerant parser — the v0.3 regex walker
|
|
8
|
+
* this replaces re-scanned the whole document per keystroke.
|
|
9
|
+
*
|
|
10
|
+
* No `vscode` imports here: this module is unit-tested in plain Node.
|
|
11
|
+
*/
|
|
12
|
+
import { classifyKeyword } from "./contextKeywords";
|
|
13
|
+
import { nodeAtOffset, parseScript, type BlockNode, type ParseResult, type Statement } from "./parser";
|
|
14
|
+
|
|
15
|
+
export type BlockContext = "trigger" | "effect" | "value" | "unknown";
|
|
16
|
+
|
|
17
|
+
export interface ContextResult {
|
|
18
|
+
context: BlockContext;
|
|
19
|
+
/** The block keyword that decided the context, or null at top level. */
|
|
20
|
+
keyword: string | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function childBlock(stmt: Statement): BlockNode | null {
|
|
24
|
+
const v = stmt.value;
|
|
25
|
+
if (!v) return null;
|
|
26
|
+
if (v.kind === "block") return v;
|
|
27
|
+
if (v.kind === "tagged-block") return v.block;
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The stack of enclosing block keywords at `offset` (outermost first, "<anon>" for bare blocks). */
|
|
32
|
+
export function blockStackFromParse(parse: ParseResult, offset: number): string[] {
|
|
33
|
+
return blockPathFromParse(parse, offset).map((stmt) =>
|
|
34
|
+
stmt && stmt.kind === "assignment" ? stmt.key.text : "<anon>"
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** As blockStackFromParse, but the enclosing statements themselves (null for bare blocks). */
|
|
39
|
+
export function blockPathFromParse(parse: ParseResult, offset: number): Array<Statement | null> {
|
|
40
|
+
const hit = nodeAtOffset(parse.root, offset);
|
|
41
|
+
if (!hit) return [];
|
|
42
|
+
const path: Array<Statement | null> = [];
|
|
43
|
+
for (const stmt of hit.path) {
|
|
44
|
+
const block = childBlock(stmt);
|
|
45
|
+
// Only statements whose *block* encloses the offset contribute to the stack;
|
|
46
|
+
// the leaf statement under the cursor (a half-typed word) does not.
|
|
47
|
+
if (!block) continue;
|
|
48
|
+
if (offset <= block.openBrace) continue;
|
|
49
|
+
if (block.closeBrace !== null && offset > block.closeBrace) continue;
|
|
50
|
+
path.push(stmt.kind === "assignment" ? stmt : null);
|
|
51
|
+
}
|
|
52
|
+
return path;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Detect the completion context at `offset` from a cached parse. Walks the
|
|
57
|
+
* block stack from the innermost block outward: the first keyword classified
|
|
58
|
+
* as trigger or effect wins; transparent keywords (if/else/AND/OR/scope
|
|
59
|
+
* changes) are skipped; an unrecognized keyword yields "unknown" (never hide
|
|
60
|
+
* results when unsure).
|
|
61
|
+
*/
|
|
62
|
+
export function detectContextFromParse(parse: ParseResult, offset: number): ContextResult {
|
|
63
|
+
const stack = blockStackFromParse(parse, offset);
|
|
64
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
65
|
+
const keyword = stack[i];
|
|
66
|
+
if (keyword === "<anon>") continue;
|
|
67
|
+
const cls = classifyKeyword(keyword);
|
|
68
|
+
if (cls === "trigger" || cls === "effect" || cls === "value") return { context: cls, keyword };
|
|
69
|
+
if (cls === "transparent") continue;
|
|
70
|
+
return { context: "unknown", keyword };
|
|
71
|
+
}
|
|
72
|
+
return { context: "unknown", keyword: null };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Convenience for tests and one-off callers: parse + detect in one step. */
|
|
76
|
+
export function detectContext(text: string, offset: number): ContextResult {
|
|
77
|
+
return detectContextFromParse(parseScript(text), offset);
|
|
78
|
+
}
|