@platformos/platformos-language-server-common 0.0.12 → 0.0.13

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 (69) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/PropertyShapeInference.d.ts +5 -0
  3. package/dist/PropertyShapeInference.js +28 -0
  4. package/dist/PropertyShapeInference.js.map +1 -1
  5. package/dist/TypeSystem.js +106 -55
  6. package/dist/TypeSystem.js.map +1 -1
  7. package/dist/VariableShapeExtractor.js +2 -4
  8. package/dist/VariableShapeExtractor.js.map +1 -1
  9. package/dist/completions/CompletionsProvider.d.ts +1 -0
  10. package/dist/completions/CompletionsProvider.js +6 -0
  11. package/dist/completions/CompletionsProvider.js.map +1 -1
  12. package/dist/completions/providers/GraphQLFieldCompletionProvider.d.ts +12 -0
  13. package/dist/completions/providers/GraphQLFieldCompletionProvider.js +75 -0
  14. package/dist/completions/providers/GraphQLFieldCompletionProvider.js.map +1 -0
  15. package/dist/definitions/DefinitionProvider.d.ts +19 -1
  16. package/dist/definitions/DefinitionProvider.js +33 -1
  17. package/dist/definitions/DefinitionProvider.js.map +1 -1
  18. package/dist/definitions/providers/PageRouteDefinitionProvider.d.ts +51 -0
  19. package/dist/definitions/providers/PageRouteDefinitionProvider.js +204 -0
  20. package/dist/definitions/providers/PageRouteDefinitionProvider.js.map +1 -0
  21. package/dist/definitions/providers/RenderPartialDefinitionProvider.d.ts +14 -0
  22. package/dist/definitions/providers/RenderPartialDefinitionProvider.js +50 -0
  23. package/dist/definitions/providers/RenderPartialDefinitionProvider.js.map +1 -0
  24. package/dist/diagnostics/runChecks.d.ts +3 -1
  25. package/dist/diagnostics/runChecks.js +2 -1
  26. package/dist/diagnostics/runChecks.js.map +1 -1
  27. package/dist/documentLinks/DocumentLinksProvider.d.ts +3 -1
  28. package/dist/documentLinks/DocumentLinksProvider.js +16 -20
  29. package/dist/documentLinks/DocumentLinksProvider.js.map +1 -1
  30. package/dist/hover/HoverProvider.d.ts +1 -0
  31. package/dist/hover/HoverProvider.js +6 -0
  32. package/dist/hover/HoverProvider.js.map +1 -1
  33. package/dist/hover/providers/GraphQLFieldHoverProvider.d.ts +12 -0
  34. package/dist/hover/providers/GraphQLFieldHoverProvider.js +85 -0
  35. package/dist/hover/providers/GraphQLFieldHoverProvider.js.map +1 -0
  36. package/dist/server/AppGraphManager.js +3 -4
  37. package/dist/server/AppGraphManager.js.map +1 -1
  38. package/dist/server/CachedFileSystem.js +7 -2
  39. package/dist/server/CachedFileSystem.js.map +1 -1
  40. package/dist/server/startServer.js +71 -5
  41. package/dist/server/startServer.js.map +1 -1
  42. package/dist/tsconfig.tsbuildinfo +1 -1
  43. package/dist/utils/searchPaths.d.ts +21 -0
  44. package/dist/utils/searchPaths.js +34 -0
  45. package/dist/utils/searchPaths.js.map +1 -0
  46. package/package.json +6 -4
  47. package/src/PropertyShapeInference.spec.ts +44 -0
  48. package/src/PropertyShapeInference.ts +27 -0
  49. package/src/TypeSystem.spec.ts +59 -0
  50. package/src/TypeSystem.ts +119 -58
  51. package/src/VariableShapeExtractor.ts +2 -4
  52. package/src/completions/CompletionsProvider.ts +11 -0
  53. package/src/completions/providers/GraphQLFieldCompletionProvider.ts +78 -0
  54. package/src/definitions/DefinitionProvider.ts +52 -0
  55. package/src/definitions/providers/PageRouteDefinitionProvider.spec.ts +753 -0
  56. package/src/definitions/providers/PageRouteDefinitionProvider.ts +250 -0
  57. package/src/definitions/providers/RenderPartialDefinitionProvider.spec.ts +191 -0
  58. package/src/definitions/providers/RenderPartialDefinitionProvider.ts +70 -0
  59. package/src/diagnostics/runChecks.ts +4 -0
  60. package/src/documentLinks/DocumentLinksProvider.spec.ts +131 -10
  61. package/src/documentLinks/DocumentLinksProvider.ts +23 -32
  62. package/src/hover/HoverProvider.ts +11 -0
  63. package/src/hover/providers/GraphQLFieldHoverProvider.spec.ts +124 -0
  64. package/src/hover/providers/GraphQLFieldHoverProvider.ts +90 -0
  65. package/src/server/AppGraphManager.ts +3 -5
  66. package/src/server/CachedFileSystem.ts +11 -2
  67. package/src/server/startServer.ts +85 -5
  68. package/src/utils/searchPaths.spec.ts +101 -0
  69. package/src/utils/searchPaths.ts +32 -0
@@ -10,7 +10,7 @@ import {
10
10
  SourceCodeType,
11
11
  UriString,
12
12
  } from '@platformos/platformos-check-common';
13
- import { TranslationProvider } from '@platformos/platformos-common';
13
+ import { TranslationProvider, isPage } from '@platformos/platformos-common';
14
14
  import {
15
15
  Connection,
16
16
  FileChangeType,
@@ -44,6 +44,7 @@ import {
44
44
  AppGraphRootRequest,
45
45
  } from '../types';
46
46
  import { debounce } from '../utils';
47
+ import { SearchPathsLoader } from '../utils/searchPaths';
47
48
  import { VERSION } from '../version';
48
49
  import { CachedFileSystem } from './CachedFileSystem';
49
50
  import { Configuration, INCLUDE_FILES_FROM_DISK } from './Configuration';
@@ -55,6 +56,13 @@ import { URI } from 'vscode-uri';
55
56
 
56
57
  const defaultLogger = () => {};
57
58
 
59
+ /**
60
+ * When a file-watcher batch contains this many page-file changes, we
61
+ * assume a bulk operation (git checkout, branch switch, stash pop) and
62
+ * fully rebuild the route table instead of applying incremental updates.
63
+ */
64
+ const BULK_PAGE_CHANGE_THRESHOLD = 10;
65
+
58
66
  /**
59
67
  * The `git:` VFS does not support the `fs.readDirectory` call and makes most things break.
60
68
  * `git` URIs are the ones you'd encounter when doing a git diff in VS Code. They're not
@@ -108,11 +116,13 @@ export function startServer(
108
116
  const diagnosticsManager = new DiagnosticsManager(connection);
109
117
  const documentsLocator = new DocumentsLocator(fs);
110
118
  const translationProvider = new TranslationProvider(fs);
119
+ const searchPathsCache = new SearchPathsLoader(fs);
111
120
  const documentLinksProvider = new DocumentLinksProvider(
112
121
  documentManager,
113
122
  findAppRootURI,
114
123
  documentsLocator,
115
124
  translationProvider,
125
+ searchPathsCache,
116
126
  );
117
127
  const codeActionsProvider = new CodeActionsProvider(documentManager, diagnosticsManager);
118
128
  const onTypeFormattingProvider = new OnTypeFormattingProvider(
@@ -161,6 +171,7 @@ export function startServer(
161
171
  jsonValidationSet,
162
172
  appGraphManager,
163
173
  includeFilesFromDisk: () => configuration[INCLUDE_FILES_FROM_DISK],
174
+ getRouteTable: () => definitionsProvider.getRouteTable(),
164
175
  }),
165
176
  100,
166
177
  );
@@ -215,7 +226,14 @@ export function startServer(
215
226
  return (documentManager.get(defaultLocaleFileUri) as AugmentedJsonSourceCode) ?? null;
216
227
  }
217
228
 
218
- const definitionsProvider = new DefinitionProvider(documentManager, getDefaultLocaleSourceCode);
229
+ const definitionsProvider = new DefinitionProvider(
230
+ documentManager,
231
+ getDefaultLocaleSourceCode,
232
+ fs,
233
+ findAppRootURI,
234
+ documentsLocator,
235
+ searchPathsCache,
236
+ );
219
237
  const jsonLanguageService = new JSONLanguageService(documentManager, jsonValidationSet);
220
238
  const cssLanguageService = new CSSLanguageService(documentManager);
221
239
  const completionsProvider = new CompletionsProvider({
@@ -342,6 +360,9 @@ export function startServer(
342
360
  {
343
361
  globPattern: '**/*.css',
344
362
  },
363
+ {
364
+ globPattern: '**/app/config.yml',
365
+ },
345
366
  ],
346
367
  });
347
368
  });
@@ -388,7 +409,11 @@ export function startServer(
388
409
  cssLanguageService.change(uri, params.contentChanges[0].text, version);
389
410
  return;
390
411
  }
391
- documentManager.change(uri, params.contentChanges[0].text, version);
412
+ const text = params.contentChanges[0].text;
413
+ documentManager.change(uri, text, version);
414
+ if (isPage(uri)) {
415
+ definitionsProvider.onPageFileChanged(uri, text);
416
+ }
392
417
  if (await configuration.shouldCheckOnChange()) {
393
418
  runChecks([uri]);
394
419
  } else {
@@ -400,6 +425,15 @@ export function startServer(
400
425
  connection.onDidSaveTextDocument(async (params) => {
401
426
  if (hasUnsupportedDocument(params)) return;
402
427
  const { uri } = params.textDocument;
428
+
429
+ // onDidChangeWatchedFiles also fires for in-editor saves, but it arrives after
430
+ // onDidSaveTextDocument. Invalidate the search-paths cache here immediately so
431
+ // that go-to-definition requests triggered by the same save don't see stale data
432
+ // while waiting for the file-watcher notification.
433
+ if (uri.endsWith('/app/config.yml')) {
434
+ searchPathsCache.invalidate();
435
+ }
436
+
403
437
  if (await configuration.shouldCheckOnSave()) {
404
438
  runChecks([uri]);
405
439
  }
@@ -534,6 +568,16 @@ export function startServer(
534
568
  if (params.changes.length === 0) return;
535
569
 
536
570
  const triggerUris = params.changes.map((change) => change.uri);
571
+
572
+ // When many page files change at once (e.g., git checkout, branch switch, stash pop),
573
+ // incremental updates aren't reliable — files not open in the editor won't get
574
+ // individual onDidChangeTextDocument events. VS Code reports branch-switch changes
575
+ // as FileChangeType.Changed, so we count all change types.
576
+ const bulkPageChanges = params.changes.filter((c) => isPage(c.uri));
577
+ if (bulkPageChanges.length >= BULK_PAGE_CHANGE_THRESHOLD) {
578
+ definitionsProvider.invalidateRouteTable();
579
+ }
580
+
537
581
  const updates: Promise<any>[] = [];
538
582
  for (const change of params.changes) {
539
583
  // App Check config changes should clear the config cache
@@ -542,6 +586,21 @@ export function startServer(
542
586
  continue;
543
587
  }
544
588
 
589
+ // app/config.yml changes should clear expanded search paths cache
590
+ // and force re-check of all open documents.
591
+ // Note: readFile for config.yml bypasses the CachedFileSystem cache,
592
+ // so we don't need to invalidate it — reads are always fresh.
593
+ if (change.uri.endsWith('/app/config.yml')) {
594
+ documentsLocator.clearExpandedPathsCache();
595
+ searchPathsCache.invalidate();
596
+
597
+ // Ensure open liquid files are re-checked with the new search paths
598
+ for (const doc of documentManager.openDocuments) {
599
+ triggerUris.push(doc.uri);
600
+ }
601
+ continue;
602
+ }
603
+
545
604
  // Rename cache invalidation is handled by onDidRenameFiles
546
605
  if (documentManager.hasRecentRename(change.uri)) {
547
606
  documentManager.clearRecentRename(change.uri);
@@ -556,7 +615,14 @@ export function startServer(
556
615
  fs.stat.invalidate(change.uri);
557
616
  appGraphManager.create(change.uri);
558
617
  // If a file is created under out feet, we update its contents.
559
- updates.push(documentManager.changeFromDisk(change.uri));
618
+ updates.push(
619
+ documentManager.changeFromDisk(change.uri).then(() => {
620
+ if (isPage(change.uri)) {
621
+ const doc = documentManager.get(change.uri);
622
+ if (doc) definitionsProvider.onPageFileChanged(change.uri, doc.source);
623
+ }
624
+ }),
625
+ );
560
626
  break;
561
627
 
562
628
  case FileChangeType.Changed:
@@ -568,7 +634,20 @@ export function startServer(
568
634
  // If it is open, then we don't need to update it because the document manager
569
635
  // will have the version from the editor.
570
636
  if (documentManager.get(change.uri)?.version === undefined) {
571
- updates.push(documentManager.changeFromDisk(change.uri));
637
+ updates.push(
638
+ documentManager.changeFromDisk(change.uri).then(() => {
639
+ if (isPage(change.uri)) {
640
+ const doc = documentManager.get(change.uri);
641
+ if (doc) definitionsProvider.onPageFileChanged(change.uri, doc.source);
642
+ }
643
+ }),
644
+ );
645
+ } else if (isPage(change.uri)) {
646
+ // File is open in editor but changed externally (e.g. git checkout).
647
+ // The doc manager already has the editor version, but the route table
648
+ // may be stale — update it from whatever the doc manager holds.
649
+ const doc = documentManager.get(change.uri);
650
+ if (doc) definitionsProvider.onPageFileChanged(change.uri, doc.source);
572
651
  }
573
652
  break;
574
653
 
@@ -578,6 +657,7 @@ export function startServer(
578
657
  fs.readFile.invalidate(change.uri);
579
658
  fs.stat.invalidate(change.uri);
580
659
  appGraphManager.delete(change.uri);
660
+ if (isPage(change.uri)) definitionsProvider.onPageFileDeleted(change.uri);
581
661
  // If a file is deleted, it's removed from the document manager
582
662
  documentManager.delete(change.uri);
583
663
  break;
@@ -0,0 +1,101 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { URI } from 'vscode-uri';
3
+ import { SearchPathsLoader } from './searchPaths';
4
+
5
+ const rootUri = URI.parse('file:///project');
6
+ const configUri = 'file:///project/app/config.yml';
7
+
8
+ function createMockFs(configContent: string | null) {
9
+ return {
10
+ readFile: vi.fn(async (uri: string) => {
11
+ if (uri === configUri && configContent !== null) return configContent;
12
+ throw new Error(`File not found: ${uri}`);
13
+ }),
14
+ readDirectory: vi.fn(async () => []),
15
+ stat: vi.fn(async () => ({ type: 1, size: 0 })),
16
+ };
17
+ }
18
+
19
+ describe('SearchPathsLoader', () => {
20
+ it('should cache results across calls for the same root', async () => {
21
+ const fs = createMockFs('theme_search_paths:\n - theme/dress');
22
+ const loader = new SearchPathsLoader(fs);
23
+
24
+ const result1 = await loader.get(rootUri);
25
+ expect(result1).toEqual(['theme/dress']);
26
+
27
+ const result2 = await loader.get(rootUri);
28
+ expect(result2).toEqual(['theme/dress']);
29
+
30
+ // readFile should only be called once — second call served from cache
31
+ expect(fs.readFile).toHaveBeenCalledTimes(1);
32
+ });
33
+
34
+ it('should return fresh results after invalidate()', async () => {
35
+ let configContent = 'theme_search_paths:\n - theme/dress';
36
+ const fs = {
37
+ readFile: vi.fn(async (uri: string) => {
38
+ if (uri === configUri) return configContent;
39
+ throw new Error(`File not found: ${uri}`);
40
+ }),
41
+ readDirectory: vi.fn(async () => []),
42
+ stat: vi.fn(async () => ({ type: 1, size: 0 })),
43
+ };
44
+ const loader = new SearchPathsLoader(fs);
45
+
46
+ const result1 = await loader.get(rootUri);
47
+ expect(result1).toEqual(['theme/dress']);
48
+
49
+ // Config changes externally
50
+ configContent = 'theme_search_paths:\n - theme/simple';
51
+
52
+ // Without invalidation, still returns cached result
53
+ const result2 = await loader.get(rootUri);
54
+ expect(result2).toEqual(['theme/dress']);
55
+
56
+ // After invalidation, reads fresh
57
+ loader.invalidate();
58
+ const result3 = await loader.get(rootUri);
59
+ expect(result3).toEqual(['theme/simple']);
60
+
61
+ expect(fs.readFile).toHaveBeenCalledTimes(2);
62
+ });
63
+
64
+ it('should return null when config has no search paths', async () => {
65
+ const fs = createMockFs('some_other_key: value');
66
+ const loader = new SearchPathsLoader(fs);
67
+
68
+ const result = await loader.get(rootUri);
69
+ expect(result).toBeNull();
70
+ });
71
+
72
+ it('should return null when config.yml does not exist', async () => {
73
+ const fs = createMockFs(null);
74
+ const loader = new SearchPathsLoader(fs);
75
+
76
+ const result = await loader.get(rootUri);
77
+ expect(result).toBeNull();
78
+ });
79
+
80
+ it('should cache per root URI independently', async () => {
81
+ const root2 = URI.parse('file:///other-project');
82
+ const config2Uri = 'file:///other-project/app/config.yml';
83
+ const fs = {
84
+ readFile: vi.fn(async (uri: string) => {
85
+ if (uri === configUri) return 'theme_search_paths:\n - theme/a';
86
+ if (uri === config2Uri) return 'theme_search_paths:\n - theme/b';
87
+ throw new Error(`File not found: ${uri}`);
88
+ }),
89
+ readDirectory: vi.fn(async () => []),
90
+ stat: vi.fn(async () => ({ type: 1, size: 0 })),
91
+ };
92
+ const loader = new SearchPathsLoader(fs);
93
+
94
+ const result1 = await loader.get(rootUri);
95
+ const result2 = await loader.get(root2);
96
+
97
+ expect(result1).toEqual(['theme/a']);
98
+ expect(result2).toEqual(['theme/b']);
99
+ expect(fs.readFile).toHaveBeenCalledTimes(2);
100
+ });
101
+ });
@@ -0,0 +1,32 @@
1
+ import { AbstractFileSystem, loadSearchPaths } from '@platformos/platformos-common';
2
+ import { URI } from 'vscode-uri';
3
+
4
+ /**
5
+ * Cached loader for theme_search_paths from app/config.yml.
6
+ *
7
+ * Caches per root URI so that repeated calls (document links, definitions,
8
+ * checks) within the same editor session don't re-read and re-parse the
9
+ * config file on every invocation.
10
+ *
11
+ * Call `invalidate()` when app/config.yml changes on disk — the file watcher
12
+ * in startServer.ts handles this. This is the only reliable invalidation
13
+ * point because config.yml changes may come from external tools (vim, CLI)
14
+ * that don't trigger editor-level change events.
15
+ */
16
+ export class SearchPathsLoader {
17
+ private cache = new Map<string, Promise<string[] | null>>();
18
+
19
+ constructor(private fs: AbstractFileSystem) {}
20
+
21
+ get(rootUri: URI): Promise<string[] | null> {
22
+ const key = rootUri.toString();
23
+ if (!this.cache.has(key)) {
24
+ this.cache.set(key, loadSearchPaths(this.fs, rootUri));
25
+ }
26
+ return this.cache.get(key)!;
27
+ }
28
+
29
+ invalidate(): void {
30
+ this.cache.clear();
31
+ }
32
+ }