UNPKG

@shopify/theme-language-server-common

Version:

<h1 align="center" style="position: relative;" > <br> <img src="https://github.com/Shopify/theme-check-vscode/blob/main/images/shopify_glyph.png?raw=true" alt="logo" width="141" height="160"> <br> Theme Language Server </h1>

303 lines 15.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.BlockRenameHandler = void 0; const liquid_html_parser_1 = require("@shopify/liquid-html-parser"); const theme_check_common_1 = require("@shopify/theme-check-common"); const vscode_languageserver_protocol_1 = require("vscode-languageserver-protocol"); const documents_1 = require("../../documents"); const uri_1 = require("../../utils/uri"); const utils_1 = require("./utils"); const annotationId = 'renameBlock'; /** * The BlockRenameHandler will handle block renames. * * Whenever a block gets renamed, a lot of things need to happen: * 1. References in files with a {% schema %} must be changed * 2. References in template files must be changed * 3. References in section groups must be changed * 4. References in {% content_for "block", type: "oldName" %} must be changed * * Things we're not doing: * 5. If isPublic(oldName) && isPrivate(newName) && "schema.blocks" accepts "@theme", * Then the block should be added to the "blocks" array * * Reasoning: this is more noisy than useful. a now-private block * could be used by a preset, template or section group. Doing a * toil-free rename would require visiting all preset, templates and * section groups to see if a parent that uses the new block name * was supporting "@theme" blocks. It's a lot. It's O(S*(S+T+SG)) where * S is the number of sections, T is the number of templates and SG is the * number of section groups. It's not worth it. * * This stuff is complicated enough as it is 😅. */ class BlockRenameHandler { constructor(documentManager, connection, capabilities, findThemeRootURI) { this.documentManager = documentManager; this.connection = connection; this.capabilities = capabilities; this.findThemeRootURI = findThemeRootURI; } async onDidRenameFiles(params) { if (!this.capabilities.hasApplyEditSupport) return; const relevantRenames = params.files.filter((file) => (0, uri_1.isBlock)(file.oldUri) && (0, uri_1.isBlock)(file.newUri)); // Only preload if you have something to do (folder renames not supported yet). if (relevantRenames.length !== 1) return; const rename = relevantRenames[0]; const rootUri = await this.findThemeRootURI(theme_check_common_1.path.dirname(params.files[0].oldUri)); await this.documentManager.preload(rootUri); const theme = this.documentManager.theme(rootUri, true); const liquidFiles = theme.filter(documents_1.isLiquidSourceCode); const sectionsAndBlocks = liquidFiles.filter((file) => (0, uri_1.isBlock)(file.uri) || (0, uri_1.isSection)(file.uri)); const templates = theme.filter(documents_1.isJsonSourceCode).filter((file) => (0, uri_1.isTemplate)(file.uri)); const sectionGroups = theme.filter(documents_1.isJsonSourceCode).filter((file) => (0, uri_1.isSectionGroup)(file.uri)); const oldBlockName = (0, uri_1.blockName)(rename.oldUri); const newBlockName = (0, uri_1.blockName)(rename.newUri); const editLabel = `Rename block '${oldBlockName}' to '${newBlockName}'`; const workspaceEdit = { documentChanges: [], changeAnnotations: { [annotationId]: { label: editLabel, needsConfirmation: false, }, }, }; // We need to keep track of sections that have local blocks, because we // shouldn't rename those. Only uses of "@theme" or specifically named blocks // should be renamed when the blocks/*.liquid file is renamed. const sectionsWithLocalBlocks = new Set(); const sectionAndBlocksChanges = await Promise.all(sectionsAndBlocks.map(this.getSchemaChanges(sectionsWithLocalBlocks, oldBlockName, newBlockName))); // All the templates/*.json files need to be updated with the new block name // when the old block name wasn't a local block. const [templateChanges, sectionGroupChanges, contentForChanges] = await Promise.all([ Promise.all(templates.map(this.getTemplateChanges(oldBlockName, newBlockName, sectionsWithLocalBlocks))), Promise.all(sectionGroups.map(this.getSectionGroupChanges(oldBlockName, newBlockName, sectionsWithLocalBlocks))), Promise.all(liquidFiles.map(this.getContentForChanges(oldBlockName, newBlockName))), ]); for (const docChange of [ ...sectionAndBlocksChanges, ...templateChanges, ...sectionGroupChanges, ]) { if (docChange !== null) { workspaceEdit.documentChanges.push(docChange); } } // Because contentForChanges could make a change to an existing document, we need // to group the edits together by document. Or else we might have index // drifting issues. for (const docChange of contentForChanges) { if (docChange !== null) { const existingDocChange = workspaceEdit.documentChanges.find((dc) => dc.textDocument.uri === (docChange === null || docChange === void 0 ? void 0 : docChange.textDocument.uri)); if (existingDocChange) { existingDocChange.edits.push(...docChange.edits); } else { workspaceEdit.documentChanges.push(docChange); } } } if (workspaceEdit.documentChanges.length === 0) { console.error('Nothing to do!'); return; } await this.connection.sendRequest(vscode_languageserver_protocol_1.ApplyWorkspaceEditRequest.type, { label: editLabel, edit: workspaceEdit, }); } getSchemaChanges(sectionsWithLocalBlocks, oldBlockName, newBlockName) { return async (sourceCode) => { if (sourceCode.ast instanceof Error) return null; const textDocument = sourceCode.textDocument; const schema = await sourceCode.getSchema(); if (!(0, theme_check_common_1.isBlockSchema)(schema) && !(0, theme_check_common_1.isSectionSchema)(schema)) return null; if ((0, theme_check_common_1.isError)(schema.validSchema) || (0, theme_check_common_1.isError)(schema.ast)) return null; const { validSchema, ast, offset } = schema; const edits = []; if (validSchema.blocks) { for (let i = 0; i < validSchema.blocks.length; i++) { const blockDef = validSchema.blocks[i]; if (isLocalBlock(blockDef)) { // If the section has a local blocks, we shouldn't rename // anything in this file. if ((0, theme_check_common_1.isSectionSchema)(schema)) { sectionsWithLocalBlocks.add(schema.name); } return null; } if (blockDef.type !== oldBlockName) continue; const node = (0, theme_check_common_1.nodeAtPath)(ast, ['blocks', i, 'type']); edits.push({ annotationId, newText: newBlockName, range: vscode_languageserver_protocol_1.Range.create(textDocument.positionAt(offset + node.loc.start.offset + 1), textDocument.positionAt(offset + node.loc.end.offset - 1)), }); } } const presetEdits = (presetBlock, path) => { if (!presetBlock || !('blocks' in presetBlock)) return []; if (Array.isArray(presetBlock.blocks)) { return presetBlock.blocks.flatMap((block, index) => { const edits = presetEdits(block, [...path, 'blocks', index]); if (block.type === oldBlockName) { const node = (0, theme_check_common_1.nodeAtPath)(ast, [...path, 'blocks', index, 'type']); edits.push({ annotationId, newText: newBlockName, range: vscode_languageserver_protocol_1.Range.create(textDocument.positionAt(offset + node.loc.start.offset + 1), textDocument.positionAt(offset + node.loc.end.offset - 1)), }); } return edits; }); } else if (typeof presetBlock.blocks === 'object') { return Object.entries(presetBlock.blocks).flatMap(([key, block]) => { const edits = presetEdits(block, [...path, 'blocks', key]); if (block.type === oldBlockName) { const node = (0, theme_check_common_1.nodeAtPath)(ast, [...path, 'blocks', key, 'type']); edits.push({ annotationId, newText: newBlockName, range: vscode_languageserver_protocol_1.Range.create(textDocument.positionAt(offset + node.loc.start.offset + 1), textDocument.positionAt(offset + node.loc.end.offset - 1)), }); } return edits; }); } else { return []; } }; if (validSchema.presets) { edits.push(...validSchema.presets.flatMap((preset, i) => presetEdits(preset, ['presets', i]))); } if (edits.length === 0) return null; return documentChanges(sourceCode, edits); }; } getTemplateChanges(oldBlockName, newBlockName, sectionsWithLocalBlocks) { return async (sourceCode) => { // assuming that the JSON is valid... const { textDocument, ast, source } = sourceCode; const parsed = (0, theme_check_common_1.parseJSON)(source); if (!parsed || (0, theme_check_common_1.isError)(parsed) || (0, theme_check_common_1.isError)(ast)) return null; const getBlocksEdits = getBlocksEditsFactory(oldBlockName, newBlockName, textDocument, ast); const edits = !(0, utils_1.isValidTemplate)(parsed) ? [] : Object.entries(parsed.sections).flatMap(([key, section]) => { if ('blocks' in section && !!section.blocks && !sectionsWithLocalBlocks.has(section.type) // don't rename local blocks ) { return getBlocksEdits(section.blocks, ['sections', key, 'blocks']); } else { return []; } }); if (edits.length === 0) return null; return documentChanges(sourceCode, edits); }; } getSectionGroupChanges(oldBlockName, newBlockName, sectionsWithLocalBlocks) { return async (sourceCode) => { const { textDocument, ast, source } = sourceCode; const parsed = (0, theme_check_common_1.parseJSON)(source); if (!parsed || (0, theme_check_common_1.isError)(parsed) || (0, theme_check_common_1.isError)(ast)) return null; const getBlocksEdits = getBlocksEditsFactory(oldBlockName, newBlockName, textDocument, ast); const edits = !(0, utils_1.isValidSectionGroup)(parsed) ? [] : Object.entries(parsed.sections).flatMap(([key, section]) => { if ('blocks' in section && !!section.blocks && !sectionsWithLocalBlocks.has(section.type) // don't rename local blocks ) { return getBlocksEdits(section.blocks, ['sections', key, 'blocks']); } else { return []; } }); if (edits.length === 0) return null; return documentChanges(sourceCode, edits); }; } getContentForChanges(oldBlockName, newBlockName) { return async (sourceCode) => { const { textDocument, ast } = sourceCode; if ((0, theme_check_common_1.isError)(ast)) return null; const edits = (0, theme_check_common_1.visit)(ast, { LiquidTag(node) { if (node.name !== 'content_for') return; if (typeof node.markup === 'string') return; if (node.markup.contentForType.value !== 'block') return; const typeNode = node.markup.args.find((arg) => arg.name === 'type'); if (!typeNode || typeNode.value.type !== liquid_html_parser_1.NodeTypes.String || typeNode.value.value !== oldBlockName) { return; } return { annotationId, newText: newBlockName, range: vscode_languageserver_protocol_1.Range.create(textDocument.positionAt(typeNode.value.position.start + 1), textDocument.positionAt(typeNode.value.position.end - 1)), }; }, }); if (edits.length === 0) return null; return documentChanges(sourceCode, edits); }; } } exports.BlockRenameHandler = BlockRenameHandler; function isLocalBlock(blockDef) { return 'name' in blockDef && typeof blockDef.name === 'string'; } function getBlocksEditsFactory(oldBlockName, newBlockName, textDocument, ast) { return function getBlocksEdits(blocks, path) { if (!blocks) return []; return Object.entries(blocks).flatMap(([key, block]) => { const edits = getBlocksEdits(block.blocks, [...path, key, 'blocks']); if (block.type === oldBlockName) { const node = (0, theme_check_common_1.nodeAtPath)(ast, [...path, key, 'type']); edits.push({ annotationId, newText: newBlockName, range: vscode_languageserver_protocol_1.Range.create(textDocument.positionAt(node.loc.start.offset + 1), textDocument.positionAt(node.loc.end.offset - 1)), }); } return edits; }); }; } function documentChanges(sourceCode, edits) { var _a; return { textDocument: { uri: sourceCode.uri, version: (_a = sourceCode.version) !== null && _a !== void 0 ? _a : null /* null means file from disk in this API */, }, edits, }; } //# sourceMappingURL=BlockRenameHandler.js.map