diff --git a/packages/theme-check-common/package.json b/packages/theme-check-common/package.json index eaa58726f..4ff125d48 100644 --- a/packages/theme-check-common/package.json +++ b/packages/theme-check-common/package.json @@ -1,6 +1,6 @@ { "name": "@shopify/theme-check-common", - "version": "3.26.1", + "version": "3.26.1-flow.0", "license": "MIT", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/theme-check-common/src/AugmentedThemeDocset.ts b/packages/theme-check-common/src/AugmentedThemeDocset.ts index b168b8335..91126d401 100644 --- a/packages/theme-check-common/src/AugmentedThemeDocset.ts +++ b/packages/theme-check-common/src/AugmentedThemeDocset.ts @@ -103,6 +103,25 @@ export class AugmentedThemeDocset implements ThemeDocset { public isAugmented = true; + private objectsByPrefix = new Map(); + + setObjectsForURI(uri: string, objects: ObjectEntry[]) { + this.objectsByPrefix.set(uri, objects); + } + + getObjectsForURI(uri: string): ObjectEntry[] | undefined { + // Exact match first + const exact = this.objectsByPrefix.get(uri); + if (exact) return exact; + + // Prefix match: allows registering by step path and matching file URIs within it + for (const [prefix, objects] of this.objectsByPrefix) { + if (uri.startsWith(prefix)) return objects; + } + + return undefined; + } + filters = memo(async (): Promise => { const officialFilters = await this.themeDocset.filters(); return [ diff --git a/packages/theme-check-common/src/checks/undefined-object/index.ts b/packages/theme-check-common/src/checks/undefined-object/index.ts index 9057bbb94..8ea558974 100644 --- a/packages/theme-check-common/src/checks/undefined-object/index.ts +++ b/packages/theme-check-common/src/checks/undefined-object/index.ts @@ -146,7 +146,7 @@ export const UndefinedObject: LiquidCheckDefinition = { }, async onCodePathEnd() { - const objects = await globalObjects(themeDocset, relativePath, context.mode); + const objects = await globalObjects(themeDocset, relativePath, context.file.uri, context.mode); objects.forEach((obj) => fileScopedVariables.add(obj.name)); @@ -172,8 +172,9 @@ export const UndefinedObject: LiquidCheckDefinition = { }, }; -async function globalObjects(themeDocset: ThemeDocset, relativePath: string, mode: Mode = 'theme') { - const objects = await themeDocset.objects(); +async function globalObjects(themeDocset: ThemeDocset, relativePath: string, uri?: string, mode: Mode = 'theme') { + const perURI = uri ? themeDocset.getObjectsForURI?.(uri) : undefined; + const objects = perURI ?? (await themeDocset.objects()); const contextualObjects = getContextualObjects(relativePath, mode); const globalObjects = objects.filter(({ access, name }) => { diff --git a/packages/theme-check-common/src/types/theme-liquid-docs.ts b/packages/theme-check-common/src/types/theme-liquid-docs.ts index 38d404518..94e03ba5c 100644 --- a/packages/theme-check-common/src/types/theme-liquid-docs.ts +++ b/packages/theme-check-common/src/types/theme-liquid-docs.ts @@ -21,6 +21,9 @@ export interface ThemeDocset { /** Returns system translations available on themes. */ systemTranslations(): Promise; + + /** Returns objects scoped to a specific URI, if available. */ + getObjectsForURI?(uri: string): ObjectEntry[] | undefined; } /** A URI that will uniquely describe the schema */ diff --git a/packages/theme-language-server-common/src/ClientCapabilities.ts b/packages/theme-language-server-common/src/ClientCapabilities.ts index 8a45be78b..ec519c630 100644 --- a/packages/theme-language-server-common/src/ClientCapabilities.ts +++ b/packages/theme-language-server-common/src/ClientCapabilities.ts @@ -43,6 +43,10 @@ export class ClientCapabilities { return !!this.capabilities?.window?.workDoneProgress; } + get supportsSetObjects(): boolean { + return this.initializationOption('shopify.supportsSetObjects', false); + } + initializationOption(key: string, defaultValue: T): T { // { 'themeCheck.checkOnSave': true } const direct = this.initializationOptions?.[key]; diff --git a/packages/theme-language-server-common/src/TypeSystem.ts b/packages/theme-language-server-common/src/TypeSystem.ts index 9b8859d3c..2425b90bb 100644 --- a/packages/theme-language-server-common/src/TypeSystem.ts +++ b/packages/theme-language-server-common/src/TypeSystem.ts @@ -48,6 +48,7 @@ export class TypeSystem { private readonly getThemeSettingsSchemaForURI: GetThemeSettingsSchemaForURI, private readonly getMetafieldDefinitions: (rootUri: string) => Promise, private readonly getModeForURI: GetModeForURI = async () => 'theme', + private readonly isUriScopedMode: () => boolean = () => false, ) {} async inferType( @@ -123,7 +124,7 @@ export class TypeSystem { */ public objectMap = async (uri: string, ast: LiquidHtmlNode): Promise => { const [objectMap, themeSettingProperties, metafieldDefinitionsObjectMap] = await Promise.all([ - this._objectMap(), + this._objectMap(uri), this.themeSettingProperties(uri), this.metafieldDefinitionsObjectMap(uri), ]); @@ -261,9 +262,19 @@ export class TypeSystem { return result; } - // This is the big one we reuse (memoized) - private _objectMap = memo(async (): Promise => { - const entries = await this.objectEntries(); + private async _objectMap(uri?: string): Promise { + if (!this.isUriScopedMode()) return this._memoObjectMap(); + const entries = await this.objectEntries(uri); + return entries.reduce((map, entry) => { + map[entry.name] = entry; + return map; + }, {} as ObjectMap); + } + + private _memoObjectEntries = memo(() => this.themeDocset.objects()); + + private _memoObjectMap = memo(async (): Promise => { + const entries = await this._memoObjectEntries(); return entries.reduce((map, entry) => { map[entry.name] = entry; return map; @@ -283,9 +294,17 @@ export class TypeSystem { return this.themeDocset.filters(); }); - public objectEntries = memo(async () => { - return this.themeDocset.objects(); - }); + public hasObjectsForURI(uri: string): boolean { + return !!this.themeDocset.getObjectsForURI?.(uri); + } + + public async objectEntries(uri?: string): Promise { + if (this.isUriScopedMode() && uri && this.themeDocset.getObjectsForURI) { + const perURI = this.themeDocset.getObjectsForURI(uri); + if (perURI) return perURI; + } + return this._memoObjectEntries(); + } private async symbolsTable(partialAst: LiquidHtmlNode, uri: string): Promise { const seedSymbolsTable = await this.seedSymbolsTable(uri); @@ -303,7 +322,7 @@ export class TypeSystem { */ private seedSymbolsTable = async (uri: string) => { const [globalVariables, contextualVariables] = await Promise.all([ - this.globalVariables(), + this.globalVariables(uri), this.contextualVariables(uri), ]); return globalVariables.concat(contextualVariables).reduce((table, objectEntry) => { @@ -317,15 +336,23 @@ export class TypeSystem { }, {} as SymbolsTable); }; - private globalVariables = memo(async () => { - const entries = await this.objectEntries(); + private _memoGlobalVariables = memo(async () => { + const entries = await this._memoObjectEntries(); return entries.filter( (entry) => !entry.access || entry.access.global === true || entry.access.template.length > 0, ); }); + private globalVariables = async (uri?: string) => { + if (!this.isUriScopedMode()) return this._memoGlobalVariables(); + const entries = await this.objectEntries(uri); + return entries.filter( + (entry) => !entry.access || entry.access.global === true || entry.access.template.length > 0, + ); + }; + private contextualVariables = async (uri: string) => { - const [entries, mode] = await Promise.all([this.objectEntries(), this.getModeForURI(uri)]); + const [entries, mode] = await Promise.all([this.objectEntries(uri), this.getModeForURI(uri)]); const contextualEntries = getContextualEntries(uri, mode); return entries.filter((entry) => contextualEntries.includes(entry.name)); }; diff --git a/packages/theme-language-server-common/src/completions/CompletionsProvider.ts b/packages/theme-language-server-common/src/completions/CompletionsProvider.ts index 64704916d..eff2013e1 100644 --- a/packages/theme-language-server-common/src/completions/CompletionsProvider.ts +++ b/packages/theme-language-server-common/src/completions/CompletionsProvider.ts @@ -42,6 +42,7 @@ export interface CompletionProviderDependencies { getDocDefinitionForURI?: GetDocDefinitionForURI; getThemeBlockNames?: (rootUri: string, includePrivate: boolean) => Promise; getModeForURI?: (uri: string) => Promise; + isUriScopedMode?: () => boolean; log?: (message: string) => void; } @@ -61,6 +62,7 @@ export class CompletionsProvider { getDocDefinitionForURI = async (uri, _relativePath) => ({ uri }), getThemeBlockNames = async (_rootUri: string, _includePrivate: boolean) => [], getModeForURI, + isUriScopedMode = () => false, log = () => {}, }: CompletionProviderDependencies) { this.documentManager = documentManager; @@ -71,6 +73,7 @@ export class CompletionsProvider { getThemeSettingsSchemaForURI, getMetafieldDefinitions, getModeForURI, + isUriScopedMode, ); this.providers = [ diff --git a/packages/theme-language-server-common/src/completions/providers/ObjectAttributeCompletionProvider.ts b/packages/theme-language-server-common/src/completions/providers/ObjectAttributeCompletionProvider.ts index 3f8c90cd3..fec08da43 100644 --- a/packages/theme-language-server-common/src/completions/providers/ObjectAttributeCompletionProvider.ts +++ b/packages/theme-language-server-common/src/completions/providers/ObjectAttributeCompletionProvider.ts @@ -45,12 +45,15 @@ export class ObjectAttributeCompletionProvider implements Provider { partialAst, params.textDocument.uri, ); + const hasCustomObjects = this.typeSystem.hasObjectsForURI(params.textDocument.uri); if (isArrayType(parentType)) { + if (hasCustomObjects) return []; return completionItems( ArrayCoreProperties.map((name) => ({ name })), partial, ); } else if (parentType === 'string') { + if (hasCustomObjects) return []; return completionItems( StringCoreProperties.map((name) => ({ name })), partial, diff --git a/packages/theme-language-server-common/src/hover/HoverProvider.ts b/packages/theme-language-server-common/src/hover/HoverProvider.ts index 22210611c..edb17db7c 100644 --- a/packages/theme-language-server-common/src/hover/HoverProvider.ts +++ b/packages/theme-language-server-common/src/hover/HoverProvider.ts @@ -39,12 +39,14 @@ export class HoverProvider { readonly getSettingsSchemaForURI: GetThemeSettingsSchemaForURI = async () => [], readonly getDocDefinitionForURI: GetDocDefinitionForURI = async () => undefined, readonly getModeForURI: (uri: string) => Promise = async () => 'theme', + readonly isUriScopedMode: () => boolean = () => false, ) { const typeSystem = new TypeSystem( themeDocset, getSettingsSchemaForURI, getMetafieldDefinitions, getModeForURI, + isUriScopedMode, ); this.providers = [ new ContentForArgumentHoverProvider(getDocDefinitionForURI), diff --git a/packages/theme-language-server-common/src/server/startServer.ts b/packages/theme-language-server-common/src/server/startServer.ts index a13d4164b..3c13a6e8f 100644 --- a/packages/theme-language-server-common/src/server/startServer.ts +++ b/packages/theme-language-server-common/src/server/startServer.ts @@ -47,6 +47,7 @@ import { RenameHandler } from '../renamed/RenameHandler'; import { GetTranslationsForURI } from '../translations'; import { Dependencies, + SetObjectsNotification, ThemeGraphDeadCodeRequest, ThemeGraphDependenciesRequest, ThemeGraphReferenceRequest, @@ -341,6 +342,7 @@ export function startServer( getMetafieldDefinitions, getDocDefinitionForURI, getModeForURI, + isUriScopedMode: () => clientCapabilities.supportsSetObjects, }); const hoverProvider = new HoverProvider( documentManager, @@ -350,6 +352,7 @@ export function startServer( getThemeSettingsSchemaForURI, getDocDefinitionForURI, getModeForURI, + () => clientCapabilities.supportsSetObjects, ); const executeCommandProvider = new ExecuteCommandProvider( @@ -762,5 +765,9 @@ export function startServer( return deadFiles; }); + connection.onNotification(SetObjectsNotification.type, ({ uri, objects }) => { + themeDocset.setObjectsForURI(uri, objects); + }); + connection.listen(); } diff --git a/packages/theme-language-server-common/src/types.ts b/packages/theme-language-server-common/src/types.ts index 57acb1420..b986525c4 100644 --- a/packages/theme-language-server-common/src/types.ts +++ b/packages/theme-language-server-common/src/types.ts @@ -2,6 +2,7 @@ import { AbstractFileSystem, Config, Dependencies as ThemeCheckDependencies, + ObjectEntry, } from '@shopify/theme-check-common'; import { URI } from 'vscode-languageserver'; import * as rpc from 'vscode-jsonrpc'; @@ -148,6 +149,15 @@ export namespace ThemeGraphDidUpdateNotification { } } +export namespace SetObjectsNotification { + export const method = 'shopify/setObjects'; + export interface Params { + uri: string; + objects: ObjectEntry[]; + } + export const type = new rpc.NotificationType(method); +} + export type AugmentedLocationWithExistence = { uri: string; range: undefined;