Skip to content

Commit 50e2939

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 50e2939

4 files changed

Lines changed: 166 additions & 9 deletions

File tree

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: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import {
99
} from 'vscode-languageserver';
1010
import { CURSOR, LiquidCompletionParams } from '../params';
1111
import { Provider } from './common';
12-
import { formatLiquidDocParameter, getParameterCompletionTemplate } from '../../utils/liquidDoc';
12+
import {
13+
formatLiquidDocParameter,
14+
getInlineSnippetDocParams,
15+
getParameterCompletionTemplate,
16+
} from '../../utils/liquidDoc';
1317
import { GetDocDefinitionForURI } from '@shopify/theme-check-common';
1418

1519
export type GetSnippetNamesForURI = (uri: string) => Promise<string[]>;
@@ -27,21 +31,25 @@ export class RenderSnippetParameterCompletionProvider implements Provider {
2731
!node ||
2832
!parentNode ||
2933
node.type !== NodeTypes.VariableLookup ||
30-
parentNode.type !== NodeTypes.RenderMarkup ||
31-
parentNode.snippet.type !== 'String'
34+
parentNode.type !== NodeTypes.RenderMarkup
3235
) {
3336
return [];
3437
}
3538

3639
const userInputStr = node.name?.replace(CURSOR, '') || '';
40+
let liquidDocParams;
3741

38-
const snippetDefinition = await this.getDocDefinitionForURI(
39-
params.textDocument.uri,
40-
'snippets',
41-
parentNode.snippet.value,
42-
);
42+
if (parentNode.snippet.type === 'String') {
43+
const snippetDefinition = await this.getDocDefinitionForURI(
44+
params.textDocument.uri,
45+
'snippets',
46+
parentNode.snippet.value,
47+
);
4348

44-
const liquidDocParams = snippetDefinition?.liquidDoc?.parameters;
49+
liquidDocParams = snippetDefinition?.liquidDoc?.parameters;
50+
} else if (parentNode.snippet.type === NodeTypes.VariableLookup && parentNode.snippet.name) {
51+
liquidDocParams = getInlineSnippetDocParams(params.document.ast, parentNode.snippet.name);
52+
}
4553

4654
if (!liquidDocParams) {
4755
return [];

packages/theme-language-server-common/src/utils/liquidDoc.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,16 @@ import {
44
getDefaultValueForType,
55
LiquidDocParameter,
66
SupportedDocTagTypes,
7+
visit,
78
} from '@shopify/theme-check-common';
9+
import {
10+
LiquidDocParamNode,
11+
LiquidHtmlNode,
12+
LiquidRawTag,
13+
LiquidTag,
14+
LiquidTagSnippet,
15+
NodeTypes,
16+
} from '@shopify/liquid-html-parser';
817

918
export function formatLiquidDocParameter(
1019
{ name, type, description, required }: LiquidDocParameter,
@@ -100,3 +109,47 @@ export function formatLiquidDocContentMarkdown(
100109

101110
return parts.join('\n');
102111
}
112+
113+
export function getInlineSnippetDocParams(
114+
ast: LiquidHtmlNode | Error,
115+
snippetName: string,
116+
): LiquidDocParameter[] {
117+
if (ast instanceof Error || ast.type !== NodeTypes.Document) return [];
118+
119+
let snippetNode: LiquidTagSnippet | undefined;
120+
121+
visit(ast, {
122+
LiquidTag(node: LiquidTag) {
123+
if (
124+
node.name === 'snippet' &&
125+
typeof node.markup !== 'string' &&
126+
node.markup.type === NodeTypes.VariableLookup &&
127+
node.markup.name === snippetName
128+
) {
129+
snippetNode = node as LiquidTagSnippet;
130+
}
131+
},
132+
});
133+
134+
if (!snippetNode?.children) return [];
135+
136+
const docNode = snippetNode.children.find(
137+
(node): node is LiquidRawTag => node.type === NodeTypes.LiquidRawTag && node.name === 'doc',
138+
);
139+
140+
if (!docNode) return [];
141+
142+
const paramNodes = (docNode.body.nodes as LiquidHtmlNode[]).filter(
143+
(node): node is LiquidDocParamNode => node.type === NodeTypes.LiquidDocParamNode,
144+
);
145+
146+
return paramNodes.map(
147+
(node): LiquidDocParameter => ({
148+
nodeType: 'param',
149+
name: node.paramName.value,
150+
description: node.paramDescription?.value ?? null,
151+
type: node.paramType?.value ?? null,
152+
required: node.required,
153+
}),
154+
);
155+
}

0 commit comments

Comments
 (0)