Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add block rename handler (replace old->new everywhere!) #655

Merged
merged 6 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/green-mice-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/theme-check-common': minor
---

[internal] Add `offset` information on Schema objects
12 changes: 12 additions & 0 deletions .changeset/wild-beers-study.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@shopify/theme-language-server-common': minor
'theme-check-vscode': minor
---

Add "On theme block rename" handling

Whenever a theme block gets renamed, the following will now happen:
1. References in files with a `{% schema %}` will be updated automatically
2. References in template files will be updated automatically
3. References in section groups will be updated automatically
4. References in `{% content_for "block", type: "oldName" %}` will be updated automatically
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ describe('Module: ValidSchemaName', () => {
validSchema: new Error('Invalid schema'),
name: 'file',
type: ThemeSchemaType.Section,
offset: 0,
}),
});

Expand Down
7 changes: 5 additions & 2 deletions packages/theme-check-common/src/to-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,13 @@ export function isSection(uri: UriString) {
}

export function isBlockSchema(
schema: SectionSchema | ThemeBlockSchema | undefined,
schema: AppBlockSchema | SectionSchema | ThemeBlockSchema | undefined,
): schema is ThemeBlockSchema {
return schema?.type === ThemeSchemaType.Block;
}

export function isSectionSchema(
schema: SectionSchema | ThemeBlockSchema | undefined,
schema: AppBlockSchema | SectionSchema | ThemeBlockSchema | undefined,
): schema is SectionSchema {
return schema?.type === ThemeSchemaType.Section;
}
Expand Down Expand Up @@ -84,6 +84,7 @@ export async function toBlockSchema(
return {
type: ThemeSchemaType.Block,
validSchema: await toValidSchema<ThemeBlock.Schema>(uri, schemaNode, parsed, isValidSchema),
offset: schemaNode instanceof Error ? 0 : schemaNode.blockStartPosition.end,
name,
parsed,
ast,
Expand All @@ -105,6 +106,7 @@ export async function toSectionSchema(
return {
type: ThemeSchemaType.Section,
validSchema: await toValidSchema(uri, schemaNode, parsed, isValidSchema),
offset: schemaNode instanceof Error ? 0 : schemaNode.blockStartPosition.end,
name,
parsed,
ast,
Expand All @@ -122,6 +124,7 @@ export async function toAppBlockSchema(
const ast = toAst(schemaNode);
return {
type: ThemeSchemaType.AppBlock,
offset: schemaNode instanceof Error ? 0 : schemaNode.blockStartPosition.end,
name,
parsed,
ast,
Expand Down
3 changes: 3 additions & 0 deletions packages/theme-check-common/src/types/theme-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ export interface ThemeSchema<T extends ThemeSchemaType> {

/** Parsed as a JavaScript object or an Error */
parsed: any | Error;

/** 0-based index of the start of JSON object in the document */
offset: number;
}

/** See {@link ThemeSchema} */
Expand Down
6 changes: 6 additions & 0 deletions packages/theme-language-server-common/src/documents/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,9 @@ export type AugmentedSourceCode<SCT extends SourceCodeType = SourceCodeType> = {
[SourceCodeType.JSON]: AugmentedJsonSourceCode;
[SourceCodeType.LiquidHtml]: AugmentedLiquidSourceCode;
}[SCT];

export const isLiquidSourceCode = (file: AugmentedSourceCode): file is AugmentedLiquidSourceCode =>
file.type === SourceCodeType.LiquidHtml;

export const isJsonSourceCode = (file: AugmentedSourceCode): file is AugmentedJsonSourceCode =>
file.type === SourceCodeType.JSON;
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { path } from '@shopify/theme-check-common';
import { Connection } from 'vscode-languageserver';
import { RenameFilesParams } from 'vscode-languageserver-protocol';
import { ClientCapabilities } from '../ClientCapabilities';
import { DocumentManager } from '../documents';
import { BaseRenameHandler } from './BaseRenameHandler';
import { AssetRenameHandler } from './handlers/AssetRenameHandler';
import { BlockRenameHandler } from './handlers/BlockRenameHandler';
import { SnippetRenameHandler } from './handlers/SnippetRenameHandler';

/**
Expand All @@ -26,6 +26,7 @@ export class RenameHandler {
this.handlers = [
new SnippetRenameHandler(documentManager, connection, capabilities, findThemeRootURI),
new AssetRenameHandler(documentManager, connection, capabilities, findThemeRootURI),
new BlockRenameHandler(documentManager, connection, capabilities, findThemeRootURI),
];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
WorkspaceEdit,
} from 'vscode-languageserver-protocol';
import { ClientCapabilities } from '../../ClientCapabilities';
import { AugmentedLiquidSourceCode, AugmentedSourceCode, DocumentManager } from '../../documents';
import { DocumentManager, isLiquidSourceCode } from '../../documents';
import { assetName, isAsset } from '../../utils/uri';
import { BaseRenameHandler } from '../BaseRenameHandler';

Expand All @@ -35,77 +35,72 @@ export class AssetRenameHandler implements BaseRenameHandler {

async onDidRenameFiles(params: RenameFilesParams): Promise<void> {
if (!this.capabilities.hasApplyEditSupport) return;
const isLiquidSourceCode = (file: AugmentedSourceCode): file is AugmentedLiquidSourceCode =>
file.type === SourceCodeType.LiquidHtml;

const relevantRenames = params.files.filter(
(file) => isAsset(file.oldUri) && isAsset(file.newUri),
);

// Only preload if you have something to do
if (relevantRenames.length === 0) return;
// Only preload if you have something to do (folder renames are not supported)
if (relevantRenames.length !== 1) return;
const rename = relevantRenames[0];
const rootUri = await this.findThemeRootURI(path.dirname(params.files[0].oldUri));
await this.documentManager.preload(rootUri);
const theme = this.documentManager.theme(rootUri, true);
const liquidSourceCodes = theme.filter(isLiquidSourceCode);

const promises = relevantRenames.map(async (file) => {
const oldAssetName = assetName(file.oldUri);
const newAssetName = assetName(file.newUri);
const editLabel = `Rename asset '${oldAssetName}' to '${newAssetName}'`;
const annotationId = 'renameAsset';
const workspaceEdit: WorkspaceEdit = {
documentChanges: [],
changeAnnotations: {
[annotationId]: {
label: editLabel,
needsConfirmation: false,
},
const oldAssetName = assetName(rename.oldUri);
const newAssetName = assetName(rename.newUri);
const editLabel = `Rename asset '${oldAssetName}' to '${newAssetName}'`;
const annotationId = 'renameAsset';
const workspaceEdit: WorkspaceEdit = {
documentChanges: [],
changeAnnotations: {
[annotationId]: {
label: editLabel,
needsConfirmation: false,
},
};
},
};

for (const sourceCode of liquidSourceCodes) {
if (sourceCode.ast instanceof Error) continue;
const textDocument = sourceCode.textDocument;
const edits: TextEdit[] = visit<SourceCodeType.LiquidHtml, TextEdit>(sourceCode.ast, {
LiquidVariable(node: LiquidVariable) {
if (node.filters.length === 0) return;
if (node.expression.type !== NodeTypes.String) return;
if (node.filters[0].name !== 'asset_url') return;
const assetName = node.expression.value;
if (assetName !== oldAssetName) return;
return {
newText: newAssetName,
range: Range.create(
textDocument.positionAt(node.expression.position.start + 1), // +1 to skip the opening quote
textDocument.positionAt(node.expression.position.end - 1), // -1 to skip the closing quote
),
};
},
});
for (const sourceCode of liquidSourceCodes) {
if (sourceCode.ast instanceof Error) continue;
const textDocument = sourceCode.textDocument;
const edits: TextEdit[] = visit<SourceCodeType.LiquidHtml, TextEdit>(sourceCode.ast, {
LiquidVariable(node: LiquidVariable) {
if (node.filters.length === 0) return;
if (node.expression.type !== NodeTypes.String) return;
if (node.filters[0].name !== 'asset_url') return;
const assetName = node.expression.value;
if (assetName !== oldAssetName) return;
return {
newText: newAssetName,
range: Range.create(
textDocument.positionAt(node.expression.position.start + 1), // +1 to skip the opening quote
textDocument.positionAt(node.expression.position.end - 1), // -1 to skip the closing quote
),
};
},
});

if (edits.length === 0) continue;
workspaceEdit.documentChanges!.push({
textDocument: {
uri: textDocument.uri,
version: sourceCode.version ?? null /* null means file from disk in this API */,
},
annotationId,
edits,
});
}
if (edits.length === 0) continue;
workspaceEdit.documentChanges!.push({
textDocument: {
uri: textDocument.uri,
version: sourceCode.version ?? null /* null means file from disk in this API */,
},
annotationId,
edits,
});
}

if (workspaceEdit.documentChanges!.length === 0) {
console.error('Nothing to do!');
return;
}
if (workspaceEdit.documentChanges!.length === 0) {
console.error('Nothing to do!');
return;
}

return this.connection.sendRequest(ApplyWorkspaceEditRequest.type, {
label: editLabel,
edit: workspaceEdit,
});
await this.connection.sendRequest(ApplyWorkspaceEditRequest.type, {
label: editLabel,
edit: workspaceEdit,
});

await Promise.all(promises);
}
}
Loading
Loading