diff --git a/.changeset/eleven-melons-compete.md b/.changeset/eleven-melons-compete.md
new file mode 100644
index 000000000..8947d456f
--- /dev/null
+++ b/.changeset/eleven-melons-compete.md
@@ -0,0 +1,5 @@
+---
+'@shopify/theme-language-server-common': minor
+---
+
+Support code completion for inline snippet render arguments
diff --git a/packages/theme-language-server-common/src/completions/providers/RenderSnippetParameterCompletionProvider.spec.ts b/packages/theme-language-server-common/src/completions/providers/RenderSnippetParameterCompletionProvider.spec.ts
index 447be6101..85f644eb4 100644
--- a/packages/theme-language-server-common/src/completions/providers/RenderSnippetParameterCompletionProvider.spec.ts
+++ b/packages/theme-language-server-common/src/completions/providers/RenderSnippetParameterCompletionProvider.spec.ts
@@ -87,4 +87,95 @@ describe('Module: RenderSnippetParameterCompletionProvider', async () => {
it('does not provide completion options if the snippet does not exist', async () => {
await expect(provider).to.complete(`{% render 'fake-snippet', █ %}`, []);
});
+
+ describe('inline snippets', () => {
+ it('provide completion options', async () => {
+ const content = `
+ {% snippet example %}
+ {% doc %}
+ @param {string} title
+ @param {number} count
+ @param description
+ {% enddoc %}
+
{{ title }} - {{ count }}
+ {% endsnippet %}
+
+ {% render example, █ %}
+ `;
+ await expect(provider).to.complete(content, ['title', 'count', 'description']);
+ });
+
+ it('provide completion options and exclude already specified params', async () => {
+ const content = `
+ {% snippet example %}
+ {% doc %}
+ @param {string} title
+ @param {number} count
+ @param {boolean} active
+ {% enddoc %}
+ {{ title }}
+ {% endsnippet %}
+
+ {% render example, title: 'foo', █ %}
+ `;
+ await expect(provider).to.complete(content, ['count', 'active']);
+ });
+
+ it('do not provide completion options if there is no doc tag', async () => {
+ const content = `
+ {% snippet example %}
+ No doc block here
+ {% endsnippet %}
+
+ {% render example, █ %}
+ `;
+ await expect(provider).to.complete(content, []);
+ });
+
+ it('do not provide completion options if the snippet does not exist', async () => {
+ const content = `
+ {% snippet example %}
+ {% doc %}
+ @param {string} title
+ {% enddoc %}
+ {% endsnippet %}
+
+ {% render nonexistent, █ %}
+ `;
+ await expect(provider).to.complete(content, []);
+ });
+
+ it('provide completion options from the doc tag in the current scope', async () => {
+ let content = `
+ {% snippet outer %}
+ {% doc %}
+ @param {string} outerParam
+ {% enddoc %}
+ {% snippet inner %}
+ {% doc %}
+ @param {string} innerParam
+ {% enddoc %}
+ {{ innerParam }}
+ {% endsnippet %}
+ {% render inner, █ %}
+ {% endsnippet %}
+ `;
+ await expect(provider).to.complete(content, ['innerParam']);
+ content = `
+ {% snippet outer %}
+ {% doc %}
+ @param {string} outerParam
+ {% enddoc %}
+ {% snippet inner %}
+ {% doc %}
+ @param {string} innerParam
+ {% enddoc %}
+ {{ innerParam }}
+ {% endsnippet %}
+ {% endsnippet %}
+ {% render outer, █ %}
+ `;
+ await expect(provider).to.complete(content, ['outerParam']);
+ });
+ });
});
diff --git a/packages/theme-language-server-common/src/completions/providers/RenderSnippetParameterCompletionProvider.ts b/packages/theme-language-server-common/src/completions/providers/RenderSnippetParameterCompletionProvider.ts
index b12197c58..691087c1e 100644
--- a/packages/theme-language-server-common/src/completions/providers/RenderSnippetParameterCompletionProvider.ts
+++ b/packages/theme-language-server-common/src/completions/providers/RenderSnippetParameterCompletionProvider.ts
@@ -9,7 +9,11 @@ import {
} from 'vscode-languageserver';
import { CURSOR, LiquidCompletionParams } from '../params';
import { Provider } from './common';
-import { formatLiquidDocParameter, getParameterCompletionTemplate } from '../../utils/liquidDoc';
+import {
+ formatLiquidDocParameter,
+ getInlineSnippetDocParams,
+ getParameterCompletionTemplate,
+} from '../../utils/liquidDoc';
import { GetDocDefinitionForURI } from '@shopify/theme-check-common';
export type GetSnippetNamesForURI = (uri: string) => Promise;
@@ -27,21 +31,25 @@ export class RenderSnippetParameterCompletionProvider implements Provider {
!node ||
!parentNode ||
node.type !== NodeTypes.VariableLookup ||
- parentNode.type !== NodeTypes.RenderMarkup ||
- parentNode.snippet.type !== 'String'
+ parentNode.type !== NodeTypes.RenderMarkup
) {
return [];
}
const userInputStr = node.name?.replace(CURSOR, '') || '';
+ let liquidDocParams;
- const snippetDefinition = await this.getDocDefinitionForURI(
- params.textDocument.uri,
- 'snippets',
- parentNode.snippet.value,
- );
+ if (parentNode.snippet.type === 'String') {
+ const snippetDefinition = await this.getDocDefinitionForURI(
+ params.textDocument.uri,
+ 'snippets',
+ parentNode.snippet.value,
+ );
- const liquidDocParams = snippetDefinition?.liquidDoc?.parameters;
+ liquidDocParams = snippetDefinition?.liquidDoc?.parameters;
+ } else if (parentNode.snippet.type === NodeTypes.VariableLookup && parentNode.snippet.name) {
+ liquidDocParams = getInlineSnippetDocParams(params.document.ast, parentNode.snippet.name);
+ }
if (!liquidDocParams) {
return [];
diff --git a/packages/theme-language-server-common/src/utils/liquidDoc.ts b/packages/theme-language-server-common/src/utils/liquidDoc.ts
index 66c62b235..ac90db013 100644
--- a/packages/theme-language-server-common/src/utils/liquidDoc.ts
+++ b/packages/theme-language-server-common/src/utils/liquidDoc.ts
@@ -4,7 +4,16 @@ import {
getDefaultValueForType,
LiquidDocParameter,
SupportedDocTagTypes,
+ visit,
} from '@shopify/theme-check-common';
+import {
+ LiquidDocParamNode,
+ LiquidHtmlNode,
+ LiquidRawTag,
+ LiquidTag,
+ LiquidTagSnippet,
+ NodeTypes,
+} from '@shopify/liquid-html-parser';
export function formatLiquidDocParameter(
{ name, type, description, required }: LiquidDocParameter,
@@ -100,3 +109,47 @@ export function formatLiquidDocContentMarkdown(
return parts.join('\n');
}
+
+export function getInlineSnippetDocParams(
+ ast: LiquidHtmlNode | Error,
+ snippetName: string,
+): LiquidDocParameter[] {
+ if (ast instanceof Error || ast.type !== NodeTypes.Document) return [];
+
+ let snippetNode: LiquidTagSnippet | undefined;
+
+ visit(ast, {
+ LiquidTag(node: LiquidTag) {
+ if (
+ node.name === 'snippet' &&
+ typeof node.markup !== 'string' &&
+ node.markup.type === NodeTypes.VariableLookup &&
+ node.markup.name === snippetName
+ ) {
+ snippetNode = node as LiquidTagSnippet;
+ }
+ },
+ });
+
+ if (!snippetNode?.children) return [];
+
+ const docNode = snippetNode.children.find(
+ (node): node is LiquidRawTag => node.type === NodeTypes.LiquidRawTag && node.name === 'doc',
+ );
+
+ if (!docNode) return [];
+
+ const paramNodes = (docNode.body.nodes as LiquidHtmlNode[]).filter(
+ (node): node is LiquidDocParamNode => node.type === NodeTypes.LiquidDocParamNode,
+ );
+
+ return paramNodes.map(
+ (node): LiquidDocParameter => ({
+ nodeType: 'param',
+ name: node.paramName.value,
+ description: node.paramDescription?.value ?? null,
+ type: node.paramType?.value ?? null,
+ required: node.required,
+ }),
+ );
+}