From 6d5ccd2918b89803df1992894ab87b733907d883 Mon Sep 17 00:00:00 2001 From: Riley Nowak Date: Thu, 12 Dec 2024 15:21:25 -0400 Subject: [PATCH 1/2] add deeplinking for block types --- .../DocumentLinksProvider.spec.ts | 16 ++ .../documentLinks/DocumentLinksProvider.ts | 186 +++++++++++++++++- 2 files changed, 201 insertions(+), 1 deletion(-) diff --git a/packages/theme-language-server-common/src/documentLinks/DocumentLinksProvider.spec.ts b/packages/theme-language-server-common/src/documentLinks/DocumentLinksProvider.spec.ts index 42b82c2dc..749c4e398 100644 --- a/packages/theme-language-server-common/src/documentLinks/DocumentLinksProvider.spec.ts +++ b/packages/theme-language-server-common/src/documentLinks/DocumentLinksProvider.spec.ts @@ -61,4 +61,20 @@ describe('DocumentLinksProvider', () => { expect(result[i].target).toBe(expectedUrls[i]); } }); + + it('should return a list of document links with correct URLs for a LiquidRawTag document', async () => { + uriString = 'file:///path/to/liquid-raw-tag-document.liquid'; + rootUri = 'file:///path/to/project'; + + const liquidRawTagContent = ` + {% schema %} + { "blocks": [{ "type": "valid" }] } + {% endschema %} + `; + + documentManager.open(uriString, liquidRawTagContent, 1); + + const result = await documentLinksProvider.documentLinks(uriString); + expect(result).toEqual([]); + }); }); diff --git a/packages/theme-language-server-common/src/documentLinks/DocumentLinksProvider.ts b/packages/theme-language-server-common/src/documentLinks/DocumentLinksProvider.ts index 3541370b3..3392ee00e 100644 --- a/packages/theme-language-server-common/src/documentLinks/DocumentLinksProvider.ts +++ b/packages/theme-language-server-common/src/documentLinks/DocumentLinksProvider.ts @@ -1,4 +1,4 @@ -import { LiquidHtmlNode, LiquidString, NodeTypes } from '@shopify/liquid-html-parser'; +import { LiquidHtmlNode, LiquidRawTag, LiquidString, 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'; @@ -7,6 +7,8 @@ import { URI, Utils } from 'vscode-uri'; import { DocumentManager } from '../documents'; import { visit, Visitor } from '@shopify/theme-check-common'; +import { parseTree, findNodeAtLocation, ParseError, Node as JSONNode } from 'jsonc-parser'; + export class DocumentLinksProvider { constructor( private documentManager: DocumentManager, @@ -79,6 +81,42 @@ function documentLinksVisitor( Utils.resolvePath(root, 'assets', expression.value).toString(), ); }, + + LiquidRawTag(node) { + // look for schema tags + if (node.name === 'schema') { + // parse and return a tree of the schema + const errors: ParseError[] = []; + const jsonNode = parseTree(node.body.value, errors); + if (!jsonNode || errors.length > 0) { + return []; + } + + // create an array of links so we can process all block types and preset block types in the schema + const links: DocumentLink[] = []; + + // Process top-level blocks + const blocksNode = findNodeAtLocation(jsonNode, ['blocks']); + if (blocksNode && blocksNode.type === 'array' && blocksNode.children) { + links.push(...createLinksFromBlocks(blocksNode, node, textDocument, root)); + } + + // Process presets + const presetsNode = findNodeAtLocation(jsonNode, ['presets']); + if (presetsNode && presetsNode.type === 'array' && presetsNode.children) { + presetsNode.children.forEach((presetNode) => { + // Process blocks within each preset + const presetBlocksNode = findNodeAtLocation(presetNode, ['blocks']); + if (presetBlocksNode) { + links.push(...processPresetBlocks(presetBlocksNode, node, textDocument, root)); + } + }); + } + + return links; + } + return []; + }, }; } @@ -91,3 +129,149 @@ function range(textDocument: TextDocument, node: { position: LiquidHtmlNode['pos function isLiquidString(node: LiquidHtmlNode): node is LiquidString { return node.type === NodeTypes.String; } + +function createDocumentLinkForTypeNode( + typeNode: JSONNode, + parentNode: LiquidRawTag, + textDocument: TextDocument, + root: URI, + blockType: string, +): DocumentLink | null { + const startOffset = typeNode.offset; + const endOffset = typeNode.offset + typeNode.length; + const startPos = parentNode.body.position.start + startOffset; + const endPos = parentNode.body.position.start + endOffset; + + const start = textDocument.positionAt(startPos); + const end = textDocument.positionAt(endPos); + + return DocumentLink.create( + Range.create(start, end), + Utils.resolvePath(root, 'blocks', `${blockType}.liquid`).toString(), + ); +} + +function processPresetBlocks( + blocksNode: JSONNode, + parentNode: LiquidRawTag, + textDocument: TextDocument, + root: URI, +): DocumentLink[] { + const links: DocumentLink[] = []; + + if (blocksNode.type === 'object' && blocksNode.children) { + blocksNode.children.forEach((propertyNode) => { + const blockValueNode = propertyNode.children?.[1]; // The value node of the property + if (!blockValueNode) return; + + // Check if the block has a 'name' key so we don't deeplink inline block types + const nameNode = findNodeAtLocation(blockValueNode, ['name']); + if (nameNode) { + return; + } + + const typeNode = findNodeAtLocation(blockValueNode, ['type']); + if (typeNode && typeNode.type === 'string' && typeof typeNode.value === 'string') { + const blockType = typeNode.value; + if (blockType.startsWith('@')) { + return; + } + + const link = createDocumentLinkForTypeNode( + typeNode, + parentNode, + textDocument, + root, + blockType, + ); + + if (link) { + links.push(link); + } + } + + // Recursively process nested blocks + const nestedBlocksNode = findNodeAtLocation(blockValueNode, ['blocks']); + if (nestedBlocksNode) { + links.push(...processPresetBlocks(nestedBlocksNode, parentNode, textDocument, root)); + } + }); + } else if (blocksNode.type === 'array' && blocksNode.children) { + blocksNode.children.forEach((blockNode) => { + // Check if the block has a 'name' key + const nameNode = findNodeAtLocation(blockNode, ['name']); + if (nameNode) { + return; // Skip creating a link if 'name' key exists + } + + const typeNode = findNodeAtLocation(blockNode, ['type']); + if (typeNode && typeNode.type === 'string' && typeof typeNode.value === 'string') { + const blockType = typeNode.value; + if (blockType.startsWith('@')) { + return; + } + + const link = createDocumentLinkForTypeNode( + typeNode, + parentNode, + textDocument, + root, + blockType, + ); + + if (link) { + links.push(link); + } + } + + // Recursively process nested blocks + const nestedBlocksNode = findNodeAtLocation(blockNode, ['blocks']); + if (nestedBlocksNode) { + links.push(...processPresetBlocks(nestedBlocksNode, parentNode, textDocument, root)); + } + }); + } + + return links; +} + +function createLinksFromBlocks( + blocksNode: JSONNode, + parentNode: LiquidRawTag, + textDocument: TextDocument, + root: URI, +): DocumentLink[] { + const links: DocumentLink[] = []; + + if (blocksNode.children) { + blocksNode.children.forEach((blockNode: JSONNode) => { + // Check if the block has a 'name' key to avoid deeplinking inline block types + const nameNode = findNodeAtLocation(blockNode, ['name']); + if (nameNode) { + return; + } + + const typeNode = findNodeAtLocation(blockNode, ['type']); + if (typeNode && typeNode.type === 'string' && typeof typeNode.value === 'string') { + const blockType = typeNode.value; + if (blockType.startsWith('@')) { + return; + } + + const link = createDocumentLinkForTypeNode( + typeNode, + parentNode, + textDocument, + root, + blockType, + ); + + if (link) { + links.push(link); + } + } + }); + } + + return links; +} From 96ab877dd4c165090a2626f2af379100dc475a59 Mon Sep 17 00:00:00 2001 From: Riley Nowak Date: Mon, 16 Dec 2024 17:10:28 -0400 Subject: [PATCH 2/2] Added test --- .../DocumentLinksProvider.spec.ts | 33 ++++++++++++++++--- .../documentLinks/DocumentLinksProvider.ts | 15 ++------- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/packages/theme-language-server-common/src/documentLinks/DocumentLinksProvider.spec.ts b/packages/theme-language-server-common/src/documentLinks/DocumentLinksProvider.spec.ts index 749c4e398..6477de22d 100644 --- a/packages/theme-language-server-common/src/documentLinks/DocumentLinksProvider.spec.ts +++ b/packages/theme-language-server-common/src/documentLinks/DocumentLinksProvider.spec.ts @@ -62,19 +62,44 @@ describe('DocumentLinksProvider', () => { } }); - it('should return a list of document links with correct URLs for a LiquidRawTag document', async () => { + it('should return a list of document links for customizable theme block types', async () => { + // @theme and @app are not included as these are general types used for block targeting + // inline blocks are not included as there are no related block files to link to uriString = 'file:///path/to/liquid-raw-tag-document.liquid'; rootUri = 'file:///path/to/project'; const liquidRawTagContent = ` {% schema %} - { "blocks": [{ "type": "valid" }] } - {% endschema %} + { "blocks": [{ "type": "@theme" }, {"type": "top_level" }, {"type": "_private_block" }], + "name": "Test section", + "presets": [ + { + "blocks": { + "block-1": { + "name": "inline block", + "type": "inline_block" + }, + "block-2": { + "type": "nested_block" + } + } + } + ] + } + {% endschema %} `; documentManager.open(uriString, liquidRawTagContent, 1); const result = await documentLinksProvider.documentLinks(uriString); - expect(result).toEqual([]); + const expectedUrls = [ + 'file:///path/to/project/blocks/top_level.liquid', + 'file:///path/to/project/blocks/_private_block.liquid', + 'file:///path/to/project/blocks/nested_block.liquid', + ]; + expect(result.length).toBe(expectedUrls.length); + for (let i = 0; i < expectedUrls.length; i++) { + expect(result[i].target).toBe(expectedUrls[i]); + } }); }); diff --git a/packages/theme-language-server-common/src/documentLinks/DocumentLinksProvider.ts b/packages/theme-language-server-common/src/documentLinks/DocumentLinksProvider.ts index 3392ee00e..515c72ec7 100644 --- a/packages/theme-language-server-common/src/documentLinks/DocumentLinksProvider.ts +++ b/packages/theme-language-server-common/src/documentLinks/DocumentLinksProvider.ts @@ -83,29 +83,23 @@ function documentLinksVisitor( }, LiquidRawTag(node) { - // look for schema tags if (node.name === 'schema') { - // parse and return a tree of the schema const errors: ParseError[] = []; const jsonNode = parseTree(node.body.value, errors); if (!jsonNode || errors.length > 0) { return []; } - // create an array of links so we can process all block types and preset block types in the schema const links: DocumentLink[] = []; - // Process top-level blocks const blocksNode = findNodeAtLocation(jsonNode, ['blocks']); if (blocksNode && blocksNode.type === 'array' && blocksNode.children) { links.push(...createLinksFromBlocks(blocksNode, node, textDocument, root)); } - // Process presets const presetsNode = findNodeAtLocation(jsonNode, ['presets']); if (presetsNode && presetsNode.type === 'array' && presetsNode.children) { presetsNode.children.forEach((presetNode) => { - // Process blocks within each preset const presetBlocksNode = findNodeAtLocation(presetNode, ['blocks']); if (presetBlocksNode) { links.push(...processPresetBlocks(presetBlocksNode, node, textDocument, root)); @@ -161,10 +155,9 @@ function processPresetBlocks( if (blocksNode.type === 'object' && blocksNode.children) { blocksNode.children.forEach((propertyNode) => { - const blockValueNode = propertyNode.children?.[1]; // The value node of the property + const blockValueNode = propertyNode.children?.[1]; if (!blockValueNode) return; - // Check if the block has a 'name' key so we don't deeplink inline block types const nameNode = findNodeAtLocation(blockValueNode, ['name']); if (nameNode) { return; @@ -190,7 +183,6 @@ function processPresetBlocks( } } - // Recursively process nested blocks const nestedBlocksNode = findNodeAtLocation(blockValueNode, ['blocks']); if (nestedBlocksNode) { links.push(...processPresetBlocks(nestedBlocksNode, parentNode, textDocument, root)); @@ -198,10 +190,9 @@ function processPresetBlocks( }); } else if (blocksNode.type === 'array' && blocksNode.children) { blocksNode.children.forEach((blockNode) => { - // Check if the block has a 'name' key const nameNode = findNodeAtLocation(blockNode, ['name']); if (nameNode) { - return; // Skip creating a link if 'name' key exists + return; } const typeNode = findNodeAtLocation(blockNode, ['type']); @@ -224,7 +215,6 @@ function processPresetBlocks( } } - // Recursively process nested blocks const nestedBlocksNode = findNodeAtLocation(blockNode, ['blocks']); if (nestedBlocksNode) { links.push(...processPresetBlocks(nestedBlocksNode, parentNode, textDocument, root)); @@ -245,7 +235,6 @@ function createLinksFromBlocks( if (blocksNode.children) { blocksNode.children.forEach((blockNode: JSONNode) => { - // Check if the block has a 'name' key to avoid deeplinking inline block types const nameNode = findNodeAtLocation(blockNode, ['name']); if (nameNode) { return;