Skip to content

Commit ccc0c95

Browse files
authored
content_for block type completion + document link (#709)
1 parent 6ab6856 commit ccc0c95

8 files changed

+116
-3
lines changed

.changeset/popular-wombats-wait.md

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@shopify/theme-language-server-common': minor
3+
---
4+
5+
Support `content_for` block type completion + document link
6+
7+
- The following code will offer completion suggestions based on public blocks
8+
within the blocks folder.
9+
```
10+
{% content_for "block", type: "█", id: "" %}
11+
```
12+
- You can navigate to a block file by clicking through the `type` parameter value
13+
within the `content_for "block"` tag.

packages/theme-language-server-common/src/completions/CompletionsProvider.ts

+4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { GetTranslationsForURI } from '../translations';
77
import { createLiquidCompletionParams } from './params';
88
import {
99
ContentForCompletionProvider,
10+
ContentForBlockTypeCompletionProvider,
1011
FilterCompletionProvider,
1112
HtmlAttributeCompletionProvider,
1213
HtmlAttributeValueCompletionProvider,
@@ -28,6 +29,7 @@ export interface CompletionProviderDependencies {
2829
getSnippetNamesForURI?: GetSnippetNamesForURI;
2930
getThemeSettingsSchemaForURI?: GetThemeSettingsSchemaForURI;
3031
getMetafieldDefinitions: (rootUri: string) => Promise<MetafieldDefinitionMap>;
32+
getThemeBlockNames?: (rootUri: string, includePrivate: boolean) => Promise<string[]>;
3133
log?: (message: string) => void;
3234
}
3335

@@ -44,6 +46,7 @@ export class CompletionsProvider {
4446
getTranslationsForURI = async () => ({}),
4547
getSnippetNamesForURI = async () => [],
4648
getThemeSettingsSchemaForURI = async () => [],
49+
getThemeBlockNames = async (_rootUri: string, _includePrivate: boolean) => [],
4750
log = () => {},
4851
}: CompletionProviderDependencies) {
4952
this.documentManager = documentManager;
@@ -57,6 +60,7 @@ export class CompletionsProvider {
5760

5861
this.providers = [
5962
new ContentForCompletionProvider(),
63+
new ContentForBlockTypeCompletionProvider(getThemeBlockNames),
6064
new HtmlTagCompletionProvider(),
6165
new HtmlAttributeCompletionProvider(documentManager),
6266
new HtmlAttributeValueCompletionProvider(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { MetafieldDefinitionMap } from '@shopify/theme-check-common';
2+
import { beforeEach, describe, expect, it } from 'vitest';
3+
import { InsertTextFormat } from 'vscode-json-languageservice';
4+
import { DocumentManager } from '../../documents';
5+
import { CompletionsProvider } from '../CompletionsProvider';
6+
7+
describe('Module: ContentForBlockTypeCompletionProvider', async () => {
8+
let provider: CompletionsProvider;
9+
10+
beforeEach(async () => {
11+
provider = new CompletionsProvider({
12+
documentManager: new DocumentManager(),
13+
themeDocset: {
14+
filters: async () => [],
15+
objects: async () => [],
16+
tags: async () => [],
17+
systemTranslations: async () => ({}),
18+
},
19+
getMetafieldDefinitions: async (_rootUri: string) => ({} as MetafieldDefinitionMap),
20+
getThemeBlockNames: async (_rootUri: string, _includePrivate: boolean) => [
21+
'block-1',
22+
'block-2',
23+
],
24+
});
25+
});
26+
27+
it('should complete content_for "block" type parameter ', async () => {
28+
const expected = ['block-1', 'block-2'].sort();
29+
await expect(provider).to.complete('{% content_for "block", type: "█" %}', expected);
30+
});
31+
32+
it('should not complete content_for "blocks" type parameter', async () => {
33+
await expect(provider).to.complete('{% content_for "blocks", type: "█" %}', []);
34+
});
35+
36+
it('should not complete content_for "block" id parameter', async () => {
37+
await expect(provider).to.complete('{% content_for "block", type: "", id: "█" %}', []);
38+
});
39+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { NodeTypes } from '@shopify/liquid-html-parser';
2+
import { CompletionItem, CompletionItemKind } from 'vscode-languageserver';
3+
import { LiquidCompletionParams } from '../params';
4+
import { Provider } from './common';
5+
6+
export class ContentForBlockTypeCompletionProvider implements Provider {
7+
constructor(
8+
private readonly getThemeBlockNames: (
9+
rootUri: string,
10+
includePrivate: boolean,
11+
) => Promise<string[]>,
12+
) {}
13+
14+
async completions(params: LiquidCompletionParams): Promise<CompletionItem[]> {
15+
if (!params.completionContext) return [];
16+
17+
const { document } = params;
18+
const doc = document.textDocument;
19+
const { node, ancestors } = params.completionContext;
20+
const parentNode = ancestors.at(-1);
21+
const grandParentNode = ancestors.at(-2);
22+
23+
if (
24+
!node ||
25+
!parentNode ||
26+
!grandParentNode ||
27+
node.type !== NodeTypes.String ||
28+
parentNode.type !== NodeTypes.NamedArgument ||
29+
parentNode.name !== 'type' ||
30+
grandParentNode.type !== NodeTypes.ContentForMarkup ||
31+
grandParentNode.contentForType.value !== 'block'
32+
) {
33+
return [];
34+
}
35+
36+
return (await this.getThemeBlockNames(doc.uri, false)).map((blockName) => ({
37+
label: blockName,
38+
kind: CompletionItemKind.EnumMember,
39+
insertText: blockName,
40+
}));
41+
}
42+
}

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

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { ContentForCompletionProvider } from './ContentForCompletionProvider';
2+
export { ContentForBlockTypeCompletionProvider } from './ContentForBlockTypeCompletionProvider';
23
export { HtmlTagCompletionProvider } from './HtmlTagCompletionProvider';
34
export { HtmlAttributeCompletionProvider } from './HtmlAttributeCompletionProvider';
45
export { HtmlAttributeValueCompletionProvider } from './HtmlAttributeValueCompletionProvider';

packages/theme-language-server-common/src/documentLinks/DocumentLinksProvider.spec.ts

+2
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ describe('DocumentLinksProvider', () => {
4242
{% echo 'echo.js' | asset_url %}
4343
{% assign x = 'assign.css' | asset_url %}
4444
{{ 'asset.js' | asset_url }}
45+
{% content_for 'block', type: 'block_name' %}
4546
`;
4647

4748
documentManager.open(uriString, liquidHtmlContent, 1);
@@ -54,6 +55,7 @@ describe('DocumentLinksProvider', () => {
5455
'file:///path/to/project/assets/echo.js',
5556
'file:///path/to/project/assets/assign.css',
5657
'file:///path/to/project/assets/asset.js',
58+
'file:///path/to/project/blocks/block_name.liquid',
5759
];
5860

5961
expect(result.length).toBe(expectedUrls.length);

packages/theme-language-server-common/src/documentLinks/DocumentLinksProvider.ts

+14-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { LiquidHtmlNode, LiquidString, NodeTypes } from '@shopify/liquid-html-parser';
1+
import { LiquidHtmlNode, LiquidString, NamedTags, NodeTypes } from '@shopify/liquid-html-parser';
22
import { SourceCodeType } from '@shopify/theme-check-common';
33
import { DocumentLink, Range } from 'vscode-languageserver';
44
import { TextDocument } from 'vscode-languageserver-textdocument';
@@ -61,6 +61,17 @@ function documentLinksVisitor(
6161
Utils.resolvePath(root, 'sections', sectionName.value + '.liquid').toString(),
6262
);
6363
}
64+
65+
// {% content_for 'block', type: 'block_name' %}
66+
if (node.name === NamedTags.content_for && typeof node.markup !== 'string') {
67+
const typeArg = node.markup.args.find((arg) => arg.name === 'type');
68+
if (typeArg && typeArg.value.type === 'String') {
69+
return DocumentLink.create(
70+
range(textDocument, typeArg.value),
71+
Utils.resolvePath(root, 'blocks', typeArg.value.value + '.liquid').toString(),
72+
);
73+
}
74+
}
6475
},
6576

6677
// {{ 'theme.js' | asset_url }}
@@ -83,8 +94,8 @@ function documentLinksVisitor(
8394
}
8495

8596
function range(textDocument: TextDocument, node: { position: LiquidHtmlNode['position'] }): Range {
86-
const start = textDocument.positionAt(node.position.start);
87-
const end = textDocument.positionAt(node.position.end);
97+
const start = textDocument.positionAt(node.position.start + 1);
98+
const end = textDocument.positionAt(node.position.end - 1);
8899
return Range.create(start, end);
89100
}
90101

packages/theme-language-server-common/src/server/startServer.ts

+1
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ export function startServer(
243243
getSnippetNamesForURI,
244244
getThemeSettingsSchemaForURI,
245245
log,
246+
getThemeBlockNames,
246247
getMetafieldDefinitions,
247248
});
248249
const hoverProvider = new HoverProvider(

0 commit comments

Comments
 (0)