From c4e310fc579b47898c21c3485d4486493742756d Mon Sep 17 00:00:00 2001 From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com> Date: Fri, 14 Aug 2026 10:02:40 +0530 Subject: [PATCH 1/2] fix(lsp): correct workspace and document color capabilities --- src/cm/lsp/clientManager.ts | 20 +++++++++--- src/cm/lsp/documentColors.ts | 2 +- src/cm/lsp/serverRegistry.ts | 19 +++++++++-- src/cm/lsp/servers/index.ts | 3 ++ src/cm/lsp/servers/shared.ts | 34 ++++++++++++++++++++ src/cm/lsp/types.ts | 2 ++ src/cm/lsp/workspace.ts | 3 ++ tests/unit/lspWorkspaceFolders.test.js | 44 ++++++++++++++++++++++++++ 8 files changed, 118 insertions(+), 9 deletions(-) create mode 100644 tests/unit/lspWorkspaceFolders.test.js diff --git a/src/cm/lsp/clientManager.ts b/src/cm/lsp/clientManager.ts index 59eae1966..2f72d0381 100644 --- a/src/cm/lsp/clientManager.ts +++ b/src/cm/lsp/clientManager.ts @@ -26,6 +26,7 @@ import { inlayHintsExtension } from "./inlayHints"; import { addLspLog } from "./logs"; import { selectRuntimeProvider } from "./runtimeProviders"; import serverRegistry from "./serverRegistry"; +import { isTailwindCssServer } from "./servers/shared"; import { hoverTooltips, resolveLspHoverHighlightLanguage, @@ -737,11 +738,15 @@ export class LspClientManager { scope, signal, } = initContext; + const tailwindCss = isTailwindCssServer(server); const workspaceOptions = { displayFile: this.options.displayFile, openFile: this.options.openFile, resolveLanguageId: this.options.resolveLanguageId, + // Track the first folder advertised during `initialize` so it is not + // sent again as a workspace-folder change after the client connects. + initialFolders: runtimeRootUri ? [runtimeRootUri] : undefined, }; const clientConfig = { ...(server.clientConfig ?? {}) }; @@ -798,6 +803,13 @@ export class LspClientManager { workspace: { configuration: true, workspaceFolders: true, + ...(tailwindCss + ? { + didChangeWatchedFiles: { + dynamicRegistration: true, + }, + } + : {}), }, }, }; @@ -1048,9 +1060,7 @@ export class LspClientManager { client, transportHandle.transport, initializationOptions, - scope === "workspace" && server.useWorkspaceFolders - ? null - : normalizedRootUri, + runtimeRootUri, ); await waitForInitialization(client.initializing, signal, server.id); if (!client.__acodeLoggedInfo) { @@ -1076,8 +1086,8 @@ export class LspClientManager { addLspLog( server.id, "info", - normalizedRootUri - ? `Initialized workspace ${normalizedRootUri}` + runtimeRootUri + ? `Initialized workspace ${runtimeRootUri}` : "Initialized without a workspace root", ); client.__acodeLoggedInfo = true; diff --git a/src/cm/lsp/documentColors.ts b/src/cm/lsp/documentColors.ts index 99bc055c9..11496259f 100644 --- a/src/cm/lsp/documentColors.ts +++ b/src/cm/lsp/documentColors.ts @@ -533,7 +533,7 @@ export function documentColorsClientExtension(): LSPClientExtension { clientCapabilities: { textDocument: { colorProvider: { - dynamicRegistration: true, + dynamicRegistration: false, }, }, }, diff --git a/src/cm/lsp/serverRegistry.ts b/src/cm/lsp/serverRegistry.ts index 24b255630..faaeaa949 100644 --- a/src/cm/lsp/serverRegistry.ts +++ b/src/cm/lsp/serverRegistry.ts @@ -15,6 +15,11 @@ import type { TransportDescriptor, WebSocketTransportOptions, } from "./types"; +import { + addJsTsLanguageAliases, + isTailwindCssServer, + resolveJsTsLanguageId, +} from "./servers/shared"; const registry = new Map(); const listeners = new Set(); @@ -176,6 +181,11 @@ function sanitizeDefinition( const id = toKey(definition.id); if (!id) throw new Error("LSP server definition requires a non-empty id"); + const tailwindCss = isTailwindCssServer(definition); + const declaredLanguages = sanitizeLanguages(definition.languages); + const languages = tailwindCss + ? addJsTsLanguageAliases(declaredLanguages) + : declaredLanguages; const transport: RawTransportDescriptor = definition.transport ?? {}; const kind = (transport.kind ?? "stdio") as @@ -189,7 +199,7 @@ function sanitizeDefinition( if ( !("languages" in definition) || - !sanitizeLanguages(definition.languages).length + !languages.length ) { throw new Error(`LSP server ${id} must declare supported languages`); } @@ -304,7 +314,7 @@ function sanitizeDefinition( Number.isFinite(definition.priority) ? definition.priority : 0, - languages: sanitizeLanguages(definition.languages), + languages, transport: sanitizedTransport, initializationOptions: clone(definition.initializationOptions), workspaceConfiguration: clone(definition.workspaceConfiguration), @@ -323,7 +333,10 @@ function sanitizeDefinition( resolveLanguageId: typeof definition.resolveLanguageId === "function" ? definition.resolveLanguageId - : null, + : tailwindCss + ? ({ languageId, languageName }) => + resolveJsTsLanguageId(languageId, languageName) + : null, launcher, runtimes: sanitizeRuntimeIds(definition.runtimes), useWorkspaceFolders: definition.useWorkspaceFolders === true, diff --git a/src/cm/lsp/servers/index.ts b/src/cm/lsp/servers/index.ts index 9164ea1a8..dd4539940 100644 --- a/src/cm/lsp/servers/index.ts +++ b/src/cm/lsp/servers/index.ts @@ -3,6 +3,7 @@ import { javascriptBundle, javascriptServers } from "./javascript"; import { luauBundle, luauServers } from "./luau"; import { pythonBundle, pythonServers } from "./python"; import { systemsBundle, systemsServers } from "./systems"; +import { tailwindBundle, tailwindServers } from "./tailwind"; import { webBundle, webServers } from "./web"; export const builtinServers: LspServerManifest[] = [ @@ -11,6 +12,7 @@ export const builtinServers: LspServerManifest[] = [ ...luauServers, ...webServers, ...systemsServers, + ...tailwindServers, ]; export const builtinServerBundles: LspServerBundle[] = [ @@ -19,4 +21,5 @@ export const builtinServerBundles: LspServerBundle[] = [ luauBundle, webBundle, systemsBundle, + tailwindBundle, ]; diff --git a/src/cm/lsp/servers/shared.ts b/src/cm/lsp/servers/shared.ts index 5b0d84e8c..10227fd88 100644 --- a/src/cm/lsp/servers/shared.ts +++ b/src/cm/lsp/servers/shared.ts @@ -1,3 +1,5 @@ +import type { LspServerManifest } from "../types"; + export function normalizeServerLanguageKey( value: string | undefined | null, ): string { @@ -6,6 +8,38 @@ export function normalizeServerLanguageKey( .toLowerCase(); } +export function isTailwindCssServer(server: LspServerManifest): boolean { + const identifiers = [ + server.id, + server.label, + server.transport?.command, + ...(server.transport?.args ?? []), + server.launcher?.command, + ...(server.launcher?.args ?? []), + server.launcher?.bridge?.command, + ...(server.launcher?.bridge?.args ?? []), + ]; + return identifiers.some((value) => + normalizeServerLanguageKey(value).includes("tailwindcss"), + ); +} + +export function addJsTsLanguageAliases(languages: string[]): string[] { + const aliases = new Set(languages.map(normalizeServerLanguageKey)); + const pairs = [ + ["js", "javascript"], + ["jsx", "javascriptreact"], + ["ts", "typescript"], + ["tsx", "typescriptreact"], + ]; + for (const [short, standard] of pairs) { + if (!aliases.has(short) && !aliases.has(standard)) continue; + aliases.add(short); + aliases.add(standard); + } + return [...aliases].filter(Boolean); +} + export function resolveJsTsLanguageId( languageId: string | undefined, languageName: string | undefined, diff --git a/src/cm/lsp/types.ts b/src/cm/lsp/types.ts index 692762462..1ffa6c4bd 100644 --- a/src/cm/lsp/types.ts +++ b/src/cm/lsp/types.ts @@ -519,6 +519,8 @@ export interface WorkspaceOptions { displayFile?: (uri: string) => Promise; openFile?: (uri: string) => Promise; resolveLanguageId?: (uri: string) => string | null; + /** Folders already advertised in `initialize`; do not re-notify. */ + initialFolders?: string[]; } // ============================================================================ diff --git a/src/cm/lsp/workspace.ts b/src/cm/lsp/workspace.ts index e570928f5..b3d02e36f 100644 --- a/src/cm/lsp/workspace.ts +++ b/src/cm/lsp/workspace.ts @@ -58,6 +58,9 @@ export default class AcodeWorkspace extends Workspace { this.#versions = Object.create(null) as Record; this.#workspaceFolders = new Set(); this.options = options; + for (const folder of options.initialFolders ?? []) { + if (folder) this.#workspaceFolders.add(folder); + } } #log(level: LspLogLevel, message: string, details?: unknown): void { diff --git a/tests/unit/lspWorkspaceFolders.test.js b/tests/unit/lspWorkspaceFolders.test.js new file mode 100644 index 000000000..e5e8db62d --- /dev/null +++ b/tests/unit/lspWorkspaceFolders.test.js @@ -0,0 +1,44 @@ +// @vitest-environment happy-dom + +import { describe, expect, it } from "vitest"; +import { + addJsTsLanguageAliases, + isTailwindCssServer, + resolveJsTsLanguageId, +} from "cm/lsp/servers/shared"; + +describe("Tailwind document language IDs", () => { + it("recognizes a custom server and supplies its standard aliases", () => { + const server = { + id: "custom-tailwind-test", + languages: ["tsx"], + transport: { + kind: "stdio", + command: "tailwindcss-language-server", + }, + }; + + expect(isTailwindCssServer(server)).toBe(true); + expect(addJsTsLanguageAliases(server.languages)).toEqual( + expect.arrayContaining(["tsx", "typescriptreact"]), + ); + expect(resolveJsTsLanguageId("tsx", "TSX")).toBe("typescriptreact"); + }); +}); + +describe("workspace folder initialization", () => { + it("does not notify the server twice for the initial folder", async () => { + const { default: AcodeWorkspace } = await import("cm/lsp/workspace"); + const workspace = new AcodeWorkspace( + { connected: false }, + { initialFolders: ["file:///data/user/0/app/project/"] }, + ); + + expect( + workspace.hasWorkspaceFolder("file:///data/user/0/app/project/"), + ).toBe(true); + expect( + workspace.addWorkspaceFolder("file:///data/user/0/app/project/"), + ).toBe(false); + }); +}); From 92687ab748e5ad94eb7204f8998157e205d9f44c Mon Sep 17 00:00:00 2001 From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com> Date: Fri, 14 Aug 2026 10:03:01 +0530 Subject: [PATCH 2/2] feat(lsp): add Tailwind CSS server --- src/cm/lsp/servers/tailwind.ts | 51 ++++++++++++++++++++++++++++ tests/unit/lspTailwindServer.test.js | 24 +++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 src/cm/lsp/servers/tailwind.ts create mode 100644 tests/unit/lspTailwindServer.test.js diff --git a/src/cm/lsp/servers/tailwind.ts b/src/cm/lsp/servers/tailwind.ts new file mode 100644 index 000000000..ef299228e --- /dev/null +++ b/src/cm/lsp/servers/tailwind.ts @@ -0,0 +1,51 @@ +import { defineBundle, defineServer, installers } from "../providerUtils"; +import type { LspServerBundle, LspServerManifest } from "../types"; +import { resolveJsTsLanguageId } from "./shared"; + +export const tailwindServers: LspServerManifest[] = [ + defineServer({ + id: "tailwindcss", + label: "Tailwind CSS", + languages: [ + "html", + "css", + "scss", + "less", + "javascript", + "javascriptreact", + "typescript", + "typescriptreact", + "jsx", + "tsx", + "vue", + "svelte", + "astro", + "php", + "mdx", + ], + runtimes: ["builtin-alpine"], + command: "tailwindcss-language-server", + args: ["--stdio"], + checkCommand: "which tailwindcss-language-server", + installer: installers.npm({ + executable: "tailwindcss-language-server", + packages: ["@tailwindcss/language-server"], + }), + clientConfig: { + builtinExtensions: { + formatting: false, + signature: false, + }, + }, + resolveLanguageId: ({ languageId, languageName }) => + resolveJsTsLanguageId(languageId, languageName), + useWorkspaceFolders: true, + enabled: false, + }), +]; + +export const tailwindBundle: LspServerBundle = defineBundle({ + id: "builtin-tailwindcss", + label: "Tailwind CSS", + servers: tailwindServers, +}); diff --git a/tests/unit/lspTailwindServer.test.js b/tests/unit/lspTailwindServer.test.js new file mode 100644 index 000000000..3ba677e85 --- /dev/null +++ b/tests/unit/lspTailwindServer.test.js @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; +import { tailwindServers } from "cm/lsp/servers/tailwind"; + +describe("built-in Tailwind CSS language server", () => { + it("is available but disabled by default", () => { + const server = tailwindServers.find(({ id }) => id === "tailwindcss"); + + expect(server).toBeDefined(); + expect(server.enabled).toBe(false); + expect(server.useWorkspaceFolders).toBe(true); + expect(server.launcher.bridge).toMatchObject({ + command: "tailwindcss-language-server", + args: ["--stdio"], + }); + expect(server.launcher.install).toMatchObject({ + kind: "npm", + executable: "tailwindcss-language-server", + packages: ["@tailwindcss/language-server"], + }); + expect( + server.resolveLanguageId({ languageId: "tsx", languageName: "TSX" }), + ).toBe("typescriptreact"); + }); +});