Skip to content

Commit 402e7be

Browse files
committed
Support code completion for inline snippet render arguments
Argument completion suggestions should be offered while rendering an inline snippet. This commit adds this functionality by selecting and displaying arguments defined in an inline snippets doc tag
1 parent 69282bb commit 402e7be

File tree

3 files changed

+163
-10
lines changed

3 files changed

+163
-10
lines changed
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+
Support code completion for inline snippet render arguments

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

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,95 @@ describe('Module: RenderSnippetParameterCompletionProvider', async () => {
8787
it('does not provide completion options if the snippet does not exist', async () => {
8888
await expect(provider).to.complete(`{% render 'fake-snippet', █ %}`, []);
8989
});
90+
91+
describe('inline snippets', () => {
92+
it('provide completion options', async () => {
93+
const content = `
94+
{% snippet example %}
95+
{% doc %}
96+
@param {string} title
97+
@param {number} count
98+
@param description
99+
{% enddoc %}
100+
<div>{{ title }} - {{ count }}</div>
101+
{% endsnippet %}
102+
103+
{% render example, █ %}
104+
`;
105+
await expect(provider).to.complete(content, ['title', 'count', 'description']);
106+
});
107+
108+
it('provide completion options and exclude already specified params', async () => {
109+
const content = `
110+
{% snippet example %}
111+
{% doc %}
112+
@param {string} title
113+
@param {number} count
114+
@param {boolean} active
115+
{% enddoc %}
116+
<div>{{ title }}</div>
117+
{% endsnippet %}
118+
119+
{% render example, title: 'foo', █ %}
120+
`;
121+
await expect(provider).to.complete(content, ['count', 'active']);
122+
});
123+
124+
it('do not provide completion options if there is no doc tag', async () => {
125+
const content = `
126+
{% snippet example %}
127+
<div>No doc block here</div>
128+
{% endsnippet %}
129+
130+
{% render example, █ %}
131+
`;
132+
await expect(provider).to.complete(content, []);
133+
});
134+
135+
it('do not provide completion options if the snippet does not exist', async () => {
136+
const content = `
137+
{% snippet example %}
138+
{% doc %}
139+
@param {string} title
140+
{% enddoc %}
141+
{% endsnippet %}
142+
143+
{% render nonexistent, █ %}
144+
`;
145+
await expect(provider).to.complete(content, []);
146+
});
147+
148+
it('provide completion options from the doc tag in the current scope', async () => {
149+
let content = `
150+
{% snippet outer %}
151+
{% doc %}
152+
@param {string} outerParam
153+
{% enddoc %}
154+
{% snippet inner %}
155+
{% doc %}
156+
@param {string} innerParam
157+
{% enddoc %}
158+
<div>{{ innerParam }}</div>
159+
{% endsnippet %}
160+
{% render inner, █ %}
161+
{% endsnippet %}
162+
`;
163+
await expect(provider).to.complete(content, ['innerParam']);
164+
content = `
165+
{% snippet outer %}
166+
{% doc %}
167+
@param {string} outerParam
168+
{% enddoc %}
169+
{% snippet inner %}
170+
{% doc %}
171+
@param {string} innerParam
172+
{% enddoc %}
173+
<div>{{ innerParam }}</div>
174+
{% endsnippet %}
175+
{% endsnippet %}
176+
{% render outer, █ %}
177+
`;
178+
await expect(provider).to.complete(content, ['outerParam']);
179+
});
180+
});
90181
});

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

Lines changed: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import { NodeTypes } from '@shopify/liquid-html-parser';
1+
import {
2+
LiquidDocParamNode,
3+
NodeTypes,
4+
LiquidHtmlNode,
5+
LiquidTag,
6+
LiquidVariableLookup,
7+
RenderMarkup,
8+
LiquidRawTag,
9+
} from '@shopify/liquid-html-parser';
210
import {
311
CompletionItem,
412
CompletionItemKind,
@@ -10,7 +18,7 @@ import {
1018
import { CURSOR, LiquidCompletionParams } from '../params';
1119
import { Provider } from './common';
1220
import { formatLiquidDocParameter, getParameterCompletionTemplate } from '../../utils/liquidDoc';
13-
import { GetDocDefinitionForURI } from '@shopify/theme-check-common';
21+
import { GetDocDefinitionForURI, LiquidDocParameter, visit } from '@shopify/theme-check-common';
1422

1523
export type GetSnippetNamesForURI = (uri: string) => Promise<string[]>;
1624

@@ -27,21 +35,25 @@ export class RenderSnippetParameterCompletionProvider implements Provider {
2735
!node ||
2836
!parentNode ||
2937
node.type !== NodeTypes.VariableLookup ||
30-
parentNode.type !== NodeTypes.RenderMarkup ||
31-
parentNode.snippet.type !== 'String'
38+
parentNode.type !== NodeTypes.RenderMarkup
3239
) {
3340
return [];
3441
}
3542

3643
const userInputStr = node.name?.replace(CURSOR, '') || '';
44+
let liquidDocParams;
3745

38-
const snippetDefinition = await this.getDocDefinitionForURI(
39-
params.textDocument.uri,
40-
'snippets',
41-
parentNode.snippet.value,
42-
);
46+
if (parentNode.snippet.type === 'String') {
47+
const snippetDefinition = await this.getDocDefinitionForURI(
48+
params.textDocument.uri,
49+
'snippets',
50+
parentNode.snippet.value,
51+
);
4352

44-
const liquidDocParams = snippetDefinition?.liquidDoc?.parameters;
53+
liquidDocParams = snippetDefinition?.liquidDoc?.parameters;
54+
} else if (parentNode.snippet.type === NodeTypes.VariableLookup) {
55+
liquidDocParams = getInlineSnippetDocParams(params, parentNode);
56+
}
4557

4658
if (!liquidDocParams) {
4759
return [];
@@ -77,3 +89,48 @@ export class RenderSnippetParameterCompletionProvider implements Provider {
7789
});
7890
}
7991
}
92+
93+
function getInlineSnippetDocParams(
94+
params: LiquidCompletionParams,
95+
parentNode: RenderMarkup,
96+
): LiquidDocParameter[] {
97+
const ast = params.document.ast;
98+
if (ast instanceof Error || ast.type !== NodeTypes.Document) return [];
99+
100+
const snippetName = (parentNode.snippet as LiquidVariableLookup).name;
101+
let snippetNode: LiquidHtmlNode | undefined;
102+
103+
visit(ast, {
104+
LiquidTag(node: LiquidTag) {
105+
if (
106+
node.name === 'snippet' &&
107+
typeof node.markup !== 'string' &&
108+
node.markup.type === NodeTypes.VariableLookup &&
109+
node.markup.name === snippetName
110+
) {
111+
snippetNode = node;
112+
}
113+
},
114+
});
115+
116+
if (!snippetNode || !('children' in snippetNode)) return [];
117+
118+
const docNode = snippetNode.children?.find(
119+
(node) => node.type === NodeTypes.LiquidRawTag && (node as LiquidRawTag).name === 'doc',
120+
) as LiquidRawTag | undefined;
121+
122+
if (!docNode) return [];
123+
124+
const bodyNodes = docNode.body.nodes as unknown as LiquidHtmlNode[];
125+
const paramNodes = bodyNodes.filter(
126+
(node): node is LiquidDocParamNode => node.type === NodeTypes.LiquidDocParamNode,
127+
);
128+
129+
return paramNodes.map((node) => ({
130+
nodeType: 'param',
131+
name: node.paramName.value,
132+
description: node.paramDescription?.value ?? null,
133+
type: node.paramType?.value ?? null,
134+
required: node.required,
135+
})) as LiquidDocParameter[];
136+
}

0 commit comments

Comments
 (0)