@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>
400 lines (372 loc) • 12 kB
text/typescript
import { MockFileSystem } from '@shopify/theme-check-common/src/test';
import { assert, beforeEach, describe, expect, it } from 'vitest';
import { TextDocumentEdit } from 'vscode-json-languageservice';
import { ApplyWorkspaceEditParams } from 'vscode-languageserver-protocol';
import { ClientCapabilities } from '../../ClientCapabilities';
import { DocumentManager } from '../../documents';
import { MockConnection, mockConnection } from '../../test/MockConnection';
import { RenameHandler } from '../RenameHandler';
describe('Module: BlockRenameHandler', () => {
const mockRoot = 'mock-fs:';
const findThemeRootURI = async () => mockRoot;
let capabilities: ClientCapabilities;
let documentManager: DocumentManager;
let handler: RenameHandler;
let connection: MockConnection;
let fs: MockFileSystem;
beforeEach(() => {
connection = mockConnection(mockRoot);
connection.spies.sendRequest.mockReturnValue(Promise.resolve(true));
capabilities = new ClientCapabilities();
fs = new MockFileSystem(
{
'blocks/other-block.liquid': ``,
'blocks/old-name.liquid': `
{% schema %}
{
"name": "Old Block",
"blocks": [{ "type": "other-block" }],
"presets": [{
"name": "Default",
"blocks": [{
"type": "other-block"
}]
}]
}
{% endschema %}`,
'sections/section.liquid': `
<div>{% content_for "block", id: "old-name-id", type: "old-name" %}</div>
{% schema %}
{
"name": "Section",
"blocks": [{ "type": "old-name" }],
"presets": [
{
"name": "Default",
"blocks": [
{
"type": "old-name",
"blocks": [{ "type": "other-block" }]
}
]
}
]
}
{% endschema %}`,
'sections/header.json': `
{
"type": "header",
"name": "Header",
"sections": {
"section-id": {
"type": "section",
"blocks": {
"old-name-id": {
"type": "old-name"
}
},
"block_order": ["old-name-id"]
}
},
"order": ["section-id"]
}`,
'templates/index.json': `
{
"sections": {
"section-id": {
"type": "section",
"blocks": {
"old-name-id": {
"type": "old-name"
}
},
"block_order": ["old-name-id"]
}
},
"order": ["section-id"]
}`,
},
mockRoot,
);
documentManager = new DocumentManager(
fs, // filesystem
undefined, // optional mode getter
undefined, // optional mode map
async () => 'theme', // getModeForURI
async () => true, // isValidSchema - assume all schemas are valid in tests
);
handler = new RenameHandler(connection, capabilities, documentManager, findThemeRootURI);
});
describe('when the client does not support workspace/applyEdit', () => {
beforeEach(() => {
capabilities.setup({
workspace: {
applyEdit: false,
},
});
});
it('does nothing', async () => {
await handler.onDidRenameFiles({
files: [
{
oldUri: 'mock-fs:/blocks/old-name.liquid',
newUri: 'mock-fs:/blocks/new-name.liquid',
},
],
});
expect(connection.spies.sendRequest).not.toHaveBeenCalled();
});
});
describe('when the client supports workspace/applyEdit', () => {
beforeEach(() => {
capabilities.setup({
workspace: {
applyEdit: true,
},
});
});
it('returns a needConfirmation: false workspace edit for renaming a block', async () => {
await handler.onDidRenameFiles({
files: [
{
oldUri: 'mock-fs:/blocks/old-name.liquid',
newUri: 'mock-fs:/blocks/new-name.liquid',
},
],
});
const replaceWithNewNameTextEditAtAnyLocation = {
annotationId: 'renameBlock',
newText: 'new-name',
range: {
start: expect.any(Object),
end: expect.any(Object),
},
};
const templateTextEdits = [
// Block type is updated in the section blocks array
replaceWithNewNameTextEditAtAnyLocation,
];
const sectionGroupTextEdits = [
// Block type is updated in the section blocks array,
replaceWithNewNameTextEditAtAnyLocation,
];
const sectionFileTextEdits = [
// In the content_for
replaceWithNewNameTextEditAtAnyLocation,
// In the blocks definition
replaceWithNewNameTextEditAtAnyLocation,
// In the presets
replaceWithNewNameTextEditAtAnyLocation,
];
expect(connection.spies.sendRequest).toHaveBeenCalledWith('workspace/applyEdit', {
label: "Rename block 'old-name' to 'new-name'",
edit: {
changeAnnotations: {
renameBlock: {
label: `Rename block 'old-name' to 'new-name'`,
needsConfirmation: false,
},
},
documentChanges: expect.arrayContaining([
{
textDocument: {
uri: 'mock-fs:/sections/section.liquid',
version: null,
},
edits: sectionFileTextEdits,
},
{
textDocument: {
uri: 'mock-fs:/templates/index.json',
version: null,
},
edits: templateTextEdits,
},
{
textDocument: {
uri: 'mock-fs:/sections/header.json',
version: null,
},
edits: sectionGroupTextEdits,
},
]),
},
});
});
it('replaces the correct text in the documents', async () => {
await handler.onDidRenameFiles({
files: [
{
oldUri: 'mock-fs:/blocks/old-name.liquid',
newUri: 'mock-fs:/blocks/new-name.liquid',
},
],
});
const params: ApplyWorkspaceEditParams = connection.spies.sendRequest.mock.calls[0][1];
const expectedFs = new MockFileSystem(
{
'blocks/new-name.liquid': `
{% schema %}
{
"name": "Old Block",
"blocks": [{ "type": "other-block" }],
"presets": [{
"name": "Default",
"blocks": [{
"type": "other-block"
}]
}]
}
{% endschema %}`,
// The old-name block type is updated, but not the id.
'sections/section.liquid': `
<div>{% content_for "block", id: "old-name-id", type: "new-name" %}</div>
{% schema %}
{
"name": "Section",
"blocks": [{ "type": "new-name" }],
"presets": [
{
"name": "Default",
"blocks": [
{
"type": "new-name",
"blocks": [{ "type": "other-block" }]
}
]
}
]
}
{% endschema %}`,
'sections/header.json': `
{
"type": "header",
"name": "Header",
"sections": {
"section-id": {
"type": "section",
"blocks": {
"old-name-id": {
"type": "new-name"
}
},
"block_order": ["old-name-id"]
}
},
"order": ["section-id"]
}`,
// The old-name-id's block type is updated, but not the id.
// This isn't a regex search and replace.
'templates/index.json': `
{
"sections": {
"section-id": {
"type": "section",
"blocks": {
"old-name-id": {
"type": "new-name"
}
},
"block_order": ["old-name-id"]
}
},
"order": ["section-id"]
}`,
},
mockRoot,
);
assert(params.edit);
assert(params.edit.documentChanges);
for (const docChange of params.edit.documentChanges) {
assert(TextDocumentEdit.is(docChange));
const uri = docChange.textDocument.uri;
const edits = docChange.edits;
const initialDoc = await fs.readFile(uri);
const expectedDoc = await expectedFs.readFile(uri);
expect(edits).to.applyEdits(initialDoc, expectedDoc);
}
});
it('preserves local block definitions', async () => {
fs = new MockFileSystem(
{
// a "theme" text block exists
'blocks/text.liquid': `
{% schema %}
{ "name": "text block" }
{% endschema %}`,
// this section uses a local block of type "text"
'sections/local.liquid': `
{% schema %}
{
"name": "Section with local blocks",
"blocks": [
{
"type": "text",
"name": "Local text block"
}
],
"presets": [
{
"name": "Because text is a local block definition, this preset won't get a rename",
"blocks": [
{ "type": "text" }
]
}
]
}
{% endschema %}`,
// This section group uses the local section that uses the local block, no rename needed
'sections/header.json': JSON.stringify({
type: 'header',
name: 'Header',
sections: {
local_id: {
type: 'local',
blocks: {
text_tqQTNE: {
type: 'text',
},
},
block_order: ['text_tqQTNE'],
},
},
order: ['local_id'],
}),
// This template uses the section that uses the local block, no rename needed
'templates/index.json': JSON.stringify({
sections: {
local: {
type: 'local',
blocks: {
text_tqQTNE: {
type: 'text',
},
},
block_order: ['text_tqQTNE'],
},
},
}),
},
mockRoot,
);
documentManager = new DocumentManager(
fs,
undefined,
undefined,
async () => 'theme',
async () => true,
);
handler = new RenameHandler(connection, capabilities, documentManager, findThemeRootURI);
await handler.onDidRenameFiles({
files: [
{
oldUri: 'mock-fs:/blocks/text.liquid',
newUri: 'mock-fs:/blocks/poetry.liquid',
},
],
});
// Check if sendRequest was called at all
expect(connection.spies.sendRequest).not.toHaveBeenCalled();
});
});
});