@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
@@ -0,0 +1,250 @@
1
+ import { LiquidHtmlNode, HtmlElement, NodeTypes, TextNode } from '@platformos/liquid-html-parser';
2
+ import { RouteTable, AbstractFileSystem } from '@platformos/platformos-common';
3
+ import {
4
+ SourceCodeType,
5
+ shouldSkipUrl,
6
+ isValuedAttrNode,
7
+ getAttrName,
8
+ extractUrlPattern,
9
+ getEffectiveMethod,
10
+ buildVariableMap,
11
+ ValuedAttrNode,
12
+ } from '@platformos/platformos-check-common';
13
+ import { URI } from 'vscode-uri';
14
+ import {
15
+ DefinitionParams,
16
+ DefinitionLink,
17
+ Range,
18
+ LocationLink,
19
+ } from 'vscode-languageserver-protocol';
20
+ import { DocumentManager } from '../../documents';
21
+ import { BaseDefinitionProvider } from '../BaseDefinitionProvider';
22
+
23
+ function getTagName(node: LiquidHtmlNode): string | null {
24
+ if (node.type !== NodeTypes.HtmlElement) return null;
25
+ const el = node as HtmlElement;
26
+ if (el.name.length !== 1) return null;
27
+ if ((el.name[0] as LiquidHtmlNode).type !== NodeTypes.TextNode) return null;
28
+ return (el.name[0] as TextNode).value;
29
+ }
30
+
31
+ /** Tag name → attribute that holds the URL */
32
+ const TAG_URL_ATTR: Record<string, string> = {
33
+ a: 'href',
34
+ form: 'action',
35
+ };
36
+
37
+ /**
38
+ * Find the URL-bearing attribute on an element.
39
+ * For <a> looks for href, for <form> looks for action.
40
+ */
41
+ function findUrlAttr(el: HtmlElement, tagName: string): ValuedAttrNode | null {
42
+ const urlAttrName = TAG_URL_ATTR[tagName];
43
+ if (!urlAttrName) return null;
44
+ const attr = (el.attributes as LiquidHtmlNode[]).find(
45
+ (a) => isValuedAttrNode(a) && getAttrName(a) === urlAttrName,
46
+ );
47
+ if (!attr || !isValuedAttrNode(attr)) return null;
48
+ return attr;
49
+ }
50
+
51
+ export class PageRouteDefinitionProvider implements BaseDefinitionProvider {
52
+ private routeTable: RouteTable;
53
+ private builtRoots = new Set<string>();
54
+
55
+ constructor(
56
+ private documentManager: DocumentManager,
57
+ private fs: AbstractFileSystem,
58
+ private findAppRootURI: (uri: string) => Promise<string | null>,
59
+ ) {
60
+ this.routeTable = new RouteTable(fs);
61
+ }
62
+
63
+ /** Returns the shared route table (may trigger a build if not yet built). */
64
+ getRouteTable(): RouteTable {
65
+ return this.routeTable;
66
+ }
67
+
68
+ private async ensureBuilt(uri: string): Promise<boolean> {
69
+ const rootUri = await this.findAppRootURI(uri);
70
+ if (!rootUri) return false;
71
+
72
+ if (!this.builtRoots.has(rootUri)) {
73
+ await this.routeTable.build(URI.parse(rootUri));
74
+ this.builtRoots.add(rootUri);
75
+ }
76
+ return true;
77
+ }
78
+
79
+ /**
80
+ * Called by the LSP when a page file changes on disk or in the editor.
81
+ * Keeps the route table in sync without a full rebuild.
82
+ * Callers are responsible for filtering to page URIs via isPage().
83
+ */
84
+ onPageFileChanged(uri: string, content: string): void {
85
+ this.routeTable.updateFile(uri, content);
86
+ }
87
+
88
+ /**
89
+ * Called by the LSP when a page file is deleted.
90
+ * Callers are responsible for filtering to page URIs via isPage().
91
+ */
92
+ onPageFileDeleted(uri: string): void {
93
+ this.routeTable.removeFile(uri);
94
+ }
95
+
96
+ /**
97
+ * Invalidate the cached build state so the next definition request triggers
98
+ * a full route-table rebuild. Call this after bulk file changes that bypass
99
+ * incremental updates (e.g., git checkout, branch switch, stash pop).
100
+ */
101
+ invalidate(): void {
102
+ this.builtRoots.clear();
103
+ this.routeTable = new RouteTable(this.fs);
104
+ }
105
+
106
+ async definitions(
107
+ params: DefinitionParams,
108
+ node: LiquidHtmlNode,
109
+ ancestors: LiquidHtmlNode[],
110
+ ): Promise<DefinitionLink[]> {
111
+ const sourceCode = this.documentManager.get(params.textDocument.uri);
112
+ if (!sourceCode) return [];
113
+ const doc = sourceCode.textDocument;
114
+
115
+ const resolved = this.resolveUrlContext(node, ancestors);
116
+ if (!resolved) return [];
117
+
118
+ const { urlAttr, method, element } = resolved;
119
+
120
+ // Build a variable map from {% assign %} tags that precede the current element.
121
+ // This ensures that when a variable is reassigned, each usage sees only the
122
+ // assigns that come before it in document order.
123
+ let variableMap: Map<string, string> | undefined;
124
+ if (
125
+ sourceCode.type === SourceCodeType.LiquidHtml &&
126
+ !(sourceCode.ast instanceof Error) &&
127
+ 'children' in sourceCode.ast &&
128
+ Array.isArray(sourceCode.ast.children)
129
+ ) {
130
+ variableMap = buildVariableMap(sourceCode.ast.children, element.position.start);
131
+ }
132
+
133
+ const urlPattern = extractUrlPattern(urlAttr, variableMap);
134
+ if (urlPattern === null || shouldSkipUrl(urlPattern)) return [];
135
+
136
+ const ready = await this.ensureBuilt(params.textDocument.uri);
137
+ if (!ready) return [];
138
+
139
+ const matches = this.routeTable.match(urlPattern, method);
140
+ if (matches.length === 0) return [];
141
+
142
+ const originRange = Range.create(
143
+ doc.positionAt(element.position.start),
144
+ doc.positionAt(urlAttr.value[urlAttr.value.length - 1].position.end),
145
+ );
146
+
147
+ const results: DefinitionLink[] = [];
148
+ for (const entry of matches) {
149
+ const targetRange = Range.create(0, 0, 0, 0);
150
+ results.push(LocationLink.create(entry.uri, targetRange, targetRange, originRange));
151
+ }
152
+
153
+ return results;
154
+ }
155
+
156
+ /**
157
+ * Resolve the URL context from the cursor position.
158
+ * Activates when cursor is on:
159
+ * - The tag name (e.g. `a` in `<a href="...">`)
160
+ * - The URL-bearing attribute name or value (e.g. `href` or `/about` for <a>, `action` for <form>)
161
+ * - Inside a Liquid expression within the URL attribute (e.g. `{{ url }}` inside href)
162
+ * Does NOT activate on unrelated attributes (e.g. `class` in `<a class="x" href="/about">`).
163
+ */
164
+ private resolveUrlContext(
165
+ node: LiquidHtmlNode,
166
+ ancestors: LiquidHtmlNode[],
167
+ ): { urlAttr: ValuedAttrNode; method: string; element: HtmlElement } | null {
168
+ // Case 1: node is the HtmlElement itself (cursor on tag name)
169
+ if (node.type === NodeTypes.HtmlElement) {
170
+ return this.resolveFromElement(node as HtmlElement);
171
+ }
172
+
173
+ // Case 2: node is a valued attribute — only activate if it's the URL attribute
174
+ if (isValuedAttrNode(node)) {
175
+ const element = this.findElementAncestor(ancestors);
176
+ if (!element) return null;
177
+ return this.resolveFromUrlAttr(node, element as HtmlElement);
178
+ }
179
+
180
+ // Case 3: node is inside a valued attribute (TextNode, LiquidVariableOutput, VariableLookup, etc.)
181
+ const attrAncestor = ancestors.find(isValuedAttrNode);
182
+ if (attrAncestor) {
183
+ const element = this.findElementAncestor(ancestors);
184
+ if (!element) return null;
185
+ return this.resolveFromUrlAttr(attrAncestor, element as HtmlElement);
186
+ }
187
+
188
+ // Case 4: TextNode directly under HtmlElement (e.g., tag name text)
189
+ if (node.type === NodeTypes.TextNode) {
190
+ const parentElement = ancestors[ancestors.length - 1];
191
+ if (parentElement && parentElement.type === NodeTypes.HtmlElement) {
192
+ return this.resolveFromElement(parentElement as HtmlElement);
193
+ }
194
+ }
195
+
196
+ return null;
197
+ }
198
+
199
+ /**
200
+ * Only resolve if the given attribute is the URL-bearing attribute (href/action)
201
+ * for the parent element.
202
+ */
203
+ private resolveFromUrlAttr(
204
+ attr: ValuedAttrNode,
205
+ element: HtmlElement,
206
+ ): { urlAttr: ValuedAttrNode; method: string; element: HtmlElement } | null {
207
+ const tagName = getTagName(element);
208
+ if (!tagName) return null;
209
+
210
+ const urlAttrName = TAG_URL_ATTR[tagName];
211
+ if (!urlAttrName) return null;
212
+
213
+ // Check that the cursor's attribute IS the URL attribute
214
+ const attrName = getAttrName(attr);
215
+ if (attrName !== urlAttrName) return null;
216
+
217
+ const method = this.getMethodForElement(tagName, element);
218
+ if (!method) return null;
219
+
220
+ return { urlAttr: attr, method, element };
221
+ }
222
+
223
+ private resolveFromElement(
224
+ element: HtmlElement,
225
+ ): { urlAttr: ValuedAttrNode; method: string; element: HtmlElement } | null {
226
+ const tagName = getTagName(element);
227
+ if (!tagName) return null;
228
+
229
+ const urlAttr = findUrlAttr(element, tagName);
230
+ if (!urlAttr) return null;
231
+
232
+ const method = this.getMethodForElement(tagName, element);
233
+ if (!method) return null;
234
+
235
+ return { urlAttr, method, element };
236
+ }
237
+
238
+ private findElementAncestor(ancestors: LiquidHtmlNode[]): LiquidHtmlNode | null {
239
+ for (let i = ancestors.length - 1; i >= 0; i--) {
240
+ if (ancestors[i].type === NodeTypes.HtmlElement) return ancestors[i];
241
+ }
242
+ return null;
243
+ }
244
+
245
+ private getMethodForElement(tagName: string, element: HtmlElement): string | null {
246
+ if (tagName === 'a') return 'get';
247
+ if (tagName === 'form') return getEffectiveMethod(element);
248
+ return null;
249
+ }
250
+ }
@@ -0,0 +1,191 @@
1
+ import { describe, it, expect, assert } from 'vitest';
2
+ import { toLiquidHtmlAST } from '@platformos/liquid-html-parser';
3
+ import { findCurrentNode } from '@platformos/platformos-check-common';
4
+ import { MockFileSystem } from '@platformos/platformos-check-common/src/test';
5
+ import { DocumentsLocator } from '@platformos/platformos-common';
6
+ import { DefinitionParams, Position } from 'vscode-languageserver-protocol';
7
+ import { DocumentManager } from '../../documents';
8
+ import { SearchPathsLoader } from '../../utils/searchPaths';
9
+ import { RenderPartialDefinitionProvider } from './RenderPartialDefinitionProvider';
10
+
11
+ const rootUri = 'file:///project';
12
+ const uriString = 'file:///project/app/views/pages/index.liquid';
13
+
14
+ function setup(files: Record<string, string>) {
15
+ const documentManager = new DocumentManager();
16
+ const mockFs = new MockFileSystem(files);
17
+ const provider = new RenderPartialDefinitionProvider(
18
+ documentManager,
19
+ new DocumentsLocator(mockFs),
20
+ new SearchPathsLoader(mockFs),
21
+ async () => rootUri,
22
+ );
23
+ return { documentManager, provider };
24
+ }
25
+
26
+ async function getDefinitions(source: string, cursorOffset: number, files: Record<string, string>) {
27
+ const { documentManager, provider } = setup(files);
28
+ documentManager.open(uriString, source, 1);
29
+
30
+ const ast = toLiquidHtmlAST(source);
31
+ const [node, ancestors] = findCurrentNode(ast, cursorOffset);
32
+ const params: DefinitionParams = {
33
+ textDocument: { uri: uriString },
34
+ position: Position.create(0, cursorOffset),
35
+ };
36
+
37
+ return provider.definitions(params, node, ancestors);
38
+ }
39
+
40
+ describe('RenderPartialDefinitionProvider', () => {
41
+ describe('render tag', () => {
42
+ it('should resolve render partials', async () => {
43
+ const result = await getDefinitions("{% render 'card' %}", 12, {
44
+ 'project/app/views/partials/card.liquid': 'card content',
45
+ });
46
+
47
+ expect(result).toHaveLength(1);
48
+ expect(result[0].targetUri).toBe('file:///project/app/views/partials/card.liquid');
49
+ });
50
+
51
+ it('should NOT use search paths for regular render tags', async () => {
52
+ const result = await getDefinitions("{% render 'card' %}", 12, {
53
+ 'project/app/config.yml': 'theme_search_paths:\n - theme/dress',
54
+ 'project/app/views/partials/theme/dress/card.liquid': 'dress card',
55
+ });
56
+
57
+ expect(result).toHaveLength(0);
58
+ });
59
+ });
60
+
61
+ describe('theme_render_rc tag', () => {
62
+ it('should resolve via theme_search_paths', async () => {
63
+ const result = await getDefinitions("{% theme_render_rc 'card' %}", 20, {
64
+ 'project/app/config.yml': 'theme_search_paths:\n - theme/dress\n - theme/simple',
65
+ 'project/app/views/partials/theme/dress/card.liquid': 'dress card',
66
+ });
67
+
68
+ expect(result).toHaveLength(1);
69
+ expect(result[0].targetUri).toBe(
70
+ 'file:///project/app/views/partials/theme/dress/card.liquid',
71
+ );
72
+ });
73
+
74
+ it('should fallback to standard resolution when no config exists', async () => {
75
+ const result = await getDefinitions("{% theme_render_rc 'card' %}", 20, {
76
+ 'project/app/views/partials/card.liquid': 'default card',
77
+ });
78
+
79
+ expect(result).toHaveLength(1);
80
+ expect(result[0].targetUri).toBe('file:///project/app/views/partials/card.liquid');
81
+ });
82
+
83
+ it('should resolve inside {% liquid %} blocks', async () => {
84
+ const source = `{% liquid
85
+ theme_render_rc 'components/atoms/heading', content: 'text'
86
+ %}`;
87
+ const offset = source.indexOf('components/atoms/heading');
88
+ const { documentManager, provider } = setup({
89
+ 'project/app/config.yml': "theme_search_paths:\n - ''\n - modules/components",
90
+ 'project/modules/components/public/views/partials/components/atoms/heading.liquid':
91
+ 'heading',
92
+ });
93
+ documentManager.open(uriString, source, 1);
94
+
95
+ const ast = toLiquidHtmlAST(source);
96
+ const [node, ancestors] = findCurrentNode(ast, offset);
97
+ const params: DefinitionParams = {
98
+ textDocument: { uri: uriString },
99
+ position: Position.create(1, 20),
100
+ };
101
+
102
+ const result = await provider.definitions(params, node, ancestors);
103
+
104
+ expect(result).toHaveLength(1);
105
+ expect(result[0].targetUri).toBe(
106
+ 'file:///project/modules/components/public/views/partials/components/atoms/heading.liquid',
107
+ );
108
+ });
109
+ });
110
+
111
+ describe('function tag', () => {
112
+ it('should resolve function partials', async () => {
113
+ const result = await getDefinitions("{% function result = 'commands/apply' %}", 24, {
114
+ 'project/app/lib/commands/apply.liquid': 'apply content',
115
+ });
116
+
117
+ expect(result).toHaveLength(1);
118
+ expect(result[0].targetUri).toBe('file:///project/app/lib/commands/apply.liquid');
119
+ });
120
+ });
121
+
122
+ describe('graphql tag', () => {
123
+ it('should resolve graphql references', async () => {
124
+ const result = await getDefinitions("{% graphql g = 'users/search' %}", 18, {
125
+ 'project/app/graphql/users/search.graphql': 'query { }',
126
+ });
127
+
128
+ expect(result).toHaveLength(1);
129
+ expect(result[0].targetUri).toBe('file:///project/app/graphql/users/search.graphql');
130
+ });
131
+ });
132
+
133
+ describe('non-matching nodes', () => {
134
+ it('should return empty for non-string nodes', async () => {
135
+ const result = await getDefinitions("{% assign x = 'hello' %}", 3, {});
136
+
137
+ expect(result).toHaveLength(0);
138
+ });
139
+ });
140
+
141
+ describe('search path cache invalidation', () => {
142
+ it('returns stale result when cache is not invalidated after config change', async () => {
143
+ // Regression test: SearchPathsLoader caches results across calls.
144
+ // Without explicit invalidate(), a config change is not picked up.
145
+ const files: Record<string, string> = {
146
+ 'project/app/config.yml': 'theme_search_paths:\n - theme/dress',
147
+ 'project/app/views/partials/theme/dress/card.liquid': 'dress card',
148
+ 'project/app/views/partials/theme/simple/card.liquid': 'simple card',
149
+ };
150
+ const mockFs = new MockFileSystem(files);
151
+ const searchPathsCache = new SearchPathsLoader(mockFs);
152
+ const documentManager = new DocumentManager();
153
+ const provider = new RenderPartialDefinitionProvider(
154
+ documentManager,
155
+ new DocumentsLocator(mockFs),
156
+ searchPathsCache,
157
+ async () => rootUri,
158
+ );
159
+
160
+ const source = "{% theme_render_rc 'card' %}";
161
+ const offset = source.indexOf("'card'") + 1;
162
+ documentManager.open(uriString, source, 1);
163
+
164
+ const ast = toLiquidHtmlAST(source);
165
+ const [node, ancestors] = findCurrentNode(ast, offset);
166
+ const params: DefinitionParams = {
167
+ textDocument: { uri: uriString },
168
+ position: Position.create(0, offset),
169
+ };
170
+
171
+ // First call: populates the cache with 'theme/dress'
172
+ const result1 = await provider.definitions(params, node, ancestors);
173
+ assert(result1.length === 1);
174
+ expect(result1[0].targetUri).toContain('theme/dress/card.liquid');
175
+
176
+ // Simulate config.yml change on disk (mutate mockApp in-place)
177
+ files['project/app/config.yml'] = 'theme_search_paths:\n - theme/simple';
178
+
179
+ // Without invalidation: cache still returns 'theme/dress' result
180
+ const result2 = await provider.definitions(params, node, ancestors);
181
+ assert(result2.length === 1);
182
+ expect(result2[0].targetUri).toContain('theme/dress/card.liquid');
183
+
184
+ // After invalidation: re-reads config and returns 'theme/simple' result
185
+ searchPathsCache.invalidate();
186
+ const result3 = await provider.definitions(params, node, ancestors);
187
+ assert(result3.length === 1);
188
+ expect(result3[0].targetUri).toContain('theme/simple/card.liquid');
189
+ });
190
+ });
191
+ });
@@ -0,0 +1,70 @@
1
+ import { LiquidHtmlNode, LiquidString, LiquidTag, NodeTypes } from '@platformos/liquid-html-parser';
2
+ import { DocumentsLocator, DocumentType } from '@platformos/platformos-common';
3
+ import {
4
+ DefinitionParams,
5
+ DefinitionLink,
6
+ Range,
7
+ LocationLink,
8
+ } from 'vscode-languageserver-protocol';
9
+ import { URI } from 'vscode-uri';
10
+ import { DocumentManager } from '../../documents';
11
+ import { BaseDefinitionProvider } from '../BaseDefinitionProvider';
12
+ import { SearchPathsLoader } from '../../utils/searchPaths';
13
+
14
+ const TAG_MARKUP_TYPE: Record<string, NodeTypes> = {
15
+ render: NodeTypes.RenderMarkup,
16
+ include: NodeTypes.RenderMarkup,
17
+ theme_render_rc: NodeTypes.RenderMarkup,
18
+ function: NodeTypes.FunctionMarkup,
19
+ graphql: NodeTypes.GraphQLMarkup,
20
+ };
21
+
22
+ export class RenderPartialDefinitionProvider implements BaseDefinitionProvider {
23
+ constructor(
24
+ private documentManager: DocumentManager,
25
+ private documentsLocator: DocumentsLocator,
26
+ private searchPathsCache: SearchPathsLoader,
27
+ private findAppRootURI: (uri: string) => Promise<string | null>,
28
+ ) {}
29
+
30
+ async definitions(
31
+ params: DefinitionParams,
32
+ node: LiquidHtmlNode,
33
+ ancestors: LiquidHtmlNode[],
34
+ ): Promise<DefinitionLink[]> {
35
+ if (node.type !== NodeTypes.String) return [];
36
+
37
+ const markup = ancestors.at(-1);
38
+ const tag = ancestors.at(-2);
39
+ if (!markup || !tag || tag.type !== NodeTypes.LiquidTag) return [];
40
+
41
+ const expectedMarkupType = TAG_MARKUP_TYPE[(tag as LiquidTag).name];
42
+ if (expectedMarkupType === undefined || markup.type !== expectedMarkupType) return [];
43
+
44
+ const rootUri = await this.findAppRootURI(params.textDocument.uri);
45
+ if (!rootUri) return [];
46
+
47
+ const root = URI.parse(rootUri);
48
+ const searchPaths = await this.searchPathsCache.get(root);
49
+ const docType = (tag as LiquidTag).name as DocumentType;
50
+ const fileUri = await this.documentsLocator.locate(
51
+ root,
52
+ docType,
53
+ (node as LiquidString).value,
54
+ searchPaths,
55
+ );
56
+ if (!fileUri) return [];
57
+
58
+ const sourceCode = this.documentManager.get(params.textDocument.uri);
59
+ if (!sourceCode) return [];
60
+
61
+ const doc = sourceCode.textDocument;
62
+ const originRange = Range.create(
63
+ doc.positionAt(node.position.start),
64
+ doc.positionAt(node.position.end),
65
+ );
66
+ const targetRange = Range.create(0, 0, 0, 0);
67
+
68
+ return [LocationLink.create(fileUri, targetRange, targetRange, originRange)];
69
+ }
70
+ }
@@ -6,6 +6,7 @@ import {
6
6
  Reference,
7
7
  SourceCodeType,
8
8
  } from '@platformos/platformos-check-common';
9
+ import { RouteTable } from '@platformos/platformos-common';
9
10
 
10
11
  import { DocumentManager } from '../documents';
11
12
  import { Dependencies } from '../types';
@@ -22,9 +23,11 @@ export function makeRunChecks(
22
23
  jsonValidationSet,
23
24
  appGraphManager,
24
25
  includeFilesFromDisk,
26
+ getRouteTable,
25
27
  }: Pick<Dependencies, 'fs' | 'loadConfig' | 'platformosDocset' | 'jsonValidationSet'> & {
26
28
  appGraphManager?: AppGraphManager;
27
29
  includeFilesFromDisk?: () => boolean;
30
+ getRouteTable?: () => RouteTable | undefined;
28
31
  },
29
32
  ) {
30
33
  return async function runChecks(triggerURIs: string[]): Promise<void> {
@@ -52,6 +55,7 @@ export function makeRunChecks(
52
55
  fs,
53
56
  platformosDocset,
54
57
  jsonValidationSet,
58
+ routeTable: getRouteTable?.(),
55
59
 
56
60
  async getReferences(uri: string): Promise<Reference[]> {
57
61
  if (!appGraphManager) return [];