mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-14 02:16:21 +00:00
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:
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
@@ -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.
|
||||
|
||||
|
||||
Reference in new issue
Block a user