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>

123 lines (108 loc) 4.37 kB
import { JSONNode, SourceCodeType } from '@shopify/theme-check-common'; import { CompletionsCollector, JSONPath, JSONWorkerContribution, MarkedString, } from 'vscode-json-languageservice'; import { DocumentManager } from '../documents'; import { extractParams, paramsString } from '../translations'; function nodeAtLocation(ast: JSONNode, location: JSONPath): JSONNode | undefined { return location.reduce((value: JSONNode | undefined, segment: string | number) => { if (value && typeof value !== 'string') { switch (value.type) { case 'Object': { return value.children.find((child) => child.key.value === segment)?.value; } case 'Array': { if (typeof segment !== 'number') return undefined; return value.children[segment]; } case 'Identifier': { return undefined; // trying to [segment] onto a string or number } case 'Literal': { return undefined; // trying to [segment] onto a string or number } case 'Property': { return undefined; // this shouldn't be happening } } } }, ast); } const nothing = undefined as any; export class TranslationFileContributions implements JSONWorkerContribution { private filePatterns = [/^.*\/locales\/[^\/]*\.json$/]; constructor(private documentManager: DocumentManager) {} getInfoContribution(uri: string, location: JSONPath): Promise<MarkedString[]> { // TODO: This is a hack to get around the fact that the JSON language service // actually is not typed properly and performs "if-undefined-skip" logic. // https://github.com/microsoft/vscode-json-languageservice/pull/222 // would fix this, but it's not merged yet. if (!fileMatch(uri, this.filePatterns)) return nothing; const doc = this.documentManager.get(uri); if (!doc || location.length === 0 || doc.type !== SourceCodeType.JSON) return nothing; const ast = doc.ast; if (ast instanceof Error) return nothing; const node = nodeAtLocation(ast, location); switch (true) { // Because the JSON language service doesn't support composition of hover info, // We have to hardcode the docs for the translation file schema here. case ['zero', 'one', 'two', 'few', 'many', 'other'].includes(location.at(-1) as string): { if (!node || node.type !== 'Literal' || typeof node.value !== 'string') { return Promise.resolve([`Pluralized translations should have a string value`]); } return Promise.resolve([contextualizedLabel(uri, location.slice(0, -1), node.value)]); } case location.at(-1)!.toString().endsWith('_html'): { if (!node || node.type !== 'Literal' || typeof node.value !== 'string') { return Promise.resolve([`Translations ending in '_html' should have a string value`]); } return Promise.resolve([ contextualizedLabel(uri, location, node.value), `The '_html' suffix prevents the HTML content from being escaped.`, ]); } default: { if (!node || node.type !== 'Literal' || typeof node.value !== 'string') { return Promise.resolve([`Translation group: ${location.join('.')}`]); } return Promise.resolve([contextualizedLabel(uri, location, node.value)]); } } } async collectDefaultCompletions(uri: string, result: CompletionsCollector) {} async collectPropertyCompletions( uri: string, location: JSONPath, currentWord: string, addValue: boolean, isLast: boolean, result: CompletionsCollector, ) {} async collectValueCompletions( uri: string, location: JSONPath, propertyKey: string, result: CompletionsCollector, ) {} } export function fileMatch(uri: string, patterns: RegExp[]): boolean { return patterns.some((pattern) => pattern.test(uri)); } export function contextualizedLabel( uri: string, str: (string | number)[], value: string, ): MarkedString { if (uri.includes('.schema')) { return marked(`"t:${str.join('.')}"`, 'json'); } else { const params = extractParams(value); return marked(`{{ '${str.join('.')}' | t${paramsString(params)} }}`, 'liquid'); } } function marked(value: string, language = 'liquid'): { language: string; value: string } { return { language, value }; }