diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index cf3b93a4..91dcd610 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -77,7 +77,7 @@ jobs: id: cache-language-servers uses: actions/cache@v3 with: - path: src/solidlsp/language_servers/static + path: ~/.serena/language_servers/static key: language-servers-${{ runner.os }}-v1 restore-keys: | language-servers-${{ runner.os }}- diff --git a/src/serena/project.py b/src/serena/project.py index b73a252d..d6a7ba79 100644 --- a/src/serena/project.py +++ b/src/serena/project.py @@ -5,11 +5,13 @@ from pathlib import Path import pathspec from serena.config.serena_config import DEFAULT_TOOL_TIMEOUT, ProjectConfig +from serena.constants import SERENA_MANAGED_DIR_IN_HOME from serena.text_utils import MatchedConsecutiveLines, search_files from serena.util.file_system import GitignoreParser, match_path from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language, LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.settings import SolidLSPSettings log = logging.getLogger(__name__) @@ -271,4 +273,5 @@ class Project: ls_logger, self.project_root, timeout=ls_timeout, + solidlsp_settings=SolidLSPSettings(solidlsp_dir=SERENA_MANAGED_DIR_IN_HOME), ) diff --git a/src/solidlsp/language_servers/clangd_language_server.py b/src/solidlsp/language_servers/clangd_language_server.py index b8845d84..a770d78e 100644 --- a/src/solidlsp/language_servers/clangd_language_server.py +++ b/src/solidlsp/language_servers/clangd_language_server.py @@ -13,6 +13,7 @@ from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger 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 @@ -24,25 +25,30 @@ class ClangdLanguageServer(SolidLanguageServer): Also make sure compile_commands.json is created at root of the source directory. Check clangd test case for example. """ - def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): + def __init__( + self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings + ): """ Creates a ClangdLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - clangd_executable_path = self._setup_runtime_dependencies(logger, config) + clangd_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings) super().__init__( config, logger, repository_root_path, ProcessLaunchInfo(cmd=clangd_executable_path, cwd=repository_root_path), "cpp", + solidlsp_settings, ) self.server_ready = threading.Event() self.service_ready_event = threading.Event() self.initialize_searcher_command_available = threading.Event() self.resolve_main_method_available = threading.Event() - @staticmethod - def _setup_runtime_dependencies(logger: LanguageServerLogger, config: LanguageServerConfig) -> str: + @classmethod + def _setup_runtime_dependencies( + cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings + ) -> str: """ Setup runtime dependencies for ClangdLanguageServer and return the command to start the server. """ @@ -75,7 +81,7 @@ class ClangdLanguageServer(SolidLanguageServer): ] ) - clangd_ls_dir = os.path.join(os.path.dirname(__file__), "static", "clangd") + clangd_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "clangd") dep = deps.single_for_current_platform() clangd_executable_path = deps.binary_path(clangd_ls_dir) if not os.path.exists(clangd_executable_path): diff --git a/src/solidlsp/language_servers/clojure_lsp.py b/src/solidlsp/language_servers/clojure_lsp.py index cae8abc2..f973db15 100644 --- a/src/solidlsp/language_servers/clojure_lsp.py +++ b/src/solidlsp/language_servers/clojure_lsp.py @@ -15,6 +15,7 @@ from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger 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 @@ -78,34 +79,38 @@ class ClojureLSP(SolidLanguageServer): ] ) - def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): + def __init__( + self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings + ): """ Creates a ClojureLSP instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - clojure_lsp_executable_path = self._setup_runtime_dependencies(logger, config) + clojure_lsp_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings) super().__init__( config, logger, repository_root_path, ProcessLaunchInfo(cmd=clojure_lsp_executable_path, cwd=repository_root_path), "clojure", + solidlsp_settings, ) self.server_ready = threading.Event() self.initialize_searcher_command_available = threading.Event() self.resolve_main_method_available = threading.Event() self.service_ready_event = threading.Event() - @staticmethod - def _setup_runtime_dependencies(logger: LanguageServerLogger, config: LanguageServerConfig) -> str: + @classmethod + def _setup_runtime_dependencies( + cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings + ) -> str: """Setup runtime dependencies for clojure-lsp and return the command to start the server.""" verify_clojure_cli() deps = ClojureLSP.runtime_dependencies dependency = deps.single_for_current_platform() - clojurelsp_ls_dir = os.path.join(os.path.dirname(__file__), "static", "clojure-lsp") + clojurelsp_ls_dir = cls.ls_resources_dir(solidlsp_settings) clojurelsp_executable_path = deps.binary_path(clojurelsp_ls_dir) if not os.path.exists(clojurelsp_executable_path): - os.makedirs(clojurelsp_ls_dir, exist_ok=True) logger.log( f"Downloading and extracting clojure-lsp from {dependency.url} to {clojurelsp_ls_dir}", logging.INFO, diff --git a/src/solidlsp/language_servers/csharp_language_server.py b/src/solidlsp/language_servers/csharp_language_server.py index 7d11a1df..88bf9bc4 100644 --- a/src/solidlsp/language_servers/csharp_language_server.py +++ b/src/solidlsp/language_servers/csharp_language_server.py @@ -25,6 +25,7 @@ from solidlsp.ls_logger import LanguageServerLogger 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 from .common import RuntimeDependency @@ -175,18 +176,20 @@ class CSharpLanguageServer(SolidLanguageServer): This is the official Roslyn-based language server from Microsoft. """ - def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): + def __init__( + self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings + ): """ Creates a CSharpLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - dotnet_path, language_server_path = self._ensure_server_installed(logger, config) + dotnet_path, language_server_path = self._ensure_server_installed(logger, config, solidlsp_settings) # Find solution or project file solution_or_project = find_solution_or_project_file(repository_root_path) # Create log directory - log_dir = Path(self.ls_resources_dir()) / "logs" + log_dir = Path(self.ls_resources_dir(solidlsp_settings)) / "logs" log_dir.mkdir(parents=True, exist_ok=True) # Build command using dotnet directly @@ -206,6 +209,7 @@ class CSharpLanguageServer(SolidLanguageServer): repository_root_path, ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path), "csharp", + solidlsp_settings, ) self.initialization_complete = threading.Event() @@ -215,15 +219,17 @@ class CSharpLanguageServer(SolidLanguageServer): return super().is_ignored_dirname(dirname) or dirname in ["bin", "obj", "packages", ".vs"] @classmethod - def _ensure_server_installed(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> tuple[str, str]: + def _ensure_server_installed( + cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings + ) -> tuple[str, str]: """ Ensure .NET runtime and Microsoft.CodeAnalysis.LanguageServer are available. Returns a tuple of (dotnet_path, language_server_dll_path). """ runtime_id = CSharpLanguageServer._get_runtime_id() lang_server_dep, dotnet_runtime_dep = CSharpLanguageServer._get_runtime_dependencies(runtime_id) - dotnet_path = CSharpLanguageServer._ensure_dotnet_runtime(logger, dotnet_runtime_dep) - server_dll_path = CSharpLanguageServer._ensure_language_server(logger, lang_server_dep) + dotnet_path = CSharpLanguageServer._ensure_dotnet_runtime(logger, dotnet_runtime_dep, solidlsp_settings) + server_dll_path = CSharpLanguageServer._ensure_language_server(logger, lang_server_dep, solidlsp_settings) return dotnet_path, server_dll_path @@ -261,8 +267,10 @@ class CSharpLanguageServer(SolidLanguageServer): return lang_server_dep, dotnet_runtime_dep - @staticmethod - def _ensure_dotnet_runtime(logger: LanguageServerLogger, runtime_dep: RuntimeDependency) -> str: + @classmethod + def _ensure_dotnet_runtime( + cls, logger: LanguageServerLogger, runtime_dep: RuntimeDependency, solidlsp_settings: SolidLSPSettings + ) -> str: """Ensure .NET runtime is available and return the dotnet executable path.""" # Check if dotnet is already available on the system system_dotnet = shutil.which("dotnet") @@ -277,15 +285,17 @@ class CSharpLanguageServer(SolidLanguageServer): pass # Download .NET 9 runtime using config - return CSharpLanguageServer._ensure_dotnet_runtime_from_config(logger, runtime_dep) + return cls._ensure_dotnet_runtime_from_config(logger, runtime_dep, solidlsp_settings) @classmethod - def _ensure_language_server(cls, logger: LanguageServerLogger, lang_server_dep: RuntimeDependency) -> str: + def _ensure_language_server( + cls, logger: LanguageServerLogger, lang_server_dep: RuntimeDependency, solidlsp_settings: SolidLSPSettings + ) -> str: """Ensure language server is available and return the DLL path.""" package_name = lang_server_dep.package_name package_version = lang_server_dep.package_version - server_dir = Path(cls.ls_resources_dir()) / f"{package_name}.{package_version}" + server_dir = Path(cls.ls_resources_dir(solidlsp_settings)) / f"{package_name}.{package_version}" server_dll = server_dir / lang_server_dep.binary_name if server_dll.exists(): @@ -294,10 +304,10 @@ class CSharpLanguageServer(SolidLanguageServer): # Download and install the language server logger.log(f"Downloading {package_name} version {package_version}...", logging.INFO) - package_path = CSharpLanguageServer._download_nuget_package_direct(logger, package_name, package_version) + package_path = cls._download_nuget_package_direct(logger, package_name, package_version, solidlsp_settings) # Extract and install - CSharpLanguageServer._extract_language_server(lang_server_dep, package_path, server_dir) + cls._extract_language_server(lang_server_dep, package_path, server_dir) if not server_dll.exists(): raise LanguageServerException("Microsoft.CodeAnalysis.LanguageServer.dll not found after extraction") @@ -333,7 +343,9 @@ class CSharpLanguageServer(SolidLanguageServer): shutil.copytree(source_dir, server_dir, dirs_exist_ok=True) @classmethod - def _download_nuget_package_direct(cls, logger: LanguageServerLogger, package_name: str, package_version: str) -> Path: + def _download_nuget_package_direct( + cls, logger: LanguageServerLogger, package_name: str, package_version: str, solidlsp_settings: SolidLSPSettings + ) -> Path: """ Download a NuGet package directly from the Azure NuGet feed. Returns the path to the extracted package directory. @@ -341,7 +353,7 @@ class CSharpLanguageServer(SolidLanguageServer): azure_feed_url = "https://pkgs.dev.azure.com/azure-public/vside/_packaging/vs-impl/nuget/v3/index.json" # Create temporary directory for package download - temp_dir = Path(cls.ls_resources_dir()) / "temp_downloads" + temp_dir = Path(cls.ls_resources_dir(solidlsp_settings)) / "temp_downloads" temp_dir.mkdir(parents=True, exist_ok=True) try: @@ -390,7 +402,9 @@ class CSharpLanguageServer(SolidLanguageServer): ) from e @classmethod - def _ensure_dotnet_runtime_from_config(cls, logger: LanguageServerLogger, runtime_dep: RuntimeDependency) -> str: + def _ensure_dotnet_runtime_from_config( + cls, logger: LanguageServerLogger, runtime_dep: RuntimeDependency, solidlsp_settings: SolidLSPSettings + ) -> str: """ Ensure .NET 9 runtime is available using runtime dependency configuration. Returns the path to the dotnet executable. @@ -408,7 +422,7 @@ class CSharpLanguageServer(SolidLanguageServer): pass # Download .NET 9 runtime using config - dotnet_dir = Path(cls.ls_resources_dir()) / "dotnet-runtime-9.0" + dotnet_dir = Path(cls.ls_resources_dir(solidlsp_settings)) / "dotnet-runtime-9.0" dotnet_exe = dotnet_dir / runtime_dep.binary_name if dotnet_exe.exists(): diff --git a/src/solidlsp/language_servers/dart_language_server.py b/src/solidlsp/language_servers/dart_language_server.py index 28696ad6..c9cfcad1 100644 --- a/src/solidlsp/language_servers/dart_language_server.py +++ b/src/solidlsp/language_servers/dart_language_server.py @@ -6,6 +6,7 @@ import stat from solidlsp.ls import SolidLanguageServer from solidlsp.ls_logger import LanguageServerLogger from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.settings import SolidLSPSettings from .common import RuntimeDependency, RuntimeDependencyCollection @@ -15,21 +16,22 @@ class DartLanguageServer(SolidLanguageServer): Provides Dart specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Dart. """ - def __init__(self, config, logger, repository_root_path): + def __init__(self, config, logger, repository_root_path, solidlsp_settings: SolidLSPSettings): """ 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, solidlsp_settings) super().__init__( config, logger, repository_root_path, ProcessLaunchInfo(cmd=executable_path, cwd=repository_root_path), "dart", + solidlsp_settings, ) @classmethod - def _setup_runtime_dependencies(cls, logger: "LanguageServerLogger") -> str: + def _setup_runtime_dependencies(cls, logger: "LanguageServerLogger", solidlsp_settings: SolidLSPSettings) -> str: deps = RuntimeDependencyCollection( [ RuntimeDependency( @@ -75,7 +77,7 @@ class DartLanguageServer(SolidLanguageServer): ] ) - dart_ls_dir = cls.ls_resources_dir() + dart_ls_dir = cls.ls_resources_dir(solidlsp_settings) dart_executable_path = deps.binary_path(dart_ls_dir) if not os.path.exists(dart_executable_path): diff --git a/src/solidlsp/language_servers/eclipse_jdtls.py b/src/solidlsp/language_servers/eclipse_jdtls.py index 5ebb378c..bf87aab7 100644 --- a/src/solidlsp/language_servers/eclipse_jdtls.py +++ b/src/solidlsp/language_servers/eclipse_jdtls.py @@ -44,18 +44,20 @@ class EclipseJDTLS(SolidLanguageServer): The EclipseJDTLS class provides a Java specific implementation of the LanguageServer class """ - def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): + def __init__( + self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings + ): """ 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) + runtime_dependency_paths = self._setupRuntimeDependencies(logger, config, solidlsp_settings) self.runtime_dependency_paths = runtime_dependency_paths # ws_dir is the workspace directory for the EclipseJDTLS server ws_dir = str( PurePath( - SolidLSPSettings.get_language_server_directory(), + solidlsp_settings.ls_resources_dir, "EclipseJDTLS", "workspaces", uuid.uuid4().hex, @@ -63,15 +65,15 @@ class EclipseJDTLS(SolidLanguageServer): ) # shared_cache_location is the global cache used by Eclipse JDTLS across all workspaces - shared_cache_location = str(PurePath(SolidLSPSettings.get_global_cache_directory(), "lsp", "EclipseJDTLS", "sharedIndex")) + shared_cache_location = str(PurePath(solidlsp_settings.ls_resources_dir, "lsp", "EclipseJDTLS", "sharedIndex")) + os.makedirs(shared_cache_location, exist_ok=True) + os.makedirs(ws_dir, exist_ok=True) jre_path = self.runtime_dependency_paths.jre_path lombok_jar_path = self.runtime_dependency_paths.lombok_jar_path jdtls_launcher_jar = self.runtime_dependency_paths.jdtls_launcher_jar_path - os.makedirs(ws_dir, exist_ok=True) - data_dir = str(PurePath(ws_dir, "data_dir")) jdtls_config_path = str(PurePath(ws_dir, "config_path")) @@ -132,7 +134,9 @@ class EclipseJDTLS(SolidLanguageServer): self.intellicode_enable_command_available = threading.Event() self.initialize_searcher_command_available = threading.Event() - super().__init__(config, logger, repository_root_path, ProcessLaunchInfo(cmd, proc_env, proc_cwd), "java") + super().__init__( + config, logger, repository_root_path, ProcessLaunchInfo(cmd, proc_env, proc_cwd), "java", solidlsp_settings=solidlsp_settings + ) @override def is_ignored_dirname(self, dirname: str) -> bool: @@ -153,7 +157,9 @@ class EclipseJDTLS(SolidLanguageServer): ] @classmethod - def _setupRuntimeDependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> RuntimeDependencyPaths: + def _setupRuntimeDependencies( + cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings + ) -> RuntimeDependencyPaths: """ Setup runtime dependencies for EclipseJDTLS and return the paths. """ @@ -238,7 +244,7 @@ class EclipseJDTLS(SolidLanguageServer): gradle_path = str( PurePath( - cls.ls_resources_dir(), + cls.ls_resources_dir(solidlsp_settings), "gradle-7.3.3", ) ) @@ -254,7 +260,7 @@ class EclipseJDTLS(SolidLanguageServer): assert os.path.exists(gradle_path) dependency = runtime_dependencies["vscode-java"][platformId.value] - vscode_java_path = str(PurePath(cls.ls_resources_dir(), dependency["relative_extraction_path"])) + vscode_java_path = str(PurePath(cls.ls_resources_dir(solidlsp_settings), dependency["relative_extraction_path"])) os.makedirs(vscode_java_path, exist_ok=True) jre_home_path = str(PurePath(vscode_java_path, dependency["jre_home_path"])) jre_path = str(PurePath(vscode_java_path, dependency["jre_path"])) @@ -283,7 +289,7 @@ class EclipseJDTLS(SolidLanguageServer): assert os.path.exists(jdtls_readonly_config_path) dependency = runtime_dependencies["intellicode"]["platform-agnostic"] - intellicode_directory_path = str(PurePath(cls.ls_resources_dir(), dependency["relative_extraction_path"])) + intellicode_directory_path = str(PurePath(cls.ls_resources_dir(solidlsp_settings), dependency["relative_extraction_path"])) os.makedirs(intellicode_directory_path, exist_ok=True) intellicode_jar_path = str(PurePath(intellicode_directory_path, dependency["intellicode_jar_path"])) intellisense_members_path = str(PurePath(intellicode_directory_path, dependency["intellisense_members_path"])) diff --git a/src/solidlsp/language_servers/elixir_tools/elixir_tools.py b/src/solidlsp/language_servers/elixir_tools/elixir_tools.py index 96674722..5f6a9794 100644 --- a/src/solidlsp/language_servers/elixir_tools/elixir_tools.py +++ b/src/solidlsp/language_servers/elixir_tools/elixir_tools.py @@ -1,4 +1,3 @@ -import json import logging import os import pathlib @@ -6,16 +5,18 @@ import stat import subprocess import threading import time -from pathlib import PurePath from overrides import override from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger -from solidlsp.ls_utils import FileUtils, PlatformUtils +from solidlsp.ls_utils import FileUtils, 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 class ElixirTools(SolidLanguageServer): @@ -68,8 +69,8 @@ class ElixirTools(SolidLanguageServer): return filtered_response - @staticmethod - def _get_elixir_version(): + @classmethod + def _get_elixir_version(cls): """Get the installed Elixir version or None if not found.""" try: result = subprocess.run(["elixir", "--version"], capture_output=True, text=True, check=False) @@ -79,13 +80,16 @@ class ElixirTools(SolidLanguageServer): return None return None - def setupRuntimeDependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> str: + @classmethod + def _setup_runtime_dependencies( + cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings + ) -> str: """ Setup runtime dependencies for Next LS. Downloads the Next LS binary for the current platform and returns the path to the executable. """ # Check if Elixir is available first - elixir_version = self._get_elixir_version() + elixir_version = cls._get_elixir_version() if not elixir_version: raise RuntimeError( "Elixir is not installed. Please install Elixir from https://elixir-lang.org/install.html and make sure it is added to your PATH." @@ -93,70 +97,78 @@ class ElixirTools(SolidLanguageServer): logger.log(f"Found Elixir: {elixir_version}", logging.INFO) - platformId = PlatformUtils.get_platform_id() - - with open(str(PurePath(os.path.dirname(__file__), "runtime_dependencies.json")), encoding="utf-8") as f: - runtimeDependencies = json.load(f) - del runtimeDependencies["_description"] - - os.makedirs(str(PurePath(os.path.abspath(os.path.dirname(__file__)), "static")), exist_ok=True) - - # Map platform IDs to runtime dependency keys - platform_mapping = { - "linux-x64": "linux-x64", - "osx-x64": "darwin-x64", - "osx-arm64": "darwin-arm64", - "darwin-x64": "darwin-x64", - "darwin-arm64": "darwin-arm64", - "win-x64": "win-x64", - } - - platform_key = platform_mapping.get(platformId.value) - if not platform_key: - raise RuntimeError(f"Unsupported platform for Next LS: {platformId.value}") + platform_id = PlatformUtils.get_platform_id() # Check for Windows and provide a helpful error message - if platformId.value.startswith("win"): + if platform_id.value.startswith("win"): raise RuntimeError( "Windows is not supported by Next LS. The Next LS project does not provide Windows binaries. " "Consider using Windows Subsystem for Linux (WSL) or a virtual machine with Linux/macOS." ) - dependency = runtimeDependencies["next_ls"][platform_key] - next_ls_dir = str(PurePath(os.path.abspath(os.path.dirname(__file__)), "static", dependency["relative_extraction_path"])) - os.makedirs(next_ls_dir, exist_ok=True) + valid_platforms = [ + PlatformId.LINUX_x64, + PlatformId.OSX_x64, + PlatformId.OSX_arm64, + ] + assert platform_id in valid_platforms, f"Platform {platform_id} is not supported for Next LS at the moment" - executable_path = str(PurePath(next_ls_dir, dependency["executable_name"])) - binary_path = str(PurePath(next_ls_dir, dependency["binary_name"])) + next_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "next-ls") + + # Define runtime dependencies inline + runtime_deps = { + PlatformId.LINUX_x64: RuntimeDependency( + id="next_ls_linux_amd64", + platform_id="linux-x64", + url="https://github.com/elixir-tools/next-ls/releases/download/v0.23.3/next_ls_linux_amd64", + archive_type="binary", + binary_name="next_ls_linux_amd64", + extract_path="next_ls", + ), + PlatformId.OSX_x64: RuntimeDependency( + id="next_ls_darwin_amd64", + platform_id="osx-x64", + url="https://github.com/elixir-tools/next-ls/releases/download/v0.23.3/next_ls_darwin_amd64", + archive_type="binary", + binary_name="next_ls_darwin_amd64", + extract_path="next_ls", + ), + PlatformId.OSX_arm64: RuntimeDependency( + id="next_ls_darwin_arm64", + platform_id="osx-arm64", + url="https://github.com/elixir-tools/next-ls/releases/download/v0.23.3/next_ls_darwin_arm64", + archive_type="binary", + binary_name="next_ls_darwin_arm64", + extract_path="next_ls", + ), + } + + dependency = runtime_deps[platform_id] + executable_path = os.path.join(next_ls_dir, "nextls") + binary_path = os.path.join(next_ls_dir, dependency.binary_name) if not os.path.exists(executable_path): - logger.log(f"Downloading Next LS binary from {dependency['url']}", logging.INFO) - FileUtils.download_file(logger, dependency["url"], binary_path) + logger.log(f"Downloading Next LS binary from {dependency.url}", logging.INFO) + FileUtils.download_file(logger, dependency.url, binary_path) # Make the binary executable on Unix-like systems - if not platformId.value.startswith("win"): - os.chmod(binary_path, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH) + os.chmod(binary_path, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH) - # Create a symlink or copy with the expected name + # Create a symlink with the expected name if binary_path != executable_path: if os.path.exists(executable_path): os.remove(executable_path) - if platformId.value.startswith("win"): - # On Windows, copy the file - import shutil - - shutil.copy2(binary_path, executable_path) - else: - # On Unix-like systems, create a symlink - os.symlink(os.path.basename(binary_path), executable_path) + os.symlink(os.path.basename(binary_path), executable_path) assert os.path.exists(executable_path), f"Next LS executable not found at {executable_path}" logger.log(f"Next LS binary ready at: {executable_path}", logging.INFO) return executable_path - def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): - nextls_executable_path = self.setupRuntimeDependencies(logger, config) + def __init__( + self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings + ): + nextls_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings) super().__init__( config, @@ -164,6 +176,7 @@ class ElixirTools(SolidLanguageServer): repository_root_path, ProcessLaunchInfo(cmd=f'"{nextls_executable_path}" --stdio', cwd=repository_root_path), "elixir", + solidlsp_settings, ) self.server_ready = threading.Event() self.request_id = 0 @@ -171,29 +184,66 @@ class ElixirTools(SolidLanguageServer): # Set generous timeout for Next LS which can be slow to initialize and respond self.set_request_timeout(180.0) # 60 seconds for all environments - def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: + @staticmethod + def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: """ Returns the initialize params for the Next LS Language Server. """ - with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), encoding="utf-8") as f: - d = json.load(f) + root_uri = pathlib.Path(repository_absolute_path).as_uri() + initialize_params = { + "processId": os.getpid(), + "locale": "en", + "rootPath": repository_absolute_path, + "rootUri": root_uri, + "initializationOptions": { + "mix_env": "dev", + "mix_target": "host", + "experimental": {"completions": {"enable": False}}, + "extensions": {"credo": {"enable": True, "cli_options": []}}, + }, + "capabilities": { + "textDocument": { + "synchronization": {"didSave": True, "dynamicRegistration": True}, + "completion": { + "dynamicRegistration": True, + "completionItem": {"snippetSupport": True, "documentationFormat": ["markdown", "plaintext"]}, + }, + "definition": {"dynamicRegistration": True}, + "references": {"dynamicRegistration": True}, + "documentSymbol": { + "dynamicRegistration": True, + "hierarchicalDocumentSymbolSupport": True, + "symbolKind": {"valueSet": list(range(1, 27))}, + }, + "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]}, + "formatting": {"dynamicRegistration": True}, + "codeAction": { + "dynamicRegistration": True, + "codeActionLiteralSupport": { + "codeActionKind": { + "valueSet": [ + "quickfix", + "refactor", + "refactor.extract", + "refactor.inline", + "refactor.rewrite", + "source", + "source.organizeImports", + ] + } + }, + }, + }, + "workspace": { + "workspaceFolders": True, + "didChangeConfiguration": {"dynamicRegistration": True}, + "executeCommand": {"dynamicRegistration": True}, + }, + }, + "workspaceFolders": [{"uri": root_uri, "name": os.path.basename(repository_absolute_path)}], + } - del d["_description"] - - d["processId"] = os.getpid() - assert d["rootPath"] == "$rootPath" - d["rootPath"] = repository_absolute_path - - assert d["rootUri"] == "$rootUri" - d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() - - assert d["workspaceFolders"][0]["uri"] == "$uri" - d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri() - - assert d["workspaceFolders"][0]["name"] == "$name" - d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) - - return d + return initialize_params def _start_server(self): """Start Next LS server process""" diff --git a/src/solidlsp/language_servers/elixir_tools/initialize_params.json b/src/solidlsp/language_servers/elixir_tools/initialize_params.json deleted file mode 100644 index 51ce266f..00000000 --- a/src/solidlsp/language_servers/elixir_tools/initialize_params.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize", - "processId": "os.getpid()", - "locale": "en", - "rootPath": "$rootPath", - "rootUri": "$rootUri", - "initializationOptions": { - "mix_env": "dev", - "mix_target": "host", - "experimental": { - "completions": { - "enable": false - } - }, - "extensions": { - "credo": { - "enable": true, - "cli_options": [] - } - } - }, - "capabilities": { - "textDocument": { - "synchronization": { - "didSave": true, - "dynamicRegistration": true - }, - "completion": { - "dynamicRegistration": true, - "completionItem": { - "snippetSupport": true, - "documentationFormat": [ - "markdown", - "plaintext" - ] - } - }, - "definition": { - "dynamicRegistration": true - }, - "references": { - "dynamicRegistration": true - }, - "documentSymbol": { - "dynamicRegistration": true, - "hierarchicalDocumentSymbolSupport": true, - "symbolKind": { - "valueSet": [ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, - 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26 - ] - } - }, - "hover": { - "dynamicRegistration": true, - "contentFormat": [ - "markdown", - "plaintext" - ] - }, - "formatting": { - "dynamicRegistration": true - }, - "codeAction": { - "dynamicRegistration": true, - "codeActionLiteralSupport": { - "codeActionKind": { - "valueSet": [ - "quickfix", - "refactor", - "refactor.extract", - "refactor.inline", - "refactor.rewrite", - "source", - "source.organizeImports" - ] - } - } - } - }, - "workspace": { - "workspaceFolders": true, - "didChangeConfiguration": { - "dynamicRegistration": true - }, - "executeCommand": { - "dynamicRegistration": true - } - } - }, - "workspaceFolders": [ - { - "uri": "$uri", - "name": "$name" - } - ] -} \ No newline at end of file diff --git a/src/solidlsp/language_servers/elixir_tools/runtime_dependencies.json b/src/solidlsp/language_servers/elixir_tools/runtime_dependencies.json deleted file mode 100644 index 32641a0d..00000000 --- a/src/solidlsp/language_servers/elixir_tools/runtime_dependencies.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "_description": "This file lists the runtime dependencies for the Elixir Language Server (Next LS)", - "next_ls": { - "linux-x64": { - "url": "https://github.com/elixir-tools/next-ls/releases/download/v0.23.3/next_ls_linux_amd64", - "archiveType": "binary", - "relative_extraction_path": "next_ls", - "binary_name": "next_ls_linux_amd64", - "executable_name": "nextls" - }, - "darwin-x64": { - "url": "https://github.com/elixir-tools/next-ls/releases/download/v0.23.3/next_ls_darwin_amd64", - "archiveType": "binary", - "relative_extraction_path": "next_ls", - "binary_name": "next_ls_darwin_amd64", - "executable_name": "nextls" - }, - "darwin-arm64": { - "url": "https://github.com/elixir-tools/next-ls/releases/download/v0.23.3/next_ls_darwin_arm64", - "archiveType": "binary", - "relative_extraction_path": "next_ls", - "binary_name": "next_ls_darwin_arm64", - "executable_name": "nextls" - }, - "win-x64": { - "url": "https://github.com/elixir-tools/next-ls/releases/download/v0.23.3/next_ls_windows_amd64.exe", - "archiveType": "binary", - "relative_extraction_path": "next_ls", - "binary_name": "next_ls_windows_amd64.exe", - "executable_name": "nextls.exe" - } - } -} \ No newline at end of file diff --git a/src/solidlsp/language_servers/gopls.py b/src/solidlsp/language_servers/gopls.py index 70eb15fa..c21663b7 100644 --- a/src/solidlsp/language_servers/gopls.py +++ b/src/solidlsp/language_servers/gopls.py @@ -11,6 +11,7 @@ from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.settings import SolidLSPSettings class Gopls(SolidLanguageServer): @@ -70,7 +71,9 @@ class Gopls(SolidLanguageServer): return True - def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): + def __init__( + self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings + ): self._setup_runtime_dependency() super().__init__( @@ -79,6 +82,7 @@ class Gopls(SolidLanguageServer): repository_root_path, ProcessLaunchInfo(cmd="gopls", cwd=repository_root_path), "go", + solidlsp_settings, ) self.server_ready = threading.Event() self.request_id = 0 diff --git a/src/solidlsp/language_servers/intelephense.py b/src/solidlsp/language_servers/intelephense.py index f9c83976..2d1d48e2 100644 --- a/src/solidlsp/language_servers/intelephense.py +++ b/src/solidlsp/language_servers/intelephense.py @@ -16,6 +16,7 @@ from solidlsp.ls_logger import LanguageServerLogger from solidlsp.ls_utils import PlatformId, PlatformUtils from solidlsp.lsp_protocol_handler.lsp_types import DefinitionParams, InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.settings import SolidLSPSettings from .common import RuntimeDependency, RuntimeDependencyCollection @@ -34,7 +35,9 @@ class Intelephense(SolidLanguageServer): return super().is_ignored_dirname(dirname) or dirname in ["node_modules", "vendor", "cache"] @classmethod - def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> str: + def _setup_runtime_dependencies( + cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings + ) -> str: """ Setup runtime dependencies for Intelephense and return the command to start the server. """ @@ -58,7 +61,7 @@ class Intelephense(SolidLanguageServer): assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again." # Install intelephense if not already installed - intelephense_ls_dir = os.path.join(cls.ls_resources_dir(), "php-lsp") + intelephense_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "php-lsp") os.makedirs(intelephense_ls_dir, exist_ok=True) intelephense_executable_path = os.path.join(intelephense_ls_dir, "node_modules", ".bin", "intelephense") if not os.path.exists(intelephense_executable_path): @@ -79,11 +82,20 @@ class Intelephense(SolidLanguageServer): return f"{intelephense_executable_path} --stdio" - def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): + def __init__( + self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings + ): # Setup runtime dependencies before initializing - intelephense_cmd = self._setup_runtime_dependencies(logger, config) + intelephense_cmd = self._setup_runtime_dependencies(logger, config, solidlsp_settings) - super().__init__(config, logger, repository_root_path, ProcessLaunchInfo(cmd=intelephense_cmd, cwd=repository_root_path), "php") + super().__init__( + config, + logger, + repository_root_path, + ProcessLaunchInfo(cmd=intelephense_cmd, cwd=repository_root_path), + "php", + solidlsp_settings, + ) self.request_id = 0 @staticmethod diff --git a/src/solidlsp/language_servers/jedi_server.py b/src/solidlsp/language_servers/jedi_server.py index 161ce754..956e00b9 100644 --- a/src/solidlsp/language_servers/jedi_server.py +++ b/src/solidlsp/language_servers/jedi_server.py @@ -13,6 +13,7 @@ from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.settings import SolidLSPSettings class JediServer(SolidLanguageServer): @@ -20,7 +21,9 @@ class JediServer(SolidLanguageServer): Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python. """ - def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): + def __init__( + self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings + ): """ Creates a JediServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ @@ -30,6 +33,7 @@ class JediServer(SolidLanguageServer): repository_root_path, ProcessLaunchInfo(cmd="jedi-language-server", cwd=repository_root_path), "python", + solidlsp_settings, ) @override diff --git a/src/solidlsp/language_servers/kotlin_language_server.py b/src/solidlsp/language_servers/kotlin_language_server.py index ef2a77ca..5d0d2083 100644 --- a/src/solidlsp/language_servers/kotlin_language_server.py +++ b/src/solidlsp/language_servers/kotlin_language_server.py @@ -14,6 +14,7 @@ from solidlsp.ls_logger import LanguageServerLogger from solidlsp.ls_utils import FileUtils, PlatformUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.settings import SolidLSPSettings @dataclasses.dataclass @@ -32,11 +33,13 @@ class KotlinLanguageServer(SolidLanguageServer): Provides Kotlin specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Kotlin. """ - def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): + def __init__( + self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings + ): """ Creates a Kotlin Language Server instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - runtime_dependency_paths = self._setup_runtime_dependencies(logger, config) + runtime_dependency_paths = self._setup_runtime_dependencies(logger, config, solidlsp_settings) self.runtime_dependency_paths = runtime_dependency_paths # Create command to execute the Kotlin Language Server script @@ -51,10 +54,13 @@ class KotlinLanguageServer(SolidLanguageServer): repository_root_path, ProcessLaunchInfo(cmd=cmd, env=proc_env, cwd=repository_root_path), "kotlin", + solidlsp_settings, ) @classmethod - def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> KotlinRuntimeDependencyPaths: + def _setup_runtime_dependencies( + cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings + ) -> KotlinRuntimeDependencyPaths: """ Setup runtime dependencies for Kotlin Language Server and return the paths. """ @@ -111,7 +117,7 @@ class KotlinLanguageServer(SolidLanguageServer): java_dependency = runtime_dependencies["java"][platform_id.value] # Setup paths for dependencies - static_dir = os.path.join(cls.ls_resources_dir(), "kotlin_language_server") + static_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "kotlin_language_server") os.makedirs(static_dir, exist_ok=True) # Setup Java paths diff --git a/src/solidlsp/language_servers/omnisharp.py b/src/solidlsp/language_servers/omnisharp.py index eafaa310..a23a58a8 100644 --- a/src/solidlsp/language_servers/omnisharp.py +++ b/src/solidlsp/language_servers/omnisharp.py @@ -19,6 +19,7 @@ from solidlsp.ls_logger import LanguageServerLogger from solidlsp.ls_utils import DotnetVersion, FileUtils, PlatformId, PlatformUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.settings import SolidLSPSettings def breadth_first_file_scan(root) -> Iterable[str]: @@ -59,11 +60,13 @@ class OmniSharp(SolidLanguageServer): Provides C# specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C#. """ - def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): + def __init__( + self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings + ): """ Creates an OmniSharp instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - omnisharp_executable_path, dll_path = self._setup_runtime_dependencies(logger, config) + omnisharp_executable_path, dll_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings) slnfilename = find_least_depth_sln_file(repository_root_path) if slnfilename is None: @@ -102,7 +105,9 @@ class OmniSharp(SolidLanguageServer): "formattingOptions:indentationSize=4", ] ) - super().__init__(config, logger, repository_root_path, ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path), "csharp") + super().__init__( + config, logger, repository_root_path, ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path), "csharp", solidlsp_settings + ) self.server_ready = threading.Event() self.definition_available = threading.Event() @@ -138,7 +143,9 @@ class OmniSharp(SolidLanguageServer): return d @classmethod - def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> tuple[str, str]: + def _setup_runtime_dependencies( + cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings + ) -> tuple[str, str]: """ Setup runtime dependencies for OmniSharp. """ @@ -181,7 +188,7 @@ class OmniSharp(SolidLanguageServer): assert "OmniSharp" in runtime_dependencies assert "RazorOmnisharp" in runtime_dependencies - omnisharp_ls_dir = os.path.join(cls.ls_resources_dir(), "OmniSharp") + omnisharp_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "OmniSharp") if not os.path.exists(omnisharp_ls_dir): os.makedirs(omnisharp_ls_dir) FileUtils.download_and_extract_archive(logger, runtime_dependencies["OmniSharp"]["url"], omnisharp_ls_dir, "zip") @@ -189,7 +196,7 @@ class OmniSharp(SolidLanguageServer): assert os.path.exists(omnisharp_executable_path) os.chmod(omnisharp_executable_path, stat.S_IEXEC) - razor_omnisharp_ls_dir = os.path.join(cls.ls_resources_dir(), "RazorOmnisharp") + razor_omnisharp_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "RazorOmnisharp") if not os.path.exists(razor_omnisharp_ls_dir): os.makedirs(razor_omnisharp_ls_dir) FileUtils.download_and_extract_archive(logger, runtime_dependencies["RazorOmnisharp"]["url"], razor_omnisharp_ls_dir, "zip") diff --git a/src/solidlsp/language_servers/pyright_server.py b/src/solidlsp/language_servers/pyright_server.py index 905bb3d6..c760ebc1 100644 --- a/src/solidlsp/language_servers/pyright_server.py +++ b/src/solidlsp/language_servers/pyright_server.py @@ -15,6 +15,7 @@ from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.settings import SolidLSPSettings class PyrightServer(SolidLanguageServer): @@ -23,7 +24,9 @@ class PyrightServer(SolidLanguageServer): Contains various configurations and settings specific to Python. """ - def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): + def __init__( + self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings + ): """ Creates a PyrightServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. @@ -36,6 +39,7 @@ class PyrightServer(SolidLanguageServer): # Note 2: we can also use `bpyright-langserver --stdio` if we ever are unhappy with pyright ProcessLaunchInfo(cmd="python -m pyright.langserver --stdio", cwd=repository_root_path), "python", + solidlsp_settings, ) # Event to signal when initial workspace analysis is complete diff --git a/src/solidlsp/language_servers/rust_analyzer.py b/src/solidlsp/language_servers/rust_analyzer.py index 0f2f2cf1..48bae28a 100644 --- a/src/solidlsp/language_servers/rust_analyzer.py +++ b/src/solidlsp/language_servers/rust_analyzer.py @@ -15,6 +15,7 @@ from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger 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 @@ -24,17 +25,20 @@ class RustAnalyzer(SolidLanguageServer): Provides Rust specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Rust. """ - def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): + def __init__( + self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings + ): """ Creates a RustAnalyzer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - rustanalyzer_executable_path = self._setup_runtime_dependencies(logger, config) + rustanalyzer_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings) super().__init__( config, logger, repository_root_path, ProcessLaunchInfo(cmd=rustanalyzer_executable_path, cwd=repository_root_path), "rust", + solidlsp_settings, ) self.server_ready = threading.Event() self.service_ready_event = threading.Event() @@ -46,7 +50,9 @@ class RustAnalyzer(SolidLanguageServer): return super().is_ignored_dirname(dirname) or dirname in ["target"] @classmethod - def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> str: + def _setup_runtime_dependencies( + cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings + ) -> str: """ Setup runtime dependencies for rust_analyzer and return the command to start the server. """ @@ -92,7 +98,7 @@ class RustAnalyzer(SolidLanguageServer): # "win-x64", # ], "Only linux-x64 and win-x64 platform is supported for in multilspy at the moment" - rustanalyzer_ls_dir = os.path.join(cls.ls_resources_dir(), "RustAnalyzer") + rustanalyzer_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "RustAnalyzer") rustanalyzer_executable_path = deps.binary_path(rustanalyzer_ls_dir) if not os.path.exists(rustanalyzer_ls_dir): os.makedirs(rustanalyzer_ls_dir) diff --git a/src/solidlsp/language_servers/solargraph.py b/src/solidlsp/language_servers/solargraph.py index 8034ad80..56a1b7f7 100644 --- a/src/solidlsp/language_servers/solargraph.py +++ b/src/solidlsp/language_servers/solargraph.py @@ -18,6 +18,7 @@ from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.settings import SolidLSPSettings class Solargraph(SolidLanguageServer): @@ -26,7 +27,9 @@ class Solargraph(SolidLanguageServer): Contains various configurations and settings specific to Ruby. """ - def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): + def __init__( + self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings + ): """ Creates a Solargraph instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. @@ -38,6 +41,7 @@ class Solargraph(SolidLanguageServer): repository_root_path, ProcessLaunchInfo(cmd=f"{solargraph_executable_path} stdio", cwd=repository_root_path), "ruby", + solidlsp_settings, ) self.server_ready = threading.Event() self.service_ready_event = threading.Event() diff --git a/src/solidlsp/language_servers/terraform_ls.py b/src/solidlsp/language_servers/terraform_ls.py index 5a230a23..e40d9af1 100644 --- a/src/solidlsp/language_servers/terraform_ls.py +++ b/src/solidlsp/language_servers/terraform_ls.py @@ -12,6 +12,7 @@ from solidlsp.ls_logger import LanguageServerLogger from solidlsp.ls_utils import PathUtils, 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 @@ -57,7 +58,7 @@ class TerraformLS(SolidLanguageServer): ) @classmethod - def _setup_runtime_dependencies(cls, logger: LanguageServerLogger) -> str: + def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, solidlsp_settings: SolidLSPSettings) -> str: """ Setup runtime dependencies for terraform-ls. Downloads and installs terraform-ls if not already present. @@ -102,10 +103,10 @@ class TerraformLS(SolidLanguageServer): ) dependency = deps.single_for_current_platform() - terraform_ls_executable_path = deps.binary_path(cls.ls_resources_dir()) + terraform_ls_executable_path = deps.binary_path(cls.ls_resources_dir(solidlsp_settings)) if not os.path.exists(terraform_ls_executable_path): logger.log(f"Downloading terraform-ls from {dependency.url}", logging.INFO) - deps.install(logger, cls.ls_resources_dir()) + deps.install(logger, cls.ls_resources_dir(solidlsp_settings)) assert os.path.exists(terraform_ls_executable_path), f"terraform-ls executable not found at {terraform_ls_executable_path}" @@ -115,11 +116,13 @@ class TerraformLS(SolidLanguageServer): return terraform_ls_executable_path - def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): + def __init__( + self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings + ): """ Creates a TerraformLS instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - terraform_ls_executable_path = self._setup_runtime_dependencies(logger) + terraform_ls_executable_path = self._setup_runtime_dependencies(logger, solidlsp_settings) super().__init__( config, @@ -127,6 +130,7 @@ class TerraformLS(SolidLanguageServer): repository_root_path, ProcessLaunchInfo(cmd=f"{terraform_ls_executable_path} serve", cwd=repository_root_path), "terraform", + solidlsp_settings, ) self.server_ready = threading.Event() self.request_id = 0 diff --git a/src/solidlsp/language_servers/typescript_language_server.py b/src/solidlsp/language_servers/typescript_language_server.py index 48bcf444..7b700deb 100644 --- a/src/solidlsp/language_servers/typescript_language_server.py +++ b/src/solidlsp/language_servers/typescript_language_server.py @@ -18,6 +18,7 @@ from solidlsp.ls_logger import LanguageServerLogger 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 @@ -42,17 +43,20 @@ class TypeScriptLanguageServer(SolidLanguageServer): Provides TypeScript specific instantiation of the LanguageServer class. Contains various configurations and settings specific to TypeScript. """ - def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): + def __init__( + self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings + ): """ 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(logger, config) + ts_lsp_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings) super().__init__( config, logger, repository_root_path, ProcessLaunchInfo(cmd=ts_lsp_executable_path, cwd=repository_root_path), "typescript", + solidlsp_settings, ) self.server_ready = threading.Event() self.initialize_searcher_command_available = threading.Event() @@ -67,7 +71,9 @@ class TypeScriptLanguageServer(SolidLanguageServer): ] @classmethod - def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> str: + def _setup_runtime_dependencies( + cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings + ) -> str: """ Setup runtime dependencies for TypeScript Language Server and return the command to start the server. """ @@ -114,7 +120,7 @@ class TypeScriptLanguageServer(SolidLanguageServer): 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 - tsserver_ls_dir = os.path.join(cls.ls_resources_dir(), "ts-lsp") + 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") if not os.path.exists(tsserver_executable_path): logger.log(f"Typescript Language Server executable not found at {tsserver_executable_path}. Installing...", logging.INFO) diff --git a/src/solidlsp/language_servers/vts_language_server.py b/src/solidlsp/language_servers/vts_language_server.py index 40de7b55..c8596b89 100644 --- a/src/solidlsp/language_servers/vts_language_server.py +++ b/src/solidlsp/language_servers/vts_language_server.py @@ -19,6 +19,7 @@ from solidlsp.ls_logger import LanguageServerLogger 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 @@ -29,17 +30,20 @@ class VtsLanguageServer(SolidLanguageServer): Contains various configurations and settings specific to TypeScript via vtsls wrapper. """ - def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): + def __init__( + self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings + ): """ Creates a VtsLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - vts_lsp_executable_path = self._setup_runtime_dependencies(logger, config) + vts_lsp_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings) super().__init__( config, logger, repository_root_path, ProcessLaunchInfo(cmd=vts_lsp_executable_path, cwd=repository_root_path), "typescript", + solidlsp_settings, ) self.server_ready = threading.Event() self.initialize_searcher_command_available = threading.Event() @@ -54,7 +58,9 @@ class VtsLanguageServer(SolidLanguageServer): ] @classmethod - def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> str: + def _setup_runtime_dependencies( + cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings + ) -> str: """ Setup runtime dependencies for VTS Language Server and return the command to start the server. """ @@ -81,7 +87,7 @@ class VtsLanguageServer(SolidLanguageServer): ), ] ) - vts_ls_dir = os.path.join(cls.ls_resources_dir(), "vts-lsp") + vts_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "vts-lsp") vts_executable_path = os.path.join(vts_ls_dir, "vtsls") # Verify both node and npm are installed diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 7cb82d72..47fe6813 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -5,6 +5,7 @@ import logging import os import pathlib import pickle +import shutil import subprocess import threading from abc import ABC, abstractmethod @@ -34,6 +35,7 @@ from solidlsp.lsp_protocol_handler.server import ( ProcessLaunchInfo, StringDict, ) +from solidlsp.settings import SolidLSPSettings GenericDocumentSymbol = Union[LSPTypes.DocumentSymbol, LSPTypes.SymbolInformation, ls_types.UnifiedSymbolInformation] @@ -89,19 +91,34 @@ class SolidLanguageServer(ABC): return dirname.startswith(".") @classmethod - def ls_resources_dir(cls, mkdir: bool = True) -> str: + def ls_resources_dir(cls, solidlsp_settings: SolidLSPSettings, mkdir: bool = True) -> str: """ Returns the directory where the language server resources are downloaded. This is used to store language server binaries, configuration files, etc. """ - result = os.path.join(os.path.dirname(__file__), "language_servers", "static", cls.__name__) + result = os.path.join(solidlsp_settings.ls_resources_dir, cls.__name__) + + # Migration of previously downloaded LS resources that were downloaded to a subdir of solidlsp instead of to the user's home + pre_migration_ls_resources_dir = os.path.join(os.path.dirname(__file__), "language_servers", "static", cls.__name__) + if os.path.exists(pre_migration_ls_resources_dir): + if os.path.exists(result): + # if the directory already exists, we just remove the old resources + shutil.rmtree(result, ignore_errors=True) + else: + # move old resources to the new location + shutil.move(pre_migration_ls_resources_dir, result) if mkdir: os.makedirs(result, exist_ok=True) return result @classmethod def create( - cls, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, timeout: float | None = None + cls, + config: LanguageServerConfig, + logger: LanguageServerLogger, + repository_root_path: str, + timeout: float | None = None, + solidlsp_settings: SolidLSPSettings | None = None, ) -> "SolidLanguageServer": """ Creates a language specific LanguageServer instance based on the given configuration, and appropriate settings for the programming language. @@ -116,96 +133,98 @@ class SolidLanguageServer(ABC): :return LanguageServer: A language specific LanguageServer instance. """ ls: SolidLanguageServer + if solidlsp_settings is None: + solidlsp_settings = SolidLSPSettings() if config.code_language == Language.PYTHON: from solidlsp.language_servers.pyright_server import ( PyrightServer, ) - ls = PyrightServer(config, logger, repository_root_path) + ls = PyrightServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings) elif config.code_language == Language.PYTHON_JEDI: from solidlsp.language_servers.jedi_server import JediServer - ls = JediServer(config, logger, repository_root_path) + ls = JediServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings) elif config.code_language == Language.JAVA: from solidlsp.language_servers.eclipse_jdtls import ( EclipseJDTLS, ) - ls = EclipseJDTLS(config, logger, repository_root_path) + ls = EclipseJDTLS(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings) elif config.code_language == Language.KOTLIN: from solidlsp.language_servers.kotlin_language_server import ( KotlinLanguageServer, ) - ls = KotlinLanguageServer(config, logger, repository_root_path) + ls = KotlinLanguageServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings) elif config.code_language == Language.RUST: from solidlsp.language_servers.rust_analyzer import ( RustAnalyzer, ) - ls = RustAnalyzer(config, logger, repository_root_path) + ls = RustAnalyzer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings) elif config.code_language == Language.CSHARP: from solidlsp.language_servers.csharp_language_server import CSharpLanguageServer - ls = CSharpLanguageServer(config, logger, repository_root_path) + ls = CSharpLanguageServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings) elif config.code_language == Language.CSHARP_OMNISHARP: from solidlsp.language_servers.omnisharp import OmniSharp - ls = OmniSharp(config, logger, repository_root_path) + ls = OmniSharp(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings) elif config.code_language == Language.TYPESCRIPT: from solidlsp.language_servers.typescript_language_server import ( TypeScriptLanguageServer, ) - ls = TypeScriptLanguageServer(config, logger, repository_root_path) + ls = TypeScriptLanguageServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings) elif config.code_language == Language.TYPESCRIPT_VTS: # VTS based Language Server implementation, need to experiment to see if it improves performance from solidlsp.language_servers.vts_language_server import VtsLanguageServer - ls = VtsLanguageServer(config, logger, repository_root_path) + ls = VtsLanguageServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings) elif config.code_language == Language.GO: from solidlsp.language_servers.gopls import Gopls - ls = Gopls(config, logger, repository_root_path) + ls = Gopls(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings) elif config.code_language == Language.RUBY: from solidlsp.language_servers.solargraph import Solargraph - ls = Solargraph(config, logger, repository_root_path) + ls = Solargraph(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings) elif config.code_language == Language.DART: from solidlsp.language_servers.dart_language_server import DartLanguageServer - ls = DartLanguageServer(config, logger, repository_root_path) + ls = DartLanguageServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings) elif config.code_language == Language.CPP: from solidlsp.language_servers.clangd_language_server import ClangdLanguageServer - ls = ClangdLanguageServer(config, logger, repository_root_path) + ls = ClangdLanguageServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings) elif config.code_language == Language.PHP: from solidlsp.language_servers.intelephense import Intelephense - ls = Intelephense(config, logger, repository_root_path) + ls = Intelephense(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings) elif config.code_language == Language.CLOJURE: from solidlsp.language_servers.clojure_lsp import ClojureLSP - ls = ClojureLSP(config, logger, repository_root_path) + ls = ClojureLSP(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings) elif config.code_language == Language.ELIXIR: from solidlsp.language_servers.elixir_tools.elixir_tools import ElixirTools - ls = ElixirTools(config, logger, repository_root_path) + ls = ElixirTools(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings) elif config.code_language == Language.TERRAFORM: from solidlsp.language_servers.terraform_ls import TerraformLS - ls = TerraformLS(config, logger, repository_root_path) + ls = TerraformLS(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings) else: logger.log(f"Language {config.code_language} is not supported", logging.ERROR) @@ -221,6 +240,7 @@ class SolidLanguageServer(ABC): repository_root_path: str, process_launch_info: ProcessLaunchInfo, language_id: str, + solidlsp_settings: SolidLSPSettings, ): """ Initializes a LanguageServer instance. @@ -235,6 +255,7 @@ class SolidLanguageServer(ABC): 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. """ + self._solidlsp_settings = solidlsp_settings self.logger = logger self.repository_root_path: str = repository_root_path self.logger.log( diff --git a/src/solidlsp/ls_utils.py b/src/solidlsp/ls_utils.py index d1161880..bfe02216 100644 --- a/src/solidlsp/ls_utils.py +++ b/src/solidlsp/ls_utils.py @@ -183,6 +183,7 @@ class FileUtils: """ Downloads the file from the given URL to the given {target_path} """ + os.makedirs(os.path.dirname(target_path), exist_ok=True) try: response = requests.get(url, stream=True, timeout=60) if response.status_code != 200: diff --git a/src/solidlsp/settings.py b/src/solidlsp/settings.py index 38b30102..0b956bff 100644 --- a/src/solidlsp/settings.py +++ b/src/solidlsp/settings.py @@ -4,21 +4,17 @@ Defines settings for Solid-LSP import os import pathlib +from dataclasses import dataclass +@dataclass class SolidLSPSettings: - @staticmethod - def get_language_server_directory() -> str: - """Returns the directory for language servers""" - user_home = pathlib.Path.home() - multilspy_dir = str(pathlib.PurePath(user_home, ".multilspy")) - lsp_dir = str(pathlib.PurePath(multilspy_dir, "lsp")) - os.makedirs(lsp_dir, exist_ok=True) - return lsp_dir + solidlsp_dir: str = str(pathlib.Path.home() / ".solidlsp") - @staticmethod - def get_global_cache_directory() -> str: - """Returns the cache directory""" - global_cache_dir = os.path.join(str(pathlib.Path.home()), ".multilspy", "global_cache") - os.makedirs(global_cache_dir, exist_ok=True) - return global_cache_dir + def __post_init__(self): + os.makedirs(str(self.solidlsp_dir), exist_ok=True) + os.makedirs(str(self.ls_resources_dir), exist_ok=True) + + @property + def ls_resources_dir(self): + return os.path.join(str(self.solidlsp_dir), "language_servers", "static") diff --git a/test/conftest.py b/test/conftest.py index 90e522d0..849af543 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -4,11 +4,13 @@ from pathlib import Path import pytest from sensai.util.logging import configure +from serena.constants import SERENA_MANAGED_DIR_IN_HOME from serena.project import Project from serena.util.file_system import GitignoreParser from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import Language, LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.settings import SolidLSPSettings configure(level=logging.ERROR) @@ -43,7 +45,9 @@ def create_ls( ignored_paths.extend(spec.patterns) config = LanguageServerConfig(code_language=language, ignored_paths=ignored_paths, trace_lsp_communication=trace_lsp_communication) logger = LanguageServerLogger(log_level=log_level) - return SolidLanguageServer.create(config, logger, repo_path) + return SolidLanguageServer.create( + config, logger, repo_path, solidlsp_settings=SolidLSPSettings(solidlsp_dir=SERENA_MANAGED_DIR_IN_HOME) + ) def create_default_ls(language: Language) -> SolidLanguageServer: diff --git a/test/solidlsp/csharp/test_csharp_basic.py b/test/solidlsp/csharp/test_csharp_basic.py index 59f393ac..89d3caea 100644 --- a/test/solidlsp/csharp/test_csharp_basic.py +++ b/test/solidlsp/csharp/test_csharp_basic.py @@ -13,6 +13,7 @@ from solidlsp.language_servers.csharp_language_server import ( ) from solidlsp.ls_config import Language, LanguageServerConfig from solidlsp.ls_utils import SymbolUtils +from solidlsp.settings import SolidLSPSettings @pytest.mark.csharp @@ -214,7 +215,9 @@ class TestCSharpSolutionProjectOpening: mock_config.ignored_paths = [] # Create CSharpLanguageServer instance - CSharpLanguageServer(mock_config, mock_logger, str(temp_path)) + mock_settings = Mock(spec=SolidLSPSettings) + mock_settings.ls_resources_dir = "/tmp/test_ls_resources" + CSharpLanguageServer(mock_config, mock_logger, str(temp_path), mock_settings) # Verify that logger was called with solution file discovery mock_logger.log.assert_any_call(f"Found solution/project file: {solution_file}", 20) # logging.INFO @@ -236,7 +239,9 @@ class TestCSharpSolutionProjectOpening: mock_config.ignored_paths = [] # Create CSharpLanguageServer instance - CSharpLanguageServer(mock_config, mock_logger, str(temp_path)) + mock_settings = Mock(spec=SolidLSPSettings) + mock_settings.ls_resources_dir = "/tmp/test_ls_resources" + CSharpLanguageServer(mock_config, mock_logger, str(temp_path), mock_settings) # Verify that logger was called with warning about no solution/project files mock_logger.log.assert_any_call(