@player-lang/json-language-service 0.0.2-next.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 (59) hide show
  1. package/dist/cjs/index.cjs +2314 -0
  2. package/dist/cjs/index.cjs.map +1 -0
  3. package/dist/index.legacy-esm.js +2249 -0
  4. package/dist/index.mjs +2249 -0
  5. package/dist/index.mjs.map +1 -0
  6. package/package.json +40 -0
  7. package/src/__tests__/__snapshots__/service.test.ts.snap +213 -0
  8. package/src/__tests__/service.test.ts +298 -0
  9. package/src/constants.ts +38 -0
  10. package/src/index.ts +490 -0
  11. package/src/parser/__tests__/parse.test.ts +18 -0
  12. package/src/parser/document.ts +456 -0
  13. package/src/parser/edits.ts +31 -0
  14. package/src/parser/index.ts +38 -0
  15. package/src/parser/jsonParseErrors.ts +69 -0
  16. package/src/parser/types.ts +314 -0
  17. package/src/parser/utils.ts +94 -0
  18. package/src/plugins/__tests__/asset-wrapper-array-plugin.test.ts +112 -0
  19. package/src/plugins/__tests__/binding-schema-plugin.test.ts +62 -0
  20. package/src/plugins/__tests__/duplicate-id-plugin.test.ts +195 -0
  21. package/src/plugins/__tests__/missing-asset-wrapper-plugin.test.ts +190 -0
  22. package/src/plugins/__tests__/nav-state-plugin.test.ts +136 -0
  23. package/src/plugins/__tests__/view-node-plugin.test.ts +154 -0
  24. package/src/plugins/asset-wrapper-array-plugin.ts +123 -0
  25. package/src/plugins/binding-schema-plugin.ts +289 -0
  26. package/src/plugins/duplicate-id-plugin.ts +158 -0
  27. package/src/plugins/missing-asset-wrapper-plugin.ts +96 -0
  28. package/src/plugins/nav-state-plugin.ts +139 -0
  29. package/src/plugins/view-node-plugin.ts +225 -0
  30. package/src/plugins/xlr-plugin.ts +371 -0
  31. package/src/types.ts +119 -0
  32. package/src/utils.ts +143 -0
  33. package/src/xlr/__tests__/__snapshots__/transform.test.ts.snap +390 -0
  34. package/src/xlr/__tests__/transform.test.ts +108 -0
  35. package/src/xlr/index.ts +3 -0
  36. package/src/xlr/registry.ts +99 -0
  37. package/src/xlr/service.ts +190 -0
  38. package/src/xlr/transforms.ts +169 -0
  39. package/types/constants.d.ts +7 -0
  40. package/types/index.d.ts +69 -0
  41. package/types/parser/document.d.ts +25 -0
  42. package/types/parser/edits.d.ts +10 -0
  43. package/types/parser/index.d.ts +16 -0
  44. package/types/parser/jsonParseErrors.d.ts +27 -0
  45. package/types/parser/types.d.ts +188 -0
  46. package/types/parser/utils.d.ts +26 -0
  47. package/types/plugins/asset-wrapper-array-plugin.d.ts +9 -0
  48. package/types/plugins/binding-schema-plugin.d.ts +15 -0
  49. package/types/plugins/duplicate-id-plugin.d.ts +7 -0
  50. package/types/plugins/missing-asset-wrapper-plugin.d.ts +9 -0
  51. package/types/plugins/nav-state-plugin.d.ts +9 -0
  52. package/types/plugins/view-node-plugin.d.ts +9 -0
  53. package/types/plugins/xlr-plugin.d.ts +7 -0
  54. package/types/types.d.ts +81 -0
  55. package/types/utils.d.ts +24 -0
  56. package/types/xlr/index.d.ts +4 -0
  57. package/types/xlr/registry.d.ts +17 -0
  58. package/types/xlr/service.d.ts +22 -0
  59. package/types/xlr/transforms.d.ts +18 -0
@@ -0,0 +1,371 @@
1
+ import type { NodeType } from "@xlr-lib/xlr";
2
+ import type { ValidationMessage } from "@xlr-lib/xlr-sdk";
3
+ import { ValidationSeverity, XLRSDK } from "@xlr-lib/xlr-sdk";
4
+ import type { CompletionItem } from "vscode-languageserver-types";
5
+ import {
6
+ CompletionItemKind,
7
+ DiagnosticSeverity,
8
+ MarkupKind,
9
+ } from "vscode-languageserver-types";
10
+ import type { Node } from "jsonc-parser";
11
+ import type {
12
+ ASTVisitor,
13
+ PlayerLanguageService,
14
+ PlayerLanguageServicePlugin,
15
+ ValidationContext,
16
+ } from "..";
17
+ import { mapFlowStateToType } from "../utils";
18
+ import type { ASTNode, ObjectASTNode } from "../parser";
19
+ import type { EnhancedDocumentContextWithPosition } from "../types";
20
+
21
+ function isError(issue: ValidationMessage): boolean {
22
+ return issue.severity === DiagnosticSeverity.Error;
23
+ }
24
+
25
+ /** BFS search to find a JSONC node in children of some AST Node */
26
+ const findErrorNode = (rootNode: ASTNode, nodeToFind: Node): ASTNode => {
27
+ const children: Array<ASTNode> = [rootNode];
28
+
29
+ while (children.length > 0) {
30
+ const child = children.pop() as ASTNode;
31
+ if (child.jsonNode === nodeToFind) {
32
+ return child;
33
+ }
34
+
35
+ if (child.children) {
36
+ children.push(...child.children);
37
+ }
38
+ }
39
+
40
+ // if the node can't be found return the original
41
+ return rootNode;
42
+ };
43
+
44
+ /**
45
+ * Translates an SDK severity level to an LSP severity level
46
+ * Relies on both levels having the values associated to the underlying levels
47
+ */
48
+ const translateSeverity = (
49
+ severity: ValidationSeverity,
50
+ ): DiagnosticSeverity => {
51
+ return severity as DiagnosticSeverity;
52
+ };
53
+
54
+ /**
55
+ * Create Validation walkers
56
+ */
57
+ function createValidationVisitor(
58
+ ctx: ValidationContext,
59
+ sdk: XLRSDK,
60
+ ): ASTVisitor {
61
+ const nodesWithErrors = new Set<Node>();
62
+
63
+ // TODO cache nodes for LSP usecase. Need to examine which nodes can be cached and how to cache complex objects
64
+ return {
65
+ AssetNode: (assetNode) => {
66
+ // Try and find custom asset but fall back on just validating if its an valid asset if its not found.
67
+ let expectedType = assetNode.assetType?.valueNode?.value as string;
68
+
69
+ if (!sdk.hasType(expectedType)) {
70
+ ctx.addViolation({
71
+ node: assetNode,
72
+ message: `Warning - Asset Type ${assetNode.assetType?.valueNode?.value} was not loaded into Validator definitions`,
73
+ severity: DiagnosticSeverity.Error,
74
+ });
75
+ expectedType = "Asset";
76
+ }
77
+
78
+ const validationIssues = sdk.validateByName(
79
+ expectedType,
80
+ assetNode.jsonNode,
81
+ );
82
+ validationIssues.forEach((issue) => {
83
+ if (!(nodesWithErrors.has(issue.node) && isError(issue))) {
84
+ ctx.addViolation({
85
+ node: findErrorNode(assetNode, issue.node),
86
+ message: isError(issue)
87
+ ? `Asset Validation Error - ${issue.type}: ${issue.message}`
88
+ : issue.message,
89
+ severity: translateSeverity(issue.severity),
90
+ });
91
+ if (isError(issue)) {
92
+ nodesWithErrors.add(issue.node);
93
+ }
94
+ }
95
+ });
96
+ },
97
+ ViewNode: (viewNode) => {
98
+ let expectedType = viewNode.viewType?.valueNode?.value as string;
99
+
100
+ if (!sdk.hasType(expectedType)) {
101
+ ctx.addViolation({
102
+ node: viewNode,
103
+ message: `Warning - View Type ${viewNode.viewType?.valueNode?.value} was not loaded into Validator definitions`,
104
+ severity: DiagnosticSeverity.Error,
105
+ });
106
+ expectedType = "View";
107
+ }
108
+
109
+ const validationIssues = sdk.validateByName(
110
+ expectedType,
111
+ viewNode.jsonNode,
112
+ );
113
+ validationIssues.forEach((issue) => {
114
+ if (!(nodesWithErrors.has(issue.node) && isError(issue))) {
115
+ ctx.addViolation({
116
+ node: findErrorNode(viewNode, issue.node),
117
+ message: isError(issue)
118
+ ? `View Validation Error - ${issue.type}: ${issue.message}`
119
+ : issue.message,
120
+ severity: translateSeverity(issue.severity),
121
+ });
122
+ if (isError(issue)) {
123
+ nodesWithErrors.add(issue.node);
124
+ }
125
+ }
126
+ });
127
+ },
128
+ ContentNode: (contentNode) => {
129
+ const flowType = sdk.getType("Flow");
130
+
131
+ if (!flowType) {
132
+ throw new Error(
133
+ "Flow is not a registered type, can't validate content. Did you load a version of the base Player types?",
134
+ );
135
+ }
136
+
137
+ /**
138
+ * Since the `views.validation` property contains a runtime conditional that is validated anyways when we get to
139
+ * the ViewNode, replace the reference to `View` with a basic `Asset` instead of trying to solve the generic
140
+ * for every view present
141
+ */
142
+
143
+ const assetType = sdk.getType("Asset");
144
+ if (!assetType) {
145
+ throw new Error(
146
+ "Asset is not a registered type, can't validate content. Did you load a version of the base Player types?",
147
+ );
148
+ }
149
+
150
+ if (
151
+ flowType.type === "object" &&
152
+ flowType.properties.views?.node.type === "array"
153
+ ) {
154
+ flowType.properties.views.node.elementType = assetType;
155
+ }
156
+
157
+ const validationIssues = sdk.validateByType(
158
+ flowType,
159
+ contentNode.jsonNode,
160
+ );
161
+
162
+ validationIssues.forEach((issue) => {
163
+ if (!nodesWithErrors.has(issue.node) || issue.type === "missing") {
164
+ nodesWithErrors.add(issue.node);
165
+ ctx.addViolation({
166
+ node: findErrorNode(contentNode, issue.node),
167
+ message: `Content Validation Error - ${issue.type}: ${issue.message}`,
168
+ severity: DiagnosticSeverity.Error,
169
+ });
170
+ }
171
+ });
172
+ },
173
+ NavigationNode: (navigationNode) => {
174
+ const expectedType = "Navigation";
175
+ const validationIssues = sdk.validateByName(
176
+ expectedType,
177
+ navigationNode.jsonNode,
178
+ );
179
+ validationIssues.forEach((issue) => {
180
+ if (!nodesWithErrors.has(issue.node) || issue.type === "missing") {
181
+ nodesWithErrors.add(issue.node);
182
+ ctx.addViolation({
183
+ node: findErrorNode(navigationNode, issue.node),
184
+ message: `Navigation Validation Error - ${issue.type}: ${issue.message}`,
185
+ severity: DiagnosticSeverity.Error,
186
+ });
187
+ }
188
+ });
189
+ },
190
+ FlowNode: (flowNode) => {
191
+ const expectedType = "NavigationFlow";
192
+ const validationIssues = sdk.validateByName(
193
+ expectedType,
194
+ flowNode.jsonNode,
195
+ );
196
+ validationIssues.forEach((issue) => {
197
+ if (!nodesWithErrors.has(issue.node) || issue.type === "missing") {
198
+ nodesWithErrors.add(issue.node);
199
+ ctx.addViolation({
200
+ node: findErrorNode(flowNode, issue.node),
201
+ message: `Navigation Flow Validation Error - ${issue.type}: ${issue.message}`,
202
+ severity: DiagnosticSeverity.Error,
203
+ });
204
+ }
205
+ });
206
+ },
207
+ FlowStateNode: (flowStateNode) => {
208
+ const flowxlr = mapFlowStateToType(
209
+ flowStateNode.stateType?.valueNode?.value,
210
+ );
211
+
212
+ if (flowxlr) {
213
+ const validationIssues = sdk.validateByName(
214
+ flowxlr,
215
+ flowStateNode.jsonNode,
216
+ );
217
+ validationIssues.forEach((issue) => {
218
+ if (!nodesWithErrors.has(issue.node) || issue.type === "missing") {
219
+ nodesWithErrors.add(issue.node);
220
+ ctx.addViolation({
221
+ node: findErrorNode(flowStateNode, issue.node),
222
+ message: `Navigation Node Validation Error - ${issue.type}: ${issue.message}`,
223
+ severity: DiagnosticSeverity.Error,
224
+ });
225
+ }
226
+ });
227
+ } else {
228
+ ctx.addViolation({
229
+ node: flowStateNode,
230
+ message:
231
+ "Unknown Flow Type, valid options are: VIEW, END, ACTION, EXTERNAL, FLOW",
232
+ severity: DiagnosticSeverity.Error,
233
+ });
234
+ }
235
+ },
236
+ };
237
+ }
238
+
239
+ /** Gets object completions */
240
+ function getObjectCompletions(
241
+ authoredNode: ASTNode,
242
+ potentialTypes: Array<NodeType>,
243
+ ) {
244
+ const completions: Array<CompletionItem> = [];
245
+
246
+ const presentKeys = new Set();
247
+ if ((authoredNode as ObjectASTNode).properties) {
248
+ (authoredNode as ObjectASTNode).properties.forEach((propertyNode) =>
249
+ presentKeys.add(propertyNode.keyNode.value),
250
+ );
251
+ }
252
+
253
+ potentialTypes.forEach((node) => {
254
+ if (node.type === "object") {
255
+ Object.keys(node.properties).forEach((prop) => {
256
+ if (!presentKeys.has(prop)) {
257
+ completions.push({
258
+ label: prop,
259
+ documentation:
260
+ node.properties[prop].node.description ??
261
+ node.properties[prop].node.title,
262
+ kind: CompletionItemKind.Property,
263
+ });
264
+ }
265
+ });
266
+ } else if (node.type === "and") {
267
+ completions.push(...getObjectCompletions(authoredNode, node.and));
268
+ } else if (node.type === "or") {
269
+ completions.push(...getObjectCompletions(authoredNode, node.or));
270
+ }
271
+ });
272
+
273
+ return completions;
274
+ }
275
+
276
+ /** get value completions */
277
+ function getPropertyCompletions(
278
+ propertyName: string,
279
+ potentialTypes: Array<NodeType>,
280
+ ) {
281
+ const completions: Array<CompletionItem> = [];
282
+ potentialTypes.forEach((nodeType) => {
283
+ if (nodeType.type === "object") {
284
+ const propertyNode = nodeType.properties[propertyName]?.node;
285
+ if (
286
+ propertyNode &&
287
+ propertyNode.type === "string" &&
288
+ propertyNode.const
289
+ ) {
290
+ completions.push({
291
+ label: propertyNode.const,
292
+ kind: CompletionItemKind.Value,
293
+ });
294
+ }
295
+ }
296
+ });
297
+
298
+ return completions;
299
+ }
300
+
301
+ /** returns completions for object properties */
302
+ function complete(
303
+ ctx: EnhancedDocumentContextWithPosition,
304
+ ): Array<CompletionItem> {
305
+ if (ctx.XLR?.nearestObjects) {
306
+ if (ctx.node.type === "string" && ctx.node?.parent?.type === "property") {
307
+ return getPropertyCompletions(
308
+ ctx.node.parent.keyNode.value,
309
+ ctx.XLR.nearestObjects,
310
+ );
311
+ }
312
+
313
+ return getObjectCompletions(ctx.node, ctx.XLR.nearestObjects);
314
+ }
315
+
316
+ return [];
317
+ }
318
+
319
+ /** gets hover docs */
320
+ function hover(ctx: EnhancedDocumentContextWithPosition) {
321
+ if (ctx.XLR && ctx.node.type === "string") {
322
+ const docStrings: Array<string> = [];
323
+ const prop = ctx.node.value;
324
+
325
+ ctx.XLR.nearestObjects.forEach((typeNode) => {
326
+ const docString =
327
+ typeNode.properties[prop]?.node?.description ??
328
+ typeNode.properties[prop]?.node?.title ??
329
+ undefined;
330
+ if (docString) {
331
+ docStrings.push(docString);
332
+ }
333
+ });
334
+
335
+ if (docStrings.length > 1) {
336
+ return {
337
+ contents: {
338
+ kind: MarkupKind.PlainText,
339
+ value:
340
+ "Docs unavailable - More than one type could exist at this location",
341
+ },
342
+ };
343
+ }
344
+
345
+ return {
346
+ contents: {
347
+ kind: MarkupKind.PlainText,
348
+ value: docStrings[0] ?? "Error getting docs",
349
+ },
350
+ };
351
+ }
352
+ }
353
+
354
+ /** The plugin to enable duplicate id checking/fixing */
355
+ export class XLRPlugin implements PlayerLanguageServicePlugin {
356
+ name = "xlr-plugin";
357
+
358
+ apply(service: PlayerLanguageService): void {
359
+ service.hooks.validate.tap(this.name, async (ctx, validation) => {
360
+ validation.useASTVisitor(
361
+ createValidationVisitor(validation, service.XLRService.XLRSDK),
362
+ );
363
+ });
364
+ service.hooks.complete.tap(this.name, async (ctx, completion) => {
365
+ complete(ctx).map((i) => completion.addCompletionItem(i));
366
+ });
367
+ service.hooks.hover.tap(this.name, (ctx) => {
368
+ return hover(ctx);
369
+ });
370
+ }
371
+ }
package/src/types.ts ADDED
@@ -0,0 +1,119 @@
1
+ import type { TextDocument } from "vscode-languageserver-textdocument";
2
+ import type {
3
+ Position,
4
+ DiagnosticSeverity,
5
+ Diagnostic,
6
+ CompletionItem,
7
+ } from "vscode-languageserver-types";
8
+ import type {
9
+ ASTNode,
10
+ PlayerContent,
11
+ StringASTNode,
12
+ NumberASTNode,
13
+ ArrayASTNode,
14
+ BooleanASTNode,
15
+ PropertyASTNode,
16
+ ObjectASTNode,
17
+ ViewASTNode,
18
+ ContentASTNode,
19
+ NullASTNode,
20
+ AssetASTNode,
21
+ FlowStateASTNode,
22
+ NavigationASTNode,
23
+ FlowASTNode,
24
+ NodeEdit,
25
+ EmptyASTNode,
26
+ } from "./parser";
27
+ import type { XLRContext } from "./xlr";
28
+
29
+ export type LogFn = (msg: string) => void;
30
+ export const LOG_TYPES = ["debug", "info", "warn", "error"] as const;
31
+ export type LogType = (typeof LOG_TYPES)[number];
32
+ export type Logger = Record<LogType, LogFn>;
33
+
34
+ export interface DocumentContext {
35
+ /** A logger to log messages */
36
+ log: Logger;
37
+
38
+ /** the underlying text document */
39
+ document: TextDocument;
40
+
41
+ /** the json parsed text document */
42
+ PlayerContent: PlayerContent;
43
+ }
44
+
45
+ export interface DocumentContextWithPosition extends DocumentContext {
46
+ /** the position we care about */
47
+ position: Position;
48
+
49
+ /** the node at that position */
50
+ node: ASTNode;
51
+ }
52
+
53
+ export interface EnhancedDocumentContextWithPosition extends DocumentContextWithPosition {
54
+ /** the XLRs context */
55
+ XLR?: XLRContext;
56
+ }
57
+
58
+ export type ASTVisitorFn<T extends ASTNode> = (node: T) => void;
59
+
60
+ export interface ASTVisitor {
61
+ /** a string node visitor */
62
+ StringNode?: ASTVisitorFn<StringASTNode>;
63
+ /** a number node visitor */
64
+ NumberNode?: ASTVisitorFn<NumberASTNode>;
65
+ /** an boolean node visitor */
66
+ BooleanNode?: ASTVisitorFn<BooleanASTNode>;
67
+ /** an array node visitor */
68
+ ArrayNode?: ASTVisitorFn<ArrayASTNode>;
69
+ /** a null node visitor */
70
+ NullNode?: ASTVisitorFn<NullASTNode>;
71
+ /** an empty node visitor */
72
+ EmptyNode?: ASTVisitorFn<EmptyASTNode>;
73
+ /** a property node visitor */
74
+ PropertyNode?: ASTVisitorFn<PropertyASTNode>;
75
+ /** an object node visitor */
76
+ ObjectNode?: ASTVisitorFn<ObjectASTNode>;
77
+ /** an asset node visitor */
78
+ AssetNode?: ASTVisitorFn<AssetASTNode>;
79
+ /** a view node visitor */
80
+ ViewNode?: ASTVisitorFn<ViewASTNode>;
81
+ /** a flow node visitor */
82
+ ContentNode?: ASTVisitorFn<ContentASTNode>;
83
+ /** a navigation node visitor */
84
+ NavigationNode?: ASTVisitorFn<NavigationASTNode>;
85
+ /** a flow node visitor */
86
+ FlowNode?: ASTVisitorFn<FlowASTNode>;
87
+ /** a flow state node visitor */
88
+ FlowStateNode?: ASTVisitorFn<FlowStateASTNode>;
89
+ }
90
+
91
+ export interface Violation {
92
+ /** the node the violation is for */
93
+ node: ASTNode;
94
+
95
+ /** the message to show */
96
+ message: string;
97
+
98
+ /** how much do we care? */
99
+ severity: DiagnosticSeverity;
100
+
101
+ /** A function that can make this good */
102
+ fix?: () => {
103
+ /** the edit to apply */
104
+ edit: NodeEdit;
105
+
106
+ /** A name for your fix */
107
+ name: string;
108
+ };
109
+ }
110
+
111
+ export interface ValidationContext {
112
+ addViolation(violation: Violation): void;
113
+ addDiagnostic(diagnostic: Diagnostic): void;
114
+ useASTVisitor(visitor: ASTVisitor): void;
115
+ }
116
+
117
+ export interface CompletionContext {
118
+ addCompletionItem(item: CompletionItem): void;
119
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,143 @@
1
+ import { Range, Location } from "vscode-languageserver-types";
2
+ import { TextDocument } from "vscode-languageserver-textdocument";
3
+ import detectIndent from "detect-indent";
4
+ import type {
5
+ ASTNode,
6
+ PlayerContent,
7
+ ObjectASTNode,
8
+ PropertyASTNode,
9
+ } from "./parser";
10
+ import type { ASTVisitor } from "./types";
11
+
12
+ export const typeToVisitorMap: Record<ASTNode["type"], keyof ASTVisitor> = {
13
+ string: "StringNode",
14
+ number: "NumberNode",
15
+ boolean: "BooleanNode",
16
+ array: "ArrayNode",
17
+ null: "NullNode",
18
+ empty: "EmptyNode",
19
+ property: "PropertyNode",
20
+ object: "ObjectNode",
21
+ asset: "AssetNode",
22
+ view: "ViewNode",
23
+ content: "ContentNode",
24
+ navigation: "NavigationNode",
25
+ flow: "FlowNode",
26
+ state: "FlowStateNode",
27
+ };
28
+
29
+ /** Check to see if the source range contains the target one */
30
+ export function containsRange(source: Range, range: Range): boolean {
31
+ const { start, end } = range;
32
+ const { start: srcStart, end: srcEnd } = source;
33
+
34
+ return (
35
+ start.line === srcStart.line &&
36
+ end.line === srcEnd.line &&
37
+ start.character >= srcStart.character &&
38
+ end.character <= srcEnd.character
39
+ );
40
+ }
41
+
42
+ /** Create a dummy TextDocument from the given string */
43
+ export function toTextDocument(str: string): TextDocument {
44
+ return TextDocument.create("foo", "json", 1, str);
45
+ }
46
+
47
+ /** Check to see if the document successfully parsed into a known root type */
48
+ export function isKnownRootType(document: PlayerContent): boolean {
49
+ const { type } = document.root;
50
+
51
+ return type === "view" || type === "asset" || type === "content";
52
+ }
53
+
54
+ /** Check to see if the node is the value of an object */
55
+ export function isValueCompletion(node: ASTNode): boolean {
56
+ return node.parent?.type === "property" && node.parent.valueNode === node;
57
+ }
58
+
59
+ /** Check to see if the node is the key of an object */
60
+ export function isPropertyCompletion(node: ASTNode): boolean {
61
+ return node.parent?.type === "property" && node.parent.keyNode === node;
62
+ }
63
+
64
+ /** Search the object for a property with the given name */
65
+ export function getProperty<T extends ASTNode>(
66
+ obj: T,
67
+ name: string,
68
+ ): PropertyASTNode | undefined {
69
+ if ("properties" in obj) {
70
+ return (obj as ObjectASTNode).properties.find(
71
+ (p) => p.keyNode.value === name,
72
+ );
73
+ }
74
+ }
75
+
76
+ /** Get the LSP Location of an AST node in a document */
77
+ export function getLSLocationOfNode(
78
+ document: TextDocument,
79
+ node: ASTNode,
80
+ ): Location {
81
+ const nodeRange = Range.create(
82
+ document.positionAt(node.jsonNode.offset),
83
+ document.positionAt(node.jsonNode.offset + node.jsonNode.length),
84
+ );
85
+
86
+ return Location.create(document.uri, nodeRange);
87
+ }
88
+
89
+ /** Get the depth of the property */
90
+ function getDepth(node: ASTNode): number {
91
+ if (!node.parent) {
92
+ return 0;
93
+ }
94
+
95
+ if (node.type === "property") {
96
+ return getDepth(node.parent);
97
+ }
98
+
99
+ return 1 + getDepth(node.parent);
100
+ }
101
+
102
+ /** Format a new node like an existing one (including the indentation) */
103
+ export function formatLikeNode(
104
+ document: TextDocument,
105
+ originalNode: ASTNode,
106
+ replacement: Record<string, unknown>,
107
+ ): string {
108
+ const { indent } = detectIndent(document.getText());
109
+ const depth = getDepth(originalNode);
110
+
111
+ return JSON.stringify(replacement, null, indent)
112
+ .split("\n")
113
+ .map((l, index) => (index === 0 ? l : `${indent.repeat(depth)}${l}`))
114
+ .join("\n");
115
+ }
116
+
117
+ /** Maps the string identifying the FlowType to the named type */
118
+ export function mapFlowStateToType(
119
+ flowType: string | undefined,
120
+ ): string | undefined {
121
+ let flowXLR;
122
+ switch (flowType) {
123
+ case "VIEW":
124
+ flowXLR = "NavigationFlowViewState";
125
+ break;
126
+ case "END":
127
+ flowXLR = "NavigationFlowEndState";
128
+ break;
129
+ case "ACTION":
130
+ flowXLR = "NavigationFlowActionState";
131
+ break;
132
+ case "EXTERNAL":
133
+ flowXLR = "NavigationFlowExternalState";
134
+ break;
135
+ case "FLOW":
136
+ flowXLR = "NavigationFlowFlowState";
137
+ break;
138
+ default:
139
+ break;
140
+ }
141
+
142
+ return flowXLR;
143
+ }