@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
package/src/index.ts ADDED
@@ -0,0 +1,490 @@
1
+ import type {
2
+ Range as JSONRange,
3
+ FormattingOptions as JSONFormattingOptions,
4
+ } from "jsonc-parser";
5
+ import { format as formatJSON } from "jsonc-parser";
6
+ import type { TextDocument } from "vscode-languageserver-textdocument";
7
+ import {
8
+ AsyncParallelHook,
9
+ SyncBailHook,
10
+ SyncHook,
11
+ SyncWaterfallHook,
12
+ } from "tapable-ts";
13
+ import type {
14
+ CodeAction,
15
+ CodeActionContext,
16
+ CompletionItem,
17
+ Diagnostic,
18
+ FormattingOptions,
19
+ Hover,
20
+ Position,
21
+ Location,
22
+ } from "vscode-languageserver-types";
23
+ import {
24
+ CompletionList,
25
+ Range,
26
+ TextEdit,
27
+ CodeActionKind,
28
+ } from "vscode-languageserver-types";
29
+
30
+ import type { TransformFunction } from "@xlr-lib/xlr";
31
+ import type {
32
+ DocumentContext,
33
+ ValidationContext,
34
+ CompletionContext,
35
+ EnhancedDocumentContextWithPosition,
36
+ Violation,
37
+ ASTVisitor,
38
+ } from "./types";
39
+ import { DEFAULT_FILTERS, PLUGINS, TRANSFORM_FUNCTIONS } from "./constants";
40
+
41
+ import type { ASTNode, PlayerContent } from "./parser";
42
+ import { parse, toRange, toTextEdit, walk } from "./parser";
43
+
44
+ import { containsRange, isKnownRootType, typeToVisitorMap } from "./utils";
45
+ import { XLRService } from "./xlr";
46
+ import { TSManifest } from "@xlr-lib/xlr";
47
+
48
+ export * from "./utils";
49
+ export * from "./constants";
50
+ export * from "./types";
51
+ export * from "./parser";
52
+ export * from "./xlr/index";
53
+
54
+ export interface PlayerLanguageServicePlugin {
55
+ /** The name of the plugin */
56
+ name: string;
57
+
58
+ /** the handle to get the LSP */
59
+ apply(languageService: PlayerLanguageService): void;
60
+ }
61
+
62
+ /** The thing that handles most of the LSP work */
63
+ export class PlayerLanguageService {
64
+ public readonly XLRService: XLRService;
65
+
66
+ private parseCache = new Map<
67
+ string,
68
+ {
69
+ /** the version of the document */
70
+ version: number;
71
+
72
+ /** The parsed document */
73
+ parsed: PlayerContent;
74
+ }
75
+ >();
76
+
77
+ private fixableViolationsForDocument = new Map<
78
+ string,
79
+ Map<Diagnostic, Violation>
80
+ >();
81
+
82
+ public readonly hooks: {
83
+ onDocumentUpdate: SyncHook<[DocumentContext], Record<string, any>>;
84
+ validate: AsyncParallelHook<[DocumentContext, ValidationContext], void>;
85
+ onValidateEnd: SyncWaterfallHook<
86
+ [
87
+ Diagnostic[],
88
+ {
89
+ /** The context of the document */
90
+ documentContext: DocumentContext;
91
+ /** A callback for adding a new fixable rule */
92
+ addFixableViolation: (diag: Diagnostic, violation: Violation) => void;
93
+ },
94
+ ],
95
+ Record<string, any>
96
+ >;
97
+ complete: AsyncParallelHook<
98
+ [EnhancedDocumentContextWithPosition, CompletionContext],
99
+ void
100
+ >;
101
+ hover: SyncBailHook<
102
+ [EnhancedDocumentContextWithPosition],
103
+ Hover | undefined,
104
+ Record<string, any>
105
+ >;
106
+ definition: SyncBailHook<
107
+ [EnhancedDocumentContextWithPosition],
108
+ Location | undefined,
109
+ Record<string, any>
110
+ >;
111
+ } = {
112
+ onDocumentUpdate: new SyncHook<[DocumentContext]>(),
113
+
114
+ validate: new AsyncParallelHook<
115
+ [DocumentContext, ValidationContext],
116
+ void
117
+ >(),
118
+
119
+ onValidateEnd: new SyncWaterfallHook<
120
+ [
121
+ Diagnostic[],
122
+ {
123
+ /** The context of the document */
124
+ documentContext: DocumentContext;
125
+ /** A callback for adding a new fixable rule */
126
+ addFixableViolation: (diag: Diagnostic, violation: Violation) => void;
127
+ },
128
+ ]
129
+ >(),
130
+
131
+ complete: new AsyncParallelHook<
132
+ [EnhancedDocumentContextWithPosition, CompletionContext],
133
+ void
134
+ >(),
135
+
136
+ hover: new SyncBailHook<
137
+ [EnhancedDocumentContextWithPosition],
138
+ Hover | undefined
139
+ >(),
140
+
141
+ definition: new SyncBailHook<
142
+ [EnhancedDocumentContextWithPosition],
143
+ Location | undefined
144
+ >(),
145
+ };
146
+
147
+ constructor(config?: {
148
+ /** A list of plugins to include */
149
+ plugins?: Array<PlayerLanguageServicePlugin>;
150
+ }) {
151
+ // load base definitions?
152
+ this.XLRService = new XLRService();
153
+
154
+ PLUGINS.forEach((p) => p.apply(this));
155
+ config?.plugins?.forEach((p) => p.apply(this));
156
+ }
157
+
158
+ private parseTextDocument(document: TextDocument): PlayerContent {
159
+ if (!this.parseCache.has(document.uri)) {
160
+ const parsed = parse(document);
161
+ this.parseCache.set(document.uri, { version: document.version, parsed });
162
+ return parsed;
163
+ }
164
+
165
+ const cached = this.parseCache.get(document.uri);
166
+
167
+ if (!cached || cached.version < document.version) {
168
+ this.parseCache.delete(document.uri);
169
+ return this.parseTextDocument(document);
170
+ }
171
+
172
+ return cached.parsed;
173
+ }
174
+
175
+ private async updateSource(document: TextDocument): Promise<DocumentContext> {
176
+ const parsed = this.parseTextDocument(document);
177
+ const documentContext: DocumentContext = {
178
+ log: {
179
+ debug: console.log,
180
+ info: console.log,
181
+ warn: console.warn,
182
+ error: console.error,
183
+ },
184
+ document,
185
+ PlayerContent: parsed,
186
+ };
187
+
188
+ const ctx = {
189
+ ...documentContext,
190
+ };
191
+
192
+ this.hooks.onDocumentUpdate.call(ctx);
193
+ return ctx;
194
+ }
195
+
196
+ private async getJSONPositionInfo(
197
+ ctx: DocumentContext,
198
+ position: Position,
199
+ ): Promise<{
200
+ /** the node at the given node */
201
+ node?: ASTNode;
202
+ }> {
203
+ const { document, PlayerContent } = ctx;
204
+ const node = PlayerContent.getNodeFromOffset(document.offsetAt(position));
205
+
206
+ return {
207
+ node,
208
+ };
209
+ }
210
+
211
+ private async updateSourceWithPosition(
212
+ document: TextDocument,
213
+ position: Position,
214
+ ): Promise<EnhancedDocumentContextWithPosition | undefined> {
215
+ const ctx = await this.updateSource(document);
216
+
217
+ const { node } = await this.getJSONPositionInfo(ctx, position);
218
+
219
+ const XLRInfo = this.XLRService.getTypeInfoAtPosition(node);
220
+ if (!node || !XLRInfo) {
221
+ return undefined;
222
+ }
223
+
224
+ return {
225
+ ...ctx,
226
+ node,
227
+ position,
228
+ XLR: XLRInfo,
229
+ };
230
+ }
231
+
232
+ public onClose(document: TextDocument): void {
233
+ this.fixableViolationsForDocument.delete(document.uri);
234
+ this.parseCache.delete(document.uri);
235
+ }
236
+
237
+ async formatTextDocument(
238
+ document: TextDocument,
239
+ options: FormattingOptions,
240
+ range?: Range,
241
+ ): Promise<Array<TextEdit> | undefined> {
242
+ const formattingOptions: JSONFormattingOptions = {
243
+ tabSize: options.tabSize,
244
+ insertSpaces: options.insertSpaces,
245
+ };
246
+
247
+ let formatRange: JSONRange | undefined;
248
+
249
+ if (range) {
250
+ const startOffset = document.offsetAt(range.start);
251
+ formatRange = {
252
+ offset: startOffset,
253
+ length: document.offsetAt(range.end) - startOffset,
254
+ };
255
+ }
256
+
257
+ return formatJSON(document.getText(), formatRange, formattingOptions).map(
258
+ (edit) => {
259
+ return TextEdit.replace(
260
+ Range.create(
261
+ document.positionAt(edit.offset),
262
+ document.positionAt(edit.offset + edit.length),
263
+ ),
264
+ edit.content,
265
+ );
266
+ },
267
+ );
268
+ }
269
+
270
+ async validateTextDocument(
271
+ document: TextDocument,
272
+ ): Promise<Array<Diagnostic> | undefined> {
273
+ const ctx = await this.updateSource(document);
274
+ this.fixableViolationsForDocument.delete(document.uri);
275
+
276
+ if (!isKnownRootType(ctx.PlayerContent)) {
277
+ return;
278
+ }
279
+
280
+ const diagnostics = [...ctx.PlayerContent.syntaxErrors];
281
+ const astVisitors: Array<ASTVisitor> = [];
282
+
283
+ /** Add a matching violation fix to the original diagnostic */
284
+ const addFixableViolation = (
285
+ diagnostic: Diagnostic,
286
+ violation: Violation,
287
+ ) => {
288
+ if (!this.fixableViolationsForDocument.has(document.uri)) {
289
+ this.fixableViolationsForDocument.set(document.uri, new Map());
290
+ }
291
+
292
+ const fixableDiags = this.fixableViolationsForDocument.get(document.uri);
293
+
294
+ fixableDiags?.set(diagnostic, violation);
295
+ };
296
+
297
+ if (ctx.PlayerContent.root) {
298
+ const validationContext: ValidationContext = {
299
+ addViolation: (violation) => {
300
+ const { message, node, severity, fix } = violation;
301
+
302
+ const range: Range = toRange(document, node);
303
+
304
+ const diagnostic: Diagnostic = {
305
+ message,
306
+ severity,
307
+ range,
308
+ };
309
+
310
+ if (fix) {
311
+ addFixableViolation(diagnostic, violation);
312
+ }
313
+
314
+ diagnostics.push(diagnostic);
315
+ },
316
+ useASTVisitor: (visitor) => {
317
+ astVisitors.push(visitor);
318
+ },
319
+ addDiagnostic(d) {
320
+ diagnostics.push(d);
321
+ },
322
+ };
323
+
324
+ await this.hooks.validate.call(ctx, validationContext);
325
+
326
+ // Walk the tree using any of the registered visitors
327
+ // This is for perf so we only walk the tree once
328
+
329
+ await walk(ctx.PlayerContent.root, async (node) => {
330
+ const visitorProp = typeToVisitorMap[node.type];
331
+
332
+ astVisitors.forEach(async (visitor) => {
333
+ try {
334
+ await visitor[visitorProp]?.(node as any);
335
+ } catch (e: any) {
336
+ ctx.log?.error(
337
+ `Error running rules for ${visitorProp}: ${e.message}, Stack ${e.stack}`,
338
+ );
339
+ }
340
+ });
341
+
342
+ return false;
343
+ });
344
+ }
345
+
346
+ return this.hooks.onValidateEnd.call(diagnostics, {
347
+ documentContext: ctx,
348
+ addFixableViolation,
349
+ });
350
+ }
351
+
352
+ async getCompletionsAtPosition(
353
+ document: TextDocument,
354
+ position: Position,
355
+ ): Promise<CompletionList> {
356
+ const ctxWithPos = await this.updateSourceWithPosition(document, position);
357
+
358
+ if (!ctxWithPos) {
359
+ return CompletionList.create();
360
+ }
361
+
362
+ const completionItems: Array<CompletionItem> = [];
363
+
364
+ const completionContext: CompletionContext = {
365
+ addCompletionItem: (i) => {
366
+ completionItems.push(i);
367
+ },
368
+ };
369
+ await this.hooks.complete.call(ctxWithPos, completionContext);
370
+
371
+ return CompletionList.create(completionItems);
372
+ }
373
+
374
+ async resolveCompletionItem(
375
+ completionItem: CompletionItem,
376
+ ): Promise<CompletionItem> {
377
+ return completionItem;
378
+ }
379
+
380
+ async getHoverInfoAtPosition(
381
+ document: TextDocument,
382
+ position: Position,
383
+ ): Promise<Hover | undefined | null> {
384
+ const context = await this.updateSourceWithPosition(document, position);
385
+
386
+ if (!context) {
387
+ return undefined;
388
+ }
389
+
390
+ return this.hooks.hover.call(context);
391
+ }
392
+
393
+ async getCodeActionsInRange(
394
+ document: TextDocument,
395
+ context: CodeActionContext,
396
+ ): Promise<CodeAction[]> {
397
+ const diagsForDocument = this.fixableViolationsForDocument.get(
398
+ document.uri,
399
+ );
400
+
401
+ if (
402
+ !diagsForDocument ||
403
+ diagsForDocument.size === 0 ||
404
+ context.diagnostics.length === 0
405
+ ) {
406
+ return [];
407
+ }
408
+
409
+ const actions: CodeAction[] = [];
410
+ // Get all overlapping rules
411
+
412
+ diagsForDocument.forEach((violation, diagnostic) => {
413
+ const matching = context.diagnostics.find((diag) =>
414
+ containsRange(diagnostic.range, diag.range),
415
+ );
416
+ const fixedAction = violation.fix?.();
417
+
418
+ if (!matching || !fixedAction) {
419
+ return;
420
+ }
421
+
422
+ actions.push({
423
+ title: fixedAction.name,
424
+ kind: CodeActionKind.QuickFix,
425
+ edit: {
426
+ changes: {
427
+ [document.uri]: [toTextEdit(document, fixedAction.edit)],
428
+ },
429
+ },
430
+ });
431
+ });
432
+
433
+ return actions;
434
+ }
435
+
436
+ public async getDefinitionAtPosition(
437
+ document: TextDocument,
438
+ position: Position,
439
+ ): Promise<Location | undefined | null> {
440
+ const context = await this.updateSourceWithPosition(document, position);
441
+
442
+ if (!context) {
443
+ return undefined;
444
+ }
445
+
446
+ return this.hooks.definition.call(context);
447
+ }
448
+
449
+ public addXLRTransforms(transforms: Record<string, TransformFunction>): void {
450
+ Object.entries(transforms).forEach(([name, fn]) =>
451
+ this.XLRService.XLRSDK.addTransformFunction(name, fn),
452
+ );
453
+ }
454
+
455
+ public addLSPPlugin(plugin: PlayerLanguageServicePlugin): void {
456
+ plugin.apply(this);
457
+ }
458
+
459
+ async setAssetTypes(typeFiles: Array<string>): Promise<void> {
460
+ // await this.typescriptService.setAssetTypes(typeFiles);
461
+ typeFiles.forEach((file) => {
462
+ // Find a better way of loading default types
463
+ if (file.includes("types")) {
464
+ this.XLRService.XLRSDK.loadDefinitionsFromDisk(file, {});
465
+ } else {
466
+ this.XLRService.XLRSDK.loadDefinitionsFromDisk(
467
+ file,
468
+ DEFAULT_FILTERS,
469
+ TRANSFORM_FUNCTIONS,
470
+ );
471
+ }
472
+ });
473
+ }
474
+
475
+ async setAssetTypesFromModule(manifest: Array<TSManifest>): Promise<void> {
476
+ await Promise.allSettled(
477
+ manifest.map((m) => {
478
+ if (m.capabilities["Types"]?.length) {
479
+ return this.XLRService.XLRSDK.loadDefinitionsFromModule(m);
480
+ } else {
481
+ return this.XLRService.XLRSDK.loadDefinitionsFromModule(
482
+ m,
483
+ DEFAULT_FILTERS,
484
+ TRANSFORM_FUNCTIONS,
485
+ );
486
+ }
487
+ }),
488
+ );
489
+ }
490
+ }
@@ -0,0 +1,18 @@
1
+ import { test, expect } from "vitest";
2
+ import { TextDocument } from "vscode-languageserver-textdocument";
3
+ import { parse } from "../document";
4
+
5
+ test("parses simple content", () => {
6
+ const document = parse(
7
+ TextDocument.create(
8
+ "foo.json",
9
+ "json",
10
+ 1,
11
+ JSON.stringify({
12
+ views: [{ id: "foo" }],
13
+ }),
14
+ ),
15
+ );
16
+
17
+ expect(document.root.type).toBe("content");
18
+ });