From 58f2727800d95e9999f11afa4cc6ceacccc8d385 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 13 Feb 2026 02:44:15 +0100 Subject: [PATCH 1/8] BashLanguageServer: Fix run command failing in case of spaces in path --- src/solidlsp/language_servers/bash_language_server.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/solidlsp/language_servers/bash_language_server.py b/src/solidlsp/language_servers/bash_language_server.py index 7b231830..d9a1429f 100644 --- a/src/solidlsp/language_servers/bash_language_server.py +++ b/src/solidlsp/language_servers/bash_language_server.py @@ -30,11 +30,11 @@ class BashLanguageServer(SolidLanguageServer): Creates a BashLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - bash_lsp_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings) + cmd = self._setup_runtime_dependencies(config, solidlsp_settings) super().__init__( config, repository_root_path, - ProcessLaunchInfo(cmd=bash_lsp_executable_path, cwd=repository_root_path), + ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path), "bash", solidlsp_settings, ) @@ -42,7 +42,7 @@ class BashLanguageServer(SolidLanguageServer): self.initialize_searcher_command_available = threading.Event() @classmethod - def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> str: + def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> list[str]: """ Setup runtime dependencies for Bash Language Server and return the command to start the server. """ @@ -80,7 +80,7 @@ class BashLanguageServer(SolidLanguageServer): raise FileNotFoundError( f"bash-language-server executable not found at {bash_executable_path}, something went wrong with the installation." ) - return f"{bash_executable_path} start" + return [bash_executable_path, "start"] @staticmethod def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: From 4809d5882b6cd6bf3b10123e18a8b23255f03338 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 13 Feb 2026 02:55:50 +0100 Subject: [PATCH 2/8] TaploLanguageServer: Use DependencyProvider, fixing run command not working with spaces in path --- src/solidlsp/language_servers/taplo_server.py | 188 +++++++++--------- 1 file changed, 96 insertions(+), 92 deletions(-) diff --git a/src/solidlsp/language_servers/taplo_server.py b/src/solidlsp/language_servers/taplo_server.py index f44e2d8f..ea6de30e 100644 --- a/src/solidlsp/language_servers/taplo_server.py +++ b/src/solidlsp/language_servers/taplo_server.py @@ -17,11 +17,10 @@ from typing import Any # Download timeout in seconds (prevents indefinite hangs) DOWNLOAD_TIMEOUT_SECONDS = 120 -from solidlsp.ls import SolidLanguageServer +from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_utils import PathUtils 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__) @@ -119,115 +118,120 @@ class TaploServer(SolidLanguageServer): Creates a TaploServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - taplo_executable_path = self._setup_runtime_dependencies(solidlsp_settings) super().__init__( config, repository_root_path, - ProcessLaunchInfo(cmd=f"{taplo_executable_path} lsp stdio", cwd=repository_root_path), + None, "toml", solidlsp_settings, ) - @classmethod - def _setup_runtime_dependencies(cls, solidlsp_settings: SolidLSPSettings) -> str: - """ - Setup runtime dependencies for Taplo and return the command to start the server. - """ - # First check if taplo is already installed system-wide - system_taplo = shutil.which("taplo") - if system_taplo: - log.info(f"Using system-installed Taplo at: {system_taplo}") - return system_taplo + def _create_dependency_provider(self) -> LanguageServerDependencyProvider: + return self.DependencyProvider(self._custom_settings, self._ls_resources_dir) - # Setup local installation directory - taplo_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "taplo") - os.makedirs(taplo_dir, exist_ok=True) + class DependencyProvider(LanguageServerDependencyProviderSinglePath): + def _get_or_install_core_dependency(self) -> str: + """ + Setup runtime dependencies for Taplo and return the command to start the server. + """ + # First check if taplo is already installed system-wide + system_taplo = shutil.which("taplo") + if system_taplo: + log.info(f"Using system-installed Taplo at: {system_taplo}") + return system_taplo - _, executable_name = _get_taplo_download_url() - taplo_executable = os.path.join(taplo_dir, executable_name) + # Setup local installation directory + taplo_dir = os.path.join(self._ls_resources_dir, "taplo") + os.makedirs(taplo_dir, exist_ok=True) - if os.path.exists(taplo_executable) and os.access(taplo_executable, os.X_OK): - log.info(f"Using cached Taplo at: {taplo_executable}") - return taplo_executable + _, executable_name = _get_taplo_download_url() + taplo_executable = os.path.join(taplo_dir, executable_name) - # Download and install Taplo - log.info(f"Taplo not found. Downloading version {TAPLO_VERSION}...") - cls._download_taplo(taplo_dir, taplo_executable) + if os.path.exists(taplo_executable) and os.access(taplo_executable, os.X_OK): + log.info(f"Using cached Taplo at: {taplo_executable}") + return taplo_executable - if not os.path.exists(taplo_executable): - raise FileNotFoundError( - f"Taplo executable not found at {taplo_executable}. " - "Installation may have failed. Try installing manually: cargo install taplo-cli --locked" - ) + # Download and install Taplo + log.info(f"Taplo not found. Downloading version {TAPLO_VERSION}...") + self._download_taplo(taplo_dir, taplo_executable) - return taplo_executable - - @classmethod - def _download_taplo(cls, install_dir: str, executable_path: str) -> None: - """Download and extract Taplo binary with SHA256 verification.""" - # TODO: consider using existing download utilities in SolidLSP instead of the custom logic here - download_url, _ = _get_taplo_download_url() - archive_filename = os.path.basename(download_url) - - try: - log.info(f"Downloading Taplo from: {download_url}") - archive_path = os.path.join(install_dir, archive_filename) - - # Download the archive with timeout to prevent indefinite hangs - old_timeout = socket.getdefaulttimeout() - try: - socket.setdefaulttimeout(DOWNLOAD_TIMEOUT_SECONDS) - urllib.request.urlretrieve(download_url, archive_path) - finally: - socket.setdefaulttimeout(old_timeout) - - # Verify SHA256 checksum - expected_hash = TAPLO_SHA256_CHECKSUMS.get(archive_filename) - if expected_hash: - if not _verify_sha256(archive_path, expected_hash): - os.remove(archive_path) - raise RuntimeError( - f"SHA256 checksum verification failed for {archive_filename}. " - "The downloaded file may be corrupted or tampered with. " - "Try installing manually: cargo install taplo-cli --locked" - ) - log.info(f"SHA256 checksum verified for {archive_filename}") - else: - log.warning( - f"No SHA256 checksum available for {archive_filename}. " - "Skipping verification - consider installing manually: cargo install taplo-cli --locked" + if not os.path.exists(taplo_executable): + raise FileNotFoundError( + f"Taplo executable not found at {taplo_executable}. " + "Installation may have failed. Try installing manually: cargo install taplo-cli --locked" ) - # Extract based on format - if archive_path.endswith(".gz") and not archive_path.endswith(".tar.gz"): - # Single file gzip - with gzip.open(archive_path, "rb") as f_in: - with open(executable_path, "wb") as f_out: - f_out.write(f_in.read()) - elif archive_path.endswith(".zip"): - import zipfile + return taplo_executable - with zipfile.ZipFile(archive_path, "r") as zip_ref: - # Security: Validate paths to prevent zip slip vulnerability - for member in zip_ref.namelist(): - member_path = os.path.normpath(os.path.join(install_dir, member)) - if not member_path.startswith(os.path.normpath(install_dir)): - raise RuntimeError(f"Zip slip detected: {member} attempts to escape install directory") - zip_ref.extractall(install_dir) + def _create_launch_command(self, core_path: str) -> list[str] | str: + return [core_path, "lsp", "stdio"] - # Make executable on Unix systems - if os.name != "nt": - os.chmod(executable_path, os.stat(executable_path).st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + @classmethod + def _download_taplo(cls, install_dir: str, executable_path: str) -> None: + """Download and extract Taplo binary with SHA256 verification.""" + # TODO: consider using existing download utilities in SolidLSP instead of the custom logic here + download_url, _ = _get_taplo_download_url() + archive_filename = os.path.basename(download_url) - # Clean up archive - os.remove(archive_path) - log.info(f"Taplo installed successfully at: {executable_path}") + try: + log.info(f"Downloading Taplo from: {download_url}") + archive_path = os.path.join(install_dir, archive_filename) - except Exception as e: - log.error(f"Failed to download Taplo: {e}") - raise RuntimeError( - f"Failed to download Taplo from {download_url}. Try installing manually: cargo install taplo-cli --locked" - ) from e + # Download the archive with timeout to prevent indefinite hangs + old_timeout = socket.getdefaulttimeout() + try: + socket.setdefaulttimeout(DOWNLOAD_TIMEOUT_SECONDS) + urllib.request.urlretrieve(download_url, archive_path) + finally: + socket.setdefaulttimeout(old_timeout) + + # Verify SHA256 checksum + expected_hash = TAPLO_SHA256_CHECKSUMS.get(archive_filename) + if expected_hash: + if not _verify_sha256(archive_path, expected_hash): + os.remove(archive_path) + raise RuntimeError( + f"SHA256 checksum verification failed for {archive_filename}. " + "The downloaded file may be corrupted or tampered with. " + "Try installing manually: cargo install taplo-cli --locked" + ) + log.info(f"SHA256 checksum verified for {archive_filename}") + else: + log.warning( + f"No SHA256 checksum available for {archive_filename}. " + "Skipping verification - consider installing manually: cargo install taplo-cli --locked" + ) + + # Extract based on format + if archive_path.endswith(".gz") and not archive_path.endswith(".tar.gz"): + # Single file gzip + with gzip.open(archive_path, "rb") as f_in: + with open(executable_path, "wb") as f_out: + f_out.write(f_in.read()) + elif archive_path.endswith(".zip"): + import zipfile + + with zipfile.ZipFile(archive_path, "r") as zip_ref: + # Security: Validate paths to prevent zip slip vulnerability + for member in zip_ref.namelist(): + member_path = os.path.normpath(os.path.join(install_dir, member)) + if not member_path.startswith(os.path.normpath(install_dir)): + raise RuntimeError(f"Zip slip detected: {member} attempts to escape install directory") + zip_ref.extractall(install_dir) + + # Make executable on Unix systems + if os.name != "nt": + os.chmod(executable_path, os.stat(executable_path).st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + + # Clean up archive + os.remove(archive_path) + log.info(f"Taplo installed successfully at: {executable_path}") + + except Exception as e: + log.error(f"Failed to download Taplo: {e}") + raise RuntimeError( + f"Failed to download Taplo from {download_url}. Try installing manually: cargo install taplo-cli --locked" + ) from e @staticmethod def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: From c195b2961a94c75d13e3d559a7de52f06e63236e Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 13 Feb 2026 01:37:09 +0100 Subject: [PATCH 3/8] Handle end_col when extracting symbol body Fixes #1029 --- src/solidlsp/ls.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 6286776f..5a0bc625 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -18,6 +18,7 @@ from typing import Self, Union, cast import pathspec from sensai.util.pickle import getstate, load_pickle +from sensai.util.string import ToStringMixin from serena.text_utils import MatchedConsecutiveLines from serena.util.file_system import match_path @@ -130,7 +131,7 @@ class LSPFileBuffer: return self.contents.split("\n") -class SymbolBody: +class SymbolBody(ToStringMixin): """ Representation of the body of a symbol, which allows the extraction of the symbol's text from the lines of the file it is defined in. @@ -147,14 +148,21 @@ class SymbolBody: self._end_line = end_line self._end_col = end_col + def _tostring_excludes(self) -> list[str]: + return ["_lines"] + def get_text(self) -> str: # extract relevant lines symbol_body = "\n".join(self._lines[self._start_line : self._end_line + 1]) - # remove leading indentation + # remove leading content from the first line symbol_body = symbol_body[self._start_col :] - # TODO: handle end_col properly (this was never implemented) + # remove trailing content from the last line + last_line = self._lines[self._end_line] + trailing_length = len(last_line) - self._end_col + if trailing_length > 0: + symbol_body = symbol_body[: -(len(last_line) - self._end_col)] return symbol_body From 754806af3e1a6d62e8b864fe85acff87c61d3aed Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 13 Feb 2026 02:59:35 +0100 Subject: [PATCH 4/8] BashLanguageServer: Use DependencyProvider --- .../language_servers/bash_language_server.py | 93 +++++++++++-------- 1 file changed, 52 insertions(+), 41 deletions(-) diff --git a/src/solidlsp/language_servers/bash_language_server.py b/src/solidlsp/language_servers/bash_language_server.py index d9a1429f..fddce311 100644 --- a/src/solidlsp/language_servers/bash_language_server.py +++ b/src/solidlsp/language_servers/bash_language_server.py @@ -10,10 +10,15 @@ import shutil import threading from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection -from solidlsp.ls import DocumentSymbols, LSPFileBuffer, SolidLanguageServer +from solidlsp.ls import ( + DocumentSymbols, + LanguageServerDependencyProvider, + LanguageServerDependencyProviderSinglePath, + LSPFileBuffer, + 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__) @@ -30,57 +35,63 @@ class BashLanguageServer(SolidLanguageServer): Creates a BashLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - cmd = self._setup_runtime_dependencies(config, solidlsp_settings) super().__init__( config, repository_root_path, - ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path), + None, "bash", solidlsp_settings, ) self.server_ready = threading.Event() self.initialize_searcher_command_available = threading.Event() - @classmethod - def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> list[str]: - """ - Setup runtime dependencies for Bash Language Server and return the command to start the server. - """ - # 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." + def _create_dependency_provider(self) -> LanguageServerDependencyProvider: + return self.DependencyProvider(self._custom_settings, self._ls_resources_dir) - deps = RuntimeDependencyCollection( - [ - RuntimeDependency( - id="bash-language-server", - description="bash-language-server package", - command="npm install --prefix ./ bash-language-server@5.6.0", - platform_id="any", - ), - ] - ) + class DependencyProvider(LanguageServerDependencyProviderSinglePath): + def _get_or_install_core_dependency(self) -> str: + """ + Setup runtime dependencies for Bash Language Server and return the command to start the server. + """ + # 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 bash-language-server if not already installed - bash_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "bash-lsp") - bash_executable_path = os.path.join(bash_ls_dir, "node_modules", ".bin", "bash-language-server") - - # Handle Windows executable extension - if os.name == "nt": - bash_executable_path += ".cmd" - - if not os.path.exists(bash_executable_path): - log.info(f"Bash Language Server executable not found at {bash_executable_path}. Installing...") - deps.install(bash_ls_dir) - log.info("Bash language server dependencies installed successfully") - - if not os.path.exists(bash_executable_path): - raise FileNotFoundError( - f"bash-language-server executable not found at {bash_executable_path}, something went wrong with the installation." + deps = RuntimeDependencyCollection( + [ + RuntimeDependency( + id="bash-language-server", + description="bash-language-server package", + command="npm install --prefix ./ bash-language-server@5.6.0", + platform_id="any", + ), + ] ) - return [bash_executable_path, "start"] + + # Install bash-language-server if not already installed + bash_ls_dir = os.path.join(self._ls_resources_dir, "bash-lsp") + bash_executable_path = os.path.join(bash_ls_dir, "node_modules", ".bin", "bash-language-server") + + # Handle Windows executable extension + if os.name == "nt": + bash_executable_path += ".cmd" + + if not os.path.exists(bash_executable_path): + log.info(f"Bash Language Server executable not found at {bash_executable_path}. Installing...") + deps.install(bash_ls_dir) + log.info("Bash language server dependencies installed successfully") + + if not os.path.exists(bash_executable_path): + raise FileNotFoundError( + f"bash-language-server executable not found at {bash_executable_path}, something went wrong with the installation." + ) + + return bash_executable_path + + def _create_launch_command(self, core_path: str) -> list[str] | str: + return [core_path, "start"] @staticmethod def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: From 1ab7e0aa97ff5d97acb19c3dbdc9723531ab6f44 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 13 Feb 2026 03:02:59 +0100 Subject: [PATCH 5/8] YamlLanguageServer: Use DependencyProvider, fixing run command not working with spaces in path --- .../language_servers/yaml_language_server.py | 87 ++++++++++--------- 1 file changed, 46 insertions(+), 41 deletions(-) diff --git a/src/solidlsp/language_servers/yaml_language_server.py b/src/solidlsp/language_servers/yaml_language_server.py index bb813a2a..d78b80e2 100644 --- a/src/solidlsp/language_servers/yaml_language_server.py +++ b/src/solidlsp/language_servers/yaml_language_server.py @@ -10,10 +10,9 @@ import shutil from typing import Any from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection -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__) @@ -46,55 +45,61 @@ class YamlLanguageServer(SolidLanguageServer): Creates a YamlLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - yaml_lsp_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings) super().__init__( config, repository_root_path, - ProcessLaunchInfo(cmd=yaml_lsp_executable_path, cwd=repository_root_path), + None, "yaml", solidlsp_settings, ) - @classmethod - def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> str: - """ - Setup runtime dependencies for YAML Language Server and return the command to start the server. - """ - # 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." + def _create_dependency_provider(self) -> LanguageServerDependencyProvider: + return self.DependencyProvider(self._custom_settings, self._ls_resources_dir) - deps = RuntimeDependencyCollection( - [ - RuntimeDependency( - id="yaml-language-server", - description="yaml-language-server package (Red Hat)", - command="npm install --prefix ./ yaml-language-server@1.19.2", - platform_id="any", - ), - ] - ) + class DependencyProvider(LanguageServerDependencyProviderSinglePath): + def _get_or_install_core_dependency(self) -> str: + """ + Setup runtime dependencies for YAML Language Server and return the command to start the server. + """ + # 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 yaml-language-server if not already installed - yaml_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "yaml-lsp") - yaml_executable_path = os.path.join(yaml_ls_dir, "node_modules", ".bin", "yaml-language-server") - - # Handle Windows executable extension - if os.name == "nt": - yaml_executable_path += ".cmd" - - if not os.path.exists(yaml_executable_path): - log.info(f"YAML Language Server executable not found at {yaml_executable_path}. Installing...") - deps.install(yaml_ls_dir) - log.info("YAML language server dependencies installed successfully") - - if not os.path.exists(yaml_executable_path): - raise FileNotFoundError( - f"yaml-language-server executable not found at {yaml_executable_path}, something went wrong with the installation." + deps = RuntimeDependencyCollection( + [ + RuntimeDependency( + id="yaml-language-server", + description="yaml-language-server package (Red Hat)", + command="npm install --prefix ./ yaml-language-server@1.19.2", + platform_id="any", + ), + ] ) - return f"{yaml_executable_path} --stdio" + + # Install yaml-language-server if not already installed + yaml_ls_dir = os.path.join(self._ls_resources_dir, "yaml-lsp") + yaml_executable_path = os.path.join(yaml_ls_dir, "node_modules", ".bin", "yaml-language-server") + + # Handle Windows executable extension + if os.name == "nt": + yaml_executable_path += ".cmd" + + if not os.path.exists(yaml_executable_path): + log.info(f"YAML Language Server executable not found at {yaml_executable_path}. Installing...") + deps.install(yaml_ls_dir) + log.info("YAML language server dependencies installed successfully") + + if not os.path.exists(yaml_executable_path): + raise FileNotFoundError( + f"yaml-language-server executable not found at {yaml_executable_path}, something went wrong with the installation." + ) + + return yaml_executable_path + + def _create_launch_command(self, core_path: str) -> list[str]: + return [core_path, "--stdio"] @staticmethod def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: From 8c54806abe42dccdc41884b65db66c66a637d5b6 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 13 Feb 2026 03:10:25 +0100 Subject: [PATCH 6/8] Marksman: Use DependencyProvider, fixing run command not working with spaces in path --- src/solidlsp/language_servers/marksman.py | 125 +++++++++++----------- 1 file changed, 64 insertions(+), 61 deletions(-) diff --git a/src/solidlsp/language_servers/marksman.py b/src/solidlsp/language_servers/marksman.py index 97c840e2..14805f21 100644 --- a/src/solidlsp/language_servers/marksman.py +++ b/src/solidlsp/language_servers/marksman.py @@ -9,10 +9,9 @@ import pathlib 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 from .common import RuntimeDependency, RuntimeDependencyCollection @@ -25,80 +24,84 @@ class Marksman(SolidLanguageServer): Provides Markdown specific instantiation of the LanguageServer class using marksman. """ - marksman_releases = "https://github.com/artempyanykh/marksman/releases/download/2024-12-18" - runtime_dependencies = RuntimeDependencyCollection( - [ - RuntimeDependency( - id="marksman", - url=f"{marksman_releases}/marksman-linux-x64", - platform_id="linux-x64", - archive_type="binary", - binary_name="marksman", - ), - RuntimeDependency( - id="marksman", - url=f"{marksman_releases}/marksman-linux-arm64", - platform_id="linux-arm64", - archive_type="binary", - binary_name="marksman", - ), - RuntimeDependency( - id="marksman", - url=f"{marksman_releases}/marksman-macos", - platform_id="osx-x64", - archive_type="binary", - binary_name="marksman", - ), - RuntimeDependency( - id="marksman", - url=f"{marksman_releases}/marksman-macos", - platform_id="osx-arm64", - archive_type="binary", - binary_name="marksman", - ), - RuntimeDependency( - id="marksman", - url=f"{marksman_releases}/marksman.exe", - platform_id="win-x64", - archive_type="binary", - binary_name="marksman.exe", - ), - ] - ) + class DependencyProvider(LanguageServerDependencyProviderSinglePath): + marksman_releases = "https://github.com/artempyanykh/marksman/releases/download/2024-12-18" + runtime_dependencies = RuntimeDependencyCollection( + [ + RuntimeDependency( + id="marksman", + url=f"{marksman_releases}/marksman-linux-x64", + platform_id="linux-x64", + archive_type="binary", + binary_name="marksman", + ), + RuntimeDependency( + id="marksman", + url=f"{marksman_releases}/marksman-linux-arm64", + platform_id="linux-arm64", + archive_type="binary", + binary_name="marksman", + ), + RuntimeDependency( + id="marksman", + url=f"{marksman_releases}/marksman-macos", + platform_id="osx-x64", + archive_type="binary", + binary_name="marksman", + ), + RuntimeDependency( + id="marksman", + url=f"{marksman_releases}/marksman-macos", + platform_id="osx-arm64", + archive_type="binary", + binary_name="marksman", + ), + RuntimeDependency( + id="marksman", + url=f"{marksman_releases}/marksman.exe", + platform_id="win-x64", + archive_type="binary", + binary_name="marksman.exe", + ), + ] + ) - @classmethod - def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> str: - """Setup runtime dependencies for marksman and return the command to start the server.""" - deps = cls.runtime_dependencies - dependency = deps.get_single_dep_for_current_platform() + def _get_or_install_core_dependency(self) -> str: + """Setup runtime dependencies for marksman and return the command to start the server.""" + deps = self.runtime_dependencies + dependency = deps.get_single_dep_for_current_platform() - marksman_ls_dir = cls.ls_resources_dir(solidlsp_settings) - marksman_executable_path = deps.binary_path(marksman_ls_dir) - if not os.path.exists(marksman_executable_path): - log.info( - f"Downloading marksman from {dependency.url} to {marksman_ls_dir}", - ) - deps.install(marksman_ls_dir) - if not os.path.exists(marksman_executable_path): - raise FileNotFoundError(f"Download failed? Could not find marksman executable at {marksman_executable_path}") - os.chmod(marksman_executable_path, 0o755) - return marksman_executable_path + marksman_ls_dir = self._ls_resources_dir + marksman_executable_path = deps.binary_path(marksman_ls_dir) + if not os.path.exists(marksman_executable_path): + log.info( + f"Downloading marksman from {dependency.url} to {marksman_ls_dir}", + ) + deps.install(marksman_ls_dir) + if not os.path.exists(marksman_executable_path): + raise FileNotFoundError(f"Download failed? Could not find marksman executable at {marksman_executable_path}") + os.chmod(marksman_executable_path, 0o755) + return marksman_executable_path + + def _create_launch_command(self, core_path: str) -> list[str]: + return [core_path, "server"] def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): """ Creates a Marksman instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - marksman_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings) - super().__init__( config, repository_root_path, - ProcessLaunchInfo(cmd=f"{marksman_executable_path} server", cwd=repository_root_path), + None, "markdown", solidlsp_settings, ) + 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 ["node_modules", ".obsidian", ".vitepress", ".vuepress"] From cd2f6f6717a668d1efbb6125475d20da1422b129 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 13 Feb 2026 03:04:07 +0100 Subject: [PATCH 7/8] Update list of LS supporting ls_path --- docs/02-usage/050_configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/02-usage/050_configuration.md b/docs/02-usage/050_configuration.md index 51204018..88192cb5 100644 --- a/docs/02-usage/050_configuration.md +++ b/docs/02-usage/050_configuration.md @@ -209,7 +209,7 @@ ls_specific_settings: ``` This is supported by all language servers deriving their dependency provider from `LanguageServerDependencyProviderSinglePath`. -Currently, this includes the following languages: `clojure`, `cpp`, `php`, `python`, `rust`, `typescript`. +Currently, this includes the following languages: `bash`, `clojure`, `cpp`, `markdown`, `php`, `php_phpactor`, `python`, `rust`, `toml`, `typescript`, `yaml`. We will add support for more languages over time. #### C# (Roslyn Language Server) From 256cb61416f9adfbfdd8e1b36d29ecaff3c80baf Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 13 Feb 2026 03:20:13 +0100 Subject: [PATCH 8/8] DependencyProviders: Allow only lists for commands to ensure robustness --- .../language_servers/bash_language_server.py | 2 +- .../language_servers/ccls_language_server.py | 2 +- src/solidlsp/language_servers/clojure_lsp.py | 2 +- .../language_servers/csharp_language_server.py | 2 +- src/solidlsp/language_servers/eclipse_jdtls.py | 2 +- .../language_servers/matlab_language_server.py | 4 ++-- src/solidlsp/language_servers/rust_analyzer.py | 2 +- src/solidlsp/language_servers/taplo_server.py | 2 +- src/solidlsp/ls.py | 12 +++++------- 9 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/solidlsp/language_servers/bash_language_server.py b/src/solidlsp/language_servers/bash_language_server.py index fddce311..b868cebb 100644 --- a/src/solidlsp/language_servers/bash_language_server.py +++ b/src/solidlsp/language_servers/bash_language_server.py @@ -90,7 +90,7 @@ class BashLanguageServer(SolidLanguageServer): return bash_executable_path - def _create_launch_command(self, core_path: str) -> list[str] | str: + def _create_launch_command(self, core_path: str) -> list[str]: return [core_path, "start"] @staticmethod diff --git a/src/solidlsp/language_servers/ccls_language_server.py b/src/solidlsp/language_servers/ccls_language_server.py index f8a0db45..ade8aaf4 100644 --- a/src/solidlsp/language_servers/ccls_language_server.py +++ b/src/solidlsp/language_servers/ccls_language_server.py @@ -91,7 +91,7 @@ class CCLS(SolidLanguageServer): log.info(f"Using system-installed ccls at {ccls_path}") return ccls_path - def _create_launch_command(self, core_path: str) -> list[str] | str: + def _create_launch_command(self, core_path: str) -> list[str]: return [core_path] @staticmethod diff --git a/src/solidlsp/language_servers/clojure_lsp.py b/src/solidlsp/language_servers/clojure_lsp.py index 5ed597c0..505f8228 100644 --- a/src/solidlsp/language_servers/clojure_lsp.py +++ b/src/solidlsp/language_servers/clojure_lsp.py @@ -123,7 +123,7 @@ class ClojureLSP(SolidLanguageServer): os.chmod(clojurelsp_executable_path, 0o755) return clojurelsp_executable_path - def _create_launch_command(self, core_path: str) -> list[str] | str: + def _create_launch_command(self, core_path: str) -> list[str]: return [core_path] @staticmethod diff --git a/src/solidlsp/language_servers/csharp_language_server.py b/src/solidlsp/language_servers/csharp_language_server.py index a5f4f040..ee33aa23 100644 --- a/src/solidlsp/language_servers/csharp_language_server.py +++ b/src/solidlsp/language_servers/csharp_language_server.py @@ -306,7 +306,7 @@ class CSharpLanguageServer(SolidLanguageServer): self._repository_root_path = repository_root_path self._dotnet_path, self._language_server_path = self._ensure_server_installed() - def create_launch_command(self) -> list[str] | str: + def create_launch_command(self) -> list[str]: # Find solution or project file solution_or_project = find_solution_or_project_file(self._repository_root_path) diff --git a/src/solidlsp/language_servers/eclipse_jdtls.py b/src/solidlsp/language_servers/eclipse_jdtls.py index 880a5adb..773776d9 100644 --- a/src/solidlsp/language_servers/eclipse_jdtls.py +++ b/src/solidlsp/language_servers/eclipse_jdtls.py @@ -271,7 +271,7 @@ class EclipseJDTLS(SolidLanguageServer): intellisense_members_path=intellisense_members_path, ) - def create_launch_command(self) -> list[str] | str: + def create_launch_command(self) -> list[str]: # ws_dir is the workspace directory for the EclipseJDTLS server ws_dir = str( PurePath( diff --git a/src/solidlsp/language_servers/matlab_language_server.py b/src/solidlsp/language_servers/matlab_language_server.py index bd16fc03..1b9ef439 100644 --- a/src/solidlsp/language_servers/matlab_language_server.py +++ b/src/solidlsp/language_servers/matlab_language_server.py @@ -38,7 +38,7 @@ import platform import shutil import threading import zipfile -from typing import Any, Union, cast +from typing import Any, cast import requests @@ -339,7 +339,7 @@ class MatlabLanguageServer(SolidLanguageServer): self._matlab_path = matlab_path return matlab_path - def create_launch_command(self) -> Union[str, list[str]]: + def create_launch_command(self) -> list[str]: # Verify node is installed node_path = shutil.which("node") if node_path is None: diff --git a/src/solidlsp/language_servers/rust_analyzer.py b/src/solidlsp/language_servers/rust_analyzer.py index 259acb85..b7c95947 100644 --- a/src/solidlsp/language_servers/rust_analyzer.py +++ b/src/solidlsp/language_servers/rust_analyzer.py @@ -165,7 +165,7 @@ class RustAnalyzer(SolidLanguageServer): def _get_or_install_core_dependency(self) -> str: return self._ensure_rust_analyzer_installed() - def _create_launch_command(self, core_path: str) -> list[str] | str: + def _create_launch_command(self, core_path: str) -> list[str]: return [core_path] def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): diff --git a/src/solidlsp/language_servers/taplo_server.py b/src/solidlsp/language_servers/taplo_server.py index ea6de30e..213af874 100644 --- a/src/solidlsp/language_servers/taplo_server.py +++ b/src/solidlsp/language_servers/taplo_server.py @@ -163,7 +163,7 @@ class TaploServer(SolidLanguageServer): return taplo_executable - def _create_launch_command(self, core_path: str) -> list[str] | str: + def _create_launch_command(self, core_path: str) -> list[str]: return [core_path, "lsp", "stdio"] @classmethod diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 5a0bc625..67d95361 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -241,13 +241,12 @@ class LanguageServerDependencyProvider(ABC): self._ls_resources_dir = ls_resources_dir @abstractmethod - def create_launch_command(self) -> list[str] | str: + def create_launch_command(self) -> list[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. + :return: the launch command as a list containing the executable and its arguments """ def create_launch_command_env(self) -> dict[str, str]: @@ -279,7 +278,7 @@ class LanguageServerDependencyProviderSinglePath(LanguageServerDependencyProvide :return: the core dependency's path (e.g. executable, jar, etc.) """ - def create_launch_command(self) -> Union[str, list[str]]: + def create_launch_command(self) -> list[str]: path = self._custom_settings.get("ls_path", None) if path is not None: core_path = path @@ -288,11 +287,10 @@ class LanguageServerDependencyProviderSinglePath(LanguageServerDependencyProvide return self._create_launch_command(core_path) @abstractmethod - def _create_launch_command(self, core_path: str) -> list[str] | str: + def _create_launch_command(self, core_path: str) -> list[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. + :return: the launch command as a list containing the executable and its arguments """