@platformos/platformos-language-server-common 0.0.11 → 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.
- package/CHANGELOG.md +43 -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 +117 -60
- 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/codeActions/providers/GenerateDocBlockProvider.d.ts +10 -0
- package/dist/codeActions/providers/GenerateDocBlockProvider.js +39 -0
- package/dist/codeActions/providers/GenerateDocBlockProvider.js.map +1 -0
- package/dist/commands/providers/GenerateDocBlockCommandProvider.d.ts +19 -0
- package/dist/commands/providers/GenerateDocBlockCommandProvider.js +64 -0
- package/dist/commands/providers/GenerateDocBlockCommandProvider.js.map +1 -0
- 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/params/LiquidCompletionParams.js +7 -2
- package/dist/completions/params/LiquidCompletionParams.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 +16 -20
- package/dist/documentLinks/DocumentLinksProvider.js.map +1 -1
- package/dist/documents/DocumentManager.js +0 -4
- package/dist/documents/DocumentManager.js.map +1 -1
- package/dist/documents/types.d.ts +1 -20
- package/dist/documents/types.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/json/completions/providers/BlockTypeCompletionProvider.d.ts +4 -3
- package/dist/json/completions/providers/BlockTypeCompletionProvider.js +5 -10
- package/dist/json/completions/providers/BlockTypeCompletionProvider.js.map +1 -1
- package/dist/json/hover/providers/BlockSettingsHoverProvider.js +5 -10
- package/dist/json/hover/providers/BlockSettingsHoverProvider.js.map +1 -1
- package/dist/json/schemaSettings.d.ts +3 -3
- package/dist/json/schemaSettings.js.map +1 -1
- package/dist/renamed/handlers/BlockRenameHandler.d.ts +3 -3
- package/dist/renamed/handlers/BlockRenameHandler.js +86 -15
- package/dist/renamed/handlers/BlockRenameHandler.js.map +1 -1
- package/dist/renamed/handlers/SectionRenameHandler.d.ts +3 -3
- package/dist/renamed/handlers/SectionRenameHandler.js +3 -3
- package/dist/renamed/handlers/SectionRenameHandler.js.map +1 -1
- 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/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 +132 -63
- package/src/VariableShapeExtractor.ts +2 -4
- package/src/completions/CompletionsProvider.ts +11 -0
- package/src/completions/params/LiquidCompletionParams.ts +7 -2
- package/src/completions/providers/GraphQLFieldCompletionProvider.ts +78 -0
- package/src/definitions/DefinitionProvider.ts +52 -0
- package/src/definitions/providers/PageRouteDefinitionProvider.spec.ts +753 -0
- package/src/definitions/providers/PageRouteDefinitionProvider.ts +250 -0
- package/src/definitions/providers/RenderPartialDefinitionProvider.spec.ts +191 -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 +23 -32
- package/src/documents/DocumentManager.ts +0 -4
- package/src/documents/types.ts +1 -21
- 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.ts +85 -5
- 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.13",
|
|
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.12",
|
|
31
|
+
"@platformos/platformos-check-common": "0.0.13",
|
|
32
|
+
"@platformos/platformos-graph": "0.0.13",
|
|
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) {
|
|
@@ -868,12 +837,16 @@ async function buildSymbolsTable(
|
|
|
868
837
|
}
|
|
869
838
|
}
|
|
870
839
|
|
|
840
|
+
// For hash/array targets like function hash['key'] = 'partial', skip type tracking
|
|
841
|
+
const fnVarName = markup.name.name;
|
|
842
|
+
if (!fnVarName || markup.name.lookups.length > 0) return;
|
|
843
|
+
|
|
871
844
|
// Track shape for potential hash_assign merging
|
|
872
845
|
if (returnType && isShapeType(returnType)) {
|
|
873
|
-
if (!variableShapes.has(
|
|
874
|
-
variableShapes.set(
|
|
846
|
+
if (!variableShapes.has(fnVarName)) {
|
|
847
|
+
variableShapes.set(fnVarName, []);
|
|
875
848
|
}
|
|
876
|
-
variableShapes.get(
|
|
849
|
+
variableShapes.get(fnVarName)!.push({
|
|
877
850
|
shape: returnType.shape,
|
|
878
851
|
rangeEnd: node.position.end,
|
|
879
852
|
});
|
|
@@ -881,7 +854,7 @@ async function buildSymbolsTable(
|
|
|
881
854
|
|
|
882
855
|
// Always add function variable to symbolsTable, using Untyped as fallback
|
|
883
856
|
return {
|
|
884
|
-
identifier:
|
|
857
|
+
identifier: fnVarName,
|
|
885
858
|
type: returnType ?? Untyped,
|
|
886
859
|
range: [node.position.end],
|
|
887
860
|
};
|
|
@@ -1568,7 +1541,11 @@ async function inferFunctionReturnType(
|
|
|
1568
1541
|
|
|
1569
1542
|
await visit<SourceCodeType.LiquidHtml, void>(partialAst, {
|
|
1570
1543
|
async LiquidTag(node) {
|
|
1571
|
-
if (
|
|
1544
|
+
if (
|
|
1545
|
+
node.name === NamedTags.return &&
|
|
1546
|
+
node.markup !== null &&
|
|
1547
|
+
typeof node.markup !== 'string'
|
|
1548
|
+
) {
|
|
1572
1549
|
// markup is LiquidVariable - infer its type
|
|
1573
1550
|
const type = inferType(node.markup, partialSymbolsTable, objectMap, filtersMap);
|
|
1574
1551
|
// Flatten union types into individual types
|
|
@@ -1738,6 +1715,98 @@ function mergeNestedPropertyIntoShape(
|
|
|
1738
1715
|
return { kind: 'object', properties: newProperties };
|
|
1739
1716
|
}
|
|
1740
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
|
+
|
|
1741
1810
|
/**
|
|
1742
1811
|
* Find the last applicable shape for a variable at a given position
|
|
1743
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 [];
|
|
@@ -228,7 +228,9 @@ function findCurrentNode(
|
|
|
228
228
|
(isInLiquidLiquidTagContext(finder) && isCovered(cursor, current.blockStartPosition))
|
|
229
229
|
) {
|
|
230
230
|
if (hasNonNullProperty(current, 'markup') && typeof current.markup !== 'string') {
|
|
231
|
-
finder.current = Array.isArray(current.markup)
|
|
231
|
+
finder.current = Array.isArray(current.markup)
|
|
232
|
+
? current.markup.at(-1)
|
|
233
|
+
: (current.markup ?? undefined);
|
|
232
234
|
} else {
|
|
233
235
|
// Exits the loop and the node is the thing to complete
|
|
234
236
|
// (presumably name or something else)
|
|
@@ -244,7 +246,9 @@ function findCurrentNode(
|
|
|
244
246
|
|
|
245
247
|
case NodeTypes.LiquidBranch:
|
|
246
248
|
if (isCovered(cursor, current.blockStartPosition) && typeof current.markup !== 'string') {
|
|
247
|
-
finder.current = Array.isArray(current.markup)
|
|
249
|
+
finder.current = Array.isArray(current.markup)
|
|
250
|
+
? current.markup.at(-1)
|
|
251
|
+
: (current.markup ?? undefined);
|
|
248
252
|
} else if (hasNonEmptyArrayProperty(current, 'children')) {
|
|
249
253
|
finder.current = last(current.children);
|
|
250
254
|
} else {
|
|
@@ -280,6 +284,7 @@ function findCurrentNode(
|
|
|
280
284
|
case NodeTypes.YAMLFrontmatter:
|
|
281
285
|
case NodeTypes.HtmlDoctype:
|
|
282
286
|
case NodeTypes.HtmlComment:
|
|
287
|
+
case NodeTypes.HtmlProcessingInstruction:
|
|
283
288
|
case NodeTypes.RawMarkup: {
|
|
284
289
|
break;
|
|
285
290
|
}
|
|
@@ -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
|
+
}
|