From 791ddf59a8ceafbf4f036559d576d9025499aad7 Mon Sep 17 00:00:00 2001 From: Orlando Qiu Date: Wed, 22 Oct 2025 16:32:23 -0400 Subject: [PATCH] Added support for render tag completion for inline snippets --- .changeset/flat-geckos-breathe.md | 5 ++ .../RenderSnippetCompletionProvider.spec.ts | 16 +++++- .../RenderSnippetCompletionProvider.ts | 57 ++++++++++++++----- 3 files changed, 63 insertions(+), 15 deletions(-) create mode 100644 .changeset/flat-geckos-breathe.md diff --git a/.changeset/flat-geckos-breathe.md b/.changeset/flat-geckos-breathe.md new file mode 100644 index 000000000..05dd28f66 --- /dev/null +++ b/.changeset/flat-geckos-breathe.md @@ -0,0 +1,5 @@ +--- +'@shopify/theme-language-server-common': minor +--- + +Added completion support for inline snippets in the `render` tag diff --git a/packages/theme-language-server-common/src/completions/providers/RenderSnippetCompletionProvider.spec.ts b/packages/theme-language-server-common/src/completions/providers/RenderSnippetCompletionProvider.spec.ts index 828f22d2b..67b3bdfbc 100644 --- a/packages/theme-language-server-common/src/completions/providers/RenderSnippetCompletionProvider.spec.ts +++ b/packages/theme-language-server-common/src/completions/providers/RenderSnippetCompletionProvider.spec.ts @@ -24,11 +24,23 @@ describe('Module: RenderSnippetCompletionProvider', async () => { it('should complete snippets correctly', async () => { await expect(provider).to.complete('{% render "', ['product-card', 'image']); - await expect(provider).to.complete('{% render "product', [ + // VSCode handles filtering + }); + + it('should complete inline snippets', async () => { + const template = `{% snippet my-inline-snippet %} + {% echo 'hello' %} +{% endsnippet %} + +{% render m█ %}`; + + await expect(provider).to.complete(template, ['my-inline-snippet']); + // VSCode handles filtering + await expect(provider).to.complete(template, [ expect.objectContaining({ documentation: { kind: 'markdown', - value: 'snippets/product-card.liquid', + value: `Inline snippet "my-inline-snippet"`, }, }), ]); diff --git a/packages/theme-language-server-common/src/completions/providers/RenderSnippetCompletionProvider.ts b/packages/theme-language-server-common/src/completions/providers/RenderSnippetCompletionProvider.ts index 828143c98..0ff33da98 100644 --- a/packages/theme-language-server-common/src/completions/providers/RenderSnippetCompletionProvider.ts +++ b/packages/theme-language-server-common/src/completions/providers/RenderSnippetCompletionProvider.ts @@ -1,7 +1,9 @@ -import { NodeTypes } from '@shopify/liquid-html-parser'; +import { LiquidHtmlNode, LiquidTag, NodeTypes } from '@shopify/liquid-html-parser'; import { CompletionItem, CompletionItemKind } from 'vscode-languageserver'; import { LiquidCompletionParams } from '../params'; import { Provider } from './common'; +import { SourceCodeType, visit, isError } from '@shopify/theme-check-common'; +import { findInlineSnippetAncestor } from '@shopify/theme-check-common/dist/checks/utils'; export type GetSnippetNamesForURI = (uri: string) => Promise; @@ -14,21 +16,13 @@ export class RenderSnippetCompletionProvider implements Provider { const { node, ancestors } = params.completionContext; const parentNode = ancestors.at(-1); - if ( - !node || - !parentNode || - node.type !== NodeTypes.String || - parentNode.type !== NodeTypes.RenderMarkup - ) { + if (!node || !parentNode || parentNode.type !== NodeTypes.RenderMarkup) { return []; } - const options = await this.getSnippetNamesForURI(params.textDocument.uri); - const partial = node.value; - - return options - .filter((option) => option.startsWith(partial)) - .map( + if (node.type === NodeTypes.String) { + const fileSnippets = await this.getSnippetNamesForURI(params.textDocument.uri); + const fileCompletionItems = fileSnippets.map( (option: string): CompletionItem => ({ label: option, kind: CompletionItemKind.Snippet, @@ -38,5 +32,42 @@ export class RenderSnippetCompletionProvider implements Provider { }, }), ); + return fileCompletionItems; + } else if (node.type === NodeTypes.VariableLookup) { + const containingSnippet = findInlineSnippetAncestor(ancestors); + const containingSnippetName = + containingSnippet && typeof containingSnippet.markup !== 'string' + ? containingSnippet.markup.name + : null; + + const fullAst = params.document.ast; + const allInlineSnippets = isError(fullAst) ? [] : getInlineSnippetsNames(fullAst); + + const inlineSnippets = allInlineSnippets.filter((name) => name !== containingSnippetName); + + const inlineCompletionItems = inlineSnippets.map( + (option: string): CompletionItem => ({ + label: option, + kind: CompletionItemKind.Snippet, + documentation: { + kind: 'markdown', + value: `Inline snippet "${option}"`, + }, + }), + ); + return inlineCompletionItems; + } else { + return []; + } } } + +function getInlineSnippetsNames(ast: LiquidHtmlNode): string[] { + return visit(ast, { + LiquidTag(node: LiquidTag) { + if (node.name === 'snippet' && typeof node.markup !== 'string' && node.markup.name) { + return node.markup.name; + } + }, + }); +}