From 6add25c97d30e3631fce1317b3d3fcc6cca512d0 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Tue, 11 Aug 2026 18:36:51 +0200 Subject: [PATCH] Language Service: Resolve partials against the view root the index found --- .../packages/language-server/src/session.ts | 13 +++++- .../src/definition_provider.ts | 9 +++- .../test/definition_provider.test.ts | 42 +++++++++++++++++++ 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/javascript/packages/language-server/src/session.ts b/javascript/packages/language-server/src/session.ts index 9a7a4c1eb..8bb32be75 100644 --- a/javascript/packages/language-server/src/session.ts +++ b/javascript/packages/language-server/src/session.ts @@ -1,3 +1,4 @@ +import { join } from "node:path" import { existsSync, readFileSync } from "node:fs" import { Connection, InitializeParams } from "vscode-languageserver/node" @@ -57,7 +58,8 @@ export class Session { } catch { return null } - } + }, + documentPath => this.viewRootFor(documentPath), ) this.projects = new Projects(this.connection, this.workspaceFolders, { @@ -102,6 +104,15 @@ export class Session { }) } + private viewRootFor(documentPath: string): string | null { + const project = this.projects.containing(documentPath) + const viewRoot = project?.partialIndexService.index?.viewRoot + + if (!project || viewRoot === undefined) return null + + return viewRoot === "." ? project.root : join(project.root, viewRoot) + } + async init() { this.connection.console.log(`[Client] Diagnostic related information: ${this.capabilities.hasDiagnosticRelatedInformation ? "supported" : "not supported"}`) diff --git a/javascript/packages/language-service/src/definition_provider.ts b/javascript/packages/language-service/src/definition_provider.ts index c9f1db9a7..482719c94 100644 --- a/javascript/packages/language-service/src/definition_provider.ts +++ b/javascript/packages/language-service/src/definition_provider.ts @@ -36,15 +36,18 @@ export class DefinitionProvider { private parserService: ParserService private exists: (filePath: string) => boolean private read: (filePath: string) => string | null + private viewRootFor: (documentPath: string) => string | null constructor( parserService: ParserService, exists: (filePath: string) => boolean, - read: (filePath: string) => string | null + read: (filePath: string) => string | null, + viewRootFor: (documentPath: string) => string | null = () => null ) { this.parserService = parserService this.exists = exists this.read = read + this.viewRootFor = viewRootFor } getDefinition(document: TextDocument, position: Position): LocationLink[] { @@ -374,8 +377,10 @@ export class DefinitionProvider { } private viewsRoot(documentPath: string): string | null { - const index = documentPath.lastIndexOf(VIEWS_DIRECTORY) + const indexed = this.viewRootFor(documentPath) + if (indexed !== null) return indexed + const index = documentPath.lastIndexOf(VIEWS_DIRECTORY) if (index === -1) return null return documentPath.slice(0, index + VIEWS_DIRECTORY.length - 1) diff --git a/javascript/packages/language-service/test/definition_provider.test.ts b/javascript/packages/language-service/test/definition_provider.test.ts index 67741b234..eeae6dc1c 100644 --- a/javascript/packages/language-service/test/definition_provider.test.ts +++ b/javascript/packages/language-service/test/definition_provider.test.ts @@ -667,4 +667,46 @@ describe("DefinitionProvider", () => { expect(definitions(service, content, `"card"`, "file:///project/lib/templates/mailer.html.erb")).toEqual([]) }) }) + + describe("view root from the index", () => { + const TEMPLATES_URI = "file:///project/templates/events/show.html.erb" + + function serviceRootedAt(viewRoot: string | null, ...files: string[]) { + const existing = new Set(files) + + return new DefinitionProvider( + parserService, + filePath => existing.has(filePath), + () => "", + () => viewRoot, + ) + } + + it("resolves a qualified partial against the root the index reports", () => { + const service = serviceRootedAt("/project/templates", "/project/templates/shared/_header.html.erb") + + expect(uris(service, `<%= render "shared/header" %>`, "shared/header", TEMPLATES_URI)) + .toContain("file:///project/templates/shared/_header.html.erb") + }) + + it("finds nothing under a conventional layout the project does not use", () => { + const service = createService("/project/templates/shared/_header.html.erb") + + expect(uris(service, `<%= render "shared/header" %>`, "shared/header", TEMPLATES_URI)).toEqual([]) + }) + + it("treats a project root view root as the project root itself", () => { + const service = serviceRootedAt("/project", "/project/shared/_header.html.erb") + + expect(uris(service, `<%= render "shared/header" %>`, "shared/header", "file:///project/events/show.html.erb")) + .toContain("file:///project/shared/_header.html.erb") + }) + + it("falls back to the conventional layout when nothing is indexed", () => { + const service = serviceRootedAt(null, "/project/app/views/shared/_header.html.erb") + + expect(uris(service, `<%= render "shared/header" %>`, "shared/header")) + .toContain("file:///project/app/views/shared/_header.html.erb") + }) + }) })