Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/eleven-melons-compete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/theme-language-server-common': minor
---

Support code completion for inline snippet render arguments
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
<div>{{ title }} - {{ count }}</div>
{% 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 %}
<div>{{ title }}</div>
{% 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 %}
<div>No doc block here</div>
{% 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 %}
<div>{{ innerParam }}</div>
{% 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 %}
<div>{{ innerParam }}</div>
{% endsnippet %}
{% endsnippet %}
{% render outer, █ %}
`;
await expect(provider).to.complete(content, ['outerParam']);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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<string[]>;
Expand All @@ -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 [];
Expand Down
53 changes: 53 additions & 0 deletions packages/theme-language-server-common/src/utils/liquidDoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
}),
);
}