from __future__ import annotations
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Mapping
@dataclass(frozen=True)
class LSPServerConfig:
"""Configuration for one language server."""
command: str
args: list[str] = field(default_factory=list)
# Language routing
file_extensions: list[str] = field(default_factory=list) # without leading dot
# LSP languageId to use for didOpen/didChange
language_id: str | None = None
language_id_map: dict[str, str] = field(default_factory=dict) # ext -> languageId
# LSP initialize params
initialization_options: dict[str, Any] = field(default_factory=dict)
# workspace/didChangeConfiguration payload ("settings" object)
settings: dict[str, Any] = field(default_factory=dict)
# Extra environment variables to set when launching the server
env: dict[str, str] = field(default_factory=dict)
@dataclass
class BridgeConfig:
"""Full bridge configuration."""
servers: dict[str, LSPServerConfig]
# Default workspace root if no better root can be detected
default_root: str | None = None
# Derived index: extension -> server key
_ext_to_server: dict[str, str] = field(init=False, default_factory=dict)
def __post_init__(self) -> None:
self._ext_to_server = {}
for server_key, cfg in self.servers.items():
for ext in cfg.file_extensions:
norm = ext.lower().lstrip(".")
# First match wins; allow explicit overrides by ordering in config
self._ext_to_server.setdefault(norm, server_key)
def server_for_extension(self, ext: str) -> str | None:
return self._ext_to_server.get(ext.lower().lstrip("."))
def _as_list(value: Any) -> list[str]:
if value is None:
return []
if isinstance(value, list):
return [str(v) for v in value]
return [str(value)]
def _as_dict(value: Any) -> dict[str, Any]:
if value is None:
return {}
if isinstance(value, dict):
return dict(value)
raise TypeError(f"Expected dict, got {type(value)}")
def default_config() -> BridgeConfig:
"""Reasonable defaults for a multi-language repo."""
servers: dict[str, LSPServerConfig] = {
"python": LSPServerConfig(
command=os.getenv("PY_LSP_CMD", "basedpyright-langserver"),
args=["--stdio"],
file_extensions=["py"],
language_id="python",
),
"rust": LSPServerConfig(
command=os.getenv("RUST_LSP_CMD", "rust-analyzer"),
args=[],
file_extensions=["rs"],
language_id="rust",
),
"cpp": LSPServerConfig(
command=os.getenv("CPP_LSP_CMD", "clangd"),
args=[],
file_extensions=["c", "cc", "cpp", "cxx", "h", "hpp", "hxx"],
language_id="cpp",
),
"typescript": LSPServerConfig(
command=os.getenv("TS_LSP_CMD", "typescript-language-server"),
args=["--stdio"],
file_extensions=["ts", "tsx", "js", "jsx", "mjs", "cjs"],
language_id_map={
"ts": "typescript",
"tsx": "typescriptreact",
"js": "javascript",
"jsx": "javascriptreact",
"mjs": "javascript",
"cjs": "javascript",
},
),
"html": LSPServerConfig(
command=os.getenv("HTML_LSP_CMD", "vscode-html-language-server"),
args=["--stdio"],
file_extensions=["html", "htm"],
language_id="html",
),
"css": LSPServerConfig(
command=os.getenv("CSS_LSP_CMD", "vscode-css-language-server"),
args=["--stdio"],
file_extensions=["css", "scss", "less"],
language_id_map={
"css": "css",
"scss": "scss",
"less": "less",
},
),
}
return BridgeConfig(servers=servers, default_root=None)
def load_config(path: str | Path | None) -> BridgeConfig:
"""Load bridge config from TOML.
If path is None or missing, returns default_config().
"""
if path is None:
return default_config()
p = Path(path).expanduser().resolve()
if not p.exists():
return default_config()
# Python 3.11+ has tomllib built-in.
import tomllib
data = tomllib.loads(p.read_text(encoding="utf-8"))
default_root = None
if isinstance(data.get("bridge"), dict):
default_root = data["bridge"].get("default_root")
servers_raw: Mapping[str, Any] = data.get("servers", {})
servers: dict[str, LSPServerConfig] = {}
for server_key, cfg_raw in servers_raw.items():
if not isinstance(cfg_raw, dict):
continue
cmd = str(cfg_raw.get("command", "")).strip()
if not cmd:
raise ValueError(f"servers.{server_key}.command is required")
servers[server_key] = LSPServerConfig(
command=cmd,
args=_as_list(cfg_raw.get("args")),
file_extensions=[
str(x).lower().lstrip(".")
for x in _as_list(cfg_raw.get("file_extensions"))
],
language_id=cfg_raw.get("language_id"),
language_id_map={
str(k).lower().lstrip("."): str(v)
for k, v in _as_dict(cfg_raw.get("language_id_map")).items()
},
initialization_options=_as_dict(cfg_raw.get("initialization_options")),
settings=_as_dict(cfg_raw.get("settings")),
env={str(k): str(v) for k, v in _as_dict(cfg_raw.get("env")).items()},
)
if not servers:
return default_config()
return BridgeConfig(servers=servers, default_root=default_root)