From 9560ee651def61c4363363df9fc80be31b92468d Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 13 Jan 2026 22:14:42 +0100 Subject: [PATCH 01/14] Introduce LanguageServerDependencyProvider abstraction * Add specialisation LanguageServerDependencyProviderSinglePath, where the core path can be overridden via ls_specific_settings #917 * Implement in MatLabLanguageServer and TypeScriptLanguageServer * Backwards compatibility is retained; the new approach is optional --- .../matlab_language_server.py | 339 +++++++++--------- .../typescript_language_server.py | 174 ++++----- src/solidlsp/ls.py | 94 ++++- 3 files changed, 350 insertions(+), 257 deletions(-) diff --git a/src/solidlsp/language_servers/matlab_language_server.py b/src/solidlsp/language_servers/matlab_language_server.py index 74928ceb..9be9390b 100644 --- a/src/solidlsp/language_servers/matlab_language_server.py +++ b/src/solidlsp/language_servers/matlab_language_server.py @@ -38,14 +38,13 @@ import platform import shutil import threading import zipfile -from typing import Any, cast +from typing import Any, Union, cast import requests -from solidlsp.ls import LSPFileBuffer, SolidLanguageServer +from solidlsp.ls import LanguageServerDependencyProvider, LSPFileBuffer, SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.lsp_protocol_handler.lsp_types import DocumentSymbol, InitializeParams, SymbolInformation -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings log = logging.getLogger(__name__) @@ -151,228 +150,230 @@ class MatlabLanguageServer(SolidLanguageServer): Creates a MatlabLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - matlab_lsp_command, matlab_path = self._setup_runtime_dependencies(config, solidlsp_settings) - self._matlab_path = matlab_path - - # Set environment for MATLAB - proc_env = { - "MATLAB_INSTALL_PATH": matlab_path, - } - super().__init__( config, repository_root_path, - ProcessLaunchInfo(cmd=matlab_lsp_command, cwd=repository_root_path, env=proc_env), + None, "matlab", solidlsp_settings, ) + + assert isinstance(self._dependency_provider, self.DependencyProvider) + self._matlab_path = self._dependency_provider.get_matlab_path() + self.server_ready = threading.Event() self.initialize_searcher_command_available = threading.Event() - @classmethod - def _download_matlab_extension(cls, url: str, target_dir: str) -> bool: - """ - Download and extract the MATLAB extension from VS Code marketplace. + def _create_dependency_provider(self) -> LanguageServerDependencyProvider: + return self.DependencyProvider(self._custom_settings, self._ls_resources_dir) - The VS Code marketplace packages extensions as .vsix files (which are ZIP archives). - This method downloads the VSIX file and extracts it to get the language server. + class DependencyProvider(LanguageServerDependencyProvider): + def __init__(self, custom_settings: SolidLSPSettings.CustomLSSettings, ls_resources_dir: str): + super().__init__(custom_settings, ls_resources_dir) + self._matlab_path: str | None = None - Args: - url: VS Code marketplace URL for the MATLAB extension - target_dir: Directory where the extension will be extracted + @classmethod + def _download_matlab_extension(cls, url: str, target_dir: str) -> bool: + """ + Download and extract the MATLAB extension from VS Code marketplace. - Returns: - True if successful, False otherwise + The VS Code marketplace packages extensions as .vsix files (which are ZIP archives). + This method downloads the VSIX file and extracts it to get the language server. - """ - try: - log.info(f"Downloading MATLAB extension from {url}") + Args: + url: VS Code marketplace URL for the MATLAB extension + target_dir: Directory where the extension will be extracted - # Create target directory for the extension - os.makedirs(target_dir, exist_ok=True) + Returns: + True if successful, False otherwise - # Download with proper headers to mimic VS Code marketplace client - headers = { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", - "Accept": "application/octet-stream, application/vsix, */*", - } + """ + try: + log.info(f"Downloading MATLAB extension from {url}") - response = requests.get(url, headers=headers, stream=True, timeout=300) - response.raise_for_status() + # Create target directory for the extension + os.makedirs(target_dir, exist_ok=True) - # Save to temporary VSIX file - temp_file = os.path.join(target_dir, "matlab_extension_temp.vsix") - total_size = int(response.headers.get("content-length", 0)) + # Download with proper headers to mimic VS Code marketplace client + headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", + "Accept": "application/octet-stream, application/vsix, */*", + } - log.info(f"Downloading {total_size / 1024 / 1024:.1f} MB...") + response = requests.get(url, headers=headers, stream=True, timeout=300) + response.raise_for_status() - with open(temp_file, "wb") as f: - downloaded = 0 - for chunk in response.iter_content(chunk_size=8192): - if chunk: - f.write(chunk) - downloaded += len(chunk) - if total_size > 0 and downloaded % (10 * 1024 * 1024) == 0: - progress = (downloaded / total_size) * 100 - log.info(f"Download progress: {progress:.1f}%") + # Save to temporary VSIX file + temp_file = os.path.join(target_dir, "matlab_extension_temp.vsix") + total_size = int(response.headers.get("content-length", 0)) - log.info("Download complete, extracting...") + log.info(f"Downloading {total_size / 1024 / 1024:.1f} MB...") - # Extract VSIX file (VSIX files are ZIP archives) - with zipfile.ZipFile(temp_file, "r") as zip_ref: - zip_ref.extractall(target_dir) + with open(temp_file, "wb") as f: + downloaded = 0 + for chunk in response.iter_content(chunk_size=8192): + if chunk: + f.write(chunk) + downloaded += len(chunk) + if total_size > 0 and downloaded % (10 * 1024 * 1024) == 0: + progress = (downloaded / total_size) * 100 + log.info(f"Download progress: {progress:.1f}%") - # Clean up temp file - os.remove(temp_file) + log.info("Download complete, extracting...") - log.info("MATLAB extension extracted successfully") - return True + # Extract VSIX file (VSIX files are ZIP archives) + with zipfile.ZipFile(temp_file, "r") as zip_ref: + zip_ref.extractall(target_dir) - except Exception as e: - log.error(f"Error downloading/extracting MATLAB extension: {e}") - return False + # Clean up temp file + os.remove(temp_file) - @classmethod - def _find_matlab_extension(cls, solidlsp_settings: SolidLSPSettings) -> str | None: - """ - Find MATLAB extension in various locations. + log.info("MATLAB extension extracted successfully") + return True - Search order: - 1. Environment variable (MATLAB_EXTENSION_PATH) - 2. Default download location (~/.serena/ls_resources/matlab-extension) - 3. VS Code installed extensions + except Exception as e: + log.error(f"Error downloading/extracting MATLAB extension: {e}") + return False - Returns: - Path to MATLAB extension directory or None if not found + def _find_matlab_extension(self) -> str | None: + """ + Find MATLAB extension in various locations. - """ - # Check environment variable - env_path = os.environ.get("MATLAB_EXTENSION_PATH") - if env_path and os.path.exists(env_path): - log.debug(f"Found MATLAB extension via MATLAB_EXTENSION_PATH: {env_path}") - return env_path - elif env_path: - log.warning(f"MATLAB_EXTENSION_PATH set but directory not found: {env_path}") + Search order: + 1. Environment variable (MATLAB_EXTENSION_PATH) + 2. Default download location (~/.serena/ls_resources/matlab-extension) + 3. VS Code installed extensions - # Check default download location - default_path = os.path.join(cls.ls_resources_dir(solidlsp_settings), "matlab-extension", "extension") - if os.path.exists(default_path): - log.debug(f"Found MATLAB extension in default location: {default_path}") - return default_path + Returns: + Path to MATLAB extension directory or None if not found - # Search VS Code extensions - vscode_extensions_dir = os.path.expanduser("~/.vscode/extensions") - if os.path.exists(vscode_extensions_dir): - for entry in os.listdir(vscode_extensions_dir): - if entry.startswith("mathworks.language-matlab"): - ext_path = os.path.join(vscode_extensions_dir, entry) - if os.path.isdir(ext_path): - log.debug(f"Found MATLAB extension in VS Code: {ext_path}") - return ext_path + """ + # Check environment variable + env_path = os.environ.get("MATLAB_EXTENSION_PATH") + if env_path and os.path.exists(env_path): + log.debug(f"Found MATLAB extension via MATLAB_EXTENSION_PATH: {env_path}") + return env_path + elif env_path: + log.warning(f"MATLAB_EXTENSION_PATH set but directory not found: {env_path}") - log.debug("MATLAB extension not found in any known location") - return None + # Check default download location + default_path = os.path.join(self._ls_resources_dir, "matlab-extension", "extension") + if os.path.exists(default_path): + log.debug(f"Found MATLAB extension in default location: {default_path}") + return default_path - @classmethod - def _download_and_install_matlab_extension(cls, solidlsp_settings: SolidLSPSettings) -> str | None: - """ - Download and install MATLAB extension from VS Code marketplace. + # Search VS Code extensions + vscode_extensions_dir = os.path.expanduser("~/.vscode/extensions") + if os.path.exists(vscode_extensions_dir): + for entry in os.listdir(vscode_extensions_dir): + if entry.startswith("mathworks.language-matlab"): + ext_path = os.path.join(vscode_extensions_dir, entry) + if os.path.isdir(ext_path): + log.debug(f"Found MATLAB extension in VS Code: {ext_path}") + return ext_path - Returns: - Path to installed extension or None if download failed + log.debug("MATLAB extension not found in any known location") + return None - """ - matlab_extension_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "matlab-extension") + def _download_and_install_matlab_extension(self) -> str | None: + """ + Download and install MATLAB extension from VS Code marketplace. - log.info(f"Downloading MATLAB extension from: {MATLAB_EXTENSION_URL}") + Returns: + Path to installed extension or None if download failed - if cls._download_matlab_extension(MATLAB_EXTENSION_URL, matlab_extension_dir): - extension_path = os.path.join(matlab_extension_dir, "extension") - if os.path.exists(extension_path): - log.info("MATLAB extension downloaded and installed successfully") - return extension_path + """ + matlab_extension_dir = os.path.join(self._ls_resources_dir, "matlab-extension") + + log.info(f"Downloading MATLAB extension from: {MATLAB_EXTENSION_URL}") + + if self._download_matlab_extension(MATLAB_EXTENSION_URL, matlab_extension_dir): + extension_path = os.path.join(matlab_extension_dir, "extension") + if os.path.exists(extension_path): + log.info("MATLAB extension downloaded and installed successfully") + return extension_path + else: + log.error(f"Download completed but extension not found at: {extension_path}") else: - log.error(f"Download completed but extension not found at: {extension_path}") - else: - log.error("Failed to download MATLAB extension from marketplace") + log.error("Failed to download MATLAB extension from marketplace") - return None + return None - @classmethod - def _get_executable_path(cls, extension_path: str, system: str) -> str: - """ - Get the path to the MATLAB language server executable based on platform. + @classmethod + def _get_executable_path(cls, extension_path: str) -> str: + """ + Get the path to the MATLAB language server executable based on platform. - The language server is a Node.js script located in the extension's server directory. - """ - # The MATLAB extension bundles the language server in the 'server' directory - server_dir = os.path.join(extension_path, "server", "out") - main_script = os.path.join(server_dir, "index.js") + The language server is a Node.js script located in the extension's server directory. + """ + # The MATLAB extension bundles the language server in the 'server' directory + server_dir = os.path.join(extension_path, "server", "out") + main_script = os.path.join(server_dir, "index.js") - if os.path.exists(main_script): - return main_script + if os.path.exists(main_script): + return main_script - # Alternative location - alt_script = os.path.join(extension_path, "out", "index.js") - if os.path.exists(alt_script): - return alt_script + # Alternative location + alt_script = os.path.join(extension_path, "out", "index.js") + if os.path.exists(alt_script): + return alt_script - raise RuntimeError(f"MATLAB language server script not found in extension at {extension_path}") + raise RuntimeError(f"MATLAB language server script not found in extension at {extension_path}") - @classmethod - def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> tuple[list[str], str]: - """ - Setup runtime dependencies for MATLAB Language Server and return the command to start the server. + def get_matlab_path(self) -> str: + """Get MATLAB path from settings or auto-detect.""" + if self._matlab_path is not None: + return self._matlab_path - Returns: - Tuple of (command to start the server, MATLAB installation path) + matlab_path = self._custom_settings.get("matlab_path") - """ - system = platform.system() + if not matlab_path: + matlab_path = MatlabLanguageServer._find_matlab_installation() # Raises RuntimeError if not found - # Verify node is installed - node_path = shutil.which("node") - if node_path is None: - raise RuntimeError("Node.js is not installed or isn't in PATH. Please install Node.js and try again.") + # Verify MATLAB path exists + if not os.path.isdir(matlab_path): + raise RuntimeError(f"MATLAB installation directory does not exist: {matlab_path}") - # Get MATLAB path from settings or auto-detect - language_specific_config = solidlsp_settings.get_ls_specific_settings(cls.get_language_enum_instance()) - matlab_path = language_specific_config.get("matlab_path") + log.info(f"Using MATLAB installation: {matlab_path}") - if not matlab_path: - matlab_path = cls._find_matlab_installation() # Raises RuntimeError if not found + self._matlab_path = matlab_path + return matlab_path - # Verify MATLAB path exists - if not os.path.isdir(matlab_path): - raise RuntimeError(f"MATLAB installation directory does not exist: {matlab_path}") + def create_launch_command(self) -> Union[str, list[str]]: + # Verify node is installed + node_path = shutil.which("node") + if node_path is None: + raise RuntimeError("Node.js is not installed or isn't in PATH. Please install Node.js and try again.") - log.info(f"Using MATLAB installation: {matlab_path}") + # Find existing extension or download if needed + extension_path = self._find_matlab_extension() + if extension_path is None: + log.info("MATLAB extension not found on disk, attempting to download...") + extension_path = self._download_and_install_matlab_extension() - # Find existing extension or download if needed - extension_path = cls._find_matlab_extension(solidlsp_settings) - if extension_path is None: - log.info("MATLAB extension not found on disk, attempting to download...") - extension_path = cls._download_and_install_matlab_extension(solidlsp_settings) + if extension_path is None: + raise RuntimeError( + "Failed to locate or download MATLAB Language Server. Please either:\n" + "1. Set MATLAB_EXTENSION_PATH environment variable to the MATLAB extension directory\n" + "2. Install the MATLAB extension in VS Code (MathWorks.language-matlab)\n" + "3. Ensure internet connection for automatic download" + ) - if extension_path is None: - raise RuntimeError( - "Failed to locate or download MATLAB Language Server. Please either:\n" - "1. Set MATLAB_EXTENSION_PATH environment variable to the MATLAB extension directory\n" - "2. Install the MATLAB extension in VS Code (MathWorks.language-matlab)\n" - "3. Ensure internet connection for automatic download" - ) + # Get the language server script path + server_script = self._get_executable_path(extension_path) - # Get the language server script path - server_script = cls._get_executable_path(extension_path, system) + if not os.path.exists(server_script): + raise RuntimeError(f"MATLAB Language Server script not found at: {server_script}") - if not os.path.exists(server_script): - raise RuntimeError(f"MATLAB Language Server script not found at: {server_script}") + # Build the command to run the language server + # The MATLAB language server is run via Node.js with the --stdio flag + cmd = [node_path, server_script, "--stdio"] + return cmd - # Build the command to run the language server - # The MATLAB language server is run via Node.js with the --stdio flag - cmd = [node_path, server_script, "--stdio"] - - return cmd, matlab_path + def create_process_env(self) -> dict[str, str]: + return { + "MATLAB_INSTALL_PATH": self.get_matlab_path(), + } @staticmethod def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: diff --git a/src/solidlsp/language_servers/typescript_language_server.py b/src/solidlsp/language_servers/typescript_language_server.py index 4734da45..779ed4e5 100644 --- a/src/solidlsp/language_servers/typescript_language_server.py +++ b/src/solidlsp/language_servers/typescript_language_server.py @@ -13,11 +13,10 @@ from overrides import override from sensai.util.logging import LogTime from solidlsp import ls_types -from solidlsp.ls import SolidLanguageServer +from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_utils import PlatformId, PlatformUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings from .common import RuntimeDependency, RuntimeDependencyCollection @@ -72,17 +71,19 @@ class TypeScriptLanguageServer(SolidLanguageServer): """ Creates a TypeScriptLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - ts_lsp_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings) super().__init__( config, repository_root_path, - ProcessLaunchInfo(cmd=ts_lsp_executable_path, cwd=repository_root_path), + None, "typescript", solidlsp_settings, ) self.server_ready = threading.Event() self.initialize_searcher_command_available = threading.Event() + def _create_dependency_provider(self) -> LanguageServerDependencyProvider: + return self.DependencyProvider(self._custom_settings, self._ls_resources_dir) + @override def is_ignored_dirname(self, dirname: str) -> bool: return super().is_ignored_dirname(dirname) or dirname in [ @@ -97,91 +98,96 @@ class TypeScriptLanguageServer(SolidLanguageServer): """Classify typescript-language-server stderr output to avoid false-positive errors.""" return SolidLanguageServer._determine_log_level(line) - @classmethod - def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> list[str]: - """ - Setup runtime dependencies for TypeScript Language Server and return the command to start the server. - """ - platform_id = PlatformUtils.get_platform_id() + class DependencyProvider(LanguageServerDependencyProviderSinglePath): + def _get_or_install_core_dependency(self) -> str: + """ + Setup runtime dependencies for TypeScript Language Server and return the path to the executable. + """ + platform_id = PlatformUtils.get_platform_id() - valid_platforms = [ - PlatformId.LINUX_x64, - PlatformId.LINUX_arm64, - PlatformId.OSX, - PlatformId.OSX_x64, - PlatformId.OSX_arm64, - PlatformId.WIN_x64, - PlatformId.WIN_arm64, - ] - assert platform_id in valid_platforms, f"Platform {platform_id} is not supported for multilspy javascript/typescript at the moment" - - # Get version settings from ls_specific_settings or use defaults - language_specific_config = solidlsp_settings.get_ls_specific_settings(cls.get_language_enum_instance()) - typescript_version = language_specific_config.get("typescript_version", "5.9.3") - typescript_language_server_version = language_specific_config.get("typescript_language_server_version", "5.1.3") - - deps = RuntimeDependencyCollection( - [ - RuntimeDependency( - id="typescript", - description="typescript package", - command=["npm", "install", "--prefix", "./", f"typescript@{typescript_version}"], - platform_id="any", - ), - RuntimeDependency( - id="typescript-language-server", - description="typescript-language-server package", - command=["npm", "install", "--prefix", "./", f"typescript-language-server@{typescript_language_server_version}"], - platform_id="any", - ), + valid_platforms = [ + PlatformId.LINUX_x64, + PlatformId.LINUX_arm64, + PlatformId.OSX, + PlatformId.OSX_x64, + PlatformId.OSX_arm64, + PlatformId.WIN_x64, + PlatformId.WIN_arm64, ] - ) + assert ( + platform_id in valid_platforms + ), f"Platform {platform_id} is not supported for multilspy javascript/typescript at the moment" - # Verify both node and npm are installed - is_node_installed = shutil.which("node") is not None - assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again." - is_npm_installed = shutil.which("npm") is not None - assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again." + # Get version settings from ls_specific_settings or use defaults + language_specific_config = self._custom_settings + typescript_version = language_specific_config.get("typescript_version", "5.9.3") + typescript_language_server_version = language_specific_config.get("typescript_language_server_version", "5.1.3") - # Install typescript and typescript-language-server if not already installed or version mismatch - tsserver_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "ts-lsp") - tsserver_executable_path = os.path.join(tsserver_ls_dir, "node_modules", ".bin", "typescript-language-server") - - # Check if installation is needed based on executable AND version - version_file = os.path.join(tsserver_ls_dir, ".installed_version") - expected_version = f"{typescript_version}_{typescript_language_server_version}" - - needs_install = False - if not os.path.exists(tsserver_executable_path): - log.info(f"Typescript Language Server executable not found at {tsserver_executable_path}.") - needs_install = True - elif os.path.exists(version_file): - with open(version_file) as f: - installed_version = f.read().strip() - if installed_version != expected_version: - log.info( - f"TypeScript Language Server version mismatch: installed={installed_version}, expected={expected_version}. Reinstalling..." - ) - needs_install = True - else: - # No version file exists, assume old installation needs refresh - log.info("TypeScript Language Server version file not found. Reinstalling to ensure correct version...") - needs_install = True - - if needs_install: - log.info("Installing TypeScript Language Server dependencies...") - with LogTime("Installation of TypeScript language server dependencies", logger=log): - deps.install(tsserver_ls_dir) - # Write version marker file - with open(version_file, "w") as f: - f.write(expected_version) - log.info("TypeScript language server dependencies installed successfully") - - if not os.path.exists(tsserver_executable_path): - raise FileNotFoundError( - f"typescript-language-server executable not found at {tsserver_executable_path}, something went wrong with the installation." + deps = RuntimeDependencyCollection( + [ + RuntimeDependency( + id="typescript", + description="typescript package", + command=["npm", "install", "--prefix", "./", f"typescript@{typescript_version}"], + platform_id="any", + ), + RuntimeDependency( + id="typescript-language-server", + description="typescript-language-server package", + command=["npm", "install", "--prefix", "./", f"typescript-language-server@{typescript_language_server_version}"], + platform_id="any", + ), + ] ) - return [tsserver_executable_path, "--stdio"] + + # Verify both node and npm are installed + is_node_installed = shutil.which("node") is not None + assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again." + is_npm_installed = shutil.which("npm") is not None + assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again." + + # Install typescript and typescript-language-server if not already installed or version mismatch + tsserver_ls_dir = os.path.join(self._ls_resources_dir, "ts-lsp") + tsserver_executable_path = os.path.join(tsserver_ls_dir, "node_modules", ".bin", "typescript-language-server") + + # Check if installation is needed based on executable AND version + version_file = os.path.join(tsserver_ls_dir, ".installed_version") + expected_version = f"{typescript_version}_{typescript_language_server_version}" + + needs_install = False + if not os.path.exists(tsserver_executable_path): + log.info(f"Typescript Language Server executable not found at {tsserver_executable_path}.") + needs_install = True + elif os.path.exists(version_file): + with open(version_file) as f: + installed_version = f.read().strip() + if installed_version != expected_version: + log.info( + f"TypeScript Language Server version mismatch: installed={installed_version}, expected={expected_version}. Reinstalling..." + ) + needs_install = True + else: + # No version file exists, assume old installation needs refresh + log.info("TypeScript Language Server version file not found. Reinstalling to ensure correct version...") + needs_install = True + + if needs_install: + log.info("Installing TypeScript Language Server dependencies...") + with LogTime("Installation of TypeScript language server dependencies", logger=log): + deps.install(tsserver_ls_dir) + # Write version marker file + with open(version_file, "w") as f: + f.write(expected_version) + log.info("TypeScript language server dependencies installed successfully") + + if not os.path.exists(tsserver_executable_path): + raise FileNotFoundError( + f"typescript-language-server executable not found at {tsserver_executable_path}, something went wrong with the installation." + ) + return tsserver_executable_path + + def _create_launch_command(self, core_path: str) -> list[str]: + return [core_path, "--stdio"] def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: """ diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index e5e9e785..445705a7 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -130,6 +130,68 @@ class DocumentSymbols: return self._all_symbols, self.root_symbols +class LanguageServerDependencyProvider(ABC): + """ + Prepares dependencies for a language server (if any), ultimately enabling the launch command to be constructed + and optionally providing environment variables that are necessary for the execution. + """ + + def __init__(self, custom_settings: SolidLSPSettings.CustomLSSettings, ls_resources_dir: str): + self._custom_settings = custom_settings + self._ls_resources_dir = ls_resources_dir + + @abstractmethod + def create_launch_command(self) -> list[str] | str: + """ + Creates the launch command for this language server, potentially downloading and installing dependencies + beforehand. + + :return: the launch command as a list containing the executable and its arguments (preferred for robustness) + or the entire command in a single string. + """ + + def create_process_env(self) -> dict[str, str]: + """ + Provides environment variables to be set when executing the launch command. + + This method is intended to be overridden by subclasses that need to set variables. + + :return: a mapping for variable names to values + """ + return {} + + +class LanguageServerDependencyProviderSinglePath(LanguageServerDependencyProvider, ABC): + """ + Special case of a dependency provider, where there is a single core dependency which provides + the basis for the launch command. + """ + + @abstractmethod + def _get_or_install_core_dependency(self) -> str: + """ + Gets the language server's core path, potentially installing dependencies beforehand. + + :return: the core dependency's path (e.g. executable, jar, etc.) + """ + + def create_launch_command(self) -> Union[str, list[str]]: + path = self._custom_settings.get("ls_path", None) + if path is not None: + core_path = path + else: + core_path = self._get_or_install_core_dependency() + return self._create_launch_command(core_path) + + @abstractmethod + def _create_launch_command(self, core_path: str) -> list[str] | str: + """ + :param core_path: path to the core dependency + :return: the launch command as a list containing the executable and its arguments (preferred for robustness) + or the entire command in a single string. + """ + + class SolidLanguageServer(ABC): """ The LanguageServer class provides a language agnostic interface to the Language Server Protocol. @@ -242,7 +304,7 @@ class SolidLanguageServer(ABC): self, config: LanguageServerConfig, repository_root_path: str, - process_launch_info: ProcessLaunchInfo, + process_launch_info: ProcessLaunchInfo | None, language_id: str, solidlsp_settings: SolidLSPSettings, cache_version_raw_document_symbols: Hashable = 1, @@ -254,7 +316,8 @@ class SolidLanguageServer(ABC): :param config: the global SolidLSP configuration. :param repository_root_path: the root path of the repository. - :param process_launch_info: the command used to start the actual language server. + :param process_launch_info: (DEPRECATED - implement _create_dependency_provider instead) + the command used to start the actual language server. The command must pass appropriate flags to the binary, so that it runs in the stdio mode, as opposed to HTTP, TCP modes supported by some language servers. :param cache_version_raw_document_symbols: the version, for caching, of the raw document symbols coming @@ -265,9 +328,11 @@ class SolidLanguageServer(ABC): self._solidlsp_settings = solidlsp_settings lang = self.get_language_enum_instance() self._custom_settings = solidlsp_settings.get_ls_specific_settings(lang) + self._ls_resources_dir = self.ls_resources_dir(solidlsp_settings) log.debug(f"Custom config (LS-specific settings) for {lang}: {self._custom_settings}") self._encoding = config.encoding self.repository_root_path: str = repository_root_path + log.debug( f"Creating language server instance for {repository_root_path=} with {language_id=} and process launch info: {process_launch_info}" ) @@ -303,8 +368,12 @@ class SolidLanguageServer(ABC): else: logging_fn = None # type: ignore - # cmd is obtained from the child classes, which provide the language specific command to start the language server - # LanguageServerHandler provides the functionality to start the language server and communicate with it + # create the LanguageServerHandler, which provides the functionality to start the language server and communicate with it, + # preparing the launch command beforehand + self._dependency_provider: LanguageServerDependencyProvider | None = None + if process_launch_info is None: + self._dependency_provider = self._create_dependency_provider() + process_launch_info = self._create_process_launch_info() log.debug(f"Creating language server instance with {language_id=} and process launch info: {process_launch_info}") self.server = SolidLanguageServerHandler( process_launch_info, @@ -330,6 +399,23 @@ class SolidLanguageServer(ABC): self._has_waited_for_cross_file_references = False + def _create_dependency_provider(self) -> LanguageServerDependencyProvider: + """ + Creates the dependency provider for this language server. + + Subclasses should override this method to provide their specific dependency provider. + This method is only called if process_launch_info is not passed to __init__. + """ + raise NotImplementedError( + f"{self.__class__.__name__} must implement _create_dependency_provider() or pass process_launch_info to __init__()" + ) + + def _create_process_launch_info(self) -> ProcessLaunchInfo: + assert self._dependency_provider is not None + cmd = self._dependency_provider.create_launch_command() + env = self._dependency_provider.create_process_env() + return ProcessLaunchInfo(cmd=cmd, cwd=self.repository_root_path, env=env) + def _get_wait_time_for_cross_file_referencing(self) -> float: """Meant to be overridden by subclasses for LS that don't have a reliable "finished initializing" signal. From 941f4ea253eb2e62bcfe8cd5133e1a689bf29721 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 13 Jan 2026 23:41:10 +0100 Subject: [PATCH 02/14] PyrightServer: Refactor to use DependencyProvider --- src/solidlsp/language_servers/pyright_server.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/solidlsp/language_servers/pyright_server.py b/src/solidlsp/language_servers/pyright_server.py index 649dad24..6780e60d 100644 --- a/src/solidlsp/language_servers/pyright_server.py +++ b/src/solidlsp/language_servers/pyright_server.py @@ -10,11 +10,9 @@ import threading from typing import cast from overrides import override - -from solidlsp.ls import SolidLanguageServer +from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings log = logging.getLogger(__name__) @@ -34,7 +32,7 @@ class PyrightServer(SolidLanguageServer): super().__init__( config, repository_root_path, - ProcessLaunchInfo(cmd="python -m pyright.langserver --stdio", cwd=repository_root_path), + None, "python", solidlsp_settings, ) @@ -43,6 +41,16 @@ class PyrightServer(SolidLanguageServer): self.analysis_complete = threading.Event() self.found_source_files = False + def _create_dependency_provider(self) -> LanguageServerDependencyProvider: + return self.DependencyProvider(self._custom_settings, self._ls_resources_dir) + + class DependencyProvider(LanguageServerDependencyProviderSinglePath): + def _get_or_install_core_dependency(self) -> str: + return "python" + + def _create_launch_command(self, core_path: str) -> list[str]: + return [core_path, "-m", "pyright.langserver", "--stdio"] + @override def is_ignored_dirname(self, dirname: str) -> bool: return super().is_ignored_dirname(dirname) or dirname in ["venv", "__pycache__"] From 9350017605dc24294e4ebd0bd9a9c9ef827cf3bc Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 13 Jan 2026 23:41:57 +0100 Subject: [PATCH 03/14] PyrightServer: Use sys.executable instead of just 'python' (just in case) --- src/solidlsp/language_servers/pyright_server.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/solidlsp/language_servers/pyright_server.py b/src/solidlsp/language_servers/pyright_server.py index 6780e60d..515616a0 100644 --- a/src/solidlsp/language_servers/pyright_server.py +++ b/src/solidlsp/language_servers/pyright_server.py @@ -6,10 +6,12 @@ import logging import os import pathlib import re +import sys import threading from typing import cast from overrides import override + from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams @@ -46,7 +48,7 @@ class PyrightServer(SolidLanguageServer): class DependencyProvider(LanguageServerDependencyProviderSinglePath): def _get_or_install_core_dependency(self) -> str: - return "python" + return sys.executable def _create_launch_command(self, core_path: str) -> list[str]: return [core_path, "-m", "pyright.langserver", "--stdio"] From 20b91047028407539bae8a5d72d711e1c29c8169 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Wed, 14 Jan 2026 00:05:30 +0100 Subject: [PATCH 04/14] EclipseJDTLS: Refactor to use DependencyProvider --- .../language_servers/eclipse_jdtls.py | 479 +++++++++--------- 1 file changed, 246 insertions(+), 233 deletions(-) diff --git a/src/solidlsp/language_servers/eclipse_jdtls.py b/src/solidlsp/language_servers/eclipse_jdtls.py index e6fa2338..c2fd34f5 100644 --- a/src/solidlsp/language_servers/eclipse_jdtls.py +++ b/src/solidlsp/language_servers/eclipse_jdtls.py @@ -16,12 +16,11 @@ from typing import cast from overrides import override from solidlsp import ls_types -from solidlsp.ls import LSPFileBuffer, SolidLanguageServer +from solidlsp.ls import LanguageServerDependencyProvider, LSPFileBuffer, SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_types import UnifiedSymbolInformation from solidlsp.ls_utils import FileUtils, PlatformUtils from solidlsp.lsp_protocol_handler.lsp_types import DocumentSymbol, InitializeParams, SymbolInformation -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings log = logging.getLogger(__name__) @@ -69,90 +68,19 @@ class EclipseJDTLS(SolidLanguageServer): Creates a new EclipseJDTLS instance initializing the language server settings appropriately. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - runtime_dependency_paths = self._setupRuntimeDependencies(config, solidlsp_settings) - self.runtime_dependency_paths = runtime_dependency_paths + super().__init__(config, repository_root_path, None, "java", solidlsp_settings) - # ws_dir is the workspace directory for the EclipseJDTLS server - ws_dir = str( - PurePath( - solidlsp_settings.ls_resources_dir, - "EclipseJDTLS", - "workspaces", - uuid.uuid4().hex, - ) - ) - - # shared_cache_location is the global cache used by Eclipse JDTLS across all workspaces - shared_cache_location = str(PurePath(solidlsp_settings.ls_resources_dir, "lsp", "EclipseJDTLS", "sharedIndex")) - os.makedirs(shared_cache_location, exist_ok=True) - os.makedirs(ws_dir, exist_ok=True) - - jre_path = self.runtime_dependency_paths.jre_path - lombok_jar_path = self.runtime_dependency_paths.lombok_jar_path - - jdtls_launcher_jar = self.runtime_dependency_paths.jdtls_launcher_jar_path - - data_dir = str(PurePath(ws_dir, "data_dir")) - jdtls_config_path = str(PurePath(ws_dir, "config_path")) - - jdtls_readonly_config_path = self.runtime_dependency_paths.jdtls_readonly_config_path - - if not os.path.exists(jdtls_config_path): - shutil.copytree(jdtls_readonly_config_path, jdtls_config_path) - - for static_path in [ - jre_path, - lombok_jar_path, - jdtls_launcher_jar, - jdtls_config_path, - jdtls_readonly_config_path, - ]: - assert os.path.exists(static_path), static_path - - # TODO: Add "self.runtime_dependency_paths.jre_home_path"/bin to $PATH as well - proc_env = {"syntaxserver": "false", "JAVA_HOME": self.runtime_dependency_paths.jre_home_path} - proc_cwd = repository_root_path - cmd = [ - jre_path, - "--add-modules=ALL-SYSTEM", - "--add-opens", - "java.base/java.util=ALL-UNNAMED", - "--add-opens", - "java.base/java.lang=ALL-UNNAMED", - "--add-opens", - "java.base/sun.nio.fs=ALL-UNNAMED", - "-Declipse.application=org.eclipse.jdt.ls.core.id1", - "-Dosgi.bundles.defaultStartLevel=4", - "-Declipse.product=org.eclipse.jdt.ls.core.product", - "-Djava.import.generatesMetadataFilesAtProjectRoot=false", - "-Dfile.encoding=utf8", - "-noverify", - "-XX:+UseParallelGC", - "-XX:GCTimeRatio=4", - "-XX:AdaptiveSizePolicyWeight=90", - "-Dsun.zip.disableMemoryMapping=true", - "-Djava.lsp.joinOnCompletion=true", - "-Xmx3G", - "-Xms100m", - "-Xlog:disable", - "-Dlog.level=ALL", - f"-javaagent:{lombok_jar_path}", - f"-Djdt.core.sharedIndexLocation={shared_cache_location}", - "-jar", - f"{jdtls_launcher_jar}", - "-configuration", - f"{jdtls_config_path}", - "-data", - f"{data_dir}", - ] + # Extract runtime_dependency_paths from the dependency provider + assert isinstance(self._dependency_provider, self.DependencyProvider) + self.runtime_dependency_paths = self._dependency_provider.runtime_dependency_paths self._service_ready_event = threading.Event() self._project_ready_event = threading.Event() self._intellicode_enable_command_available = threading.Event() - super().__init__( - config, repository_root_path, ProcessLaunchInfo(cmd, proc_env, proc_cwd), "java", solidlsp_settings=solidlsp_settings - ) + def _create_dependency_provider(self) -> LanguageServerDependencyProvider: + ls_resources_dir = self.ls_resources_dir(self._solidlsp_settings) + return self.DependencyProvider(self._custom_settings, ls_resources_dir, self._solidlsp_settings, self.repository_root_path) @override def is_ignored_dirname(self, dirname: str) -> bool: @@ -172,168 +100,253 @@ class EclipseJDTLS(SolidLanguageServer): "lib", # General ] - @classmethod - def _setupRuntimeDependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> RuntimeDependencyPaths: - """ - Setup runtime dependencies for EclipseJDTLS and return the paths. - """ - platformId = PlatformUtils.get_platform_id() + class DependencyProvider(LanguageServerDependencyProvider): + def __init__( + self, + custom_settings: SolidLSPSettings.CustomLSSettings, + ls_resources_dir: str, + solidlsp_settings: SolidLSPSettings, + repository_root_path: str, + ): + super().__init__(custom_settings, ls_resources_dir) + self._solidlsp_settings = solidlsp_settings + self._repository_root_path = repository_root_path + self.runtime_dependency_paths = self._setup_runtime_dependencies(ls_resources_dir) - runtime_dependencies = { - "gradle": { - "platform-agnostic": { - "url": "https://services.gradle.org/distributions/gradle-8.14.2-bin.zip", - "archiveType": "zip", - "relative_extraction_path": ".", - } - }, - "vscode-java": { - "darwin-arm64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix", - "archiveType": "zip", - "relative_extraction_path": "vscode-java", - }, - "osx-arm64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix", - "archiveType": "zip", - "relative_extraction_path": "vscode-java", - "jre_home_path": "extension/jre/21.0.7-macosx-aarch64", - "jre_path": "extension/jre/21.0.7-macosx-aarch64/bin/java", - "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", - "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", - "jdtls_readonly_config_path": "extension/server/config_mac_arm", - }, - "osx-x64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-x64-1.42.0-561.vsix", - "archiveType": "zip", - "relative_extraction_path": "vscode-java", - "jre_home_path": "extension/jre/21.0.7-macosx-x86_64", - "jre_path": "extension/jre/21.0.7-macosx-x86_64/bin/java", - "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", - "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", - "jdtls_readonly_config_path": "extension/server/config_mac", - }, - "linux-arm64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-arm64-1.42.0-561.vsix", - "archiveType": "zip", - "relative_extraction_path": "vscode-java", - "jre_home_path": "extension/jre/21.0.7-linux-aarch64", - "jre_path": "extension/jre/21.0.7-linux-aarch64/bin/java", - "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", - "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", - "jdtls_readonly_config_path": "extension/server/config_linux_arm", - }, - "linux-x64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-x64-1.42.0-561.vsix", - "archiveType": "zip", - "relative_extraction_path": "vscode-java", - "jre_home_path": "extension/jre/21.0.7-linux-x86_64", - "jre_path": "extension/jre/21.0.7-linux-x86_64/bin/java", - "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", - "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", - "jdtls_readonly_config_path": "extension/server/config_linux", - }, - "win-x64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-win32-x64-1.42.0-561.vsix", - "archiveType": "zip", - "relative_extraction_path": "vscode-java", - "jre_home_path": "extension/jre/21.0.7-win32-x86_64", - "jre_path": "extension/jre/21.0.7-win32-x86_64/bin/java.exe", - "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", - "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", - "jdtls_readonly_config_path": "extension/server/config_win", - }, - }, - "intellicode": { - "platform-agnostic": { - "url": "https://VisualStudioExptTeam.gallery.vsassets.io/_apis/public/gallery/publisher/VisualStudioExptTeam/extension/vscodeintellicode/1.2.30/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage", - "alternate_url": "https://marketplace.visualstudio.com/_apis/public/gallery/publishers/VisualStudioExptTeam/vsextensions/vscodeintellicode/1.2.30/vspackage", - "archiveType": "zip", - "relative_extraction_path": "intellicode", - "intellicode_jar_path": "extension/dist/com.microsoft.jdtls.intellicode.core-0.7.0.jar", - "intellisense_members_path": "extension/dist/bundledModels/java_intellisense-members", - } - }, - } + @classmethod + def _setup_runtime_dependencies(cls, ls_resources_dir: str) -> RuntimeDependencyPaths: + """ + Setup runtime dependencies for EclipseJDTLS and return the paths. + """ + platformId = PlatformUtils.get_platform_id() - # assert platformId.value in [ - # "linux-x64", - # "win-x64", - # ], "Only linux-x64 platform is supported for in multilspy at the moment" + runtime_dependencies = { + "gradle": { + "platform-agnostic": { + "url": "https://services.gradle.org/distributions/gradle-8.14.2-bin.zip", + "archiveType": "zip", + "relative_extraction_path": ".", + } + }, + "vscode-java": { + "darwin-arm64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix", + "archiveType": "zip", + "relative_extraction_path": "vscode-java", + }, + "osx-arm64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix", + "archiveType": "zip", + "relative_extraction_path": "vscode-java", + "jre_home_path": "extension/jre/21.0.7-macosx-aarch64", + "jre_path": "extension/jre/21.0.7-macosx-aarch64/bin/java", + "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", + "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", + "jdtls_readonly_config_path": "extension/server/config_mac_arm", + }, + "osx-x64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-x64-1.42.0-561.vsix", + "archiveType": "zip", + "relative_extraction_path": "vscode-java", + "jre_home_path": "extension/jre/21.0.7-macosx-x86_64", + "jre_path": "extension/jre/21.0.7-macosx-x86_64/bin/java", + "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", + "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", + "jdtls_readonly_config_path": "extension/server/config_mac", + }, + "linux-arm64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-arm64-1.42.0-561.vsix", + "archiveType": "zip", + "relative_extraction_path": "vscode-java", + "jre_home_path": "extension/jre/21.0.7-linux-aarch64", + "jre_path": "extension/jre/21.0.7-linux-aarch64/bin/java", + "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", + "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", + "jdtls_readonly_config_path": "extension/server/config_linux_arm", + }, + "linux-x64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-x64-1.42.0-561.vsix", + "archiveType": "zip", + "relative_extraction_path": "vscode-java", + "jre_home_path": "extension/jre/21.0.7-linux-x86_64", + "jre_path": "extension/jre/21.0.7-linux-x86_64/bin/java", + "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", + "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", + "jdtls_readonly_config_path": "extension/server/config_linux", + }, + "win-x64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-win32-x64-1.42.0-561.vsix", + "archiveType": "zip", + "relative_extraction_path": "vscode-java", + "jre_home_path": "extension/jre/21.0.7-win32-x86_64", + "jre_path": "extension/jre/21.0.7-win32-x86_64/bin/java.exe", + "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", + "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", + "jdtls_readonly_config_path": "extension/server/config_win", + }, + }, + "intellicode": { + "platform-agnostic": { + "url": "https://VisualStudioExptTeam.gallery.vsassets.io/_apis/public/gallery/publisher/VisualStudioExptTeam/extension/vscodeintellicode/1.2.30/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage", + "alternate_url": "https://marketplace.visualstudio.com/_apis/public/gallery/publishers/VisualStudioExptTeam/vsextensions/vscodeintellicode/1.2.30/vspackage", + "archiveType": "zip", + "relative_extraction_path": "intellicode", + "intellicode_jar_path": "extension/dist/com.microsoft.jdtls.intellicode.core-0.7.0.jar", + "intellisense_members_path": "extension/dist/bundledModels/java_intellisense-members", + } + }, + } - gradle_path = str( - PurePath( - cls.ls_resources_dir(solidlsp_settings), - "gradle-8.14.2", - ) - ) - - if not os.path.exists(gradle_path): - FileUtils.download_and_extract_archive( - runtime_dependencies["gradle"]["platform-agnostic"]["url"], - str(PurePath(gradle_path).parent), - runtime_dependencies["gradle"]["platform-agnostic"]["archiveType"], + gradle_path = str( + PurePath( + ls_resources_dir, + "gradle-8.14.2", + ) ) - assert os.path.exists(gradle_path) + if not os.path.exists(gradle_path): + FileUtils.download_and_extract_archive( + runtime_dependencies["gradle"]["platform-agnostic"]["url"], + str(PurePath(gradle_path).parent), + runtime_dependencies["gradle"]["platform-agnostic"]["archiveType"], + ) - dependency = runtime_dependencies["vscode-java"][platformId.value] - vscode_java_path = str(PurePath(cls.ls_resources_dir(solidlsp_settings), dependency["relative_extraction_path"])) - os.makedirs(vscode_java_path, exist_ok=True) - jre_home_path = str(PurePath(vscode_java_path, dependency["jre_home_path"])) - jre_path = str(PurePath(vscode_java_path, dependency["jre_path"])) - lombok_jar_path = str(PurePath(vscode_java_path, dependency["lombok_jar_path"])) - jdtls_launcher_jar_path = str(PurePath(vscode_java_path, dependency["jdtls_launcher_jar_path"])) - jdtls_readonly_config_path = str(PurePath(vscode_java_path, dependency["jdtls_readonly_config_path"])) - if not all( - [ - os.path.exists(vscode_java_path), - os.path.exists(jre_home_path), - os.path.exists(jre_path), - os.path.exists(lombok_jar_path), - os.path.exists(jdtls_launcher_jar_path), - os.path.exists(jdtls_readonly_config_path), + assert os.path.exists(gradle_path) + + dependency = runtime_dependencies["vscode-java"][platformId.value] + vscode_java_path = str(PurePath(ls_resources_dir, dependency["relative_extraction_path"])) + os.makedirs(vscode_java_path, exist_ok=True) + jre_home_path = str(PurePath(vscode_java_path, dependency["jre_home_path"])) + jre_path = str(PurePath(vscode_java_path, dependency["jre_path"])) + lombok_jar_path = str(PurePath(vscode_java_path, dependency["lombok_jar_path"])) + jdtls_launcher_jar_path = str(PurePath(vscode_java_path, dependency["jdtls_launcher_jar_path"])) + jdtls_readonly_config_path = str(PurePath(vscode_java_path, dependency["jdtls_readonly_config_path"])) + if not all( + [ + os.path.exists(vscode_java_path), + os.path.exists(jre_home_path), + os.path.exists(jre_path), + os.path.exists(lombok_jar_path), + os.path.exists(jdtls_launcher_jar_path), + os.path.exists(jdtls_readonly_config_path), + ] + ): + FileUtils.download_and_extract_archive(dependency["url"], vscode_java_path, dependency["archiveType"]) + + os.chmod(jre_path, 0o755) + + assert os.path.exists(vscode_java_path) + assert os.path.exists(jre_home_path) + assert os.path.exists(jre_path) + assert os.path.exists(lombok_jar_path) + assert os.path.exists(jdtls_launcher_jar_path) + assert os.path.exists(jdtls_readonly_config_path) + + dependency = runtime_dependencies["intellicode"]["platform-agnostic"] + intellicode_directory_path = str(PurePath(ls_resources_dir, dependency["relative_extraction_path"])) + os.makedirs(intellicode_directory_path, exist_ok=True) + intellicode_jar_path = str(PurePath(intellicode_directory_path, dependency["intellicode_jar_path"])) + intellisense_members_path = str(PurePath(intellicode_directory_path, dependency["intellisense_members_path"])) + if not all( + [ + os.path.exists(intellicode_directory_path), + os.path.exists(intellicode_jar_path), + os.path.exists(intellisense_members_path), + ] + ): + FileUtils.download_and_extract_archive(dependency["url"], intellicode_directory_path, dependency["archiveType"]) + + assert os.path.exists(intellicode_directory_path) + assert os.path.exists(intellicode_jar_path) + assert os.path.exists(intellisense_members_path) + + return RuntimeDependencyPaths( + gradle_path=gradle_path, + lombok_jar_path=lombok_jar_path, + jre_path=jre_path, + jre_home_path=jre_home_path, + jdtls_launcher_jar_path=jdtls_launcher_jar_path, + jdtls_readonly_config_path=jdtls_readonly_config_path, + intellicode_jar_path=intellicode_jar_path, + intellisense_members_path=intellisense_members_path, + ) + + def create_launch_command(self) -> list[str] | str: + # ws_dir is the workspace directory for the EclipseJDTLS server + ws_dir = str( + PurePath( + self._solidlsp_settings.ls_resources_dir, + "EclipseJDTLS", + "workspaces", + uuid.uuid4().hex, + ) + ) + + # shared_cache_location is the global cache used by Eclipse JDTLS across all workspaces + shared_cache_location = str(PurePath(self._solidlsp_settings.ls_resources_dir, "lsp", "EclipseJDTLS", "sharedIndex")) + os.makedirs(shared_cache_location, exist_ok=True) + os.makedirs(ws_dir, exist_ok=True) + + jre_path = self.runtime_dependency_paths.jre_path + lombok_jar_path = self.runtime_dependency_paths.lombok_jar_path + + jdtls_launcher_jar = self.runtime_dependency_paths.jdtls_launcher_jar_path + + data_dir = str(PurePath(ws_dir, "data_dir")) + jdtls_config_path = str(PurePath(ws_dir, "config_path")) + + jdtls_readonly_config_path = self.runtime_dependency_paths.jdtls_readonly_config_path + + if not os.path.exists(jdtls_config_path): + shutil.copytree(jdtls_readonly_config_path, jdtls_config_path) + + for static_path in [ + jre_path, + lombok_jar_path, + jdtls_launcher_jar, + jdtls_config_path, + jdtls_readonly_config_path, + ]: + assert os.path.exists(static_path), static_path + + cmd = [ + jre_path, + "--add-modules=ALL-SYSTEM", + "--add-opens", + "java.base/java.util=ALL-UNNAMED", + "--add-opens", + "java.base/java.lang=ALL-UNNAMED", + "--add-opens", + "java.base/sun.nio.fs=ALL-UNNAMED", + "-Declipse.application=org.eclipse.jdt.ls.core.id1", + "-Dosgi.bundles.defaultStartLevel=4", + "-Declipse.product=org.eclipse.jdt.ls.core.product", + "-Djava.import.generatesMetadataFilesAtProjectRoot=false", + "-Dfile.encoding=utf8", + "-noverify", + "-XX:+UseParallelGC", + "-XX:GCTimeRatio=4", + "-XX:AdaptiveSizePolicyWeight=90", + "-Dsun.zip.disableMemoryMapping=true", + "-Djava.lsp.joinOnCompletion=true", + "-Xmx3G", + "-Xms100m", + "-Xlog:disable", + "-Dlog.level=ALL", + f"-javaagent:{lombok_jar_path}", + f"-Djdt.core.sharedIndexLocation={shared_cache_location}", + "-jar", + f"{jdtls_launcher_jar}", + "-configuration", + f"{jdtls_config_path}", + "-data", + f"{data_dir}", ] - ): - FileUtils.download_and_extract_archive(dependency["url"], vscode_java_path, dependency["archiveType"]) - os.chmod(jre_path, 0o755) + return cmd - assert os.path.exists(vscode_java_path) - assert os.path.exists(jre_home_path) - assert os.path.exists(jre_path) - assert os.path.exists(lombok_jar_path) - assert os.path.exists(jdtls_launcher_jar_path) - assert os.path.exists(jdtls_readonly_config_path) - - dependency = runtime_dependencies["intellicode"]["platform-agnostic"] - intellicode_directory_path = str(PurePath(cls.ls_resources_dir(solidlsp_settings), dependency["relative_extraction_path"])) - os.makedirs(intellicode_directory_path, exist_ok=True) - intellicode_jar_path = str(PurePath(intellicode_directory_path, dependency["intellicode_jar_path"])) - intellisense_members_path = str(PurePath(intellicode_directory_path, dependency["intellisense_members_path"])) - if not all( - [ - os.path.exists(intellicode_directory_path), - os.path.exists(intellicode_jar_path), - os.path.exists(intellisense_members_path), - ] - ): - FileUtils.download_and_extract_archive(dependency["url"], intellicode_directory_path, dependency["archiveType"]) - - assert os.path.exists(intellicode_directory_path) - assert os.path.exists(intellicode_jar_path) - assert os.path.exists(intellisense_members_path) - - return RuntimeDependencyPaths( - gradle_path=gradle_path, - lombok_jar_path=lombok_jar_path, - jre_path=jre_path, - jre_home_path=jre_home_path, - jdtls_launcher_jar_path=jdtls_launcher_jar_path, - jdtls_readonly_config_path=jdtls_readonly_config_path, - intellicode_jar_path=intellicode_jar_path, - intellisense_members_path=intellisense_members_path, - ) + def create_process_env(self) -> dict[str, str]: + return {"syntaxserver": "false", "JAVA_HOME": self.runtime_dependency_paths.jre_home_path} def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: """ From df642b8ca140a7cd527ad98e60de582c96958b55 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Wed, 14 Jan 2026 01:00:24 +0100 Subject: [PATCH 05/14] VueTypeScriptServer: Refactor to use DependencyProvider --- .../language_servers/vue_language_server.py | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/solidlsp/language_servers/vue_language_server.py b/src/solidlsp/language_servers/vue_language_server.py index ee419345..42aad243 100644 --- a/src/solidlsp/language_servers/vue_language_server.py +++ b/src/solidlsp/language_servers/vue_language_server.py @@ -36,8 +36,6 @@ log = logging.getLogger(__name__) class VueTypeScriptServer(TypeScriptLanguageServer): """TypeScript LS configured with @vue/typescript-plugin for Vue file support.""" - _pending_ts_ls_executable: list[str] | None = None - @classmethod @override def get_language_enum_instance(cls) -> Language: @@ -49,12 +47,13 @@ class VueTypeScriptServer(TypeScriptLanguageServer): """ return Language.TYPESCRIPT - @classmethod - @override - def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> list[str]: - if cls._pending_ts_ls_executable is not None: - return cls._pending_ts_ls_executable - return ["typescript-language-server", "--stdio"] + class DependencyProvider(TypeScriptLanguageServer.DependencyProvider): + override_ts_ls_executable: str | None = None + + def _get_or_install_core_dependency(self) -> str: + if self.override_ts_ls_executable is not None: + return self.override_ts_ls_executable + return super()._get_or_install_core_dependency() @override def _get_language_id_for_file(self, relative_file_path: str) -> str: @@ -81,13 +80,13 @@ class VueTypeScriptServer(TypeScriptLanguageServer): solidlsp_settings: SolidLSPSettings, vue_plugin_path: str, tsdk_path: str, - ts_ls_executable_path: list[str], + ts_ls_executable_path: str, ): self._vue_plugin_path = vue_plugin_path self._custom_tsdk_path = tsdk_path - VueTypeScriptServer._pending_ts_ls_executable = ts_ls_executable_path + VueTypeScriptServer.DependencyProvider.override_ts_ls_executable = ts_ls_executable_path super().__init__(config, repository_root_path, solidlsp_settings) - VueTypeScriptServer._pending_ts_ls_executable = None + VueTypeScriptServer.DependencyProvider.override_ts_ls_executable = None @override def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: @@ -383,9 +382,7 @@ class VueLanguageServer(SolidLanguageServer): return self._ts_server.request_rename_symbol_edit(relative_file_path, line, column, new_name) @classmethod - def _setup_runtime_dependencies( - cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings - ) -> tuple[list[str], str, list[str]]: + def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> tuple[list[str], str, str]: is_node_installed = shutil.which("node") is not None assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again." is_npm_installed = shutil.which("npm") is not None @@ -476,7 +473,7 @@ class VueLanguageServer(SolidLanguageServer): f"typescript-language-server executable not found at {ts_ls_executable_path}, something went wrong with the installation." ) - return [vue_executable_path, "--stdio"], tsdk_path, [ts_ls_executable_path, "--stdio"] + return [vue_executable_path, "--stdio"], tsdk_path, ts_ls_executable_path def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: root_uri = pathlib.Path(repository_absolute_path).as_uri() From 97a777c55b43f3cfdd937e4f1296aafcd5343cc0 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Wed, 14 Jan 2026 02:02:19 +0100 Subject: [PATCH 06/14] MatLabLanguageServer: Move _find_matlab_installation into DependencyProvider --- .../matlab_language_server.py | 144 +++++++++--------- 1 file changed, 72 insertions(+), 72 deletions(-) diff --git a/src/solidlsp/language_servers/matlab_language_server.py b/src/solidlsp/language_servers/matlab_language_server.py index 9be9390b..3c3fbe44 100644 --- a/src/solidlsp/language_servers/matlab_language_server.py +++ b/src/solidlsp/language_servers/matlab_language_server.py @@ -74,77 +74,6 @@ class MatlabLanguageServer(SolidLanguageServer): - matlab_path: Path to MATLAB installation (overrides MATLAB_PATH env var) """ - @staticmethod - def _find_matlab_installation() -> str: - """ - Find MATLAB installation path. - - Search order: - 1. MATLAB_PATH environment variable - 2. Common installation locations based on platform - - Returns: - Path to MATLAB installation directory. - - Raises: - RuntimeError: If MATLAB installation is not found. - - """ - # Check environment variable first - matlab_path = os.environ.get(MATLAB_PATH_ENV_VAR) - if matlab_path and os.path.isdir(matlab_path): - log.info(f"Using MATLAB from environment variable {MATLAB_PATH_ENV_VAR}: {matlab_path}") - return matlab_path - - system = platform.system() - - if system == "Darwin": # macOS - # Check common macOS locations - search_patterns = [ - "/Applications/MATLAB_*.app", - "/Volumes/*/Applications/MATLAB_*.app", - os.path.expanduser("~/Applications/MATLAB_*.app"), - ] - for pattern in search_patterns: - matches = sorted(glob.glob(pattern), reverse=True) # Newest version first - for match in matches: - if os.path.isdir(match): - log.info(f"Found MATLAB installation: {match}") - return match - - elif system == "Windows": - # Check common Windows locations - search_patterns = [ - "C:\\Program Files\\MATLAB\\R*", - "C:\\Program Files (x86)\\MATLAB\\R*", - ] - for pattern in search_patterns: - matches = sorted(glob.glob(pattern), reverse=True) - for match in matches: - if os.path.isdir(match): - log.info(f"Found MATLAB installation: {match}") - return match - - elif system == "Linux": - # Check common Linux locations - search_patterns = [ - "/usr/local/MATLAB/R*", - "/opt/MATLAB/R*", - os.path.expanduser("~/MATLAB/R*"), - ] - for pattern in search_patterns: - matches = sorted(glob.glob(pattern), reverse=True) - for match in matches: - if os.path.isdir(match): - log.info(f"Found MATLAB installation: {match}") - return match - - raise RuntimeError( - f"MATLAB installation not found. Set the {MATLAB_PATH_ENV_VAR} environment variable " - "to your MATLAB installation directory (e.g., /Applications/MATLAB_R2024b.app on macOS, " - "C:\\Program Files\\MATLAB\\R2024b on Windows, or /usr/local/MATLAB/R2024b on Linux)." - ) - def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): """ Creates a MatlabLanguageServer instance. This class is not meant to be instantiated directly. @@ -320,6 +249,77 @@ class MatlabLanguageServer(SolidLanguageServer): raise RuntimeError(f"MATLAB language server script not found in extension at {extension_path}") + @staticmethod + def _find_matlab_installation() -> str: + """ + Find MATLAB installation path. + + Search order: + 1. MATLAB_PATH environment variable + 2. Common installation locations based on platform + + Returns: + Path to MATLAB installation directory. + + Raises: + RuntimeError: If MATLAB installation is not found. + + """ + # Check environment variable first + matlab_path = os.environ.get(MATLAB_PATH_ENV_VAR) + if matlab_path and os.path.isdir(matlab_path): + log.info(f"Using MATLAB from environment variable {MATLAB_PATH_ENV_VAR}: {matlab_path}") + return matlab_path + + system = platform.system() + + if system == "Darwin": # macOS + # Check common macOS locations + search_patterns = [ + "/Applications/MATLAB_*.app", + "/Volumes/*/Applications/MATLAB_*.app", + os.path.expanduser("~/Applications/MATLAB_*.app"), + ] + for pattern in search_patterns: + matches = sorted(glob.glob(pattern), reverse=True) # Newest version first + for match in matches: + if os.path.isdir(match): + log.info(f"Found MATLAB installation: {match}") + return match + + elif system == "Windows": + # Check common Windows locations + search_patterns = [ + "C:\\Program Files\\MATLAB\\R*", + "C:\\Program Files (x86)\\MATLAB\\R*", + ] + for pattern in search_patterns: + matches = sorted(glob.glob(pattern), reverse=True) + for match in matches: + if os.path.isdir(match): + log.info(f"Found MATLAB installation: {match}") + return match + + elif system == "Linux": + # Check common Linux locations + search_patterns = [ + "/usr/local/MATLAB/R*", + "/opt/MATLAB/R*", + os.path.expanduser("~/MATLAB/R*"), + ] + for pattern in search_patterns: + matches = sorted(glob.glob(pattern), reverse=True) + for match in matches: + if os.path.isdir(match): + log.info(f"Found MATLAB installation: {match}") + return match + + raise RuntimeError( + f"MATLAB installation not found. Set the {MATLAB_PATH_ENV_VAR} environment variable " + "to your MATLAB installation directory (e.g., /Applications/MATLAB_R2024b.app on macOS, " + "C:\\Program Files\\MATLAB\\R2024b on Windows, or /usr/local/MATLAB/R2024b on Linux)." + ) + def get_matlab_path(self) -> str: """Get MATLAB path from settings or auto-detect.""" if self._matlab_path is not None: @@ -328,7 +328,7 @@ class MatlabLanguageServer(SolidLanguageServer): matlab_path = self._custom_settings.get("matlab_path") if not matlab_path: - matlab_path = MatlabLanguageServer._find_matlab_installation() # Raises RuntimeError if not found + matlab_path = self._find_matlab_installation() # Raises RuntimeError if not found # Verify MATLAB path exists if not os.path.isdir(matlab_path): From 62cff92b0f92e4d0016c485b3d99ba93d9687051 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Wed, 14 Jan 2026 02:05:39 +0100 Subject: [PATCH 07/14] Refactor DependencyProvider: Rename method create_process_env -> create_launch_command_env --- src/solidlsp/language_servers/eclipse_jdtls.py | 2 +- src/solidlsp/language_servers/matlab_language_server.py | 2 +- src/solidlsp/ls.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/solidlsp/language_servers/eclipse_jdtls.py b/src/solidlsp/language_servers/eclipse_jdtls.py index c2fd34f5..f7ed5c22 100644 --- a/src/solidlsp/language_servers/eclipse_jdtls.py +++ b/src/solidlsp/language_servers/eclipse_jdtls.py @@ -345,7 +345,7 @@ class EclipseJDTLS(SolidLanguageServer): return cmd - def create_process_env(self) -> dict[str, str]: + def create_launch_command_env(self) -> dict[str, str]: return {"syntaxserver": "false", "JAVA_HOME": self.runtime_dependency_paths.jre_home_path} def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: diff --git a/src/solidlsp/language_servers/matlab_language_server.py b/src/solidlsp/language_servers/matlab_language_server.py index 3c3fbe44..bf23a425 100644 --- a/src/solidlsp/language_servers/matlab_language_server.py +++ b/src/solidlsp/language_servers/matlab_language_server.py @@ -370,7 +370,7 @@ class MatlabLanguageServer(SolidLanguageServer): cmd = [node_path, server_script, "--stdio"] return cmd - def create_process_env(self) -> dict[str, str]: + def create_launch_command_env(self) -> dict[str, str]: return { "MATLAB_INSTALL_PATH": self.get_matlab_path(), } diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 445705a7..a4db066c 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -150,7 +150,7 @@ class LanguageServerDependencyProvider(ABC): or the entire command in a single string. """ - def create_process_env(self) -> dict[str, str]: + def create_launch_command_env(self) -> dict[str, str]: """ Provides environment variables to be set when executing the launch command. @@ -413,7 +413,7 @@ class SolidLanguageServer(ABC): def _create_process_launch_info(self) -> ProcessLaunchInfo: assert self._dependency_provider is not None cmd = self._dependency_provider.create_launch_command() - env = self._dependency_provider.create_process_env() + env = self._dependency_provider.create_launch_command_env() return ProcessLaunchInfo(cmd=cmd, cwd=self.repository_root_path, env=env) def _get_wait_time_for_cross_file_referencing(self) -> float: From b33a4085c2c60e46d8f10bd8ae90895cbc574829 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Wed, 14 Jan 2026 02:27:55 +0100 Subject: [PATCH 08/14] CSharpLanguageServer: Refactored to use DependencyProvider --- .../csharp_language_server.py | 457 +++++++++--------- test/solidlsp/csharp/test_csharp_basic.py | 4 +- 2 files changed, 235 insertions(+), 226 deletions(-) diff --git a/src/solidlsp/language_servers/csharp_language_server.py b/src/solidlsp/language_servers/csharp_language_server.py index e7ee5d7d..24b39a38 100644 --- a/src/solidlsp/language_servers/csharp_language_server.py +++ b/src/solidlsp/language_servers/csharp_language_server.py @@ -18,12 +18,11 @@ from typing import Any, cast from overrides import override -from solidlsp.ls import SolidLanguageServer +from solidlsp.ls import LanguageServerDependencyProvider, SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_exceptions import SolidLSPException from solidlsp.ls_utils import PathUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams, InitializeResult -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings from solidlsp.util.zip import SafeZipExtractor @@ -212,266 +211,276 @@ class CSharpLanguageServer(SolidLanguageServer): Creates a CSharpLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - dotnet_path, language_server_path = self._ensure_server_installed(config, solidlsp_settings) - - # Find solution or project file - solution_or_project = find_solution_or_project_file(repository_root_path) - - # Create log directory - log_dir = Path(self.ls_resources_dir(solidlsp_settings)) / "logs" - log_dir.mkdir(parents=True, exist_ok=True) - - # Build command using dotnet directly - cmd = [dotnet_path, language_server_path, "--logLevel=Information", f"--extensionLogDirectory={log_dir}", "--stdio"] - - # The language server will discover the solution/project from the workspace root - if solution_or_project: - log.info(f"Found solution/project file: {solution_or_project}") - else: - log.warning("No .sln or .csproj file found, language server will attempt auto-discovery") - - log.debug(f"Language server command: {' '.join(cmd)}") - - super().__init__(config, repository_root_path, ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path), "csharp", solidlsp_settings) + super().__init__(config, repository_root_path, None, "csharp", solidlsp_settings) self.initialization_complete = threading.Event() + def _create_dependency_provider(self) -> LanguageServerDependencyProvider: + return self.DependencyProvider(self._custom_settings, self._ls_resources_dir, self._solidlsp_settings, self.repository_root_path) + @override def is_ignored_dirname(self, dirname: str) -> bool: return super().is_ignored_dirname(dirname) or dirname in ["bin", "obj", "packages", ".vs"] - @classmethod - def _ensure_server_installed(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> tuple[str, str]: - """ - Ensure .NET runtime and Microsoft.CodeAnalysis.LanguageServer are available. - Returns a tuple of (dotnet_path, language_server_dll_path). - """ - language_specific_config = solidlsp_settings.get_ls_specific_settings(cls.get_language_enum_instance()) - runtime_dependency_overrides = cast(list[dict[str, Any]], language_specific_config.get("runtime_dependencies", [])) + class DependencyProvider(LanguageServerDependencyProvider): + def __init__( + self, + custom_settings: SolidLSPSettings.CustomLSSettings, + ls_resources_dir: str, + solidlsp_settings: SolidLSPSettings, + repository_root_path: str, + ): + super().__init__(custom_settings, ls_resources_dir) + self._solidlsp_settings = solidlsp_settings + self._repository_root_path = repository_root_path + self._dotnet_path, self._language_server_path = self._ensure_server_installed() - log.debug("Resolving runtime dependencies") + def create_launch_command(self) -> list[str] | str: + # Find solution or project file + solution_or_project = find_solution_or_project_file(self._repository_root_path) - runtime_dependencies = RuntimeDependencyCollection( - _RUNTIME_DEPENDENCIES, - overrides=runtime_dependency_overrides, - ) + # Create log directory + log_dir = Path(self._ls_resources_dir) / "logs" + log_dir.mkdir(parents=True, exist_ok=True) - log.debug( - f"Available runtime dependencies: {runtime_dependencies.get_dependencies_for_current_platform}", - ) + # Build command using dotnet directly + cmd = [self._dotnet_path, self._language_server_path, "--logLevel=Information", f"--extensionLogDirectory={log_dir}", "--stdio"] - # Find the dependencies for our platform - lang_server_dep = runtime_dependencies.get_single_dep_for_current_platform("CSharpLanguageServer") - dotnet_runtime_dep = runtime_dependencies.get_single_dep_for_current_platform("DotNetRuntime") - dotnet_path = CSharpLanguageServer._ensure_dotnet_runtime(dotnet_runtime_dep, solidlsp_settings) - server_dll_path = CSharpLanguageServer._ensure_language_server(lang_server_dep, solidlsp_settings) + # The language server will discover the solution/project from the workspace root + if solution_or_project: + log.info(f"Found solution/project file: {solution_or_project}") + else: + log.warning("No .sln or .csproj file found, language server will attempt auto-discovery") - return dotnet_path, server_dll_path + log.debug(f"Language server command: {' '.join(cmd)}") - @classmethod - def _ensure_dotnet_runtime(cls, dotnet_runtime_dep: RuntimeDependency, solidlsp_settings: SolidLSPSettings) -> str: - """Ensure .NET runtime is available and return the dotnet executable path.""" - # TODO: use RuntimeDependency util methods instead of custom validation/download logic + return cmd - # Check if dotnet is already available on the system - system_dotnet = shutil.which("dotnet") - if system_dotnet: - # Check if it's .NET 9 - try: - result = subprocess.run([system_dotnet, "--list-runtimes"], capture_output=True, text=True, check=True) - if "Microsoft.NETCore.App 9." in result.stdout: - log.info("Found system .NET 9 runtime") - return system_dotnet - except subprocess.CalledProcessError: - pass + def _ensure_server_installed(self) -> tuple[str, str]: + """ + Ensure .NET runtime and Microsoft.CodeAnalysis.LanguageServer are available. + Returns a tuple of (dotnet_path, language_server_dll_path). + """ + runtime_dependency_overrides = cast(list[dict[str, Any]], self._custom_settings.get("runtime_dependencies", [])) - # Download .NET 9 runtime using config - return cls._ensure_dotnet_runtime_from_config(dotnet_runtime_dep, solidlsp_settings) + log.debug("Resolving runtime dependencies") - @classmethod - def _ensure_language_server(cls, lang_server_dep: RuntimeDependency, solidlsp_settings: SolidLSPSettings) -> str: - """Ensure language server is available and return the DLL path.""" - package_name = lang_server_dep.package_name - package_version = lang_server_dep.package_version + runtime_dependencies = RuntimeDependencyCollection( + _RUNTIME_DEPENDENCIES, + overrides=runtime_dependency_overrides, + ) - server_dir = Path(cls.ls_resources_dir(solidlsp_settings)) / f"{package_name}.{package_version}" - assert lang_server_dep.binary_name is not None - server_dll = server_dir / lang_server_dep.binary_name + log.debug( + f"Available runtime dependencies: {runtime_dependencies.get_dependencies_for_current_platform}", + ) - if server_dll.exists(): - log.info(f"Using cached Microsoft.CodeAnalysis.LanguageServer from {server_dll}") + # Find the dependencies for our platform + lang_server_dep = runtime_dependencies.get_single_dep_for_current_platform("CSharpLanguageServer") + dotnet_runtime_dep = runtime_dependencies.get_single_dep_for_current_platform("DotNetRuntime") + dotnet_path = self._ensure_dotnet_runtime(dotnet_runtime_dep) + server_dll_path = self._ensure_language_server(lang_server_dep) + + return dotnet_path, server_dll_path + + def _ensure_dotnet_runtime(self, dotnet_runtime_dep: RuntimeDependency) -> str: + """Ensure .NET runtime is available and return the dotnet executable path.""" + # TODO: use RuntimeDependency util methods instead of custom validation/download logic + + # Check if dotnet is already available on the system + system_dotnet = shutil.which("dotnet") + if system_dotnet: + # Check if it's .NET 9 + try: + result = subprocess.run([system_dotnet, "--list-runtimes"], capture_output=True, text=True, check=True) + if "Microsoft.NETCore.App 9." in result.stdout: + log.info("Found system .NET 9 runtime") + return system_dotnet + except subprocess.CalledProcessError: + pass + + # Download .NET 9 runtime using config + return self._ensure_dotnet_runtime_from_config(dotnet_runtime_dep) + + def _ensure_language_server(self, lang_server_dep: RuntimeDependency) -> str: + """Ensure language server is available and return the DLL path.""" + package_name = lang_server_dep.package_name + package_version = lang_server_dep.package_version + + server_dir = Path(self._ls_resources_dir) / f"{package_name}.{package_version}" + assert lang_server_dep.binary_name is not None + server_dll = server_dir / lang_server_dep.binary_name + + if server_dll.exists(): + log.info(f"Using cached Microsoft.CodeAnalysis.LanguageServer from {server_dll}") + return str(server_dll) + + # Download and install the language server + log.info(f"Downloading {package_name} version {package_version}...") + assert package_version is not None + assert package_name is not None + package_path = self._download_nuget_package_direct(package_name, package_version) + + # Extract and install + self._extract_language_server(lang_server_dep, package_path, server_dir) + + if not server_dll.exists(): + raise SolidLSPException("Microsoft.CodeAnalysis.LanguageServer.dll not found after extraction") + + # Make executable on Unix systems + if platform.system().lower() != "windows": + server_dll.chmod(0o755) + + log.info(f"Successfully installed Microsoft.CodeAnalysis.LanguageServer to {server_dll}") return str(server_dll) - # Download and install the language server - log.info(f"Downloading {package_name} version {package_version}...") - assert package_version is not None - assert package_name is not None - package_path = cls._download_nuget_package_direct(package_name, package_version, solidlsp_settings) + @staticmethod + def _extract_language_server(lang_server_dep: RuntimeDependency, package_path: Path, server_dir: Path) -> None: + """Extract language server files from downloaded package.""" + extract_path = lang_server_dep.extract_path or "lib/net9.0" + source_dir = package_path / extract_path - # Extract and install - cls._extract_language_server(lang_server_dep, package_path, server_dir) + if not source_dir.exists(): + # Try alternative locations + for possible_dir in [ + package_path / "tools" / "net9.0" / "any", + package_path / "lib" / "net9.0", + package_path / "contentFiles" / "any" / "net9.0", + ]: + if possible_dir.exists(): + source_dir = possible_dir + break + else: + raise SolidLSPException(f"Could not find language server files in package. Searched in {package_path}") - if not server_dll.exists(): - raise SolidLSPException("Microsoft.CodeAnalysis.LanguageServer.dll not found after extraction") + # Copy files to cache directory + server_dir.mkdir(parents=True, exist_ok=True) + shutil.copytree(source_dir, server_dir, dirs_exist_ok=True) - # Make executable on Unix systems - if platform.system().lower() != "windows": - server_dll.chmod(0o755) + def _download_nuget_package_direct(self, package_name: str, package_version: str) -> Path: + """ + Download a NuGet package directly from the Azure NuGet feed. + Returns the path to the extracted package directory. + """ + azure_feed_url = "https://pkgs.dev.azure.com/azure-public/vside/_packaging/vs-impl/nuget/v3/index.json" - log.info(f"Successfully installed Microsoft.CodeAnalysis.LanguageServer to {server_dll}") - return str(server_dll) + # Create temporary directory for package download + temp_dir = Path(self._ls_resources_dir) / "temp_downloads" + temp_dir.mkdir(parents=True, exist_ok=True) - @staticmethod - def _extract_language_server(lang_server_dep: RuntimeDependency, package_path: Path, server_dir: Path) -> None: - """Extract language server files from downloaded package.""" - extract_path = lang_server_dep.extract_path or "lib/net9.0" - source_dir = package_path / extract_path - - if not source_dir.exists(): - # Try alternative locations - for possible_dir in [ - package_path / "tools" / "net9.0" / "any", - package_path / "lib" / "net9.0", - package_path / "contentFiles" / "any" / "net9.0", - ]: - if possible_dir.exists(): - source_dir = possible_dir - break - else: - raise SolidLSPException(f"Could not find language server files in package. Searched in {package_path}") - - # Copy files to cache directory - server_dir.mkdir(parents=True, exist_ok=True) - shutil.copytree(source_dir, server_dir, dirs_exist_ok=True) - - @classmethod - def _download_nuget_package_direct(cls, package_name: str, package_version: str, solidlsp_settings: SolidLSPSettings) -> Path: - """ - Download a NuGet package directly from the Azure NuGet feed. - Returns the path to the extracted package directory. - """ - azure_feed_url = "https://pkgs.dev.azure.com/azure-public/vside/_packaging/vs-impl/nuget/v3/index.json" - - # Create temporary directory for package download - temp_dir = Path(cls.ls_resources_dir(solidlsp_settings)) / "temp_downloads" - temp_dir.mkdir(parents=True, exist_ok=True) - - try: - # First, get the service index from the Azure feed - log.debug("Fetching NuGet service index from Azure feed...") - with urllib.request.urlopen(azure_feed_url) as response: - service_index = json.loads(response.read().decode()) - - # Find the package base address (for downloading packages) - package_base_address = None - for resource in service_index.get("resources", []): - if resource.get("@type") == "PackageBaseAddress/3.0.0": - package_base_address = resource.get("@id") - break - - if not package_base_address: - raise SolidLSPException("Could not find package base address in Azure NuGet feed") - - # Construct the download URL for the specific package - package_id_lower = package_name.lower() - package_version_lower = package_version.lower() - package_url = f"{package_base_address.rstrip('/')}/{package_id_lower}/{package_version_lower}/{package_id_lower}.{package_version_lower}.nupkg" - - log.debug(f"Downloading package from: {package_url}") - - # Download the .nupkg file - nupkg_file = temp_dir / f"{package_name}.{package_version}.nupkg" - urllib.request.urlretrieve(package_url, nupkg_file) - - # Extract the .nupkg file (it's just a zip file) - package_extract_dir = temp_dir / f"{package_name}.{package_version}" - package_extract_dir.mkdir(exist_ok=True) - - # Use SafeZipExtractor to handle long paths and skip errors - extractor = SafeZipExtractor(archive_path=nupkg_file, extract_dir=package_extract_dir, verbose=False) - extractor.extract_all() - - # Clean up the nupkg file - nupkg_file.unlink() - - log.info(f"Successfully downloaded and extracted {package_name} version {package_version}") - return package_extract_dir - - except Exception as e: - raise SolidLSPException( - f"Failed to download package {package_name} version {package_version} from Azure NuGet feed: {e}" - ) from e - - @classmethod - def _ensure_dotnet_runtime_from_config(cls, dotnet_runtime_dep: RuntimeDependency, solidlsp_settings: SolidLSPSettings) -> str: - """ - Ensure .NET 9 runtime is available using runtime dependency configuration. - Returns the path to the dotnet executable. - """ - # TODO: use RuntimeDependency util methods instead of custom download logic - - # Check if dotnet is already available on the system - system_dotnet = shutil.which("dotnet") - if system_dotnet: - # Check if it's .NET 9 try: - result = subprocess.run([system_dotnet, "--list-runtimes"], capture_output=True, text=True, check=True) - if "Microsoft.NETCore.App 9." in result.stdout: - log.info("Found system .NET 9 runtime") - return system_dotnet - except subprocess.CalledProcessError: - pass + # First, get the service index from the Azure feed + log.debug("Fetching NuGet service index from Azure feed...") + with urllib.request.urlopen(azure_feed_url) as response: + service_index = json.loads(response.read().decode()) - # Download .NET 9 runtime using config - dotnet_dir = Path(cls.ls_resources_dir(solidlsp_settings)) / "dotnet-runtime-9.0" - assert dotnet_runtime_dep.binary_name is not None, "Runtime dependency must have a binary_name" - dotnet_exe = dotnet_dir / dotnet_runtime_dep.binary_name + # Find the package base address (for downloading packages) + package_base_address = None + for resource in service_index.get("resources", []): + if resource.get("@type") == "PackageBaseAddress/3.0.0": + package_base_address = resource.get("@id") + break - if dotnet_exe.exists(): - log.info(f"Using cached .NET runtime from {dotnet_exe}") - return str(dotnet_exe) + if not package_base_address: + raise SolidLSPException("Could not find package base address in Azure NuGet feed") - # Download .NET runtime - log.info("Downloading .NET 9 runtime...") - dotnet_dir.mkdir(parents=True, exist_ok=True) + # Construct the download URL for the specific package + package_id_lower = package_name.lower() + package_version_lower = package_version.lower() + package_url = f"{package_base_address.rstrip('/')}/{package_id_lower}/{package_version_lower}/{package_id_lower}.{package_version_lower}.nupkg" - custom_settings = solidlsp_settings.get_ls_specific_settings(cls.get_language_enum_instance()) - custom_dotnet_runtime_url = custom_settings.get("dotnet_runtime_url") - if custom_dotnet_runtime_url is not None: - log.info(f"Using custom .NET runtime url: {custom_dotnet_runtime_url}") - url = custom_dotnet_runtime_url - else: - url = dotnet_runtime_dep.url + log.debug(f"Downloading package from: {package_url}") - archive_type = dotnet_runtime_dep.archive_type + # Download the .nupkg file + nupkg_file = temp_dir / f"{package_name}.{package_version}.nupkg" + urllib.request.urlretrieve(package_url, nupkg_file) - # Download the runtime - download_path = dotnet_dir / f"dotnet-runtime.{archive_type}" - try: - log.debug(f"Downloading from {url}") - urllib.request.urlretrieve(url, download_path) + # Extract the .nupkg file (it's just a zip file) + package_extract_dir = temp_dir / f"{package_name}.{package_version}" + package_extract_dir.mkdir(exist_ok=True) - # Extract the archive - if archive_type == "zip": - with zipfile.ZipFile(download_path, "r") as zip_ref: - zip_ref.extractall(dotnet_dir) + # Use SafeZipExtractor to handle long paths and skip errors + extractor = SafeZipExtractor(archive_path=nupkg_file, extract_dir=package_extract_dir, verbose=False) + extractor.extract_all() + + # Clean up the nupkg file + nupkg_file.unlink() + + log.info(f"Successfully downloaded and extracted {package_name} version {package_version}") + return package_extract_dir + + except Exception as e: + raise SolidLSPException( + f"Failed to download package {package_name} version {package_version} from Azure NuGet feed: {e}" + ) from e + + def _ensure_dotnet_runtime_from_config(self, dotnet_runtime_dep: RuntimeDependency) -> str: + """ + Ensure .NET 9 runtime is available using runtime dependency configuration. + Returns the path to the dotnet executable. + """ + # TODO: use RuntimeDependency util methods instead of custom download logic + + # Check if dotnet is already available on the system + system_dotnet = shutil.which("dotnet") + if system_dotnet: + # Check if it's .NET 9 + try: + result = subprocess.run([system_dotnet, "--list-runtimes"], capture_output=True, text=True, check=True) + if "Microsoft.NETCore.App 9." in result.stdout: + log.info("Found system .NET 9 runtime") + return system_dotnet + except subprocess.CalledProcessError: + pass + + # Download .NET 9 runtime using config + dotnet_dir = Path(self._ls_resources_dir) / "dotnet-runtime-9.0" + assert dotnet_runtime_dep.binary_name is not None, "Runtime dependency must have a binary_name" + dotnet_exe = dotnet_dir / dotnet_runtime_dep.binary_name + + if dotnet_exe.exists(): + log.info(f"Using cached .NET runtime from {dotnet_exe}") + return str(dotnet_exe) + + # Download .NET runtime + log.info("Downloading .NET 9 runtime...") + dotnet_dir.mkdir(parents=True, exist_ok=True) + + custom_dotnet_runtime_url = self._custom_settings.get("dotnet_runtime_url") + if custom_dotnet_runtime_url is not None: + log.info(f"Using custom .NET runtime url: {custom_dotnet_runtime_url}") + url = custom_dotnet_runtime_url else: - # tar.gz - with tarfile.open(download_path, "r:gz") as tar_ref: - tar_ref.extractall(dotnet_dir) + url = dotnet_runtime_dep.url - # Remove the archive - download_path.unlink() + archive_type = dotnet_runtime_dep.archive_type - # Make dotnet executable on Unix - if platform.system().lower() != "windows": - dotnet_exe.chmod(0o755) + # Download the runtime + download_path = dotnet_dir / f"dotnet-runtime.{archive_type}" + try: + log.debug(f"Downloading from {url}") + urllib.request.urlretrieve(url, download_path) - log.info(f"Successfully installed .NET 9 runtime to {dotnet_exe}") - return str(dotnet_exe) + # Extract the archive + if archive_type == "zip": + with zipfile.ZipFile(download_path, "r") as zip_ref: + zip_ref.extractall(dotnet_dir) + else: + # tar.gz + with tarfile.open(download_path, "r:gz") as tar_ref: + tar_ref.extractall(dotnet_dir) - except Exception as e: - raise SolidLSPException(f"Failed to download .NET 9 runtime from {url}: {e}") from e + # Remove the archive + download_path.unlink() + + # Make dotnet executable on Unix + if platform.system().lower() != "windows": + dotnet_exe.chmod(0o755) + + log.info(f"Successfully installed .NET 9 runtime to {dotnet_exe}") + return str(dotnet_exe) + + except Exception as e: + raise SolidLSPException(f"Failed to download .NET 9 runtime from {url}: {e}") from e def _get_initialize_params(self) -> InitializeParams: """ diff --git a/test/solidlsp/csharp/test_csharp_basic.py b/test/solidlsp/csharp/test_csharp_basic.py index 5bd7beb1..38f52058 100644 --- a/test/solidlsp/csharp/test_csharp_basic.py +++ b/test/solidlsp/csharp/test_csharp_basic.py @@ -237,7 +237,7 @@ class TestCSharpSolutionProjectOpening: # Should still prefer .sln file even though it's deeper assert result == str(solution_file) - @patch("solidlsp.language_servers.csharp_language_server.CSharpLanguageServer._ensure_server_installed") + @patch("solidlsp.language_servers.csharp_language_server.CSharpLanguageServer.DependencyProvider._ensure_server_installed") @patch("solidlsp.language_servers.csharp_language_server.CSharpLanguageServer._start_server") def test_csharp_language_server_logs_solution_discovery(self, mock_start_server, mock_ensure_server_installed): """Test that CSharpLanguageServer logs solution/project discovery during initialization.""" @@ -266,7 +266,7 @@ class TestCSharpSolutionProjectOpening: expected_log_msg = f"Found solution/project file: {solution_file}" assert expected_log_msg in mem_log.get_log() - @patch("solidlsp.language_servers.csharp_language_server.CSharpLanguageServer._ensure_server_installed") + @patch("solidlsp.language_servers.csharp_language_server.CSharpLanguageServer.DependencyProvider._ensure_server_installed") @patch("solidlsp.language_servers.csharp_language_server.CSharpLanguageServer._start_server") def test_csharp_language_server_logs_no_solution_warning(self, mock_start_server, mock_ensure_server_installed): """Test that CSharpLanguageServer logs warning when no solution/project files are found.""" From f45db064d424a469131f2ab89019f30c416aac58 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Wed, 14 Jan 2026 02:42:38 +0100 Subject: [PATCH 09/14] Intelephense: Refactored to use DependencyProvider --- src/solidlsp/language_servers/intelephense.py | 98 +++++++++---------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/src/solidlsp/language_servers/intelephense.py b/src/solidlsp/language_servers/intelephense.py index 64f80e03..d984bf16 100644 --- a/src/solidlsp/language_servers/intelephense.py +++ b/src/solidlsp/language_servers/intelephense.py @@ -10,11 +10,10 @@ from time import sleep from overrides import override -from solidlsp.ls import SolidLanguageServer +from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_utils import PlatformId, PlatformUtils from solidlsp.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, InitializeParams, LocationLink -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings from ..lsp_protocol_handler import lsp_types @@ -37,70 +36,71 @@ class Intelephense(SolidLanguageServer): def is_ignored_dirname(self, dirname: str) -> bool: return super().is_ignored_dirname(dirname) or dirname in self._ignored_dirnames - @classmethod - def _setup_runtime_dependencies(cls, solidlsp_settings: SolidLSPSettings) -> list[str]: - """ - Setup runtime dependencies for Intelephense and return the command to start the server. - """ - platform_id = PlatformUtils.get_platform_id() + class DependencyProvider(LanguageServerDependencyProviderSinglePath): + def _get_or_install_core_dependency(self) -> str: + """ + Setup runtime dependencies for Intelephense and return the path to the executable. + """ + platform_id = PlatformUtils.get_platform_id() - valid_platforms = [ - PlatformId.LINUX_x64, - PlatformId.LINUX_arm64, - PlatformId.OSX, - PlatformId.OSX_x64, - PlatformId.OSX_arm64, - PlatformId.WIN_x64, - PlatformId.WIN_arm64, - ] - assert platform_id in valid_platforms, f"Platform {platform_id} is not supported by {cls.__name__} at the moment" + valid_platforms = [ + PlatformId.LINUX_x64, + PlatformId.LINUX_arm64, + PlatformId.OSX, + PlatformId.OSX_x64, + PlatformId.OSX_arm64, + PlatformId.WIN_x64, + PlatformId.WIN_arm64, + ] + assert platform_id in valid_platforms, f"Platform {platform_id} is not supported by Intelephense at the moment" - # Verify both node and npm are installed - is_node_installed = shutil.which("node") is not None - assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again." - is_npm_installed = shutil.which("npm") is not None - assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again." + # Verify both node and npm are installed + is_node_installed = shutil.which("node") is not None + assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again." + is_npm_installed = shutil.which("npm") is not None + assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again." - # Install intelephense if not already installed - intelephense_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "php-lsp") - os.makedirs(intelephense_ls_dir, exist_ok=True) - intelephense_executable_path = os.path.join(intelephense_ls_dir, "node_modules", ".bin", "intelephense") - if not os.path.exists(intelephense_executable_path): - deps = RuntimeDependencyCollection( - [ - RuntimeDependency( - id="intelephense", - command="npm install --prefix ./ intelephense@1.14.4", - platform_id="any", - ) - ] - ) - deps.install(intelephense_ls_dir) + # Install intelephense if not already installed + intelephense_ls_dir = os.path.join(self._ls_resources_dir, "php-lsp") + os.makedirs(intelephense_ls_dir, exist_ok=True) + intelephense_executable_path = os.path.join(intelephense_ls_dir, "node_modules", ".bin", "intelephense") + if not os.path.exists(intelephense_executable_path): + deps = RuntimeDependencyCollection( + [ + RuntimeDependency( + id="intelephense", + command="npm install --prefix ./ intelephense@1.14.4", + platform_id="any", + ) + ] + ) + deps.install(intelephense_ls_dir) - assert os.path.exists( - intelephense_executable_path - ), f"intelephense executable not found at {intelephense_executable_path}, something went wrong." + assert os.path.exists( + intelephense_executable_path + ), f"intelephense executable not found at {intelephense_executable_path}, something went wrong." - return [intelephense_executable_path, "--stdio"] + return intelephense_executable_path + + def _create_launch_command(self, core_path: str) -> list[str]: + return [core_path, "--stdio"] def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): - # Setup runtime dependencies before initializing - intelephense_cmd = self._setup_runtime_dependencies(solidlsp_settings) - - super().__init__( - config, repository_root_path, ProcessLaunchInfo(cmd=intelephense_cmd, cwd=repository_root_path), "php", solidlsp_settings - ) + super().__init__(config, repository_root_path, None, "php", solidlsp_settings) self.request_id = 0 # For PHP projects, we should ignore: # - node_modules: if the project has JavaScript components # - cache: commonly used for caching - # - (configurable) vendor: third-party dependencies LanguageServerDependencyProvider: + return self.DependencyProvider(self._custom_settings, self._ls_resources_dir) + def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: """ Returns the initialization params for the Intelephense Language Server. From ac1531b305ffab66e47db58df9742106878e9f78 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Wed, 14 Jan 2026 02:47:23 +0100 Subject: [PATCH 10/14] ClojureLSP: Refactored to use DependencyProvider --- src/solidlsp/language_servers/clojure_lsp.py | 45 +++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/src/solidlsp/language_servers/clojure_lsp.py b/src/solidlsp/language_servers/clojure_lsp.py index f32a4d5a..e8d14b96 100644 --- a/src/solidlsp/language_servers/clojure_lsp.py +++ b/src/solidlsp/language_servers/clojure_lsp.py @@ -10,10 +10,9 @@ import subprocess import threading from typing import cast -from solidlsp.ls import SolidLanguageServer +from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings from .common import RuntimeDependency, RuntimeDependencyCollection @@ -91,11 +90,10 @@ class ClojureLSP(SolidLanguageServer): """ Creates a ClojureLSP instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - clojure_lsp_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings) super().__init__( config, repository_root_path, - ProcessLaunchInfo(cmd=clojure_lsp_executable_path, cwd=repository_root_path), + None, "clojure", solidlsp_settings, ) @@ -104,24 +102,29 @@ class ClojureLSP(SolidLanguageServer): self.resolve_main_method_available = threading.Event() self.service_ready_event = threading.Event() - @classmethod - def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> str: - """Setup runtime dependencies for clojure-lsp and return the command to start the server.""" - verify_clojure_cli() - deps = ClojureLSP.runtime_dependencies - dependency = deps.get_single_dep_for_current_platform() + def _create_dependency_provider(self) -> LanguageServerDependencyProvider: + return self.DependencyProvider(self._custom_settings, self._ls_resources_dir) - clojurelsp_ls_dir = cls.ls_resources_dir(solidlsp_settings) - clojurelsp_executable_path = deps.binary_path(clojurelsp_ls_dir) - if not os.path.exists(clojurelsp_executable_path): - log.info( - f"Downloading and extracting clojure-lsp from {dependency.url} to {clojurelsp_ls_dir}", - ) - deps.install(clojurelsp_ls_dir) - if not os.path.exists(clojurelsp_executable_path): - raise FileNotFoundError(f"Download failed? Could not find clojure-lsp executable at {clojurelsp_executable_path}") - os.chmod(clojurelsp_executable_path, 0o755) - return clojurelsp_executable_path + class DependencyProvider(LanguageServerDependencyProviderSinglePath): + def _get_or_install_core_dependency(self) -> str: + """Setup runtime dependencies for clojure-lsp and return the path to the executable.""" + verify_clojure_cli() + deps = ClojureLSP.runtime_dependencies + dependency = deps.get_single_dep_for_current_platform() + + clojurelsp_executable_path = deps.binary_path(self._ls_resources_dir) + if not os.path.exists(clojurelsp_executable_path): + log.info( + f"Downloading and extracting clojure-lsp from {dependency.url} to {self._ls_resources_dir}", + ) + deps.install(self._ls_resources_dir) + if not os.path.exists(clojurelsp_executable_path): + raise FileNotFoundError(f"Download failed? Could not find clojure-lsp executable at {clojurelsp_executable_path}") + os.chmod(clojurelsp_executable_path, 0o755) + return clojurelsp_executable_path + + def _create_launch_command(self, core_path: str) -> list[str] | str: + return [core_path] @staticmethod def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: From 2217f516af7f135027046b8513a12ec2bbf5a5e4 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Wed, 14 Jan 2026 02:50:43 +0100 Subject: [PATCH 11/14] ClangdLanguageServer: Refactored to use DependencyProvider --- .../clangd_language_server.py | 160 +++++++++--------- 1 file changed, 81 insertions(+), 79 deletions(-) diff --git a/src/solidlsp/language_servers/clangd_language_server.py b/src/solidlsp/language_servers/clangd_language_server.py index eccd6017..192ef199 100644 --- a/src/solidlsp/language_servers/clangd_language_server.py +++ b/src/solidlsp/language_servers/clangd_language_server.py @@ -8,10 +8,9 @@ import pathlib import threading from typing import Any, cast -from solidlsp.ls import SolidLanguageServer +from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings from .common import RuntimeDependency, RuntimeDependencyCollection @@ -30,92 +29,95 @@ class ClangdLanguageServer(SolidLanguageServer): """ Creates a ClangdLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - clangd_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings) - super().__init__( - config, repository_root_path, ProcessLaunchInfo(cmd=clangd_executable_path, cwd=repository_root_path), "cpp", solidlsp_settings - ) + super().__init__(config, repository_root_path, None, "cpp", solidlsp_settings) self.server_ready = threading.Event() self.service_ready_event = threading.Event() self.initialize_searcher_command_available = threading.Event() self.resolve_main_method_available = threading.Event() - @classmethod - def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> str: - """ - Setup runtime dependencies for ClangdLanguageServer and return the command to start the server. - """ - import shutil + def _create_dependency_provider(self) -> LanguageServerDependencyProvider: + return self.DependencyProvider(self._custom_settings, self._ls_resources_dir) - deps = RuntimeDependencyCollection( - [ - RuntimeDependency( - id="Clangd", - description="Clangd for Linux (x64)", - url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-linux-19.1.2.zip", - platform_id="linux-x64", - archive_type="zip", - binary_name="clangd_19.1.2/bin/clangd", - ), - RuntimeDependency( - id="Clangd", - description="Clangd for Windows (x64)", - url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-windows-19.1.2.zip", - platform_id="win-x64", - archive_type="zip", - binary_name="clangd_19.1.2/bin/clangd.exe", - ), - RuntimeDependency( - id="Clangd", - description="Clangd for macOS (x64)", - url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-mac-19.1.2.zip", - platform_id="osx-x64", - archive_type="zip", - binary_name="clangd_19.1.2/bin/clangd", - ), - RuntimeDependency( - id="Clangd", - description="Clangd for macOS (Arm64)", - url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-mac-19.1.2.zip", - platform_id="osx-arm64", - archive_type="zip", - binary_name="clangd_19.1.2/bin/clangd", - ), - ] - ) + class DependencyProvider(LanguageServerDependencyProviderSinglePath): + def _get_or_install_core_dependency(self) -> str: + """ + Setup runtime dependencies for ClangdLanguageServer and return the path to the executable. + """ + import shutil - clangd_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "clangd") + deps = RuntimeDependencyCollection( + [ + RuntimeDependency( + id="Clangd", + description="Clangd for Linux (x64)", + url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-linux-19.1.2.zip", + platform_id="linux-x64", + archive_type="zip", + binary_name="clangd_19.1.2/bin/clangd", + ), + RuntimeDependency( + id="Clangd", + description="Clangd for Windows (x64)", + url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-windows-19.1.2.zip", + platform_id="win-x64", + archive_type="zip", + binary_name="clangd_19.1.2/bin/clangd.exe", + ), + RuntimeDependency( + id="Clangd", + description="Clangd for macOS (x64)", + url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-mac-19.1.2.zip", + platform_id="osx-x64", + archive_type="zip", + binary_name="clangd_19.1.2/bin/clangd", + ), + RuntimeDependency( + id="Clangd", + description="Clangd for macOS (Arm64)", + url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-mac-19.1.2.zip", + platform_id="osx-arm64", + archive_type="zip", + binary_name="clangd_19.1.2/bin/clangd", + ), + ] + ) - try: - dep = deps.get_single_dep_for_current_platform() - except RuntimeError: - dep = None + clangd_ls_dir = os.path.join(self._ls_resources_dir, "clangd") - if dep is None: - # No prebuilt binary available, look for system-installed clangd - clangd_executable_path = shutil.which("clangd") - if not clangd_executable_path: - raise FileNotFoundError( - "Clangd is not installed on your system.\n" - + "Please install clangd using your system package manager:\n" - + " Ubuntu/Debian: sudo apt-get install clangd\n" - + " Fedora/RHEL: sudo dnf install clang-tools-extra\n" - + " Arch Linux: sudo pacman -S clang\n" - + "See https://clangd.llvm.org/installation for more details." - ) - log.info(f"Using system-installed clangd at {clangd_executable_path}") - else: - # Standard download and install for platforms with prebuilt binaries - clangd_executable_path = deps.binary_path(clangd_ls_dir) - if not os.path.exists(clangd_executable_path): - log.info(f"Clangd executable not found at {clangd_executable_path}. Downloading from {dep.url}") - _ = deps.install(clangd_ls_dir) - if not os.path.exists(clangd_executable_path): - raise FileNotFoundError( - f"Clangd executable not found at {clangd_executable_path}.\n" - + "Make sure you have installed clangd. See https://clangd.llvm.org/installation" - ) - os.chmod(clangd_executable_path, 0o755) - return clangd_executable_path + try: + dep = deps.get_single_dep_for_current_platform() + except RuntimeError: + dep = None + + if dep is None: + # No prebuilt binary available, look for system-installed clangd + clangd_executable_path = shutil.which("clangd") + if not clangd_executable_path: + raise FileNotFoundError( + "Clangd is not installed on your system.\n" + + "Please install clangd using your system package manager:\n" + + " Ubuntu/Debian: sudo apt-get install clangd\n" + + " Fedora/RHEL: sudo dnf install clang-tools-extra\n" + + " Arch Linux: sudo pacman -S clang\n" + + "See https://clangd.llvm.org/installation for more details." + ) + log.info(f"Using system-installed clangd at {clangd_executable_path}") + else: + # Standard download and install for platforms with prebuilt binaries + clangd_executable_path = deps.binary_path(clangd_ls_dir) + if not os.path.exists(clangd_executable_path): + log.info(f"Clangd executable not found at {clangd_executable_path}. Downloading from {dep.url}") + _ = deps.install(clangd_ls_dir) + if not os.path.exists(clangd_executable_path): + raise FileNotFoundError( + f"Clangd executable not found at {clangd_executable_path}.\n" + + "Make sure you have installed clangd. See https://clangd.llvm.org/installation" + ) + os.chmod(clangd_executable_path, 0o755) + return clangd_executable_path + + def _create_launch_command(self, core_path: str) -> list[str] | str: + return [core_path] @staticmethod def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: From ccca8dad2635f9dea9810c39646f94fbaba8301a Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Wed, 14 Jan 2026 23:18:16 +0100 Subject: [PATCH 12/14] Update language addition guide to use DependencyProvider --- .../adding_new_language_support_guide.md | 87 ++++++++----------- 1 file changed, 34 insertions(+), 53 deletions(-) diff --git a/.serena/memories/adding_new_language_support_guide.md b/.serena/memories/adding_new_language_support_guide.md index 18d12436..8ffa4684 100644 --- a/.serena/memories/adding_new_language_support_guide.md +++ b/.serena/memories/adding_new_language_support_guide.md @@ -10,72 +10,48 @@ Adding a new language involves: 2. **Language Registration** - Adding the language to enums and configurations 3. **Test Repository** - Creating a minimal test project 4. **Test Suite** - Writing comprehensive tests -5. **Runtime Dependencies** - Configuring automatic language server downloads ## Step 1: Language Server Implementation ### 1.1 Create Language Server Class Create a new file in `src/solidlsp/language_servers/` (e.g., `new_language_server.py`). -Have a look at `intelephense.py` for a reference implementation of a language server which downloads all its dependencies, at `gopls.py` for an LS that needs some preinstalled -dependencies, and on `pyright_server.py` that does not need any additional dependencies -because the language server can be installed directly as python package. -```python -from solidlsp.ls import SolidLanguageServer -from solidlsp.ls_config import LanguageServerConfig -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +#### Providing the Launch Command via a DependencyProvider +All language servers use the `DependencyProvider` pattern to handle + * runtime dependency installation/discovery + * launch command creation (and, optionally, environment setup) -class NewLanguageServer(SolidLanguageServer): - """ - Language server implementation for NewLanguage. - """ +To implement a new language server using the DependencyProvider pattern: + * Pass `None` for `process_launch_info` in `super().__init__()` - the base class creates it via `_create_dependency_provider()` + * Implement `_create_dependency_provider()` to return an inner `DependencyProvider` class instance. + In simple cases, it can be instantiated with only two parameters: + ```python + def _create_dependency_provider(self) -> LanguageServerDependencyProvider: + return self.DependencyProvider(self._custom_settings, self._ls_resources_dir) + ``` + The resource dir that is passed is the directory in which installed dependencies should be stored! - def __init__(self, config: LanguageServerConfig, repository_root_path: str): - # Determine language server command - cmd = self._get_language_server_command() +**Base Classes:** - super().__init__(config, - ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path), - "new_language",, +- **`LanguageServerDependencyProviderSinglePath`** - For language servers with a single core dependency (e.g., an executable or JAR file) + - Provides automatic support for the `ls_path` custom setting, allowing users to override the core dependency path (if they have it installed it themselves) + - Implement `_get_or_install_core_dependency()` to return the path to the core dependency, downloading/installing it automatically if necessary + - Implement `_create_launch_command(core_path)` to build the full command from the core path + - Reference implementations: `TypeScriptLanguageServer`, `Intelephense`, `ClojureLSP`, `ClangdLanguageServer`, `PyrightServer` - def _get_language_server_command(self) -> list[str]: - """Get the command to start the language server.""" - # Example: return ["new-language-server", "--stdio"] - pass +- **`LanguageServerDependencyProvider`** - The base class, which can be directly inherited from for complex cases with multiple dependencies or custom setup + - Implement `create_launch_command()` directly + - Reference implementations: `EclipseJDTLS`, `CSharpLanguageServer`, `MatlabLanguageServer` - @override - def is_ignored_dirname(self, dirname: str) -> bool: - """Define language-specific directories to ignore.""" - return super().is_ignored_dirname(dirname) or dirname in ["build", - "dist", "target"] -``` +**Implementation Pointers::** + - When returning the command, prefer the list-based representation for robustness + - Override `create_launch_command_env` if the launch command needs environment variables to be set (defaults to `{}` in the base implementation) -### 1.2 Language Server Discovery and Installation +You should look at at least one existing implementation of each base class to understand how they work. -For languages requiring automatic installation, implement download logic similar to C#: - -```python -@classmethod -def _ensure_server_installed(cls) -> str: - """Ensure language server is installed and return path.""" - # Check system installation first - system_server = shutil.which("new-language-server") - if system_server: - return system_server - - # Download and install if needed - server_path = cls._download_and_install_server() - return server_path - -def _download_and_install_server(cls) -> str: - """Download and install the language server.""" - # Implementation specific to your language server - pass -``` - -### 1.3 LSP Initialization +### 1.2 LSP Initialization Override initialization methods if needed: @@ -98,9 +74,14 @@ def _start_server(self): # Start server and initialize self.server.start() init_response = self.server.send.initialize(self._get_initialize_params()) + self.server.notify.initialized({}) ``` +After `_start_server` returns, the language server should be fully operational. +If the server requires that one waits for certain notifications or responses before being ready, implement that logic here. +For an example, see `EclipseJDTLS._start_server`. + ## Step 2: Language Registration ### 2.1 Add to Language Enum @@ -215,6 +196,6 @@ Consider adding new cases to the parametrized tests in `test_serena_agent.py` fo Update: -- **README.md** - Add language to supported languages list +- **README.md** - Add language to the list of languages +- **docs/01-about/020_programming-languages.md** - Add language to the list and mention any special notes, compatibility or requirements (e.g. installations the user is required to do) - **CHANGELOG.md** - Document the new language support -- **Language-specific docs** - Installation requirements, known issues From 95a856c2e5e1d3d89445058b64225153786238fe Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Thu, 15 Jan 2026 18:48:13 +0100 Subject: [PATCH 13/14] LanguageServerDependencyProviderSinglePath: Mention ls_path configurability --- src/solidlsp/ls.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index a4db066c..9dd49138 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -165,6 +165,10 @@ class LanguageServerDependencyProviderSinglePath(LanguageServerDependencyProvide """ Special case of a dependency provider, where there is a single core dependency which provides the basis for the launch command. + + The core dependency's path can be overridden by the user in LS-specific settings (SerenaConfig) + via the key "ls_path". If the user provides the key, the specified path is used directly. + Otherwise, the provider implementation is called to get or install the core dependency. """ @abstractmethod From eefebaf018c5bae8fd3a783a7fda705b4074484e Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Thu, 15 Jan 2026 19:15:27 +0100 Subject: [PATCH 14/14] Add ls_path to documentation and general improvements to ls_specific_settings documentation --- docs/02-usage/050_configuration.md | 31 ++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/docs/02-usage/050_configuration.md b/docs/02-usage/050_configuration.md index 68a99284..42e80a4c 100644 --- a/docs/02-usage/050_configuration.md +++ b/docs/02-usage/050_configuration.md @@ -134,9 +134,10 @@ For advanced users, Serena's configuration can be further customized. The Serena user data directory (where configuration, language server files, logs, etc. are stored) defaults to `~/.serena`. You can change this location by setting the `SERENA_HOME` environment variable to your desired path. -### Language server specific settings (`ls_specific_settings`) +### Language Server-Specific settings (`ls_specific_settings`) -`ls_specific_settings` lets you pass per-language, language-server-specific configuration. +Under the key `ls_specific_settings` in `serena_config.yml`, you can you pass per-language, +language server-specific configuration. Structure: @@ -146,10 +147,28 @@ ls_specific_settings: # language-server-specific keys ``` -Where to put it: -- Global default: `~/.serena/serena_config.yml` -- Per-project override: `/.serena/project.yml` (takes precedence over the global config for the same keys) -- There is currently no `serena-mcp-server` CLI option to set `ls_specific_settings`; use the config files above. +:::{attention} +Most settings are currently undocumented. Please refer to the +[source code of the respective language server](https://github.com/oraios/serena/tree/main/src/solidlsp/language_servers) +implementation to determine supported settings. +::: + +#### Overriding the Language Server Path + +Some language servers, particularly those that use a single core path for the language server (e.g. the main executable), +support overriding that path via the `ls_path` setting. +Therefore, if you have installed the language server yourself and want to use your installation +instead of Serena's managed installation, you can set the `ls_path` setting as follows: + +```yaml +ls_specific_settings: + : + ls_path: "/path/to/language-server" +``` + +This is supported by all language servers deriving their dependency provider from `LanguageServerDependencyProviderSinglePath`. +Currently, this includes the following languages: `clojure`, `cpp`, `php`, `python`, `typescript`. +We will add support for more languages over time. #### Go (`gopls`)