diff --git a/packages/platformos-common/src/documents-locator/DocumentsLocator.spec.ts b/packages/platformos-common/src/documents-locator/DocumentsLocator.spec.ts index af59b76..f40342b 100644 --- a/packages/platformos-common/src/documents-locator/DocumentsLocator.spec.ts +++ b/packages/platformos-common/src/documents-locator/DocumentsLocator.spec.ts @@ -55,6 +55,74 @@ function createMockFileSystem(files: Record): 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({ diff --git a/packages/platformos-common/src/documents-locator/DocumentsLocator.ts b/packages/platformos-common/src/documents-locator/DocumentsLocator.ts index f130d5c..b3f78e0 100644 --- a/packages/platformos-common/src/documents-locator/DocumentsLocator.ts +++ b/packages/platformos-common/src/documents-locator/DocumentsLocator.ts @@ -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, diff --git a/packages/platformos-language-server-common/src/definitions/providers/RenderPartialDefinitionProvider.spec.ts b/packages/platformos-language-server-common/src/definitions/providers/RenderPartialDefinitionProvider.spec.ts index 8568486..b8d2bce 100644 --- a/packages/platformos-language-server-common/src/definitions/providers/RenderPartialDefinitionProvider.spec.ts +++ b/packages/platformos-language-server-common/src/definitions/providers/RenderPartialDefinitionProvider.spec.ts @@ -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'); }); }); @@ -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, {}); diff --git a/packages/platformos-language-server-common/src/definitions/providers/RenderPartialDefinitionProvider.ts b/packages/platformos-language-server-common/src/definitions/providers/RenderPartialDefinitionProvider.ts index df05c75..87195ef 100644 --- a/packages/platformos-language-server-common/src/definitions/providers/RenderPartialDefinitionProvider.ts +++ b/packages/platformos-language-server-common/src/definitions/providers/RenderPartialDefinitionProvider.ts @@ -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); diff --git a/packages/platformos-language-server-common/src/documentLinks/DocumentLinksProvider.ts b/packages/platformos-language-server-common/src/documentLinks/DocumentLinksProvider.ts index 0f4348e..e9f61b8 100644 --- a/packages/platformos-language-server-common/src/documentLinks/DocumentLinksProvider.ts +++ b/packages/platformos-language-server-common/src/documentLinks/DocumentLinksProvider.ts @@ -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) {