From faf909881d8fc01e925e7dd0f09382ed2a9e8b1c Mon Sep 17 00:00:00 2001 From: Bo Jeanes Date: Wed, 11 Nov 2026 08:29:11 +1100 Subject: [PATCH] Allow sending add-on languages to the server An add-on can make the server handle a language besides Ruby and ERB. For example, my add-on, ruby-lsp-slim-lang, does this for the Ruby inside Slim templates. Since the extension only sends Ruby and ERB files to the server, an add-on like that can't be used from VS Code. This adds a `rubyLsp.addonLanguageIds` setting, empty by default. The extension adds each language ID listed there alongside Ruby and ERB in the client's document selector, in the language status items, and in the checks that activate a workspace when a document opens. It's opt-in, because without a matching add-on the server would parse those files as Ruby. Since any change under `rubyLsp` restarts the server, the client picks up the new list when it rebuilds its document selector. The language status items only read the list when the extension activates, however. A change only reaches them after a window reload, which I've left as is to keep this change small. For context, the add-on API has no way to register a document type, though @vinistock suggested in #1233 that one might be designed[1]. So, ruby-lsp-slim-lang currently patches the server's document store to create its own. That is separate from this change, which only concerns which files the extension sends. [1]: https://github.com/Shopify/ruby-lsp/issues/1233#issuecomment-1847296835 Co-Authored-By: Claude Opus 5.5 --- vscode/package.json | 8 ++++++++ vscode/src/client.ts | 7 ++++--- vscode/src/common.ts | 7 +++++++ vscode/src/rubyLsp.ts | 6 +++--- vscode/src/status.ts | 4 ++-- vscode/src/test/suite/common.test.ts | 30 +++++++++++++++++++++++++++- 6 files changed, 53 insertions(+), 9 deletions(-) diff --git a/vscode/package.json b/vscode/package.json index 0734024340..0d114432be 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -551,6 +551,14 @@ "type": "boolean", "default": true }, + "rubyLsp.addonLanguageIds": { + "description": "Language IDs of other files to send to the server, for languages that an installed add-on supports (for example, `slim`). Without such an add-on, the server treats these files as Ruby", + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, "rubyLsp.featureFlags": { "description": "Allows opting in or out of feature flags", "type": "object", diff --git a/vscode/src/client.ts b/vscode/src/client.ts index c717379376..2b8b1dde0a 100644 --- a/vscode/src/client.ts +++ b/vscode/src/client.ts @@ -30,7 +30,7 @@ import { ErrorCodes, } from "vscode-languageclient/node"; -import { LSP_NAME, ClientInterface, Addon, SUPPORTED_LANGUAGE_IDS, FEATURE_FLAGS, featureEnabled } from "./common"; +import { LSP_NAME, ClientInterface, Addon, supportedLanguageIds, FEATURE_FLAGS, featureEnabled } from "./common"; import { Ruby } from "./ruby"; import { WorkspaceChannel } from "./workspaceChannel"; @@ -169,14 +169,15 @@ function collectClientOptions( // 1. Files inside of the workspace itself // 2. Bundled gems // 3. Default gems - let documentSelector: DocumentSelector = SUPPORTED_LANGUAGE_IDS.flatMap((language) => { + const languageIds = supportedLanguageIds(); + let documentSelector: DocumentSelector = languageIds.flatMap((language) => { return { scheme: "file", language, pattern: `${fsPath}/**/*` }; }); // Only the first language server we spawn should handle unsaved files, otherwise requests will be duplicated across // all workspaces if (isMainWorkspace) { - SUPPORTED_LANGUAGE_IDS.forEach((language) => { + languageIds.forEach((language) => { documentSelector.push({ language, scheme: "untitled", diff --git a/vscode/src/common.ts b/vscode/src/common.ts index e27ba06fc5..39dfe67b25 100644 --- a/vscode/src/common.ts +++ b/vscode/src/common.ts @@ -75,6 +75,13 @@ export const LOG_CHANNEL = vscode.window.createOutputChannel(LSP_NAME, { }); export const SUPPORTED_LANGUAGE_IDS = ["ruby", "erb"]; +// Add-ons can make the server understand other languages, such as Slim templates. Users opt into sending those files to +// the server, since without a matching add-on it would treat them as Ruby +export function supportedLanguageIds(): string[] { + const addonLanguageIds = vscode.workspace.getConfiguration("rubyLsp").get("addonLanguageIds") ?? []; + return [...SUPPORTED_LANGUAGE_IDS, ...addonLanguageIds]; +} + // A list of feature flags where the key is the name and the value is the rollout percentage. // // Note: names added here should also be added to the `rubyLsp.optedOutFeatureFlags` enum in the `package.json` file diff --git a/vscode/src/rubyLsp.ts b/vscode/src/rubyLsp.ts index e6336fbba6..09ff582e1e 100644 --- a/vscode/src/rubyLsp.ts +++ b/vscode/src/rubyLsp.ts @@ -6,7 +6,7 @@ import { Range } from "vscode-languageclient/node"; import DocumentProvider from "./documentProvider"; import { Workspace } from "./workspace"; -import { Command, featureEnabled, LOG_CHANNEL, STATUS_EMITTER, SUPPORTED_LANGUAGE_IDS } from "./common"; +import { Command, featureEnabled, LOG_CHANNEL, STATUS_EMITTER, supportedLanguageIds } from "./common"; import { ManagerIdentifier, ManagerConfiguration } from "./ruby"; import { StatusItems } from "./status"; import { TestController } from "./testController"; @@ -72,7 +72,7 @@ export class RubyLsp { }), // Lazily activate workspaces that do not contain a lockfile vscode.workspace.onDidOpenTextDocument(async (document) => { - if (!SUPPORTED_LANGUAGE_IDS.includes(document.languageId)) { + if (!supportedLanguageIds().includes(document.languageId)) { return; } @@ -127,7 +127,7 @@ export class RubyLsp { // eagerly activate the workspace for that file too const activeDocument = vscode.window.activeTextEditor?.document; - if (activeDocument && SUPPORTED_LANGUAGE_IDS.includes(activeDocument.languageId)) { + if (activeDocument && supportedLanguageIds().includes(activeDocument.languageId)) { const workspaceFolder = vscode.workspace.getWorkspaceFolder(activeDocument.uri); if (workspaceFolder) { diff --git a/vscode/src/status.ts b/vscode/src/status.ts index 3492b9120a..3fe6177ec1 100644 --- a/vscode/src/status.ts +++ b/vscode/src/status.ts @@ -1,7 +1,7 @@ import * as vscode from "vscode"; import { State } from "vscode-languageclient"; -import { Command, STATUS_EMITTER, WorkspaceInterface, SUPPORTED_LANGUAGE_IDS } from "./common"; +import { Command, STATUS_EMITTER, WorkspaceInterface, supportedLanguageIds } from "./common"; const STOPPED_SERVER_OPTIONS = [ { label: "Ruby LSP: Start", description: Command.Start }, @@ -21,7 +21,7 @@ export abstract class StatusItem { public item: vscode.LanguageStatusItem; constructor(id: string) { - const documentSelector = SUPPORTED_LANGUAGE_IDS.map((language) => ({ + const documentSelector = supportedLanguageIds().map((language) => ({ language, })); diff --git a/vscode/src/test/suite/common.test.ts b/vscode/src/test/suite/common.test.ts index 954797fe45..b8b7c1ff64 100644 --- a/vscode/src/test/suite/common.test.ts +++ b/vscode/src/test/suite/common.test.ts @@ -3,7 +3,7 @@ import * as assert from "assert"; import * as vscode from "vscode"; import sinon from "sinon"; -import { featureEnabled, FEATURE_FLAGS } from "../../common"; +import { featureEnabled, FEATURE_FLAGS, supportedLanguageIds } from "../../common"; suite("featureEnabled", () => { let sandbox: sinon.SinonSandbox; @@ -131,3 +131,31 @@ suite("featureEnabled", () => { secondStub.restore(); }); }); + +suite("supportedLanguageIds", () => { + let sandbox: sinon.SinonSandbox; + + setup(() => { + sandbox = sinon.createSandbox(); + }); + + teardown(() => { + sandbox.restore(); + }); + + test("returns Ruby and ERB by default", () => { + sandbox.stub(vscode.workspace, "getConfiguration").returns({ + get: () => undefined, + } as any); + + assert.deepStrictEqual(supportedLanguageIds(), ["ruby", "erb"]); + }); + + test("includes the language IDs configured for add-ons", () => { + sandbox.stub(vscode.workspace, "getConfiguration").returns({ + get: (key: string) => (key === "addonLanguageIds" ? ["slim"] : undefined), + } as any); + + assert.deepStrictEqual(supportedLanguageIds(), ["ruby", "erb", "slim"]); + }); +});