From 47e8e4ec3a6f6884406cd4cdd66df7546a79e4a7 Mon Sep 17 00:00:00 2001 From: "J. Simon Richard" Date: Wed, 1 Oct 2025 19:33:05 -0400 Subject: [PATCH 1/3] Allow customizing the LSP binary (rust-analyzer only, for now) --- .../language_servers/rust_analyzer/rust_analyzer.py | 6 ++++++ src/multilspy/multilspy_config.py | 3 +++ 2 files changed, 9 insertions(+) diff --git a/src/multilspy/language_servers/rust_analyzer/rust_analyzer.py b/src/multilspy/language_servers/rust_analyzer/rust_analyzer.py index 1a95eec..d51c907 100644 --- a/src/multilspy/language_servers/rust_analyzer/rust_analyzer.py +++ b/src/multilspy/language_servers/rust_analyzer/rust_analyzer.py @@ -43,6 +43,12 @@ def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyC """ Setup runtime dependencies for rust_analyzer. """ + + if config.custom_lsp_binary: + path = os.path.abspath(config.custom_lsp_binary) + assert os.path.exists(path) + return path + platform_id = PlatformUtils.get_platform_id() with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r") as f: diff --git a/src/multilspy/multilspy_config.py b/src/multilspy/multilspy_config.py index 86c6a6c..5b99369 100644 --- a/src/multilspy/multilspy_config.py +++ b/src/multilspy/multilspy_config.py @@ -4,6 +4,7 @@ from enum import Enum from dataclasses import dataclass +from typing import Optional class Language(str, Enum): """ @@ -33,6 +34,8 @@ class MultilspyConfig: code_language: Language trace_lsp_communication: bool = False start_independent_lsp_process: bool = True + # Only works for Rust Analyzer + custom_lsp_binary: Optional[bool] = None @classmethod def from_dict(cls, env: dict): From b1d49568c76caa794be410a0b8c9c75dd9a9467f Mon Sep 17 00:00:00 2001 From: "J. Simon Richard" Date: Wed, 1 Oct 2025 19:57:54 -0400 Subject: [PATCH 2/3] Fix type --- src/multilspy/multilspy_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/multilspy/multilspy_config.py b/src/multilspy/multilspy_config.py index 5b99369..ec86199 100644 --- a/src/multilspy/multilspy_config.py +++ b/src/multilspy/multilspy_config.py @@ -35,7 +35,7 @@ class MultilspyConfig: trace_lsp_communication: bool = False start_independent_lsp_process: bool = True # Only works for Rust Analyzer - custom_lsp_binary: Optional[bool] = None + custom_lsp_binary: Optional[str] = None @classmethod def from_dict(cls, env: dict): From 963b5ca58a9afefa8076d18a3805594e330996eb Mon Sep 17 00:00:00 2001 From: "J. Simon Richard" Date: Sat, 7 Feb 2026 19:23:02 -0500 Subject: [PATCH 3/3] Update to support custom 'binary' path for all LSPs --- .../clangd_language_server.py | 6 ++++++ .../dart_language_server.py | 19 +++++++++++++++--- .../eclipse_jdtls/eclipse_jdtls.py | 12 +++++++++-- src/multilspy/language_servers/gopls/gopls.py | 15 +++++++++++--- .../jedi_language_server/jedi_server.py | 8 +++++++- .../kotlin_language_server.py | 6 ++++++ .../language_servers/omnisharp/omnisharp.py | 7 +++++++ .../language_servers/solargraph/solargraph.py | 5 +++++ .../typescript_language_server.py | 9 +++++++++ src/multilspy/multilspy_config.py | 20 ++++++++++++++++++- 10 files changed, 97 insertions(+), 10 deletions(-) diff --git a/src/multilspy/language_servers/clangd_language_server/clangd_language_server.py b/src/multilspy/language_servers/clangd_language_server/clangd_language_server.py index a5e5326..fb229da 100644 --- a/src/multilspy/language_servers/clangd_language_server/clangd_language_server.py +++ b/src/multilspy/language_servers/clangd_language_server/clangd_language_server.py @@ -57,6 +57,12 @@ def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyC "osx-arm64", ], "Unsupported platform: " + platform_id.value + # Check for custom binary after platform validation + if config.custom_lsp_binary: + path = os.path.abspath(config.custom_lsp_binary) + assert os.path.exists(path) + return path + runtime_dependencies = d["runtimeDependencies"] runtime_dependencies = [ dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value diff --git a/src/multilspy/language_servers/dart_language_server/dart_language_server.py b/src/multilspy/language_servers/dart_language_server/dart_language_server.py index b814306..8f10710 100644 --- a/src/multilspy/language_servers/dart_language_server/dart_language_server.py +++ b/src/multilspy/language_servers/dart_language_server/dart_language_server.py @@ -21,7 +21,7 @@ def __init__(self, config, logger, repository_root_path): Creates a DartServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - executable_path = self.setup_runtime_dependencies(logger) + executable_path = self.setup_runtime_dependencies(logger, config) super().__init__( config, logger, @@ -30,7 +30,20 @@ def __init__(self, config, logger, repository_root_path): "dart", ) - def setup_runtime_dependencies(self, logger: "MultilspyLogger") -> str: + def setup_runtime_dependencies(self, logger: "MultilspyLogger", config: "MultilspyConfig") -> str: + lsp_args = [ + "language-server", + "--client-id", + "multilspy.dart", + "--client-version", + "1.2", + ] + # Check for custom binary after platform validation + if config.custom_lsp_binary: + path = os.path.abspath(config.custom_lsp_binary) + assert os.path.exists(path) + return f"{path} {' '.join(lsp_args)}" + platform_id = PlatformUtils.get_platform_id() with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r") as f: @@ -58,7 +71,7 @@ def setup_runtime_dependencies(self, logger: "MultilspyLogger") -> str: assert os.path.exists(dart_executable_path) os.chmod(dart_executable_path, stat.S_IEXEC) - return f"{dart_executable_path} language-server --client-id multilspy.dart --client-version 1.2" + return f"{dart_executable_path} {' '.join(lsp_args)}" def _get_initialize_params(self, repository_absolute_path: str): diff --git a/src/multilspy/language_servers/eclipse_jdtls/eclipse_jdtls.py b/src/multilspy/language_servers/eclipse_jdtls/eclipse_jdtls.py index ed4bfae..782d86e 100644 --- a/src/multilspy/language_servers/eclipse_jdtls/eclipse_jdtls.py +++ b/src/multilspy/language_servers/eclipse_jdtls/eclipse_jdtls.py @@ -51,7 +51,7 @@ def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_ Creates a new EclipseJDTLS instance initializing the language server settings appropriately. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - + runtime_dependency_paths = self.setupRuntimeDependencies(logger, config) self.runtime_dependency_paths = runtime_dependency_paths @@ -97,6 +97,14 @@ def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_ # TODO: Add "self.runtime_dependency_paths.jre_home_path"/bin to $PATH as well proc_env = {"syntaxserver": "false", "JAVA_HOME": self.runtime_dependency_paths.jre_home_path} proc_cwd = repository_root_path + + # Override JDTLS launcher jar path if custom "binary" is provided + launcher_jar = jdtls_launcher_jar + if config.custom_lsp_binary: + custom_path = os.path.abspath(config.custom_lsp_binary) + assert os.path.exists(custom_path) + launcher_jar = custom_path + cmd = " ".join( [ jre_path, @@ -125,7 +133,7 @@ def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_ f"-javaagent:{lombok_jar_path}", f"-Djdt.core.sharedIndexLocation={shared_cache_location}", "-jar", - jdtls_launcher_jar, + launcher_jar, "-configuration", jdtls_config_path, "-data", diff --git a/src/multilspy/language_servers/gopls/gopls.py b/src/multilspy/language_servers/gopls/gopls.py index 40031db..aa97c4e 100644 --- a/src/multilspy/language_servers/gopls/gopls.py +++ b/src/multilspy/language_servers/gopls/gopls.py @@ -42,7 +42,7 @@ def _get_gopls_version(): return None @classmethod - def setup_runtime_dependency(cls): + def setup_runtime_dependency(cls, config: MultilspyConfig): """ Check if required Go runtime dependencies are available. Raises RuntimeError with helpful message if dependencies are missing. @@ -53,6 +53,9 @@ def setup_runtime_dependency(cls): go_version = cls._get_go_version() if not go_version: missing_deps.append(("Go", "https://golang.org/doc/install")) + + if config.custom_lsp_binary: + return True # Check for gopls gopls_version = cls._get_gopls_version() @@ -69,13 +72,19 @@ def setup_runtime_dependency(cls): def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): # Check runtime dependencies before initializing - self.setup_runtime_dependency() + self.setup_runtime_dependency(config) + + cmd = "gopls" + if config.custom_lsp_binary: + path = os.path.abspath(config.custom_lsp_binary) + assert os.path.exists(path) + cmd = path super().__init__( config, logger, repository_root_path, - ProcessLaunchInfo(cmd="gopls", cwd=repository_root_path), + ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path), "go", ) self.server_ready = asyncio.Event() diff --git a/src/multilspy/language_servers/jedi_language_server/jedi_server.py b/src/multilspy/language_servers/jedi_language_server/jedi_server.py index d49ace2..b211484 100644 --- a/src/multilspy/language_servers/jedi_language_server/jedi_server.py +++ b/src/multilspy/language_servers/jedi_language_server/jedi_server.py @@ -25,11 +25,17 @@ def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_ """ Creates a JediServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ + cmd = "jedi-language-server" + if config.custom_lsp_binary: + path = os.path.abspath(config.custom_lsp_binary) + assert os.path.exists(path) + cmd = path + super().__init__( config, logger, repository_root_path, - ProcessLaunchInfo(cmd="jedi-language-server", cwd=repository_root_path), + ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path), "python", ) diff --git a/src/multilspy/language_servers/kotlin_language_server/kotlin_language_server.py b/src/multilspy/language_servers/kotlin_language_server/kotlin_language_server.py index 76bc1e8..b80f967 100644 --- a/src/multilspy/language_servers/kotlin_language_server/kotlin_language_server.py +++ b/src/multilspy/language_servers/kotlin_language_server/kotlin_language_server.py @@ -124,6 +124,12 @@ def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyC else: raise FileNotFoundError(f"Kotlin Language Server script not found at {kotlin_script}") + # Override executable path if custom binary is provided + if config.custom_lsp_binary: + custom_path = os.path.abspath(config.custom_lsp_binary) + assert os.path.exists(custom_path) + kotlin_executable_path = custom_path + return KotlinRuntimeDependencyPaths( java_path=java_path, java_home_path=java_home_path, diff --git a/src/multilspy/language_servers/omnisharp/omnisharp.py b/src/multilspy/language_servers/omnisharp/omnisharp.py index 0f295a0..8c51582 100644 --- a/src/multilspy/language_servers/omnisharp/omnisharp.py +++ b/src/multilspy/language_servers/omnisharp/omnisharp.py @@ -197,6 +197,13 @@ def setupRuntimeDependencies(self, logger: MultilspyLogger, config: MultilspyCon ) assert os.path.exists(razor_omnisharp_dll_path) + # Override executable path if custom binary is provided + # Note: Only the OmniSharp executable path is replaced. + if config.custom_lsp_binary: + custom_path = os.path.abspath(config.custom_lsp_binary) + assert os.path.exists(custom_path) + omnisharp_executable_path = custom_path + return omnisharp_executable_path, razor_omnisharp_dll_path @asynccontextmanager diff --git a/src/multilspy/language_servers/solargraph/solargraph.py b/src/multilspy/language_servers/solargraph/solargraph.py index 8326779..5b1a14c 100644 --- a/src/multilspy/language_servers/solargraph/solargraph.py +++ b/src/multilspy/language_servers/solargraph/solargraph.py @@ -47,6 +47,11 @@ def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyC """ Setup runtime dependencies for Solargraph. """ + # Check for custom binary after Ruby validation + if config.custom_lsp_binary: + path = os.path.abspath(config.custom_lsp_binary) + assert os.path.exists(path) + return path with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r") as f: d = json.load(f) diff --git a/src/multilspy/language_servers/typescript_language_server/typescript_language_server.py b/src/multilspy/language_servers/typescript_language_server/typescript_language_server.py index fde7d3b..c07454f 100644 --- a/src/multilspy/language_servers/typescript_language_server/typescript_language_server.py +++ b/src/multilspy/language_servers/typescript_language_server/typescript_language_server.py @@ -61,6 +61,15 @@ def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyC ] assert platform_id in valid_platforms, f"Platform {platform_id} is not supported for multilspy javascript/typescript at the moment" + # Check for custom binary after platform validation + # Note: The '--stdio' flag is automatically appended to the custom binary path, + # as it's required for LSP communication. Provide the path to the + # typescript-language-server executable. + if config.custom_lsp_binary: + path = os.path.abspath(config.custom_lsp_binary) + assert os.path.exists(path) + return f"{path} --stdio" + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r") as f: d = json.load(f) del d["_description"] diff --git a/src/multilspy/multilspy_config.py b/src/multilspy/multilspy_config.py index ec86199..eeb9c09 100644 --- a/src/multilspy/multilspy_config.py +++ b/src/multilspy/multilspy_config.py @@ -34,7 +34,25 @@ class MultilspyConfig: code_language: Language trace_lsp_communication: bool = False start_independent_lsp_process: bool = True - # Only works for Rust Analyzer + + # Optional path to a custom LSP binary/executable to use instead of the default. + # + # For most language servers, this simply replaces the executable path. However, there + # are some exceptions: + # + # - Java (EclipseJDTLS): Replaces only the launcher jar path. All Java/JVM arguments, + # runtime setup, and configuration are preserved. The custom binary should be a path + # to a JDTLS launcher jar file. + # + # - Kotlin: Replaces only the Kotlin LSP executable path. All Java setup (JAVA_HOME, etc.) + # is still performed and preserved, as it's required for Kotlin to function. + # + # - C# (OmniSharp): Replaces only the OmniSharp executable path. All setup including + # RazorOmnisharp DLL download is still performed, and the DLL path is still available + # if needed by the custom binary. + # + # For all other language servers (Rust, Python, Go, Ruby, C++), the custom binary + # path directly replaces the default executable with no other modifications. custom_lsp_binary: Optional[str] = None @classmethod