From 99ce79ac888cd470cb5cb885fd2f73b81529a321 Mon Sep 17 00:00:00 2001 From: Evan Reichard Date: Tue, 5 May 2026 23:44:21 -0400 Subject: [PATCH] fix(lsp): support server workspace configuration --- server.ts | 43 +++++++++++++++++++++++++++++++++++++++++++ src/client.ts | 26 +++++++++++++++++++++++--- src/types.ts | 19 +++++++++++++++++++ 3 files changed, 85 insertions(+), 3 deletions(-) diff --git a/server.ts b/server.ts index b6e78b7..648a133 100644 --- a/server.ts +++ b/server.ts @@ -3,8 +3,42 @@ // // Add new servers here. `match` is a list of file extensions (no dot) OR // language ids; either matches. +import * as fs from "node:fs"; +import * as path from "node:path"; import type { ServerConfig } from "./src/types.ts"; +// Resolve Python Path - Prefer the project's virtualenv when present so +// Pyright sees the same interpreter/dependencies as project commands. +function resolvePythonPath(rootDir: string): string | undefined { + const candidates = [ + path.join(rootDir, ".venv", "bin", "python"), + path.join(rootDir, "venv", "bin", "python"), + ]; + return candidates.find((candidate) => fs.existsSync(candidate)); +} + +// Pyright Settings - Minimal editor settings needed for diagnostics and import +// resolution. Shared by workspace/configuration and didChangeConfiguration. +function pyrightSettings(rootDir: string): { + pythonPath: string | undefined; + analysis: { + diagnosticMode: string; + typeCheckingMode: string; + autoSearchPaths: boolean; + useLibraryCodeForTypes: boolean; + }; +} { + return { + pythonPath: resolvePythonPath(rootDir), + analysis: { + diagnosticMode: "openFilesOnly", + typeCheckingMode: "basic", + autoSearchPaths: true, + useLibraryCodeForTypes: true, + }, + }; +} + // Global Root Markers — appended to every server's rootMarkers list export const globalRootMarkers = [".git"]; @@ -32,6 +66,15 @@ export const servers: ServerConfig[] = [ args: ["--stdio"], rootMarkers: ["pyproject.toml", "setup.py", "setup.cfg"], languageId: "python", + workspaceConfiguration: { + initialSettings: ({ rootDir }) => ({ python: pyrightSettings(rootDir) }), + getSection: (section, { rootDir }) => { + const settings = pyrightSettings(rootDir); + if (section === "python") return settings; + if (section === "python.analysis") return settings.analysis; + return null; + }, + }, }, { id: "lua-language-server", diff --git a/src/client.ts b/src/client.ts index d5d8cc6..6e23893 100644 --- a/src/client.ts +++ b/src/client.ts @@ -102,9 +102,19 @@ export class LspClient { } }, ); - // Accept Common Server Requests - Return empty/null so servers don't - // stall. Good enough for a CLI; a real client would answer properly. - this.conn.onRequest("workspace/configuration", () => []); + // Accept Common Server Requests - Return one configuration response per + // requested item. Server-specific settings live in server.ts so adding + // another picky server doesn't grow conditionals in this transport layer. + this.conn.onRequest( + "workspace/configuration", + (params: { items?: { section?: string }[] }) => { + const items = params.items ?? []; + const config = this.server.workspaceConfiguration; + return items.map((item) => + config?.getSection?.(item.section, { rootDir, env }) ?? null, + ); + }, + ); this.conn.onRequest("client/registerCapability", () => null); this.conn.onRequest("client/unregisterCapability", () => null); @@ -136,6 +146,16 @@ export class LspClient { }, }); this.conn.sendNotification("initialized", {}); + + // Push Configuration - Some servers do not always request workspace/configuration, + // but still consume settings delivered through didChangeConfiguration. + const settings = this.server.workspaceConfiguration?.initialSettings?.({ + rootDir, + env, + }); + if (settings !== undefined && settings !== null) { + this.conn.sendNotification("workspace/didChangeConfiguration", { settings }); + } } // Wait For Ready - Resolves when there are no outstanding progress diff --git a/src/types.ts b/src/types.ts index 8e1103f..aeb12d5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -17,6 +17,22 @@ export class ServerNotFoundError extends Error { } } +export interface WorkspaceConfigurationContext { + rootDir: string; + env: NodeJS.ProcessEnv; +} + +export interface ServerWorkspaceConfiguration { + // Initial Settings - Optional payload pushed via workspace/didChangeConfiguration + // after initialize/initialized for servers that don't always request config. + initialSettings?: (ctx: WorkspaceConfigurationContext) => unknown; + // Section Settings - Optional handler for workspace/configuration requests. + getSection?: ( + section: string | undefined, + ctx: WorkspaceConfigurationContext, + ) => unknown; +} + export interface ServerConfig { // Stable identifier (useful for logs and future daemon cache keys). id: string; @@ -38,6 +54,9 @@ export interface ServerConfig { // hover/definition/references/completion/documentSymbol but included // in lsp_diagnostics and auto-check. diagnosticsOnly?: boolean; + // Workspace Configuration - Optional server-specific settings exposed through + // workspace/configuration and workspace/didChangeConfiguration. + workspaceConfiguration?: ServerWorkspaceConfiguration; } // Supported high-level commands exposed via the CLI. Extend this union