Skip to content

Commit

Permalink
Support presets blocks settings completion + hover
Browse files Browse the repository at this point in the history
  • Loading branch information
aswamy committed Jan 24, 2025
1 parent 150cbcb commit 820091c
Show file tree
Hide file tree
Showing 10 changed files with 449 additions and 117 deletions.
10 changes: 10 additions & 0 deletions .changeset/selfish-ants-stare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@shopify/theme-language-server-common': minor
'@shopify/theme-language-server-node': minor
---

Support completion + hover for presets blocks settings under `{% schema %}` tag

- Hover + Completion description for `presets.[].blocks.[].settings` will be from the referenced
block's setting's label - i.e. `settings.[].label`
- The label will be translated if it contains a translation key
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import { RequestContext } from './RequestContext';
import { findSchemaNode } from './utils';
import { SettingsPropertyCompletionProvider } from './completions/providers/SettingsPropertyCompletionProvider';
import { SettingsHoverProvider } from './hover/providers/SettingsHoverProvider';
import { PresetsBlockSettingsPropertyCompletionProvider } from './completions/providers/PresetsBlockSettingsPropertyCompletionProvider';
import { PresetsBlockSettingsHoverProvider } from './hover/providers/PresetsBlockSettingsHoverProvider';

/** The getInfoContribution API will only fallback if we return undefined synchronously */
const SKIP_CONTRIBUTION = undefined as any;
Expand Down Expand Up @@ -55,11 +57,16 @@ export class JSONContributions implements JSONWorkerContribution {
new TranslationPathHoverProvider(),
new SchemaTranslationHoverProvider(getDefaultSchemaTranslations),
new SettingsHoverProvider(getDefaultSchemaTranslations),
new PresetsBlockSettingsHoverProvider(getDefaultSchemaTranslations, getThemeBlockSchema),
];
this.completionProviders = [
new SchemaTranslationsCompletionProvider(getDefaultSchemaTranslations),
new BlockTypeCompletionProvider(getThemeBlockNames),
new PresetsBlockTypeCompletionProvider(getThemeBlockNames, getThemeBlockSchema),
new PresetsBlockSettingsPropertyCompletionProvider(
getDefaultSchemaTranslations,
getThemeBlockSchema,
),
new SettingsPropertyCompletionProvider(getDefaultSchemaTranslations),
];
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { JSONPath } from 'vscode-json-languageservice';
import { GetThemeBlockSchema } from '../../JSONContributions';
import { RequestContext } from '../../RequestContext';
import { JSONCompletionItem, JSONCompletionProvider } from '../JSONCompletionProvider';
import { isSectionOrBlockFile } from '../../utils';
import { deepGet, isError, SourceCodeType } from '@shopify/theme-check-common';
import { isSectionOrBlockSchema } from './BlockTypeCompletionProvider';
import { GetTranslationsForURI } from '../../../translations';
import { schemaSettingsPropertyCompletionItems } from './helpers/schemaSettings';

/**
* The PresetsBlockSettingsPropertyCompletionProvider offers value completions of the
* `presets.[].(recursive blocks.[]).settings` keys inside section and theme block `{% schema %}` tags.
*
* @example
* {% schema %}
* {
* "presets": [
* {
* "blocks": [
* {
* "type": "block-type",
* "settings": {
* "█"
* }
* },
* ]
* },
* ]
* }
* {% endschema %}
*/
export class PresetsBlockSettingsPropertyCompletionProvider implements JSONCompletionProvider {
constructor(
private getDefaultSchemaTranslations: GetTranslationsForURI,
private getThemeBlockSchema: GetThemeBlockSchema,
) {}

async completeProperty(context: RequestContext, path: JSONPath): Promise<JSONCompletionItem[]> {
const { doc } = context;

if (doc.type !== SourceCodeType.LiquidHtml) return [];
if (!isSectionOrBlockFile(doc.uri) || !isPresetsBlocksSettingsPath(path)) {
return [];
}

const schema = await doc.getSchema();

if (!schema || !isSectionOrBlockSchema(schema) || isError(schema.parsed)) {
return [];
}

const blockType = deepGet(schema.parsed, [...path.slice(0, -1), 'type']);

if (!blockType) {
return [];
}

const blockOriginSchema = await this.getThemeBlockSchema(doc.uri, blockType);

if (
!blockOriginSchema ||
isError(blockOriginSchema.parsed) ||
!isSectionOrBlockSchema(blockOriginSchema)
) {
return [];
}

if (!blockOriginSchema.parsed?.settings || !Array.isArray(blockOriginSchema.parsed?.settings)) {
return [];
}

const translations = await this.getDefaultSchemaTranslations(doc.textDocument.uri);

return schemaSettingsPropertyCompletionItems(blockOriginSchema.parsed, translations);
}
}

// `blocks` can be nested within other `blocks`
// We need to ensure the last leg of the path is { "blocks": [{ "settings": { "█" } }] }
function isPresetsBlocksSettingsPath(path: JSONPath) {
return path.at(0) === 'presets' && path.at(-3) === 'blocks' && path.at(-1) === 'settings';
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
isCompletionList,
mockJSONLanguageService,
} from '../../test/test-helpers';
import { SourceCodeType, ThemeBlock } from '@shopify/theme-check-common';

describe('Unit: PresetsBlockTypeCompletionProvider', () => {
const rootUri = 'file:///root/';
Expand Down Expand Up @@ -130,63 +129,4 @@ describe('Unit: PresetsBlockTypeCompletionProvider', () => {
});
}
});

describe('invalid schema', () => {
it('does not complete when schema is invalid', async () => {
const source = `
{% schema %}
typo
{
"blocks": [{"type": "@theme"}],
"presets": [{
"blocks": [
{ "type": "█" },
]
}]
}
{% endschema %}
`;

const params = getRequestParams(documentManager, 'sections/section.liquid', source);
const completions = await jsonLanguageService.completions(params);

assert(isCompletionList(completions));
expect(completions.items).to.have.lengthOf(0);
});

it('does not complete when parent block schema is invalid', async () => {
const source = `
{% schema %}
{
"blocks": [{"type": "custom-block"}],
"presets": [{
"blocks": [
{
"type": "custom-block",
"blocks": [{"type": "█"}],
},
]
}]
}
{% endschema %}
`;

documentManager.open(
`${rootUri}/blocks/custom-block.liquid`,
`{% schema %}
typo
{
"blocks": [{"type": "@theme"}]
}
{% endschema %}`,
1,
);

const params = getRequestParams(documentManager, 'sections/section.liquid', source);
const completions = await jsonLanguageService.completions(params);

assert(isCompletionList(completions));
expect(completions.items).to.have.lengthOf(0);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { parse as jsonParse } from 'jsonc-parser';
import { isError, SourceCodeType } from '@shopify/theme-check-common';
import { JSONPath } from 'vscode-json-languageservice';
import { JSONCompletionItem } from 'vscode-json-languageservice/lib/umd/jsonContributions';
import { RequestContext } from '../../RequestContext';
import { isBlockFile, isSectionFile } from '../../utils';
import { JSONCompletionProvider } from '../JSONCompletionProvider';
import { GetTranslationsForURI } from '../../../translations';
import { isSectionOrBlockSchema } from './BlockTypeCompletionProvider';
import { CompletionItemKind } from 'vscode-languageserver-protocol';
import { GetTranslationsForURI, renderTranslation, translationValue } from '../../../translations';
import { schemaSettingsPropertyCompletionItems } from './helpers/schemaSettings';

/**
* The SettingsPropertyCompletionProvider offers property completions for:
Expand Down Expand Up @@ -36,76 +35,34 @@ export class SettingsPropertyCompletionProvider implements JSONCompletionProvide
constructor(public getDefaultSchemaTranslations: GetTranslationsForURI) {}

async completeProperty(context: RequestContext, path: JSONPath): Promise<JSONCompletionItem[]> {
if (context.doc.type !== SourceCodeType.LiquidHtml) return [];
const { doc } = context;

if (doc.type !== SourceCodeType.LiquidHtml) return [];

// section files can have schemas with `presets` and `default`
// block files can have schemas with `presets` only
if (
!(
isSectionFile(context.doc.uri) &&
(isPresetSettingsPath(path) || isDefaultSettingsPath(path))
) &&
!(isBlockFile(context.doc.uri) && isPresetSettingsPath(path))
!(isSectionFile(doc.uri) && (isPresetSettingsPath(path) || isDefaultSettingsPath(path))) &&
!(isBlockFile(doc.uri) && isPresetSettingsPath(path))
) {
return [];
}

const { doc } = context;
const schema = await doc.getSchema();

if (!schema || !isSectionOrBlockSchema(schema)) {
if (!schema || !isSectionOrBlockSchema(schema) || isError(schema.parsed)) {
return [];
}

let parsedSchema: any;

/**
* Since we are auto-completing JSON properties, we could be in a state where the schema is invalid.
* E.g.
* {
* "█"
* }
*
* In that case, we manually parse the schema ourselves with a more fault-tolerant approach.
*/
if (isError(schema.parsed)) {
parsedSchema = jsonParse(schema.value);
} else {
parsedSchema = schema.parsed;
}
const parsedSchema = schema.parsed;

if (!parsedSchema?.settings || !Array.isArray(parsedSchema.settings)) {
return [];
}

const translations = await this.getDefaultSchemaTranslations(doc.textDocument.uri);

return parsedSchema.settings
.filter((setting: any) => setting.id)
.map((setting: any) => {
let docValue = '';

if (setting.label) {
if (setting.label.startsWith('t:')) {
const translation = translationValue(setting.label.substring(2), translations);
if (translation) {
docValue = renderTranslation(translation);
}
} else {
docValue = setting.label;
}
}

return {
kind: CompletionItemKind.Property,
label: `"${setting.id}"`,
insertText: `"${setting.id}"`,
documentation: {
kind: 'markdown',
value: docValue,
},
};
});
return schemaSettingsPropertyCompletionItems(parsedSchema, translations);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { CompletionItemKind, MarkupKind } from 'vscode-json-languageservice';
import { renderTranslation, translationValue } from '../../../../translations';
import { Translations } from '@shopify/theme-check-common';

export function schemaSettingsPropertyCompletionItems(
parsedSchema: any,
translations: Translations,
) {
return parsedSchema.settings
.filter((setting: any) => setting.id)
.map((setting: any) => {
let docValue = '';

if (setting.label) {
if (setting.label.startsWith('t:')) {
const translation = translationValue(setting.label.substring(2), translations);

if (translation) {
docValue = renderTranslation(translation);
}
} else {
docValue = setting.label;
}
}

const completionText = setting.id ? `"${setting.id}"` : '';

return {
kind: CompletionItemKind.Property,
label: completionText,
insertText: completionText,
documentation: {
kind: MarkupKind.Markdown,
value: docValue,
},
};
});
}
Loading

0 comments on commit 820091c

Please sign in to comment.