Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,74 @@ function createMockFileSystem(files: Record<string, string>): AbstractFileSystem
describe('DocumentsLocator', () => {
const rootUri = URI.parse('file:///project');

describe('locateDefault', () => {
it('render → app/views/partials', () => {
const locator = new DocumentsLocator(createMockFileSystem({}));
expect(locator.locateDefault(rootUri, 'render', 'my/partial')).toBe(
'file:///project/app/views/partials/my/partial.liquid',
);
});

it('include → app/views/partials', () => {
const locator = new DocumentsLocator(createMockFileSystem({}));
expect(locator.locateDefault(rootUri, 'include', 'card')).toBe(
'file:///project/app/views/partials/card.liquid',
);
});

it('function → app/lib', () => {
const locator = new DocumentsLocator(createMockFileSystem({}));
expect(locator.locateDefault(rootUri, 'function', 'commands/apply')).toBe(
'file:///project/app/lib/commands/apply.liquid',
);
});

it('graphql → app/graphql', () => {
const locator = new DocumentsLocator(createMockFileSystem({}));
expect(locator.locateDefault(rootUri, 'graphql', 'users/search')).toBe(
'file:///project/app/graphql/users/search.graphql',
);
});

it('theme_render_rc → undefined', () => {
const locator = new DocumentsLocator(createMockFileSystem({}));
expect(locator.locateDefault(rootUri, 'theme_render_rc', 'card')).toBeUndefined();
});

it('module render → modules/.../public/views/partials', () => {
const locator = new DocumentsLocator(createMockFileSystem({}));
expect(locator.locateDefault(rootUri, 'render', 'modules/core/my/partial')).toBe(
'file:///project/modules/core/public/views/partials/my/partial.liquid',
);
});

it('module function → modules/.../public/lib', () => {
const locator = new DocumentsLocator(createMockFileSystem({}));
expect(locator.locateDefault(rootUri, 'function', 'modules/core/commands/apply')).toBe(
'file:///project/modules/core/public/lib/commands/apply.liquid',
);
});

it('module graphql → modules/.../public/graphql', () => {
const locator = new DocumentsLocator(createMockFileSystem({}));
expect(locator.locateDefault(rootUri, 'graphql', 'modules/core/users/search')).toBe(
'file:///project/modules/core/public/graphql/users/search.graphql',
);
});

it('deeply nested path — creates all missing intermediate dirs', () => {
const locator = new DocumentsLocator(createMockFileSystem({}));
expect(locator.locateDefault(rootUri, 'render', 'a/b/c/d/partial')).toBe(
'file:///project/app/views/partials/a/b/c/d/partial.liquid',
);
});

it('asset → undefined (no canonical creation path)', () => {
const locator = new DocumentsLocator(createMockFileSystem({}));
expect(locator.locateDefault(rootUri, 'asset', 'logo.png')).toBeUndefined();
});
});

describe('locate', () => {
it('should locate a partial file in app/lib', async () => {
const fs = createMockFileSystem({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,39 @@ export class DocumentsLocator {
return undefined;
}

/**
* Returns the canonical default URI for a missing file so it can be created.
* Returns undefined for theme_render_rc (ambiguous search path) and asset.
*/
locateDefault(rootUri: URI, nodeName: DocumentType, fileName: string): string | undefined {
const parsed = this.parseModulePath(fileName);

let basePath: string;
let ext: string;

switch (nodeName) {
case 'render':
case 'include':
basePath = parsed.isModule
? `modules/${parsed.moduleName}/public/views/partials`
: 'app/views/partials';
ext = '.liquid';
break;
case 'function':
basePath = parsed.isModule ? `modules/${parsed.moduleName}/public/lib` : 'app/lib';
ext = '.liquid';
break;
case 'graphql':
basePath = parsed.isModule ? `modules/${parsed.moduleName}/public/graphql` : 'app/graphql';
ext = '.graphql';
break;
default:
return undefined;
}

return Utils.joinPath(rootUri, basePath, parsed.key + ext).toString();
}

async locate(
rootUri: URI,
nodeName: DocumentType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,17 @@ describe('RenderPartialDefinitionProvider', () => {
expect(result[0].targetUri).toBe('file:///project/app/views/partials/card.liquid');
});

it('should NOT use search paths for regular render tags', async () => {
it('should NOT use search paths for regular render tags — falls back to default creation path', async () => {
// The file only exists under a search path (theme/dress/card.liquid), not the standard path.
// render should NOT pick up search-path files; instead it falls back to the default
// app/views/partials/card.liquid creation path.
const result = await getDefinitions("{% render 'card' %}", 12, {
'project/app/config.yml': 'theme_search_paths:\n - theme/dress',
'project/app/views/partials/theme/dress/card.liquid': 'dress card',
});

expect(result).toHaveLength(0);
expect(result).toHaveLength(1);
expect(result[0].targetUri).toBe('file:///project/app/views/partials/card.liquid');
});
});

Expand Down Expand Up @@ -130,6 +134,37 @@ describe('RenderPartialDefinitionProvider', () => {
});
});

describe('missing files — fall back to default path', () => {
it('render: missing partial resolves to app/views/partials', async () => {
const result = await getDefinitions("{% render 'my/missing/partial' %}", 12, {});

expect(result).toHaveLength(1);
expect(result[0].targetUri).toBe(
'file:///project/app/views/partials/my/missing/partial.liquid',
);
});

it('function: missing partial resolves to app/lib', async () => {
const result = await getDefinitions("{% function result = 'commands/missing' %}", 24, {});

expect(result).toHaveLength(1);
expect(result[0].targetUri).toBe('file:///project/app/lib/commands/missing.liquid');
});

it('graphql: missing file resolves to app/graphql', async () => {
const result = await getDefinitions("{% graphql g = 'users/missing' %}", 18, {});

expect(result).toHaveLength(1);
expect(result[0].targetUri).toBe('file:///project/app/graphql/users/missing.graphql');
});

it('theme_render_rc: missing file returns empty (no default path)', async () => {
const result = await getDefinitions("{% theme_render_rc 'missing' %}", 20, {});

expect(result).toHaveLength(0);
});
});

describe('non-matching nodes', () => {
it('should return empty for non-string nodes', async () => {
const result = await getDefinitions("{% assign x = 'hello' %}", 3, {});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,13 @@ export class RenderPartialDefinitionProvider implements BaseDefinitionProvider {
const root = URI.parse(rootUri);
const searchPaths = await this.searchPathsCache.get(root);
const docType = (tag as LiquidTag).name as DocumentType;
const fileUri = await this.documentsLocator.locate(
root,
docType,
(node as LiquidString).value,
searchPaths,
);
const fileUri =
(await this.documentsLocator.locate(
root,
docType,
(node as LiquidString).value,
searchPaths,
)) ?? this.documentsLocator.locateDefault(root, docType, (node as LiquidString).value);
if (!fileUri) return [];

const sourceCode = this.documentManager.get(params.textDocument.uri);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,18 +64,18 @@ function documentLinksVisitor(

// render, include, function, theme_render_rc all have a .partial field
if ('partial' in markup && isLiquidString(markup.partial)) {
return DocumentLink.create(
range(textDocument, markup.partial),
await documentsLocator.locate(root, name, markup.partial.value, searchPaths),
);
const uri =
(await documentsLocator.locate(root, name, markup.partial.value, searchPaths)) ??
documentsLocator.locateDefault(root, name, markup.partial.value);
return DocumentLink.create(range(textDocument, markup.partial), uri);
}

// graphql has a .graphql field
if ('graphql' in markup && isLiquidString(markup.graphql)) {
return DocumentLink.create(
range(textDocument, markup.graphql),
await documentsLocator.locate(root, name, markup.graphql.value),
);
const uri =
(await documentsLocator.locate(root, name, markup.graphql.value)) ??
documentsLocator.locateDefault(root, name, markup.graphql.value);
return DocumentLink.create(range(textDocument, markup.graphql), uri);
}
},
async LiquidVariable(node) {
Expand Down