Skip to content

Commit bcee95e

Browse files
committed
Added support for render tag completion for inline snippets
1 parent a47fc50 commit bcee95e

File tree

6 files changed

+99
-22
lines changed

6 files changed

+99
-22
lines changed

.changeset/flat-geckos-breathe.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/theme-language-server-common': minor
3+
---
4+
5+
Added completion support for inline snippets in the render tag

.changeset/weak-weeks-fetch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/theme-check-common': minor
3+
---
4+
5+
Disable the `Undefined Object` theme check for the new `snippet` tag

packages/theme-check-common/src/checks/undefined-object/index.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,4 +459,18 @@ describe('Module: UndefinedObject', () => {
459459
assert(offenses.length == 0);
460460
expect(offenses).to.be.empty;
461461
});
462+
463+
it('should not report an offense for inline snippet names in snippet and render tags', async () => {
464+
const sourceCode = `
465+
{% snippet my_inline_snippet %}
466+
{% echo 'hello' %}
467+
{% endsnippet %}
468+
469+
{% render my_inline_snippet %}
470+
`;
471+
472+
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
473+
474+
expect(offenses).toHaveLength(0);
475+
});
462476
});

packages/theme-check-common/src/checks/undefined-object/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,10 @@ export const UndefinedObject: LiquidCheckDefinition = {
142142
const parent = last(ancestors);
143143
if (isLiquidTag(parent) && isLiquidTagCapture(parent)) return;
144144

145+
if (parent?.type === NodeTypes.RenderMarkup && parent.snippet === node) return;
146+
147+
if (isLiquidTag(parent) && parent.name === 'snippet' && parent.markup === node) return;
148+
145149
variables.push(node);
146150
},
147151

packages/theme-language-server-common/src/completions/providers/RenderSnippetCompletionProvider.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,22 @@ describe('Module: RenderSnippetCompletionProvider', async () => {
3333
}),
3434
]);
3535
});
36+
37+
it('should complete inline snippets', async () => {
38+
const template = `{% snippet my-inline-snippet %}
39+
{% echo 'hello' %}
40+
{% endsnippet %}
41+
42+
{% render m`;
43+
44+
await expect(provider).to.complete(template, ['my-inline-snippet']);
45+
await expect(provider).to.complete(template, [
46+
expect.objectContaining({
47+
documentation: {
48+
kind: 'markdown',
49+
value: `Inline snippet (defined in this file)`,
50+
},
51+
}),
52+
]);
53+
});
3654
});
Lines changed: 53 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { NodeTypes } from '@shopify/liquid-html-parser';
1+
import { LiquidHtmlNode, LiquidTag, NodeTypes } from '@shopify/liquid-html-parser';
22
import { CompletionItem, CompletionItemKind } from 'vscode-languageserver';
33
import { LiquidCompletionParams } from '../params';
44
import { Provider } from './common';
5+
import { SourceCodeType, visit } from '@shopify/theme-check-common';
56

67
export type GetSnippetNamesForURI = (uri: string) => Promise<string[]>;
78

@@ -14,29 +15,59 @@ export class RenderSnippetCompletionProvider implements Provider {
1415
const { node, ancestors } = params.completionContext;
1516
const parentNode = ancestors.at(-1);
1617

17-
if (
18-
!node ||
19-
!parentNode ||
20-
node.type !== NodeTypes.String ||
21-
parentNode.type !== NodeTypes.RenderMarkup
22-
) {
18+
if (!node || !parentNode || parentNode.type !== NodeTypes.RenderMarkup) {
2319
return [];
2420
}
2521

26-
const options = await this.getSnippetNamesForURI(params.textDocument.uri);
27-
const partial = node.value;
28-
29-
return options
30-
.filter((option) => option.startsWith(partial))
31-
.map(
32-
(option: string): CompletionItem => ({
33-
label: option,
34-
kind: CompletionItemKind.Snippet,
35-
documentation: {
36-
kind: 'markdown',
37-
value: `snippets/${option}.liquid`,
38-
},
39-
}),
40-
);
22+
let partial = '';
23+
if (node.type === NodeTypes.String) {
24+
partial = node.value;
25+
const fileSnippets = await this.getSnippetNamesForURI(params.textDocument.uri);
26+
const fileCompletionItems = fileSnippets
27+
.filter((option) => option.startsWith(partial))
28+
.map(
29+
(option: string): CompletionItem => ({
30+
label: option,
31+
kind: CompletionItemKind.Snippet,
32+
documentation: {
33+
kind: 'markdown',
34+
value: `snippets/${option}.liquid`,
35+
},
36+
}),
37+
);
38+
return fileCompletionItems;
39+
} else if (node.type === NodeTypes.VariableLookup) {
40+
partial = node.name || '';
41+
const inlineSnippets = getInlineSnippetsNames(params.completionContext.partialAst);
42+
const inlineCompletionItems = inlineSnippets
43+
.filter((option) => option.startsWith(partial))
44+
.map(
45+
(option: string): CompletionItem => ({
46+
label: option,
47+
kind: CompletionItemKind.Snippet,
48+
documentation: {
49+
kind: 'markdown',
50+
value: `Inline snippet (defined in this file)`,
51+
},
52+
}),
53+
);
54+
return inlineCompletionItems;
55+
} else {
56+
return [];
57+
}
4158
}
4259
}
60+
61+
function getInlineSnippetsNames(ast: LiquidHtmlNode): string[] {
62+
if (ast instanceof Error) return [];
63+
64+
const inlineSnippetNames = visit<SourceCodeType.LiquidHtml, string>(ast, {
65+
LiquidTag(node: LiquidTag) {
66+
if (node.name === 'snippet' && typeof node.markup !== 'string' && node.markup.name) {
67+
return node.markup.name;
68+
}
69+
},
70+
});
71+
72+
return inlineSnippetNames.filter((name): name is string => !!name);
73+
}

0 commit comments

Comments
 (0)