Skip to content

Commit

Permalink
Add hover support for named parameters in {% render %} snippet tags
Browse files Browse the repository at this point in the history
  • Loading branch information
jamesmengo committed Feb 7, 2025
1 parent 76bff7a commit 5eaf295
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/thick-lamps-speak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/theme-language-server-common': minor
---

Add hover support for named parameters in {% render %} snippet tags. Parameters that have a corresponding liquidDoc @param will render information when hovered.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
LiquidTagHoverProvider,
TranslationHoverProvider,
RenderSnippetHoverProvider,
RenderSnippetParameterHoverProvider,
} from './providers';
import { HtmlAttributeValueHoverProvider } from './providers/HtmlAttributeValueHoverProvider';
import { findCurrentNode } from '@shopify/theme-check-common';
Expand Down Expand Up @@ -50,6 +51,7 @@ export class HoverProvider {
new HtmlAttributeValueHoverProvider(),
new TranslationHoverProvider(getTranslationsForURI, documentManager),
new RenderSnippetHoverProvider(getSnippetDefinitionForURI),
new RenderSnippetParameterHoverProvider(getSnippetDefinitionForURI),
];
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { describe, beforeEach, it, expect } from 'vitest';
import { DocumentManager } from '../../documents';
import { HoverProvider } from '../HoverProvider';
import { MetafieldDefinitionMap } from '@shopify/theme-check-common';
import { GetSnippetDefinitionForURI, SnippetDefinition } from '../../liquidDoc';
import '../../../../theme-check-common/src/test/test-setup';

describe('Module: RenderSnippetParameterHoverProvider', async () => {
let provider: HoverProvider;
let getSnippetDefinition: GetSnippetDefinitionForURI;
const mockSnippetDefinition: SnippetDefinition = {
name: 'product-card',
liquidDoc: {
parameters: [
{
name: 'title',
description: 'The title of the product',
type: 'string',
required: true,
},
{
name: 'border-radius',
description: 'The border radius in px',
type: 'number',
required: false,
},
{
name: 'no-type',
description: 'This parameter has no type',
type: null,
required: true,
},
{
name: 'no-description',
description: null,
type: 'string',
required: true,
},
{
name: 'no-type-or-description',
description: null,
type: null,
required: true,
},
],
},
};

describe('hover', () => {
beforeEach(() => {
provider = createProvider(async () => mockSnippetDefinition);
});

it('should return null if snippet definition not found', async () => {
getSnippetDefinition = async () => undefined;
provider = createProvider(getSnippetDefinition);
await expect(provider).to.hover(`{% render 'product-card' tit█le: 'value' %}`, null);
});

// should return null if no parameters are defined
it('should return null if no parameters are defined in liquidDoc', async () => {
getSnippetDefinition = async () => ({
name: 'product-card',
liquidDoc: {
parameters: [],
},
});
provider = createProvider(getSnippetDefinition);
await expect(provider).to.hover(`{% render 'product-card' tit█le: 'value' %}`, null);
});

it('should return null if parameter not found in snippet definition', async () => {
await expect(provider).to.hover(`{% render 'product-card' unknown-para█m: 'value' %}`, null);
});

it('should return parameter info with type and description', async () => {
await expect(provider).to.hover(
`{% render 'product-card' ti█tle: 'My Product' %}`,
'`title`: `string`\n- The title of the product',
);
});

it('should return parameter info with only type', async () => {
await expect(provider).to.hover(
`{% render 'product-card' no-descri█ption: 'value' %}`,
'`no-description`: `string`',
);
});

it('should return parameter info with only description', async () => {
await expect(provider).to.hover(
`{% render 'product-card' no-ty█pe: 'value' %}`,
'`no-type`\n- This parameter has no type',
);
});

it('should return only parameter name when no type or description', async () => {
await expect(provider).to.hover(
`{% render 'product-card' no-type-or-descri█ption: 'value' %}`,
'`no-type-or-description`',
);
});
});
});

const createProvider = (getSnippetDefinition: GetSnippetDefinitionForURI) => {
return new HoverProvider(
new DocumentManager(),
{
filters: async () => [],
objects: async () => [],
tags: async () => [],
systemTranslations: async () => ({}),
},
async (_rootUri: string) => ({} as MetafieldDefinitionMap),
async () => ({}),
async () => [],
getSnippetDefinition,
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { NodeTypes } from '@shopify/liquid-html-parser';
import { LiquidHtmlNode } from '@shopify/theme-check-common';
import { Hover, HoverParams } from 'vscode-languageserver';
import { BaseHoverProvider } from '../BaseHoverProvider';
import { SnippetDefinition, LiquidDocParameter } from '../../liquidDoc';

export class RenderSnippetParameterHoverProvider implements BaseHoverProvider {
constructor(
private getSnippetDefinitionForURI: (
uri: string,
snippetName: string,
) => Promise<SnippetDefinition | undefined>,
) {}

async hover(
currentNode: LiquidHtmlNode,
ancestors: LiquidHtmlNode[],
params: HoverParams,
): Promise<Hover | null> {
const parentNode = ancestors.at(-1);
if (
currentNode.type !== NodeTypes.NamedArgument ||
!parentNode ||
parentNode.type !== NodeTypes.RenderMarkup ||
parentNode.snippet.type !== NodeTypes.String
) {
return null;
}

const snippetName = parentNode.snippet.value;
const snippetDefinition = await this.getSnippetDefinitionForURI(
params.textDocument.uri,
snippetName,
);

if (!snippetDefinition?.liquidDoc?.parameters?.length) {
return null;
}

const paramName = currentNode.name;
const hoveredParameter = snippetDefinition.liquidDoc.parameters.find(
(parameter) => parameter.name === paramName,
);

if (!hoveredParameter) {
return null;
}

const parts = [];
parts.push(
hoveredParameter.type
? `\`${hoveredParameter.name}\`: \`${hoveredParameter.type}\``
: `\`${hoveredParameter.name}\``,
);

if (hoveredParameter.description) {
parts.push(`- ${hoveredParameter.description}`);
}

return {
contents: {
kind: 'markdown',
value: parts.join('\n'),
},
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export { HtmlAttributeHoverProvider } from './HtmlAttributeHoverProvider';
export { HtmlAttributeValueHoverProvider } from './HtmlAttributeValueHoverProvider';
export { TranslationHoverProvider } from './TranslationHoverProvider';
export { RenderSnippetHoverProvider } from './RenderSnippetHoverProvider';
export { RenderSnippetParameterHoverProvider } from './RenderSnippetParameterHoverProvider';

0 comments on commit 5eaf295

Please sign in to comment.