Skip to content

Commit

Permalink
Support content_for block type completion + document link
Browse files Browse the repository at this point in the history
  • Loading branch information
aswamy committed Jan 14, 2025
1 parent 4a429e1 commit b16e3ec
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 3 deletions.
13 changes: 13 additions & 0 deletions .changeset/popular-wombats-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@shopify/theme-language-server-common': minor
---

Support `content_for` block type completion + document link

- The following code will offer completion suggestions based on public blocks
within the blocks folder.
```
{% content_for "block", type: "█", id: "" %}
```
- You can navigate to a block file by clicking through the `type` parameter value
within the `content_for "block"` tag.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { GetTranslationsForURI } from '../translations';
import { createLiquidCompletionParams } from './params';
import {
ContentForCompletionProvider,
ContentForBlockTypeCompletionProvider,
FilterCompletionProvider,
HtmlAttributeCompletionProvider,
HtmlAttributeValueCompletionProvider,
Expand All @@ -28,6 +29,7 @@ export interface CompletionProviderDependencies {
getSnippetNamesForURI?: GetSnippetNamesForURI;
getThemeSettingsSchemaForURI?: GetThemeSettingsSchemaForURI;
getMetafieldDefinitions: (rootUri: string) => Promise<MetafieldDefinitionMap>;
getThemeBlockNames?: (rootUri: string, includePrivate: boolean) => Promise<string[]>;
log?: (message: string) => void;
}

Expand All @@ -44,6 +46,7 @@ export class CompletionsProvider {
getTranslationsForURI = async () => ({}),
getSnippetNamesForURI = async () => [],
getThemeSettingsSchemaForURI = async () => [],
getThemeBlockNames = async (_rootUri: string, _includePrivate: boolean) => [],
log = () => {},
}: CompletionProviderDependencies) {
this.documentManager = documentManager;
Expand All @@ -57,6 +60,7 @@ export class CompletionsProvider {

this.providers = [
new ContentForCompletionProvider(),
new ContentForBlockTypeCompletionProvider(getThemeBlockNames),
new HtmlTagCompletionProvider(),
new HtmlAttributeCompletionProvider(documentManager),
new HtmlAttributeValueCompletionProvider(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { MetafieldDefinitionMap } from '@shopify/theme-check-common';
import { beforeEach, describe, expect, it } from 'vitest';
import { InsertTextFormat } from 'vscode-json-languageservice';
import { DocumentManager } from '../../documents';
import { CompletionsProvider } from '../CompletionsProvider';

describe('Module: ContentForBlockTypeCompletionProvider', async () => {
let provider: CompletionsProvider;

beforeEach(async () => {
provider = new CompletionsProvider({
documentManager: new DocumentManager(),
themeDocset: {
filters: async () => [],
objects: async () => [],
tags: async () => [],
systemTranslations: async () => ({}),
},
getMetafieldDefinitions: async (_rootUri: string) => ({} as MetafieldDefinitionMap),
getThemeBlockNames: async (_rootUri: string, _includePrivate: boolean) => [
'block-1',
'block-2',
],
});
});

it('should complete content_for "block" type parameter ', async () => {
const expected = ['block-1', 'block-2'].sort();
await expect(provider).to.complete('{% content_for "block", type: "█" %}', expected);
});

it('should not complete content_for "blocks" type parameter', async () => {
await expect(provider).to.complete('{% content_for "blocks", type: "█" %}', []);
});

it('should not complete content_for "block" id parameter', async () => {
await expect(provider).to.complete('{% content_for "block", type: "", id: "█" %}', []);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { NodeTypes } from '@shopify/liquid-html-parser';
import { CompletionItem, CompletionItemKind } from 'vscode-languageserver';
import { LiquidCompletionParams } from '../params';
import { Provider } from './common';

export class ContentForBlockTypeCompletionProvider implements Provider {
constructor(
private readonly getThemeBlockNames: (
rootUri: string,
includePrivate: boolean,
) => Promise<string[]>,
) {}

async completions(params: LiquidCompletionParams): Promise<CompletionItem[]> {
if (!params.completionContext) return [];

const { document } = params;
const doc = document.textDocument;
const { node, ancestors } = params.completionContext;
const parentNode = ancestors.at(-1);
const grandParentNode = ancestors.at(-2);

if (
!node ||
!parentNode ||
!grandParentNode ||
node.type !== NodeTypes.String ||
parentNode.type !== NodeTypes.NamedArgument ||
parentNode.name !== 'type' ||
grandParentNode.type !== NodeTypes.ContentForMarkup ||
grandParentNode.contentForType.value !== 'block'
) {
return [];
}

return (await this.getThemeBlockNames(doc.uri, false)).map((blockName) => ({
label: blockName,
kind: CompletionItemKind.Keyword,
insertText: blockName,
}));
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { ContentForCompletionProvider } from './ContentForCompletionProvider';
export { ContentForBlockTypeCompletionProvider } from './ContentForBlockTypeCompletionProvider';
export { HtmlTagCompletionProvider } from './HtmlTagCompletionProvider';
export { HtmlAttributeCompletionProvider } from './HtmlAttributeCompletionProvider';
export { HtmlAttributeValueCompletionProvider } from './HtmlAttributeValueCompletionProvider';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ describe('DocumentLinksProvider', () => {
{% echo 'echo.js' | asset_url %}
{% assign x = 'assign.css' | asset_url %}
{{ 'asset.js' | asset_url }}
{% content_for 'block', type: 'block_name' %}
`;

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

expect(result.length).toBe(expectedUrls.length);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LiquidHtmlNode, LiquidString, NodeTypes } from '@shopify/liquid-html-parser';
import { LiquidHtmlNode, LiquidString, NamedTags, NodeTypes } from '@shopify/liquid-html-parser';
import { SourceCodeType } from '@shopify/theme-check-common';
import { DocumentLink, Range } from 'vscode-languageserver';
import { TextDocument } from 'vscode-languageserver-textdocument';
Expand Down Expand Up @@ -61,6 +61,17 @@ function documentLinksVisitor(
Utils.resolvePath(root, 'sections', sectionName.value + '.liquid').toString(),
);
}

// {% content_for 'block', type: 'block_name' %}
if (node.name === NamedTags.content_for && typeof node.markup !== 'string') {
const typeArg = node.markup.args.find((arg) => arg.name === 'type');
if (typeArg && typeArg.value.type === 'String') {
return DocumentLink.create(
range(textDocument, typeArg.value),
Utils.resolvePath(root, 'blocks', typeArg.value.value + '.liquid').toString(),
);
}
}
},

// {{ 'theme.js' | asset_url }}
Expand All @@ -83,8 +94,8 @@ function documentLinksVisitor(
}

function range(textDocument: TextDocument, node: { position: LiquidHtmlNode['position'] }): Range {
const start = textDocument.positionAt(node.position.start);
const end = textDocument.positionAt(node.position.end);
const start = textDocument.positionAt(node.position.start + 1);
const end = textDocument.positionAt(node.position.end - 1);
return Range.create(start, end);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ export function startServer(
getSnippetNamesForURI,
getThemeSettingsSchemaForURI,
log,
getThemeBlockNames,
getMetafieldDefinitions,
});
const hoverProvider = new HoverProvider(
Expand Down

0 comments on commit b16e3ec

Please sign in to comment.