Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/theme-check-common/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@shopify/theme-check-common",
"version": "3.26.1",
"version": "3.26.1-flow.0",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to also update theme-language-server-common's package.json so it uses it

"license": "MIT",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
19 changes: 19 additions & 0 deletions packages/theme-check-common/src/AugmentedThemeDocset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,25 @@ export class AugmentedThemeDocset implements ThemeDocset {

public isAugmented = true;

private objectsByPrefix = new Map<string, ObjectEntry[]>();

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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A URI under step-10 can match a previously registered step-1 prefix, and overlapping prefixes will not pick the most specific match. This should use a normalized prefix boundary and/or longest-prefix match.

if (uri.startsWith(prefix)) return objects;
}

return undefined;
}

filters = memo(async (): Promise<FilterEntry[]> => {
const officialFilters = await this.themeDocset.filters();
return [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand All @@ -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 }) => {
Expand Down
3 changes: 3 additions & 0 deletions packages/theme-check-common/src/types/theme-liquid-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export interface ThemeDocset {

/** Returns system translations available on themes. */
systemTranslations(): Promise<Translations>;

/** Returns objects scoped to a specific URI, if available. */
getObjectsForURI?(uri: string): ObjectEntry[] | undefined;
}

/** A URI that will uniquely describe the schema */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ export class ClientCapabilities {
return !!this.capabilities?.window?.workDoneProgress;
}

get supportsSetObjects(): boolean {
return this.initializationOption('shopify.supportsSetObjects', false);
}

initializationOption<T>(key: string, defaultValue: T): T {
// { 'themeCheck.checkOnSave': true }
const direct = this.initializationOptions?.[key];
Expand Down
49 changes: 38 additions & 11 deletions packages/theme-language-server-common/src/TypeSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export class TypeSystem {
private readonly getThemeSettingsSchemaForURI: GetThemeSettingsSchemaForURI,
private readonly getMetafieldDefinitions: (rootUri: string) => Promise<MetafieldDefinitionMap>,
private readonly getModeForURI: GetModeForURI = async () => 'theme',
private readonly isUriScopedMode: () => boolean = () => false,
) {}

async inferType(
Expand Down Expand Up @@ -123,7 +124,7 @@ export class TypeSystem {
*/
public objectMap = async (uri: string, ast: LiquidHtmlNode): Promise<ObjectMap> => {
const [objectMap, themeSettingProperties, metafieldDefinitionsObjectMap] = await Promise.all([
this._objectMap(),
this._objectMap(uri),
this.themeSettingProperties(uri),
this.metafieldDefinitionsObjectMap(uri),
]);
Expand Down Expand Up @@ -261,9 +262,19 @@ export class TypeSystem {
return result;
}

// This is the big one we reuse (memoized)
private _objectMap = memo(async (): Promise<ObjectMap> => {
const entries = await this.objectEntries();
private async _objectMap(uri?: string): Promise<ObjectMap> {
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<ObjectMap> => {
const entries = await this._memoObjectEntries();
return entries.reduce((map, entry) => {
map[entry.name] = entry;
return map;
Expand All @@ -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<ObjectEntry[]> {
if (this.isUriScopedMode() && uri && this.themeDocset.getObjectsForURI) {
const perURI = this.themeDocset.getObjectsForURI(uri);
if (perURI) return perURI;
}
Comment on lines +302 to +305

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we okay if Flow sends only step-specific objects, built-in Shopify types and augmented objects disappear for completion/hover/diagnostics? More question than concern here.

return this._memoObjectEntries();
}

private async symbolsTable(partialAst: LiquidHtmlNode, uri: string): Promise<SymbolsTable> {
const seedSymbolsTable = await this.seedSymbolsTable(uri);
Expand All @@ -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) => {
Expand All @@ -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));
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export interface CompletionProviderDependencies {
getDocDefinitionForURI?: GetDocDefinitionForURI;
getThemeBlockNames?: (rootUri: string, includePrivate: boolean) => Promise<string[]>;
getModeForURI?: (uri: string) => Promise<Mode>;
isUriScopedMode?: () => boolean;
log?: (message: string) => void;
}

Expand All @@ -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;
Expand All @@ -71,6 +73,7 @@ export class CompletionsProvider {
getThemeSettingsSchemaForURI,
getMetafieldDefinitions,
getModeForURI,
isUriScopedMode,
);

this.providers = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,14 @@ export class HoverProvider {
readonly getSettingsSchemaForURI: GetThemeSettingsSchemaForURI = async () => [],
readonly getDocDefinitionForURI: GetDocDefinitionForURI = async () => undefined,
readonly getModeForURI: (uri: string) => Promise<Mode> = async () => 'theme',
readonly isUriScopedMode: () => boolean = () => false,
) {
const typeSystem = new TypeSystem(
themeDocset,
getSettingsSchemaForURI,
getMetafieldDefinitions,
getModeForURI,
isUriScopedMode,
);
this.providers = [
new ContentForArgumentHoverProvider(getDocDefinitionForURI),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { RenameHandler } from '../renamed/RenameHandler';
import { GetTranslationsForURI } from '../translations';
import {
Dependencies,
SetObjectsNotification,
ThemeGraphDeadCodeRequest,
ThemeGraphDependenciesRequest,
ThemeGraphReferenceRequest,
Expand Down Expand Up @@ -341,6 +342,7 @@ export function startServer(
getMetafieldDefinitions,
getDocDefinitionForURI,
getModeForURI,
isUriScopedMode: () => clientCapabilities.supportsSetObjects,
});
const hoverProvider = new HoverProvider(
documentManager,
Expand All @@ -350,6 +352,7 @@ export function startServer(
getThemeSettingsSchemaForURI,
getDocDefinitionForURI,
getModeForURI,
() => clientCapabilities.supportsSetObjects,
);

const executeCommandProvider = new ExecuteCommandProvider(
Expand Down Expand Up @@ -762,5 +765,9 @@ export function startServer(
return deadFiles;
});

connection.onNotification(SetObjectsNotification.type, ({ uri, objects }) => {
themeDocset.setObjectsForURI(uri, objects);
});
Comment on lines +768 to +770

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why a notification and not a request? Does the client not care about if/when this resolves? Don't care to await to make sure the objects are set before proceeding?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

100% valid concern

e.g.

  • flow opens a config field doc
  • the doc contains some object
  • the language server receives textDocument/didOpen
  • this triggers runChecks
  • at this point Flow hasn't sent shopify/setObjects
  • unknown object occurs


connection.listen();
}
10 changes: 10 additions & 0 deletions packages/theme-language-server-common/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<Params>(method);
}

export type AugmentedLocationWithExistence = {
uri: string;
range: undefined;
Expand Down
Loading