Skip to content

Commit def4cf4

Browse files
authored
Merge pull request #801 from Shopify/add-hover-support-for-description-tag
Add hover support for the description tag
2 parents 10dbac2 + 1e7094c commit def4cf4

File tree

11 files changed

+107
-23
lines changed

11 files changed

+107
-23
lines changed

.changeset/popular-geese-flow.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/theme-language-server-common': patch
3+
---
4+
5+
Add hover support for the `@description` tag

packages/liquid-html-parser/src/stage-1-cst.spec.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -1082,8 +1082,8 @@ describe('Unit: Stage 1 (CST)', () => {
10821082
});
10831083

10841084
it('should parse @param with malformed optional delimiters as Text Nodes', () => {
1085-
const testStr = `{% doc %}
1086-
@param paramWithMissingHeadDelim]
1085+
const testStr = `{% doc %}
1086+
@param paramWithMissingHeadDelim]
10871087
@param [paramWithMissingTailDelim
10881088
@param missingHeadWithDescription] - description value
10891089
@param [missingTailWithDescription - description value
@@ -1330,10 +1330,10 @@ describe('Unit: Stage 1 (CST)', () => {
13301330
{% enddoc %}`;
13311331
cst = toCST(testStr);
13321332
expectPath(cst, '0.children.0.type').to.equal('LiquidDocDescriptionNode');
1333-
expectPath(cst, '0.children.0.content.value').to.equal('This is a description\n');
1333+
expectPath(cst, '0.children.0.content.value').to.equal('This is a description');
13341334

13351335
expectPath(cst, '0.children.1.type').to.equal('LiquidDocDescriptionNode');
1336-
expectPath(cst, '0.children.1.content.value').to.equal('This is a second description\n');
1336+
expectPath(cst, '0.children.1.content.value').to.equal('This is a second description');
13371337
});
13381338

13391339
it('should parse and strip whitespace from description tag with content that has leading whitespace', () => {
@@ -1362,7 +1362,7 @@ describe('Unit: Stage 1 (CST)', () => {
13621362
expectPath(cst, '0.children.0.type').to.equal('LiquidDocDescriptionNode');
13631363
expectPath(cst, '0.children.0.name').to.equal('description');
13641364
expectPath(cst, '0.children.0.content.value').to.equal(
1365-
'hello there my friend\n This is a description\n It supports multiple lines\n',
1365+
'hello there my friend\n This is a description\n It supports multiple lines',
13661366
);
13671367
});
13681368

@@ -1373,9 +1373,9 @@ describe('Unit: Stage 1 (CST)', () => {
13731373
{% enddoc %}`;
13741374
cst = toCST(testStr);
13751375
expectPath(cst, '0.children.0.type').to.equal('LiquidDocDescriptionNode');
1376-
expectPath(cst, '0.children.0.content.value').to.equal('hello there\n');
1376+
expectPath(cst, '0.children.0.content.value').to.equal('hello there');
13771377
expectPath(cst, '0.children.1.type').to.equal('LiquidDocDescriptionNode');
1378-
expectPath(cst, '0.children.1.content.value').to.equal('second description\n');
1378+
expectPath(cst, '0.children.1.content.value').to.equal('second description');
13791379
});
13801380
}
13811381
});

packages/liquid-html-parser/src/stage-1-cst.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -1400,7 +1400,15 @@ function toLiquidDocAST(source: string, matchingSource: string, offset: number)
14001400
source,
14011401
content: 2,
14021402
},
1403-
descriptionContent: textNode(),
1403+
descriptionContent: {
1404+
type: ConcreteNodeTypes.TextNode,
1405+
value: function (this: Node) {
1406+
return this.sourceString.trim();
1407+
},
1408+
locStart,
1409+
locEnd,
1410+
source,
1411+
},
14041412
paramType: 2,
14051413
paramTypeContent: textNode(),
14061414
paramName: {

packages/liquid-html-parser/src/stage-2-ast.spec.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1385,10 +1385,10 @@ describe('Unit: Stage 2 (AST)', () => {
13851385
{% enddoc %}
13861386
`);
13871387
expectPath(ast, 'children.0.body.nodes.0.type').to.eql('LiquidDocDescriptionNode');
1388-
expectPath(ast, 'children.0.body.nodes.0.content.value').to.eql('This is a description\n');
1388+
expectPath(ast, 'children.0.body.nodes.0.content.value').to.eql('This is a description');
13891389
expectPath(ast, 'children.0.body.nodes.1.type').to.eql('LiquidDocDescriptionNode');
13901390
expectPath(ast, 'children.0.body.nodes.1.content.value').to.eql(
1391-
'This is another description\n it can have multiple lines\n',
1391+
'This is another description\n it can have multiple lines',
13921392
);
13931393

13941394
ast = toLiquidAST(`
@@ -1399,7 +1399,7 @@ describe('Unit: Stage 2 (AST)', () => {
13991399
{% enddoc %}
14001400
`);
14011401
expectPath(ast, 'children.0.body.nodes.0.type').to.eql('LiquidDocDescriptionNode');
1402-
expectPath(ast, 'children.0.body.nodes.0.content.value').to.eql('This is a description\n');
1402+
expectPath(ast, 'children.0.body.nodes.0.content.value').to.eql('This is a description');
14031403

14041404
expectPath(ast, 'children.0.body.nodes.1.type').to.eql('LiquidDocExampleNode');
14051405
expectPath(ast, 'children.0.body.nodes.1.name').to.eql('example');
@@ -1572,7 +1572,7 @@ describe('Unit: Stage 2 (AST)', () => {
15721572
@p█
15731573
`);
15741574
expectPath(ast, 'children.0.body.nodes.0.type').to.eql('LiquidDocDescriptionNode');
1575-
expectPath(ast, 'children.0.body.nodes.0.content.value').to.eql('This is a description\n');
1575+
expectPath(ast, 'children.0.body.nodes.0.content.value').to.eql('This is a description');
15761576

15771577
expectPath(ast, 'children.0.body.nodes.1.type').to.eql('LiquidDocExampleNode');
15781578
expectPath(ast, 'children.0.body.nodes.1.name').to.eql('example');

packages/prettier-plugin-liquid/src/printer/print/liquid.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -584,7 +584,7 @@ export function printLiquidDocDescription(
584584
const parts: Doc[] = ['@description'];
585585

586586
if (node.content?.value) {
587-
parts.push(' ', node.content.value.trim());
587+
parts.push(' ', node.content.value);
588588
}
589589

590590
return parts;

packages/prettier-plugin-liquid/src/test/liquid-doc/fixed.liquid

+2-2
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,12 @@ It should add a space between a description tag and content
102102

103103
It should handle param, example, and description nodes
104104
{% doc %}
105+
@description This is a description
106+
105107
@param {String} paramName - param with description
106108
107109
@example
108110
This is a valid example
109-
110-
@description This is a description
111111
{% enddoc %}
112112

113113
It should add padding between dissimilar nodes

packages/prettier-plugin-liquid/src/test/liquid-doc/index.liquid

+1-1
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,10 @@ It should add a space between a description tag and content
101101

102102
It should handle param, example, and description nodes
103103
{% doc %}
104+
@description This is a description
104105
@param {String} paramName - param with description
105106
@example
106107
This is a valid example
107-
@description This is a description
108108
{% enddoc %}
109109

110110
It should add padding between dissimilar nodes

packages/theme-check-common/src/liquid-doc/liquidDoc.spec.ts

+39
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,45 @@ describe('Unit: getSnippetDefinition', () => {
192192
});
193193
});
194194

195+
it('should extract description from @description annotations', async () => {
196+
const ast = toAST(`
197+
{% doc %}
198+
@description This is a description
199+
{% enddoc %}
200+
`);
201+
202+
const result = getSnippetDefinition(ast, 'product-card');
203+
expect(result).to.deep.equal({
204+
name: 'product-card',
205+
liquidDoc: {
206+
description: {
207+
content: 'This is a description',
208+
nodeType: 'description',
209+
},
210+
},
211+
});
212+
});
213+
214+
it('should extract only the first @description annotation', async () => {
215+
const ast = toAST(`
216+
{% doc %}
217+
@description This is a description
218+
@description This is another description
219+
{% enddoc %}
220+
`);
221+
222+
const result = getSnippetDefinition(ast, 'product-card');
223+
expect(result).to.deep.equal({
224+
name: 'product-card',
225+
liquidDoc: {
226+
description: {
227+
content: 'This is a description',
228+
nodeType: 'description',
229+
},
230+
},
231+
});
232+
});
233+
195234
it('should return snippetDefinition without liquidDoc property if doc header is not present', async () => {
196235
const ast = toAST(`
197236
<div>No doc header here</div>

packages/theme-check-common/src/liquid-doc/liquidDoc.ts

+29-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { SourceCodeType } from '../types';
22
import { visit } from '../visitor';
33
import { LiquidHtmlNode } from '../types';
4-
import { LiquidDocExampleNode, LiquidDocParamNode } from '@shopify/liquid-html-parser';
4+
import {
5+
LiquidDocExampleNode,
6+
LiquidDocParamNode,
7+
LiquidDocDescriptionNode,
8+
} from '@shopify/liquid-html-parser';
59

610
export type GetSnippetDefinitionForURI = (
711
uri: string,
@@ -16,10 +20,11 @@ export type SnippetDefinition = {
1620
type LiquidDocDefinition = {
1721
parameters?: LiquidDocParameter[];
1822
examples?: LiquidDocExample[];
23+
description?: LiquidDocDescription;
1924
};
2025

2126
interface LiquidDocNode {
22-
nodeType: 'param' | 'example';
27+
nodeType: 'param' | 'example' | 'description';
2328
}
2429

2530
export interface LiquidDocParameter extends LiquidDocNode {
@@ -34,14 +39,19 @@ export interface LiquidDocExample extends LiquidDocNode {
3439
nodeType: 'example';
3540
}
3641

42+
export interface LiquidDocDescription extends LiquidDocNode {
43+
content: string;
44+
nodeType: 'description';
45+
}
46+
3747
export function getSnippetDefinition(
3848
snippet: LiquidHtmlNode,
3949
snippetName: string,
4050
): SnippetDefinition {
4151
let hasDocTag = false;
42-
const nodes: (LiquidDocParameter | LiquidDocExample)[] = visit<
52+
const nodes: (LiquidDocParameter | LiquidDocExample | LiquidDocDescription)[] = visit<
4353
SourceCodeType.LiquidHtml,
44-
LiquidDocParameter | LiquidDocExample
54+
LiquidDocParameter | LiquidDocExample | LiquidDocDescription
4555
>(snippet, {
4656
LiquidRawTag(node) {
4757
if (node.name === 'doc') hasDocTag = true;
@@ -62,17 +72,29 @@ export function getSnippetDefinition(
6272
nodeType: 'example',
6373
};
6474
},
75+
LiquidDocDescriptionNode(node: LiquidDocDescriptionNode) {
76+
return {
77+
content: node.content.value,
78+
nodeType: 'description',
79+
};
80+
},
6581
});
66-
const { parameters, examples } = nodes.reduce(
82+
const { parameters, examples, description } = nodes.reduce(
6783
(acc, node) => {
6884
if (node.nodeType === 'param') {
6985
acc.parameters.push(node as LiquidDocParameter);
7086
} else if (node.nodeType === 'example') {
7187
acc.examples.push(node as LiquidDocExample);
88+
} else if (node.nodeType === 'description' && !acc.description) {
89+
acc.description = node as LiquidDocDescription;
7290
}
7391
return acc;
7492
},
75-
{ parameters: [] as LiquidDocParameter[], examples: [] as LiquidDocExample[] },
93+
{
94+
parameters: [] as LiquidDocParameter[],
95+
examples: [] as LiquidDocExample[],
96+
description: undefined as LiquidDocDescription | undefined,
97+
},
7698
);
7799

78100
if (!hasDocTag) return { name: snippetName };
@@ -82,6 +104,7 @@ export function getSnippetDefinition(
82104
liquidDoc: {
83105
...(parameters.length && { parameters }),
84106
...(examples.length && { examples }),
107+
...(description && { description }),
85108
},
86109
};
87110
}

packages/theme-language-server-common/src/hover/providers/RenderSnippetHoverProvider.spec.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ describe('Module: RenderSnippetHoverProvider', async () => {
4848
nodeType: 'param',
4949
},
5050
],
51+
description: {
52+
content: 'This is a description',
53+
nodeType: 'description',
54+
},
5155
examples: [
5256
{
5357
content: '{{ product }}',
@@ -66,7 +70,7 @@ describe('Module: RenderSnippetHoverProvider', async () => {
6670
provider = createProvider(async () => mockSnippetDefinition);
6771
await expect(provider).to.hover(
6872
`{% render 'product-car█d' %}`,
69-
'### product-card\n\n**Parameters:**\n- `title`: string - The title of the product\n- `border-radius` (Optional): number - The border radius in px\n- `no-type` - This parameter has no type\n- `no-description`: string\n- `no-type-or-description`\n\n**Examples:**\n```liquid{{ product }}```\n```liquid{{ product.title }}```',
73+
'### product-card\n\n**Description:**\n\n\nThis is a description\n\n**Parameters:**\n- `title`: string - The title of the product\n- `border-radius` (Optional): number - The border radius in px\n- `no-type` - This parameter has no type\n- `no-description`: string\n- `no-type-or-description`\n\n**Examples:**\n```liquid{{ product }}```\n```liquid{{ product.title }}```',
7074
);
7175
});
7276

packages/theme-language-server-common/src/hover/providers/RenderSnippetHoverProvider.ts

+5
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ export class RenderSnippetHoverProvider implements BaseHoverProvider {
4949

5050
const parts = [`### ${snippetDefinition.name}`];
5151

52+
if (liquidDoc.description) {
53+
const description = liquidDoc.description.content;
54+
parts.push('', '**Description:**', '\n', description);
55+
}
56+
5257
if (liquidDoc.parameters?.length) {
5358
const parameters = this.buildParameters(liquidDoc.parameters);
5459
parts.push('', '**Parameters:**', parameters);

0 commit comments

Comments
 (0)