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
This commit is contained in:
Dominik Jain authored and Dominik Jain committed 2026-01-14 02:29:15 +01:00
1 parent b7142cbfd4
commit 9560ee651d
3 files changed
+350 -257

No files matched your search

@@ -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:
@@ -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:
"""
+90 -4
View File
@@ -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.