Skip to content

Commit

Permalink
Support hover + completion for presets settings (#700)
Browse files Browse the repository at this point in the history
  • Loading branch information
aswamy authored Jan 15, 2025
1 parent 4a429e1 commit c60e61b
Show file tree
Hide file tree
Showing 13 changed files with 533 additions and 35 deletions.
5 changes: 5 additions & 0 deletions .changeset/fifty-pugs-confess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/theme-check-common': patch
---

[Internal] Add schema raw value to schema node
5 changes: 5 additions & 0 deletions .changeset/healthy-ads-protect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/theme-language-server-common': minor
---

Support hover and code completion for presets settings properties
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ describe('Module: ValidSchemaName', () => {
name: 'file',
type: ThemeSchemaType.Section,
offset: 0,
value: '',
}),
});

Expand Down
6 changes: 6 additions & 0 deletions packages/theme-check-common/src/to-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,15 @@ export async function toBlockSchema(
const schemaNode = toSchemaNode(liquidAst);
const parsed = toParsed(schemaNode);
const ast = toAst(schemaNode);

return {
type: ThemeSchemaType.Block,
validSchema: await toValidSchema<ThemeBlock.Schema>(uri, schemaNode, parsed, isValidSchema),
offset: schemaNode instanceof Error ? 0 : schemaNode.blockStartPosition.end,
name,
parsed,
ast,
value: schemaNode instanceof Error ? '' : schemaNode.body.value,
};
}

Expand All @@ -103,13 +105,15 @@ export async function toSectionSchema(
const schemaNode = toSchemaNode(liquidAst);
const parsed = toParsed(schemaNode);
const ast = toAst(schemaNode);

return {
type: ThemeSchemaType.Section,
validSchema: await toValidSchema(uri, schemaNode, parsed, isValidSchema),
offset: schemaNode instanceof Error ? 0 : schemaNode.blockStartPosition.end,
name,
parsed,
ast,
value: schemaNode instanceof Error ? '' : schemaNode.body.value,
};
}

Expand All @@ -122,12 +126,14 @@ export async function toAppBlockSchema(
const schemaNode = toSchemaNode(liquidAst);
const parsed = toParsed(schemaNode);
const ast = toAst(schemaNode);

return {
type: ThemeSchemaType.AppBlock,
offset: schemaNode instanceof Error ? 0 : schemaNode.blockStartPosition.end,
name,
parsed,
ast,
value: schemaNode instanceof Error ? '' : schemaNode.body.value,
};
}

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 @@ -32,6 +32,9 @@ export interface ThemeSchema<T extends ThemeSchemaType> {

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

/** schema node value */
value: string;
}

/** See {@link ThemeSchema} */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import { SchemaTranslationHoverProvider } from './hover/providers/SchemaTranslat
import { TranslationPathHoverProvider } from './hover/providers/TranslationPathHoverProvider';
import { RequestContext } from './RequestContext';
import { findSchemaNode } from './utils';
import { PresetsSettingsPropertyCompletionProvider } from './completions/providers/PresetsSettingsPropertyCompletionProvider';
import { PresetsSettingsHoverProvider } from './hover/providers/PresetsSettingsHoverProvider';

/** The getInfoContribution API will only fallback if we return undefined synchronously */
const SKIP_CONTRIBUTION = undefined as any;
Expand Down Expand Up @@ -52,11 +54,13 @@ export class JSONContributions implements JSONWorkerContribution {
this.hoverProviders = [
new TranslationPathHoverProvider(),
new SchemaTranslationHoverProvider(getDefaultSchemaTranslations),
new PresetsSettingsHoverProvider(getDefaultSchemaTranslations),
];
this.completionProviders = [
new SchemaTranslationsCompletionProvider(getDefaultSchemaTranslations),
new BlockTypeCompletionProvider(getThemeBlockNames),
new PresetsBlockTypeCompletionProvider(getThemeBlockNames, getThemeBlockSchema),
new PresetsSettingsPropertyCompletionProvider(getDefaultSchemaTranslations),
];
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { describe, it, expect, assert, beforeEach } from 'vitest';
import { JSONLanguageService } from '../../JSONLanguageService';
import { DocumentManager } from '../../../documents';
import { getRequestParams, isCompletionList } from '../../test/test-helpers';
import {
getRequestParams,
isCompletionList,
mockJSONLanguageService,
} from '../../test/test-helpers';
import { SourceCodeType, ThemeBlock } from '@shopify/theme-check-common';

describe('Unit: PresetsBlockTypeCompletionProvider', () => {
Expand All @@ -17,31 +21,11 @@ describe('Unit: PresetsBlockTypeCompletionProvider', () => {
async () => 'theme', // 'theme' mode
async () => false, // schema is invalid for tests
);
jsonLanguageService = new JSONLanguageService(
documentManager,
{
schemas: async () => [
{
uri: 'https://shopify.dev/block-schema.json',
schema: JSON.stringify({
$schema: 'http://json-schema.org/draft-07/schema#',
}),
fileMatch: ['**/{blocks,sections}/*.liquid'],
},
],
},
async () => ({}),
async () => 'theme',
async () => ['block-1', 'block-2', 'custom-block'],
async (_uri: string, name: string) => {
const blockUri = `${rootUri}/blocks/${name}.liquid`;
const doc = documentManager.get(blockUri);
if (!doc || doc.type !== SourceCodeType.LiquidHtml) {
return;
}
return doc.getSchema();
},
);
jsonLanguageService = mockJSONLanguageService(rootUri, documentManager, undefined, async () => [
'block-1',
'block-2',
'custom-block',
]);

await jsonLanguageService.setup({
textDocument: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
import {
AppBlockSchema,
deepGet,
isError,
SectionSchema,
ThemeBlockSchema,
ThemeSchemaType,
} from '@shopify/theme-check-common';
import { deepGet, isError } from '@shopify/theme-check-common';
import { JSONPath } from 'vscode-json-languageservice';
import { JSONCompletionItem } from 'vscode-json-languageservice/lib/umd/jsonContributions';
import { CompletionItemKind } from 'vscode-languageserver-protocol';
import { isLiquidRequestContext, RequestContext } from '../../RequestContext';
import { fileMatch } from '../../utils';
import { JSONCompletionProvider } from '../JSONCompletionProvider';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { describe, it, expect, assert, beforeEach } from 'vitest';
import { JSONLanguageService } from '../../JSONLanguageService';
import { DocumentManager } from '../../../documents';
import {
getRequestParams,
isCompletionList,
mockJSONLanguageService,
} from '../../test/test-helpers';

describe('Unit: PresetsSettingsPropertyCompletionProvider', () => {
const rootUri = 'file:///root/';
let jsonLanguageService: JSONLanguageService;
let documentManager: DocumentManager;

beforeEach(async () => {
documentManager = new DocumentManager(
undefined, // don't need a fs
undefined, // don't need a connection
undefined, // don't need client capabilities
async () => 'theme', // 'theme' mode
async () => false, // schema is invalid for tests
);
jsonLanguageService = mockJSONLanguageService(
rootUri,
documentManager,
async (uri: string) => ({
general: {
fake: 'Fake Setting',
},
}),
);

await jsonLanguageService.setup({
textDocument: {
completion: {
contextSupport: true,
completionItem: {
snippetSupport: true,
commitCharactersSupport: true,
documentationFormat: ['markdown'],
deprecatedSupport: true,
preselectSupport: true,
},
},
},
});
});

describe('valid schema', () => {
it('completes preset setting property when settings exist', async () => {
const source = `
{% schema %}
{
"settings": [
{"id": "custom-setting"},
{"id": "fake-setting"},
{},
],
"presets": [{
"settings": {
"█": "",
}
}]
}
{% endschema %}
`;
const params = getRequestParams(documentManager, 'blocks/block.liquid', source);
const completions = await jsonLanguageService.completions(params);

assert(isCompletionList(completions));
expect(completions.items).to.have.lengthOf(2);
expect(completions.items.map((item) => item.label)).to.include.members([
`"custom-setting"`,
`"fake-setting"`,
]);
});

it('offers no suggestions for preset setting property when there are no settings', async () => {
const source = `
{% schema %}
{
"presets": [{
"settings": {
"█": "",
}
}]
}
{% endschema %}
`;
const params = getRequestParams(documentManager, 'blocks/block.liquid', source);
const completions = await jsonLanguageService.completions(params);

assert(isCompletionList(completions));
expect(completions.items).to.have.lengthOf(0);
expect(completions.items.map((item) => item.label)).to.include.members([]);
});

it('offers presets setting completion with docs from setting.label', async () => {
const source = `
{% schema %}
{
"settings": [
{"id": "custom-setting", "label": "Custom Setting"},
{"id": "fake-setting", "label": "t:general.fake"},
],
"presets": [{
"settings": {
"█": "",
}
}]
}
{% endschema %}
`;
const params = getRequestParams(documentManager, 'blocks/block.liquid', source);
const completions = await jsonLanguageService.completions(params);

assert(isCompletionList(completions));
expect(completions.items).to.have.lengthOf(2);
expect(completions.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
documentation: { kind: 'markdown', value: 'Custom Setting' },
}),
expect.objectContaining({
documentation: { kind: 'markdown', value: 'Fake Setting' },
}),
]),
);
});
});

describe('invalid schema', () => {
it('completes preset setting property when settings exist and schema has small errors', async () => {
const source = `
{% schema %}
{
"settings": [
{"id": "custom-setting"},
{"id": "fake-setting"},
{},
],
"presets": [{
"settings": {
"█"
}
}]
}
{% endschema %}
`;
const params = getRequestParams(documentManager, 'blocks/block.liquid', source);
const completions = await jsonLanguageService.completions(params);

assert(isCompletionList(completions));
expect(completions.items).to.have.lengthOf(2);
expect(completions.items.map((item) => item.label)).to.include.members([
`"custom-setting"`,
`"fake-setting"`,
]);
});
});
});
Loading

0 comments on commit c60e61b

Please sign in to comment.