@platformos/platformos-language-server-common 0.0.12 → 0.0.16
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/CHANGELOG.md +67 -0
- package/dist/PropertyShapeInference.d.ts +5 -0
- package/dist/PropertyShapeInference.js +28 -0
- package/dist/PropertyShapeInference.js.map +1 -1
- package/dist/TypeSystem.js +106 -55
- package/dist/TypeSystem.js.map +1 -1
- package/dist/VariableShapeExtractor.js +2 -4
- package/dist/VariableShapeExtractor.js.map +1 -1
- package/dist/codeActions/CodeActionsProvider.d.ts +1 -1
- package/dist/completions/CompletionsProvider.d.ts +1 -0
- package/dist/completions/CompletionsProvider.js +6 -0
- package/dist/completions/CompletionsProvider.js.map +1 -1
- package/dist/completions/providers/GraphQLFieldCompletionProvider.d.ts +12 -0
- package/dist/completions/providers/GraphQLFieldCompletionProvider.js +75 -0
- package/dist/completions/providers/GraphQLFieldCompletionProvider.js.map +1 -0
- package/dist/definitions/DefinitionProvider.d.ts +19 -1
- package/dist/definitions/DefinitionProvider.js +33 -1
- package/dist/definitions/DefinitionProvider.js.map +1 -1
- package/dist/definitions/providers/PageRouteDefinitionProvider.d.ts +51 -0
- package/dist/definitions/providers/PageRouteDefinitionProvider.js +204 -0
- package/dist/definitions/providers/PageRouteDefinitionProvider.js.map +1 -0
- package/dist/definitions/providers/RenderPartialDefinitionProvider.d.ts +14 -0
- package/dist/definitions/providers/RenderPartialDefinitionProvider.js +50 -0
- package/dist/definitions/providers/RenderPartialDefinitionProvider.js.map +1 -0
- package/dist/diagnostics/runChecks.d.ts +3 -1
- package/dist/diagnostics/runChecks.js +2 -1
- package/dist/diagnostics/runChecks.js.map +1 -1
- package/dist/documentLinks/DocumentLinksProvider.d.ts +3 -1
- package/dist/documentLinks/DocumentLinksProvider.js +18 -20
- package/dist/documentLinks/DocumentLinksProvider.js.map +1 -1
- package/dist/hover/HoverProvider.d.ts +1 -0
- package/dist/hover/HoverProvider.js +6 -0
- package/dist/hover/HoverProvider.js.map +1 -1
- package/dist/hover/providers/GraphQLFieldHoverProvider.d.ts +12 -0
- package/dist/hover/providers/GraphQLFieldHoverProvider.js +85 -0
- package/dist/hover/providers/GraphQLFieldHoverProvider.js.map +1 -0
- package/dist/server/AppGraphManager.js +3 -4
- package/dist/server/AppGraphManager.js.map +1 -1
- package/dist/server/CachedFileSystem.js +7 -2
- package/dist/server/CachedFileSystem.js.map +1 -1
- package/dist/server/startServer.js +71 -5
- package/dist/server/startServer.js.map +1 -1
- package/dist/test/MockConnection.d.ts +1 -1
- package/dist/test/MockConnection.js +11 -4
- package/dist/test/MockConnection.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/searchPaths.d.ts +21 -0
- package/dist/utils/searchPaths.js +34 -0
- package/dist/utils/searchPaths.js.map +1 -0
- package/package.json +6 -4
- package/src/PropertyShapeInference.spec.ts +44 -0
- package/src/PropertyShapeInference.ts +27 -0
- package/src/TypeSystem.spec.ts +59 -0
- package/src/TypeSystem.ts +119 -58
- package/src/VariableShapeExtractor.ts +2 -4
- package/src/completions/CompletionsProvider.ts +11 -0
- package/src/completions/providers/GraphQLFieldCompletionProvider.ts +78 -0
- package/src/definitions/DefinitionProvider.ts +52 -0
- package/src/definitions/providers/PageRouteDefinitionProvider.spec.ts +776 -0
- package/src/definitions/providers/PageRouteDefinitionProvider.ts +250 -0
- package/src/definitions/providers/RenderPartialDefinitionProvider.spec.ts +226 -0
- package/src/definitions/providers/RenderPartialDefinitionProvider.ts +70 -0
- package/src/diagnostics/runChecks.ts +4 -0
- package/src/documentLinks/DocumentLinksProvider.spec.ts +131 -10
- package/src/documentLinks/DocumentLinksProvider.ts +27 -35
- package/src/hover/HoverProvider.ts +11 -0
- package/src/hover/providers/GraphQLFieldHoverProvider.spec.ts +124 -0
- package/src/hover/providers/GraphQLFieldHoverProvider.ts +90 -0
- package/src/server/AppGraphManager.ts +3 -5
- package/src/server/CachedFileSystem.ts +11 -2
- package/src/server/startServer.spec.ts +62 -0
- package/src/server/startServer.ts +85 -5
- package/src/test/MockConnection.ts +16 -4
- package/src/utils/searchPaths.spec.ts +101 -0
- package/src/utils/searchPaths.ts +32 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { AbstractFileSystem } from '@platformos/platformos-common';
|
|
2
|
+
import { URI } from 'vscode-uri';
|
|
3
|
+
/**
|
|
4
|
+
* Cached loader for theme_search_paths from app/config.yml.
|
|
5
|
+
*
|
|
6
|
+
* Caches per root URI so that repeated calls (document links, definitions,
|
|
7
|
+
* checks) within the same editor session don't re-read and re-parse the
|
|
8
|
+
* config file on every invocation.
|
|
9
|
+
*
|
|
10
|
+
* Call `invalidate()` when app/config.yml changes on disk — the file watcher
|
|
11
|
+
* in startServer.ts handles this. This is the only reliable invalidation
|
|
12
|
+
* point because config.yml changes may come from external tools (vim, CLI)
|
|
13
|
+
* that don't trigger editor-level change events.
|
|
14
|
+
*/
|
|
15
|
+
export declare class SearchPathsLoader {
|
|
16
|
+
private fs;
|
|
17
|
+
private cache;
|
|
18
|
+
constructor(fs: AbstractFileSystem);
|
|
19
|
+
get(rootUri: URI): Promise<string[] | null>;
|
|
20
|
+
invalidate(): void;
|
|
21
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SearchPathsLoader = void 0;
|
|
4
|
+
const platformos_common_1 = require("@platformos/platformos-common");
|
|
5
|
+
/**
|
|
6
|
+
* Cached loader for theme_search_paths from app/config.yml.
|
|
7
|
+
*
|
|
8
|
+
* Caches per root URI so that repeated calls (document links, definitions,
|
|
9
|
+
* checks) within the same editor session don't re-read and re-parse the
|
|
10
|
+
* config file on every invocation.
|
|
11
|
+
*
|
|
12
|
+
* Call `invalidate()` when app/config.yml changes on disk — the file watcher
|
|
13
|
+
* in startServer.ts handles this. This is the only reliable invalidation
|
|
14
|
+
* point because config.yml changes may come from external tools (vim, CLI)
|
|
15
|
+
* that don't trigger editor-level change events.
|
|
16
|
+
*/
|
|
17
|
+
class SearchPathsLoader {
|
|
18
|
+
constructor(fs) {
|
|
19
|
+
this.fs = fs;
|
|
20
|
+
this.cache = new Map();
|
|
21
|
+
}
|
|
22
|
+
get(rootUri) {
|
|
23
|
+
const key = rootUri.toString();
|
|
24
|
+
if (!this.cache.has(key)) {
|
|
25
|
+
this.cache.set(key, (0, platformos_common_1.loadSearchPaths)(this.fs, rootUri));
|
|
26
|
+
}
|
|
27
|
+
return this.cache.get(key);
|
|
28
|
+
}
|
|
29
|
+
invalidate() {
|
|
30
|
+
this.cache.clear();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
exports.SearchPathsLoader = SearchPathsLoader;
|
|
34
|
+
//# sourceMappingURL=searchPaths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"searchPaths.js","sourceRoot":"","sources":["../../src/utils/searchPaths.ts"],"names":[],"mappings":";;;AAAA,qEAAoF;AAGpF;;;;;;;;;;;GAWG;AACH,MAAa,iBAAiB;IAG5B,YAAoB,EAAsB;QAAtB,OAAE,GAAF,EAAE,CAAoB;QAFlC,UAAK,GAAG,IAAI,GAAG,EAAoC,CAAC;IAEf,CAAC;IAE9C,GAAG,CAAC,OAAY;QACd,MAAM,GAAG,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;QAC/B,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YACzB,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,IAAA,mCAAe,EAAC,IAAI,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC,CAAC;QACzD,CAAC;QACD,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAE,CAAC;IAC9B,CAAC;IAED,UAAU;QACR,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;IACrB,CAAC;CACF;AAhBD,8CAgBC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformos/platformos-language-server-common",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.16",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"author": "platformOS",
|
|
@@ -27,10 +27,12 @@
|
|
|
27
27
|
"type-check": "tsc --noEmit -p ./tsconfig.json"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@platformos/liquid-html-parser": "^0.0.
|
|
31
|
-
"@platformos/platformos-check-common": "0.0.
|
|
32
|
-
"@platformos/platformos-graph": "0.0.
|
|
30
|
+
"@platformos/liquid-html-parser": "^0.0.15",
|
|
31
|
+
"@platformos/platformos-check-common": "0.0.16",
|
|
32
|
+
"@platformos/platformos-graph": "0.0.16",
|
|
33
33
|
"@vscode/web-custom-data": "^0.4.6",
|
|
34
|
+
"graphql": "^16.12.0",
|
|
35
|
+
"graphql-language-service": "^5.2.2",
|
|
34
36
|
"vscode-json-languageservice": "^5.7.1",
|
|
35
37
|
"vscode-languageserver": "^9.0.1",
|
|
36
38
|
"vscode-css-languageservice": "^6.3.9",
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { shapeToJSONPlaceholder } from './PropertyShapeInference';
|
|
3
|
+
import type { PropertyShape } from './PropertyShapeInference';
|
|
4
|
+
|
|
5
|
+
describe('shapeToJSONPlaceholder', () => {
|
|
6
|
+
it('returns "null" for undefined', () => {
|
|
7
|
+
expect(shapeToJSONPlaceholder(undefined)).toBe('null');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('returns "" for string primitive', () => {
|
|
11
|
+
const shape: PropertyShape = { kind: 'primitive', primitiveType: 'string' };
|
|
12
|
+
expect(shapeToJSONPlaceholder(shape)).toBe('""');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('returns "0" for number primitive', () => {
|
|
16
|
+
const shape: PropertyShape = { kind: 'primitive', primitiveType: 'number' };
|
|
17
|
+
expect(shapeToJSONPlaceholder(shape)).toBe('0');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('returns "true" for boolean primitive', () => {
|
|
21
|
+
const shape: PropertyShape = { kind: 'primitive', primitiveType: 'boolean' };
|
|
22
|
+
expect(shapeToJSONPlaceholder(shape)).toBe('true');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('returns "null" for null primitive', () => {
|
|
26
|
+
const shape: PropertyShape = { kind: 'primitive', primitiveType: 'null' };
|
|
27
|
+
expect(shapeToJSONPlaceholder(shape)).toBe('null');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('returns "null" for untyped primitive', () => {
|
|
31
|
+
const shape: PropertyShape = { kind: 'primitive' };
|
|
32
|
+
expect(shapeToJSONPlaceholder(shape)).toBe('null');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('returns "[]" for array shape', () => {
|
|
36
|
+
const shape: PropertyShape = { kind: 'array' };
|
|
37
|
+
expect(shapeToJSONPlaceholder(shape)).toBe('[]');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('returns "{}" for object shape', () => {
|
|
41
|
+
const shape: PropertyShape = { kind: 'object', properties: new Map() };
|
|
42
|
+
expect(shapeToJSONPlaceholder(shape)).toBe('{}');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -472,3 +472,30 @@ export function mergePropertyIntoShape(
|
|
|
472
472
|
newProperties.set(key, valueShape);
|
|
473
473
|
return { kind: 'object', properties: newProperties };
|
|
474
474
|
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Map a PropertyShape to a JSON literal placeholder string suitable for
|
|
478
|
+
* substituting a {{ expr | json }} expression during static analysis.
|
|
479
|
+
*/
|
|
480
|
+
export function shapeToJSONPlaceholder(shape: PropertyShape | undefined): string {
|
|
481
|
+
if (!shape) return 'null';
|
|
482
|
+
switch (shape.kind) {
|
|
483
|
+
case 'primitive':
|
|
484
|
+
switch (shape.primitiveType) {
|
|
485
|
+
case 'string':
|
|
486
|
+
return '""';
|
|
487
|
+
case 'number':
|
|
488
|
+
return '0';
|
|
489
|
+
case 'boolean':
|
|
490
|
+
return 'true';
|
|
491
|
+
default:
|
|
492
|
+
return 'null';
|
|
493
|
+
}
|
|
494
|
+
case 'array':
|
|
495
|
+
return '[]';
|
|
496
|
+
case 'object':
|
|
497
|
+
return '{}';
|
|
498
|
+
default:
|
|
499
|
+
return 'null';
|
|
500
|
+
}
|
|
501
|
+
}
|
package/src/TypeSystem.spec.ts
CHANGED
|
@@ -901,4 +901,63 @@ query {
|
|
|
901
901
|
}
|
|
902
902
|
});
|
|
903
903
|
});
|
|
904
|
+
|
|
905
|
+
describe('parse_json block with {{ expr | json }} children', () => {
|
|
906
|
+
it('infers object shape from static JSON in block form (baseline)', async () => {
|
|
907
|
+
// Existing behaviour: static JSON works fine
|
|
908
|
+
const ast = toLiquidHtmlAST(
|
|
909
|
+
`{% parse_json data %}{"id": 1, "name": "hello"}{% endparse_json %}{{ data }}`,
|
|
910
|
+
);
|
|
911
|
+
const output = ast.children[1];
|
|
912
|
+
assert(isLiquidVariableOutput(output));
|
|
913
|
+
const inferredType = await typeSystem.inferType(output.markup, ast, 'file:///file.liquid');
|
|
914
|
+
assert(typeof inferredType !== 'string' && inferredType.kind === 'shape');
|
|
915
|
+
expect(inferredType.shape.properties?.has('id')).toBe(true);
|
|
916
|
+
expect(inferredType.shape.properties?.has('name')).toBe(true);
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
it('infers object shape when values are {{ expr | json }} — unresolvable falls back to null', async () => {
|
|
920
|
+
// object.unknown_var is not in the docset, so shape is unresolvable → null placeholder
|
|
921
|
+
const ast = toLiquidHtmlAST(
|
|
922
|
+
`{% parse_json data %}\n{"id": {{ object.unknown_var | json }}, "name": {{ object.unknown_var2 | json }}}\n{% endparse_json %}{{ data }}`,
|
|
923
|
+
);
|
|
924
|
+
const output = ast.children[1];
|
|
925
|
+
assert(isLiquidVariableOutput(output));
|
|
926
|
+
const inferredType = await typeSystem.inferType(output.markup, ast, 'file:///file.liquid');
|
|
927
|
+
// Keys must be discovered even when types are unknown
|
|
928
|
+
assert(typeof inferredType !== 'string' && inferredType.kind === 'shape');
|
|
929
|
+
expect(inferredType.shape.properties?.has('id')).toBe(true);
|
|
930
|
+
expect(inferredType.shape.properties?.has('name')).toBe(true);
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
it('infers correct value types when the expression resolves via the type system', async () => {
|
|
934
|
+
// user is established as a ShapeType by the first parse_json block
|
|
935
|
+
const ast = toLiquidHtmlAST(
|
|
936
|
+
`{% parse_json user %}{"name": "John", "age": 30}{% endparse_json %}\n{% parse_json data %}\n{"username": {{ user.name | json }}, "years": {{ user.age | json }}}\n{% endparse_json %}{{ data }}`,
|
|
937
|
+
);
|
|
938
|
+
const output = ast.children[2];
|
|
939
|
+
assert(isLiquidVariableOutput(output));
|
|
940
|
+
const inferredType = await typeSystem.inferType(output.markup, ast, 'file:///file.liquid');
|
|
941
|
+
assert(typeof inferredType !== 'string' && inferredType.kind === 'shape');
|
|
942
|
+
const usernameShape = inferredType.shape.properties?.get('username');
|
|
943
|
+
expect(usernameShape?.kind).toBe('primitive');
|
|
944
|
+
expect(usernameShape?.primitiveType).toBe('string');
|
|
945
|
+
const yearsShape = inferredType.shape.properties?.get('years');
|
|
946
|
+
expect(yearsShape?.kind).toBe('primitive');
|
|
947
|
+
expect(yearsShape?.primitiveType).toBe('number');
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
it('falls back gracefully when no | json filter present on LiquidVariableOutput', async () => {
|
|
951
|
+
// Without | json, fall back to null for that key; key still discoverable via TextNode context
|
|
952
|
+
const ast = toLiquidHtmlAST(
|
|
953
|
+
`{% parse_json data %}\n{"title": "{{ object.title }}"}\n{% endparse_json %}{{ data }}`,
|
|
954
|
+
);
|
|
955
|
+
const output = ast.children[1];
|
|
956
|
+
assert(isLiquidVariableOutput(output));
|
|
957
|
+
const inferredType = await typeSystem.inferType(output.markup, ast, 'file:///file.liquid');
|
|
958
|
+
// key must still be discovered (the TextNodes provide `"title": "` and `"`)
|
|
959
|
+
assert(typeof inferredType !== 'string' && inferredType.kind === 'shape');
|
|
960
|
+
expect(inferredType.shape.properties?.has('title')).toBe(true);
|
|
961
|
+
});
|
|
962
|
+
});
|
|
904
963
|
});
|
package/src/TypeSystem.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
LiquidTagIncrement,
|
|
12
12
|
LiquidVariable,
|
|
13
13
|
LiquidVariableLookup,
|
|
14
|
+
LiquidVariableOutput,
|
|
14
15
|
NamedTags,
|
|
15
16
|
NodeTypes,
|
|
16
17
|
TextNode,
|
|
@@ -28,8 +29,6 @@ import {
|
|
|
28
29
|
ReturnType,
|
|
29
30
|
SourceCodeType,
|
|
30
31
|
PlatformOSDocset,
|
|
31
|
-
isError,
|
|
32
|
-
parseJSON,
|
|
33
32
|
path,
|
|
34
33
|
BasicParamTypes,
|
|
35
34
|
getValidParamTypes,
|
|
@@ -42,10 +41,9 @@ import {
|
|
|
42
41
|
inferShapeFromJSONString,
|
|
43
42
|
inferShapeFromJsonLiteral,
|
|
44
43
|
inferShapeFromGraphQL,
|
|
45
|
-
lookupPropertyPath,
|
|
46
44
|
mergeShapes,
|
|
47
45
|
shapeToTypeString,
|
|
48
|
-
|
|
46
|
+
shapeToJSONPlaceholder,
|
|
49
47
|
} from './PropertyShapeInference';
|
|
50
48
|
import { AbstractFileSystem, DocumentsLocator } from '@platformos/platformos-common';
|
|
51
49
|
import { URI } from 'vscode-uri';
|
|
@@ -396,58 +394,12 @@ async function buildSymbolsTable(
|
|
|
396
394
|
let valueShape: PropertyShape | undefined;
|
|
397
395
|
|
|
398
396
|
// Resolver for variable references inside JSON literals
|
|
399
|
-
const resolveExpr = (
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
? findLastApplicableShape(shapes, node.position.start)
|
|
406
|
-
: undefined;
|
|
407
|
-
let shape = shapeEntry?.shape;
|
|
408
|
-
|
|
409
|
-
// Fall back to seedSymbolsTable
|
|
410
|
-
if (!shape && seedSymbolsTable[expr.name]) {
|
|
411
|
-
for (const tr of seedSymbolsTable[expr.name]) {
|
|
412
|
-
if (tr.range[0] < node.position.start) {
|
|
413
|
-
if (typeof tr.type !== 'string' && tr.type.kind === 'shape') {
|
|
414
|
-
shape = tr.type.shape;
|
|
415
|
-
} else if (typeof tr.type === 'string' || tr.type.kind === 'array') {
|
|
416
|
-
shape = resolvedTypeToShape(tr.type);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
if (!shape) return undefined;
|
|
423
|
-
|
|
424
|
-
// Resolve lookups (e.g. a.x or a["key"])
|
|
425
|
-
for (const lookup of expr.lookups) {
|
|
426
|
-
if (shape.kind === 'object' && lookup.type === NodeTypes.String && shape.properties) {
|
|
427
|
-
const prop = shape.properties.get(lookup.value);
|
|
428
|
-
if (prop) {
|
|
429
|
-
shape = prop;
|
|
430
|
-
} else {
|
|
431
|
-
return undefined;
|
|
432
|
-
}
|
|
433
|
-
} else if (shape.kind === 'array') {
|
|
434
|
-
if (
|
|
435
|
-
lookup.type === NodeTypes.Number ||
|
|
436
|
-
(lookup.type === NodeTypes.String &&
|
|
437
|
-
(lookup.value === 'first' || lookup.value === 'last'))
|
|
438
|
-
) {
|
|
439
|
-
shape = shape.itemShape;
|
|
440
|
-
if (!shape) return undefined;
|
|
441
|
-
} else {
|
|
442
|
-
return undefined;
|
|
443
|
-
}
|
|
444
|
-
} else {
|
|
445
|
-
return undefined;
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
return shape;
|
|
450
|
-
};
|
|
397
|
+
const resolveExpr = resolveExpressionShape(
|
|
398
|
+
node.position.start,
|
|
399
|
+
variableShapes,
|
|
400
|
+
seedSymbolsTable,
|
|
401
|
+
objectMap,
|
|
402
|
+
);
|
|
451
403
|
|
|
452
404
|
// Case 1: JSON literal expression
|
|
453
405
|
if (
|
|
@@ -649,9 +601,26 @@ async function buildSymbolsTable(
|
|
|
649
601
|
else if (isLiquidTagParseJson(node)) {
|
|
650
602
|
const variableName = node.markup.name;
|
|
651
603
|
if (variableName && node.children) {
|
|
604
|
+
const resolveExprAtTag = resolveExpressionShape(
|
|
605
|
+
node.position.start,
|
|
606
|
+
variableShapes,
|
|
607
|
+
seedSymbolsTable,
|
|
608
|
+
objectMap,
|
|
609
|
+
);
|
|
652
610
|
const textContent = node.children
|
|
653
|
-
.
|
|
654
|
-
|
|
611
|
+
.map((child) => {
|
|
612
|
+
if (child.type === NodeTypes.TextNode) return (child as TextNode).value;
|
|
613
|
+
if (child.type === NodeTypes.LiquidVariableOutput) {
|
|
614
|
+
const variable = (child as LiquidVariableOutput).markup;
|
|
615
|
+
if (typeof variable === 'string') return 'null';
|
|
616
|
+
const lastFilter = variable.filters?.at(-1);
|
|
617
|
+
if (lastFilter?.name === 'json') {
|
|
618
|
+
return shapeToJSONPlaceholder(resolveExprAtTag(variable.expression));
|
|
619
|
+
}
|
|
620
|
+
return 'null';
|
|
621
|
+
}
|
|
622
|
+
return 'null';
|
|
623
|
+
})
|
|
655
624
|
.join('');
|
|
656
625
|
const shape = inferShapeFromJSONString(textContent);
|
|
657
626
|
if (shape) {
|
|
@@ -1746,6 +1715,98 @@ function mergeNestedPropertyIntoShape(
|
|
|
1746
1715
|
return { kind: 'object', properties: newProperties };
|
|
1747
1716
|
}
|
|
1748
1717
|
|
|
1718
|
+
/**
|
|
1719
|
+
* Resolve a LiquidExpression to a PropertyShape using accumulated variable shapes,
|
|
1720
|
+
* the seed symbols table, and optionally the docset object map.
|
|
1721
|
+
*
|
|
1722
|
+
* Returns a resolver function that takes a LiquidExpression and returns a PropertyShape
|
|
1723
|
+
* or undefined.
|
|
1724
|
+
*/
|
|
1725
|
+
function resolveExpressionShape(
|
|
1726
|
+
atPosition: number,
|
|
1727
|
+
variableShapes: Map<string, { shape: PropertyShape; rangeEnd: number }[]>,
|
|
1728
|
+
seedSymbolsTable: Record<string, TypeRange[]>,
|
|
1729
|
+
objectMap?: Record<string, ObjectEntry>,
|
|
1730
|
+
): (expr: LiquidExpression | ComplexLiquidExpression) => PropertyShape | undefined {
|
|
1731
|
+
return (expr) => {
|
|
1732
|
+
if (expr.type !== NodeTypes.VariableLookup || !expr.name) return undefined;
|
|
1733
|
+
|
|
1734
|
+
// Look up the variable's shape from already-processed assignments
|
|
1735
|
+
const shapes = variableShapes.get(expr.name);
|
|
1736
|
+
const shapeEntry = shapes ? findLastApplicableShape(shapes, atPosition) : undefined;
|
|
1737
|
+
let shape: PropertyShape | undefined = shapeEntry?.shape;
|
|
1738
|
+
|
|
1739
|
+
// Track the current PseudoType (docset object name) for objectMap-based property resolution
|
|
1740
|
+
let pseudoType: string | undefined;
|
|
1741
|
+
|
|
1742
|
+
// Fall back to seedSymbolsTable
|
|
1743
|
+
if (!shape && !pseudoType && seedSymbolsTable[expr.name]) {
|
|
1744
|
+
for (const tr of seedSymbolsTable[expr.name]) {
|
|
1745
|
+
if (tr.range[0] < atPosition) {
|
|
1746
|
+
if (typeof tr.type !== 'string' && tr.type.kind === 'shape') {
|
|
1747
|
+
shape = tr.type.shape;
|
|
1748
|
+
} else if (typeof tr.type === 'string') {
|
|
1749
|
+
pseudoType = tr.type;
|
|
1750
|
+
shape = resolvedTypeToShape(tr.type);
|
|
1751
|
+
} else if (tr.type.kind === 'array') {
|
|
1752
|
+
shape = resolvedTypeToShape(tr.type);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
if (!shape && !pseudoType) return undefined;
|
|
1759
|
+
|
|
1760
|
+
// Resolve lookups (e.g. a.x or a["key"])
|
|
1761
|
+
for (const lookup of expr.lookups) {
|
|
1762
|
+
// PseudoType resolution via objectMap (e.g. context.current_user where context is a docset type)
|
|
1763
|
+
if (pseudoType !== undefined && objectMap) {
|
|
1764
|
+
if (lookup.type !== NodeTypes.String) return undefined;
|
|
1765
|
+
const propType = inferPseudoTypePropertyType(pseudoType, lookup, objectMap);
|
|
1766
|
+
// inferPseudoTypePropertyType returns PseudoType | ArrayType (string or { kind: 'array' })
|
|
1767
|
+
if (typeof propType === 'string') {
|
|
1768
|
+
pseudoType = propType;
|
|
1769
|
+
shape = resolvedTypeToShape(propType);
|
|
1770
|
+
} else if (isArrayType(propType)) {
|
|
1771
|
+
pseudoType = undefined;
|
|
1772
|
+
shape = resolvedTypeToShape(propType);
|
|
1773
|
+
} else {
|
|
1774
|
+
return undefined;
|
|
1775
|
+
}
|
|
1776
|
+
continue;
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
if (!shape) return undefined;
|
|
1780
|
+
|
|
1781
|
+
if (shape.kind === 'object' && lookup.type === NodeTypes.String && shape.properties) {
|
|
1782
|
+
const prop = shape.properties.get(lookup.value);
|
|
1783
|
+
if (prop) {
|
|
1784
|
+
pseudoType = undefined;
|
|
1785
|
+
shape = prop;
|
|
1786
|
+
} else {
|
|
1787
|
+
return undefined;
|
|
1788
|
+
}
|
|
1789
|
+
} else if (shape.kind === 'array') {
|
|
1790
|
+
if (
|
|
1791
|
+
lookup.type === NodeTypes.Number ||
|
|
1792
|
+
(lookup.type === NodeTypes.String &&
|
|
1793
|
+
(lookup.value === 'first' || lookup.value === 'last'))
|
|
1794
|
+
) {
|
|
1795
|
+
pseudoType = undefined;
|
|
1796
|
+
shape = shape.itemShape;
|
|
1797
|
+
if (!shape) return undefined;
|
|
1798
|
+
} else {
|
|
1799
|
+
return undefined;
|
|
1800
|
+
}
|
|
1801
|
+
} else {
|
|
1802
|
+
return undefined;
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
return shape;
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1749
1810
|
/**
|
|
1750
1811
|
* Find the last applicable shape for a variable at a given position
|
|
1751
1812
|
*/
|
|
@@ -107,8 +107,7 @@ export async function extractVariableShapes(
|
|
|
107
107
|
closeShapeRange(variableName, node.position.end);
|
|
108
108
|
|
|
109
109
|
const textContent = node.children
|
|
110
|
-
.
|
|
111
|
-
.map((c) => c.value)
|
|
110
|
+
.map((c) => (c.type === NodeTypes.TextNode ? (c as TextNode).value : 'null'))
|
|
112
111
|
.join('');
|
|
113
112
|
const shape = inferShapeFromJSONString(textContent);
|
|
114
113
|
if (shape) {
|
|
@@ -129,8 +128,7 @@ export async function extractVariableShapes(
|
|
|
129
128
|
|
|
130
129
|
if (node.children) {
|
|
131
130
|
const textContent = node.children
|
|
132
|
-
.
|
|
133
|
-
.map((c) => c.value)
|
|
131
|
+
.map((c) => (c.type === NodeTypes.TextNode ? (c as TextNode).value : 'null'))
|
|
134
132
|
.join('');
|
|
135
133
|
const shape = inferShapeFromGraphQL(textContent, graphqlSchema);
|
|
136
134
|
if (shape) {
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
TranslationCompletionProvider,
|
|
28
28
|
} from './providers';
|
|
29
29
|
import { GetPartialNamesForURI } from './providers/PartialCompletionProvider';
|
|
30
|
+
import { GraphQLFieldCompletionProvider } from './providers/GraphQLFieldCompletionProvider';
|
|
30
31
|
|
|
31
32
|
export interface CompletionProviderDependencies {
|
|
32
33
|
documentManager: DocumentManager;
|
|
@@ -47,6 +48,7 @@ export interface CompletionProviderDependencies {
|
|
|
47
48
|
|
|
48
49
|
export class CompletionsProvider {
|
|
49
50
|
private providers: Provider[] = [];
|
|
51
|
+
private graphqlFieldCompletionProvider: GraphQLFieldCompletionProvider;
|
|
50
52
|
readonly documentManager: DocumentManager;
|
|
51
53
|
readonly platformosDocset: PlatformOSDocset;
|
|
52
54
|
readonly log: (message: string) => void;
|
|
@@ -66,6 +68,10 @@ export class CompletionsProvider {
|
|
|
66
68
|
this.platformosDocset = platformosDocset;
|
|
67
69
|
this.log = log;
|
|
68
70
|
const typeSystem = new TypeSystem(platformosDocset, fs, documentsLocator, findAppRootURI);
|
|
71
|
+
this.graphqlFieldCompletionProvider = new GraphQLFieldCompletionProvider(
|
|
72
|
+
platformosDocset,
|
|
73
|
+
documentManager,
|
|
74
|
+
);
|
|
69
75
|
|
|
70
76
|
this.providers = [
|
|
71
77
|
new HtmlTagCompletionProvider(),
|
|
@@ -88,6 +94,11 @@ export class CompletionsProvider {
|
|
|
88
94
|
const uri = params.textDocument.uri;
|
|
89
95
|
const document = this.documentManager.get(uri);
|
|
90
96
|
|
|
97
|
+
// GraphQL files get dedicated completion support
|
|
98
|
+
if (document?.type === SourceCodeType.GraphQL) {
|
|
99
|
+
return this.graphqlFieldCompletionProvider.completions(params);
|
|
100
|
+
}
|
|
101
|
+
|
|
91
102
|
// Supports only Liquid resources
|
|
92
103
|
if (document?.type !== SourceCodeType.LiquidHtml) {
|
|
93
104
|
return [];
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { CompletionItem, CompletionItemKind, CompletionParams } from 'vscode-languageserver';
|
|
2
|
+
import { DocumentManager } from '../../documents';
|
|
3
|
+
import { PlatformOSDocset } from '@platformos/platformos-check-common';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* graphql-language-service and graphql must be loaded dynamically to avoid
|
|
7
|
+
* the "Cannot use GraphQLList from another module or realm" error that occurs
|
|
8
|
+
* when vitest transforms the graphql ESM module into separate instances.
|
|
9
|
+
*/
|
|
10
|
+
let _glsMod: typeof import('graphql-language-service') | undefined;
|
|
11
|
+
function getGLS(): typeof import('graphql-language-service') {
|
|
12
|
+
if (!_glsMod) _glsMod = require('graphql-language-service');
|
|
13
|
+
return _glsMod!;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let _graphqlMod: typeof import('graphql') | undefined;
|
|
17
|
+
function getGraphQL(): typeof import('graphql') {
|
|
18
|
+
if (!_graphqlMod) _graphqlMod = require('graphql');
|
|
19
|
+
return _graphqlMod!;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class GraphQLFieldCompletionProvider {
|
|
23
|
+
private schemaCache: any;
|
|
24
|
+
private schemaLoaded = false;
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
private platformosDocset: PlatformOSDocset,
|
|
28
|
+
private documentManager: DocumentManager,
|
|
29
|
+
) {}
|
|
30
|
+
|
|
31
|
+
private async getSchema(): Promise<any> {
|
|
32
|
+
if (!this.schemaLoaded) {
|
|
33
|
+
const sdl = await this.platformosDocset.graphQL();
|
|
34
|
+
if (sdl) {
|
|
35
|
+
try {
|
|
36
|
+
this.schemaCache = getGraphQL().buildSchema(sdl);
|
|
37
|
+
} catch {
|
|
38
|
+
// Invalid schema SDL
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
this.schemaLoaded = true;
|
|
42
|
+
}
|
|
43
|
+
return this.schemaCache;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async completions(params: CompletionParams): Promise<CompletionItem[]> {
|
|
47
|
+
const uri = params.textDocument.uri;
|
|
48
|
+
const document = this.documentManager.get(uri);
|
|
49
|
+
if (!document) return [];
|
|
50
|
+
|
|
51
|
+
const schema = await this.getSchema();
|
|
52
|
+
if (!schema) return [];
|
|
53
|
+
|
|
54
|
+
const content = document.textDocument.getText();
|
|
55
|
+
const gls = getGLS();
|
|
56
|
+
const position = new gls.Position(params.position.line, params.position.character);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const suggestions = gls.getAutocompleteSuggestions(schema, content, position);
|
|
60
|
+
|
|
61
|
+
return suggestions.map((suggestion) => ({
|
|
62
|
+
label: suggestion.label,
|
|
63
|
+
kind: toCompletionItemKind(suggestion.kind),
|
|
64
|
+
detail: suggestion.detail ?? undefined,
|
|
65
|
+
documentation: suggestion.documentation ?? undefined,
|
|
66
|
+
}));
|
|
67
|
+
} catch {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function toCompletionItemKind(kind: number | undefined): CompletionItemKind {
|
|
74
|
+
if (kind !== undefined && kind >= 1 && kind <= 25) {
|
|
75
|
+
return kind as CompletionItemKind;
|
|
76
|
+
}
|
|
77
|
+
return CompletionItemKind.Field;
|
|
78
|
+
}
|
|
@@ -1,20 +1,72 @@
|
|
|
1
1
|
import { findCurrentNode, SourceCodeType } from '@platformos/platformos-check-common';
|
|
2
|
+
import { AbstractFileSystem, DocumentsLocator, RouteTable } from '@platformos/platformos-common';
|
|
2
3
|
import { DefinitionLink, DefinitionParams } from 'vscode-languageserver';
|
|
3
4
|
|
|
4
5
|
import { AugmentedJsonSourceCode, DocumentManager } from '../documents';
|
|
6
|
+
import { SearchPathsLoader } from '../utils/searchPaths';
|
|
5
7
|
import { BaseDefinitionProvider } from './BaseDefinitionProvider';
|
|
8
|
+
import { PageRouteDefinitionProvider } from './providers/PageRouteDefinitionProvider';
|
|
9
|
+
import { RenderPartialDefinitionProvider } from './providers/RenderPartialDefinitionProvider';
|
|
6
10
|
import { TranslationStringDefinitionProvider } from './providers/TranslationStringDefinitionProvider';
|
|
7
11
|
|
|
8
12
|
export class DefinitionProvider {
|
|
9
13
|
private providers: BaseDefinitionProvider[];
|
|
14
|
+
private pageRouteProvider?: PageRouteDefinitionProvider;
|
|
10
15
|
|
|
11
16
|
constructor(
|
|
12
17
|
private documentManager: DocumentManager,
|
|
13
18
|
getDefaultLocaleSourceCode: (uri: string) => Promise<AugmentedJsonSourceCode | null>,
|
|
19
|
+
fs?: AbstractFileSystem,
|
|
20
|
+
findAppRootURI?: (uri: string) => Promise<string | null>,
|
|
21
|
+
documentsLocator?: DocumentsLocator,
|
|
22
|
+
searchPathsCache?: SearchPathsLoader,
|
|
14
23
|
) {
|
|
15
24
|
this.providers = [
|
|
16
25
|
new TranslationStringDefinitionProvider(documentManager, getDefaultLocaleSourceCode),
|
|
17
26
|
];
|
|
27
|
+
|
|
28
|
+
if (fs && findAppRootURI) {
|
|
29
|
+
this.pageRouteProvider = new PageRouteDefinitionProvider(documentManager, fs, findAppRootURI);
|
|
30
|
+
this.providers.push(this.pageRouteProvider);
|
|
31
|
+
|
|
32
|
+
if (documentsLocator && searchPathsCache) {
|
|
33
|
+
this.providers.push(
|
|
34
|
+
new RenderPartialDefinitionProvider(
|
|
35
|
+
documentManager,
|
|
36
|
+
documentsLocator,
|
|
37
|
+
searchPathsCache,
|
|
38
|
+
findAppRootURI,
|
|
39
|
+
),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Notify the route table that a page file was created or changed. */
|
|
46
|
+
onPageFileChanged(uri: string, content: string): void {
|
|
47
|
+
this.pageRouteProvider?.onPageFileChanged(uri, content);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Notify the route table that a page file was deleted. */
|
|
51
|
+
onPageFileDeleted(uri: string): void {
|
|
52
|
+
this.pageRouteProvider?.onPageFileDeleted(uri);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Invalidate the route table so it will be fully rebuilt on next use.
|
|
57
|
+
* Call after bulk filesystem changes (e.g., git checkout, branch switch).
|
|
58
|
+
*/
|
|
59
|
+
invalidateRouteTable(): void {
|
|
60
|
+
this.pageRouteProvider?.invalidate();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Returns the shared RouteTable, or undefined if route support is not configured.
|
|
65
|
+
* When undefined (no fs/findAppRootURI), the check pipeline will build a fresh
|
|
66
|
+
* RouteTable per run via makeGetRouteTable in context-utils.ts.
|
|
67
|
+
*/
|
|
68
|
+
getRouteTable(): RouteTable | undefined {
|
|
69
|
+
return this.pageRouteProvider?.getRouteTable();
|
|
18
70
|
}
|
|
19
71
|
|
|
20
72
|
async definitions(params: DefinitionParams): Promise<DefinitionLink[] | null> {
|