Skip to content

Commit bffb95b

Browse files
authored
Merge pull request #763 from Shopify/example-tag-hover-support
Example tag hover support
2 parents 8af587d + d628ec8 commit bffb95b

File tree

8 files changed

+193
-81
lines changed

8 files changed

+193
-81
lines changed

.changeset/beige-boxes-lay.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+
Add hover support for @example in liquid doc tags
6+
EX:
7+
8+
```liquid
9+
{% doc %}
10+
@example
11+
{{ product }}
12+
{% enddoc %}
13+
```

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

+2-36
Original file line numberDiff line numberDiff line change
@@ -550,44 +550,10 @@ export function printLiquidDocExample(
550550

551551
const content = node.exampleContent.value;
552552
if (content) {
553-
// Count leading newlines before content (\n\nmy content)
554-
const leadingNewlines = content.match(/^\n*/)?.[0]?.length ?? 0;
555-
const trimmedContent = content.trim();
556-
557-
// Push inline content to new line
558-
parts.push(hardline);
559-
560-
// If there were two or more leading newlines, push another new line
561-
if (leadingNewlines > 1) {
553+
if (content.includes('\n')) {
562554
parts.push(hardline);
563555
}
564-
565-
// If content doesn't have newlines in it, make sure it's on a new line (not inline)
566-
if (!trimmedContent.includes('\n')) {
567-
parts.push(trimmedContent);
568-
return parts;
569-
}
570-
571-
// For multi-line content
572-
const lines = trimmedContent.split('\n');
573-
const processedLines: string[] = [];
574-
let emptyLineCount = 0;
575-
576-
for (let i = 0; i < lines.length; i++) {
577-
const line = lines[i].trim();
578-
579-
if (line === '') {
580-
emptyLineCount++;
581-
if (emptyLineCount <= 2) {
582-
processedLines.push('');
583-
}
584-
} else {
585-
emptyLineCount = 0;
586-
processedLines.push(line);
587-
}
588-
}
589-
590-
parts.push(join(hardline, processedLines));
556+
parts.push(content.trim());
591557
}
592558

593559
return parts;

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

-14
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ It should push example content to the next line
5151
It should allow single empty lines between content
5252
{% doc %}
5353
@example
54-
5554
This is a valid example
5655
5756
So is this without a newline
@@ -61,7 +60,6 @@ It should allow single empty lines between content
6160
It should allow multiple empty lines between content
6261
{% doc %}
6362
@example
64-
6563
This is a valid example
6664
6765
@@ -73,15 +71,13 @@ It should allow multiple empty lines between content
7371
It should remove empty lines at the end of the example content
7472
{% doc %}
7573
@example
76-
7774
Here is my content and a newline
7875
{% enddoc %}
7976

8077
It should respect example content with param and description
8178
{% doc %}
8279
@param paramName - param with description
8380
@example
84-
8581
This is a valid example
8682
{% enddoc %}
8783

@@ -92,13 +88,3 @@ It should allow multiple example nodes
9288
@example
9389
Second Example
9490
{% enddoc %}
95-
96-
It should default to two lines between example content when there are three or more new lines
97-
{% doc %}
98-
@example
99-
100-
There are three lines between me and the next line
101-
102-
103-
It will format down to two lines
104-
{% enddoc %}

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

-15
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ It should push example content to the next line
4949
It should allow single empty lines between content
5050
{% doc %}
5151
@example
52-
5352
This is a valid example
5453
5554
So is this without a newline
@@ -59,7 +58,6 @@ It should allow single empty lines between content
5958
It should allow multiple empty lines between example content
6059
{% doc %}
6160
@example
62-
6361
This is a valid example
6462
6563
@@ -71,7 +69,6 @@ It should allow multiple empty lines between example content
7169
It should remove empty lines at the end of the example content
7270
{% doc %}
7371
@example
74-
7572
Here is my content and a newline
7673
7774
{% enddoc %}
@@ -80,7 +77,6 @@ It should respect example content with param and description
8077
{% doc %}
8178
@param paramName - param with description
8279
@example
83-
8480
This is a valid example
8581
{% enddoc %}
8682

@@ -91,14 +87,3 @@ It should allow multiple example nodes
9187
@example
9288
Second Example
9389
{% enddoc %}
94-
95-
It should default to two lines between example content when there are three or more new lines
96-
{% doc %}
97-
@example
98-
99-
There are three lines between me and the next line
100-
101-
102-
103-
It will format down to two lines
104-
{% enddoc %}

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

+18-1
Original file line numberDiff line numberDiff line change
@@ -17,30 +17,45 @@ describe('Module: RenderSnippetHoverProvider', async () => {
1717
description: 'The title of the product',
1818
type: 'string',
1919
required: true,
20+
nodeType: 'param',
2021
},
2122
{
2223
name: 'border-radius',
2324
description: 'The border radius in px',
2425
type: 'number',
2526
required: false,
27+
nodeType: 'param',
2628
},
2729
{
2830
name: 'no-type',
2931
description: 'This parameter has no type',
3032
type: null,
3133
required: true,
34+
nodeType: 'param',
3235
},
3336
{
3437
name: 'no-description',
3538
description: null,
3639
type: 'string',
3740
required: true,
41+
nodeType: 'param',
3842
},
3943
{
4044
name: 'no-type-or-description',
4145
description: null,
4246
type: null,
4347
required: true,
48+
nodeType: 'param',
49+
},
50+
],
51+
examples: [
52+
{
53+
content: '{{ product }}',
54+
nodeType: 'example',
55+
},
56+
{
57+
content: '{{ product.title }}',
58+
nodeType: 'example',
4459
},
4560
],
4661
},
@@ -51,7 +66,7 @@ describe('Module: RenderSnippetHoverProvider', async () => {
5166
provider = createProvider(async () => mockSnippetDefinition);
5267
await expect(provider).to.hover(
5368
`{% render 'product-car█d' %}`,
54-
'### 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`',
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 }}```',
5570
);
5671
});
5772

@@ -82,12 +97,14 @@ describe('Module: RenderSnippetHoverProvider', async () => {
8297
description: 'The title of the product',
8398
type: 'string',
8499
required: true,
100+
nodeType: 'param',
85101
},
86102
{
87103
name: 'border-radius',
88104
description: 'The border radius in px',
89105
type: 'number',
90106
required: false,
107+
nodeType: 'param',
91108
},
92109
],
93110
},

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

+8
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@ export class RenderSnippetHoverProvider implements BaseHoverProvider {
5454
parts.push('', '**Parameters:**', parameters);
5555
}
5656

57+
if (liquidDoc.examples?.length) {
58+
const examples = liquidDoc.examples
59+
?.map(({ content }) => `\`\`\`liquid${content}\`\`\``)
60+
.join('\n');
61+
62+
parts.push('', '**Examples:**', examples);
63+
}
64+
5765
return {
5866
contents: {
5967
kind: 'markdown',

packages/theme-language-server-common/src/liquidDoc.spec.ts

+116
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ describe('Unit: getSnippetDefinition', () => {
2222
name: 'product-card',
2323
liquidDoc: {
2424
parameters: [],
25+
examples: [],
2526
},
2627
});
2728
});
@@ -48,36 +49,151 @@ describe('Unit: getSnippetDefinition', () => {
4849
description: 'The first param',
4950
type: 'String',
5051
required: true,
52+
nodeType: 'param',
5153
},
5254
{
5355
name: 'secondParam',
5456
description: 'The second param',
5557
type: 'Number',
5658
required: true,
59+
nodeType: 'param',
5760
},
5861
{
5962
name: 'optionalParam',
6063
description: 'The optional param',
6164
type: 'String',
6265
required: false,
66+
nodeType: 'param',
6367
},
6468
{
6569
name: 'paramWithNoType',
6670
description: 'param with no type',
6771
type: null,
6872
required: true,
73+
nodeType: 'param',
6974
},
7075
{
7176
name: 'paramWithOnlyName',
7277
description: null,
7378
type: null,
7479
required: true,
80+
nodeType: 'param',
7581
},
7682
{
7783
name: 'paramWithNoDescription',
7884
description: null,
7985
type: 'Number',
8086
required: true,
87+
nodeType: 'param',
88+
},
89+
],
90+
examples: [],
91+
},
92+
});
93+
});
94+
95+
it('should extract examples from @example annotations', async () => {
96+
const ast = toAST(`
97+
{% doc %}
98+
@example
99+
{{ product }}
100+
{% enddoc %}
101+
`);
102+
103+
const result = getSnippetDefinition(ast, 'product-card');
104+
expect(result).to.deep.equal({
105+
name: 'product-card',
106+
liquidDoc: {
107+
parameters: [],
108+
examples: [
109+
{
110+
content: '\n {{ product }}\n',
111+
nodeType: 'example',
112+
},
113+
],
114+
},
115+
});
116+
});
117+
118+
it('should extract examples from @example annotations with multiple lines', async () => {
119+
const ast = toAST(`
120+
{% doc %}
121+
@example
122+
{{ product }}
123+
{{ product.title }}
124+
{% enddoc %}
125+
`);
126+
127+
const result = getSnippetDefinition(ast, 'product-card');
128+
expect(result).to.deep.equal({
129+
name: 'product-card',
130+
liquidDoc: {
131+
parameters: [],
132+
examples: [
133+
{
134+
content: '\n {{ product }}\n {{ product.title }}\n',
135+
nodeType: 'example',
136+
},
137+
],
138+
},
139+
});
140+
});
141+
142+
it('should extract example from @example and @param annotations', async () => {
143+
const ast = toAST(`
144+
{% doc %}
145+
@param {String} product - The product
146+
@example
147+
{{ product }} // This is an example
148+
{% enddoc %}
149+
`);
150+
151+
const result = getSnippetDefinition(ast, 'product-card');
152+
expect(result).to.deep.equal({
153+
name: 'product-card',
154+
liquidDoc: {
155+
parameters: [
156+
{
157+
name: 'product',
158+
description: 'The product',
159+
type: 'String',
160+
required: true,
161+
nodeType: 'param',
162+
},
163+
],
164+
examples: [
165+
{
166+
content: '\n {{ product }} // This is an example\n',
167+
nodeType: 'example',
168+
},
169+
],
170+
},
171+
});
172+
});
173+
174+
it('should extract multiple examples from @example annotations', async () => {
175+
const ast = toAST(`
176+
{% doc %}
177+
@example
178+
{{ product }}
179+
@example
180+
{{ product.title }}
181+
{% enddoc %}
182+
`);
183+
184+
const result = getSnippetDefinition(ast, 'product-card');
185+
expect(result).to.deep.equal({
186+
name: 'product-card',
187+
liquidDoc: {
188+
parameters: [],
189+
examples: [
190+
{
191+
content: '\n {{ product }}\n',
192+
nodeType: 'example',
193+
},
194+
{
195+
content: '\n {{ product.title }}\n',
196+
nodeType: 'example',
81197
},
82198
],
83199
},

0 commit comments

Comments
 (0)