diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index e510b722..cf3b93a4 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -14,8 +14,6 @@ jobs: cpu: name: Tests on ${{ matrix.os }} runs-on: ${{ matrix.os }} - timeout-minutes: 15 - if: "!contains(github.event.head_commit.message, 'ci skip')" strategy: fail-fast: false matrix: @@ -33,9 +31,25 @@ jobs: # Add Go bin directory to PATH for this workflow # GITHUB_PATH is a special file that GitHub Actions uses to modify PATH # Writing to this file adds the directory to the PATH for subsequent steps + - name: Cache Go binaries + id: cache-go-binaries + uses: actions/cache@v3 + with: + path: | + ~/go/bin + ~/.cache/go-build + key: go-binaries-${{ runner.os }}-gopls-latest - name: Install gopls + if: steps.cache-go-binaries.outputs.cache-hit != 'true' shell: bash run: go install golang.org/x/tools/gopls@latest + - name: Set up Elixir + if: runner.os != 'Windows' + uses: erlef/setup-beam@v1 + with: + elixir-version: '1.18.4' + otp-version: '26.1' + - name: Prepare java uses: actions/setup-java@v3 with: @@ -45,6 +59,11 @@ jobs: uses: DeLaGuardo/setup-clojure@13.4 with: cli: latest + - name: Install Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: "1.5.0" + terraform_wrapper: false - name: Install uv shell: bash run: curl -LsSf https://astral.sh/uv/install.sh | sh @@ -54,6 +73,14 @@ jobs: with: path: .venv key: uv-venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('uv.lock') }} + - name: Cache language servers + id: cache-language-servers + uses: actions/cache@v3 + with: + path: src/solidlsp/language_servers/static + key: language-servers-${{ runner.os }}-v1 + restore-keys: | + language-servers-${{ runner.os }}- - name: Create virtual environment shell: bash run: | diff --git a/.gitignore b/.gitignore index fabbe519..64c7e71e 100644 --- a/.gitignore +++ b/.gitignore @@ -204,6 +204,7 @@ pylint.html # dynamic LS installations /src/multilspy/language_servers/*/static /src/solidlsp/language_servers/*/static +/src/solidlsp/language_servers/static # clojure-lsp temporary files .calva/ @@ -221,3 +222,8 @@ tmp/ # Claude settings .claude/settings.local.json + +# Elixir +/test/resources/repos/elixir/test_repo/deps +# Exception: Don't ignore Elixir test repository lib directory (contains source code) +!/test/resources/repos/elixir/test_repo/lib diff --git a/.vscode/settings.json b/.vscode/settings.json index 56a7855f..6c3ee2ca 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,4 +11,4 @@ "sensai", "vibing" ], -} \ No newline at end of file +} diff --git a/README.md b/README.md index 15c7056c..7ec55dae 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ With Serena, we provide * Rust * C# (requires dotnet to be installed. We switched the underlying language server recently, please report any issues you encounter) * Java (_Note_: startup is slow, initial startup especially so. There may be issues with java on macos and linux, we are working on it.) + * Elixir (Requires NextLS and Elixir install; **Windows not supported** - Next LS does not provide Windows binaries) * Clojure * C/C++ (You may experience issues with finding references, we are working on it) * indirect support (may require some code changes/manual installation) for: @@ -353,9 +354,8 @@ For example, when using `uvx`, you would run claude mcp add serena -- uvx --from git+https://github.com/oraios/serena serena-mcp-server --context ide-assistant --project $(pwd) ``` -ℹ️ Once in Claude Code, you should ask Claude to "Read the initial instructions" as your first prompt, such that it will receive information -on how to use Serena's tools. Provide this prompt whenever you start a new conversation and perhaps even after a compacting operation, -such that Claude is correctly primed to use Serena's tools at all times. +ℹ️ Once in Claude Code, run `/mcp__serena__initial_instructions` to load instructions for using Serena's tools. Run this command +whenever you start a new conversation and after any compacting operation to ensure Claude remains properly configured to use Serena's tools. ### Claude Desktop @@ -857,3 +857,4 @@ Here is the full list of Serena's tools with a short description (output of `uv * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. + diff --git a/pyproject.toml b/pyproject.toml index 7eacf741..d1bff6bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -248,6 +248,8 @@ markers = [ "typescript: language server running for TypeScript", "php: language server running for PHP", "csharp: language server running for C#", + "elixir: language server running for Elixir", + "terraform: language server running for Terraform", "snapshot: snapshot tests for symbolic editing operations", ] diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py index bffad291..3bf7afea 100644 --- a/src/serena/config/serena_config.py +++ b/src/serena/config/serena_config.py @@ -161,9 +161,14 @@ class ProjectConfig(ToolInclusionDefinition, ToStringMixin): language_composition = determine_programming_language_composition(str(project_root)) if len(language_composition) == 0: raise ValueError( - f"Failed to autogenerate project.yaml: no programming language detected in project {project_root}. " - f"You can either add some files that correspond to one of the supported programming languages, " - f"or create the file {os.path.join(project_root, cls.rel_path_to_project_yml())} manually and specify the language there." + f"No source files found in {project_root}\n\n" + f"To use Serena with this project, you need to either:\n" + f"1. Add source files in one of the supported languages (Python, JavaScript/TypeScript, Java, C#, Rust, Go, Ruby, C++, PHP)\n" + f"2. Create a project configuration file manually at:\n" + f" {os.path.join(project_root, cls.rel_path_to_project_yml())}\n\n" + f"Example project.yml:\n" + f" project_name: {project_name}\n" + f" language: python # or typescript, java, csharp, rust, go, ruby, cpp, php\n" ) # find the language with the highest percentage dominant_language = max(language_composition.keys(), key=lambda lang: language_composition[lang]) diff --git a/src/serena/symbol.py b/src/serena/symbol.py index 5a85bb53..6aa76fbf 100644 --- a/src/serena/symbol.py +++ b/src/serena/symbol.py @@ -191,8 +191,11 @@ class SymbolLocation: if self.relative_path is not None: self.relative_path = self.relative_path.replace("/", os.path.sep) - def to_dict(self) -> dict[str, Any]: - return asdict(self) + def to_dict(self, include_relative_path: bool = True) -> dict[str, Any]: + result = asdict(self) + if not include_relative_path: + result.pop("relative_path", None) + return result def has_position_in_file(self) -> bool: return self.relative_path is not None and self.line is not None and self.column is not None @@ -489,7 +492,13 @@ class Symbol(AbstractSymbol, ToStringMixin): return result def to_dict( - self, kind: bool = False, location: bool = False, depth: int = 0, include_body: bool = False, include_children_body: bool = False + self, + kind: bool = False, + location: bool = False, + depth: int = 0, + include_body: bool = False, + include_children_body: bool = False, + include_relative_path=True, ) -> dict[str, Any]: """ Converts the symbol to a dictionary. @@ -502,6 +511,8 @@ class Symbol(AbstractSymbol, ToStringMixin): Note that the body of the children is part of the body of the parent symbol, so there is usually no need to set this to True unless you want process the output and pass the children without passing the parent body to the LM. + :param include_relative_path: whether to include the relative path of the symbol in the location + entry. Relative paths of the symbol's children are always excluded. :return: a dictionary representation of the symbol """ result: dict[str, Any] = {"name": self.name, "name_path": self.get_name_path()} @@ -510,7 +521,7 @@ class Symbol(AbstractSymbol, ToStringMixin): result["kind"] = self.kind if location: - result["location"] = self.location.to_dict() + result["location"] = self.location.to_dict(include_relative_path=include_relative_path) body_start_line, body_end_line = self.get_body_line_numbers() result["body_location"] = {"start_line": body_start_line, "end_line": body_end_line} @@ -529,6 +540,8 @@ class Symbol(AbstractSymbol, ToStringMixin): depth=depth - 1, include_body=include_children_body, include_children_body=include_children_body, + # all children have the same relative path as the parent + include_relative_path=False, ) ) return children diff --git a/src/serena/util/exception.py b/src/serena/util/exception.py index af194314..8ab2f007 100644 --- a/src/serena/util/exception.py +++ b/src/serena/util/exception.py @@ -1,8 +1,45 @@ +import os import sys from serena.agent import log +def is_headless_environment() -> bool: + """ + Detect if we're running in a headless environment where GUI operations would fail. + + Returns True if: + - No DISPLAY variable on Linux/Unix + - Running in SSH session + - Running in WSL without X server + - Running in Docker container + """ + # Check if we're on Windows - GUI usually works there + if sys.platform == "win32": + return False + + # Check for DISPLAY variable (required for X11) + if not os.environ.get("DISPLAY"): + return True + + # Check for SSH session + if os.environ.get("SSH_CONNECTION") or os.environ.get("SSH_CLIENT"): + return True + + # Check for common CI/container environments + if os.environ.get("CI") or os.environ.get("CONTAINER") or os.path.exists("/.dockerenv"): + return True + + # Check for WSL (only on Unix-like systems where os.uname exists) + if hasattr(os, "uname"): + if "microsoft" in os.uname().release.lower(): + # In WSL, even with DISPLAY set, X server might not be running + # This is a simplified check - could be improved + return True + + return False + + def show_fatal_exception_safe(e: Exception) -> None: """ Shows the given exception in the GUI log viewer on the main thread and ensures that the exception is logged or at @@ -12,6 +49,11 @@ def show_fatal_exception_safe(e: Exception) -> None: log.error(f"Fatal exception: {e}", exc_info=e) print(f"Fatal exception: {e}", file=sys.stderr) + # Don't attempt GUI in headless environments + if is_headless_environment(): + log.debug("Skipping GUI error display in headless environment") + return + # attempt to show the error in the GUI try: # NOTE: The import can fail on macOS if Tk is not available (depends on Python interpreter installation, which uv @@ -19,5 +61,5 @@ def show_fatal_exception_safe(e: Exception) -> None: from serena.gui_log_viewer import show_fatal_exception show_fatal_exception(e) - except: - pass + except Exception as gui_error: + log.debug(f"Failed to show GUI error dialog: {gui_error}") diff --git a/src/solidlsp/.gitignore b/src/solidlsp/.gitignore new file mode 100644 index 00000000..c6d33567 --- /dev/null +++ b/src/solidlsp/.gitignore @@ -0,0 +1 @@ +language_servers/static \ No newline at end of file diff --git a/src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py b/src/solidlsp/language_servers/clangd_language_server.py similarity index 64% rename from src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py rename to src/solidlsp/language_servers/clangd_language_server.py index 893c5cc7..b8845d84 100644 --- a/src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py +++ b/src/solidlsp/language_servers/clangd_language_server.py @@ -2,7 +2,6 @@ Provides C/C++ specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C/C++. """ -import json import logging import os import pathlib @@ -12,10 +11,11 @@ import threading 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.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from .common import RuntimeDependency, RuntimeDependencyCollection + class ClangdLanguageServer(SolidLanguageServer): """ @@ -28,7 +28,7 @@ class ClangdLanguageServer(SolidLanguageServer): """ 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) super().__init__( config, logger, @@ -41,42 +41,49 @@ class ClangdLanguageServer(SolidLanguageServer): self.initialize_searcher_command_available = threading.Event() self.resolve_main_method_available = threading.Event() - def setup_runtime_dependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> str: + @staticmethod + def _setup_runtime_dependencies(logger: LanguageServerLogger, config: LanguageServerConfig) -> str: """ - Setup runtime dependencies for ClangdLanguageServer. + Setup runtime dependencies for ClangdLanguageServer and return the command to start the server. """ - platform_id = PlatformUtils.get_platform_id() - - with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json")) as f: - d = json.load(f) - del d["_description"] - - assert platform_id.value in [ - "linux-x64", - "win-x64", - "osx-arm64", - ], ( - "Unsupported platform: " + platform_id.value + deps = RuntimeDependencyCollection( + [ + RuntimeDependency( + id="Clangd", + description="Clangd for Linux (x64)", + url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-linux-19.1.2.zip", + platform_id="linux-x64", + archive_type="zip", + binary_name="clangd_19.1.2/bin/clangd", + ), + RuntimeDependency( + id="Clangd", + description="Clangd for Windows (x64)", + url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-windows-19.1.2.zip", + platform_id="win-x64", + archive_type="zip", + binary_name="clangd_19.1.2/bin/clangd.exe", + ), + RuntimeDependency( + id="Clangd", + description="Clangd for macOS (Arm64)", + url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-mac-19.1.2.zip", + platform_id="osx-arm64", + archive_type="zip", + binary_name="clangd_19.1.2/bin/clangd", + ), + ] ) - runtime_dependencies = d["runtimeDependencies"] - runtime_dependencies = [dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value] - assert len(runtime_dependencies) == 1 - # Select dependency matching the current platform - dependency = next((dep for dep in runtime_dependencies if dep["platformId"] == platform_id.value), None) - if dependency is None: - raise RuntimeError(f"No runtime dependency found for platform {platform_id.value}") - clangd_ls_dir = os.path.join(os.path.dirname(__file__), "static", "clangd") - clangd_executable_path = os.path.join(clangd_ls_dir, "clangd_19.1.2", "bin", dependency["binaryName"]) + dep = deps.single_for_current_platform() + clangd_executable_path = deps.binary_path(clangd_ls_dir) if not os.path.exists(clangd_executable_path): - clangd_url = dependency["url"] - logger.log(f"Clangd executable not found at {clangd_executable_path}. Downloading from {clangd_url}", logging.INFO) - os.makedirs(clangd_ls_dir, exist_ok=True) - if dependency["archiveType"] == "zip": - FileUtils.download_and_extract_archive(logger, clangd_url, clangd_ls_dir, dependency["archiveType"]) - else: - raise RuntimeError(f"Unsupported archive type: {dependency['archiveType']}") + logger.log( + f"Clangd executable not found at {clangd_executable_path}. Downloading from {dep.url}", + logging.INFO, + ) + deps.install(logger, clangd_ls_dir) if not os.path.exists(clangd_executable_path): raise FileNotFoundError( f"Clangd executable not found at {clangd_executable_path}.\n" @@ -86,29 +93,34 @@ class ClangdLanguageServer(SolidLanguageServer): return clangd_executable_path - 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 clangd Language Server. """ - with open(os.path.join(os.path.dirname(__file__), "initialize_params.json")) as f: - d = json.load(f) + root_uri = pathlib.Path(repository_absolute_path).as_uri() + initialize_params = { + "locale": "en", + "capabilities": { + "textDocument": { + "synchronization": {"didSave": True, "dynamicRegistration": True}, + "completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": True}}, + "definition": {"dynamicRegistration": True}, + }, + "workspace": {"workspaceFolders": True, "didChangeConfiguration": {"dynamicRegistration": True}}, + }, + "processId": os.getpid(), + "rootPath": repository_absolute_path, + "rootUri": root_uri, + "workspaceFolders": [ + { + "uri": root_uri, + "name": "$name", + } + ], + } - 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): """ diff --git a/src/solidlsp/language_servers/clangd_language_server/initialize_params.json b/src/solidlsp/language_servers/clangd_language_server/initialize_params.json deleted file mode 100644 index 4330560e..00000000 --- a/src/solidlsp/language_servers/clangd_language_server/initialize_params.json +++ /dev/null @@ -1,36 +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", - "capabilities": { - "textDocument": { - "synchronization": { - "didSave": true, - "dynamicRegistration": true - }, - "completion": { - "dynamicRegistration": true, - "completionItem": { - "snippetSupport": true - } - }, - "definition": { - "dynamicRegistration": true - } - }, - "workspace": { - "workspaceFolders": true, - "didChangeConfiguration": { - "dynamicRegistration": true - } - } - }, - "workspaceFolders": [ - { - "uri": "$uri", - "name": "$name" - } - ] -} \ No newline at end of file diff --git a/src/solidlsp/language_servers/clangd_language_server/runtime_dependencies.json b/src/solidlsp/language_servers/clangd_language_server/runtime_dependencies.json deleted file mode 100644 index f96239db..00000000 --- a/src/solidlsp/language_servers/clangd_language_server/runtime_dependencies.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "_description": "Used to download the runtime dependencies for running Clangd.", - "runtimeDependencies": [ - { - "id": "Clangd", - "description": "Clangd for Linux (x64)", - "url": "https://github.com/clangd/clangd/releases/download/19.1.2/clangd-linux-19.1.2.zip", - "platformId": "linux-x64", - "archiveType": "zip", - "binaryName": "clangd" - }, - { - "id": "Clangd", - "description": "Clangd for Windows (x64)", - "url": "https://github.com/clangd/clangd/releases/download/19.1.2/clangd-windows-19.1.2.zip", - "platformId": "win-x64", - "archiveType": "zip", - "binaryName": "clangd.exe" - }, - { - "id": "Clangd", - "description": "Clangd for macOS (Arm64)", - "url": "https://github.com/clangd/clangd/releases/download/19.1.2/clangd-mac-19.1.2.zip", - "platformId": "osx-arm64", - "archiveType": "zip", - "binaryName": "clangd" - } - ] -} diff --git a/src/solidlsp/language_servers/clojure_lsp/clojure_lsp.py b/src/solidlsp/language_servers/clojure_lsp.py similarity index 72% rename from src/solidlsp/language_servers/clojure_lsp/clojure_lsp.py rename to src/solidlsp/language_servers/clojure_lsp.py index e83b540d..cae8abc2 100644 --- a/src/solidlsp/language_servers/clojure_lsp/clojure_lsp.py +++ b/src/solidlsp/language_servers/clojure_lsp.py @@ -13,10 +13,11 @@ import threading 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.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from .common import RuntimeDependency, RuntimeDependencyCollection + def run_command(cmd: list, capture_output: bool = True) -> subprocess.CompletedProcess: return subprocess.run( @@ -44,38 +45,44 @@ class ClojureLSP(SolidLanguageServer): """ clojure_lsp_releases = "https://github.com/clojure-lsp/clojure-lsp/releases/latest/download" - runtime_dependencies = [ - { - "url": f"{clojure_lsp_releases}/clojure-lsp-native-macos-aarch64.zip", - "platformId": "osx-arm64", - "archiveType": "zip", - "binaryName": "clojure-lsp", - }, - { - "url": f"{clojure_lsp_releases}/clojure-lsp-native-linux-aarch64.zip", - "platformId": "linux-arm64", - "archiveType": "zip", - "binaryName": "clojure-lsp", - }, - { - "url": f"{clojure_lsp_releases}/clojure-lsp-native-linux-amd64.zip", - "platformId": "linux-x64", - "archiveType": "zip", - "binaryName": "clojure-lsp", - }, - { - "url": f"{clojure_lsp_releases}/clojure-lsp-native-windows-amd64.zip", - "platformId": "win-x64", - "archiveType": "zip", - "binaryName": "clojure-lsp.exe", - }, - ] + runtime_dependencies = RuntimeDependencyCollection( + [ + RuntimeDependency( + id="clojure-lsp", + url=f"{clojure_lsp_releases}/clojure-lsp-native-macos-aarch64.zip", + platform_id="osx-arm64", + archive_type="zip", + binary_name="clojure-lsp", + ), + RuntimeDependency( + id="clojure-lsp", + url=f"{clojure_lsp_releases}/clojure-lsp-native-linux-aarch64.zip", + platform_id="linux-arm64", + archive_type="zip", + binary_name="clojure-lsp", + ), + RuntimeDependency( + id="clojure-lsp", + url=f"{clojure_lsp_releases}/clojure-lsp-native-linux-amd64.zip", + platform_id="linux-x64", + archive_type="zip", + binary_name="clojure-lsp", + ), + RuntimeDependency( + id="clojure-lsp", + url=f"{clojure_lsp_releases}/clojure-lsp-native-windows-amd64.zip", + platform_id="win-x64", + archive_type="zip", + binary_name="clojure-lsp.exe", + ), + ] + ) def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): """ 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) super().__init__( config, logger, @@ -88,44 +95,40 @@ class ClojureLSP(SolidLanguageServer): self.resolve_main_method_available = threading.Event() self.service_ready_event = threading.Event() - def setup_runtime_dependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> str: - """Setup runtime dependencies for clojure-lsp.""" + @staticmethod + def _setup_runtime_dependencies(logger: LanguageServerLogger, config: LanguageServerConfig) -> str: + """Setup runtime dependencies for clojure-lsp and return the command to start the server.""" verify_clojure_cli() - platform_id = PlatformUtils.get_platform_id() - - runtime_dependencies = [dependency for dependency in self.runtime_dependencies if dependency["platformId"] == platform_id.value] - if len(runtime_dependencies) != 1: - raise RuntimeError(f"Could not find a suitable clojure-lsp runtime dependency for platform {platform_id.value}.") - dependency = runtime_dependencies[0] + deps = ClojureLSP.runtime_dependencies + dependency = deps.single_for_current_platform() clojurelsp_ls_dir = os.path.join(os.path.dirname(__file__), "static", "clojure-lsp") - clojurelsp_executable_path = os.path.join(clojurelsp_ls_dir, dependency["binaryName"]) + 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) - FileUtils.download_and_extract_archive(logger, dependency["url"], clojurelsp_ls_dir, dependency["archiveType"]) + logger.log( + f"Downloading and extracting clojure-lsp from {dependency.url} to {clojurelsp_ls_dir}", + logging.INFO, + ) + deps.install(logger, clojurelsp_ls_dir) if not os.path.exists(clojurelsp_executable_path): raise FileNotFoundError(f"Download failed? Could not find clojure-lsp executable at {clojurelsp_executable_path}") os.chmod(clojurelsp_executable_path, stat.S_IEXEC) return clojurelsp_executable_path - def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: + @staticmethod + def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: """Returns the init params for clojure-lsp.""" root_uri = pathlib.Path(repository_absolute_path).as_uri() return { # type: ignore "processId": os.getpid(), "rootPath": repository_absolute_path, "rootUri": root_uri, - "clientInfo": {"name": "solidlsp", "version": "0.1.0"}, "capabilities": { "workspace": { "applyEdit": True, "workspaceEdit": {"documentChanges": True}, - "symbol": { - "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] - } - }, + "symbol": {"symbolKind": {"valueSet": list(range(1, 27))}}, "workspaceFolders": True, }, "textDocument": { @@ -133,13 +136,10 @@ class ClojureLSP(SolidLanguageServer): "publishDiagnostics": {"relatedInformation": True, "tagSupport": {"valueSet": [1, 2]}}, "definition": {"linkSupport": True}, "references": {}, - "completion": {"completionItem": {"snippetSupport": True, "documentationFormat": ["markdown", "plaintext"]}}, "hover": {"contentFormat": ["markdown", "plaintext"]}, "documentSymbol": { "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] - }, + "symbolKind": {"valueSet": list(range(1, 27))}, # }, }, "general": {"positionEncodings": ["utf-16"]}, diff --git a/src/solidlsp/language_servers/common.py b/src/solidlsp/language_servers/common.py new file mode 100644 index 00000000..a0973e56 --- /dev/null +++ b/src/solidlsp/language_servers/common.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +import os +import subprocess +from collections.abc import Sequence +from dataclasses import dataclass + +from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.ls_utils import FileUtils, PlatformUtils + + +@dataclass(kw_only=True) +class RuntimeDependency: + """Represents a runtime dependency for a language server.""" + + id: str + platform_id: str | None = None + url: str | None = None + archive_type: str | None = None + binary_name: str | None = None + command: str | None = None + package_name: str | None = None + package_version: str | None = None + extract_path: str | None = None + description: str | None = None + + +class RuntimeDependencyCollection: + """Utility to handle installation of runtime dependencies.""" + + def __init__(self, dependencies: Sequence[RuntimeDependency]): + self._dependencies = list(dependencies) + + def for_platform(self, platform_id: str) -> list[RuntimeDependency]: + return [d for d in self._dependencies if d.platform_id in (platform_id, "any", "platform-agnostic", None)] + + def for_current_platform(self) -> list[RuntimeDependency]: + return self.for_platform(PlatformUtils.get_platform_id().value) + + def single_for_current_platform(self) -> RuntimeDependency: + deps = self.for_current_platform() + if len(deps) != 1: + raise RuntimeError(f"Expected exactly one runtime dependency for {PlatformUtils.get_platform_id().value}, found {len(deps)}") + return deps[0] + + def binary_path(self, target_dir: str) -> str: + dep = self.single_for_current_platform() + if not dep.binary_name: + return target_dir + return os.path.join(target_dir, dep.binary_name) + + def install(self, logger: LanguageServerLogger, target_dir: str) -> dict[str, str]: + """Install all dependencies for the current platform into *target_dir*. + + Returns a mapping from dependency id to the resolved binary path. + """ + os.makedirs(target_dir, exist_ok=True) + results: dict[str, str] = {} + for dep in self.for_current_platform(): + if dep.url: + self._install_from_url(dep, logger, target_dir) + if dep.command: + self._run_command(dep.command, target_dir) + if dep.binary_name: + results[dep.id] = os.path.join(target_dir, dep.binary_name) + else: + results[dep.id] = target_dir + return results + + @staticmethod + def _run_command(command: str, cwd: str) -> None: + if PlatformUtils.get_platform_id().value.startswith("win"): + subprocess.run( + command, + shell=True, + check=True, + cwd=cwd, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + else: + import pwd + + user = pwd.getpwuid(os.getuid()).pw_name + subprocess.run( + command, + shell=True, + check=True, + user=user, + cwd=cwd, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + @staticmethod + def _install_from_url(dep: RuntimeDependency, logger: LanguageServerLogger, target_dir: str) -> None: + if dep.archive_type == "gz" and dep.binary_name: + dest = os.path.join(target_dir, dep.binary_name) + FileUtils.download_and_extract_archive(logger, dep.url, dest, dep.archive_type) + else: + FileUtils.download_and_extract_archive(logger, dep.url, target_dir, dep.archive_type or "zip") diff --git a/src/solidlsp/language_servers/csharp_language_server/csharp_language_server.py b/src/solidlsp/language_servers/csharp_language_server.py similarity index 84% rename from src/solidlsp/language_servers/csharp_language_server/csharp_language_server.py rename to src/solidlsp/language_servers/csharp_language_server.py index fbf13779..7d11a1df 100644 --- a/src/solidlsp/language_servers/csharp_language_server/csharp_language_server.py +++ b/src/solidlsp/language_servers/csharp_language_server.py @@ -8,11 +8,11 @@ import os import platform import shutil import stat +import subprocess import tarfile import threading import urllib.request import zipfile -from dataclasses import dataclass from pathlib import Path from typing import cast @@ -26,21 +26,7 @@ from solidlsp.ls_utils import PathUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo - -@dataclass -class RuntimeDependency: - """Represents a runtime dependency for the C# language server.""" - - id: str - description: str - platform_id: str - archive_type: str - binary_name: str - package_name: str | None = None - package_version: str | None = None - extract_path: str | None = None - url: str | None = None - +from .common import RuntimeDependency # Runtime dependencies configuration RUNTIME_DEPENDENCIES = [ @@ -194,13 +180,13 @@ class CSharpLanguageServer(SolidLanguageServer): Creates a CSharpLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - dotnet_path, language_server_path, cache_dir = self._ensure_server_installed(logger, config) + dotnet_path, language_server_path = self._ensure_server_installed(logger, config) # Find solution or project file solution_or_project = find_solution_or_project_file(repository_root_path) # Create log directory - log_dir = cache_dir / "logs" + log_dir = Path(self.ls_resources_dir()) / "logs" log_dir.mkdir(parents=True, exist_ok=True) # Build command using dotnet directly @@ -228,30 +214,21 @@ class CSharpLanguageServer(SolidLanguageServer): def is_ignored_dirname(self, dirname: str) -> bool: return super().is_ignored_dirname(dirname) or dirname in ["bin", "obj", "packages", ".vs"] - def _ensure_server_installed(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> tuple[str, str, Path]: + @classmethod + def _ensure_server_installed(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> tuple[str, str]: """ Ensure .NET runtime and Microsoft.CodeAnalysis.LanguageServer are available. - Returns a tuple of (dotnet_path, language_server_dll_path, cache_dir). + Returns a tuple of (dotnet_path, language_server_dll_path). """ - # Determine platform runtime ID - runtime_id = self._get_runtime_id() + 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) - # Set up cache directory - cache_dir = Path.home() / ".cache" / "serena" / "language-servers" / "csharp" - cache_dir.mkdir(parents=True, exist_ok=True) + return dotnet_path, server_dll_path - # Get runtime dependencies for this platform - lang_server_dep, dotnet_runtime_dep = self._get_runtime_dependencies(runtime_id) - - # Ensure .NET runtime is available - dotnet_path = self._ensure_dotnet_runtime(logger, cache_dir, dotnet_runtime_dep) - - # Ensure language server is available - server_dll_path = self._ensure_language_server(logger, cache_dir, lang_server_dep) - - return dotnet_path, server_dll_path, cache_dir - - def _get_runtime_id(self) -> str: + @staticmethod + def _get_runtime_id() -> str: """Determine the runtime ID based on the platform.""" system = platform.system().lower() machine = platform.machine().lower() @@ -265,7 +242,8 @@ class CSharpLanguageServer(SolidLanguageServer): else: raise LanguageServerException(f"Unsupported platform: {system} {machine}") - def _get_runtime_dependencies(self, runtime_id: str) -> tuple[RuntimeDependency, RuntimeDependency]: + @staticmethod + def _get_runtime_dependencies(runtime_id: str) -> tuple[RuntimeDependency, RuntimeDependency]: """Get the language server and .NET runtime dependencies for the platform.""" lang_server_dep = None dotnet_runtime_dep = None @@ -283,15 +261,14 @@ class CSharpLanguageServer(SolidLanguageServer): return lang_server_dep, dotnet_runtime_dep - def _ensure_dotnet_runtime(self, logger: LanguageServerLogger, cache_dir: Path, runtime_dep: RuntimeDependency) -> str: + @staticmethod + def _ensure_dotnet_runtime(logger: LanguageServerLogger, runtime_dep: RuntimeDependency) -> str: """Ensure .NET runtime is available and return the dotnet executable path.""" - # Check if dotnet is already available in system + # Check if dotnet is already available on the system system_dotnet = shutil.which("dotnet") if system_dotnet: # Check if it's .NET 9 try: - import subprocess - result = subprocess.run([system_dotnet, "--list-runtimes"], capture_output=True, text=True, check=True) if "Microsoft.NETCore.App 9." in result.stdout: logger.log("Found system .NET 9 runtime", logging.INFO) @@ -300,14 +277,15 @@ class CSharpLanguageServer(SolidLanguageServer): pass # Download .NET 9 runtime using config - return self._ensure_dotnet_runtime_from_config(logger, cache_dir, runtime_dep) + return CSharpLanguageServer._ensure_dotnet_runtime_from_config(logger, runtime_dep) - def _ensure_language_server(self, logger: LanguageServerLogger, cache_dir: Path, lang_server_dep: RuntimeDependency) -> str: + @classmethod + def _ensure_language_server(cls, logger: LanguageServerLogger, lang_server_dep: RuntimeDependency) -> 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 = cache_dir / f"{package_name}.{package_version}" + server_dir = Path(cls.ls_resources_dir()) / f"{package_name}.{package_version}" server_dll = server_dir / lang_server_dep.binary_name if server_dll.exists(): @@ -316,10 +294,10 @@ class CSharpLanguageServer(SolidLanguageServer): # Download and install the language server logger.log(f"Downloading {package_name} version {package_version}...", logging.INFO) - package_path = self._download_nuget_package_direct(logger, package_name, package_version, cache_dir) + package_path = CSharpLanguageServer._download_nuget_package_direct(logger, package_name, package_version) # Extract and install - self._extract_language_server(lang_server_dep, package_path, server_dir) + CSharpLanguageServer._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") @@ -331,7 +309,8 @@ class CSharpLanguageServer(SolidLanguageServer): logger.log(f"Successfully installed Microsoft.CodeAnalysis.LanguageServer to {server_dll}", logging.INFO) return str(server_dll) - def _extract_language_server(self, lang_server_dep: RuntimeDependency, package_path: Path, server_dir: Path) -> None: + @staticmethod + def _extract_language_server(lang_server_dep: RuntimeDependency, package_path: Path, server_dir: Path) -> None: """Extract language server files from downloaded package.""" extract_path = lang_server_dep.extract_path or "lib/net9.0" source_dir = package_path / extract_path @@ -353,9 +332,8 @@ class CSharpLanguageServer(SolidLanguageServer): server_dir.mkdir(parents=True, exist_ok=True) shutil.copytree(source_dir, server_dir, dirs_exist_ok=True) - def _download_nuget_package_direct( - self, logger: LanguageServerLogger, package_name: str, package_version: str, cache_dir: Path - ) -> Path: + @classmethod + def _download_nuget_package_direct(cls, logger: LanguageServerLogger, package_name: str, package_version: str) -> Path: """ Download a NuGet package directly from the Azure NuGet feed. Returns the path to the extracted package directory. @@ -363,7 +341,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 = cache_dir / "temp_downloads" + temp_dir = Path(cls.ls_resources_dir()) / "temp_downloads" temp_dir.mkdir(parents=True, exist_ok=True) try: @@ -411,18 +389,17 @@ class CSharpLanguageServer(SolidLanguageServer): f"Failed to download package {package_name} version {package_version} from Azure NuGet feed: {e}" ) from e - def _ensure_dotnet_runtime_from_config(self, logger: LanguageServerLogger, cache_dir: Path, runtime_dep: RuntimeDependency) -> str: + @classmethod + def _ensure_dotnet_runtime_from_config(cls, logger: LanguageServerLogger, runtime_dep: RuntimeDependency) -> str: """ Ensure .NET 9 runtime is available using runtime dependency configuration. Returns the path to the dotnet executable. """ - # Check if dotnet is already available in system + # Check if dotnet is already available on the system system_dotnet = shutil.which("dotnet") if system_dotnet: # Check if it's .NET 9 try: - import subprocess - result = subprocess.run([system_dotnet, "--list-runtimes"], capture_output=True, text=True, check=True) if "Microsoft.NETCore.App 9." in result.stdout: logger.log("Found system .NET 9 runtime", logging.INFO) @@ -431,7 +408,7 @@ class CSharpLanguageServer(SolidLanguageServer): pass # Download .NET 9 runtime using config - dotnet_dir = cache_dir / "dotnet-runtime-9.0" + dotnet_dir = Path(cls.ls_resources_dir()) / "dotnet-runtime-9.0" dotnet_exe = dotnet_dir / runtime_dep.binary_name if dotnet_exe.exists(): @@ -499,9 +476,7 @@ class CSharpLanguageServer(SolidLanguageServer): "didChangeWatchedFiles": {"dynamicRegistration": True}, "symbol": { "dynamicRegistration": 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] - }, + "symbolKind": {"valueSet": list(range(1, 27))}, }, "executeCommand": {"dynamicRegistration": True}, "configuration": True, @@ -510,20 +485,6 @@ class CSharpLanguageServer(SolidLanguageServer): }, "textDocument": { "synchronization": {"dynamicRegistration": True, "willSave": True, "willSaveWaitUntil": True, "didSave": True}, - "completion": { - "dynamicRegistration": True, - "contextSupport": True, - "completionItem": { - "snippetSupport": True, - "commitCharactersSupport": True, - "documentationFormat": ["markdown", "plaintext"], - "deprecatedSupport": True, - "preselectSupport": True, - }, - "completionItemKind": { - "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] - }, - }, "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]}, "signatureHelp": { "dynamicRegistration": True, @@ -534,38 +495,11 @@ class CSharpLanguageServer(SolidLanguageServer): }, "definition": {"dynamicRegistration": True}, "references": {"dynamicRegistration": True}, - "documentHighlight": {"dynamicRegistration": True}, "documentSymbol": { "dynamicRegistration": 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] - }, + "symbolKind": {"valueSet": list(range(1, 27))}, "hierarchicalDocumentSymbolSupport": True, }, - "codeAction": { - "dynamicRegistration": True, - "codeActionLiteralSupport": { - "codeActionKind": { - "valueSet": [ - "", - "quickfix", - "refactor", - "refactor.extract", - "refactor.inline", - "refactor.rewrite", - "source", - "source.organizeImports", - ] - } - }, - }, - "codeLens": {"dynamicRegistration": True}, - "formatting": {"dynamicRegistration": True}, - "rangeFormatting": {"dynamicRegistration": True}, - "onTypeFormatting": {"dynamicRegistration": True}, - "rename": {"dynamicRegistration": True}, - "publishDiagnostics": {"relatedInformation": True}, - "foldingRange": {"dynamicRegistration": True, "rangeLimit": 5000, "lineFoldingOnly": True}, }, }, }, diff --git a/src/solidlsp/language_servers/dart_language_server.py b/src/solidlsp/language_servers/dart_language_server.py new file mode 100644 index 00000000..28696ad6 --- /dev/null +++ b/src/solidlsp/language_servers/dart_language_server.py @@ -0,0 +1,157 @@ +import logging +import os +import pathlib +import stat + +from solidlsp.ls import SolidLanguageServer +from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo + +from .common import RuntimeDependency, RuntimeDependencyCollection + + +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): + """ + Creates a DartServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. + """ + executable_path = self._setup_runtime_dependencies(logger) + super().__init__( + config, + logger, + repository_root_path, + ProcessLaunchInfo(cmd=executable_path, cwd=repository_root_path), + "dart", + ) + + @classmethod + def _setup_runtime_dependencies(cls, logger: "LanguageServerLogger") -> str: + deps = RuntimeDependencyCollection( + [ + RuntimeDependency( + id="DartLanguageServer", + description="Dart Language Server for Linux (x64)", + url="https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-linux-x64-release.zip", + platform_id="linux-x64", + archive_type="zip", + binary_name="dart-sdk/bin/dart", + ), + RuntimeDependency( + id="DartLanguageServer", + description="Dart Language Server for Windows (x64)", + url="https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-windows-x64-release.zip", + platform_id="win-x64", + archive_type="zip", + binary_name="dart-sdk/bin/dart.exe", + ), + RuntimeDependency( + id="DartLanguageServer", + description="Dart Language Server for Windows (arm64)", + url="https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-windows-arm64-release.zip", + platform_id="win-arm64", + archive_type="zip", + binary_name="dart-sdk/bin/dart.exe", + ), + RuntimeDependency( + id="DartLanguageServer", + description="Dart Language Server for macOS (x64)", + url="https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-macos-x64-release.zip", + platform_id="osx-x64", + archive_type="zip", + binary_name="dart-sdk/bin/dart", + ), + RuntimeDependency( + id="DartLanguageServer", + description="Dart Language Server for macOS (arm64)", + url="https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-macos-arm64-release.zip", + platform_id="osx-arm64", + archive_type="zip", + binary_name="dart-sdk/bin/dart", + ), + ] + ) + + dart_ls_dir = cls.ls_resources_dir() + dart_executable_path = deps.binary_path(dart_ls_dir) + + if not os.path.exists(dart_executable_path): + deps.install(logger, dart_ls_dir) + + 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" + + @staticmethod + def _get_initialize_params(repository_absolute_path: str): + """ + Returns the initialize params for the Dart Language Server. + """ + root_uri = pathlib.Path(repository_absolute_path).as_uri() + initialize_params = { + "capabilities": {}, + "initializationOptions": { + "onlyAnalyzeProjectsWithOpenFiles": False, + "closingLabels": False, + "outline": False, + "flutterOutline": False, + "allowOpenUri": False, + }, + "trace": "verbose", + "processId": os.getpid(), + "rootPath": repository_absolute_path, + "rootUri": pathlib.Path(repository_absolute_path).as_uri(), + "workspaceFolders": [ + { + "uri": root_uri, + "name": os.path.basename(repository_absolute_path), + } + ], + } + + return initialize_params + + def _start_server(self): + """ + Start the language server and yield when the server is ready. + """ + + def execute_client_command_handler(params): + return [] + + def do_nothing(params): + return + + def check_experimental_status(params): + pass + + def window_log_message(msg): + self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + self.server.on_request("client/registerCapability", do_nothing) + self.server.on_notification("language/status", do_nothing) + self.server.on_notification("window/logMessage", window_log_message) + self.server.on_request("workspace/executeClientCommand", execute_client_command_handler) + self.server.on_notification("$/progress", do_nothing) + self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + self.server.on_notification("language/actionableNotification", do_nothing) + self.server.on_notification("experimental/serverStatus", check_experimental_status) + + self.logger.log("Starting dart-language-server server process", logging.INFO) + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) + self.logger.log( + "Sending initialize request to dart-language-server", + logging.DEBUG, + ) + init_response = self.server.send_request("initialize", initialize_params) + self.logger.log( + f"Received initialize response from dart-language-server: {init_response}", + logging.INFO, + ) + + self.server.notify.initialized({}) diff --git a/src/solidlsp/language_servers/dart_language_server/dart_language_server.py b/src/solidlsp/language_servers/dart_language_server/dart_language_server.py deleted file mode 100644 index 58d71014..00000000 --- a/src/solidlsp/language_servers/dart_language_server/dart_language_server.py +++ /dev/null @@ -1,119 +0,0 @@ -import json -import logging -import os -import pathlib -import stat - -from solidlsp.ls import SolidLanguageServer -from solidlsp.ls_logger import LanguageServerLogger -from solidlsp.ls_utils import FileUtils, PlatformUtils -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo - - -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): - """ - Creates a DartServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. - """ - executable_path = self.setup_runtime_dependencies(logger) - super().__init__( - config, - logger, - repository_root_path, - ProcessLaunchInfo(cmd=executable_path, cwd=repository_root_path), - "dart", - ) - - def setup_runtime_dependencies(self, logger: "LanguageServerLogger") -> str: - platform_id = PlatformUtils.get_platform_id() - - with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json")) as f: - d = json.load(f) - del d["_description"] - - runtime_dependencies = d["runtimeDependencies"] - runtime_dependencies = [dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value] - - assert len(runtime_dependencies) == 1 - dependency = runtime_dependencies[0] - - dart_ls_dir = os.path.join(os.path.dirname(__file__), "static", "dart-language-server") - dart_executable_path = os.path.join(dart_ls_dir, dependency["binaryName"]) - - if not os.path.exists(dart_ls_dir): - os.makedirs(dart_ls_dir) - FileUtils.download_and_extract_archive(logger, dependency["url"], dart_ls_dir, dependency["archiveType"]) - - 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" - - def _get_initialize_params(self, repository_absolute_path: str): - """ - Returns the initialize params for the Dart Language Server. - """ - with open(os.path.join(os.path.dirname(__file__), "initialize_params.json")) as f: - d = json.load(f) - - 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 - - def _start_server(self): - """ - Start the language server and yield when the server is ready. - """ - - def execute_client_command_handler(params): - return [] - - def do_nothing(params): - return - - def check_experimental_status(params): - pass - - def window_log_message(msg): - self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) - - self.server.on_request("client/registerCapability", do_nothing) - self.server.on_notification("language/status", do_nothing) - self.server.on_notification("window/logMessage", window_log_message) - self.server.on_request("workspace/executeClientCommand", execute_client_command_handler) - self.server.on_notification("$/progress", do_nothing) - self.server.on_notification("textDocument/publishDiagnostics", do_nothing) - self.server.on_notification("language/actionableNotification", do_nothing) - self.server.on_notification("experimental/serverStatus", check_experimental_status) - - self.logger.log("Starting dart-language-server server process", logging.INFO) - self.server.start() - initialize_params = self._get_initialize_params(self.repository_root_path) - self.logger.log( - "Sending initialize request to dart-language-server", - logging.DEBUG, - ) - init_response = self.server.send_request("initialize", initialize_params) - self.logger.log( - f"Received initialize response from dart-language-server: {init_response}", - logging.INFO, - ) - - self.server.notify.initialized({}) diff --git a/src/solidlsp/language_servers/dart_language_server/initialize_params.json b/src/solidlsp/language_servers/dart_language_server/initialize_params.json deleted file mode 100644 index e90e408a..00000000 --- a/src/solidlsp/language_servers/dart_language_server/initialize_params.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "_description": "This file contains the initialization parameters for the Dart Language Server.", - "processId": "$processId", - "rootPath": "$rootPath", - "rootUri": "$rootUri", - "capabilities": {}, - "initializationOptions": { - "onlyAnalyzeProjectsWithOpenFiles": false, - "suggestFromUnimportedLibraries": true, - "closingLabels": false, - "outline": false, - "flutterOutline": false, - "allowOpenUri": false - }, - "trace": "verbose", - "workspaceFolders": [ - { - "uri": "$uri", - "name": "$name" - } - ] - -} \ No newline at end of file diff --git a/src/solidlsp/language_servers/dart_language_server/runtime_dependencies.json b/src/solidlsp/language_servers/dart_language_server/runtime_dependencies.json deleted file mode 100644 index 32e49299..00000000 --- a/src/solidlsp/language_servers/dart_language_server/runtime_dependencies.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "_description": "Used to download the runtime dependencies for running Dart Language Server, downloaded from https://dart.dev/get-dart/archive", - "runtimeDependencies": [ - { - "id": "DartLanguageServer", - "description": "Dart Language Server for Linux (x64)", - "url": "https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-linux-x64-release.zip", - "platformId": "linux-x64", - "archiveType": "zip", - "binaryName": "dart-sdk/bin/dart" - }, - { - "id": "DartLanguageServer", - "description": "Dart Language Server for Windows (x64)", - "url": "https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-windows-x64-release.zip", - "platformId": "win-x64", - "archiveType": "zip", - "binaryName": "dart-sdk/bin/dart.exe" - }, - { - "id": "DartLanguageServer", - "description": "Dart Language Server for Windows (arm64)", - "url": "https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-windows-arm64-release.zip", - "platformId": "win-arm64", - "archiveType": "zip", - "binaryName": "dart-sdk/bin/dart.exe" - }, - { - "id": "DartLanguageServer", - "description": "Dart Language Server for macOS (x64)", - "url": "https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-macos-x64-release.zip", - "platformId": "osx-x64", - "archiveType": "zip", - "binaryName": "dart-sdk/bin/dart" - }, - { - "id": "DartLanguageServer", - "description": "Dart Language Server for macOS (arm64)", - "url": "https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-macos-arm64-release.zip", - "platformId": "osx-arm64", - "archiveType": "zip", - "binaryName": "dart-sdk/bin/dart" - } - ] -} \ No newline at end of file diff --git a/src/solidlsp/language_servers/eclipse_jdtls.py b/src/solidlsp/language_servers/eclipse_jdtls.py new file mode 100644 index 00000000..5ebb378c --- /dev/null +++ b/src/solidlsp/language_servers/eclipse_jdtls.py @@ -0,0 +1,718 @@ +""" +Provides Java specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Java. +""" + +import dataclasses +import logging +import os +import pathlib +import shutil +import stat +import threading +import uuid +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.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.settings import SolidLSPSettings + + +@dataclasses.dataclass +class RuntimeDependencyPaths: + """ + Stores the paths to the runtime dependencies of EclipseJDTLS + """ + + gradle_path: str + lombok_jar_path: str + jre_path: str + jre_home_path: str + jdtls_launcher_jar_path: str + jdtls_readonly_config_path: str + intellicode_jar_path: str + intellisense_members_path: str + + +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): + """ + 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 + + # ws_dir is the workspace directory for the EclipseJDTLS server + ws_dir = str( + PurePath( + SolidLSPSettings.get_language_server_directory(), + "EclipseJDTLS", + "workspaces", + uuid.uuid4().hex, + ) + ) + + # 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")) + + 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")) + + jdtls_readonly_config_path = self.runtime_dependency_paths.jdtls_readonly_config_path + + if not os.path.exists(jdtls_config_path): + shutil.copytree(jdtls_readonly_config_path, jdtls_config_path) + + for static_path in [ + jre_path, + lombok_jar_path, + jdtls_launcher_jar, + jdtls_config_path, + jdtls_readonly_config_path, + ]: + assert os.path.exists(static_path), static_path + + # 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 + cmd = " ".join( + [ + jre_path, + "--add-modules=ALL-SYSTEM", + "--add-opens", + "java.base/java.util=ALL-UNNAMED", + "--add-opens", + "java.base/java.lang=ALL-UNNAMED", + "--add-opens", + "java.base/sun.nio.fs=ALL-UNNAMED", + "-Declipse.application=org.eclipse.jdt.ls.core.id1", + "-Dosgi.bundles.defaultStartLevel=4", + "-Declipse.product=org.eclipse.jdt.ls.core.product", + "-Djava.import.generatesMetadataFilesAtProjectRoot=false", + "-Dfile.encoding=utf8", + "-noverify", + "-XX:+UseParallelGC", + "-XX:GCTimeRatio=4", + "-XX:AdaptiveSizePolicyWeight=90", + "-Dsun.zip.disableMemoryMapping=true", + "-Djava.lsp.joinOnCompletion=true", + "-Xmx3G", + "-Xms100m", + "-Xlog:disable", + "-Dlog.level=ALL", + f'"-javaagent:{lombok_jar_path}"', + f'"-Djdt.core.sharedIndexLocation={shared_cache_location}"', + "-jar", + f'"{jdtls_launcher_jar}"', + "-configuration", + f'"{jdtls_config_path}"', + "-data", + f'"{data_dir}"', + ] + ) + + self.service_ready_event = threading.Event() + 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") + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + # Ignore common Java build directories from different build tools: + # - Maven: target + # - Gradle: build, .gradle + # - Eclipse: bin, .settings + # - IntelliJ IDEA: out, .idea + # - General: classes, dist, lib + return super().is_ignored_dirname(dirname) or dirname in [ + "target", # Maven + "build", # Gradle + "bin", # Eclipse + "out", # IntelliJ IDEA + "classes", # General + "dist", # General + "lib", # General + ] + + @classmethod + def _setupRuntimeDependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> RuntimeDependencyPaths: + """ + Setup runtime dependencies for EclipseJDTLS and return the paths. + """ + platformId = PlatformUtils.get_platform_id() + + runtime_dependencies = { + "gradle": { + "platform-agnostic": { + "url": "https://services.gradle.org/distributions/gradle-7.3.3-bin.zip", + "archiveType": "zip", + "relative_extraction_path": ".", + } + }, + "vscode-java": { + "darwin-arm64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix", + "archiveType": "zip", + "relative_extraction_path": "vscode-java", + }, + "osx-arm64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix", + "archiveType": "zip", + "relative_extraction_path": "vscode-java", + "jre_home_path": "extension/jre/21.0.7-macosx-aarch64", + "jre_path": "extension/jre/21.0.7-macosx-aarch64/bin/java", + "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", + "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", + "jdtls_readonly_config_path": "extension/server/config_mac_arm", + }, + "osx-x64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-x64-1.42.0-561.vsix", + "archiveType": "zip", + "relative_extraction_path": "vscode-java", + "jre_home_path": "extension/jre/21.0.7-macosx-x86_64", + "jre_path": "extension/jre/21.0.7-macosx-x86_64/bin/java", + "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", + "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", + "jdtls_readonly_config_path": "extension/server/config_mac", + }, + "linux-arm64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-arm64-1.42.0-561.vsix", + "archiveType": "zip", + "relative_extraction_path": "vscode-java", + }, + "linux-x64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-x64-1.42.0-561.vsix", + "archiveType": "zip", + "relative_extraction_path": "vscode-java", + "jre_home_path": "extension/jre/21.0.7-linux-x86_64", + "jre_path": "extension/jre/21.0.7-linux-x86_64/bin/java", + "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", + "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", + "jdtls_readonly_config_path": "extension/server/config_linux", + }, + "win-x64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-win32-x64-1.42.0-561.vsix", + "archiveType": "zip", + "relative_extraction_path": "vscode-java", + "jre_home_path": "extension/jre/21.0.7-win32-x86_64", + "jre_path": "extension/jre/21.0.7-win32-x86_64/bin/java.exe", + "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", + "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", + "jdtls_readonly_config_path": "extension/server/config_win", + }, + }, + "intellicode": { + "platform-agnostic": { + "url": "https://VisualStudioExptTeam.gallery.vsassets.io/_apis/public/gallery/publisher/VisualStudioExptTeam/extension/vscodeintellicode/1.2.30/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage", + "alternate_url": "https://marketplace.visualstudio.com/_apis/public/gallery/publishers/VisualStudioExptTeam/vsextensions/vscodeintellicode/1.2.30/vspackage", + "archiveType": "zip", + "relative_extraction_path": "intellicode", + "intellicode_jar_path": "extension/dist/com.microsoft.jdtls.intellicode.core-0.7.0.jar", + "intellisense_members_path": "extension/dist/bundledModels/java_intellisense-members", + } + }, + } + + # assert platformId.value in [ + # "linux-x64", + # "win-x64", + # ], "Only linux-x64 platform is supported for in multilspy at the moment" + + gradle_path = str( + PurePath( + cls.ls_resources_dir(), + "gradle-7.3.3", + ) + ) + + if not os.path.exists(gradle_path): + FileUtils.download_and_extract_archive( + logger, + runtime_dependencies["gradle"]["platform-agnostic"]["url"], + str(PurePath(gradle_path).parent), + runtime_dependencies["gradle"]["platform-agnostic"]["archiveType"], + ) + + 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"])) + 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"])) + lombok_jar_path = str(PurePath(vscode_java_path, dependency["lombok_jar_path"])) + jdtls_launcher_jar_path = str(PurePath(vscode_java_path, dependency["jdtls_launcher_jar_path"])) + jdtls_readonly_config_path = str(PurePath(vscode_java_path, dependency["jdtls_readonly_config_path"])) + if not all( + [ + os.path.exists(vscode_java_path), + os.path.exists(jre_home_path), + os.path.exists(jre_path), + os.path.exists(lombok_jar_path), + os.path.exists(jdtls_launcher_jar_path), + os.path.exists(jdtls_readonly_config_path), + ] + ): + FileUtils.download_and_extract_archive(logger, dependency["url"], vscode_java_path, dependency["archiveType"]) + + os.chmod(jre_path, stat.S_IEXEC) + + assert os.path.exists(vscode_java_path) + assert os.path.exists(jre_home_path) + assert os.path.exists(jre_path) + assert os.path.exists(lombok_jar_path) + assert os.path.exists(jdtls_launcher_jar_path) + 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"])) + 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"])) + if not all( + [ + os.path.exists(intellicode_directory_path), + os.path.exists(intellicode_jar_path), + os.path.exists(intellisense_members_path), + ] + ): + FileUtils.download_and_extract_archive(logger, dependency["url"], intellicode_directory_path, dependency["archiveType"]) + + assert os.path.exists(intellicode_directory_path) + assert os.path.exists(intellicode_jar_path) + assert os.path.exists(intellisense_members_path) + + return RuntimeDependencyPaths( + gradle_path=gradle_path, + lombok_jar_path=lombok_jar_path, + jre_path=jre_path, + jre_home_path=jre_home_path, + jdtls_launcher_jar_path=jdtls_launcher_jar_path, + jdtls_readonly_config_path=jdtls_readonly_config_path, + intellicode_jar_path=intellicode_jar_path, + intellisense_members_path=intellisense_members_path, + ) + + def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: + """ + Returns the initialize parameters for the EclipseJDTLS server. + """ + # Look into https://github.com/eclipse/eclipse.jdt.ls/blob/master/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/Preferences.java to understand all the options available + + if not os.path.isabs(repository_absolute_path): + repository_absolute_path = os.path.abspath(repository_absolute_path) + repo_uri = pathlib.Path(repository_absolute_path).as_uri() + + initialize_params = { + "locale": "en", + "rootPath": repository_absolute_path, + "rootUri": pathlib.Path(repository_absolute_path).as_uri(), + "capabilities": { + "workspace": { + "applyEdit": True, + "workspaceEdit": { + "documentChanges": True, + "resourceOperations": ["create", "rename", "delete"], + "failureHandling": "textOnlyTransactional", + "normalizesLineEndings": True, + "changeAnnotationSupport": {"groupsOnLabel": True}, + }, + "didChangeConfiguration": {"dynamicRegistration": True}, + "didChangeWatchedFiles": {"dynamicRegistration": True, "relativePatternSupport": True}, + "symbol": { + "dynamicRegistration": True, + "symbolKind": {"valueSet": list(range(1, 27))}, + "tagSupport": {"valueSet": [1]}, + "resolveSupport": {"properties": ["location.range"]}, + }, + "codeLens": {"refreshSupport": True}, + "executeCommand": {"dynamicRegistration": True}, + "configuration": True, + "workspaceFolders": True, + "semanticTokens": {"refreshSupport": True}, + "fileOperations": { + "dynamicRegistration": True, + "didCreate": True, + "didRename": True, + "didDelete": True, + "willCreate": True, + "willRename": True, + "willDelete": True, + }, + "inlineValue": {"refreshSupport": True}, + "inlayHint": {"refreshSupport": True}, + "diagnostics": {"refreshSupport": True}, + }, + "textDocument": { + "publishDiagnostics": { + "relatedInformation": True, + "versionSupport": False, + "tagSupport": {"valueSet": [1, 2]}, + "codeDescriptionSupport": True, + "dataSupport": True, + }, + "synchronization": {"dynamicRegistration": True, "willSave": True, "willSaveWaitUntil": True, "didSave": True}, + # TODO: we have an assert that completion provider is not included in the capabilities at server startup + # Removing this will cause the assert to fail. Investigate why this is the case, simplify config + "completion": { + "dynamicRegistration": True, + "contextSupport": True, + "completionItem": { + "snippetSupport": False, + "commitCharactersSupport": True, + "documentationFormat": ["markdown", "plaintext"], + "deprecatedSupport": True, + "preselectSupport": True, + "tagSupport": {"valueSet": [1]}, + "insertReplaceSupport": False, + "resolveSupport": {"properties": ["documentation", "detail", "additionalTextEdits"]}, + "insertTextModeSupport": {"valueSet": [1, 2]}, + "labelDetailsSupport": True, + }, + "insertTextMode": 2, + "completionItemKind": { + "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] + }, + "completionList": {"itemDefaults": ["commitCharacters", "editRange", "insertTextFormat", "insertTextMode"]}, + }, + "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]}, + "signatureHelp": { + "dynamicRegistration": True, + "signatureInformation": { + "documentationFormat": ["markdown", "plaintext"], + "parameterInformation": {"labelOffsetSupport": True}, + "activeParameterSupport": True, + }, + }, + "definition": {"dynamicRegistration": True, "linkSupport": True}, + "references": {"dynamicRegistration": True}, + "documentSymbol": { + "dynamicRegistration": True, + "symbolKind": {"valueSet": list(range(1, 27))}, + "hierarchicalDocumentSymbolSupport": True, + "tagSupport": {"valueSet": [1]}, + "labelSupport": True, + }, + "rename": { + "dynamicRegistration": True, + "prepareSupport": True, + "prepareSupportDefaultBehavior": 1, + "honorsChangeAnnotations": True, + }, + "documentLink": {"dynamicRegistration": True, "tooltipSupport": True}, + "typeDefinition": {"dynamicRegistration": True, "linkSupport": True}, + "implementation": {"dynamicRegistration": True, "linkSupport": True}, + "colorProvider": {"dynamicRegistration": True}, + "declaration": {"dynamicRegistration": True, "linkSupport": True}, + "selectionRange": {"dynamicRegistration": True}, + "callHierarchy": {"dynamicRegistration": True}, + "semanticTokens": { + "dynamicRegistration": True, + "tokenTypes": [ + "namespace", + "type", + "class", + "enum", + "interface", + "struct", + "typeParameter", + "parameter", + "variable", + "property", + "enumMember", + "event", + "function", + "method", + "macro", + "keyword", + "modifier", + "comment", + "string", + "number", + "regexp", + "operator", + "decorator", + ], + "tokenModifiers": [ + "declaration", + "definition", + "readonly", + "static", + "deprecated", + "abstract", + "async", + "modification", + "documentation", + "defaultLibrary", + ], + "formats": ["relative"], + "requests": {"range": True, "full": {"delta": True}}, + "multilineTokenSupport": False, + "overlappingTokenSupport": False, + "serverCancelSupport": True, + "augmentsSyntaxTokens": True, + }, + "typeHierarchy": {"dynamicRegistration": True}, + "inlineValue": {"dynamicRegistration": True}, + "diagnostic": {"dynamicRegistration": True, "relatedDocumentSupport": False}, + }, + "general": { + "staleRequestSupport": { + "cancel": True, + "retryOnContentModified": [ + "textDocument/semanticTokens/full", + "textDocument/semanticTokens/range", + "textDocument/semanticTokens/full/delta", + ], + }, + "regularExpressions": {"engine": "ECMAScript", "version": "ES2020"}, + "positionEncodings": ["utf-16"], + }, + "notebookDocument": {"synchronization": {"dynamicRegistration": True, "executionSummarySupport": True}}, + }, + "initializationOptions": { + "bundles": ["intellicode-core.jar"], + "settings": { + "java": { + "home": None, + "jdt": { + "ls": { + "java": {"home": None}, + "vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx1G -Xms100m -Xlog:disable", + "lombokSupport": {"enabled": True}, + "protobufSupport": {"enabled": True}, + "androidSupport": {"enabled": True}, + } + }, + "errors": {"incompleteClasspath": {"severity": "error"}}, + "configuration": { + "checkProjectSettingsExclusions": False, + "updateBuildConfiguration": "interactive", + "maven": { + "userSettings": None, + "globalSettings": None, + "notCoveredPluginExecutionSeverity": "warning", + "defaultMojoExecutionAction": "ignore", + }, + "workspaceCacheLimit": 90, + "runtimes": [ + {"name": "JavaSE-21", "path": "static/vscode-java/extension/jre/21.0.7-linux-x86_64", "default": True} + ], + }, + "trace": {"server": "verbose"}, + "import": { + "maven": { + "enabled": True, + "offline": {"enabled": False}, + "disableTestClasspathFlag": False, + }, + "gradle": { + "enabled": True, + "wrapper": {"enabled": True}, + "version": None, + "home": "abs(static/gradle-7.3.3)", + "java": {"home": "abs(static/launch_jres/21.0.7-linux-x86_64)"}, + "offline": {"enabled": False}, + "arguments": None, + "jvmArguments": None, + "user": {"home": None}, + "annotationProcessing": {"enabled": True}, + }, + "exclusions": [ + "**/node_modules/**", + "**/.metadata/**", + "**/archetype-resources/**", + "**/META-INF/maven/**", + ], + "generatesMetadataFilesAtProjectRoot": False, + }, + "maven": {"downloadSources": True, "updateSnapshots": True}, + "eclipse": {"downloadSources": True}, + "signatureHelp": {"enabled": True, "description": {"enabled": True}}, + "implementationsCodeLens": {"enabled": True}, + "format": { + "enabled": True, + "settings": {"url": None, "profile": None}, + "comments": {"enabled": True}, + "onType": {"enabled": True}, + "insertSpaces": True, + "tabSize": 4, + }, + "saveActions": {"organizeImports": False}, + "project": { + "referencedLibraries": ["lib/**/*.jar"], + "importOnFirstTimeStartup": "automatic", + "importHint": True, + "resourceFilters": ["node_modules", "\\.git"], + "encoding": "ignore", + "exportJar": {"targetPath": "${workspaceFolder}/${workspaceFolderBasename}.jar"}, + }, + "contentProvider": {"preferred": None}, + "autobuild": {"enabled": True}, + "maxConcurrentBuilds": 1, + "selectionRange": {"enabled": True}, + "showBuildStatusOnStart": {"enabled": "notification"}, + "server": {"launchMode": "Standard"}, + "sources": {"organizeImports": {"starThreshold": 99, "staticStarThreshold": 99}}, + "imports": {"gradle": {"wrapper": {"checksums": []}}}, + "templates": {"fileHeader": [], "typeComment": []}, + "references": {"includeAccessors": True, "includeDecompiledSources": True}, + "typeHierarchy": {"lazyLoad": False}, + "settings": {"url": None}, + "symbols": {"includeSourceMethodDeclarations": False}, + "inlayHints": {"parameterNames": {"enabled": "literals", "exclusions": []}}, + "codeAction": {"sortMembers": {"avoidVolatileChanges": True}}, + "compile": { + "nullAnalysis": { + "nonnull": [ + "javax.annotation.Nonnull", + "org.eclipse.jdt.annotation.NonNull", + "org.springframework.lang.NonNull", + ], + "nullable": [ + "javax.annotation.Nullable", + "org.eclipse.jdt.annotation.Nullable", + "org.springframework.lang.Nullable", + ], + "mode": "automatic", + } + }, + "sharedIndexes": {"enabled": "auto", "location": ""}, + "silentNotification": False, + "dependency": { + "showMembers": False, + "syncWithFolderExplorer": True, + "autoRefresh": True, + "refreshDelay": 2000, + "packagePresentation": "flat", + }, + "help": {"firstView": "auto", "showReleaseNotes": True, "collectErrorLog": False}, + "test": {"defaultConfig": "", "config": {}}, + } + }, + }, + "trace": "verbose", + "processId": os.getpid(), + "workspaceFolders": [ + { + "uri": repo_uri, + "name": os.path.basename(repository_absolute_path), + } + ], + } + + initialize_params["initializationOptions"]["workspaceFolders"] = [repo_uri] + bundles = [self.runtime_dependency_paths.intellicode_jar_path] + initialize_params["initializationOptions"]["bundles"] = bundles + initialize_params["initializationOptions"]["settings"]["java"]["configuration"]["runtimes"] = [ + {"name": "JavaSE-21", "path": self.runtime_dependency_paths.jre_home_path, "default": True} + ] + + for runtime in initialize_params["initializationOptions"]["settings"]["java"]["configuration"]["runtimes"]: + assert "name" in runtime + assert "path" in runtime + assert os.path.exists(runtime["path"]), f"Runtime required for eclipse_jdtls at path {runtime['path']} does not exist" + + gradle_settings = initialize_params["initializationOptions"]["settings"]["java"]["import"]["gradle"] + gradle_settings["home"] = self.runtime_dependency_paths.gradle_path + gradle_settings["java"]["home"] = self.runtime_dependency_paths.jre_path + return initialize_params + + def _start_server(self): + """ + Starts the Eclipse JDTLS Language Server + """ + + def register_capability_handler(params): + assert "registrations" in params + for registration in params["registrations"]: + if registration["method"] == "textDocument/completion": + assert registration["registerOptions"]["resolveProvider"] == True + assert registration["registerOptions"]["triggerCharacters"] == [ + ".", + "@", + "#", + "*", + " ", + ] + self.completions_available.set() + if registration["method"] == "workspace/executeCommand": + if "java.intellicode.enable" in registration["registerOptions"]["commands"]: + self.intellicode_enable_command_available.set() + return + + def lang_status_handler(params): + # TODO: Should we wait for + # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}} + # Before proceeding? + if params["type"] == "ServiceReady" and params["message"] == "ServiceReady": + self.service_ready_event.set() + + def execute_client_command_handler(params): + assert params["command"] == "_java.reloadBundles.command" + assert params["arguments"] == [] + return [] + + def window_log_message(msg): + self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + def do_nothing(params): + return + + self.server.on_request("client/registerCapability", register_capability_handler) + self.server.on_notification("language/status", lang_status_handler) + self.server.on_notification("window/logMessage", window_log_message) + self.server.on_request("workspace/executeClientCommand", execute_client_command_handler) + self.server.on_notification("$/progress", do_nothing) + self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + self.server.on_notification("language/actionableNotification", do_nothing) + + self.logger.log("Starting EclipseJDTLS server process", logging.INFO) + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) + + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) + assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 + assert "completionProvider" not in init_response["capabilities"] + assert "executeCommandProvider" not in init_response["capabilities"] + + self.server.notify.initialized({}) + + self.server.notify.workspace_did_change_configuration({"settings": initialize_params["initializationOptions"]["settings"]}) + + self.intellicode_enable_command_available.wait() + + java_intellisense_members_path = self.runtime_dependency_paths.intellisense_members_path + assert os.path.exists(java_intellisense_members_path) + intellicode_enable_result = self.server.send.execute_command( + { + "command": "java.intellicode.enable", + "arguments": [True, java_intellisense_members_path], + } + ) + assert intellicode_enable_result + + # TODO: Add comments about why we wait here, and how this can be optimized + self.service_ready_event.wait() diff --git a/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py b/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py deleted file mode 100644 index 7453bf15..00000000 --- a/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py +++ /dev/null @@ -1,388 +0,0 @@ -""" -Provides Java specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Java. -""" - -import dataclasses -import json -import logging -import os -import pathlib -import shutil -import stat -import threading -import uuid -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.lsp_protocol_handler.lsp_types import InitializeParams -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo -from solidlsp.settings import SolidLSPSettings - - -@dataclasses.dataclass -class RuntimeDependencyPaths: - """ - Stores the paths to the runtime dependencies of EclipseJDTLS - """ - - gradle_path: str - lombok_jar_path: str - jre_path: str - jre_home_path: str - jdtls_launcher_jar_path: str - jdtls_readonly_config_path: str - intellicode_jar_path: str - intellisense_members_path: str - - -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): - """ - 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 - - # ws_dir is the workspace directory for the EclipseJDTLS server - ws_dir = str( - PurePath( - SolidLSPSettings.get_language_server_directory(), - "EclipseJDTLS", - "workspaces", - uuid.uuid4().hex, - ) - ) - - # 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")) - - 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")) - - jdtls_readonly_config_path = self.runtime_dependency_paths.jdtls_readonly_config_path - - if not os.path.exists(jdtls_config_path): - shutil.copytree(jdtls_readonly_config_path, jdtls_config_path) - - for static_path in [ - jre_path, - lombok_jar_path, - jdtls_launcher_jar, - jdtls_config_path, - jdtls_readonly_config_path, - ]: - assert os.path.exists(static_path), static_path - - # 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 - cmd = " ".join( - [ - jre_path, - "--add-modules=ALL-SYSTEM", - "--add-opens", - "java.base/java.util=ALL-UNNAMED", - "--add-opens", - "java.base/java.lang=ALL-UNNAMED", - "--add-opens", - "java.base/sun.nio.fs=ALL-UNNAMED", - "-Declipse.application=org.eclipse.jdt.ls.core.id1", - "-Dosgi.bundles.defaultStartLevel=4", - "-Declipse.product=org.eclipse.jdt.ls.core.product", - "-Djava.import.generatesMetadataFilesAtProjectRoot=false", - "-Dfile.encoding=utf8", - "-noverify", - "-XX:+UseParallelGC", - "-XX:GCTimeRatio=4", - "-XX:AdaptiveSizePolicyWeight=90", - "-Dsun.zip.disableMemoryMapping=true", - "-Djava.lsp.joinOnCompletion=true", - "-Xmx3G", - "-Xms100m", - "-Xlog:disable", - "-Dlog.level=ALL", - f'"-javaagent:{lombok_jar_path}"', - f'"-Djdt.core.sharedIndexLocation={shared_cache_location}"', - "-jar", - f'"{jdtls_launcher_jar}"', - "-configuration", - f'"{jdtls_config_path}"', - "-data", - f'"{data_dir}"', - ] - ) - - self.service_ready_event = threading.Event() - 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") - - @override - def is_ignored_dirname(self, dirname: str) -> bool: - # Ignore common Java build directories from different build tools: - # - Maven: target - # - Gradle: build, .gradle - # - Eclipse: bin, .settings - # - IntelliJ IDEA: out, .idea - # - General: classes, dist, lib - return super().is_ignored_dirname(dirname) or dirname in [ - "target", # Maven - "build", # Gradle - "bin", # Eclipse - "out", # IntelliJ IDEA - "classes", # General - "dist", # General - "lib", # General - ] - - def setupRuntimeDependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> RuntimeDependencyPaths: - """ - Setup runtime dependencies for EclipseJDTLS. - """ - 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) - - # assert platformId.value in [ - # "linux-x64", - # "win-x64", - # ], "Only linux-x64 platform is supported for in multilspy at the moment" - - gradle_path = str( - PurePath( - os.path.abspath(os.path.dirname(__file__)), - "static/gradle-7.3.3", - ) - ) - - if not os.path.exists(gradle_path): - FileUtils.download_and_extract_archive( - logger, - runtimeDependencies["gradle"]["platform-agnostic"]["url"], - str(PurePath(gradle_path).parent), - runtimeDependencies["gradle"]["platform-agnostic"]["archiveType"], - ) - - assert os.path.exists(gradle_path) - - dependency = runtimeDependencies["vscode-java"][platformId.value] - vscode_java_path = str(PurePath(os.path.abspath(os.path.dirname(__file__)), "static", 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"])) - lombok_jar_path = str(PurePath(vscode_java_path, dependency["lombok_jar_path"])) - jdtls_launcher_jar_path = str(PurePath(vscode_java_path, dependency["jdtls_launcher_jar_path"])) - jdtls_readonly_config_path = str(PurePath(vscode_java_path, dependency["jdtls_readonly_config_path"])) - if not all( - [ - os.path.exists(vscode_java_path), - os.path.exists(jre_home_path), - os.path.exists(jre_path), - os.path.exists(lombok_jar_path), - os.path.exists(jdtls_launcher_jar_path), - os.path.exists(jdtls_readonly_config_path), - ] - ): - FileUtils.download_and_extract_archive(logger, dependency["url"], vscode_java_path, dependency["archiveType"]) - - os.chmod(jre_path, stat.S_IEXEC) - - assert os.path.exists(vscode_java_path) - assert os.path.exists(jre_home_path) - assert os.path.exists(jre_path) - assert os.path.exists(lombok_jar_path) - assert os.path.exists(jdtls_launcher_jar_path) - assert os.path.exists(jdtls_readonly_config_path) - - dependency = runtimeDependencies["intellicode"]["platform-agnostic"] - intellicode_directory_path = str( - PurePath(os.path.abspath(os.path.dirname(__file__)), "static", 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"])) - if not all( - [ - os.path.exists(intellicode_directory_path), - os.path.exists(intellicode_jar_path), - os.path.exists(intellisense_members_path), - ] - ): - FileUtils.download_and_extract_archive(logger, dependency["url"], intellicode_directory_path, dependency["archiveType"]) - - assert os.path.exists(intellicode_directory_path) - assert os.path.exists(intellicode_jar_path) - assert os.path.exists(intellisense_members_path) - - return RuntimeDependencyPaths( - gradle_path=gradle_path, - lombok_jar_path=lombok_jar_path, - jre_path=jre_path, - jre_home_path=jre_home_path, - jdtls_launcher_jar_path=jdtls_launcher_jar_path, - jdtls_readonly_config_path=jdtls_readonly_config_path, - intellicode_jar_path=intellicode_jar_path, - intellisense_members_path=intellisense_members_path, - ) - - def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: - """ - Returns the initialize parameters for the EclipseJDTLS server. - """ - # Look into https://github.com/eclipse/eclipse.jdt.ls/blob/master/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/Preferences.java to understand all the options available - with open(str(PurePath(os.path.dirname(__file__), "initialize_params.json")), encoding="utf-8") as f: - d: InitializeParams = json.load(f) - - del d["_description"] - - if not os.path.isabs(repository_absolute_path): - repository_absolute_path = os.path.abspath(repository_absolute_path) - - assert d["processId"] == "os.getpid()" - d["processId"] = os.getpid() - - assert d["rootPath"] == "repository_absolute_path" - d["rootPath"] = repository_absolute_path - - assert d["rootUri"] == "pathlib.Path(repository_absolute_path).as_uri()" - d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() - - assert d["initializationOptions"]["workspaceFolders"] == "[pathlib.Path(repository_absolute_path).as_uri()]" - d["initializationOptions"]["workspaceFolders"] = [pathlib.Path(repository_absolute_path).as_uri()] - - assert ( - d["workspaceFolders"] - == '[\n {\n "uri": pathlib.Path(repository_absolute_path).as_uri(),\n "name": os.path.basename(repository_absolute_path),\n }\n ]' - ) - d["workspaceFolders"] = [ - { - "uri": pathlib.Path(repository_absolute_path).as_uri(), - "name": os.path.basename(repository_absolute_path), - } - ] - - assert d["initializationOptions"]["bundles"] == ["intellicode-core.jar"] - bundles = [self.runtime_dependency_paths.intellicode_jar_path] - d["initializationOptions"]["bundles"] = bundles - - assert d["initializationOptions"]["settings"]["java"]["configuration"]["runtimes"] == [ - {"name": "JavaSE-21", "path": "static/vscode-java/extension/jre/21.0.7-linux-x86_64", "default": True} - ] - d["initializationOptions"]["settings"]["java"]["configuration"]["runtimes"] = [ - {"name": "JavaSE-21", "path": self.runtime_dependency_paths.jre_home_path, "default": True} - ] - - for runtime in d["initializationOptions"]["settings"]["java"]["configuration"]["runtimes"]: - assert "name" in runtime - assert "path" in runtime - assert os.path.exists(runtime["path"]), f"Runtime required for eclipse_jdtls at path {runtime['path']} does not exist" - - assert d["initializationOptions"]["settings"]["java"]["import"]["gradle"]["home"] == "abs(static/gradle-7.3.3)" - d["initializationOptions"]["settings"]["java"]["import"]["gradle"]["home"] = self.runtime_dependency_paths.gradle_path - - d["initializationOptions"]["settings"]["java"]["import"]["gradle"]["java"]["home"] = self.runtime_dependency_paths.jre_path - - return d - - def _start_server(self): - """ - Starts the Eclipse JDTLS Language Server - """ - - def register_capability_handler(params): - assert "registrations" in params - for registration in params["registrations"]: - if registration["method"] == "textDocument/completion": - assert registration["registerOptions"]["resolveProvider"] == True - assert registration["registerOptions"]["triggerCharacters"] == [ - ".", - "@", - "#", - "*", - " ", - ] - self.completions_available.set() - if registration["method"] == "workspace/executeCommand": - if "java.intellicode.enable" in registration["registerOptions"]["commands"]: - self.intellicode_enable_command_available.set() - return - - def lang_status_handler(params): - # TODO: Should we wait for - # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}} - # Before proceeding? - if params["type"] == "ServiceReady" and params["message"] == "ServiceReady": - self.service_ready_event.set() - - def execute_client_command_handler(params): - assert params["command"] == "_java.reloadBundles.command" - assert params["arguments"] == [] - return [] - - def window_log_message(msg): - self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) - - def do_nothing(params): - return - - self.server.on_request("client/registerCapability", register_capability_handler) - self.server.on_notification("language/status", lang_status_handler) - self.server.on_notification("window/logMessage", window_log_message) - self.server.on_request("workspace/executeClientCommand", execute_client_command_handler) - self.server.on_notification("$/progress", do_nothing) - self.server.on_notification("textDocument/publishDiagnostics", do_nothing) - self.server.on_notification("language/actionableNotification", do_nothing) - - self.logger.log("Starting EclipseJDTLS server process", logging.INFO) - self.server.start() - initialize_params = self._get_initialize_params(self.repository_root_path) - - self.logger.log( - "Sending initialize request from LSP client to LSP server and awaiting response", - logging.INFO, - ) - init_response = self.server.send.initialize(initialize_params) - assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 - assert "completionProvider" not in init_response["capabilities"] - assert "executeCommandProvider" not in init_response["capabilities"] - - self.server.notify.initialized({}) - - self.server.notify.workspace_did_change_configuration({"settings": initialize_params["initializationOptions"]["settings"]}) - - self.intellicode_enable_command_available.wait() - - java_intellisense_members_path = self.runtime_dependency_paths.intellisense_members_path - assert os.path.exists(java_intellisense_members_path) - intellicode_enable_result = self.server.send.execute_command( - { - "command": "java.intellicode.enable", - "arguments": [True, java_intellisense_members_path], - } - ) - assert intellicode_enable_result - - # TODO: Add comments about why we wait here, and how this can be optimized - self.service_ready_event.wait() diff --git a/src/solidlsp/language_servers/eclipse_jdtls/initialize_params.json b/src/solidlsp/language_servers/eclipse_jdtls/initialize_params.json deleted file mode 100644 index b65b7b62..00000000 --- a/src/solidlsp/language_servers/eclipse_jdtls/initialize_params.json +++ /dev/null @@ -1,849 +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()", - "clientInfo": { - "name": "Visual Studio Code - Insiders", - "version": "1.77.0-insider" - }, - "locale": "en", - "rootPath": "repository_absolute_path", - "rootUri": "pathlib.Path(repository_absolute_path).as_uri()", - "capabilities": { - "workspace": { - "applyEdit": true, - "workspaceEdit": { - "documentChanges": true, - "resourceOperations": [ - "create", - "rename", - "delete" - ], - "failureHandling": "textOnlyTransactional", - "normalizesLineEndings": true, - "changeAnnotationSupport": { - "groupsOnLabel": true - } - }, - "didChangeConfiguration": { - "dynamicRegistration": true - }, - "didChangeWatchedFiles": { - "dynamicRegistration": true, - "relativePatternSupport": true - }, - "symbol": { - "dynamicRegistration": 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 - ] - }, - "tagSupport": { - "valueSet": [ - 1 - ] - }, - "resolveSupport": { - "properties": [ - "location.range" - ] - } - }, - "codeLens": { - "refreshSupport": true - }, - "executeCommand": { - "dynamicRegistration": true - }, - "configuration": true, - "workspaceFolders": true, - "semanticTokens": { - "refreshSupport": true - }, - "fileOperations": { - "dynamicRegistration": true, - "didCreate": true, - "didRename": true, - "didDelete": true, - "willCreate": true, - "willRename": true, - "willDelete": true - }, - "inlineValue": { - "refreshSupport": true - }, - "inlayHint": { - "refreshSupport": true - }, - "diagnostics": { - "refreshSupport": true - } - }, - "textDocument": { - "publishDiagnostics": { - "relatedInformation": true, - "versionSupport": false, - "tagSupport": { - "valueSet": [ - 1, - 2 - ] - }, - "codeDescriptionSupport": true, - "dataSupport": true - }, - "synchronization": { - "dynamicRegistration": true, - "willSave": true, - "willSaveWaitUntil": true, - "didSave": true - }, - "completion": { - "dynamicRegistration": true, - "contextSupport": true, - "completionItem": { - "snippetSupport": false, - "commitCharactersSupport": true, - "documentationFormat": [ - "markdown", - "plaintext" - ], - "deprecatedSupport": true, - "preselectSupport": true, - "tagSupport": { - "valueSet": [ - 1 - ] - }, - "insertReplaceSupport": false, - "resolveSupport": { - "properties": [ - "documentation", - "detail", - "additionalTextEdits" - ] - }, - "insertTextModeSupport": { - "valueSet": [ - 1, - 2 - ] - }, - "labelDetailsSupport": true - }, - "insertTextMode": 2, - "completionItemKind": { - "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 - ] - }, - "completionList": { - "itemDefaults": [ - "commitCharacters", - "editRange", - "insertTextFormat", - "insertTextMode" - ] - } - }, - "hover": { - "dynamicRegistration": true, - "contentFormat": [ - "markdown", - "plaintext" - ] - }, - "signatureHelp": { - "dynamicRegistration": true, - "signatureInformation": { - "documentationFormat": [ - "markdown", - "plaintext" - ], - "parameterInformation": { - "labelOffsetSupport": true - }, - "activeParameterSupport": true - }, - "contextSupport": true - }, - "definition": { - "dynamicRegistration": true, - "linkSupport": true - }, - "references": { - "dynamicRegistration": true - }, - "documentHighlight": { - "dynamicRegistration": true - }, - "documentSymbol": { - "dynamicRegistration": 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 - ] - }, - "hierarchicalDocumentSymbolSupport": true, - "tagSupport": { - "valueSet": [ - 1 - ] - }, - "labelSupport": true - }, - "codeAction": { - "dynamicRegistration": true, - "isPreferredSupport": true, - "disabledSupport": true, - "dataSupport": true, - "resolveSupport": { - "properties": [ - "edit" - ] - }, - "codeActionLiteralSupport": { - "codeActionKind": { - "valueSet": [ - "", - "quickfix", - "refactor", - "refactor.extract", - "refactor.inline", - "refactor.rewrite", - "source", - "source.organizeImports" - ] - } - }, - "honorsChangeAnnotations": false - }, - "codeLens": { - "dynamicRegistration": true - }, - "formatting": { - "dynamicRegistration": true - }, - "rangeFormatting": { - "dynamicRegistration": true - }, - "onTypeFormatting": { - "dynamicRegistration": true - }, - "rename": { - "dynamicRegistration": true, - "prepareSupport": true, - "prepareSupportDefaultBehavior": 1, - "honorsChangeAnnotations": true - }, - "documentLink": { - "dynamicRegistration": true, - "tooltipSupport": true - }, - "typeDefinition": { - "dynamicRegistration": true, - "linkSupport": true - }, - "implementation": { - "dynamicRegistration": true, - "linkSupport": true - }, - "colorProvider": { - "dynamicRegistration": true - }, - "foldingRange": { - "dynamicRegistration": true, - "rangeLimit": 5000, - "lineFoldingOnly": true, - "foldingRangeKind": { - "valueSet": [ - "comment", - "imports", - "region" - ] - }, - "foldingRange": { - "collapsedText": false - } - }, - "declaration": { - "dynamicRegistration": true, - "linkSupport": true - }, - "selectionRange": { - "dynamicRegistration": true - }, - "callHierarchy": { - "dynamicRegistration": true - }, - "semanticTokens": { - "dynamicRegistration": true, - "tokenTypes": [ - "namespace", - "type", - "class", - "enum", - "interface", - "struct", - "typeParameter", - "parameter", - "variable", - "property", - "enumMember", - "event", - "function", - "method", - "macro", - "keyword", - "modifier", - "comment", - "string", - "number", - "regexp", - "operator", - "decorator" - ], - "tokenModifiers": [ - "declaration", - "definition", - "readonly", - "static", - "deprecated", - "abstract", - "async", - "modification", - "documentation", - "defaultLibrary" - ], - "formats": [ - "relative" - ], - "requests": { - "range": true, - "full": { - "delta": true - } - }, - "multilineTokenSupport": false, - "overlappingTokenSupport": false, - "serverCancelSupport": true, - "augmentsSyntaxTokens": true - }, - "linkedEditingRange": { - "dynamicRegistration": true - }, - "typeHierarchy": { - "dynamicRegistration": true - }, - "inlineValue": { - "dynamicRegistration": true - }, - "inlayHint": { - "dynamicRegistration": true, - "resolveSupport": { - "properties": [ - "tooltip", - "textEdits", - "label.tooltip", - "label.location", - "label.command" - ] - } - }, - "diagnostic": { - "dynamicRegistration": true, - "relatedDocumentSupport": false - } - }, - "window": { - "showMessage": { - "messageActionItem": { - "additionalPropertiesSupport": true - } - }, - "showDocument": { - "support": true - }, - "workDoneProgress": true - }, - "general": { - "staleRequestSupport": { - "cancel": true, - "retryOnContentModified": [ - "textDocument/semanticTokens/full", - "textDocument/semanticTokens/range", - "textDocument/semanticTokens/full/delta" - ] - }, - "regularExpressions": { - "engine": "ECMAScript", - "version": "ES2020" - }, - "markdown": { - "parser": "marked", - "version": "1.1.0" - }, - "positionEncodings": [ - "utf-16" - ] - }, - "notebookDocument": { - "synchronization": { - "dynamicRegistration": true, - "executionSummarySupport": true - } - } - }, - "initializationOptions": { - "bundles": [ - "intellicode-core.jar" - ], - "workspaceFolders": "[pathlib.Path(repository_absolute_path).as_uri()]", - "settings": { - "java": { - "home": null, - "jdt": { - "ls": { - "java": { - "home": null - }, - "vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx1G -Xms100m -Xlog:disable", - "lombokSupport": { - "enabled": true - }, - "protobufSupport": { - "enabled": true - }, - "androidSupport": { - "enabled": true - } - } - }, - "errors": { - "incompleteClasspath": { - "severity": "error" - } - }, - "configuration": { - "checkProjectSettingsExclusions": false, - "updateBuildConfiguration": "interactive", - "maven": { - "userSettings": null, - "globalSettings": null, - "notCoveredPluginExecutionSeverity": "warning", - "defaultMojoExecutionAction": "ignore" - }, - "workspaceCacheLimit": 90, - "runtimes": [ - { - "name": "JavaSE-21", - "path": "static/vscode-java/extension/jre/21.0.7-linux-x86_64", - "default": true - } - ] - }, - "trace": { - "server": "verbose" - }, - "import": { - "maven": { - "enabled": true, - "offline": { - "enabled": false - }, - "disableTestClasspathFlag": false - }, - "gradle": { - "enabled": true, - "wrapper": { - "enabled": true - }, - "version": null, - "home": "abs(static/gradle-7.3.3)", - "java": { - "home": "abs(static/launch_jres/21.0.7-linux-x86_64)" - }, - "offline": { - "enabled": false - }, - "arguments": null, - "jvmArguments": null, - "user": { - "home": null - }, - "annotationProcessing": { - "enabled": true - } - }, - "exclusions": [ - "**/node_modules/**", - "**/.metadata/**", - "**/archetype-resources/**", - "**/META-INF/maven/**" - ], - "generatesMetadataFilesAtProjectRoot": false - }, - "maven": { - "downloadSources": true, - "updateSnapshots": true - }, - "eclipse": { - "downloadSources": true - }, - "referencesCodeLens": { - "enabled": true - }, - "signatureHelp": { - "enabled": true, - "description": { - "enabled": true - } - }, - "implementationsCodeLens": { - "enabled": true - }, - "format": { - "enabled": true, - "settings": { - "url": null, - "profile": null - }, - "comments": { - "enabled": true - }, - "onType": { - "enabled": true - }, - "insertSpaces": true, - "tabSize": 4 - }, - "saveActions": { - "organizeImports": false - }, - "project": { - "referencedLibraries": [ - "lib/**/*.jar" - ], - "importOnFirstTimeStartup": "automatic", - "importHint": true, - "resourceFilters": [ - "node_modules", - "\\.git" - ], - "encoding": "ignore", - "exportJar": { - "targetPath": "${workspaceFolder}/${workspaceFolderBasename}.jar" - } - }, - "contentProvider": { - "preferred": null - }, - "autobuild": { - "enabled": true - }, - "maxConcurrentBuilds": 1, - "recommendations": { - "dependency": { - "analytics": { - "show": true - } - } - }, - "completion": { - "maxResults": 0, - "enabled": true, - "guessMethodArguments": true, - "favoriteStaticMembers": [ - "org.junit.Assert.*", - "org.junit.Assume.*", - "org.junit.jupiter.api.Assertions.*", - "org.junit.jupiter.api.Assumptions.*", - "org.junit.jupiter.api.DynamicContainer.*", - "org.junit.jupiter.api.DynamicTest.*", - "org.mockito.Mockito.*", - "org.mockito.ArgumentMatchers.*", - "org.mockito.Answers.*" - ], - "filteredTypes": [ - "java.awt.*", - "com.sun.*", - "sun.*", - "jdk.*", - "org.graalvm.*", - "io.micrometer.shaded.*" - ], - "importOrder": [ - "#", - "java", - "javax", - "org", - "com", - "" - ], - "postfix": { - "enabled": false - }, - "matchCase": "off" - }, - "foldingRange": { - "enabled": true - }, - "progressReports": { - "enabled": false - }, - "codeGeneration": { - "hashCodeEquals": { - "useJava7Objects": false, - "useInstanceof": false - }, - "useBlocks": false, - "generateComments": false, - "toString": { - "template": "${object.className} [${member.name()}=${member.value}, ${otherMembers}]", - "codeStyle": "STRING_CONCATENATION", - "skipNullValues": false, - "listArrayContents": true, - "limitElements": 0 - }, - "insertionLocation": "afterCursor" - }, - "selectionRange": { - "enabled": true - }, - "showBuildStatusOnStart": { - "enabled": "notification" - }, - "server": { - "launchMode": "Standard" - }, - "sources": { - "organizeImports": { - "starThreshold": 99, - "staticStarThreshold": 99 - } - }, - "imports": { - "gradle": { - "wrapper": { - "checksums": [] - } - } - }, - "templates": { - "fileHeader": [], - "typeComment": [] - }, - "references": { - "includeAccessors": true, - "includeDecompiledSources": true - }, - "typeHierarchy": { - "lazyLoad": false - }, - "settings": { - "url": null - }, - "symbols": { - "includeSourceMethodDeclarations": false - }, - "quickfix": { - "showAt": "line" - }, - "inlayHints": { - "parameterNames": { - "enabled": "literals", - "exclusions": [] - } - }, - "codeAction": { - "sortMembers": { - "avoidVolatileChanges": true - } - }, - "compile": { - "nullAnalysis": { - "nonnull": [ - "javax.annotation.Nonnull", - "org.eclipse.jdt.annotation.NonNull", - "org.springframework.lang.NonNull" - ], - "nullable": [ - "javax.annotation.Nullable", - "org.eclipse.jdt.annotation.Nullable", - "org.springframework.lang.Nullable" - ], - "mode": "automatic" - } - }, - "cleanup": { - "actionsOnSave": [] - }, - "sharedIndexes": { - "enabled": "auto", - "location": "" - }, - "refactoring": { - "extract": { - "interface": { - "replace": true - } - } - }, - "debug": { - "logLevel": "verbose", - "settings": { - "showHex": false, - "showStaticVariables": false, - "showQualifiedNames": false, - "showLogicalStructure": true, - "showToString": true, - "maxStringLength": 0, - "numericPrecision": 0, - "hotCodeReplace": "manual", - "enableRunDebugCodeLens": true, - "forceBuildBeforeLaunch": true, - "onBuildFailureProceed": false, - "console": "integratedTerminal", - "exceptionBreakpoint": { - "skipClasses": [] - }, - "stepping": { - "skipClasses": [], - "skipSynthetics": false, - "skipStaticInitializers": false, - "skipConstructors": false - }, - "jdwp": { - "limitOfVariablesPerJdwpRequest": 100, - "requestTimeout": 3000, - "async": "auto" - }, - "vmArgs": "" - } - }, - "silentNotification": false, - "dependency": { - "showMembers": false, - "syncWithFolderExplorer": true, - "autoRefresh": true, - "refreshDelay": 2000, - "packagePresentation": "flat" - }, - "help": { - "firstView": "auto", - "showReleaseNotes": true, - "collectErrorLog": false - }, - "test": { - "defaultConfig": "", - "config": {} - } - } - }, - "extendedClientCapabilities": { - "progressReportProvider": false, - "classFileContentsSupport": true, - "overrideMethodsPromptSupport": true, - "hashCodeEqualsPromptSupport": true, - "advancedOrganizeImportsSupport": true, - "generateToStringPromptSupport": true, - "advancedGenerateAccessorsSupport": true, - "generateConstructorsPromptSupport": true, - "generateDelegateMethodsPromptSupport": true, - "advancedExtractRefactoringSupport": true, - "inferSelectionSupport": [ - "extractMethod", - "extractVariable", - "extractField" - ], - "moveRefactoringSupport": true, - "clientHoverProvider": true, - "clientDocumentSymbolProvider": true, - "gradleChecksumWrapperPromptSupport": true, - "resolveAdditionalTextEditsSupport": true, - "advancedIntroduceParameterRefactoringSupport": true, - "actionableRuntimeNotificationSupport": true, - "shouldLanguageServerExitOnShutdown": true, - "onCompletionItemSelectedCommand": "editor.action.triggerParameterHints", - "extractInterfaceSupport": true, - "advancedUpgradeGradleSupport": true - }, - "triggerFiles": [] - }, - "trace": "verbose", - "workspaceFolders": "[\n {\n \"uri\": pathlib.Path(repository_absolute_path).as_uri(),\n \"name\": os.path.basename(repository_absolute_path),\n }\n ]" -} \ No newline at end of file diff --git a/src/solidlsp/language_servers/eclipse_jdtls/runtime_dependencies.json b/src/solidlsp/language_servers/eclipse_jdtls/runtime_dependencies.json deleted file mode 100644 index 1b000405..00000000 --- a/src/solidlsp/language_servers/eclipse_jdtls/runtime_dependencies.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "_description": "This file lists the runtime dependencies for the Java Language Server", - "gradle": { - "platform-agnostic": { - "url": "https://services.gradle.org/distributions/gradle-7.3.3-bin.zip", - "archiveType": "zip", - "relative_extraction_path": "." - } - }, - "vscode-java": { - "darwin-arm64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix", - "archiveType": "zip", - "relative_extraction_path": "vscode-java" - }, - "osx-arm64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix", - "archiveType": "zip", - "relative_extraction_path": "vscode-java", - "jre_home_path": "extension/jre/21.0.7-macosx-aarch64", - "jre_path": "extension/jre/21.0.7-macosx-aarch64/bin/java", - "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", - "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", - "jdtls_readonly_config_path": "extension/server/config_mac_arm" - }, - "osx-x64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-x64-1.42.0-561.vsix", - "archiveType": "zip", - "relative_extraction_path": "vscode-java", - "jre_home_path": "extension/jre/21.0.7-macosx-x86_64", - "jre_path": "extension/jre/21.0.7-macosx-x86_64/bin/java", - "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", - "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", - "jdtls_readonly_config_path": "extension/server/config_mac" - }, - "linux-arm64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-arm64-1.42.0-561.vsix", - "archiveType": "zip", - "relative_extraction_path": "vscode-java" - }, - "linux-x64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-x64-1.42.0-561.vsix", - "archiveType": "zip", - "relative_extraction_path": "vscode-java", - "jre_home_path": "extension/jre/21.0.7-linux-x86_64", - "jre_path": "extension/jre/21.0.7-linux-x86_64/bin/java", - "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", - "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", - "jdtls_readonly_config_path": "extension/server/config_linux" - }, - "win-x64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-win32-x64-1.42.0-561.vsix", - "archiveType": "zip", - "relative_extraction_path": "vscode-java", - "jre_home_path": "extension/jre/21.0.7-win32-x86_64", - "jre_path": "extension/jre/21.0.7-win32-x86_64/bin/java.exe", - "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", - "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", - "jdtls_readonly_config_path": "extension/server/config_win" - } - }, - "intellicode": { - "platform-agnostic": { - "url": "https://VisualStudioExptTeam.gallery.vsassets.io/_apis/public/gallery/publisher/VisualStudioExptTeam/extension/vscodeintellicode/1.2.30/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage", - "alternate_url": "https://marketplace.visualstudio.com/_apis/public/gallery/publishers/VisualStudioExptTeam/vsextensions/vscodeintellicode/1.2.30/vspackage", - "archiveType": "zip", - "relative_extraction_path": "intellicode", - "intellicode_jar_path": "extension/dist/com.microsoft.jdtls.intellicode.core-0.7.0.jar", - "intellisense_members_path": "extension/dist/bundledModels/java_intellisense-members" - } - } -} diff --git a/src/solidlsp/language_servers/elixir_tools/README.md b/src/solidlsp/language_servers/elixir_tools/README.md new file mode 100644 index 00000000..b3c56d17 --- /dev/null +++ b/src/solidlsp/language_servers/elixir_tools/README.md @@ -0,0 +1,90 @@ +# Elixir Language Server Integration + +This directory contains the integration for Elixir language support using [Next LS](https://github.com/elixir-tools/next-ls) from the elixir-tools project. + +> **⚠️ Windows Not Supported**: Next LS does not provide Windows binaries, so Elixir language server integration is only available on Linux and macOS. + +## Known Issues + +### Next LS v0.23.3 Timeout Enumeration Bug +There is a known intermittent bug in Next LS v0.23.3 where `textDocument/definition` requests can fail with: +``` +Protocol.UndefinedError: protocol Enumerable not implemented for :timeout of type Atom +``` + +This bug is tracked in [Next LS Issue #543](https://github.com/elixir-tools/next-ls/issues/543) and primarily occurs in CI environments. The affected test (`test_request_defining_symbol_none`) is marked as expected to fail until this upstream bug is resolved. + +## Prerequisites + +Before using the Elixir language server integration, you need to have: + +1. **Elixir** installed and available in your PATH + - Install from: https://elixir-lang.org/install.html + - Verify with: `elixir --version` + +2. **Next LS** installed and available in your PATH + - Install from: https://github.com/elixir-tools/next-ls#installation + - Verify with: `nextls --version` + +## Features + +The Elixir integration provides: + +- **Language Server Protocol (LSP) support** via Next LS +- **File extension recognition** for `.ex` and `.exs` files +- **Project structure awareness** with proper handling of Elixir-specific directories: + - `_build/` - Compiled artifacts (ignored) + - `deps/` - Dependencies (ignored) + - `.elixir_ls/` - ElixirLS artifacts (ignored) + - `cover/` - Coverage reports (ignored) + - `lib/` - Source code (not ignored) + - `test/` - Test files (not ignored) + +## Configuration + +The integration uses the default Next LS configuration with: + +- **MIX_ENV**: `dev` +- **MIX_TARGET**: `host` +- **Experimental completions**: Disabled by default +- **Credo extension**: Enabled by default + +## Usage + +The Elixir language server is automatically selected when working with Elixir projects. It will be used for: + +- Code completion +- Go to definition +- Find references +- Document symbols +- Hover information +- Code formatting +- Diagnostics (via Credo integration) + +### Important: Project Compilation + +Next LS requires your Elixir project to be **compiled** for optimal performance, especially for: +- Cross-file reference resolution +- Complete symbol information +- Accurate go-to-definition + +**For production use**: Ensure your project is compiled with `mix compile` before using the language server. + +**For testing**: The test suite automatically compiles the test repositories before running tests to ensure optimal Next LS performance. + +## Testing + +Run the Elixir-specific tests with: + +```bash +pytest test/solidlsp/elixir/ -m elixir +``` + +## Implementation Details + +- **Main class**: `ElixirTools` in `elixir_tools.py` +- **Initialization parameters**: Defined in `initialize_params.json` +- **Language identifier**: `"elixir"` +- **Command**: `nextls --stdio` + +The implementation follows the same patterns as other language servers in this project, inheriting from `SolidLanguageServer` and providing Elixir-specific configuration and behavior. \ No newline at end of file diff --git a/src/solidlsp/language_servers/elixir_tools/__init__.py b/src/solidlsp/language_servers/elixir_tools/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/src/solidlsp/language_servers/elixir_tools/__init__.py @@ -0,0 +1 @@ + diff --git a/src/solidlsp/language_servers/elixir_tools/elixir_tools.py b/src/solidlsp/language_servers/elixir_tools/elixir_tools.py new file mode 100644 index 00000000..96674722 --- /dev/null +++ b/src/solidlsp/language_servers/elixir_tools/elixir_tools.py @@ -0,0 +1,292 @@ +import json +import logging +import os +import pathlib +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.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo + + +class ElixirTools(SolidLanguageServer): + """ + Provides Elixir specific instantiation of the LanguageServer class using Next LS from elixir-tools. + """ + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + # For Elixir projects, we should ignore: + # - _build: compiled artifacts + # - deps: dependencies + # - node_modules: if the project has JavaScript components + # - .elixir_ls: ElixirLS artifacts (in case both are present) + # - cover: coverage reports + return super().is_ignored_dirname(dirname) or dirname in ["_build", "deps", "node_modules", ".elixir_ls", "cover"] + + def _is_next_ls_internal_file(self, abs_path: str) -> bool: + """Check if an absolute path is a Next LS internal file that should be ignored.""" + return any( + pattern in abs_path + for pattern in [ + ".burrito", # Next LS runtime directory + "next_ls_erts-", # Next LS Erlang runtime + "_next_ls_private_", # Next LS private files + "/priv/monkey/", # Next LS monkey patching directory + ] + ) + + @override + def _send_references_request(self, relative_file_path: str, line: int, column: int): + """Override to filter out Next LS internal files from references.""" + from solidlsp.ls_utils import PathUtils + + # Get the raw response from the parent implementation + raw_response = super()._send_references_request(relative_file_path, line, column) + + if raw_response is None: + return None + + # Filter out Next LS internal files + filtered_response = [] + for item in raw_response: + if isinstance(item, dict) and "uri" in item: + abs_path = PathUtils.uri_to_path(item["uri"]) + if self._is_next_ls_internal_file(abs_path): + self.logger.log(f"Filtering out Next LS internal file: {abs_path}", logging.DEBUG) + continue + filtered_response.append(item) + + return filtered_response + + @staticmethod + def _get_elixir_version(): + """Get the installed Elixir version or None if not found.""" + try: + result = subprocess.run(["elixir", "--version"], capture_output=True, text=True, check=False) + if result.returncode == 0: + return result.stdout.strip() + except FileNotFoundError: + return None + return None + + def setupRuntimeDependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> 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() + 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." + ) + + 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}") + + # Check for Windows and provide a helpful error message + if platformId.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) + + executable_path = str(PurePath(next_ls_dir, dependency["executable_name"])) + binary_path = str(PurePath(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) + + # 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) + + # Create a symlink or copy 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) + + 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) + + super().__init__( + config, + logger, + repository_root_path, + ProcessLaunchInfo(cmd=f'"{nextls_executable_path}" --stdio', cwd=repository_root_path), + "elixir", + ) + self.server_ready = threading.Event() + self.request_id = 0 + + # 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: + """ + 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) + + 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 + + def _start_server(self): + """Start Next LS server process""" + + def register_capability_handler(params): + return + + def window_log_message(msg): + """Handle window/logMessage notifications from Next LS""" + message_text = msg.get("message", "") + self.logger.log(f"LSP: window/logMessage: {message_text}", logging.INFO) + + # Check for the specific Next LS readiness signal + # Based on Next LS source: "Runtime for folder #{name} is ready..." + if "Runtime for folder" in message_text and "is ready..." in message_text: + self.logger.log("Next LS runtime is ready based on official log message", logging.INFO) + self.server_ready.set() + + def do_nothing(params): + return + + def check_server_ready(params): + """ + Handle $/progress notifications from Next LS. + Keep as fallback for error detection, but primary readiness detection + is now done via window/logMessage handler. + """ + value = params.get("value", {}) + + # Check for initialization completion progress (fallback signal) + if value.get("kind") == "end": + message = value.get("message", "") + if "has initialized!" in message: + self.logger.log("Next LS initialization progress completed", logging.INFO) + # Note: We don't set server_ready here - we wait for the log message + + def work_done_progress(params): + """ + Handle $/workDoneProgress notifications from Next LS. + Keep for completeness but primary readiness detection is via window/logMessage. + """ + value = params.get("value", {}) + if value.get("kind") == "end": + self.logger.log("Next LS work done progress completed", logging.INFO) + # Note: We don't set server_ready here - we wait for the log message + + self.server.on_request("client/registerCapability", register_capability_handler) + self.server.on_notification("window/logMessage", window_log_message) + self.server.on_notification("$/progress", check_server_ready) + self.server.on_notification("window/workDoneProgress/create", do_nothing) + self.server.on_notification("$/workDoneProgress", work_done_progress) + self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + + self.logger.log("Starting Next LS server process", logging.INFO) + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) + + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) + + # Verify server capabilities - be more lenient with Next LS + self.logger.log(f"Next LS capabilities: {list(init_response['capabilities'].keys())}", logging.INFO) + + # Next LS may not provide all capabilities immediately, so we check for basic ones + assert "textDocumentSync" in init_response["capabilities"], f"Missing textDocumentSync in {init_response['capabilities']}" + + # Some capabilities might be optional or provided later + if "completionProvider" not in init_response["capabilities"]: + self.logger.log("Warning: completionProvider not available in initial capabilities", logging.WARNING) + if "definitionProvider" not in init_response["capabilities"]: + self.logger.log("Warning: definitionProvider not available in initial capabilities", logging.WARNING) + + self.server.notify.initialized({}) + self.completions_available.set() + + # Wait for Next LS to send the specific "Runtime for folder X is ready..." log message + # This is the authoritative signal that Next LS is truly ready for requests + ready_timeout = 180.0 + self.logger.log(f"Waiting up to {ready_timeout} seconds for Next LS runtime readiness...", logging.INFO) + + if self.server_ready.wait(timeout=ready_timeout): + self.logger.log("Next LS is ready and available for requests", logging.INFO) + + # Add a small settling period to ensure background indexing is complete + # Next LS often continues compilation/indexing in background after ready signal + settling_time = 120.0 + self.logger.log(f"Allowing {settling_time} seconds for Next LS background indexing to complete...", logging.INFO) + time.sleep(settling_time) + self.logger.log("Next LS settling period complete", logging.INFO) + else: + error_msg = f"Next LS failed to initialize within {ready_timeout} seconds. This may indicate a problem with the Elixir installation, project compilation, or Next LS itself." + self.logger.log(error_msg, logging.ERROR) + raise RuntimeError(error_msg) diff --git a/src/solidlsp/language_servers/elixir_tools/initialize_params.json b/src/solidlsp/language_servers/elixir_tools/initialize_params.json new file mode 100644 index 00000000..51ce266f --- /dev/null +++ b/src/solidlsp/language_servers/elixir_tools/initialize_params.json @@ -0,0 +1,97 @@ +{ + "_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 new file mode 100644 index 00000000..32641a0d --- /dev/null +++ b/src/solidlsp/language_servers/elixir_tools/runtime_dependencies.json @@ -0,0 +1,33 @@ +{ + "_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/gopls.py b/src/solidlsp/language_servers/gopls.py similarity index 75% rename from src/solidlsp/language_servers/gopls/gopls.py rename to src/solidlsp/language_servers/gopls.py index c3fadd71..70eb15fa 100644 --- a/src/solidlsp/language_servers/gopls/gopls.py +++ b/src/solidlsp/language_servers/gopls.py @@ -1,4 +1,3 @@ -import json import logging import os import pathlib @@ -49,19 +48,19 @@ class Gopls(SolidLanguageServer): return None return None - @classmethod - def setup_runtime_dependency(cls): + @staticmethod + def _setup_runtime_dependency(): """ Check if required Go runtime dependencies are available. Raises RuntimeError with helpful message if dependencies are missing. """ - go_version = cls._get_go_version() + go_version = Gopls._get_go_version() if not go_version: raise RuntimeError( "Go is not installed. Please install Go from https://golang.org/doc/install and make sure it is added to your PATH." ) - gopls_version = cls._get_gopls_version() + gopls_version = Gopls._get_gopls_version() if not gopls_version: raise RuntimeError( "Found a Go version but gopls is not installed.\n" @@ -72,7 +71,7 @@ class Gopls(SolidLanguageServer): return True def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): - self.setup_runtime_dependency() + self._setup_runtime_dependency() super().__init__( config, @@ -84,29 +83,37 @@ class Gopls(SolidLanguageServer): self.server_ready = threading.Event() self.request_id = 0 - 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 TypeScript Language Server. + Returns the initialize params for the Go Language Server. """ - with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), encoding="utf-8") as f: - d = json.load(f) - - 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 + root_uri = pathlib.Path(repository_absolute_path).as_uri() + initialize_params = { + "locale": "en", + "capabilities": { + "textDocument": { + "synchronization": {"didSave": True, "dynamicRegistration": True}, + "definition": {"dynamicRegistration": True}, + "documentSymbol": { + "dynamicRegistration": True, + "hierarchicalDocumentSymbolSupport": True, + "symbolKind": {"valueSet": list(range(1, 27))}, + }, + }, + "workspace": {"workspaceFolders": True, "didChangeConfiguration": {"dynamicRegistration": True}}, + }, + "processId": os.getpid(), + "rootPath": repository_absolute_path, + "rootUri": root_uri, + "workspaceFolders": [ + { + "uri": root_uri, + "name": os.path.basename(repository_absolute_path), + } + ], + } + return initialize_params def _start_server(self): """Start gopls server process""" diff --git a/src/solidlsp/language_servers/intelephense/intelephense.py b/src/solidlsp/language_servers/intelephense.py similarity index 66% rename from src/solidlsp/language_servers/intelephense/intelephense.py rename to src/solidlsp/language_servers/intelephense.py index 19eaa8b1..f9c83976 100644 --- a/src/solidlsp/language_servers/intelephense/intelephense.py +++ b/src/solidlsp/language_servers/intelephense.py @@ -2,12 +2,10 @@ Provides PHP specific instantiation of the LanguageServer class using Intelephense. """ -import json import logging import os import pathlib import shutil -import subprocess from time import sleep from overrides import override @@ -19,6 +17,8 @@ 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 .common import RuntimeDependency, RuntimeDependencyCollection + class Intelephense(SolidLanguageServer): """ @@ -33,9 +33,10 @@ class Intelephense(SolidLanguageServer): # - cache: commonly used for caching return super().is_ignored_dirname(dirname) or dirname in ["node_modules", "vendor", "cache"] - def setup_runtime_dependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> str: + @classmethod + def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> str: """ - Setup runtime dependencies for Intelephense. + Setup runtime dependencies for Intelephense and return the command to start the server. """ platform_id = PlatformUtils.get_platform_id() @@ -50,13 +51,6 @@ class Intelephense(SolidLanguageServer): ] assert platform_id in valid_platforms, f"Platform {platform_id} is not supported for multilspy PHP at the moment" - with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), encoding="utf-8") as f: - d = json.load(f) - del d["_description"] - - runtime_dependencies = d.get("runtimeDependencies", []) - intelephense_ls_dir = os.path.join(os.path.dirname(__file__), "static", "php-lsp") - # 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." @@ -64,69 +58,61 @@ 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 - if not os.path.exists(intelephense_ls_dir): - os.makedirs(intelephense_ls_dir, exist_ok=True) - for dependency in runtime_dependencies: - # Windows doesn't support the 'user' parameter and doesn't have pwd module - if PlatformUtils.get_platform_id().value.startswith("win"): - subprocess.run( - dependency["command"], - shell=True, - check=True, - cwd=intelephense_ls_dir, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - else: - # On Unix-like systems, run as non-root user - import pwd - - user = pwd.getpwuid(os.getuid()).pw_name - subprocess.run( - dependency["command"], - shell=True, - check=True, - user=user, - cwd=intelephense_ls_dir, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - + intelephense_ls_dir = os.path.join(cls.ls_resources_dir(), "php-lsp") + os.makedirs(intelephense_ls_dir, exist_ok=True) intelephense_executable_path = os.path.join(intelephense_ls_dir, "node_modules", ".bin", "intelephense") - assert os.path.exists(intelephense_executable_path), "intelephense executable not found. Please install intelephense and try again." + if not os.path.exists(intelephense_executable_path): + deps = RuntimeDependencyCollection( + [ + RuntimeDependency( + id="intelephense", + command="npm install --prefix ./ intelephense@1.14.4", + platform_id="any", + ) + ] + ) + deps.install(logger, intelephense_ls_dir) + + assert os.path.exists( + intelephense_executable_path + ), f"intelephense executable not found at {intelephense_executable_path}, something went wrong." return f"{intelephense_executable_path} --stdio" def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): # Setup runtime dependencies before initializing - intelephense_cmd = self.setup_runtime_dependencies(logger, config) + intelephense_cmd = self._setup_runtime_dependencies(logger, config) super().__init__(config, logger, repository_root_path, ProcessLaunchInfo(cmd=intelephense_cmd, cwd=repository_root_path), "php") self.request_id = 0 - 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 TypeScript 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 = { + "locale": "en", + "capabilities": { + "textDocument": { + "synchronization": {"didSave": True, "dynamicRegistration": True}, + "definition": {"dynamicRegistration": True}, + }, + "workspace": {"workspaceFolders": True, "didChangeConfiguration": {"dynamicRegistration": True}}, + }, + "processId": os.getpid(), + "rootPath": repository_absolute_path, + "rootUri": root_uri, + "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 Intelephense server process""" diff --git a/src/solidlsp/language_servers/intelephense/initialize_params.json b/src/solidlsp/language_servers/intelephense/initialize_params.json deleted file mode 100644 index 4330560e..00000000 --- a/src/solidlsp/language_servers/intelephense/initialize_params.json +++ /dev/null @@ -1,36 +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", - "capabilities": { - "textDocument": { - "synchronization": { - "didSave": true, - "dynamicRegistration": true - }, - "completion": { - "dynamicRegistration": true, - "completionItem": { - "snippetSupport": true - } - }, - "definition": { - "dynamicRegistration": true - } - }, - "workspace": { - "workspaceFolders": true, - "didChangeConfiguration": { - "dynamicRegistration": true - } - } - }, - "workspaceFolders": [ - { - "uri": "$uri", - "name": "$name" - } - ] -} \ No newline at end of file diff --git a/src/solidlsp/language_servers/intelephense/runtime_dependencies.json b/src/solidlsp/language_servers/intelephense/runtime_dependencies.json deleted file mode 100644 index e3a6d7e1..00000000 --- a/src/solidlsp/language_servers/intelephense/runtime_dependencies.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "_description": "Used to download the runtime dependencies for running intelephense. Obtained from https://www.npmjs.com/package/intelephense", - "runtimeDependencies": [ - { - "id": "intelephense", - "description": "Intelephense package for Linux, OSX, and Windows. Both x64 and arm64 are supported.", - "command": "npm install --prefix ./ intelephense@1.14.4" - } - ] -} diff --git a/src/solidlsp/language_servers/jedi_language_server/initialize_params.json b/src/solidlsp/language_servers/jedi_language_server/initialize_params.json deleted file mode 100644 index 5a04872c..00000000 --- a/src/solidlsp/language_servers/jedi_language_server/initialize_params.json +++ /dev/null @@ -1,905 +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()", - "clientInfo": { - "name": "Visual Studio Code - Insiders", - "version": "1.81.0-insider" - }, - "locale": "en", - "rootPath": "$rootPath", - "rootUri": "$rootUri", - "capabilities": { - "workspace": { - "applyEdit": true, - "workspaceEdit": { - "documentChanges": true, - "resourceOperations": [ - "create", - "rename", - "delete" - ], - "failureHandling": "textOnlyTransactional", - "normalizesLineEndings": true, - "changeAnnotationSupport": { - "groupsOnLabel": true - } - }, - "configuration": true, - "didChangeWatchedFiles": { - "dynamicRegistration": true, - "relativePatternSupport": true - }, - "symbol": { - "dynamicRegistration": 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 - ] - }, - "tagSupport": { - "valueSet": [ - 1 - ] - }, - "resolveSupport": { - "properties": [ - "location.range" - ] - } - }, - "codeLens": { - "refreshSupport": true - }, - "executeCommand": { - "dynamicRegistration": true - }, - "didChangeConfiguration": { - "dynamicRegistration": true - }, - "workspaceFolders": true, - "semanticTokens": { - "refreshSupport": true - }, - "fileOperations": { - "dynamicRegistration": true, - "didCreate": true, - "didRename": true, - "didDelete": true, - "willCreate": true, - "willRename": true, - "willDelete": true - }, - "inlineValue": { - "refreshSupport": true - }, - "inlayHint": { - "refreshSupport": true - }, - "diagnostics": { - "refreshSupport": true - } - }, - "textDocument": { - "publishDiagnostics": { - "relatedInformation": true, - "versionSupport": false, - "tagSupport": { - "valueSet": [ - 1, - 2 - ] - }, - "codeDescriptionSupport": true, - "dataSupport": true - }, - "synchronization": { - "dynamicRegistration": true, - "willSave": true, - "willSaveWaitUntil": true, - "didSave": true - }, - "completion": { - "dynamicRegistration": true, - "contextSupport": true, - "completionItem": { - "snippetSupport": true, - "commitCharactersSupport": true, - "documentationFormat": [ - "markdown", - "plaintext" - ], - "deprecatedSupport": true, - "preselectSupport": true, - "tagSupport": { - "valueSet": [ - 1 - ] - }, - "insertReplaceSupport": true, - "resolveSupport": { - "properties": [ - "documentation", - "detail", - "additionalTextEdits" - ] - }, - "insertTextModeSupport": { - "valueSet": [ - 1, - 2 - ] - }, - "labelDetailsSupport": true - }, - "insertTextMode": 2, - "completionItemKind": { - "valueSet": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25 - ] - }, - "completionList": { - "itemDefaults": [ - "commitCharacters", - "editRange", - "insertTextFormat", - "insertTextMode" - ] - } - }, - "hover": { - "dynamicRegistration": true, - "contentFormat": [ - "markdown", - "plaintext" - ] - }, - "signatureHelp": { - "dynamicRegistration": true, - "signatureInformation": { - "documentationFormat": [ - "markdown", - "plaintext" - ], - "parameterInformation": { - "labelOffsetSupport": true - }, - "activeParameterSupport": true - }, - "contextSupport": true - }, - "definition": { - "dynamicRegistration": true, - "linkSupport": true - }, - "references": { - "dynamicRegistration": true - }, - "documentHighlight": { - "dynamicRegistration": true - }, - "documentSymbol": { - "dynamicRegistration": 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 - ] - }, - "hierarchicalDocumentSymbolSupport": true, - "tagSupport": { - "valueSet": [ - 1 - ] - }, - "labelSupport": true - }, - "codeAction": { - "dynamicRegistration": true, - "isPreferredSupport": true, - "disabledSupport": true, - "dataSupport": true, - "resolveSupport": { - "properties": [ - "edit" - ] - }, - "codeActionLiteralSupport": { - "codeActionKind": { - "valueSet": [ - "", - "quickfix", - "refactor", - "refactor.extract", - "refactor.inline", - "refactor.rewrite", - "source", - "source.organizeImports" - ] - } - }, - "honorsChangeAnnotations": false - }, - "codeLens": { - "dynamicRegistration": true - }, - "formatting": { - "dynamicRegistration": true - }, - "rangeFormatting": { - "dynamicRegistration": true - }, - "onTypeFormatting": { - "dynamicRegistration": true - }, - "rename": { - "dynamicRegistration": true, - "prepareSupport": true, - "prepareSupportDefaultBehavior": 1, - "honorsChangeAnnotations": true - }, - "documentLink": { - "dynamicRegistration": true, - "tooltipSupport": true - }, - "typeDefinition": { - "dynamicRegistration": true, - "linkSupport": true - }, - "implementation": { - "dynamicRegistration": true, - "linkSupport": true - }, - "colorProvider": { - "dynamicRegistration": true - }, - "foldingRange": { - "dynamicRegistration": true, - "rangeLimit": 5000, - "lineFoldingOnly": true, - "foldingRangeKind": { - "valueSet": [ - "comment", - "imports", - "region" - ] - }, - "foldingRange": { - "collapsedText": false - } - }, - "declaration": { - "dynamicRegistration": true, - "linkSupport": true - }, - "selectionRange": { - "dynamicRegistration": true - }, - "callHierarchy": { - "dynamicRegistration": true - }, - "semanticTokens": { - "dynamicRegistration": true, - "tokenTypes": [ - "namespace", - "type", - "class", - "enum", - "interface", - "struct", - "typeParameter", - "parameter", - "variable", - "property", - "enumMember", - "event", - "function", - "method", - "macro", - "keyword", - "modifier", - "comment", - "string", - "number", - "regexp", - "operator", - "decorator" - ], - "tokenModifiers": [ - "declaration", - "definition", - "readonly", - "static", - "deprecated", - "abstract", - "async", - "modification", - "documentation", - "defaultLibrary" - ], - "formats": [ - "relative" - ], - "requests": { - "range": true, - "full": { - "delta": true - } - }, - "multilineTokenSupport": false, - "overlappingTokenSupport": false, - "serverCancelSupport": true, - "augmentsSyntaxTokens": false - }, - "linkedEditingRange": { - "dynamicRegistration": true - }, - "typeHierarchy": { - "dynamicRegistration": true - }, - "inlineValue": { - "dynamicRegistration": true - }, - "inlayHint": { - "dynamicRegistration": true, - "resolveSupport": { - "properties": [ - "tooltip", - "textEdits", - "label.tooltip", - "label.location", - "label.command" - ] - } - }, - "diagnostic": { - "dynamicRegistration": true, - "relatedDocumentSupport": false - } - }, - "window": { - "showMessage": { - "messageActionItem": { - "additionalPropertiesSupport": true - } - }, - "showDocument": { - "support": true - }, - "workDoneProgress": true - }, - "general": { - "staleRequestSupport": { - "cancel": true, - "retryOnContentModified": [ - "textDocument/semanticTokens/full", - "textDocument/semanticTokens/range", - "textDocument/semanticTokens/full/delta" - ] - }, - "regularExpressions": { - "engine": "ECMAScript", - "version": "ES2020" - }, - "markdown": { - "parser": "marked", - "version": "1.1.0", - "allowedTags": [ - "ul", - "li", - "p", - "code", - "blockquote", - "ol", - "h1", - "h2", - "h3", - "h4", - "h5", - "h6", - "hr", - "em", - "pre", - "table", - "thead", - "tbody", - "tr", - "th", - "td", - "div", - "del", - "a", - "strong", - "br", - "img", - "span" - ] - }, - "positionEncodings": [ - "utf-16" - ] - }, - "notebookDocument": { - "synchronization": { - "dynamicRegistration": true, - "executionSummarySupport": true - } - }, - "experimental": { - "snippetTextEdit": true, - "codeActionGroup": true, - "hoverActions": true, - "serverStatusNotification": true, - "colorDiagnosticOutput": true, - "openServerLogs": true, - "commands": { - "commands": [ - "rust-analyzer.runSingle", - "rust-analyzer.debugSingle", - "rust-analyzer.showReferences", - "rust-analyzer.gotoLocation", - "editor.action.triggerParameterHints" - ] - } - } - }, - "initializationOptions": { - "cargoRunner": null, - "runnables": { - "extraEnv": null, - "problemMatcher": [ - "$rustc" - ], - "command": null, - "extraArgs": [] - }, - "server": { - "path": null, - "extraEnv": null - }, - "trace": { - "server": "verbose", - "extension": true - }, - "debug": { - "engine": "auto", - "sourceFileMap": { - "/rustc/": "${env:USERPROFILE}/.rustup/toolchains//lib/rustlib/src/rust" - }, - "openDebugPane": false, - "engineSettings": {} - }, - "restartServerOnConfigChange": false, - "typing": { - "continueCommentsOnNewline": true, - "autoClosingAngleBrackets": { - "enable": false - } - }, - "diagnostics": { - "previewRustcOutput": false, - "useRustcErrorCode": false, - "disabled": [], - "enable": true, - "experimental": { - "enable": false - }, - "remapPrefix": {}, - "warningsAsHint": [], - "warningsAsInfo": [] - }, - "discoverProjectCommand": null, - "showUnlinkedFileNotification": true, - "showDependenciesExplorer": true, - "assist": { - "emitMustUse": false, - "expressionFillDefault": "todo" - }, - "cachePriming": { - "enable": true, - "numThreads": 0 - }, - "cargo": { - "autoreload": true, - "buildScripts": { - "enable": true, - "invocationLocation": "workspace", - "invocationStrategy": "per_workspace", - "overrideCommand": null, - "useRustcWrapper": true - }, - "cfgs": {}, - "extraArgs": [], - "extraEnv": {}, - "features": [], - "noDefaultFeatures": false, - "sysroot": "discover", - "sysrootSrc": null, - "target": null, - "unsetTest": [ - "core" - ] - }, - "checkOnSave": true, - "check": { - "allTargets": true, - "command": "check", - "extraArgs": [], - "extraEnv": {}, - "features": null, - "invocationLocation": "workspace", - "invocationStrategy": "per_workspace", - "noDefaultFeatures": null, - "overrideCommand": null, - "targets": null - }, - "completion": { - "autoimport": { - "enable": true - }, - "autoself": { - "enable": true - }, - "callable": { - "snippets": "fill_arguments" - }, - "limit": null, - "postfix": { - "enable": true - }, - "privateEditable": { - "enable": false - }, - "snippets": { - "custom": { - "Arc::new": { - "postfix": "arc", - "body": "Arc::new(${receiver})", - "requires": "std::sync::Arc", - "description": "Put the expression into an `Arc`", - "scope": "expr" - }, - "Rc::new": { - "postfix": "rc", - "body": "Rc::new(${receiver})", - "requires": "std::rc::Rc", - "description": "Put the expression into an `Rc`", - "scope": "expr" - }, - "Box::pin": { - "postfix": "pinbox", - "body": "Box::pin(${receiver})", - "requires": "std::boxed::Box", - "description": "Put the expression into a pinned `Box`", - "scope": "expr" - }, - "Ok": { - "postfix": "ok", - "body": "Ok(${receiver})", - "description": "Wrap the expression in a `Result::Ok`", - "scope": "expr" - }, - "Err": { - "postfix": "err", - "body": "Err(${receiver})", - "description": "Wrap the expression in a `Result::Err`", - "scope": "expr" - }, - "Some": { - "postfix": "some", - "body": "Some(${receiver})", - "description": "Wrap the expression in an `Option::Some`", - "scope": "expr" - } - } - } - }, - "files": { - "excludeDirs": [], - "watcher": "client" - }, - "highlightRelated": { - "breakPoints": { - "enable": true - }, - "closureCaptures": { - "enable": true - }, - "exitPoints": { - "enable": true - }, - "references": { - "enable": true - }, - "yieldPoints": { - "enable": true - } - }, - "hover": { - "actions": { - "debug": { - "enable": true - }, - "enable": true, - "gotoTypeDef": { - "enable": true - }, - "implementations": { - "enable": true - }, - "references": { - "enable": false - }, - "run": { - "enable": true - } - }, - "documentation": { - "enable": true, - "keywords": { - "enable": true - } - }, - "links": { - "enable": true - }, - "memoryLayout": { - "alignment": "hexadecimal", - "enable": true, - "niches": false, - "offset": "hexadecimal", - "size": "both" - } - }, - "imports": { - "granularity": { - "enforce": false, - "group": "crate" - }, - "group": { - "enable": true - }, - "merge": { - "glob": true - }, - "prefer": { - "no": { - "std": false - } - }, - "prefix": "plain" - }, - "inlayHints": { - "bindingModeHints": { - "enable": false - }, - "chainingHints": { - "enable": true - }, - "closingBraceHints": { - "enable": true, - "minLines": 25 - }, - "closureCaptureHints": { - "enable": false - }, - "closureReturnTypeHints": { - "enable": "never" - }, - "closureStyle": "impl_fn", - "discriminantHints": { - "enable": "never" - }, - "expressionAdjustmentHints": { - "enable": "never", - "hideOutsideUnsafe": false, - "mode": "prefix" - }, - "lifetimeElisionHints": { - "enable": "never", - "useParameterNames": false - }, - "maxLength": 25, - "parameterHints": { - "enable": true - }, - "reborrowHints": { - "enable": "never" - }, - "renderColons": true, - "typeHints": { - "enable": true, - "hideClosureInitialization": false, - "hideNamedConstructor": false - } - }, - "interpret": { - "tests": false - }, - "joinLines": { - "joinAssignments": true, - "joinElseIf": true, - "removeTrailingComma": true, - "unwrapTrivialBlock": true - }, - "lens": { - "debug": { - "enable": true - }, - "enable": true, - "forceCustomCommands": true, - "implementations": { - "enable": true - }, - "location": "above_name", - "references": { - "adt": { - "enable": false - }, - "enumVariant": { - "enable": false - }, - "method": { - "enable": false - }, - "trait": { - "enable": false - } - }, - "run": { - "enable": true - } - }, - "linkedProjects": [], - "lru": { - "capacity": null, - "query": { - "capacities": {} - } - }, - "notifications": { - "cargoTomlNotFound": true - }, - "numThreads": null, - "procMacro": { - "attributes": { - "enable": true - }, - "enable": true, - "ignored": {}, - "server": null - }, - "references": { - "excludeImports": false - }, - "rustc": { - "source": null - }, - "rustfmt": { - "extraArgs": [], - "overrideCommand": null, - "rangeFormatting": { - "enable": false - } - }, - "semanticHighlighting": { - "doc": { - "comment": { - "inject": { - "enable": true - } - } - }, - "nonStandardTokens": true, - "operator": { - "enable": true, - "specialization": { - "enable": false - } - }, - "punctuation": { - "enable": false, - "separate": { - "macro": { - "bang": false - } - }, - "specialization": { - "enable": false - } - }, - "strings": { - "enable": true - } - }, - "signatureInfo": { - "detail": "full", - "documentation": { - "enable": true - } - }, - "workspace": { - "symbols": { - "maxSymbols": 0 - } - } - }, - "trace": "verbose", - "workspaceFolders": [ - { - "uri": "$uri", - "name": "$name" - } - ] -} \ No newline at end of file diff --git a/src/solidlsp/language_servers/jedi_language_server/jedi_server.py b/src/solidlsp/language_servers/jedi_language_server/jedi_server.py deleted file mode 100644 index 467d3cab..00000000 --- a/src/solidlsp/language_servers/jedi_language_server/jedi_server.py +++ /dev/null @@ -1,107 +0,0 @@ -""" -Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python. -""" - -import json -import logging -import os -import pathlib - -from overrides import override - -from solidlsp.ls import SolidLanguageServer -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 - - -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): - """ - Creates a JediServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. - """ - super().__init__( - config, - logger, - repository_root_path, - ProcessLaunchInfo(cmd="jedi-language-server", cwd=repository_root_path), - "python", - ) - - @override - def is_ignored_dirname(self, dirname: str) -> bool: - return super().is_ignored_dirname(dirname) or dirname in ["venv", "__pycache__"] - - def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: - """ - Returns the initialize params for the Jedi Language Server. - """ - with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), encoding="utf-8") as f: - d = json.load(f) - - 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 - - def _start_server(self): - """ - Starts the JEDI Language Server - """ - - def execute_client_command_handler(params): - return [] - - def do_nothing(params): - return - - def check_experimental_status(params): - if params["quiescent"] == True: - self.completions_available.set() - - def window_log_message(msg): - self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) - - self.server.on_request("client/registerCapability", do_nothing) - self.server.on_notification("language/status", do_nothing) - self.server.on_notification("window/logMessage", window_log_message) - self.server.on_request("workspace/executeClientCommand", execute_client_command_handler) - self.server.on_notification("$/progress", do_nothing) - self.server.on_notification("textDocument/publishDiagnostics", do_nothing) - self.server.on_notification("language/actionableNotification", do_nothing) - self.server.on_notification("experimental/serverStatus", check_experimental_status) - - self.logger.log("Starting jedi-language-server server process", logging.INFO) - self.server.start() - initialize_params = self._get_initialize_params(self.repository_root_path) - - self.logger.log( - "Sending initialize request from LSP client to LSP server and awaiting response", - logging.INFO, - ) - init_response = self.server.send.initialize(initialize_params) - assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 - assert "completionProvider" in init_response["capabilities"] - assert init_response["capabilities"]["completionProvider"] == { - "triggerCharacters": [".", "'", '"'], - "resolveProvider": True, - } - - self.server.notify.initialized({}) diff --git a/src/solidlsp/language_servers/jedi_server.py b/src/solidlsp/language_servers/jedi_server.py new file mode 100644 index 00000000..161ce754 --- /dev/null +++ b/src/solidlsp/language_servers/jedi_server.py @@ -0,0 +1,194 @@ +""" +Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python. +""" + +import logging +import os +import pathlib + +from overrides import override + +from solidlsp.ls import SolidLanguageServer +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 + + +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): + """ + Creates a JediServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. + """ + super().__init__( + config, + logger, + repository_root_path, + ProcessLaunchInfo(cmd="jedi-language-server", cwd=repository_root_path), + "python", + ) + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + return super().is_ignored_dirname(dirname) or dirname in ["venv", "__pycache__"] + + @staticmethod + def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: + """ + Returns the initialize params for the Jedi Language Server. + """ + root_uri = pathlib.Path(repository_absolute_path).as_uri() + initialize_params = { + "processId": os.getpid(), + "clientInfo": {"name": "Serena", "version": "0.1.0"}, + "locale": "en", + "rootPath": repository_absolute_path, + "rootUri": root_uri, + # Note: this is not necessarily the minimal set of capabilities... + "capabilities": { + "workspace": { + "applyEdit": True, + "workspaceEdit": { + "documentChanges": True, + "resourceOperations": ["create", "rename", "delete"], + "failureHandling": "textOnlyTransactional", + "normalizesLineEndings": True, + "changeAnnotationSupport": {"groupsOnLabel": True}, + }, + "configuration": True, + "didChangeWatchedFiles": {"dynamicRegistration": True, "relativePatternSupport": True}, + "symbol": { + "dynamicRegistration": True, + "symbolKind": {"valueSet": list(range(1, 27))}, + "tagSupport": {"valueSet": [1]}, + "resolveSupport": {"properties": ["location.range"]}, + }, + "workspaceFolders": True, + "fileOperations": { + "dynamicRegistration": True, + "didCreate": True, + "didRename": True, + "didDelete": True, + "willCreate": True, + "willRename": True, + "willDelete": True, + }, + "inlineValue": {"refreshSupport": True}, + "inlayHint": {"refreshSupport": True}, + "diagnostics": {"refreshSupport": True}, + }, + "textDocument": { + "publishDiagnostics": { + "relatedInformation": True, + "versionSupport": False, + "tagSupport": {"valueSet": [1, 2]}, + "codeDescriptionSupport": True, + "dataSupport": True, + }, + "synchronization": {"dynamicRegistration": True, "willSave": True, "willSaveWaitUntil": True, "didSave": True}, + "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]}, + "signatureHelp": { + "dynamicRegistration": True, + "signatureInformation": { + "documentationFormat": ["markdown", "plaintext"], + "parameterInformation": {"labelOffsetSupport": True}, + "activeParameterSupport": True, + }, + "contextSupport": True, + }, + "definition": {"dynamicRegistration": True, "linkSupport": True}, + "references": {"dynamicRegistration": True}, + "documentHighlight": {"dynamicRegistration": True}, + "documentSymbol": { + "dynamicRegistration": True, + "symbolKind": {"valueSet": list(range(1, 27))}, + "hierarchicalDocumentSymbolSupport": True, + "tagSupport": {"valueSet": [1]}, + "labelSupport": True, + }, + "documentLink": {"dynamicRegistration": True, "tooltipSupport": True}, + "typeDefinition": {"dynamicRegistration": True, "linkSupport": True}, + "implementation": {"dynamicRegistration": True, "linkSupport": True}, + "declaration": {"dynamicRegistration": True, "linkSupport": True}, + "selectionRange": {"dynamicRegistration": True}, + "callHierarchy": {"dynamicRegistration": True}, + "linkedEditingRange": {"dynamicRegistration": True}, + "typeHierarchy": {"dynamicRegistration": True}, + "inlineValue": {"dynamicRegistration": True}, + "inlayHint": { + "dynamicRegistration": True, + "resolveSupport": {"properties": ["tooltip", "textEdits", "label.tooltip", "label.location", "label.command"]}, + }, + "diagnostic": {"dynamicRegistration": True, "relatedDocumentSupport": False}, + }, + "notebookDocument": {"synchronization": {"dynamicRegistration": True, "executionSummarySupport": True}}, + "experimental": { + "serverStatusNotification": True, + "openServerLogs": True, + }, + }, + # See https://github.com/pappasam/jedi-language-server?tab=readme-ov-file + # We use the default options except for maxSymbols, where 0 means no limit + "initializationOptions": { + "workspace": { + "symbols": {"ignoreFolders": [".nox", ".tox", ".venv", "__pycache__", "venv"], "maxSymbols": 0}, + }, + }, + "trace": "verbose", + "workspaceFolders": [ + { + "uri": root_uri, + "name": os.path.basename(repository_absolute_path), + } + ], + } + return initialize_params + + def _start_server(self): + """ + Starts the JEDI Language Server + """ + + def execute_client_command_handler(params): + return [] + + def do_nothing(params): + return + + def check_experimental_status(params): + if params["quiescent"] == True: + self.completions_available.set() + + def window_log_message(msg): + self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + self.server.on_request("client/registerCapability", do_nothing) + self.server.on_notification("language/status", do_nothing) + self.server.on_notification("window/logMessage", window_log_message) + self.server.on_request("workspace/executeClientCommand", execute_client_command_handler) + self.server.on_notification("$/progress", do_nothing) + self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + self.server.on_notification("language/actionableNotification", do_nothing) + self.server.on_notification("experimental/serverStatus", check_experimental_status) + + self.logger.log("Starting jedi-language-server server process", logging.INFO) + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) + + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) + assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 + assert "completionProvider" in init_response["capabilities"] + assert init_response["capabilities"]["completionProvider"] == { + "triggerCharacters": [".", "'", '"'], + "resolveProvider": True, + } + + self.server.notify.initialized({}) diff --git a/src/solidlsp/language_servers/kotlin_language_server.py b/src/solidlsp/language_servers/kotlin_language_server.py new file mode 100644 index 00000000..ef2a77ca --- /dev/null +++ b/src/solidlsp/language_servers/kotlin_language_server.py @@ -0,0 +1,463 @@ +""" +Provides Kotlin specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Kotlin. +""" + +import dataclasses +import logging +import os +import pathlib +import stat + +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.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo + + +@dataclasses.dataclass +class KotlinRuntimeDependencyPaths: + """ + Stores the paths to the runtime dependencies of Kotlin Language Server + """ + + java_path: str + java_home_path: str + kotlin_executable_path: str + + +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): + """ + 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) + self.runtime_dependency_paths = runtime_dependency_paths + + # Create command to execute the Kotlin Language Server script + cmd = f'"{self.runtime_dependency_paths.kotlin_executable_path}"' + + # Set environment variables including JAVA_HOME + proc_env = {"JAVA_HOME": self.runtime_dependency_paths.java_home_path} + + super().__init__( + config, + logger, + repository_root_path, + ProcessLaunchInfo(cmd=cmd, env=proc_env, cwd=repository_root_path), + "kotlin", + ) + + @classmethod + def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> KotlinRuntimeDependencyPaths: + """ + Setup runtime dependencies for Kotlin Language Server and return the paths. + """ + platform_id = PlatformUtils.get_platform_id() + + # Verify platform support + assert ( + platform_id.value.startswith("win-") or platform_id.value.startswith("linux-") or platform_id.value.startswith("osx-") + ), "Only Windows, Linux and macOS platforms are supported for Kotlin in multilspy at the moment" + + # Runtime dependency information + runtime_dependencies = { + "runtimeDependency": { + "id": "KotlinLsp", + "description": "Kotlin Language Server", + "url": "https://github.com/fwcd/kotlin-language-server/releases/download/1.3.13/server.zip", + "archiveType": "zip", + }, + "java": { + "win-x64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-win32-x64-1.42.0-561.vsix", + "archiveType": "zip", + "java_home_path": "extension/jre/21.0.7-win32-x86_64", + "java_path": "extension/jre/21.0.7-win32-x86_64/bin/java.exe", + }, + "linux-x64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-x64-1.42.0-561.vsix", + "archiveType": "zip", + "java_home_path": "extension/jre/21.0.7-linux-x86_64", + "java_path": "extension/jre/21.0.7-linux-x86_64/bin/java", + }, + "linux-arm64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-arm64-1.42.0-561.vsix", + "archiveType": "zip", + "java_home_path": "extension/jre/21.0.7-linux-aarch64", + "java_path": "extension/jre/21.0.7-linux-aarch64/bin/java", + }, + "osx-x64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-x64-1.42.0-561.vsix", + "archiveType": "zip", + "java_home_path": "extension/jre/21.0.7-macosx-x86_64", + "java_path": "extension/jre/21.0.7-macosx-x86_64/bin/java", + }, + "osx-arm64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix", + "archiveType": "zip", + "java_home_path": "extension/jre/21.0.7-macosx-aarch64", + "java_path": "extension/jre/21.0.7-macosx-aarch64/bin/java", + }, + }, + } + + kotlin_dependency = runtime_dependencies["runtimeDependency"] + java_dependency = runtime_dependencies["java"][platform_id.value] + + # Setup paths for dependencies + static_dir = os.path.join(cls.ls_resources_dir(), "kotlin_language_server") + os.makedirs(static_dir, exist_ok=True) + + # Setup Java paths + java_dir = os.path.join(static_dir, "java") + os.makedirs(java_dir, exist_ok=True) + + java_home_path = os.path.join(java_dir, java_dependency["java_home_path"]) + java_path = os.path.join(java_dir, java_dependency["java_path"]) + + # Download and extract Java if not exists + if not os.path.exists(java_path): + logger.log(f"Downloading Java for {platform_id.value}...", logging.INFO) + FileUtils.download_and_extract_archive(logger, java_dependency["url"], java_dir, java_dependency["archiveType"]) + # Make Java executable + if not platform_id.value.startswith("win-"): + os.chmod(java_path, 0o755) + + assert os.path.exists(java_path), f"Java executable not found at {java_path}" + + # Setup Kotlin Language Server paths + kotlin_ls_dir = os.path.join(static_dir, "server") + + # Get platform-specific executable script path + if platform_id.value.startswith("win-"): + kotlin_script = os.path.join(kotlin_ls_dir, "bin", "kotlin-language-server.bat") + else: + kotlin_script = os.path.join(kotlin_ls_dir, "bin", "kotlin-language-server") + + # Download and extract Kotlin Language Server if script doesn't exist + if not os.path.exists(kotlin_script): + logger.log("Downloading Kotlin Language Server...", logging.INFO) + FileUtils.download_and_extract_archive(logger, kotlin_dependency["url"], static_dir, kotlin_dependency["archiveType"]) + + # Make script executable on Unix platforms + if os.path.exists(kotlin_script) and not platform_id.value.startswith("win-"): + os.chmod( + kotlin_script, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH + ) + + # Use script file + if os.path.exists(kotlin_script): + kotlin_executable_path = kotlin_script + logger.log(f"Using Kotlin Language Server script at {kotlin_script}", logging.INFO) + else: + raise FileNotFoundError(f"Kotlin Language Server script not found at {kotlin_script}") + + return KotlinRuntimeDependencyPaths( + java_path=java_path, java_home_path=java_home_path, kotlin_executable_path=kotlin_executable_path + ) + + @staticmethod + def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: + """ + Returns the initialize params for the Kotlin Language Server. + """ + if not os.path.isabs(repository_absolute_path): + repository_absolute_path = os.path.abspath(repository_absolute_path) + + root_uri = pathlib.Path(repository_absolute_path).as_uri() + initialize_params = { + "clientInfo": {"name": "Multilspy Kotlin Client", "version": "1.0.0"}, + "locale": "en", + "rootPath": repository_absolute_path, + "rootUri": root_uri, + "capabilities": { + "workspace": { + "applyEdit": True, + "workspaceEdit": { + "documentChanges": True, + "resourceOperations": ["create", "rename", "delete"], + "failureHandling": "textOnlyTransactional", + "normalizesLineEndings": True, + "changeAnnotationSupport": {"groupsOnLabel": True}, + }, + "didChangeConfiguration": {"dynamicRegistration": True}, + "didChangeWatchedFiles": {"dynamicRegistration": True, "relativePatternSupport": True}, + "symbol": { + "dynamicRegistration": True, + "symbolKind": {"valueSet": list(range(1, 27))}, + "tagSupport": {"valueSet": [1]}, + "resolveSupport": {"properties": ["location.range"]}, + }, + "codeLens": {"refreshSupport": True}, + "executeCommand": {"dynamicRegistration": True}, + "configuration": True, + "workspaceFolders": True, + "semanticTokens": {"refreshSupport": True}, + "fileOperations": { + "dynamicRegistration": True, + "didCreate": True, + "didRename": True, + "didDelete": True, + "willCreate": True, + "willRename": True, + "willDelete": True, + }, + "inlineValue": {"refreshSupport": True}, + "inlayHint": {"refreshSupport": True}, + "diagnostics": {"refreshSupport": True}, + }, + "textDocument": { + "publishDiagnostics": { + "relatedInformation": True, + "versionSupport": False, + "tagSupport": {"valueSet": [1, 2]}, + "codeDescriptionSupport": True, + "dataSupport": True, + }, + "synchronization": {"dynamicRegistration": True, "willSave": True, "willSaveWaitUntil": True, "didSave": True}, + "completion": { + "dynamicRegistration": True, + "contextSupport": True, + "completionItem": { + "snippetSupport": False, + "commitCharactersSupport": True, + "documentationFormat": ["markdown", "plaintext"], + "deprecatedSupport": True, + "preselectSupport": True, + "tagSupport": {"valueSet": [1]}, + "insertReplaceSupport": False, + "resolveSupport": {"properties": ["documentation", "detail", "additionalTextEdits"]}, + "insertTextModeSupport": {"valueSet": [1, 2]}, + "labelDetailsSupport": True, + }, + "insertTextMode": 2, + "completionItemKind": { + "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] + }, + "completionList": {"itemDefaults": ["commitCharacters", "editRange", "insertTextFormat", "insertTextMode"]}, + }, + "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]}, + "signatureHelp": { + "dynamicRegistration": True, + "signatureInformation": { + "documentationFormat": ["markdown", "plaintext"], + "parameterInformation": {"labelOffsetSupport": True}, + "activeParameterSupport": True, + }, + "contextSupport": True, + }, + "definition": {"dynamicRegistration": True, "linkSupport": True}, + "references": {"dynamicRegistration": True}, + "documentHighlight": {"dynamicRegistration": True}, + "documentSymbol": { + "dynamicRegistration": True, + "symbolKind": {"valueSet": list(range(1, 27))}, + "hierarchicalDocumentSymbolSupport": True, + "tagSupport": {"valueSet": [1]}, + "labelSupport": True, + }, + "codeAction": { + "dynamicRegistration": True, + "isPreferredSupport": True, + "disabledSupport": True, + "dataSupport": True, + "resolveSupport": {"properties": ["edit"]}, + "codeActionLiteralSupport": { + "codeActionKind": { + "valueSet": [ + "", + "quickfix", + "refactor", + "refactor.extract", + "refactor.inline", + "refactor.rewrite", + "source", + "source.organizeImports", + ] + } + }, + "honorsChangeAnnotations": False, + }, + "codeLens": {"dynamicRegistration": True}, + "formatting": {"dynamicRegistration": True}, + "rangeFormatting": {"dynamicRegistration": True}, + "onTypeFormatting": {"dynamicRegistration": True}, + "rename": { + "dynamicRegistration": True, + "prepareSupport": True, + "prepareSupportDefaultBehavior": 1, + "honorsChangeAnnotations": True, + }, + "documentLink": {"dynamicRegistration": True, "tooltipSupport": True}, + "typeDefinition": {"dynamicRegistration": True, "linkSupport": True}, + "implementation": {"dynamicRegistration": True, "linkSupport": True}, + "colorProvider": {"dynamicRegistration": True}, + "foldingRange": { + "dynamicRegistration": True, + "rangeLimit": 5000, + "lineFoldingOnly": True, + "foldingRangeKind": {"valueSet": ["comment", "imports", "region"]}, + "foldingRange": {"collapsedText": False}, + }, + "declaration": {"dynamicRegistration": True, "linkSupport": True}, + "selectionRange": {"dynamicRegistration": True}, + "callHierarchy": {"dynamicRegistration": True}, + "semanticTokens": { + "dynamicRegistration": True, + "tokenTypes": [ + "namespace", + "type", + "class", + "enum", + "interface", + "struct", + "typeParameter", + "parameter", + "variable", + "property", + "enumMember", + "event", + "function", + "method", + "macro", + "keyword", + "modifier", + "comment", + "string", + "number", + "regexp", + "operator", + "decorator", + ], + "tokenModifiers": [ + "declaration", + "definition", + "readonly", + "static", + "deprecated", + "abstract", + "async", + "modification", + "documentation", + "defaultLibrary", + ], + "formats": ["relative"], + "requests": {"range": True, "full": {"delta": True}}, + "multilineTokenSupport": False, + "overlappingTokenSupport": False, + "serverCancelSupport": True, + "augmentsSyntaxTokens": True, + }, + "linkedEditingRange": {"dynamicRegistration": True}, + "typeHierarchy": {"dynamicRegistration": True}, + "inlineValue": {"dynamicRegistration": True}, + "inlayHint": { + "dynamicRegistration": True, + "resolveSupport": {"properties": ["tooltip", "textEdits", "label.tooltip", "label.location", "label.command"]}, + }, + "diagnostic": {"dynamicRegistration": True, "relatedDocumentSupport": False}, + }, + "window": { + "showMessage": {"messageActionItem": {"additionalPropertiesSupport": True}}, + "showDocument": {"support": True}, + "workDoneProgress": True, + }, + "general": { + "staleRequestSupport": { + "cancel": True, + "retryOnContentModified": [ + "textDocument/semanticTokens/full", + "textDocument/semanticTokens/range", + "textDocument/semanticTokens/full/delta", + ], + }, + "regularExpressions": {"engine": "ECMAScript", "version": "ES2020"}, + "markdown": {"parser": "marked", "version": "1.1.0"}, + "positionEncodings": ["utf-16"], + }, + "notebookDocument": {"synchronization": {"dynamicRegistration": True, "executionSummarySupport": True}}, + }, + "initializationOptions": { + "workspaceFolders": [root_uri], + "storagePath": None, + "codegen": {"enabled": False}, + "compiler": {"jvm": {"target": "default"}}, + "completion": {"snippets": {"enabled": True}}, + "diagnostics": {"enabled": True, "level": 4, "debounceTime": 250}, + "scripts": {"enabled": True, "buildScriptsEnabled": True}, + "indexing": {"enabled": True}, + "externalSources": {"useKlsScheme": False, "autoConvertToKotlin": False}, + "inlayHints": {"typeHints": False, "parameterHints": False, "chainedHints": False}, + "formatting": { + "formatter": "ktfmt", + "ktfmt": { + "style": "google", + "indent": 4, + "maxWidth": 100, + "continuationIndent": 8, + "removeUnusedImports": True, + }, + }, + }, + "trace": "verbose", + "processId": os.getpid(), + "workspaceFolders": [ + { + "uri": root_uri, + "name": os.path.basename(repository_absolute_path), + } + ], + } + return initialize_params + + def _start_server(self): + """ + Starts the Kotlin Language Server + """ + + def execute_client_command_handler(params): + return [] + + def do_nothing(params): + return + + def window_log_message(msg): + self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + self.server.on_request("client/registerCapability", do_nothing) + self.server.on_notification("language/status", do_nothing) + self.server.on_notification("window/logMessage", window_log_message) + self.server.on_request("workspace/executeClientCommand", execute_client_command_handler) + self.server.on_notification("$/progress", do_nothing) + self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + self.server.on_notification("language/actionableNotification", do_nothing) + + self.logger.log("Starting Kotlin server process", logging.INFO) + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) + + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) + + capabilities = init_response["capabilities"] + assert "textDocumentSync" in capabilities, "Server must support textDocumentSync" + assert "hoverProvider" in capabilities, "Server must support hover" + assert "completionProvider" in capabilities, "Server must support code completion" + assert "signatureHelpProvider" in capabilities, "Server must support signature help" + assert "definitionProvider" in capabilities, "Server must support go to definition" + assert "referencesProvider" in capabilities, "Server must support find references" + assert "documentSymbolProvider" in capabilities, "Server must support document symbols" + assert "workspaceSymbolProvider" in capabilities, "Server must support workspace symbols" + assert "semanticTokensProvider" in capabilities, "Server must support semantic tokens" + + self.server.notify.initialized({}) + self.completions_available.set() diff --git a/src/solidlsp/language_servers/kotlin_language_server/initialize_params.json b/src/solidlsp/language_servers/kotlin_language_server/initialize_params.json deleted file mode 100644 index a7b2ec0a..00000000 --- a/src/solidlsp/language_servers/kotlin_language_server/initialize_params.json +++ /dev/null @@ -1,521 +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()", - "clientInfo": { - "name": "Multilspy Kotlin Client", - "version": "1.0.0" - }, - "locale": "en", - "rootPath": "repository_absolute_path", - "rootUri": "pathlib.Path(repository_absolute_path).as_uri()", - "capabilities": { - "workspace": { - "applyEdit": true, - "workspaceEdit": { - "documentChanges": true, - "resourceOperations": [ - "create", - "rename", - "delete" - ], - "failureHandling": "textOnlyTransactional", - "normalizesLineEndings": true, - "changeAnnotationSupport": { - "groupsOnLabel": true - } - }, - "didChangeConfiguration": { - "dynamicRegistration": true - }, - "didChangeWatchedFiles": { - "dynamicRegistration": true, - "relativePatternSupport": true - }, - "symbol": { - "dynamicRegistration": 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 - ] - }, - "tagSupport": { - "valueSet": [ - 1 - ] - }, - "resolveSupport": { - "properties": [ - "location.range" - ] - } - }, - "codeLens": { - "refreshSupport": true - }, - "executeCommand": { - "dynamicRegistration": true - }, - "configuration": true, - "workspaceFolders": true, - "semanticTokens": { - "refreshSupport": true - }, - "fileOperations": { - "dynamicRegistration": true, - "didCreate": true, - "didRename": true, - "didDelete": true, - "willCreate": true, - "willRename": true, - "willDelete": true - }, - "inlineValue": { - "refreshSupport": true - }, - "inlayHint": { - "refreshSupport": true - }, - "diagnostics": { - "refreshSupport": true - } - }, - "textDocument": { - "publishDiagnostics": { - "relatedInformation": true, - "versionSupport": false, - "tagSupport": { - "valueSet": [ - 1, - 2 - ] - }, - "codeDescriptionSupport": true, - "dataSupport": true - }, - "synchronization": { - "dynamicRegistration": true, - "willSave": true, - "willSaveWaitUntil": true, - "didSave": true - }, - "completion": { - "dynamicRegistration": true, - "contextSupport": true, - "completionItem": { - "snippetSupport": false, - "commitCharactersSupport": true, - "documentationFormat": [ - "markdown", - "plaintext" - ], - "deprecatedSupport": true, - "preselectSupport": true, - "tagSupport": { - "valueSet": [ - 1 - ] - }, - "insertReplaceSupport": false, - "resolveSupport": { - "properties": [ - "documentation", - "detail", - "additionalTextEdits" - ] - }, - "insertTextModeSupport": { - "valueSet": [ - 1, - 2 - ] - }, - "labelDetailsSupport": true - }, - "insertTextMode": 2, - "completionItemKind": { - "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 - ] - }, - "completionList": { - "itemDefaults": [ - "commitCharacters", - "editRange", - "insertTextFormat", - "insertTextMode" - ] - } - }, - "hover": { - "dynamicRegistration": true, - "contentFormat": [ - "markdown", - "plaintext" - ] - }, - "signatureHelp": { - "dynamicRegistration": true, - "signatureInformation": { - "documentationFormat": [ - "markdown", - "plaintext" - ], - "parameterInformation": { - "labelOffsetSupport": true - }, - "activeParameterSupport": true - }, - "contextSupport": true - }, - "definition": { - "dynamicRegistration": true, - "linkSupport": true - }, - "references": { - "dynamicRegistration": true - }, - "documentHighlight": { - "dynamicRegistration": true - }, - "documentSymbol": { - "dynamicRegistration": 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 - ] - }, - "hierarchicalDocumentSymbolSupport": true, - "tagSupport": { - "valueSet": [ - 1 - ] - }, - "labelSupport": true - }, - "codeAction": { - "dynamicRegistration": true, - "isPreferredSupport": true, - "disabledSupport": true, - "dataSupport": true, - "resolveSupport": { - "properties": [ - "edit" - ] - }, - "codeActionLiteralSupport": { - "codeActionKind": { - "valueSet": [ - "", - "quickfix", - "refactor", - "refactor.extract", - "refactor.inline", - "refactor.rewrite", - "source", - "source.organizeImports" - ] - } - }, - "honorsChangeAnnotations": false - }, - "codeLens": { - "dynamicRegistration": true - }, - "formatting": { - "dynamicRegistration": true - }, - "rangeFormatting": { - "dynamicRegistration": true - }, - "onTypeFormatting": { - "dynamicRegistration": true - }, - "rename": { - "dynamicRegistration": true, - "prepareSupport": true, - "prepareSupportDefaultBehavior": 1, - "honorsChangeAnnotations": true - }, - "documentLink": { - "dynamicRegistration": true, - "tooltipSupport": true - }, - "typeDefinition": { - "dynamicRegistration": true, - "linkSupport": true - }, - "implementation": { - "dynamicRegistration": true, - "linkSupport": true - }, - "colorProvider": { - "dynamicRegistration": true - }, - "foldingRange": { - "dynamicRegistration": true, - "rangeLimit": 5000, - "lineFoldingOnly": true, - "foldingRangeKind": { - "valueSet": [ - "comment", - "imports", - "region" - ] - }, - "foldingRange": { - "collapsedText": false - } - }, - "declaration": { - "dynamicRegistration": true, - "linkSupport": true - }, - "selectionRange": { - "dynamicRegistration": true - }, - "callHierarchy": { - "dynamicRegistration": true - }, - "semanticTokens": { - "dynamicRegistration": true, - "tokenTypes": [ - "namespace", - "type", - "class", - "enum", - "interface", - "struct", - "typeParameter", - "parameter", - "variable", - "property", - "enumMember", - "event", - "function", - "method", - "macro", - "keyword", - "modifier", - "comment", - "string", - "number", - "regexp", - "operator", - "decorator" - ], - "tokenModifiers": [ - "declaration", - "definition", - "readonly", - "static", - "deprecated", - "abstract", - "async", - "modification", - "documentation", - "defaultLibrary" - ], - "formats": [ - "relative" - ], - "requests": { - "range": true, - "full": { - "delta": true - } - }, - "multilineTokenSupport": false, - "overlappingTokenSupport": false, - "serverCancelSupport": true, - "augmentsSyntaxTokens": true - }, - "linkedEditingRange": { - "dynamicRegistration": true - }, - "typeHierarchy": { - "dynamicRegistration": true - }, - "inlineValue": { - "dynamicRegistration": true - }, - "inlayHint": { - "dynamicRegistration": true, - "resolveSupport": { - "properties": [ - "tooltip", - "textEdits", - "label.tooltip", - "label.location", - "label.command" - ] - } - }, - "diagnostic": { - "dynamicRegistration": true, - "relatedDocumentSupport": false - } - }, - "window": { - "showMessage": { - "messageActionItem": { - "additionalPropertiesSupport": true - } - }, - "showDocument": { - "support": true - }, - "workDoneProgress": true - }, - "general": { - "staleRequestSupport": { - "cancel": true, - "retryOnContentModified": [ - "textDocument/semanticTokens/full", - "textDocument/semanticTokens/range", - "textDocument/semanticTokens/full/delta" - ] - }, - "regularExpressions": { - "engine": "ECMAScript", - "version": "ES2020" - }, - "markdown": { - "parser": "marked", - "version": "1.1.0" - }, - "positionEncodings": [ - "utf-16" - ] - }, - "notebookDocument": { - "synchronization": { - "dynamicRegistration": true, - "executionSummarySupport": true - } - } - }, - "initializationOptions": { - "workspaceFolders": "[pathlib.Path(repository_absolute_path).as_uri()]", - "storagePath": null, - "codegen": { - "enabled": false - }, - "compiler": { - "jvm": { - "target": "default" - } - }, - "completion": { - "snippets": { - "enabled": true - } - }, - "diagnostics": { - "enabled": true, - "level": 4, - "debounceTime": 250 - }, - "scripts": { - "enabled": true, - "buildScriptsEnabled": true - }, - "indexing": { - "enabled": true - }, - "externalSources": { - "useKlsScheme": false, - "autoConvertToKotlin": false - }, - "inlayHints": { - "typeHints": false, - "parameterHints": false, - "chainedHints": false - }, - "formatting": { - "formatter": "ktfmt", - "ktfmt": { - "style": "google", - "indent": 4, - "maxWidth": 100, - "continuationIndent": 8, - "removeUnusedImports": true - } - } - }, - "trace": "verbose", - "workspaceFolders": "[\n {\n \"uri\": pathlib.Path(repository_absolute_path).as_uri(),\n \"name\": os.path.basename(repository_absolute_path),\n }\n ]" -} diff --git a/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py b/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py deleted file mode 100644 index 6ddab2c7..00000000 --- a/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py +++ /dev/null @@ -1,209 +0,0 @@ -""" -Provides Kotlin specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Kotlin. -""" - -import dataclasses -import json -import logging -import os -import pathlib -import stat - -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.lsp_protocol_handler.lsp_types import InitializeParams -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo - - -@dataclasses.dataclass -class KotlinRuntimeDependencyPaths: - """ - Stores the paths to the runtime dependencies of Kotlin Language Server - """ - - java_path: str - java_home_path: str - kotlin_executable_path: str - - -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): - """ - 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) - self.runtime_dependency_paths = runtime_dependency_paths - - # Create command to execute the Kotlin Language Server script - cmd = f'"{self.runtime_dependency_paths.kotlin_executable_path}"' - - # Set environment variables including JAVA_HOME - proc_env = {"JAVA_HOME": self.runtime_dependency_paths.java_home_path} - - super().__init__( - config, - logger, - repository_root_path, - ProcessLaunchInfo(cmd=cmd, env=proc_env, cwd=repository_root_path), - "kotlin", - ) - - def setup_runtime_dependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> KotlinRuntimeDependencyPaths: - """ - Setup runtime dependencies for Kotlin Language Server. - """ - platform_id = PlatformUtils.get_platform_id() - - # Verify platform support - assert ( - platform_id.value.startswith("win-") or platform_id.value.startswith("linux-") or platform_id.value.startswith("osx-") - ), "Only Windows, Linux and macOS platforms are supported for Kotlin in multilspy at the moment" - - # Load dependency information - with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), encoding="utf-8") as f: - d = json.load(f) - del d["_description"] - - kotlin_dependency = d["runtimeDependency"] - java_dependency = d["java"][platform_id.value] - - # Setup paths for dependencies - static_dir = os.path.join(os.path.dirname(__file__), "static") - os.makedirs(static_dir, exist_ok=True) - - # Setup Java paths - java_dir = os.path.join(static_dir, "java") - os.makedirs(java_dir, exist_ok=True) - - java_home_path = os.path.join(java_dir, java_dependency["java_home_path"]) - java_path = os.path.join(java_dir, java_dependency["java_path"]) - - # Download and extract Java if not exists - if not os.path.exists(java_path): - logger.log(f"Downloading Java for {platform_id.value}...", logging.INFO) - FileUtils.download_and_extract_archive(logger, java_dependency["url"], java_dir, java_dependency["archiveType"]) - # Make Java executable - if not platform_id.value.startswith("win-"): - os.chmod(java_path, 0o755) - - assert os.path.exists(java_path), f"Java executable not found at {java_path}" - - # Setup Kotlin Language Server paths - kotlin_ls_dir = os.path.join(static_dir, "server") - - # Get platform-specific executable script path - if platform_id.value.startswith("win-"): - kotlin_script = os.path.join(kotlin_ls_dir, "bin", "kotlin-language-server.bat") - else: - kotlin_script = os.path.join(kotlin_ls_dir, "bin", "kotlin-language-server") - - # Download and extract Kotlin Language Server if script doesn't exist - if not os.path.exists(kotlin_script): - logger.log("Downloading Kotlin Language Server...", logging.INFO) - FileUtils.download_and_extract_archive(logger, kotlin_dependency["url"], static_dir, kotlin_dependency["archiveType"]) - - # Make script executable on Unix platforms - if os.path.exists(kotlin_script) and not platform_id.value.startswith("win-"): - os.chmod( - kotlin_script, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH - ) - - # Use script file - if os.path.exists(kotlin_script): - kotlin_executable_path = kotlin_script - logger.log(f"Using Kotlin Language Server script at {kotlin_script}", logging.INFO) - else: - raise FileNotFoundError(f"Kotlin Language Server script not found at {kotlin_script}") - - return KotlinRuntimeDependencyPaths( - java_path=java_path, java_home_path=java_home_path, kotlin_executable_path=kotlin_executable_path - ) - - def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: - """ - Returns the initialize params for the Kotlin Language Server. - """ - with open(str(pathlib.PurePath(os.path.dirname(__file__), "initialize_params.json")), encoding="utf-8") as f: - d: InitializeParams = json.load(f) - - del d["_description"] - - if not os.path.isabs(repository_absolute_path): - repository_absolute_path = os.path.abspath(repository_absolute_path) - - assert d["processId"] == "os.getpid()" - d["processId"] = os.getpid() - - assert d["rootPath"] == "repository_absolute_path" - d["rootPath"] = repository_absolute_path - - assert d["rootUri"] == "pathlib.Path(repository_absolute_path).as_uri()" - d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() - - assert d["initializationOptions"]["workspaceFolders"] == "[pathlib.Path(repository_absolute_path).as_uri()]" - d["initializationOptions"]["workspaceFolders"] = [pathlib.Path(repository_absolute_path).as_uri()] - - assert ( - d["workspaceFolders"] - == '[\n {\n "uri": pathlib.Path(repository_absolute_path).as_uri(),\n "name": os.path.basename(repository_absolute_path),\n }\n ]' - ) - d["workspaceFolders"] = [ - { - "uri": pathlib.Path(repository_absolute_path).as_uri(), - "name": os.path.basename(repository_absolute_path), - } - ] - - return d - - def _start_server(self): - """ - Starts the Kotlin Language Server - """ - - def execute_client_command_handler(params): - return [] - - def do_nothing(params): - return - - def window_log_message(msg): - self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) - - self.server.on_request("client/registerCapability", do_nothing) - self.server.on_notification("language/status", do_nothing) - self.server.on_notification("window/logMessage", window_log_message) - self.server.on_request("workspace/executeClientCommand", execute_client_command_handler) - self.server.on_notification("$/progress", do_nothing) - self.server.on_notification("textDocument/publishDiagnostics", do_nothing) - self.server.on_notification("language/actionableNotification", do_nothing) - - self.logger.log("Starting Kotlin server process", logging.INFO) - self.server.start() - initialize_params = self._get_initialize_params(self.repository_root_path) - - self.logger.log( - "Sending initialize request from LSP client to LSP server and awaiting response", - logging.INFO, - ) - init_response = self.server.send.initialize(initialize_params) - - capabilities = init_response["capabilities"] - assert "textDocumentSync" in capabilities, "Server must support textDocumentSync" - assert "hoverProvider" in capabilities, "Server must support hover" - assert "completionProvider" in capabilities, "Server must support code completion" - assert "signatureHelpProvider" in capabilities, "Server must support signature help" - assert "definitionProvider" in capabilities, "Server must support go to definition" - assert "referencesProvider" in capabilities, "Server must support find references" - assert "documentSymbolProvider" in capabilities, "Server must support document symbols" - assert "workspaceSymbolProvider" in capabilities, "Server must support workspace symbols" - assert "semanticTokensProvider" in capabilities, "Server must support semantic tokens" - - self.server.notify.initialized({}) - self.completions_available.set() diff --git a/src/solidlsp/language_servers/kotlin_language_server/runtime_dependencies.json b/src/solidlsp/language_servers/kotlin_language_server/runtime_dependencies.json deleted file mode 100644 index 3568c9d4..00000000 --- a/src/solidlsp/language_servers/kotlin_language_server/runtime_dependencies.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "_description": "Used to download the runtime dependencies for Kotlin Language Server from https://github.com/fwcd/kotlin-language-server", - "runtimeDependency": { - "id": "KotlinLsp", - "description": "Kotlin Language Server", - "url": "https://github.com/fwcd/kotlin-language-server/releases/download/1.3.13/server.zip", - "archiveType": "zip" - }, - "java": { - "win-x64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-win32-x64-1.42.0-561.vsix", - "archiveType": "zip", - "java_home_path": "extension/jre/21.0.7-win32-x86_64", - "java_path": "extension/jre/21.0.7-win32-x86_64/bin/java.exe" - }, - "linux-x64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-x64-1.42.0-561.vsix", - "archiveType": "zip", - "java_home_path": "extension/jre/21.0.7-linux-x86_64", - "java_path": "extension/jre/21.0.7-linux-x86_64/bin/java" - }, - "linux-arm64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-arm64-1.42.0-561.vsix", - "archiveType": "zip", - "java_home_path": "extension/jre/21.0.7-linux-aarch64", - "java_path": "extension/jre/21.0.7-linux-aarch64/bin/java" - }, - "osx-x64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-x64-1.42.0-561.vsix", - "archiveType": "zip", - "java_home_path": "extension/jre/21.0.7-macosx-x86_64", - "java_path": "extension/jre/21.0.7-macosx-x86_64/bin/java" - }, - "osx-arm64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix", - "archiveType": "zip", - "java_home_path": "extension/jre/21.0.7-macosx-aarch64", - "java_path": "extension/jre/21.0.7-macosx-aarch64/bin/java" - } - } -} diff --git a/src/solidlsp/language_servers/omnisharp/omnisharp.py b/src/solidlsp/language_servers/omnisharp.py similarity index 93% rename from src/solidlsp/language_servers/omnisharp/omnisharp.py rename to src/solidlsp/language_servers/omnisharp.py index e66540ca..046d1e84 100644 --- a/src/solidlsp/language_servers/omnisharp/omnisharp.py +++ b/src/solidlsp/language_servers/omnisharp.py @@ -63,7 +63,7 @@ class OmniSharp(SolidLanguageServer): """ Creates an OmniSharp instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - omnisharp_executable_path, dll_path = self.setupRuntimeDependencies(logger, config) + omnisharp_executable_path, dll_path = self._setup_runtime_dependencies(logger, config) slnfilename = find_least_depth_sln_file(repository_root_path) if slnfilename is None: @@ -112,11 +112,12 @@ class OmniSharp(SolidLanguageServer): def is_ignored_dirname(self, dirname: str) -> bool: return super().is_ignored_dirname(dirname) or dirname in ["bin", "obj"] - 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 Omnisharp Language Server. """ - with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), encoding="utf-8") as f: + with open(os.path.join(os.path.dirname(__file__), "omnisharp", "initialize_params.json"), encoding="utf-8") as f: d = json.load(f) del d["_description"] @@ -136,30 +137,32 @@ class OmniSharp(SolidLanguageServer): return d - def setupRuntimeDependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> tuple[str, str]: + @classmethod + def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> tuple[str, str]: """ Setup runtime dependencies for OmniSharp. """ platform_id = PlatformUtils.get_platform_id() dotnet_version = PlatformUtils.get_dotnet_version() - with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), encoding="utf-8") as f: + with open(os.path.join(os.path.dirname(__file__), "omnisharp", "runtime_dependencies.json"), encoding="utf-8") as f: d = json.load(f) del d["_description"] assert platform_id in [ PlatformId.LINUX_x64, PlatformId.WIN_x64, - ], "Only linux-x64 and win-x64 platform is supported for in multilspy at the moment" + ], f"Only linux-x64 and win-x64 platform is supported at the moment but got {platform_id=}" assert dotnet_version in [ DotnetVersion.V6, DotnetVersion.V7, DotnetVersion.V8, - ], "Only dotnet version 6 and 7 are supported in multilspy at the moment" + DotnetVersion.V9, + ], f"Only dotnet version 6-9 are supported at the moment but got {dotnet_version=}" # TODO: Do away with this assumption # Currently, runtime binaries are not available for .Net 7 and .Net 8. Hence, we assume .Net 6 runtime binaries to be compatible with .Net 7, .Net 8 - if dotnet_version in [DotnetVersion.V7, DotnetVersion.V8]: + if dotnet_version in [DotnetVersion.V7, DotnetVersion.V8, DotnetVersion.V9]: dotnet_version = DotnetVersion.V6 runtime_dependencies = d["runtimeDependencies"] @@ -178,7 +181,7 @@ class OmniSharp(SolidLanguageServer): assert "OmniSharp" in runtime_dependencies assert "RazorOmnisharp" in runtime_dependencies - omnisharp_ls_dir = os.path.join(os.path.dirname(__file__), "static", "OmniSharp") + omnisharp_ls_dir = os.path.join(cls.ls_resources_dir(), "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") @@ -186,7 +189,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(os.path.dirname(__file__), "static", "RazorOmnisharp") + razor_omnisharp_ls_dir = os.path.join(cls.ls_resources_dir(), "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") @@ -363,7 +366,7 @@ class OmniSharp(SolidLanguageServer): ) init_response = self.server.send.initialize(initialize_params) self.server.notify.initialized({}) - with open(os.path.join(os.path.dirname(__file__), "workspace_did_change_configuration.json"), encoding="utf-8") as f: + with open(os.path.join(os.path.dirname(__file__), "omnisharp", "workspace_did_change_configuration.json"), encoding="utf-8") as f: self.server.notify.workspace_did_change_configuration({"settings": json.load(f)}) assert "capabilities" in init_response if "definitionProvider" in init_response["capabilities"] and init_response["capabilities"]["definitionProvider"]: diff --git a/src/solidlsp/language_servers/pyright_language_server/pyright_server.py b/src/solidlsp/language_servers/pyright_server.py similarity index 78% rename from src/solidlsp/language_servers/pyright_language_server/pyright_server.py rename to src/solidlsp/language_servers/pyright_server.py index 893db94e..905bb3d6 100644 --- a/src/solidlsp/language_servers/pyright_language_server/pyright_server.py +++ b/src/solidlsp/language_servers/pyright_server.py @@ -46,7 +46,8 @@ class PyrightServer(SolidLanguageServer): def is_ignored_dirname(self, dirname: str) -> bool: return super().is_ignored_dirname(dirname) or dirname in ["venv", "__pycache__"] - 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 Pyright Language Server. """ @@ -68,34 +69,17 @@ class PyrightServer(SolidLanguageServer): }, "capabilities": { "workspace": { - "applyEdit": True, "workspaceEdit": {"documentChanges": True}, "didChangeConfiguration": {"dynamicRegistration": True}, "didChangeWatchedFiles": {"dynamicRegistration": True}, "symbol": { "dynamicRegistration": 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] - }, + "symbolKind": {"valueSet": list(range(1, 27))}, }, "executeCommand": {"dynamicRegistration": True}, }, "textDocument": { "synchronization": {"dynamicRegistration": True, "willSave": True, "willSaveWaitUntil": True, "didSave": True}, - "completion": { - "dynamicRegistration": True, - "contextSupport": True, - "completionItem": { - "snippetSupport": True, - "commitCharactersSupport": True, - "documentationFormat": ["markdown", "plaintext"], - "deprecatedSupport": True, - "preselectSupport": True, - }, - "completionItemKind": { - "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] - }, - }, "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]}, "signatureHelp": { "dynamicRegistration": True, @@ -106,36 +90,11 @@ class PyrightServer(SolidLanguageServer): }, "definition": {"dynamicRegistration": True}, "references": {"dynamicRegistration": True}, - "documentHighlight": {"dynamicRegistration": True}, "documentSymbol": { "dynamicRegistration": 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] - }, + "symbolKind": {"valueSet": list(range(1, 27))}, "hierarchicalDocumentSymbolSupport": True, }, - "codeAction": { - "dynamicRegistration": True, - "codeActionLiteralSupport": { - "codeActionKind": { - "valueSet": [ - "", - "quickfix", - "refactor", - "refactor.extract", - "refactor.inline", - "refactor.rewrite", - "source", - "source.organizeImports", - ] - } - }, - }, - "codeLens": {"dynamicRegistration": True}, - "formatting": {"dynamicRegistration": True}, - "rangeFormatting": {"dynamicRegistration": True}, - "onTypeFormatting": {"dynamicRegistration": True}, - "rename": {"dynamicRegistration": True}, "publishDiagnostics": {"relatedInformation": True}, }, }, diff --git a/src/solidlsp/language_servers/rust_analyzer.py b/src/solidlsp/language_servers/rust_analyzer.py new file mode 100644 index 00000000..0f2f2cf1 --- /dev/null +++ b/src/solidlsp/language_servers/rust_analyzer.py @@ -0,0 +1,640 @@ +""" +Provides Rust specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Rust. +""" + +import logging +import os +import pathlib +import stat +import threading + +from overrides import override + +from solidlsp.ls import SolidLanguageServer +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 .common import RuntimeDependency, RuntimeDependencyCollection + + +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): + """ + 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) + super().__init__( + config, + logger, + repository_root_path, + ProcessLaunchInfo(cmd=rustanalyzer_executable_path, cwd=repository_root_path), + "rust", + ) + 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() + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + return super().is_ignored_dirname(dirname) or dirname in ["target"] + + @classmethod + def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> str: + """ + Setup runtime dependencies for rust_analyzer and return the command to start the server. + """ + deps = RuntimeDependencyCollection( + [ + RuntimeDependency( + id="RustAnalyzer", + description="RustAnalyzer for macOS (arm64)", + url="https://github.com/rust-lang/rust-analyzer/releases/download/2023-10-09/rust-analyzer-aarch64-apple-darwin.gz", + platform_id="osx-arm64", + archive_type="gz", + binary_name="rust_analyzer", + ), + RuntimeDependency( + id="RustAnalyzer", + description="RustAnalyzer for Linux (x64)", + url="https://github.com/rust-lang/rust-analyzer/releases/download/2023-10-09/rust-analyzer-x86_64-unknown-linux-gnu.gz", + platform_id="linux-x64", + archive_type="gz", + binary_name="rust_analyzer", + ), + RuntimeDependency( + id="RustAnalyzer", + description="RustAnalyzer for Linux (arm64)", + url="https://github.com/rust-lang/rust-analyzer/releases/download/2023-10-09/rust-analyzer-aarch64-unknown-linux-gnu.gz", + platform_id="linux-arm64", + archive_type="gz", + binary_name="rust_analyzer", + ), + RuntimeDependency( + id="RustAnalyzer", + description="RustAnalyzer for Windows (x64)", + url="https://github.com/rust-lang/rust-analyzer/releases/download/2023-10-09/rust-analyzer-x86_64-pc-windows-msvc.zip", + platform_id="win-x64", + archive_type="zip", + binary_name="rust-analyzer.exe", + ), + ] + ) + + # assert platform_id.value in [ + # "linux-x64", + # "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_executable_path = deps.binary_path(rustanalyzer_ls_dir) + if not os.path.exists(rustanalyzer_ls_dir): + os.makedirs(rustanalyzer_ls_dir) + deps.install(logger, rustanalyzer_ls_dir) + assert os.path.exists(rustanalyzer_executable_path) + os.chmod(rustanalyzer_executable_path, stat.S_IEXEC) + + return rustanalyzer_executable_path + + @staticmethod + def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: + """ + Returns the initialize params for the Rust Analyzer Language Server. + """ + root_uri = pathlib.Path(repository_absolute_path).as_uri() + initialize_params = { + "clientInfo": {"name": "Visual Studio Code - Insiders", "version": "1.82.0-insider"}, + "locale": "en", + "capabilities": { + "workspace": { + "applyEdit": True, + "workspaceEdit": { + "documentChanges": True, + "resourceOperations": ["create", "rename", "delete"], + "failureHandling": "textOnlyTransactional", + "normalizesLineEndings": True, + "changeAnnotationSupport": {"groupsOnLabel": True}, + }, + "configuration": True, + "didChangeWatchedFiles": {"dynamicRegistration": True, "relativePatternSupport": True}, + "symbol": { + "dynamicRegistration": True, + "symbolKind": {"valueSet": list(range(1, 27))}, + "tagSupport": {"valueSet": [1]}, + "resolveSupport": {"properties": ["location.range"]}, + }, + "codeLens": {"refreshSupport": True}, + "executeCommand": {"dynamicRegistration": True}, + "didChangeConfiguration": {"dynamicRegistration": True}, + "workspaceFolders": True, + "semanticTokens": {"refreshSupport": True}, + "fileOperations": { + "dynamicRegistration": True, + "didCreate": True, + "didRename": True, + "didDelete": True, + "willCreate": True, + "willRename": True, + "willDelete": True, + }, + "inlineValue": {"refreshSupport": True}, + "inlayHint": {"refreshSupport": True}, + "diagnostics": {"refreshSupport": True}, + }, + "textDocument": { + "publishDiagnostics": { + "relatedInformation": True, + "versionSupport": False, + "tagSupport": {"valueSet": [1, 2]}, + "codeDescriptionSupport": True, + "dataSupport": True, + }, + "synchronization": {"dynamicRegistration": True, "willSave": True, "willSaveWaitUntil": True, "didSave": True}, + "completion": { + "dynamicRegistration": True, + "contextSupport": True, + "completionItem": { + "snippetSupport": True, + "commitCharactersSupport": True, + "documentationFormat": ["markdown", "plaintext"], + "deprecatedSupport": True, + "preselectSupport": True, + "tagSupport": {"valueSet": [1]}, + "insertReplaceSupport": True, + "resolveSupport": {"properties": ["documentation", "detail", "additionalTextEdits"]}, + "insertTextModeSupport": {"valueSet": [1, 2]}, + "labelDetailsSupport": True, + }, + "insertTextMode": 2, + "completionItemKind": { + "valueSet": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25] + }, + "completionList": {"itemDefaults": ["commitCharacters", "editRange", "insertTextFormat", "insertTextMode"]}, + }, + "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]}, + "signatureHelp": { + "dynamicRegistration": True, + "signatureInformation": { + "documentationFormat": ["markdown", "plaintext"], + "parameterInformation": {"labelOffsetSupport": True}, + "activeParameterSupport": True, + }, + "contextSupport": True, + }, + "definition": {"dynamicRegistration": True, "linkSupport": True}, + "references": {"dynamicRegistration": True}, + "documentHighlight": {"dynamicRegistration": True}, + "documentSymbol": { + "dynamicRegistration": True, + "symbolKind": {"valueSet": list(range(1, 27))}, + "hierarchicalDocumentSymbolSupport": True, + "tagSupport": {"valueSet": [1]}, + "labelSupport": True, + }, + "codeAction": { + "dynamicRegistration": True, + "isPreferredSupport": True, + "disabledSupport": True, + "dataSupport": True, + "resolveSupport": {"properties": ["edit"]}, + "codeActionLiteralSupport": { + "codeActionKind": { + "valueSet": [ + "", + "quickfix", + "refactor", + "refactor.extract", + "refactor.inline", + "refactor.rewrite", + "source", + "source.organizeImports", + ] + } + }, + "honorsChangeAnnotations": False, + }, + "codeLens": {"dynamicRegistration": True}, + "formatting": {"dynamicRegistration": True}, + "rangeFormatting": {"dynamicRegistration": True}, + "onTypeFormatting": {"dynamicRegistration": True}, + "rename": { + "dynamicRegistration": True, + "prepareSupport": True, + "prepareSupportDefaultBehavior": 1, + "honorsChangeAnnotations": True, + }, + "documentLink": {"dynamicRegistration": True, "tooltipSupport": True}, + "typeDefinition": {"dynamicRegistration": True, "linkSupport": True}, + "implementation": {"dynamicRegistration": True, "linkSupport": True}, + "colorProvider": {"dynamicRegistration": True}, + "foldingRange": { + "dynamicRegistration": True, + "rangeLimit": 5000, + "lineFoldingOnly": True, + "foldingRangeKind": {"valueSet": ["comment", "imports", "region"]}, + "foldingRange": {"collapsedText": False}, + }, + "declaration": {"dynamicRegistration": True, "linkSupport": True}, + "selectionRange": {"dynamicRegistration": True}, + "callHierarchy": {"dynamicRegistration": True}, + "semanticTokens": { + "dynamicRegistration": True, + "tokenTypes": [ + "namespace", + "type", + "class", + "enum", + "interface", + "struct", + "typeParameter", + "parameter", + "variable", + "property", + "enumMember", + "event", + "function", + "method", + "macro", + "keyword", + "modifier", + "comment", + "string", + "number", + "regexp", + "operator", + "decorator", + ], + "tokenModifiers": [ + "declaration", + "definition", + "readonly", + "static", + "deprecated", + "abstract", + "async", + "modification", + "documentation", + "defaultLibrary", + ], + "formats": ["relative"], + "requests": {"range": True, "full": {"delta": True}}, + "multilineTokenSupport": False, + "overlappingTokenSupport": False, + "serverCancelSupport": True, + "augmentsSyntaxTokens": False, + }, + "linkedEditingRange": {"dynamicRegistration": True}, + "typeHierarchy": {"dynamicRegistration": True}, + "inlineValue": {"dynamicRegistration": True}, + "inlayHint": { + "dynamicRegistration": True, + "resolveSupport": {"properties": ["tooltip", "textEdits", "label.tooltip", "label.location", "label.command"]}, + }, + "diagnostic": {"dynamicRegistration": True, "relatedDocumentSupport": False}, + }, + "window": { + "showMessage": {"messageActionItem": {"additionalPropertiesSupport": True}}, + "showDocument": {"support": True}, + "workDoneProgress": True, + }, + "general": { + "staleRequestSupport": { + "cancel": True, + "retryOnContentModified": [ + "textDocument/semanticTokens/full", + "textDocument/semanticTokens/range", + "textDocument/semanticTokens/full/delta", + ], + }, + "regularExpressions": {"engine": "ECMAScript", "version": "ES2020"}, + "markdown": { + "parser": "marked", + "version": "1.1.0", + "allowedTags": [ + "ul", + "li", + "p", + "code", + "blockquote", + "ol", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "hr", + "em", + "pre", + "table", + "thead", + "tbody", + "tr", + "th", + "td", + "div", + "del", + "a", + "strong", + "br", + "img", + "span", + ], + }, + "positionEncodings": ["utf-16"], + }, + "notebookDocument": {"synchronization": {"dynamicRegistration": True, "executionSummarySupport": True}}, + "experimental": { + "snippetTextEdit": True, + "codeActionGroup": True, + "hoverActions": True, + "serverStatusNotification": True, + "colorDiagnosticOutput": True, + "openServerLogs": True, + "localDocs": True, + "commands": { + "commands": [ + "rust-analyzer.runSingle", + "rust-analyzer.debugSingle", + "rust-analyzer.showReferences", + "rust-analyzer.gotoLocation", + "editor.action.triggerParameterHints", + ] + }, + }, + }, + "initializationOptions": { + "cargoRunner": None, + "runnables": {"extraEnv": None, "problemMatcher": ["$rustc"], "command": None, "extraArgs": []}, + "statusBar": {"clickAction": "openLogs"}, + "server": {"path": None, "extraEnv": None}, + "trace": {"server": "verbose", "extension": False}, + "debug": { + "engine": "auto", + "sourceFileMap": {"/rustc/": "${env:USERPROFILE}/.rustup/toolchains//lib/rustlib/src/rust"}, + "openDebugPane": False, + "engineSettings": {}, + }, + "restartServerOnConfigChange": False, + "typing": {"continueCommentsOnNewline": True, "autoClosingAngleBrackets": {"enable": False}}, + "diagnostics": { + "previewRustcOutput": False, + "useRustcErrorCode": False, + "disabled": [], + "enable": True, + "experimental": {"enable": False}, + "remapPrefix": {}, + "warningsAsHint": [], + "warningsAsInfo": [], + }, + "discoverProjectRunner": None, + "showUnlinkedFileNotification": True, + "showDependenciesExplorer": True, + "assist": {"emitMustUse": False, "expressionFillDefault": "todo"}, + "cachePriming": {"enable": True, "numThreads": 0}, + "cargo": { + "autoreload": True, + "buildScripts": { + "enable": True, + "invocationLocation": "workspace", + "invocationStrategy": "per_workspace", + "overrideCommand": None, + "useRustcWrapper": True, + }, + "cfgs": {}, + "extraArgs": [], + "extraEnv": {}, + "features": [], + "noDefaultFeatures": False, + "sysroot": "discover", + "sysrootSrc": None, + "target": None, + "unsetTest": ["core"], + }, + "checkOnSave": True, + "check": { + "allTargets": True, + "command": "check", + "extraArgs": [], + "extraEnv": {}, + "features": None, + "ignore": [], + "invocationLocation": "workspace", + "invocationStrategy": "per_workspace", + "noDefaultFeatures": None, + "overrideCommand": None, + "targets": None, + }, + "completion": { + "autoimport": {"enable": True}, + "autoself": {"enable": True}, + "callable": {"snippets": "fill_arguments"}, + "fullFunctionSignatures": {"enable": False}, + "limit": None, + "postfix": {"enable": True}, + "privateEditable": {"enable": False}, + "snippets": { + "custom": { + "Arc::new": { + "postfix": "arc", + "body": "Arc::new(${receiver})", + "requires": "std::sync::Arc", + "description": "Put the expression into an `Arc`", + "scope": "expr", + }, + "Rc::new": { + "postfix": "rc", + "body": "Rc::new(${receiver})", + "requires": "std::rc::Rc", + "description": "Put the expression into an `Rc`", + "scope": "expr", + }, + "Box::pin": { + "postfix": "pinbox", + "body": "Box::pin(${receiver})", + "requires": "std::boxed::Box", + "description": "Put the expression into a pinned `Box`", + "scope": "expr", + }, + "Ok": { + "postfix": "ok", + "body": "Ok(${receiver})", + "description": "Wrap the expression in a `Result::Ok`", + "scope": "expr", + }, + "Err": { + "postfix": "err", + "body": "Err(${receiver})", + "description": "Wrap the expression in a `Result::Err`", + "scope": "expr", + }, + "Some": { + "postfix": "some", + "body": "Some(${receiver})", + "description": "Wrap the expression in an `Option::Some`", + "scope": "expr", + }, + } + }, + }, + "files": {"excludeDirs": [], "watcher": "client"}, + "highlightRelated": { + "breakPoints": {"enable": True}, + "closureCaptures": {"enable": True}, + "exitPoints": {"enable": True}, + "references": {"enable": True}, + "yieldPoints": {"enable": True}, + }, + "hover": { + "actions": { + "debug": {"enable": True}, + "enable": True, + "gotoTypeDef": {"enable": True}, + "implementations": {"enable": True}, + "references": {"enable": False}, + "run": {"enable": True}, + }, + "documentation": {"enable": True, "keywords": {"enable": True}}, + "links": {"enable": True}, + "memoryLayout": {"alignment": "hexadecimal", "enable": True, "niches": False, "offset": "hexadecimal", "size": "both"}, + }, + "imports": { + "granularity": {"enforce": False, "group": "crate"}, + "group": {"enable": True}, + "merge": {"glob": True}, + "preferNoStd": False, + "preferPrelude": False, + "prefix": "plain", + }, + "inlayHints": { + "bindingModeHints": {"enable": False}, + "chainingHints": {"enable": True}, + "closingBraceHints": {"enable": True, "minLines": 25}, + "closureCaptureHints": {"enable": False}, + "closureReturnTypeHints": {"enable": "never"}, + "closureStyle": "impl_fn", + "discriminantHints": {"enable": "never"}, + "expressionAdjustmentHints": {"enable": "never", "hideOutsideUnsafe": False, "mode": "prefix"}, + "lifetimeElisionHints": {"enable": "never", "useParameterNames": False}, + "maxLength": 25, + "parameterHints": {"enable": True}, + "reborrowHints": {"enable": "never"}, + "renderColons": True, + "typeHints": {"enable": True, "hideClosureInitialization": False, "hideNamedConstructor": False}, + }, + "interpret": {"tests": False}, + "joinLines": {"joinAssignments": True, "joinElseIf": True, "removeTrailingComma": True, "unwrapTrivialBlock": True}, + "lens": { + "debug": {"enable": True}, + "enable": True, + "forceCustomCommands": True, + "implementations": {"enable": True}, + "location": "above_name", + "references": { + "adt": {"enable": False}, + "enumVariant": {"enable": False}, + "method": {"enable": False}, + "trait": {"enable": False}, + }, + "run": {"enable": True}, + }, + "linkedProjects": [], + "lru": {"capacity": None, "query": {"capacities": {}}}, + "notifications": {"cargoTomlNotFound": True}, + "numThreads": None, + "procMacro": {"attributes": {"enable": True}, "enable": True, "ignored": {}, "server": None}, + "references": {"excludeImports": False}, + "rust": {"analyzerTargetDir": None}, + "rustc": {"source": None}, + "rustfmt": {"extraArgs": [], "overrideCommand": None, "rangeFormatting": {"enable": False}}, + "semanticHighlighting": { + "doc": {"comment": {"inject": {"enable": True}}}, + "nonStandardTokens": True, + "operator": {"enable": True, "specialization": {"enable": False}}, + "punctuation": {"enable": False, "separate": {"macro": {"bang": False}}, "specialization": {"enable": False}}, + "strings": {"enable": True}, + }, + "signatureInfo": {"detail": "full", "documentation": {"enable": True}}, + "workspace": {"symbol": {"search": {"kind": "only_types", "limit": 128, "scope": "workspace"}}}, + }, + "trace": "verbose", + "processId": os.getpid(), + "rootPath": repository_absolute_path, + "rootUri": root_uri, + "workspaceFolders": [ + { + "uri": root_uri, + "name": os.path.basename(repository_absolute_path), + } + ], + } + return initialize_params + + def _start_server(self): + """ + Starts the Rust Analyzer Language Server + """ + + def register_capability_handler(params): + assert "registrations" in params + for registration in params["registrations"]: + if registration["method"] == "workspace/executeCommand": + self.initialize_searcher_command_available.set() + self.resolve_main_method_available.set() + return + + def lang_status_handler(params): + # TODO: Should we wait for + # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}} + # Before proceeding? + if params["type"] == "ServiceReady" and params["message"] == "ServiceReady": + self.service_ready_event.set() + + def execute_client_command_handler(params): + return [] + + def do_nothing(params): + return + + def check_experimental_status(params): + if params["quiescent"] == True: + self.server_ready.set() + + def window_log_message(msg): + self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + self.server.on_request("client/registerCapability", register_capability_handler) + self.server.on_notification("language/status", lang_status_handler) + self.server.on_notification("window/logMessage", window_log_message) + self.server.on_request("workspace/executeClientCommand", execute_client_command_handler) + self.server.on_notification("$/progress", do_nothing) + self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + self.server.on_notification("language/actionableNotification", do_nothing) + self.server.on_notification("experimental/serverStatus", check_experimental_status) + + self.logger.log("Starting RustAnalyzer server process", logging.INFO) + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) + + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) + assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 + assert "completionProvider" in init_response["capabilities"] + assert init_response["capabilities"]["completionProvider"] == { + "resolveProvider": True, + "triggerCharacters": [":", ".", "'", "("], + "completionItem": {"labelDetailsSupport": True}, + } + self.server.notify.initialized({}) + self.completions_available.set() + + self.server_ready.wait() diff --git a/src/solidlsp/language_servers/rust_analyzer/initialize_params.json b/src/solidlsp/language_servers/rust_analyzer/initialize_params.json deleted file mode 100644 index 00368eed..00000000 --- a/src/solidlsp/language_servers/rust_analyzer/initialize_params.json +++ /dev/null @@ -1,917 +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()", - "clientInfo": { - "name": "Visual Studio Code - Insiders", - "version": "1.82.0-insider" - }, - "locale": "en", - "rootPath": "$rootPath", - "rootUri": "$rootUri", - "capabilities": { - "workspace": { - "applyEdit": true, - "workspaceEdit": { - "documentChanges": true, - "resourceOperations": [ - "create", - "rename", - "delete" - ], - "failureHandling": "textOnlyTransactional", - "normalizesLineEndings": true, - "changeAnnotationSupport": { - "groupsOnLabel": true - } - }, - "configuration": true, - "didChangeWatchedFiles": { - "dynamicRegistration": true, - "relativePatternSupport": true - }, - "symbol": { - "dynamicRegistration": 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 - ] - }, - "tagSupport": { - "valueSet": [ - 1 - ] - }, - "resolveSupport": { - "properties": [ - "location.range" - ] - } - }, - "codeLens": { - "refreshSupport": true - }, - "executeCommand": { - "dynamicRegistration": true - }, - "didChangeConfiguration": { - "dynamicRegistration": true - }, - "workspaceFolders": true, - "semanticTokens": { - "refreshSupport": true - }, - "fileOperations": { - "dynamicRegistration": true, - "didCreate": true, - "didRename": true, - "didDelete": true, - "willCreate": true, - "willRename": true, - "willDelete": true - }, - "inlineValue": { - "refreshSupport": true - }, - "inlayHint": { - "refreshSupport": true - }, - "diagnostics": { - "refreshSupport": true - } - }, - "textDocument": { - "publishDiagnostics": { - "relatedInformation": true, - "versionSupport": false, - "tagSupport": { - "valueSet": [ - 1, - 2 - ] - }, - "codeDescriptionSupport": true, - "dataSupport": true - }, - "synchronization": { - "dynamicRegistration": true, - "willSave": true, - "willSaveWaitUntil": true, - "didSave": true - }, - "completion": { - "dynamicRegistration": true, - "contextSupport": true, - "completionItem": { - "snippetSupport": true, - "commitCharactersSupport": true, - "documentationFormat": [ - "markdown", - "plaintext" - ], - "deprecatedSupport": true, - "preselectSupport": true, - "tagSupport": { - "valueSet": [ - 1 - ] - }, - "insertReplaceSupport": true, - "resolveSupport": { - "properties": [ - "documentation", - "detail", - "additionalTextEdits" - ] - }, - "insertTextModeSupport": { - "valueSet": [ - 1, - 2 - ] - }, - "labelDetailsSupport": true - }, - "insertTextMode": 2, - "completionItemKind": { - "valueSet": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25 - ] - }, - "completionList": { - "itemDefaults": [ - "commitCharacters", - "editRange", - "insertTextFormat", - "insertTextMode" - ] - } - }, - "hover": { - "dynamicRegistration": true, - "contentFormat": [ - "markdown", - "plaintext" - ] - }, - "signatureHelp": { - "dynamicRegistration": true, - "signatureInformation": { - "documentationFormat": [ - "markdown", - "plaintext" - ], - "parameterInformation": { - "labelOffsetSupport": true - }, - "activeParameterSupport": true - }, - "contextSupport": true - }, - "definition": { - "dynamicRegistration": true, - "linkSupport": true - }, - "references": { - "dynamicRegistration": true - }, - "documentHighlight": { - "dynamicRegistration": true - }, - "documentSymbol": { - "dynamicRegistration": 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 - ] - }, - "hierarchicalDocumentSymbolSupport": true, - "tagSupport": { - "valueSet": [ - 1 - ] - }, - "labelSupport": true - }, - "codeAction": { - "dynamicRegistration": true, - "isPreferredSupport": true, - "disabledSupport": true, - "dataSupport": true, - "resolveSupport": { - "properties": [ - "edit" - ] - }, - "codeActionLiteralSupport": { - "codeActionKind": { - "valueSet": [ - "", - "quickfix", - "refactor", - "refactor.extract", - "refactor.inline", - "refactor.rewrite", - "source", - "source.organizeImports" - ] - } - }, - "honorsChangeAnnotations": false - }, - "codeLens": { - "dynamicRegistration": true - }, - "formatting": { - "dynamicRegistration": true - }, - "rangeFormatting": { - "dynamicRegistration": true - }, - "onTypeFormatting": { - "dynamicRegistration": true - }, - "rename": { - "dynamicRegistration": true, - "prepareSupport": true, - "prepareSupportDefaultBehavior": 1, - "honorsChangeAnnotations": true - }, - "documentLink": { - "dynamicRegistration": true, - "tooltipSupport": true - }, - "typeDefinition": { - "dynamicRegistration": true, - "linkSupport": true - }, - "implementation": { - "dynamicRegistration": true, - "linkSupport": true - }, - "colorProvider": { - "dynamicRegistration": true - }, - "foldingRange": { - "dynamicRegistration": true, - "rangeLimit": 5000, - "lineFoldingOnly": true, - "foldingRangeKind": { - "valueSet": [ - "comment", - "imports", - "region" - ] - }, - "foldingRange": { - "collapsedText": false - } - }, - "declaration": { - "dynamicRegistration": true, - "linkSupport": true - }, - "selectionRange": { - "dynamicRegistration": true - }, - "callHierarchy": { - "dynamicRegistration": true - }, - "semanticTokens": { - "dynamicRegistration": true, - "tokenTypes": [ - "namespace", - "type", - "class", - "enum", - "interface", - "struct", - "typeParameter", - "parameter", - "variable", - "property", - "enumMember", - "event", - "function", - "method", - "macro", - "keyword", - "modifier", - "comment", - "string", - "number", - "regexp", - "operator", - "decorator" - ], - "tokenModifiers": [ - "declaration", - "definition", - "readonly", - "static", - "deprecated", - "abstract", - "async", - "modification", - "documentation", - "defaultLibrary" - ], - "formats": [ - "relative" - ], - "requests": { - "range": true, - "full": { - "delta": true - } - }, - "multilineTokenSupport": false, - "overlappingTokenSupport": false, - "serverCancelSupport": true, - "augmentsSyntaxTokens": false - }, - "linkedEditingRange": { - "dynamicRegistration": true - }, - "typeHierarchy": { - "dynamicRegistration": true - }, - "inlineValue": { - "dynamicRegistration": true - }, - "inlayHint": { - "dynamicRegistration": true, - "resolveSupport": { - "properties": [ - "tooltip", - "textEdits", - "label.tooltip", - "label.location", - "label.command" - ] - } - }, - "diagnostic": { - "dynamicRegistration": true, - "relatedDocumentSupport": false - } - }, - "window": { - "showMessage": { - "messageActionItem": { - "additionalPropertiesSupport": true - } - }, - "showDocument": { - "support": true - }, - "workDoneProgress": true - }, - "general": { - "staleRequestSupport": { - "cancel": true, - "retryOnContentModified": [ - "textDocument/semanticTokens/full", - "textDocument/semanticTokens/range", - "textDocument/semanticTokens/full/delta" - ] - }, - "regularExpressions": { - "engine": "ECMAScript", - "version": "ES2020" - }, - "markdown": { - "parser": "marked", - "version": "1.1.0", - "allowedTags": [ - "ul", - "li", - "p", - "code", - "blockquote", - "ol", - "h1", - "h2", - "h3", - "h4", - "h5", - "h6", - "hr", - "em", - "pre", - "table", - "thead", - "tbody", - "tr", - "th", - "td", - "div", - "del", - "a", - "strong", - "br", - "img", - "span" - ] - }, - "positionEncodings": [ - "utf-16" - ] - }, - "notebookDocument": { - "synchronization": { - "dynamicRegistration": true, - "executionSummarySupport": true - } - }, - "experimental": { - "snippetTextEdit": true, - "codeActionGroup": true, - "hoverActions": true, - "serverStatusNotification": true, - "colorDiagnosticOutput": true, - "openServerLogs": true, - "localDocs": true, - "commands": { - "commands": [ - "rust-analyzer.runSingle", - "rust-analyzer.debugSingle", - "rust-analyzer.showReferences", - "rust-analyzer.gotoLocation", - "editor.action.triggerParameterHints" - ] - } - } - }, - "initializationOptions": { - "cargoRunner": null, - "runnables": { - "extraEnv": null, - "problemMatcher": [ - "$rustc" - ], - "command": null, - "extraArgs": [] - }, - "statusBar": { - "clickAction": "openLogs" - }, - "server": { - "path": null, - "extraEnv": null - }, - "trace": { - "server": "verbose", - "extension": false - }, - "debug": { - "engine": "auto", - "sourceFileMap": { - "/rustc/": "${env:USERPROFILE}/.rustup/toolchains//lib/rustlib/src/rust" - }, - "openDebugPane": false, - "engineSettings": {} - }, - "restartServerOnConfigChange": false, - "typing": { - "continueCommentsOnNewline": true, - "autoClosingAngleBrackets": { - "enable": false - } - }, - "diagnostics": { - "previewRustcOutput": false, - "useRustcErrorCode": false, - "disabled": [], - "enable": true, - "experimental": { - "enable": false - }, - "remapPrefix": {}, - "warningsAsHint": [], - "warningsAsInfo": [] - }, - "discoverProjectRunner": null, - "showUnlinkedFileNotification": true, - "showDependenciesExplorer": true, - "assist": { - "emitMustUse": false, - "expressionFillDefault": "todo" - }, - "cachePriming": { - "enable": true, - "numThreads": 0 - }, - "cargo": { - "autoreload": true, - "buildScripts": { - "enable": true, - "invocationLocation": "workspace", - "invocationStrategy": "per_workspace", - "overrideCommand": null, - "useRustcWrapper": true - }, - "cfgs": {}, - "extraArgs": [], - "extraEnv": {}, - "features": [], - "noDefaultFeatures": false, - "sysroot": "discover", - "sysrootSrc": null, - "target": null, - "unsetTest": [ - "core" - ] - }, - "checkOnSave": true, - "check": { - "allTargets": true, - "command": "check", - "extraArgs": [], - "extraEnv": {}, - "features": null, - "ignore": [], - "invocationLocation": "workspace", - "invocationStrategy": "per_workspace", - "noDefaultFeatures": null, - "overrideCommand": null, - "targets": null - }, - "completion": { - "autoimport": { - "enable": true - }, - "autoself": { - "enable": true - }, - "callable": { - "snippets": "fill_arguments" - }, - "fullFunctionSignatures": { - "enable": false - }, - "limit": null, - "postfix": { - "enable": true - }, - "privateEditable": { - "enable": false - }, - "snippets": { - "custom": { - "Arc::new": { - "postfix": "arc", - "body": "Arc::new(${receiver})", - "requires": "std::sync::Arc", - "description": "Put the expression into an `Arc`", - "scope": "expr" - }, - "Rc::new": { - "postfix": "rc", - "body": "Rc::new(${receiver})", - "requires": "std::rc::Rc", - "description": "Put the expression into an `Rc`", - "scope": "expr" - }, - "Box::pin": { - "postfix": "pinbox", - "body": "Box::pin(${receiver})", - "requires": "std::boxed::Box", - "description": "Put the expression into a pinned `Box`", - "scope": "expr" - }, - "Ok": { - "postfix": "ok", - "body": "Ok(${receiver})", - "description": "Wrap the expression in a `Result::Ok`", - "scope": "expr" - }, - "Err": { - "postfix": "err", - "body": "Err(${receiver})", - "description": "Wrap the expression in a `Result::Err`", - "scope": "expr" - }, - "Some": { - "postfix": "some", - "body": "Some(${receiver})", - "description": "Wrap the expression in an `Option::Some`", - "scope": "expr" - } - } - } - }, - "files": { - "excludeDirs": [], - "watcher": "client" - }, - "highlightRelated": { - "breakPoints": { - "enable": true - }, - "closureCaptures": { - "enable": true - }, - "exitPoints": { - "enable": true - }, - "references": { - "enable": true - }, - "yieldPoints": { - "enable": true - } - }, - "hover": { - "actions": { - "debug": { - "enable": true - }, - "enable": true, - "gotoTypeDef": { - "enable": true - }, - "implementations": { - "enable": true - }, - "references": { - "enable": false - }, - "run": { - "enable": true - } - }, - "documentation": { - "enable": true, - "keywords": { - "enable": true - } - }, - "links": { - "enable": true - }, - "memoryLayout": { - "alignment": "hexadecimal", - "enable": true, - "niches": false, - "offset": "hexadecimal", - "size": "both" - } - }, - "imports": { - "granularity": { - "enforce": false, - "group": "crate" - }, - "group": { - "enable": true - }, - "merge": { - "glob": true - }, - "preferNoStd": false, - "preferPrelude": false, - "prefix": "plain" - }, - "inlayHints": { - "bindingModeHints": { - "enable": false - }, - "chainingHints": { - "enable": true - }, - "closingBraceHints": { - "enable": true, - "minLines": 25 - }, - "closureCaptureHints": { - "enable": false - }, - "closureReturnTypeHints": { - "enable": "never" - }, - "closureStyle": "impl_fn", - "discriminantHints": { - "enable": "never" - }, - "expressionAdjustmentHints": { - "enable": "never", - "hideOutsideUnsafe": false, - "mode": "prefix" - }, - "lifetimeElisionHints": { - "enable": "never", - "useParameterNames": false - }, - "maxLength": 25, - "parameterHints": { - "enable": true - }, - "reborrowHints": { - "enable": "never" - }, - "renderColons": true, - "typeHints": { - "enable": true, - "hideClosureInitialization": false, - "hideNamedConstructor": false - } - }, - "interpret": { - "tests": false - }, - "joinLines": { - "joinAssignments": true, - "joinElseIf": true, - "removeTrailingComma": true, - "unwrapTrivialBlock": true - }, - "lens": { - "debug": { - "enable": true - }, - "enable": true, - "forceCustomCommands": true, - "implementations": { - "enable": true - }, - "location": "above_name", - "references": { - "adt": { - "enable": false - }, - "enumVariant": { - "enable": false - }, - "method": { - "enable": false - }, - "trait": { - "enable": false - } - }, - "run": { - "enable": true - } - }, - "linkedProjects": [], - "lru": { - "capacity": null, - "query": { - "capacities": {} - } - }, - "notifications": { - "cargoTomlNotFound": true - }, - "numThreads": null, - "procMacro": { - "attributes": { - "enable": true - }, - "enable": true, - "ignored": {}, - "server": null - }, - "references": { - "excludeImports": false - }, - "rust": { - "analyzerTargetDir": null - }, - "rustc": { - "source": null - }, - "rustfmt": { - "extraArgs": [], - "overrideCommand": null, - "rangeFormatting": { - "enable": false - } - }, - "semanticHighlighting": { - "doc": { - "comment": { - "inject": { - "enable": true - } - } - }, - "nonStandardTokens": true, - "operator": { - "enable": true, - "specialization": { - "enable": false - } - }, - "punctuation": { - "enable": false, - "separate": { - "macro": { - "bang": false - } - }, - "specialization": { - "enable": false - } - }, - "strings": { - "enable": true - } - }, - "signatureInfo": { - "detail": "full", - "documentation": { - "enable": true - } - }, - "workspace": { - "symbol": { - "search": { - "kind": "only_types", - "limit": 128, - "scope": "workspace" - } - } - } - }, - "trace": "verbose", - "workspaceFolders": [ - { - "uri": "$uri", - "name": "$name" - } - ] -} \ No newline at end of file diff --git a/src/solidlsp/language_servers/rust_analyzer/runtime_dependencies.json b/src/solidlsp/language_servers/rust_analyzer/runtime_dependencies.json deleted file mode 100644 index 517d8b87..00000000 --- a/src/solidlsp/language_servers/rust_analyzer/runtime_dependencies.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "_description": "Used to download the runtime dependencies for running RustAnalyzer. Obtained from https://github.com/rust-lang/rust-analyzer/releases", - "runtimeDependencies": [ - { - "id": "RustAnalyzer", - "description": "RustAnalyzer for Linux (x64)", - "url": "https://github.com/rust-lang/rust-analyzer/releases/download/2023-10-09/rust-analyzer-aarch64-apple-darwin.gz", - "platformId": "osx-arm64", - "archiveType": "gz", - "binaryName": "rust_analyzer" - }, - { - "id": "RustAnalyzer", - "description": "RustAnalyzer for Linux (x64)", - "url": "https://github.com/rust-lang/rust-analyzer/releases/download/2023-10-09/rust-analyzer-x86_64-unknown-linux-gnu.gz", - "platformId": "linux-x64", - "archiveType": "gz", - "binaryName": "rust_analyzer" - }, - { - "id": "RustAnalyzer", - "description": "RustAnalyzer for Windows (x64)", - "url": "https://github.com/rust-lang/rust-analyzer/releases/download/2023-10-09/rust-analyzer-x86_64-pc-windows-msvc.zip", - "platformId": "win-x64", - "archiveType": "zip", - "binaryName": "rust-analyzer.exe" - } - ] -} \ No newline at end of file diff --git a/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py b/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py deleted file mode 100644 index 1f951e5c..00000000 --- a/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py +++ /dev/null @@ -1,166 +0,0 @@ -""" -Provides Rust specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Rust. -""" - -import json -import logging -import os -import pathlib -import stat -import threading - -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.lsp_protocol_handler.lsp_types import InitializeParams -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo - - -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): - """ - 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) - super().__init__( - config, - logger, - repository_root_path, - ProcessLaunchInfo(cmd=rustanalyzer_executable_path, cwd=repository_root_path), - "rust", - ) - 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() - - @override - def is_ignored_dirname(self, dirname: str) -> bool: - return super().is_ignored_dirname(dirname) or dirname in ["target"] - - def setup_runtime_dependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> str: - """ - Setup runtime dependencies for rust_analyzer. - """ - platform_id = PlatformUtils.get_platform_id() - - with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), encoding="utf-8") as f: - d = json.load(f) - del d["_description"] - - # assert platform_id.value in [ - # "linux-x64", - # "win-x64", - # ], "Only linux-x64 and win-x64 platform is supported for in multilspy at the moment" - - runtime_dependencies = d["runtimeDependencies"] - runtime_dependencies = [dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value] - assert len(runtime_dependencies) == 1 - dependency = runtime_dependencies[0] - - rustanalyzer_ls_dir = os.path.join(os.path.dirname(__file__), "static", "RustAnalyzer") - rustanalyzer_executable_path = os.path.join(rustanalyzer_ls_dir, dependency["binaryName"]) - if not os.path.exists(rustanalyzer_ls_dir): - os.makedirs(rustanalyzer_ls_dir) - if dependency["archiveType"] == "gz": - FileUtils.download_and_extract_archive(logger, dependency["url"], rustanalyzer_executable_path, dependency["archiveType"]) - else: - FileUtils.download_and_extract_archive(logger, dependency["url"], rustanalyzer_ls_dir, dependency["archiveType"]) - assert os.path.exists(rustanalyzer_executable_path) - os.chmod(rustanalyzer_executable_path, stat.S_IEXEC) - - return rustanalyzer_executable_path - - def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: - """ - Returns the initialize params for the Rust Analyzer Language Server. - """ - with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), encoding="utf-8") as f: - d = json.load(f) - - 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 - - def _start_server(self): - """ - Starts the Rust Analyzer Language Server - """ - - def register_capability_handler(params): - assert "registrations" in params - for registration in params["registrations"]: - if registration["method"] == "workspace/executeCommand": - self.initialize_searcher_command_available.set() - self.resolve_main_method_available.set() - return - - def lang_status_handler(params): - # TODO: Should we wait for - # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}} - # Before proceeding? - if params["type"] == "ServiceReady" and params["message"] == "ServiceReady": - self.service_ready_event.set() - - def execute_client_command_handler(params): - return [] - - def do_nothing(params): - return - - def check_experimental_status(params): - if params["quiescent"] == True: - self.server_ready.set() - - def window_log_message(msg): - self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) - - self.server.on_request("client/registerCapability", register_capability_handler) - self.server.on_notification("language/status", lang_status_handler) - self.server.on_notification("window/logMessage", window_log_message) - self.server.on_request("workspace/executeClientCommand", execute_client_command_handler) - self.server.on_notification("$/progress", do_nothing) - self.server.on_notification("textDocument/publishDiagnostics", do_nothing) - self.server.on_notification("language/actionableNotification", do_nothing) - self.server.on_notification("experimental/serverStatus", check_experimental_status) - - self.logger.log("Starting RustAnalyzer server process", logging.INFO) - self.server.start() - initialize_params = self._get_initialize_params(self.repository_root_path) - - self.logger.log( - "Sending initialize request from LSP client to LSP server and awaiting response", - logging.INFO, - ) - init_response = self.server.send.initialize(initialize_params) - assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 - assert "completionProvider" in init_response["capabilities"] - assert init_response["capabilities"]["completionProvider"] == { - "resolveProvider": True, - "triggerCharacters": [":", ".", "'", "("], - "completionItem": {"labelDetailsSupport": True}, - } - self.server.notify.initialized({}) - self.completions_available.set() - - self.server_ready.wait() diff --git a/src/solidlsp/language_servers/solargraph/solargraph.py b/src/solidlsp/language_servers/solargraph.py similarity index 82% rename from src/solidlsp/language_servers/solargraph/solargraph.py rename to src/solidlsp/language_servers/solargraph.py index 171cc888..8034ad80 100644 --- a/src/solidlsp/language_servers/solargraph/solargraph.py +++ b/src/solidlsp/language_servers/solargraph.py @@ -31,7 +31,7 @@ class Solargraph(SolidLanguageServer): Creates a Solargraph instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - solargraph_executable_path = self.setup_runtime_dependencies(logger, config, repository_root_path) + solargraph_executable_path = self._setup_runtime_dependencies(logger, config, repository_root_path) super().__init__( config, logger, @@ -48,15 +48,21 @@ class Solargraph(SolidLanguageServer): def is_ignored_dirname(self, dirname: str) -> bool: return super().is_ignored_dirname(dirname) or dirname in ["vendor"] - def setup_runtime_dependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig, repository_root_path: str) -> str: + @staticmethod + def _setup_runtime_dependencies(logger: LanguageServerLogger, config: LanguageServerConfig, repository_root_path: str) -> str: """ - Setup runtime dependencies for Solargraph. + Setup runtime dependencies for Solargraph and return the command to start the server. """ - with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), encoding="utf-8") as f: - d = json.load(f) - del d["_description"] + runtime_dependencies = [ + { + "url": "https://rubygems.org/downloads/solargraph-0.51.1.gem", + "installCommand": "gem install solargraph -v 0.51.1", + "binaryName": "solargraph", + "archiveType": "gem", + } + ] - dependency = d["runtimeDependencies"][0] + dependency = runtime_dependencies[0] # Check if Ruby is installed try: @@ -93,29 +99,26 @@ class Solargraph(SolidLanguageServer): except subprocess.CalledProcessError as e: raise RuntimeError(f"Failed to check or install Solargraph. {e.stderr}") from e - 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 Solargraph Language Server. """ - with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), encoding="utf-8") as f: - d = json.load(f) - - 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 + root_uri = pathlib.Path(repository_absolute_path).as_uri() + initialize_params = { + "capabilities": {}, + "trace": "verbose", + "processId": os.getpid(), + "rootPath": repository_absolute_path, + "rootUri": pathlib.Path(repository_absolute_path).as_uri(), + "workspaceFolders": [ + { + "uri": root_uri, + "name": os.path.basename(repository_absolute_path), + } + ], + } + return initialize_params def _start_server(self): """ diff --git a/src/solidlsp/language_servers/solargraph/initialize_params.json b/src/solidlsp/language_servers/solargraph/initialize_params.json deleted file mode 100644 index 2cbb41f8..00000000 --- a/src/solidlsp/language_servers/solargraph/initialize_params.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "_description": "This file contains the initialization parameters for the Solargraph Language Server.", - "processId": "$processId", - "rootPath": "$rootPath", - "rootUri": "$rootUri", - "capabilities": { - }, - "trace": "verbose", - "workspaceFolders": [ - { - "uri": "$uri", - "name": "$name" - } - ] -} diff --git a/src/solidlsp/language_servers/solargraph/runtime_dependencies.json b/src/solidlsp/language_servers/solargraph/runtime_dependencies.json deleted file mode 100644 index 38cc2372..00000000 --- a/src/solidlsp/language_servers/solargraph/runtime_dependencies.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "_description": "This file contains URLs and other metadata required for downloading and installing the Solargraph language server.", - "runtimeDependencies": [ - { - "url": "https://rubygems.org/downloads/solargraph-0.51.1.gem", - "installCommand": "gem install solargraph -v 0.51.1", - "binaryName": "solargraph", - "archiveType": "gem" - } - ] -} diff --git a/src/solidlsp/language_servers/terraform_ls.py b/src/solidlsp/language_servers/terraform_ls.py new file mode 100644 index 00000000..5a230a23 --- /dev/null +++ b/src/solidlsp/language_servers/terraform_ls.py @@ -0,0 +1,203 @@ +import logging +import os +import shutil +import stat +import threading + +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 PathUtils, PlatformUtils +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo + +from .common import RuntimeDependency, RuntimeDependencyCollection + + +class TerraformLS(SolidLanguageServer): + """ + Provides Terraform specific instantiation of the LanguageServer class using terraform-ls. + """ + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + return super().is_ignored_dirname(dirname) or dirname in [".terraform", "terraform.tfstate.d"] + + @staticmethod + def _ensure_tf_command_available(logger: LanguageServerLogger): + logger.log("Starting terraform version detection...", logging.DEBUG) + + # 1. Try to find terraform using shutil.which + terraform_cmd = shutil.which("terraform") + if terraform_cmd is not None: + logger.log(f"Found terraform via shutil.which: {terraform_cmd}", logging.DEBUG) + return + + # TODO: is this needed? + # 2. Fallback to TERRAFORM_CLI_PATH (set by hashicorp/setup-terraform action) + if not terraform_cmd: + terraform_cli_path = os.environ.get("TERRAFORM_CLI_PATH") + if terraform_cli_path: + logger.log(f"Trying TERRAFORM_CLI_PATH: {terraform_cli_path}", logging.DEBUG) + # TODO: use binary name from runtime dependencies if we keep this code + if os.name == "nt": + terraform_binary = os.path.join(terraform_cli_path, "terraform.exe") + else: + terraform_binary = os.path.join(terraform_cli_path, "terraform") + if os.path.exists(terraform_binary): + terraform_cmd = terraform_binary + logger.log(f"Found terraform via TERRAFORM_CLI_PATH: {terraform_cmd}", logging.DEBUG) + return + + raise RuntimeError( + "Terraform executable not found, please ensure Terraform is installed." + "See https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli for instructions." + ) + + @classmethod + def _setup_runtime_dependencies(cls, logger: LanguageServerLogger) -> str: + """ + Setup runtime dependencies for terraform-ls. + Downloads and installs terraform-ls if not already present. + """ + cls._ensure_tf_command_available(logger) + platform_id = PlatformUtils.get_platform_id() + deps = RuntimeDependencyCollection( + [ + RuntimeDependency( + id="TerraformLS", + description="terraform-ls for macOS (ARM64)", + url="https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_darwin_arm64.zip", + platform_id="osx-arm64", + archive_type="zip", + binary_name="terraform-ls", + ), + RuntimeDependency( + id="TerraformLS", + description="terraform-ls for macOS (x64)", + url="https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_darwin_amd64.zip", + platform_id="osx-x64", + archive_type="zip", + binary_name="terraform-ls", + ), + RuntimeDependency( + id="TerraformLS", + description="terraform-ls for Linux (x64)", + url="https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_linux_amd64.zip", + platform_id="linux-x64", + archive_type="zip", + binary_name="terraform-ls", + ), + RuntimeDependency( + id="TerraformLS", + description="terraform-ls for Windows (x64)", + url="https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_windows_amd64.zip", + platform_id="win-x64", + archive_type="zip", + binary_name="terraform-ls.exe", + ), + ] + ) + dependency = deps.single_for_current_platform() + + terraform_ls_executable_path = deps.binary_path(cls.ls_resources_dir()) + 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()) + + assert os.path.exists(terraform_ls_executable_path), f"terraform-ls executable not found at {terraform_ls_executable_path}" + + # Make the executable file executable on Unix-like systems + if platform_id.value != "win-x64": + os.chmod(terraform_ls_executable_path, stat.S_IEXEC | stat.S_IREAD) + + return terraform_ls_executable_path + + def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): + """ + 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) + + super().__init__( + config, + logger, + repository_root_path, + ProcessLaunchInfo(cmd=f"{terraform_ls_executable_path} serve", cwd=repository_root_path), + "terraform", + ) + self.server_ready = threading.Event() + self.request_id = 0 + + @staticmethod + def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: + """ + Returns the initialize params for the Terraform Language Server. + """ + root_uri = PathUtils.path_to_uri(repository_absolute_path) + return { + "processId": os.getpid(), + "locale": "en", + "rootPath": repository_absolute_path, + "rootUri": root_uri, + "capabilities": { + "textDocument": { + "synchronization": {"didSave": True, "dynamicRegistration": True}, + "completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": True}}, + "definition": {"dynamicRegistration": True}, + "documentSymbol": { + "dynamicRegistration": True, + "hierarchicalDocumentSymbolSupport": True, + "symbolKind": {"valueSet": list(range(1, 27))}, + }, + }, + "workspace": {"workspaceFolders": True, "didChangeConfiguration": {"dynamicRegistration": True}}, + }, + "workspaceFolders": [ + { + "name": os.path.basename(repository_absolute_path), + "uri": root_uri, + } + ], + } + + def _start_server(self): + """Start terraform-ls server process""" + + def register_capability_handler(params): + return + + def window_log_message(msg): + self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + def do_nothing(params): + return + + self.server.on_request("client/registerCapability", register_capability_handler) + self.server.on_notification("window/logMessage", window_log_message) + self.server.on_notification("$/progress", do_nothing) + self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + + self.logger.log("Starting terraform-ls server process", logging.INFO) + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) + + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) + + # Verify server capabilities + assert "textDocumentSync" in init_response["capabilities"] + assert "completionProvider" in init_response["capabilities"] + assert "definitionProvider" in init_response["capabilities"] + + self.server.notify.initialized({}) + self.completions_available.set() + + # terraform-ls server is typically ready immediately after initialization + self.server_ready.set() + self.server_ready.wait() diff --git a/src/solidlsp/language_servers/gopls/initialize_params.json b/src/solidlsp/language_servers/terraform_ls/initialize_params.json similarity index 100% rename from src/solidlsp/language_servers/gopls/initialize_params.json rename to src/solidlsp/language_servers/terraform_ls/initialize_params.json diff --git a/src/solidlsp/language_servers/terraform_ls/runtime_dependencies.json b/src/solidlsp/language_servers/terraform_ls/runtime_dependencies.json new file mode 100644 index 00000000..873e7ecf --- /dev/null +++ b/src/solidlsp/language_servers/terraform_ls/runtime_dependencies.json @@ -0,0 +1,37 @@ +{ + "_description": "Used to download the runtime dependencies for running terraform-ls. Obtained from https://releases.hashicorp.com/terraform-ls/", + "runtimeDependencies": [ + { + "id": "TerraformLS", + "description": "terraform-ls for macOS (ARM64)", + "url": "https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_darwin_arm64.zip", + "platformId": "osx-arm64", + "archiveType": "zip", + "binaryName": "terraform-ls" + }, + { + "id": "TerraformLS", + "description": "terraform-ls for macOS (x64)", + "url": "https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_darwin_amd64.zip", + "platformId": "osx-x64", + "archiveType": "zip", + "binaryName": "terraform-ls" + }, + { + "id": "TerraformLS", + "description": "terraform-ls for Linux (x64)", + "url": "https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_linux_amd64.zip", + "platformId": "linux-x64", + "archiveType": "zip", + "binaryName": "terraform-ls" + }, + { + "id": "TerraformLS", + "description": "terraform-ls for Windows (x64)", + "url": "https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_windows_amd64.zip", + "platformId": "win-x64", + "archiveType": "zip", + "binaryName": "terraform-ls.exe" + } + ] +} diff --git a/src/solidlsp/language_servers/terraform_ls/terraform_ls.py b/src/solidlsp/language_servers/terraform_ls/terraform_ls.py new file mode 100644 index 00000000..62f31cb5 --- /dev/null +++ b/src/solidlsp/language_servers/terraform_ls/terraform_ls.py @@ -0,0 +1,194 @@ +import json +import logging +import os +import pathlib +import shutil +import stat +import subprocess +import threading + +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.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo + + +class TerraformLS(SolidLanguageServer): + """ + Provides Terraform specific instantiation of the LanguageServer class using terraform-ls. + """ + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + return super().is_ignored_dirname(dirname) or dirname in [".terraform", "terraform.tfstate.d"] + + @staticmethod + def _get_terraform_version(logger: LanguageServerLogger) -> str: + logger.log("Starting terraform version detection...", logging.DEBUG) + + # 1. Try to find terraform using shutil.which + terraform_cmd = shutil.which("terraform") + if terraform_cmd is not None: + logger.log(f"Found terraform via shutil.which: {terraform_cmd}", logging.DEBUG) + + # 2. Fallback to TERRAFORM_CLI_PATH (set by hashicorp/setup-terraform action) + if not terraform_cmd: + terraform_cli_path = os.environ.get("TERRAFORM_CLI_PATH") + if terraform_cli_path: + logger.log(f"Trying TERRAFORM_CLI_PATH: {terraform_cli_path}", logging.DEBUG) + if os.name == "nt": + terraform_binary = os.path.join(terraform_cli_path, "terraform.exe") + else: + terraform_binary = os.path.join(terraform_cli_path, "terraform") + if os.path.exists(terraform_binary): + terraform_cmd = terraform_binary + logger.log(f"Found terraform via TERRAFORM_CLI_PATH: {terraform_cmd}", logging.DEBUG) + + if not terraform_cmd: + raise RuntimeError("Terraform executable not found. Please ensure Terraform is installed and accessible in your system's PATH.") + + logger.log(f"Attempting to run: {terraform_cmd} version (with 15s timeout)", logging.DEBUG) + result = subprocess.run( + [terraform_cmd, "version"], + capture_output=True, + text=True, + check=False, + timeout=15, # CRITICAL: 15 second timeout to prevent hangs + ) + if result.returncode == 0: + logger.log("terraform version command succeeded", logging.DEBUG) + return result.stdout.strip() + else: + raise RuntimeError(f"terraform version command failed with return code {result.returncode}: {result.stderr}") + + # Note: needs to remain static because it's called before init is complete + @staticmethod + def _setup_runtime_dependencies(logger: LanguageServerLogger) -> str: + """ + Setup runtime dependencies for terraform-ls. + Downloads and installs terraform-ls if not already present. + """ + # First check if Terraform is available + terraform_version = TerraformLS._get_terraform_version(logger) + if not terraform_version: + raise RuntimeError( + "Terraform executable not found or failed to execute. " + "Please ensure Terraform is installed and accessible in your system's PATH.\n" + "If it's installed, check for permission issues or corrupted installation.\n" + "Download from https://www.terraform.io/downloads" + ) + + platform_id = PlatformUtils.get_platform_id() + + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), encoding="utf-8") as f: + d = json.load(f) + del d["_description"] + + runtime_dependencies = d["runtimeDependencies"] + runtime_dependencies = [dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value] + assert ( + len(runtime_dependencies) == 1 + ), f"Expected exactly one runtime dependency for platform {platform_id.value}, found {len(runtime_dependencies)}" + dependency = runtime_dependencies[0] + + terraform_ls_dir = os.path.join(os.path.dirname(__file__), "static", "TerraformLS") + terraform_ls_executable_path = os.path.join(terraform_ls_dir, dependency["binaryName"]) + + if not os.path.exists(terraform_ls_dir): + os.makedirs(terraform_ls_dir) + + if not os.path.exists(terraform_ls_executable_path): + logger.log(f"Downloading terraform-ls from {dependency['url']}", logging.INFO) + FileUtils.download_and_extract_archive(logger, dependency["url"], terraform_ls_dir, dependency["archiveType"]) + + assert os.path.exists(terraform_ls_executable_path), f"terraform-ls executable not found at {terraform_ls_executable_path}" + + # Make the executable file executable on Unix-like systems + if platform_id.value != "win-x64": + os.chmod(terraform_ls_executable_path, stat.S_IEXEC | stat.S_IREAD) + + return terraform_ls_executable_path + + def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): + """ + 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) + + super().__init__( + config, + logger, + repository_root_path, + ProcessLaunchInfo(cmd=f"{terraform_ls_executable_path} serve", cwd=repository_root_path), + "terraform", + ) + self.server_ready = threading.Event() + self.request_id = 0 + + @staticmethod + def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: + """ + Returns the initialize params for the Terraform Language Server. + """ + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), encoding="utf-8") as f: + d = json.load(f) + + 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 + + def _start_server(self): + """Start terraform-ls server process""" + + def register_capability_handler(params): + return + + def window_log_message(msg): + self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + def do_nothing(params): + return + + self.server.on_request("client/registerCapability", register_capability_handler) + self.server.on_notification("window/logMessage", window_log_message) + self.server.on_notification("$/progress", do_nothing) + self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + + self.logger.log("Starting terraform-ls server process", logging.INFO) + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) + + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) + + # Verify server capabilities + assert "textDocumentSync" in init_response["capabilities"] + assert "completionProvider" in init_response["capabilities"] + assert "definitionProvider" in init_response["capabilities"] + + self.server.notify.initialized({}) + self.completions_available.set() + + # terraform-ls server is typically ready immediately after initialization + self.server_ready.set() + self.server_ready.wait() diff --git a/src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py b/src/solidlsp/language_servers/typescript_language_server.py similarity index 73% rename from src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py rename to src/solidlsp/language_servers/typescript_language_server.py index f2204065..04ce473c 100644 --- a/src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py +++ b/src/solidlsp/language_servers/typescript_language_server.py @@ -2,12 +2,10 @@ Provides TypeScript specific instantiation of the LanguageServer class. Contains various configurations and settings specific to TypeScript. """ -import json import logging import os import pathlib import shutil -import subprocess import threading from time import sleep @@ -20,6 +18,8 @@ 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 .common import RuntimeDependency, RuntimeDependencyCollection + # Platform-specific imports if os.name != "nt": # Unix-like systems import pwd @@ -33,7 +33,7 @@ else: # Conditionally import pwd module (Unix-only) if not PlatformUtils.get_platform_id().value.startswith("win"): - import pwd + pass class TypeScriptLanguageServer(SolidLanguageServer): @@ -45,7 +45,7 @@ class TypeScriptLanguageServer(SolidLanguageServer): """ Creates a TypeScriptLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - ts_lsp_executable_path = self.setup_runtime_dependencies(logger, config) + ts_lsp_executable_path = self._setup_runtime_dependencies(logger, config) super().__init__( config, logger, @@ -65,9 +65,10 @@ class TypeScriptLanguageServer(SolidLanguageServer): "coverage", ] - def setup_runtime_dependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> str: + @classmethod + def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> str: """ - Setup runtime dependencies for TypeScript Language Server. + Setup runtime dependencies for TypeScript Language Server and return the command to start the server. """ platform_id = PlatformUtils.get_platform_id() @@ -82,12 +83,23 @@ class TypeScriptLanguageServer(SolidLanguageServer): ] assert platform_id in valid_platforms, f"Platform {platform_id} is not supported for multilspy javascript/typescript at the moment" - with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json")) as f: - d = json.load(f) - del d["_description"] - - runtime_dependencies = d.get("runtimeDependencies", []) - tsserver_ls_dir = os.path.join(os.path.dirname(__file__), "static", "ts-lsp") + deps = RuntimeDependencyCollection( + [ + RuntimeDependency( + id="typescript", + description="typescript package", + command="npm install --prefix ./ typescript@5.5.4", + platform_id="any", + ), + RuntimeDependency( + id="typescript-language-server", + description="typescript-language-server package", + command="npm install --prefix ./ typescript-language-server@4.3.3", + platform_id="any", + ), + ] + ) + tsserver_ls_dir = os.path.join(cls.ls_resources_dir(), "ts-lsp") tsserver_executable_path = os.path.join(tsserver_ls_dir, "typescript-language-server") # Verify both node and npm are installed @@ -99,29 +111,7 @@ class TypeScriptLanguageServer(SolidLanguageServer): # Install typescript and typescript-language-server if not already installed if not os.path.exists(tsserver_ls_dir): os.makedirs(tsserver_ls_dir, exist_ok=True) - for dependency in runtime_dependencies: - # Windows doesn't support the 'user' parameter and doesn't have pwd module - if PlatformUtils.get_platform_id().value.startswith("win"): - subprocess.run( - dependency["command"], - shell=True, - check=True, - cwd=tsserver_ls_dir, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - else: - # On Unix-like systems, run as non-root user - user = pwd.getpwuid(os.getuid()).pw_name - subprocess.run( - dependency["command"], - shell=True, - check=True, - user=user, - cwd=tsserver_ls_dir, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) + deps.install(logger, tsserver_ls_dir) tsserver_executable_path = os.path.join(tsserver_ls_dir, "node_modules", ".bin", "typescript-language-server") @@ -130,29 +120,46 @@ class TypeScriptLanguageServer(SolidLanguageServer): ), "typescript-language-server executable not found. Please install typescript-language-server and try again." return f"{tsserver_executable_path} --stdio" - 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 TypeScript Language Server. """ - with open(os.path.join(os.path.dirname(__file__), "initialize_params.json")) as f: - d = json.load(f) - - 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 + root_uri = pathlib.Path(repository_absolute_path).as_uri() + initialize_params = { + "locale": "en", + "capabilities": { + "textDocument": { + "synchronization": {"didSave": True, "dynamicRegistration": True}, + "completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": True}}, + "definition": {"dynamicRegistration": True}, + "references": {"dynamicRegistration": True}, + "documentSymbol": { + "dynamicRegistration": True, + "hierarchicalDocumentSymbolSupport": True, + "symbolKind": {"valueSet": list(range(1, 27))}, + }, + "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]}, + "signatureHelp": {"dynamicRegistration": True}, + "codeAction": {"dynamicRegistration": True}, + }, + "workspace": { + "workspaceFolders": True, + "didChangeConfiguration": {"dynamicRegistration": True}, + "symbol": {"dynamicRegistration": True}, + }, + }, + "processId": os.getpid(), + "rootPath": repository_absolute_path, + "rootUri": root_uri, + "workspaceFolders": [ + { + "uri": root_uri, + "name": os.path.basename(repository_absolute_path), + } + ], + } + return initialize_params def _start_server(self): """ diff --git a/src/solidlsp/language_servers/typescript_language_server/initialize_params.json b/src/solidlsp/language_servers/typescript_language_server/initialize_params.json deleted file mode 100644 index 6209ba47..00000000 --- a/src/solidlsp/language_servers/typescript_language_server/initialize_params.json +++ /dev/null @@ -1,906 +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()", - "clientInfo": { - "name": "Visual Studio Code - Insiders", - "version": "1.82.0-insider" - }, - "locale": "en", - "rootPath": "$rootPath", - "rootUri": "$rootUri", - "capabilities": { - "workspace": { - "applyEdit": true, - "workspaceEdit": { - "documentChanges": true, - "resourceOperations": [ - "create", - "rename", - "delete" - ], - "failureHandling": "textOnlyTransactional", - "normalizesLineEndings": true, - "changeAnnotationSupport": { - "groupsOnLabel": true - } - }, - "configuration": true, - "didChangeWatchedFiles": { - "dynamicRegistration": true, - "relativePatternSupport": true - }, - "symbol": { - "dynamicRegistration": 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 - ] - }, - "tagSupport": { - "valueSet": [ - 1 - ] - }, - "resolveSupport": { - "properties": [ - "location.range" - ] - } - }, - "codeLens": { - "refreshSupport": true - }, - "executeCommand": { - "dynamicRegistration": true - }, - "didChangeConfiguration": { - "dynamicRegistration": true - }, - "workspaceFolders": true, - "semanticTokens": { - "refreshSupport": true - }, - "fileOperations": { - "dynamicRegistration": true, - "didCreate": true, - "didRename": true, - "didDelete": true, - "willCreate": true, - "willRename": true, - "willDelete": true - }, - "inlineValue": { - "refreshSupport": true - }, - "inlayHint": { - "refreshSupport": true - }, - "diagnostics": { - "refreshSupport": true - } - }, - "textDocument": { - "publishDiagnostics": { - "relatedInformation": true, - "versionSupport": false, - "tagSupport": { - "valueSet": [ - 1, - 2 - ] - }, - "codeDescriptionSupport": true, - "dataSupport": true - }, - "synchronization": { - "dynamicRegistration": true, - "willSave": true, - "willSaveWaitUntil": true, - "didSave": true - }, - "completion": { - "dynamicRegistration": true, - "contextSupport": true, - "completionItem": { - "snippetSupport": true, - "commitCharactersSupport": true, - "documentationFormat": [ - "markdown", - "plaintext" - ], - "deprecatedSupport": true, - "preselectSupport": true, - "tagSupport": { - "valueSet": [ - 1 - ] - }, - "insertReplaceSupport": true, - "resolveSupport": { - "properties": [ - "documentation", - "detail", - "additionalTextEdits" - ] - }, - "insertTextModeSupport": { - "valueSet": [ - 1, - 2 - ] - }, - "labelDetailsSupport": true - }, - "insertTextMode": 2, - "completionItemKind": { - "valueSet": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25 - ] - }, - "completionList": { - "itemDefaults": [ - "commitCharacters", - "editRange", - "insertTextFormat", - "insertTextMode" - ] - } - }, - "hover": { - "dynamicRegistration": true, - "contentFormat": [ - "markdown", - "plaintext" - ] - }, - "signatureHelp": { - "dynamicRegistration": true, - "signatureInformation": { - "documentationFormat": [ - "markdown", - "plaintext" - ], - "parameterInformation": { - "labelOffsetSupport": true - }, - "activeParameterSupport": true - }, - "contextSupport": true - }, - "definition": { - "dynamicRegistration": true, - "linkSupport": true - }, - "references": { - "dynamicRegistration": true - }, - "documentHighlight": { - "dynamicRegistration": true - }, - "documentSymbol": { - "dynamicRegistration": 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 - ] - }, - "hierarchicalDocumentSymbolSupport": true, - "tagSupport": { - "valueSet": [ - 1 - ] - }, - "labelSupport": true - }, - "codeAction": { - "dynamicRegistration": true, - "isPreferredSupport": true, - "disabledSupport": true, - "dataSupport": true, - "resolveSupport": { - "properties": [ - "edit" - ] - }, - "codeActionLiteralSupport": { - "codeActionKind": { - "valueSet": [ - "", - "quickfix", - "refactor", - "refactor.extract", - "refactor.inline", - "refactor.rewrite", - "source", - "source.organizeImports" - ] - } - }, - "honorsChangeAnnotations": false - }, - "codeLens": { - "dynamicRegistration": true - }, - "formatting": { - "dynamicRegistration": true - }, - "rangeFormatting": { - "dynamicRegistration": true - }, - "onTypeFormatting": { - "dynamicRegistration": true - }, - "rename": { - "dynamicRegistration": true, - "prepareSupport": true, - "prepareSupportDefaultBehavior": 1, - "honorsChangeAnnotations": true - }, - "documentLink": { - "dynamicRegistration": true, - "tooltipSupport": true - }, - "typeDefinition": { - "dynamicRegistration": true, - "linkSupport": true - }, - "implementation": { - "dynamicRegistration": true, - "linkSupport": true - }, - "colorProvider": { - "dynamicRegistration": true - }, - "foldingRange": { - "dynamicRegistration": true, - "rangeLimit": 5000, - "lineFoldingOnly": true, - "foldingRangeKind": { - "valueSet": [ - "comment", - "imports", - "region" - ] - }, - "foldingRange": { - "collapsedText": false - } - }, - "declaration": { - "dynamicRegistration": true, - "linkSupport": true - }, - "selectionRange": { - "dynamicRegistration": true - }, - "callHierarchy": { - "dynamicRegistration": true - }, - "semanticTokens": { - "dynamicRegistration": true, - "tokenTypes": [ - "namespace", - "type", - "class", - "enum", - "interface", - "struct", - "typeParameter", - "parameter", - "variable", - "property", - "enumMember", - "event", - "function", - "method", - "macro", - "keyword", - "modifier", - "comment", - "string", - "number", - "regexp", - "operator", - "decorator" - ], - "tokenModifiers": [ - "declaration", - "definition", - "readonly", - "static", - "deprecated", - "abstract", - "async", - "modification", - "documentation", - "defaultLibrary" - ], - "formats": [ - "relative" - ], - "requests": { - "range": true, - "full": { - "delta": true - } - }, - "multilineTokenSupport": false, - "overlappingTokenSupport": false, - "serverCancelSupport": true, - "augmentsSyntaxTokens": false - }, - "linkedEditingRange": { - "dynamicRegistration": true - }, - "typeHierarchy": { - "dynamicRegistration": true - }, - "inlineValue": { - "dynamicRegistration": true - }, - "inlayHint": { - "dynamicRegistration": true, - "resolveSupport": { - "properties": [ - "tooltip", - "textEdits", - "label.tooltip", - "label.location", - "label.command" - ] - } - }, - "diagnostic": { - "dynamicRegistration": true, - "relatedDocumentSupport": false - } - }, - "general": { - "staleRequestSupport": { - "cancel": true, - "retryOnContentModified": [ - "textDocument/semanticTokens/full", - "textDocument/semanticTokens/range", - "textDocument/semanticTokens/full/delta" - ] - }, - "regularExpressions": { - "engine": "ECMAScript", - "version": "ES2020" - }, - "markdown": { - "parser": "marked", - "version": "1.1.0", - "allowedTags": [ - "ul", - "li", - "p", - "code", - "blockquote", - "ol", - "h1", - "h2", - "h3", - "h4", - "h5", - "h6", - "hr", - "em", - "pre", - "table", - "thead", - "tbody", - "tr", - "th", - "td", - "div", - "del", - "a", - "strong", - "br", - "img", - "span" - ] - }, - "positionEncodings": [ - "utf-16" - ] - }, - "notebookDocument": { - "synchronization": { - "dynamicRegistration": true, - "executionSummarySupport": true - } - }, - "experimental": { - "snippetTextEdit": true, - "codeActionGroup": true, - "hoverActions": true, - "serverStatusNotification": true, - "colorDiagnosticOutput": true, - "openServerLogs": true, - "localDocs": true, - "commands": { - "commands": [ - "rust-analyzer.runSingle", - "rust-analyzer.debugSingle", - "rust-analyzer.showReferences", - "rust-analyzer.gotoLocation", - "editor.action.triggerParameterHints" - ] - } - } - }, - "initializationOptions": { - "cargoRunner": null, - "runnables": { - "extraEnv": null, - "problemMatcher": [ - "$rustc" - ], - "command": null, - "extraArgs": [] - }, - "statusBar": { - "clickAction": "openLogs" - }, - "server": { - "path": null, - "extraEnv": null - }, - "trace": { - "server": "verbose", - "extension": false - }, - "debug": { - "engine": "auto", - "sourceFileMap": { - "/rustc/": "${env:USERPROFILE}/.rustup/toolchains//lib/rustlib/src/rust" - }, - "openDebugPane": false, - "engineSettings": {} - }, - "restartServerOnConfigChange": false, - "typing": { - "continueCommentsOnNewline": true, - "autoClosingAngleBrackets": { - "enable": false - } - }, - "diagnostics": { - "previewRustcOutput": false, - "useRustcErrorCode": false, - "disabled": [], - "enable": true, - "experimental": { - "enable": false - }, - "remapPrefix": {}, - "warningsAsHint": [], - "warningsAsInfo": [] - }, - "discoverProjectRunner": null, - "showUnlinkedFileNotification": true, - "showDependenciesExplorer": true, - "assist": { - "emitMustUse": false, - "expressionFillDefault": "todo" - }, - "cachePriming": { - "enable": true, - "numThreads": 0 - }, - "cargo": { - "autoreload": true, - "buildScripts": { - "enable": true, - "invocationLocation": "workspace", - "invocationStrategy": "per_workspace", - "overrideCommand": null, - "useRustcWrapper": true - }, - "cfgs": {}, - "extraArgs": [], - "extraEnv": {}, - "features": [], - "noDefaultFeatures": false, - "sysroot": "discover", - "sysrootSrc": null, - "target": null, - "unsetTest": [ - "core" - ] - }, - "checkOnSave": true, - "check": { - "allTargets": true, - "command": "check", - "extraArgs": [], - "extraEnv": {}, - "features": null, - "ignore": [], - "invocationLocation": "workspace", - "invocationStrategy": "per_workspace", - "noDefaultFeatures": null, - "overrideCommand": null, - "targets": null - }, - "completion": { - "autoimport": { - "enable": true - }, - "autoself": { - "enable": true - }, - "callable": { - "snippets": "fill_arguments" - }, - "fullFunctionSignatures": { - "enable": false - }, - "limit": null, - "postfix": { - "enable": true - }, - "privateEditable": { - "enable": false - }, - "snippets": { - "custom": { - "Arc::new": { - "postfix": "arc", - "body": "Arc::new(${receiver})", - "requires": "std::sync::Arc", - "description": "Put the expression into an `Arc`", - "scope": "expr" - }, - "Rc::new": { - "postfix": "rc", - "body": "Rc::new(${receiver})", - "requires": "std::rc::Rc", - "description": "Put the expression into an `Rc`", - "scope": "expr" - }, - "Box::pin": { - "postfix": "pinbox", - "body": "Box::pin(${receiver})", - "requires": "std::boxed::Box", - "description": "Put the expression into a pinned `Box`", - "scope": "expr" - }, - "Ok": { - "postfix": "ok", - "body": "Ok(${receiver})", - "description": "Wrap the expression in a `Result::Ok`", - "scope": "expr" - }, - "Err": { - "postfix": "err", - "body": "Err(${receiver})", - "description": "Wrap the expression in a `Result::Err`", - "scope": "expr" - }, - "Some": { - "postfix": "some", - "body": "Some(${receiver})", - "description": "Wrap the expression in an `Option::Some`", - "scope": "expr" - } - } - } - }, - "files": { - "excludeDirs": [], - "watcher": "client" - }, - "highlightRelated": { - "breakPoints": { - "enable": true - }, - "closureCaptures": { - "enable": true - }, - "exitPoints": { - "enable": true - }, - "references": { - "enable": true - }, - "yieldPoints": { - "enable": true - } - }, - "hover": { - "actions": { - "debug": { - "enable": true - }, - "enable": true, - "gotoTypeDef": { - "enable": true - }, - "implementations": { - "enable": true - }, - "references": { - "enable": false - }, - "run": { - "enable": true - } - }, - "documentation": { - "enable": true, - "keywords": { - "enable": true - } - }, - "links": { - "enable": true - }, - "memoryLayout": { - "alignment": "hexadecimal", - "enable": true, - "niches": false, - "offset": "hexadecimal", - "size": "both" - } - }, - "imports": { - "granularity": { - "enforce": false, - "group": "crate" - }, - "group": { - "enable": true - }, - "merge": { - "glob": true - }, - "preferNoStd": false, - "preferPrelude": false, - "prefix": "plain" - }, - "inlayHints": { - "bindingModeHints": { - "enable": false - }, - "chainingHints": { - "enable": true - }, - "closingBraceHints": { - "enable": true, - "minLines": 25 - }, - "closureCaptureHints": { - "enable": false - }, - "closureReturnTypeHints": { - "enable": "never" - }, - "closureStyle": "impl_fn", - "discriminantHints": { - "enable": "never" - }, - "expressionAdjustmentHints": { - "enable": "never", - "hideOutsideUnsafe": false, - "mode": "prefix" - }, - "lifetimeElisionHints": { - "enable": "never", - "useParameterNames": false - }, - "maxLength": 25, - "parameterHints": { - "enable": true - }, - "reborrowHints": { - "enable": "never" - }, - "renderColons": true, - "typeHints": { - "enable": true, - "hideClosureInitialization": false, - "hideNamedConstructor": false - } - }, - "interpret": { - "tests": false - }, - "joinLines": { - "joinAssignments": true, - "joinElseIf": true, - "removeTrailingComma": true, - "unwrapTrivialBlock": true - }, - "lens": { - "debug": { - "enable": true - }, - "enable": true, - "forceCustomCommands": true, - "implementations": { - "enable": true - }, - "location": "above_name", - "references": { - "adt": { - "enable": false - }, - "enumVariant": { - "enable": false - }, - "method": { - "enable": false - }, - "trait": { - "enable": false - } - }, - "run": { - "enable": true - } - }, - "linkedProjects": [], - "lru": { - "capacity": null, - "query": { - "capacities": {} - } - }, - "notifications": { - "cargoTomlNotFound": true - }, - "numThreads": null, - "procMacro": { - "attributes": { - "enable": true - }, - "enable": true, - "ignored": {}, - "server": null - }, - "references": { - "excludeImports": false - }, - "rust": { - "analyzerTargetDir": null - }, - "rustc": { - "source": null - }, - "rustfmt": { - "extraArgs": [], - "overrideCommand": null, - "rangeFormatting": { - "enable": false - } - }, - "semanticHighlighting": { - "doc": { - "comment": { - "inject": { - "enable": true - } - } - }, - "nonStandardTokens": true, - "operator": { - "enable": true, - "specialization": { - "enable": false - } - }, - "punctuation": { - "enable": false, - "separate": { - "macro": { - "bang": false - } - }, - "specialization": { - "enable": false - } - }, - "strings": { - "enable": true - } - }, - "signatureInfo": { - "detail": "full", - "documentation": { - "enable": true - } - }, - "workspace": { - "symbol": { - "search": { - "kind": "only_types", - "limit": 128, - "scope": "workspace" - } - } - } - }, - "trace": "verbose", - "workspaceFolders": [ - { - "uri": "$uri", - "name": "$name" - } - ] -} \ No newline at end of file diff --git a/src/solidlsp/language_servers/typescript_language_server/runtime_dependencies.json b/src/solidlsp/language_servers/typescript_language_server/runtime_dependencies.json deleted file mode 100644 index 90824a7c..00000000 --- a/src/solidlsp/language_servers/typescript_language_server/runtime_dependencies.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "_description": "Used to download the runtime dependencies for running typescript-language-server. Obtained from https://github.com/typescript-language-server/typescript-language-server/releases", - "runtimeDependencies": [ - { - "id": "typescript", - "description": "typescript package for Linux, OSX, and Windows. Both x64 and arm64 are supported.", - "command": "npm install --prefix ./ typescript@5.5.4" - }, - { - "id": "typescript-language-server", - "description": "typescript-language-server package for Linux, OSX, and Windows. Both x64 and arm64 are supported.", - "command": "npm install --prefix ./ typescript-language-server@4.3.3" - } - ] -} - diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 415807ee..5a3aa485 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -88,6 +88,17 @@ class SolidLanguageServer(ABC): """ return dirname.startswith(".") + @classmethod + def ls_resources_dir(cls, 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__) + 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 @@ -107,28 +118,32 @@ class SolidLanguageServer(ABC): ls: SolidLanguageServer if config.code_language == Language.PYTHON: - from solidlsp.language_servers.pyright_language_server.pyright_server import ( + # We can also use jedi + # from solidlsp.language_servers.jedi_server import JediServer + # + # ls = JediServer(config, logger, repository_root_path) + from solidlsp.language_servers.pyright_server import ( PyrightServer, ) ls = PyrightServer(config, logger, repository_root_path) elif config.code_language == Language.JAVA: - from solidlsp.language_servers.eclipse_jdtls.eclipse_jdtls import ( + from solidlsp.language_servers.eclipse_jdtls import ( EclipseJDTLS, ) ls = EclipseJDTLS(config, logger, repository_root_path) elif config.code_language == Language.KOTLIN: - from solidlsp.language_servers.kotlin_language_server.kotlin_language_server import ( + from solidlsp.language_servers.kotlin_language_server import ( KotlinLanguageServer, ) ls = KotlinLanguageServer(config, logger, repository_root_path) elif config.code_language == Language.RUST: - from solidlsp.language_servers.rust_analyzer.rust_analyzer import ( + from solidlsp.language_servers.rust_analyzer import ( RustAnalyzer, ) @@ -140,47 +155,57 @@ class SolidLanguageServer(ABC): # from solidlsp.language_servers.omnisharp.omnisharp import OmniSharp # ls = OmniSharp(config, logger, repository_root_path) - from solidlsp.language_servers.csharp_language_server.csharp_language_server import CSharpLanguageServer + from solidlsp.language_servers.csharp_language_server import CSharpLanguageServer ls = CSharpLanguageServer(config, logger, repository_root_path) elif config.code_language == Language.TYPESCRIPT: - from solidlsp.language_servers.typescript_language_server.typescript_language_server import ( + from solidlsp.language_servers.typescript_language_server import ( TypeScriptLanguageServer, ) ls = TypeScriptLanguageServer(config, logger, repository_root_path) elif config.code_language == Language.GO: - from solidlsp.language_servers.gopls.gopls import Gopls + from solidlsp.language_servers.gopls import Gopls ls = Gopls(config, logger, repository_root_path) elif config.code_language == Language.RUBY: - from solidlsp.language_servers.solargraph.solargraph import Solargraph + from solidlsp.language_servers.solargraph import Solargraph ls = Solargraph(config, logger, repository_root_path) elif config.code_language == Language.DART: - from solidlsp.language_servers.dart_language_server.dart_language_server import DartLanguageServer + from solidlsp.language_servers.dart_language_server import DartLanguageServer ls = DartLanguageServer(config, logger, repository_root_path) elif config.code_language == Language.CPP: - from solidlsp.language_servers.clangd_language_server.clangd_language_server import ClangdLanguageServer + from solidlsp.language_servers.clangd_language_server import ClangdLanguageServer ls = ClangdLanguageServer(config, logger, repository_root_path) elif config.code_language == Language.PHP: - from solidlsp.language_servers.intelephense.intelephense import Intelephense + from solidlsp.language_servers.intelephense import Intelephense ls = Intelephense(config, logger, repository_root_path) elif config.code_language == Language.CLOJURE: - from solidlsp.language_servers.clojure_lsp.clojure_lsp import ClojureLSP + from solidlsp.language_servers.clojure_lsp import ClojureLSP ls = ClojureLSP(config, logger, repository_root_path) + elif config.code_language == Language.ELIXIR: + from solidlsp.language_servers.elixir_tools.elixir_tools import ElixirTools + + ls = ElixirTools(config, logger, repository_root_path) + + elif config.code_language == Language.TERRAFORM: + from solidlsp.language_servers.terraform_ls import TerraformLS + + ls = TerraformLS(config, logger, repository_root_path) + else: logger.log(f"Language {config.code_language} is not supported", logging.ERROR) raise LanguageServerException(f"Language {config.code_language} is not supported") @@ -663,6 +688,14 @@ class SolidLanguageServer(ABC): assert LSPConstants.RANGE in item abs_path = PathUtils.uri_to_path(item[LSPConstants.URI]) + if not Path(abs_path).is_relative_to(self.repository_root_path): + self.logger.log( + "Found a reference in a path outside the repository, probably the LS is parsing things in installed packages or in the standardlib! " + f"Path: {abs_path}. This is a bug but we currently simply skip these references.", + logging.WARNING, + ) + continue + rel_path = Path(abs_path).relative_to(self.repository_root_path) if self.is_ignored_path(str(rel_path)): self.logger.log(f"Ignoring reference in {rel_path} since it should be ignored", logging.DEBUG) diff --git a/src/solidlsp/ls_config.py b/src/solidlsp/ls_config.py index 5609ec06..e5df9011 100644 --- a/src/solidlsp/ls_config.py +++ b/src/solidlsp/ls_config.py @@ -38,6 +38,8 @@ class Language(str, Enum): CPP = "cpp" PHP = "php" CLOJURE = "clojure" + ELIXIR = "elixir" + TERRAFORM = "terraform" def __str__(self) -> str: return self.value @@ -74,6 +76,10 @@ class Language(str, Enum): return FilenameMatcher("*.php") case self.CLOJURE: return FilenameMatcher("*.clj", "*.cljs", "*.cljc", "*.edn") # codespell:ignore edn + case self.ELIXIR: + return FilenameMatcher("*.ex", "*.exs") + case self.TERRAFORM: + return FilenameMatcher("*.tf", "*.tfvars", "*.tfstate") case _: raise ValueError(f"Unhandled language: {self}") diff --git a/src/solidlsp/ls_utils.py b/src/solidlsp/ls_utils.py index 57875c04..b46491ec 100644 --- a/src/solidlsp/ls_utils.py +++ b/src/solidlsp/ls_utils.py @@ -206,10 +206,10 @@ class FileUtils: os.makedirs(os.path.dirname(tmp_file_name), exist_ok=True) FileUtils.download_file(logger, url, tmp_file_name) if archive_type in ["zip", "tar", "gztar", "bztar", "xztar"]: - assert os.path.isdir(target_path) + os.makedirs(target_path, exist_ok=True) shutil.unpack_archive(tmp_file_name, target_path, archive_type) elif archive_type == "zip.gz": - assert os.path.isdir(target_path) + os.makedirs(target_path, exist_ok=True) tmp_file_name_ungzipped = tmp_file_name + ".zip" tmp_files.append(tmp_file_name_ungzipped) with gzip.open(tmp_file_name, "rb") as f_in, open(tmp_file_name_ungzipped, "wb") as f_out: @@ -257,6 +257,7 @@ class DotnetVersion(str, Enum): V6 = "6" V7 = "7" V8 = "8" + V9 = "9" VMONO = "mono" @@ -345,15 +346,14 @@ class PlatformUtils: # Check for supported versions in order of preference (latest first) for version_cmd_output in available_version_cmd_output: + if version_cmd_output.startswith("9"): + return DotnetVersion.V9 if version_cmd_output.startswith("8"): return DotnetVersion.V8 - for version_cmd_output in available_version_cmd_output: if version_cmd_output.startswith("7"): return DotnetVersion.V7 - for version_cmd_output in available_version_cmd_output: if version_cmd_output.startswith("6"): return DotnetVersion.V6 - for version_cmd_output in available_version_cmd_output: if version_cmd_output.startswith("4"): return DotnetVersion.V4 diff --git a/test/resources/repos/csharp/serena.sln b/test/resources/repos/csharp/test_repo/serena.sln similarity index 98% rename from test/resources/repos/csharp/serena.sln rename to test/resources/repos/csharp/test_repo/serena.sln index 30e1b535..ef7a1315 100644 --- a/test/resources/repos/csharp/serena.sln +++ b/test/resources/repos/csharp/test_repo/serena.sln @@ -1,38 +1,38 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.5.2.0 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{0C88DD14-F956-CE84-757C-A364CCF449FC}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "resources", "resources", "{EF7103B4-4C75-1E6D-A485-A154A88D107A}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "repos", "repos", "{E2326EEF-E677-6A44-0935-7677816F09E7}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "csharp", "csharp", "{C21E6CE7-177A-86D9-040F-A317F18B6DBF}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestProject", "test\resources\repos\csharp\test_repo\TestProject.csproj", "{A4D04E18-760A-73F9-3303-0542F6298C84}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {A4D04E18-760A-73F9-3303-0542F6298C84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A4D04E18-760A-73F9-3303-0542F6298C84}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A4D04E18-760A-73F9-3303-0542F6298C84}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A4D04E18-760A-73F9-3303-0542F6298C84}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {EF7103B4-4C75-1E6D-A485-A154A88D107A} = {0C88DD14-F956-CE84-757C-A364CCF449FC} - {E2326EEF-E677-6A44-0935-7677816F09E7} = {EF7103B4-4C75-1E6D-A485-A154A88D107A} - {C21E6CE7-177A-86D9-040F-A317F18B6DBF} = {E2326EEF-E677-6A44-0935-7677816F09E7} - {A4D04E18-760A-73F9-3303-0542F6298C84} = {C21E6CE7-177A-86D9-040F-A317F18B6DBF} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {BDCA748E-D888-4BAF-BF24-DAC683113BFC} - EndGlobalSection -EndGlobal +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{0C88DD14-F956-CE84-757C-A364CCF449FC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "resources", "resources", "{EF7103B4-4C75-1E6D-A485-A154A88D107A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "repos", "repos", "{E2326EEF-E677-6A44-0935-7677816F09E7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "csharp", "csharp", "{C21E6CE7-177A-86D9-040F-A317F18B6DBF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestProject", "test\resources\repos\csharp\test_repo\TestProject.csproj", "{A4D04E18-760A-73F9-3303-0542F6298C84}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A4D04E18-760A-73F9-3303-0542F6298C84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A4D04E18-760A-73F9-3303-0542F6298C84}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A4D04E18-760A-73F9-3303-0542F6298C84}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A4D04E18-760A-73F9-3303-0542F6298C84}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {EF7103B4-4C75-1E6D-A485-A154A88D107A} = {0C88DD14-F956-CE84-757C-A364CCF449FC} + {E2326EEF-E677-6A44-0935-7677816F09E7} = {EF7103B4-4C75-1E6D-A485-A154A88D107A} + {C21E6CE7-177A-86D9-040F-A317F18B6DBF} = {E2326EEF-E677-6A44-0935-7677816F09E7} + {A4D04E18-760A-73F9-3303-0542F6298C84} = {C21E6CE7-177A-86D9-040F-A317F18B6DBF} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {BDCA748E-D888-4BAF-BF24-DAC683113BFC} + EndGlobalSection +EndGlobal diff --git a/test/resources/repos/elixir/test_repo/.gitignore b/test/resources/repos/elixir/test_repo/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/test/resources/repos/elixir/test_repo/lib/examples.ex b/test/resources/repos/elixir/test_repo/lib/examples.ex new file mode 100644 index 00000000..57c27283 --- /dev/null +++ b/test/resources/repos/elixir/test_repo/lib/examples.ex @@ -0,0 +1,211 @@ +defmodule TestRepo.Examples do + @moduledoc """ + Examples module demonstrating usage of models and services. + Similar to Python's examples directory, this shows how different modules work together. + """ + + alias TestRepo.Models.{User, Item} + alias TestRepo.Services.{UserService, ItemService, OrderService} + + defmodule UserManagement do + @doc """ + Creates a complete user workflow example. + """ + def run_user_example do + # Start user service + {:ok, user_service} = UserService.start_link() + + # Create users + {:ok, alice} = UserService.create_user(user_service, "1", "Alice", "alice@example.com", ["admin"]) + {:ok, bob} = UserService.create_user(user_service, "2", "Bob", "bob@example.com", ["user"]) + + # Get users + {:ok, retrieved_alice} = UserService.get_user(user_service, "1") + + # List all users + all_users = UserService.list_users(user_service) + + # Clean up + GenServer.stop(user_service) + + %{ + created_alice: alice, + created_bob: bob, + retrieved_alice: retrieved_alice, + all_users: all_users + } + end + + @doc """ + Demonstrates user role management. + """ + def manage_user_roles do + user = User.new("role_user", "Role User", "role@example.com") + + # Add roles + user_with_admin = User.add_role(user, "admin") + user_with_multiple = User.add_role(user_with_admin, "moderator") + + # Check roles + has_admin = User.has_role?(user_with_multiple, "admin") + has_guest = User.has_role?(user_with_multiple, "guest") + + %{ + original_user: user, + user_with_roles: user_with_multiple, + has_admin: has_admin, + has_guest: has_guest + } + end + end + + defmodule ShoppingExample do + @doc """ + Creates a complete shopping workflow. + """ + def run_shopping_example do + # Create user and items + user = User.new("customer1", "Customer One", "customer@example.com") + item1 = Item.new("widget1", "Super Widget", 19.99, "electronics") + item2 = Item.new("gadget1", "Cool Gadget", 29.99, "electronics") + + # Create order + order = OrderService.create_order("order1", user) + + # Add items to order + order_with_item1 = OrderService.add_item_to_order(order, item1) + order_with_items = OrderService.add_item_to_order(order_with_item1, item2) + + # Process the order + processed_order = OrderService.process_order(order_with_items) + completed_order = OrderService.complete_order(processed_order) + + %{ + user: user, + items: [item1, item2], + final_order: completed_order, + total_cost: completed_order.total + } + end + + @doc """ + Demonstrates item filtering and searching. + """ + def item_filtering_example do + # Start item service + {:ok, item_service} = ItemService.start_link() + + # Create various items + ItemService.create_item(item_service, "laptop", "Gaming Laptop", 1299.99, "electronics") + ItemService.create_item(item_service, "book", "Elixir Guide", 39.99, "books") + ItemService.create_item(item_service, "phone", "Smartphone", 699.99, "electronics") + ItemService.create_item(item_service, "novel", "Great Novel", 19.99, "books") + + # Get all items + all_items = ItemService.list_items(item_service) + + # Filter by category + electronics = ItemService.list_items(item_service, "electronics") + books = ItemService.list_items(item_service, "books") + + # Clean up + Agent.stop(item_service) + + %{ + all_items: all_items, + electronics: electronics, + books: books, + total_items: length(all_items), + electronics_count: length(electronics), + books_count: length(books) + } + end + end + + defmodule IntegrationExample do + @doc """ + Runs a complete e-commerce scenario. + """ + def run_full_scenario do + # Setup services + container = TestRepo.Services.create_service_container() + TestRepo.Services.setup_sample_data(container) + + # Get sample data + {:ok, sample_user} = UserService.get_user(container.user_service, TestRepo.Services.sample_user_id()) + {:ok, sample_item} = ItemService.get_item(container.item_service, TestRepo.Services.sample_item_id()) + + # Create additional items + {:ok, premium_item} = ItemService.create_item( + container.item_service, + "premium", + "Premium Product", + 99.99, + "premium" + ) + + # Create order with multiple items + order = OrderService.create_order("big_order", sample_user, [sample_item]) + order_with_premium = OrderService.add_item_to_order(order, premium_item) + + # Process through order lifecycle + processing_order = OrderService.process_order(order_with_premium) + final_order = OrderService.complete_order(processing_order) + + # Serialize everything for output + serialized_user = TestRepo.Services.serialize_model(sample_user) + serialized_order = TestRepo.Services.serialize_model(final_order) + + # Clean up + GenServer.stop(container.user_service) + Agent.stop(container.item_service) + + %{ + scenario: "full_ecommerce", + user: serialized_user, + order: serialized_order, + total_revenue: final_order.total, + items_sold: length(final_order.items) + } + end + + @doc """ + Demonstrates error handling scenarios. + """ + def error_handling_example do + {:ok, user_service} = UserService.start_link() + + # Try to create duplicate user + {:ok, _user1} = UserService.create_user(user_service, "dup", "User", "user@example.com") + duplicate_result = UserService.create_user(user_service, "dup", "Another User", "another@example.com") + + # Try to get non-existent user + missing_user_result = UserService.get_user(user_service, "nonexistent") + + # Try to delete non-existent user + delete_result = UserService.delete_user(user_service, "nonexistent") + + GenServer.stop(user_service) + + %{ + duplicate_user_error: duplicate_result, + missing_user_error: missing_user_result, + delete_missing_error: delete_result + } + end + end + + @doc """ + Main function to run all examples. + """ + def run_all_examples do + %{ + user_management: UserManagement.run_user_example(), + role_management: UserManagement.manage_user_roles(), + shopping: ShoppingExample.run_shopping_example(), + item_filtering: ShoppingExample.item_filtering_example(), + integration: IntegrationExample.run_full_scenario(), + error_handling: IntegrationExample.error_handling_example() + } + end +end \ No newline at end of file diff --git a/test/resources/repos/elixir/test_repo/lib/ignored_dir/ignored_module.ex b/test/resources/repos/elixir/test_repo/lib/ignored_dir/ignored_module.ex new file mode 100644 index 00000000..a311f1d5 --- /dev/null +++ b/test/resources/repos/elixir/test_repo/lib/ignored_dir/ignored_module.ex @@ -0,0 +1,23 @@ +defmodule TestRepo.IgnoredDir.IgnoredModule do + @moduledoc """ + This module is in a directory that should be ignored by the language server. + It's used for testing directory filtering functionality. + """ + + alias TestRepo.Models.User + + @doc """ + This function references the User model to test that ignored directories + don't show up in symbol references. + """ + def create_ignored_user do + User.new("ignored", "Ignored User", "ignored@example.com") + end + + @doc """ + Another function that uses models. + """ + def process_ignored_user(user) do + User.add_role(user, "ignored_role") + end +end \ No newline at end of file diff --git a/test/resources/repos/elixir/test_repo/lib/models.ex b/test/resources/repos/elixir/test_repo/lib/models.ex new file mode 100644 index 00000000..4e8a42d3 --- /dev/null +++ b/test/resources/repos/elixir/test_repo/lib/models.ex @@ -0,0 +1,166 @@ +defmodule TestRepo.Models do + @moduledoc """ + Models module demonstrating various Elixir patterns including structs, protocols, and behaviours. + """ + + defprotocol Serializable do + @doc "Convert model to map representation" + def to_map(model) + end + + defmodule User do + @type t :: %__MODULE__{ + id: String.t(), + name: String.t() | nil, + email: String.t(), + roles: list(String.t()) + } + + defstruct [:id, :name, :email, roles: []] + + @doc """ + Creates a new user. + + ## Examples + + iex> TestRepo.Models.User.new("1", "Alice", "alice@example.com") + %TestRepo.Models.User{id: "1", name: "Alice", email: "alice@example.com", roles: []} + + """ + def new(id, name, email, roles \\ []) do + %__MODULE__{id: id, name: name, email: email, roles: roles} + end + + @doc """ + Checks if user has a specific role. + """ + def has_role?(%__MODULE__{roles: roles}, role) do + role in roles + end + + @doc """ + Adds a role to the user. + """ + def add_role(%__MODULE__{roles: roles} = user, role) do + %{user | roles: [role | roles]} + end + end + + defmodule Item do + @type t :: %__MODULE__{ + id: String.t(), + name: String.t(), + price: float(), + category: String.t() + } + + defstruct [:id, :name, :price, :category] + + @doc """ + Creates a new item. + + ## Examples + + iex> TestRepo.Models.Item.new("1", "Widget", 19.99, "electronics") + %TestRepo.Models.Item{id: "1", name: "Widget", price: 19.99, category: "electronics"} + + """ + def new(id, name, price, category) do + %__MODULE__{id: id, name: name, price: price, category: category} + end + + @doc """ + Formats price for display. + """ + def display_price(%__MODULE__{price: price}) do + "$#{:erlang.float_to_binary(price, decimals: 2)}" + end + + @doc """ + Checks if item is in a specific category. + """ + def in_category?(%__MODULE__{category: category}, target_category) do + category == target_category + end + end + + defmodule Order do + alias TestRepo.Models.{User, Item} + + @type t :: %__MODULE__{ + id: String.t(), + user: User.t(), + items: list(Item.t()), + total: float(), + status: atom() + } + + defstruct [:id, :user, items: [], total: 0.0, status: :pending] + + @doc """ + Creates a new order. + """ + def new(id, user, items \\ []) do + total = calculate_total(items) + %__MODULE__{id: id, user: user, items: items, total: total} + end + + @doc """ + Adds an item to the order. + """ + def add_item(%__MODULE__{items: items} = order, item) do + new_items = [item | items] + %{order | items: new_items, total: calculate_total(new_items)} + end + + @doc """ + Updates order status. + """ + def update_status(%__MODULE__{} = order, status) do + %{order | status: status} + end + + defp calculate_total(items) do + Enum.reduce(items, 0.0, fn item, acc -> acc + item.price end) + end + end + + # Protocol implementations + defimpl Serializable, for: User do + def to_map(%User{id: id, name: name, email: email, roles: roles}) do + %{id: id, name: name, email: email, roles: roles} + end + end + + defimpl Serializable, for: Item do + def to_map(%Item{id: id, name: name, price: price, category: category}) do + %{id: id, name: name, price: price, category: category} + end + end + + defimpl Serializable, for: Order do + def to_map(%Order{id: id, user: user, items: items, total: total, status: status}) do + %{ + id: id, + user: Serializable.to_map(user), + items: Enum.map(items, &Serializable.to_map/1), + total: total, + status: status + } + end + end + + @doc """ + Factory function to create a sample user. + """ + def create_sample_user do + User.new("sample", "Sample User", "sample@example.com", ["user"]) + end + + @doc """ + Factory function to create a sample item. + """ + def create_sample_item do + Item.new("sample", "Sample Item", 9.99, "sample") + end +end \ No newline at end of file diff --git a/test/resources/repos/elixir/test_repo/lib/services.ex b/test/resources/repos/elixir/test_repo/lib/services.ex new file mode 100644 index 00000000..b6768a9a --- /dev/null +++ b/test/resources/repos/elixir/test_repo/lib/services.ex @@ -0,0 +1,257 @@ +defmodule TestRepo.Services do + @moduledoc """ + Services module demonstrating function usage and dependencies. + Similar to Python's services.py, this module uses the models defined in TestRepo.Models. + """ + + alias TestRepo.Models.{User, Item, Order, Serializable} + + defmodule UserService do + use GenServer + + # Client API + + @doc """ + Starts the UserService GenServer. + """ + def start_link(opts \\ []) do + GenServer.start_link(__MODULE__, %{}, opts) + end + + @doc """ + Creates a new user and stores it. + """ + def create_user(pid, id, name, email, roles \\ []) do + GenServer.call(pid, {:create_user, id, name, email, roles}) + end + + @doc """ + Gets a user by ID. + """ + def get_user(pid, id) do + GenServer.call(pid, {:get_user, id}) + end + + @doc """ + Lists all users. + """ + def list_users(pid) do + GenServer.call(pid, :list_users) + end + + @doc """ + Deletes a user by ID. + """ + def delete_user(pid, id) do + GenServer.call(pid, {:delete_user, id}) + end + + # Server callbacks + + @impl true + def init(_) do + {:ok, %{}} + end + + @impl true + def handle_call({:create_user, id, name, email, roles}, _from, users) do + if Map.has_key?(users, id) do + {:reply, {:error, "User with ID #{id} already exists"}, users} + else + user = User.new(id, name, email, roles) + new_users = Map.put(users, id, user) + {:reply, {:ok, user}, new_users} + end + end + + @impl true + def handle_call({:get_user, id}, _from, users) do + case Map.get(users, id) do + nil -> {:reply, {:error, :not_found}, users} + user -> {:reply, {:ok, user}, users} + end + end + + @impl true + def handle_call(:list_users, _from, users) do + user_list = Map.values(users) + {:reply, user_list, users} + end + + @impl true + def handle_call({:delete_user, id}, _from, users) do + if Map.has_key?(users, id) do + new_users = Map.delete(users, id) + {:reply, :ok, new_users} + else + {:reply, {:error, :not_found}, users} + end + end + end + + defmodule ItemService do + use Agent + + @doc """ + Starts the ItemService Agent. + """ + def start_link(opts \\ []) do + Agent.start_link(fn -> %{} end, opts) + end + + @doc """ + Creates a new item and stores it. + """ + def create_item(pid, id, name, price, category) do + Agent.get_and_update(pid, fn items -> + if Map.has_key?(items, id) do + {{:error, "Item with ID #{id} already exists"}, items} + else + item = Item.new(id, name, price, category) + new_items = Map.put(items, id, item) + {{:ok, item}, new_items} + end + end) + end + + @doc """ + Gets an item by ID. + """ + def get_item(pid, id) do + Agent.get(pid, fn items -> + case Map.get(items, id) do + nil -> {:error, :not_found} + item -> {:ok, item} + end + end) + end + + @doc """ + Lists all items, optionally filtered by category. + """ + def list_items(pid, category \\ nil) do + Agent.get(pid, fn items -> + item_list = Map.values(items) + + case category do + nil -> item_list + cat -> Enum.filter(item_list, &Item.in_category?(&1, cat)) + end + end) + end + + @doc """ + Deletes an item by ID. + """ + def delete_item(pid, id) do + Agent.get_and_update(pid, fn items -> + if Map.has_key?(items, id) do + new_items = Map.delete(items, id) + {:ok, new_items} + else + {{:error, :not_found}, items} + end + end) + end + end + + defmodule OrderService do + @doc """ + Creates a new order. + """ + def create_order(id, user, items \\ []) do + Order.new(id, user, items) + end + + @doc """ + Adds an item to an existing order. + """ + def add_item_to_order(order, item) do + Order.add_item(order, item) + end + + @doc """ + Updates the status of an order. + """ + def update_order_status(order, status) do + Order.update_status(order, status) + end + + @doc """ + Processes an order (changes status to :processing). + """ + def process_order(order) do + update_order_status(order, :processing) + end + + @doc """ + Completes an order (changes status to :completed). + """ + def complete_order(order) do + update_order_status(order, :completed) + end + + @doc """ + Cancels an order (changes status to :cancelled). + """ + def cancel_order(order) do + update_order_status(order, :cancelled) + end + end + + @doc """ + Factory function to create a service container. + """ + def create_service_container do + {:ok, user_service} = UserService.start_link() + {:ok, item_service} = ItemService.start_link() + + %{ + user_service: user_service, + item_service: item_service, + order_service: OrderService + } + end + + @doc """ + Helper function to serialize any model that implements the Serializable protocol. + """ + def serialize_model(model) do + Serializable.to_map(model) + end + + # Module-level variables for testing + @sample_user_id "sample_user" + @sample_item_id "sample_item" + + @doc """ + Gets the sample user ID. + """ + def sample_user_id, do: @sample_user_id + + @doc """ + Gets the sample item ID. + """ + def sample_item_id, do: @sample_item_id + + # Create some sample data at module load time + def setup_sample_data(container) do + # Create sample user + UserService.create_user( + container.user_service, + @sample_user_id, + "Sample User", + "sample@example.com", + ["user", "customer"] + ) + + # Create sample item + ItemService.create_item( + container.item_service, + @sample_item_id, + "Sample Widget", + 29.99, + "electronics" + ) + end +end \ No newline at end of file diff --git a/test/resources/repos/elixir/test_repo/lib/test_repo.ex b/test/resources/repos/elixir/test_repo/lib/test_repo.ex new file mode 100644 index 00000000..96a2fd89 --- /dev/null +++ b/test/resources/repos/elixir/test_repo/lib/test_repo.ex @@ -0,0 +1,31 @@ +defmodule TestRepo do + @moduledoc """ + Documentation for `TestRepo`. + """ + + @doc """ + Hello world. + + ## Examples + + iex> TestRepo.hello() + :world + + """ + def hello do + :world + end + + @doc """ + Adds two numbers together. + + ## Examples + + iex> TestRepo.add(2, 3) + 5 + + """ + def add(a, b) do + a + b + end +end \ No newline at end of file diff --git a/test/resources/repos/elixir/test_repo/lib/utils.ex b/test/resources/repos/elixir/test_repo/lib/utils.ex new file mode 100644 index 00000000..458823ed --- /dev/null +++ b/test/resources/repos/elixir/test_repo/lib/utils.ex @@ -0,0 +1,48 @@ +defmodule TestRepo.Utils do + @moduledoc """ + Utility functions for TestRepo. + """ + + @doc """ + Converts a string to uppercase. + + ## Examples + + iex> TestRepo.Utils.upcase("hello") + "HELLO" + + """ + def upcase(string) when is_binary(string) do + String.upcase(string) + end + + @doc """ + Calculates the factorial of a number. + + ## Examples + + iex> TestRepo.Utils.factorial(5) + 120 + + """ + def factorial(0), do: 1 + def factorial(n) when n > 0 do + n * factorial(n - 1) + end + + @doc """ + Checks if a number is even. + + ## Examples + + iex> TestRepo.Utils.even?(4) + true + + iex> TestRepo.Utils.even?(3) + false + + """ + def even?(n) when is_integer(n) do + rem(n, 2) == 0 + end +end \ No newline at end of file diff --git a/test/resources/repos/elixir/test_repo/mix.exs b/test/resources/repos/elixir/test_repo/mix.exs new file mode 100644 index 00000000..76bba38e --- /dev/null +++ b/test/resources/repos/elixir/test_repo/mix.exs @@ -0,0 +1,25 @@ +defmodule TestRepo.MixProject do + use Mix.Project + + def project do + [ + app: :test_repo, + version: "0.1.0", + elixir: "~> 1.14", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + def application do + [ + extra_applications: [:logger] + ] + end + + defp deps do + [ + {:credo, "~> 1.7", only: [:dev, :test], runtime: false} + ] + end +end \ No newline at end of file diff --git a/test/resources/repos/elixir/test_repo/mix.lock b/test/resources/repos/elixir/test_repo/mix.lock new file mode 100644 index 00000000..004414be --- /dev/null +++ b/test/resources/repos/elixir/test_repo/mix.lock @@ -0,0 +1,6 @@ +%{ + "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, + "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, + "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, +} diff --git a/test/resources/repos/elixir/test_repo/scripts/build_script.ex b/test/resources/repos/elixir/test_repo/scripts/build_script.ex new file mode 100644 index 00000000..cc89dfed --- /dev/null +++ b/test/resources/repos/elixir/test_repo/scripts/build_script.ex @@ -0,0 +1,26 @@ +defmodule TestRepo.Scripts.BuildScript do + @moduledoc """ + Build script that references models. + This is in the scripts directory which should be ignored in some tests. + """ + + alias TestRepo.Models.{User, Item} + + @doc """ + Script function that creates test data. + """ + def create_test_data do + user = User.new("script_user", "Script User", "script@example.com") + item = Item.new("script_item", "Script Item", 1.0, "script") + + {user, item} + end + + @doc """ + Another script function referencing User. + """ + def cleanup_users do + # This would reference User in a real scenario + IO.puts("Cleaning up users...") + end +end \ No newline at end of file diff --git a/test/resources/repos/elixir/test_repo/test/models_test.exs b/test/resources/repos/elixir/test_repo/test/models_test.exs new file mode 100644 index 00000000..52260471 --- /dev/null +++ b/test/resources/repos/elixir/test_repo/test/models_test.exs @@ -0,0 +1,169 @@ +defmodule TestRepo.ModelsTest do + use ExUnit.Case + doctest TestRepo.Models + + alias TestRepo.Models.{User, Item, Order, Serializable} + + describe "User" do + test "creates a new user with default roles" do + user = User.new("1", "Alice", "alice@example.com") + + assert user.id == "1" + assert user.name == "Alice" + assert user.email == "alice@example.com" + assert user.roles == [] + end + + test "creates a user with specified roles" do + user = User.new("2", "Bob", "bob@example.com", ["admin", "user"]) + + assert user.roles == ["admin", "user"] + end + + test "checks if user has role" do + user = User.new("3", "Charlie", "charlie@example.com", ["admin"]) + + assert User.has_role?(user, "admin") + refute User.has_role?(user, "guest") + end + + test "adds role to user" do + user = User.new("4", "David", "david@example.com") + user_with_role = User.add_role(user, "moderator") + + assert User.has_role?(user_with_role, "moderator") + assert length(user_with_role.roles) == 1 + end + end + + describe "Item" do + test "creates a new item" do + item = Item.new("widget1", "Super Widget", 19.99, "electronics") + + assert item.id == "widget1" + assert item.name == "Super Widget" + assert item.price == 19.99 + assert item.category == "electronics" + end + + test "formats price for display" do + item = Item.new("item1", "Test Item", 29.99, "test") + + assert Item.display_price(item) == "$29.99" + end + + test "checks if item is in category" do + item = Item.new("book1", "Elixir Book", 39.99, "books") + + assert Item.in_category?(item, "books") + refute Item.in_category?(item, "electronics") + end + end + + describe "Order" do + setup do + user = User.new("customer1", "Customer", "customer@example.com") + item1 = Item.new("item1", "Item 1", 10.00, "category1") + item2 = Item.new("item2", "Item 2", 20.00, "category2") + + %{user: user, item1: item1, item2: item2} + end + + test "creates a new order", %{user: user} do + order = Order.new("order1", user) + + assert order.id == "order1" + assert order.user == user + assert order.items == [] + assert order.total == 0.0 + assert order.status == :pending + end + + test "creates order with items", %{user: user, item1: item1, item2: item2} do + order = Order.new("order2", user, [item1, item2]) + + assert length(order.items) == 2 + assert order.total == 30.0 + end + + test "adds item to order", %{user: user, item1: item1, item2: item2} do + order = Order.new("order3", user, [item1]) + order_with_item = Order.add_item(order, item2) + + assert length(order_with_item.items) == 2 + assert order_with_item.total == 30.0 + end + + test "updates order status", %{user: user} do + order = Order.new("order4", user) + processed_order = Order.update_status(order, :processing) + + assert processed_order.status == :processing + end + end + + describe "Serializable protocol" do + test "serializes User" do + user = User.new("1", "Alice", "alice@example.com", ["admin"]) + serialized = Serializable.to_map(user) + + expected = %{ + id: "1", + name: "Alice", + email: "alice@example.com", + roles: ["admin"] + } + + assert serialized == expected + end + + test "serializes Item" do + item = Item.new("widget1", "Widget", 19.99, "electronics") + serialized = Serializable.to_map(item) + + expected = %{ + id: "widget1", + name: "Widget", + price: 19.99, + category: "electronics" + } + + assert serialized == expected + end + + test "serializes Order" do + user = User.new("1", "Alice", "alice@example.com") + item = Item.new("widget1", "Widget", 19.99, "electronics") + order = Order.new("order1", user, [item]) + + serialized = Serializable.to_map(order) + + assert serialized.id == "order1" + assert serialized.total == 19.99 + assert serialized.status == :pending + assert is_map(serialized.user) + assert is_list(serialized.items) + assert length(serialized.items) == 1 + end + end + + describe "factory functions" do + test "creates sample user" do + user = TestRepo.Models.create_sample_user() + + assert user.id == "sample" + assert user.name == "Sample User" + assert user.email == "sample@example.com" + assert "user" in user.roles + end + + test "creates sample item" do + item = TestRepo.Models.create_sample_item() + + assert item.id == "sample" + assert item.name == "Sample Item" + assert item.price == 9.99 + assert item.category == "sample" + end + end +end \ No newline at end of file diff --git a/test/resources/repos/elixir/test_repo/test/test_repo_test.exs b/test/resources/repos/elixir/test_repo/test/test_repo_test.exs new file mode 100644 index 00000000..62f87c20 --- /dev/null +++ b/test/resources/repos/elixir/test_repo/test/test_repo_test.exs @@ -0,0 +1,14 @@ +defmodule TestRepoTest do + use ExUnit.Case + doctest TestRepo + + test "greets the world" do + assert TestRepo.hello() == :world + end + + test "adds numbers correctly" do + assert TestRepo.add(2, 3) == 5 + assert TestRepo.add(-1, 1) == 0 + assert TestRepo.add(0, 0) == 0 + end +end \ No newline at end of file diff --git a/test/resources/repos/terraform/test_repo/data.tf b/test/resources/repos/terraform/test_repo/data.tf new file mode 100644 index 00000000..0dd2d93f --- /dev/null +++ b/test/resources/repos/terraform/test_repo/data.tf @@ -0,0 +1,28 @@ +# Data sources for the Terraform configuration + +# Get the latest Ubuntu AMI +data "aws_ami" "ubuntu" { + most_recent = true + owners = ["099720109477"] # Canonical + + filter { + name = "name" + values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"] + } + + filter { + name = "virtualization-type" + values = ["hvm"] + } +} + +# Get available availability zones +data "aws_availability_zones" "available" { + state = "available" +} + +# Get current AWS caller identity +data "aws_caller_identity" "current" {} + +# Get current AWS region +data "aws_region" "current" {} diff --git a/test/resources/repos/terraform/test_repo/main.tf b/test/resources/repos/terraform/test_repo/main.tf new file mode 100644 index 00000000..d186c206 --- /dev/null +++ b/test/resources/repos/terraform/test_repo/main.tf @@ -0,0 +1,126 @@ +# Main Terraform configuration +terraform { + required_version = ">= 1.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} + +provider "aws" { + region = var.aws_region +} + +# EC2 Instance +resource "aws_instance" "web_server" { + ami = data.aws_ami.ubuntu.id + instance_type = var.instance_type + + vpc_security_group_ids = [aws_security_group.web_sg.id] + subnet_id = aws_subnet.public.id + + user_data = <<-EOF + #!/bin/bash + apt-get update + apt-get install -y nginx + systemctl start nginx + systemctl enable nginx + EOF + + tags = { + Name = "${var.project_name}-web-server" + Environment = var.environment + Project = var.project_name + } +} + +# S3 Bucket +resource "aws_s3_bucket" "app_bucket" { + bucket = "${var.project_name}-${var.environment}-bucket" + + tags = { + Name = "${var.project_name}-bucket" + Environment = var.environment + Project = var.project_name + } +} + +resource "aws_s3_bucket_versioning" "app_bucket_versioning" { + bucket = aws_s3_bucket.app_bucket.id + versioning_configuration { + status = "Enabled" + } +} + +# VPC +resource "aws_vpc" "main" { + cidr_block = "10.0.0.0/16" + enable_dns_hostnames = true + enable_dns_support = true + + tags = { + Name = "${var.project_name}-vpc" + Environment = var.environment + Project = var.project_name + } +} + +# Internet Gateway +resource "aws_internet_gateway" "main" { + vpc_id = aws_vpc.main.id + + tags = { + Name = "${var.project_name}-igw" + Environment = var.environment + Project = var.project_name + } +} + +# Public Subnet +resource "aws_subnet" "public" { + vpc_id = aws_vpc.main.id + cidr_block = "10.0.1.0/24" + availability_zone = data.aws_availability_zones.available.names[0] + map_public_ip_on_launch = true + + tags = { + Name = "${var.project_name}-public-subnet" + Environment = var.environment + Project = var.project_name + } +} + +# Security Group +resource "aws_security_group" "web_sg" { + name_prefix = "${var.project_name}-web-" + vpc_id = aws_vpc.main.id + + ingress { + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "${var.project_name}-web-sg" + Environment = var.environment + Project = var.project_name + } +} diff --git a/test/resources/repos/terraform/test_repo/outputs.tf b/test/resources/repos/terraform/test_repo/outputs.tf new file mode 100644 index 00000000..856ed6dd --- /dev/null +++ b/test/resources/repos/terraform/test_repo/outputs.tf @@ -0,0 +1,46 @@ +# Output values for the Terraform configuration + +output "instance_id" { + description = "ID of the EC2 instance" + value = aws_instance.web_server.id +} + +output "instance_public_ip" { + description = "Public IP address of the EC2 instance" + value = aws_instance.web_server.public_ip +} + +output "instance_public_dns" { + description = "Public DNS name of the EC2 instance" + value = aws_instance.web_server.public_dns +} + +output "s3_bucket_name" { + description = "Name of the S3 bucket" + value = aws_s3_bucket.app_bucket.bucket +} + +output "s3_bucket_arn" { + description = "ARN of the S3 bucket" + value = aws_s3_bucket.app_bucket.arn +} + +output "vpc_id" { + description = "ID of the VPC" + value = aws_vpc.main.id +} + +output "subnet_id" { + description = "ID of the public subnet" + value = aws_subnet.public.id +} + +output "security_group_id" { + description = "ID of the security group" + value = aws_security_group.web_sg.id +} + +output "application_url" { + description = "URL to access the application" + value = "http://${aws_instance.web_server.public_dns}" +} diff --git a/test/resources/repos/terraform/test_repo/variables.tf b/test/resources/repos/terraform/test_repo/variables.tf new file mode 100644 index 00000000..92437bd9 --- /dev/null +++ b/test/resources/repos/terraform/test_repo/variables.tf @@ -0,0 +1,61 @@ +# Input variables for the Terraform configuration + +variable "aws_region" { + description = "AWS region for resources" + type = string + default = "us-west-2" +} + +variable "instance_type" { + description = "EC2 instance type" + type = string + default = "t3.micro" + + validation { + condition = contains([ + "t3.micro", "t3.small", "t3.medium", + "t2.micro", "t2.small", "t2.medium" + ], var.instance_type) + error_message = "Instance type must be a valid t2 or t3 instance type." + } +} + +variable "environment" { + description = "Environment name (dev, staging, prod)" + type = string + default = "dev" + + validation { + condition = contains(["dev", "staging", "prod"], var.environment) + error_message = "Environment must be dev, staging, or prod." + } +} + +variable "project_name" { + description = "Name of the project" + type = string + default = "terraform-test" + + validation { + condition = can(regex("^[a-z0-9-]+$", var.project_name)) + error_message = "Project name must contain only lowercase letters, numbers, and hyphens." + } +} + +variable "enable_monitoring" { + description = "Enable CloudWatch monitoring" + type = bool + default = false +} + +variable "allowed_cidr_blocks" { + description = "List of CIDR blocks allowed to access the application" + type = list(string) + default = ["0.0.0.0/0"] +} + +variable "tags" { + description = "Additional tags to apply to resources" + type = map(string) + default = {} +} diff --git a/test/serena/config/__init__.py b/test/serena/config/__init__.py new file mode 100644 index 00000000..6b95e0de --- /dev/null +++ b/test/serena/config/__init__.py @@ -0,0 +1 @@ +# Empty init file for test package diff --git a/test/serena/config/test_serena_config.py b/test/serena/config/test_serena_config.py new file mode 100644 index 00000000..14eb3cd9 --- /dev/null +++ b/test/serena/config/test_serena_config.py @@ -0,0 +1,138 @@ +import shutil +import tempfile +from pathlib import Path + +import pytest + +from serena.config.serena_config import ProjectConfig +from solidlsp.ls_config import Language + + +class TestProjectConfigAutogenerate: + """Test class for ProjectConfig autogeneration functionality.""" + + def setup_method(self): + """Set up test environment before each test method.""" + # Create a temporary directory for testing + self.test_dir = tempfile.mkdtemp() + self.project_path = Path(self.test_dir) + + def teardown_method(self): + """Clean up test environment after each test method.""" + # Remove the temporary directory + shutil.rmtree(self.test_dir) + + def test_autogenerate_empty_directory(self): + """Test that autogenerate raises ValueError with helpful message for empty directory.""" + with pytest.raises(ValueError) as exc_info: + ProjectConfig.autogenerate(self.project_path, save_to_disk=False) + + error_message = str(exc_info.value) + # Check that the error message contains all the key information + assert "No source files found" in error_message + assert str(self.project_path.resolve()) in error_message + assert "To use Serena with this project" in error_message + assert "Add source files in one of the supported languages" in error_message + assert "Create a project configuration file manually" in error_message + assert str(Path(".serena") / "project.yml") in error_message + assert "Example project.yml:" in error_message + assert f"project_name: {self.project_path.name}" in error_message + assert "language: python" in error_message + + def test_autogenerate_with_python_files(self): + """Test successful autogeneration with Python source files.""" + # Create a Python file + python_file = self.project_path / "main.py" + python_file.write_text("def hello():\n print('Hello, world!')\n") + + # Run autogenerate + config = ProjectConfig.autogenerate(self.project_path, save_to_disk=False) + + # Verify the configuration + assert config.project_name == self.project_path.name + assert config.language == Language.PYTHON + + def test_autogenerate_with_multiple_languages(self): + """Test autogeneration picks dominant language when multiple are present.""" + # Create files for multiple languages + (self.project_path / "main.py").write_text("print('Python')") + (self.project_path / "util.py").write_text("def util(): pass") + (self.project_path / "small.js").write_text("console.log('JS');") + + # Run autogenerate - should pick Python as dominant + config = ProjectConfig.autogenerate(self.project_path, save_to_disk=False) + + assert config.language == Language.PYTHON + + def test_autogenerate_saves_to_disk(self): + """Test that autogenerate can save the configuration to disk.""" + # Create a Go file + go_file = self.project_path / "main.go" + go_file.write_text("package main\n\nfunc main() {}\n") + + # Run autogenerate with save_to_disk=True + config = ProjectConfig.autogenerate(self.project_path, save_to_disk=True) + + # Verify the configuration file was created + config_path = self.project_path / ".serena" / "project.yml" + assert config_path.exists() + + # Verify the content + assert config.language == Language.GO + + def test_autogenerate_nonexistent_path(self): + """Test that autogenerate raises FileNotFoundError for non-existent path.""" + non_existent = self.project_path / "does_not_exist" + + with pytest.raises(FileNotFoundError) as exc_info: + ProjectConfig.autogenerate(non_existent, save_to_disk=False) + + assert "Project root not found" in str(exc_info.value) + + def test_autogenerate_with_gitignored_files_only(self): + """Test autogenerate behavior when only gitignored files exist.""" + # Create a .gitignore that ignores all Python files + gitignore = self.project_path / ".gitignore" + gitignore.write_text("*.py\n") + + # Create Python files that will be ignored + (self.project_path / "ignored.py").write_text("print('ignored')") + + # Should still raise ValueError as no source files are detected + with pytest.raises(ValueError) as exc_info: + ProjectConfig.autogenerate(self.project_path, save_to_disk=False) + + assert "No source files found" in str(exc_info.value) + + def test_autogenerate_custom_project_name(self): + """Test autogenerate with custom project name.""" + # Create a TypeScript file + ts_file = self.project_path / "index.ts" + ts_file.write_text("const greeting: string = 'Hello';\n") + + # Run autogenerate with custom name + custom_name = "my-custom-project" + config = ProjectConfig.autogenerate(self.project_path, project_name=custom_name, save_to_disk=False) + + assert config.project_name == custom_name + assert config.language == Language.TYPESCRIPT + + def test_autogenerate_error_message_format(self): + """Test the specific format of the error message for better user experience.""" + with pytest.raises(ValueError) as exc_info: + ProjectConfig.autogenerate(self.project_path, save_to_disk=False) + + error_lines = str(exc_info.value).split("\n") + + # Verify the structure of the error message + assert len(error_lines) >= 8 # Should have multiple lines of helpful information + + # Check for numbered instructions + assert any("1." in line for line in error_lines) + assert any("2." in line for line in error_lines) + + # Check for supported languages list + assert any("Python" in line and "TypeScript" in line for line in error_lines) + + # Check example includes comment about language options + assert any("# or typescript, java, csharp" in line for line in error_lines) diff --git a/test/serena/util/test_exception.py b/test/serena/util/test_exception.py new file mode 100644 index 00000000..e13e57f7 --- /dev/null +++ b/test/serena/util/test_exception.py @@ -0,0 +1,116 @@ +import os +from unittest.mock import MagicMock, Mock, patch + +import pytest + +from serena.util.exception import is_headless_environment, show_fatal_exception_safe + + +class TestHeadlessEnvironmentDetection: + """Test class for headless environment detection functionality.""" + + def test_is_headless_no_display(self): + """Test that environment without DISPLAY is detected as headless on Linux.""" + with patch("sys.platform", "linux"): + with patch.dict(os.environ, {}, clear=True): + assert is_headless_environment() is True + + def test_is_headless_ssh_connection(self): + """Test that SSH sessions are detected as headless.""" + with patch("sys.platform", "linux"): + with patch.dict(os.environ, {"SSH_CONNECTION": "192.168.1.1 22 192.168.1.2 22", "DISPLAY": ":0"}): + assert is_headless_environment() is True + + with patch.dict(os.environ, {"SSH_CLIENT": "192.168.1.1 22 22", "DISPLAY": ":0"}): + assert is_headless_environment() is True + + def test_is_headless_wsl(self): + """Test that WSL environment is detected as headless.""" + # Skip this test on Windows since os.uname doesn't exist + if not hasattr(os, "uname"): + pytest.skip("os.uname not available on this platform") + + with patch("sys.platform", "linux"): + with patch("os.uname") as mock_uname: + mock_uname.return_value = Mock(release="5.15.153.1-microsoft-standard-WSL2") + with patch.dict(os.environ, {"DISPLAY": ":0"}): + assert is_headless_environment() is True + + def test_is_headless_docker(self): + """Test that Docker containers are detected as headless.""" + with patch("sys.platform", "linux"): + # Test with CI environment variable + with patch.dict(os.environ, {"CI": "true", "DISPLAY": ":0"}): + assert is_headless_environment() is True + + # Test with CONTAINER environment variable + with patch.dict(os.environ, {"CONTAINER": "docker", "DISPLAY": ":0"}): + assert is_headless_environment() is True + + # Test with .dockerenv file + with patch("os.path.exists") as mock_exists: + mock_exists.return_value = True + with patch.dict(os.environ, {"DISPLAY": ":0"}): + assert is_headless_environment() is True + + def test_is_not_headless_windows(self): + """Test that Windows is never detected as headless.""" + with patch("sys.platform", "win32"): + # Even without DISPLAY, Windows should not be headless + with patch.dict(os.environ, {}, clear=True): + assert is_headless_environment() is False + + +class TestShowFatalExceptionSafe: + """Test class for safe fatal exception display functionality.""" + + @patch("serena.util.exception.is_headless_environment", return_value=True) + @patch("serena.util.exception.log") + def test_show_fatal_exception_safe_headless(self, mock_log, mock_is_headless): + """Test that GUI is not attempted in headless environment.""" + test_exception = ValueError("Test error") + + # The import should never happen in headless mode + with patch("serena.gui_log_viewer.show_fatal_exception") as mock_show_gui: + show_fatal_exception_safe(test_exception) + mock_show_gui.assert_not_called() + + # Verify debug log about skipping GUI + mock_log.debug.assert_called_once_with("Skipping GUI error display in headless environment") + + @patch("serena.util.exception.is_headless_environment", return_value=False) + @patch("serena.util.exception.log") + def test_show_fatal_exception_safe_with_gui(self, mock_log, mock_is_headless): + """Test that GUI is attempted when not in headless environment.""" + test_exception = ValueError("Test error") + + # Mock the GUI function + with patch("serena.gui_log_viewer.show_fatal_exception") as mock_show_gui: + show_fatal_exception_safe(test_exception) + mock_show_gui.assert_called_once_with(test_exception) + + @patch("serena.util.exception.is_headless_environment", return_value=False) + @patch("serena.util.exception.log") + def test_show_fatal_exception_safe_gui_failure(self, mock_log, mock_is_headless): + """Test graceful handling when GUI display fails.""" + test_exception = ValueError("Test error") + gui_error = ImportError("No module named 'tkinter'") + + # Mock the GUI function to raise an exception + with patch("serena.gui_log_viewer.show_fatal_exception", side_effect=gui_error): + show_fatal_exception_safe(test_exception) + + # Verify debug log about GUI failure + mock_log.debug.assert_called_with(f"Failed to show GUI error dialog: {gui_error}") + + def test_show_fatal_exception_safe_prints_to_stderr(self): + """Test that exceptions are always printed to stderr.""" + test_exception = ValueError("Test error message") + + with patch("sys.stderr", new_callable=MagicMock) as mock_stderr: + with patch("serena.util.exception.is_headless_environment", return_value=True): + with patch("serena.util.exception.log"): + show_fatal_exception_safe(test_exception) + + # Verify print was called with the correct arguments + mock_stderr.write.assert_any_call("Fatal exception: Test error message") diff --git a/test/solidlsp/clojure/__init__.py b/test/solidlsp/clojure/__init__.py index c16da80d..be945a53 100644 --- a/test/solidlsp/clojure/__init__.py +++ b/test/solidlsp/clojure/__init__.py @@ -1,6 +1,6 @@ from pathlib import Path -from solidlsp.language_servers.clojure_lsp.clojure_lsp import verify_clojure_cli +from solidlsp.language_servers.clojure_lsp import verify_clojure_cli def _test_clojure_cli() -> bool: diff --git a/test/solidlsp/csharp/test_csharp_basic.py b/test/solidlsp/csharp/test_csharp_basic.py index 56db5cfa..59f393ac 100644 --- a/test/solidlsp/csharp/test_csharp_basic.py +++ b/test/solidlsp/csharp/test_csharp_basic.py @@ -6,7 +6,7 @@ from unittest.mock import Mock, patch import pytest from solidlsp import SolidLanguageServer -from solidlsp.language_servers.csharp_language_server.csharp_language_server import ( +from solidlsp.language_servers.csharp_language_server import ( CSharpLanguageServer, breadth_first_file_scan, find_solution_or_project_file, @@ -57,7 +57,7 @@ class TestCSharpLanguageServer: break assert add_symbol is not None, "Could not find 'Add' method symbol in Program.cs" sel_start = add_symbol["selectionRange"]["start"] - refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"] + 1) assert any( "Program.cs" in ref.get("relativePath", "") for ref in refs ), "Program.cs should reference Add method (tried all positions in selectionRange)" @@ -196,55 +196,52 @@ class TestCSharpSolutionProjectOpening: # Should still prefer .sln file even though it's deeper assert result == str(solution_file) - @patch("solidlsp.language_servers.csharp_language_server.csharp_language_server.CSharpLanguageServer._ensure_server_installed") - @patch("solidlsp.language_servers.csharp_language_server.csharp_language_server.CSharpLanguageServer._start_server") + @patch("solidlsp.language_servers.csharp_language_server.CSharpLanguageServer._ensure_server_installed") + @patch("solidlsp.language_servers.csharp_language_server.CSharpLanguageServer._start_server") def test_csharp_language_server_logs_solution_discovery(self, mock_start_server, mock_ensure_server_installed): """Test that CSharpLanguageServer logs solution/project discovery during initialization.""" - with tempfile.TemporaryDirectory() as cache_dir: - # Mock the server installation - mock_ensure_server_installed.return_value = ("/usr/bin/dotnet", "/path/to/server.dll", Path(cache_dir)) + mock_ensure_server_installed.return_value = ("/usr/bin/dotnet", "/path/to/server.dll") - # Create test directory with solution file - with tempfile.TemporaryDirectory() as temp_dir: - temp_path = Path(temp_dir) - solution_file = temp_path / "TestSolution.sln" - solution_file.touch() + # Create test directory with solution file + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + solution_file = temp_path / "TestSolution.sln" + solution_file.touch() - # Mock logger to capture log messages - mock_logger = Mock() - mock_config = Mock(spec=LanguageServerConfig) - mock_config.ignored_paths = [] + # Mock logger to capture log messages + mock_logger = Mock() + mock_config = Mock(spec=LanguageServerConfig) + mock_config.ignored_paths = [] - # Create CSharpLanguageServer instance - CSharpLanguageServer(mock_config, mock_logger, str(temp_path)) + # Create CSharpLanguageServer instance + CSharpLanguageServer(mock_config, mock_logger, str(temp_path)) - # 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 + # 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 - @patch("solidlsp.language_servers.csharp_language_server.csharp_language_server.CSharpLanguageServer._ensure_server_installed") - @patch("solidlsp.language_servers.csharp_language_server.csharp_language_server.CSharpLanguageServer._start_server") + @patch("solidlsp.language_servers.csharp_language_server.CSharpLanguageServer._ensure_server_installed") + @patch("solidlsp.language_servers.csharp_language_server.CSharpLanguageServer._start_server") def test_csharp_language_server_logs_no_solution_warning(self, mock_start_server, mock_ensure_server_installed): """Test that CSharpLanguageServer logs warning when no solution/project files are found.""" - with tempfile.TemporaryDirectory() as cache_dir: - # Mock the server installation - mock_ensure_server_installed.return_value = ("/usr/bin/dotnet", "/path/to/server.dll", Path(cache_dir)) + # Mock the server installation + mock_ensure_server_installed.return_value = ("/usr/bin/dotnet", "/path/to/server.dll") - # Create empty test directory - with tempfile.TemporaryDirectory() as temp_dir: - temp_path = Path(temp_dir) + # Create empty test directory + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) - # Mock logger to capture log messages - mock_logger = Mock() - mock_config = Mock(spec=LanguageServerConfig) - mock_config.ignored_paths = [] + # Mock logger to capture log messages + mock_logger = Mock() + mock_config = Mock(spec=LanguageServerConfig) + mock_config.ignored_paths = [] - # Create CSharpLanguageServer instance - CSharpLanguageServer(mock_config, mock_logger, str(temp_path)) + # Create CSharpLanguageServer instance + CSharpLanguageServer(mock_config, mock_logger, str(temp_path)) - # Verify that logger was called with warning about no solution/project files - mock_logger.log.assert_any_call( - "No .sln or .csproj file found, language server will attempt auto-discovery", 30 # logging.WARNING - ) + # Verify that logger was called with warning about no solution/project files + mock_logger.log.assert_any_call( + "No .sln or .csproj file found, language server will attempt auto-discovery", 30 # logging.WARNING + ) def test_solution_and_project_opening_with_real_test_repo(self): """Test solution and project opening with the actual C# test repository.""" diff --git a/test/solidlsp/elixir/__init__.py b/test/solidlsp/elixir/__init__.py new file mode 100644 index 00000000..4d7e8860 --- /dev/null +++ b/test/solidlsp/elixir/__init__.py @@ -0,0 +1,28 @@ +import platform + + +def _test_nextls_available() -> str: + """Test if Next LS is available and return error reason if not.""" + # Check if we're on Windows (Next LS doesn't support Windows) + if platform.system() == "Windows": + return "Next LS does not support Windows" + + # Try to import and check Elixir availability + try: + from solidlsp.language_servers.elixir_tools.elixir_tools import ElixirTools + + # Check if Elixir is installed + elixir_version = ElixirTools._get_elixir_version() + if not elixir_version: + return "Elixir is not installed or not in PATH" + + return "" # No error, Next LS should be available + + except ImportError as e: + return f"Failed to import ElixirTools: {e}" + except Exception as e: + return f"Error checking Next LS availability: {e}" + + +NEXTLS_UNAVAILABLE_REASON = _test_nextls_available() +NEXTLS_UNAVAILABLE = bool(NEXTLS_UNAVAILABLE_REASON) diff --git a/test/solidlsp/elixir/conftest.py b/test/solidlsp/elixir/conftest.py new file mode 100644 index 00000000..34523765 --- /dev/null +++ b/test/solidlsp/elixir/conftest.py @@ -0,0 +1,174 @@ +""" +Elixir-specific test configuration and fixtures. +""" + +import os +import subprocess +import time +from pathlib import Path + +import pytest + + +def ensure_elixir_test_repo_compiled(repo_path: str) -> None: + """Ensure the Elixir test repository dependencies are installed and project is compiled. + + Next LS requires the project to be fully compiled and indexed before providing + complete references and symbol resolution. This function: + 1. Installs dependencies via 'mix deps.get' + 2. Compiles the project via 'mix compile' + + This is essential in CI environments where dependencies aren't pre-installed. + + Args: + repo_path: Path to the Elixir project root directory + + """ + # Check if this looks like an Elixir project + mix_file = os.path.join(repo_path, "mix.exs") + if not os.path.exists(mix_file): + return + + # Check if already compiled (optimization for repeated runs) + build_path = os.path.join(repo_path, "_build") + deps_path = os.path.join(repo_path, "deps") + + if os.path.exists(build_path) and os.path.exists(deps_path): + print(f"Elixir test repository already compiled in {repo_path}") + return + + try: + print("Installing dependencies and compiling Elixir test repository for optimal Next LS performance...") + + # First, install dependencies with increased timeout for CI + print("=" * 60) + print("Step 1/2: Installing Elixir dependencies...") + print("=" * 60) + start_time = time.time() + + deps_result = subprocess.run( + ["mix", "deps.get"], + cwd=repo_path, + capture_output=True, + text=True, + timeout=180, + check=False, # 3 minutes for dependency installation (CI can be slow) + ) + + deps_duration = time.time() - start_time + print(f"Dependencies installation completed in {deps_duration:.2f} seconds") + + # Always log the output for transparency + if deps_result.stdout.strip(): + print("Dependencies stdout:") + print("-" * 40) + print(deps_result.stdout) + print("-" * 40) + + if deps_result.stderr.strip(): + print("Dependencies stderr:") + print("-" * 40) + print(deps_result.stderr) + print("-" * 40) + + if deps_result.returncode != 0: + print(f"⚠️ Warning: Dependencies installation failed with exit code {deps_result.returncode}") + # Continue anyway - some projects might not have dependencies + else: + print("✓ Dependencies installed successfully") + + # Then compile the project with increased timeout for CI + print("=" * 60) + print("Step 2/2: Compiling Elixir project...") + print("=" * 60) + start_time = time.time() + + compile_result = subprocess.run( + ["mix", "compile"], + cwd=repo_path, + capture_output=True, + text=True, + timeout=300, + check=False, # 5 minutes for compilation (Credo compilation can be slow in CI) + ) + + compile_duration = time.time() - start_time + print(f"Compilation completed in {compile_duration:.2f} seconds") + + # Always log the output for transparency + if compile_result.stdout.strip(): + print("Compilation stdout:") + print("-" * 40) + print(compile_result.stdout) + print("-" * 40) + + if compile_result.stderr.strip(): + print("Compilation stderr:") + print("-" * 40) + print(compile_result.stderr) + print("-" * 40) + + if compile_result.returncode == 0: + print(f"✓ Elixir test repository compiled successfully in {repo_path}") + else: + print(f"⚠️ Warning: Compilation completed with exit code {compile_result.returncode}") + # Still continue - warnings are often non-fatal + + print("=" * 60) + print(f"Total setup time: {time.time() - (start_time - compile_duration - deps_duration):.2f} seconds") + print("=" * 60) + + except subprocess.TimeoutExpired as e: + print("=" * 60) + print(f"❌ TIMEOUT: Elixir setup timed out after {e.timeout} seconds") + print(f"Command: {' '.join(e.cmd)}") + print("This may indicate slow CI environment - Next LS may still work but with reduced functionality") + + # Try to get partial output if available + if hasattr(e, "stdout") and e.stdout: + print("Partial stdout before timeout:") + print("-" * 40) + print(e.stdout) + print("-" * 40) + if hasattr(e, "stderr") and e.stderr: + print("Partial stderr before timeout:") + print("-" * 40) + print(e.stderr) + print("-" * 40) + print("=" * 60) + + except FileNotFoundError: + print("❌ ERROR: 'mix' command not found - Elixir test repository may not be compiled") + print("Please ensure Elixir is installed and available in PATH") + except Exception as e: + print(f"❌ ERROR: Failed to prepare Elixir test repository: {e}") + + +@pytest.fixture(scope="session", autouse=True) +def setup_elixir_test_environment(): + """Automatically prepare Elixir test environment for all Elixir tests. + + This fixture runs once per test session and automatically: + 1. Installs dependencies via 'mix deps.get' + 2. Compiles the Elixir test repository via 'mix compile' + + It uses autouse=True so it runs automatically without needing to be explicitly + requested by tests. This ensures Next LS has a fully prepared project to work with. + + Uses generous timeouts (3-5 minutes) to accommodate slow CI environments. + All output is logged for transparency and debugging. + """ + # Get the test repo path relative to this conftest.py file + test_repo_path = Path(__file__).parent.parent.parent / "resources" / "repos" / "elixir" / "test_repo" + ensure_elixir_test_repo_compiled(str(test_repo_path)) + return str(test_repo_path) + + +@pytest.fixture(scope="session") +def elixir_test_repo_path(setup_elixir_test_environment): + """Get the path to the prepared Elixir test repository. + + This fixture depends on setup_elixir_test_environment to ensure dependencies + are installed and compilation has completed before returning the path. + """ + return setup_elixir_test_environment diff --git a/test/solidlsp/elixir/test_elixir_basic.py b/test/solidlsp/elixir/test_elixir_basic.py new file mode 100644 index 00000000..10391406 --- /dev/null +++ b/test/solidlsp/elixir/test_elixir_basic.py @@ -0,0 +1,112 @@ +""" +Basic integration tests for the Elixir language server functionality. + +These tests validate the functionality of the language server APIs +like request_references using the test repository. +""" + +import os + +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language + +from . import NEXTLS_UNAVAILABLE, NEXTLS_UNAVAILABLE_REASON + +# These marks will be applied to all tests in this module +pytestmark = [pytest.mark.elixir, pytest.mark.skipif(NEXTLS_UNAVAILABLE, reason=f"Next LS not available: {NEXTLS_UNAVAILABLE_REASON}")] + + +class TestElixirBasic: + """Basic Elixir language server functionality tests.""" + + @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) + def test_request_references_function_definition(self, language_server: SolidLanguageServer): + """Test finding references to a function definition.""" + file_path = os.path.join("lib", "models.ex") + symbols = language_server.request_document_symbols(file_path) + + # Find the User module's 'new' function + user_new_symbol = None + for symbol in symbols[0]: # Top level symbols + if symbol.get("name") == "User" and symbol.get("kind") == 2: # Module + for child in symbol.get("children", []): + if child.get("name", "").startswith("def new(") and child.get("kind") == 12: # Function + user_new_symbol = child + break + break + + if not user_new_symbol or "selectionRange" not in user_new_symbol: + pytest.skip("User.new function or its selectionRange not found") + + sel_start = user_new_symbol["selectionRange"]["start"] + references = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + + assert references is not None + assert len(references) > 0 + + # Should find at least one reference (the definition itself) + found_definition = any(ref["uri"].endswith("models.ex") for ref in references) + assert found_definition, "Should find the function definition" + + @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) + def test_request_references_create_user_function(self, language_server: SolidLanguageServer): + """Test finding references to create_user function.""" + file_path = os.path.join("lib", "services.ex") + symbols = language_server.request_document_symbols(file_path) + + # Find the UserService module's 'create_user' function + create_user_symbol = None + for symbol in symbols[0]: # Top level symbols + if symbol.get("name") == "UserService" and symbol.get("kind") == 2: # Module + for child in symbol.get("children", []): + if child.get("name", "").startswith("def create_user(") and child.get("kind") == 12: # Function + create_user_symbol = child + break + break + + if not create_user_symbol or "selectionRange" not in create_user_symbol: + pytest.skip("UserService.create_user function or its selectionRange not found") + + sel_start = create_user_symbol["selectionRange"]["start"] + references = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + + assert references is not None + assert len(references) > 0 + + @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) + def test_request_referencing_symbols_function(self, language_server: SolidLanguageServer): + """Test finding symbols that reference a specific function.""" + file_path = os.path.join("lib", "models.ex") + symbols = language_server.request_document_symbols(file_path) + + # Find the User module's 'new' function + user_new_symbol = None + for symbol in symbols[0]: # Top level symbols + if symbol.get("name") == "User" and symbol.get("kind") == 2: # Module + for child in symbol.get("children", []): + if child.get("name", "").startswith("def new(") and child.get("kind") == 12: # Function + user_new_symbol = child + break + break + + if not user_new_symbol or "selectionRange" not in user_new_symbol: + pytest.skip("User.new function or its selectionRange not found") + + sel_start = user_new_symbol["selectionRange"]["start"] + referencing_symbols = language_server.request_referencing_symbols(file_path, sel_start["line"], sel_start["character"]) + + assert referencing_symbols is not None + + @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) + def test_timeout_enumeration_bug(self, language_server: SolidLanguageServer): + """Test that enumeration doesn't timeout (regression test).""" + # This should complete without timing out + symbols = language_server.request_document_symbols("lib/models.ex") + assert symbols is not None + + # Test multiple symbol requests in succession + for _ in range(3): + symbols = language_server.request_document_symbols("lib/services.ex") + assert symbols is not None diff --git a/test/solidlsp/elixir/test_elixir_ignored_dirs.py b/test/solidlsp/elixir/test_elixir_ignored_dirs.py new file mode 100644 index 00000000..95a6f5f2 --- /dev/null +++ b/test/solidlsp/elixir/test_elixir_ignored_dirs.py @@ -0,0 +1,142 @@ +from collections.abc import Generator +from pathlib import Path + +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from test.conftest import create_ls + +from . import NEXTLS_UNAVAILABLE, NEXTLS_UNAVAILABLE_REASON + +# These marks will be applied to all tests in this module +pytestmark = [pytest.mark.elixir, pytest.mark.skipif(NEXTLS_UNAVAILABLE, reason=f"Next LS not available: {NEXTLS_UNAVAILABLE_REASON}")] + + +@pytest.fixture(scope="module") +def ls_with_ignored_dirs() -> Generator[SolidLanguageServer, None, None]: + """Fixture to set up an LS for the elixir test repo with the 'scripts' directory ignored.""" + ignored_paths = ["scripts", "ignored_dir"] + ls = create_ls(ignored_paths=ignored_paths, language=Language.ELIXIR) + ls.start() + try: + yield ls + finally: + ls.stop() + + +@pytest.mark.parametrize("ls_with_ignored_dirs", [Language.ELIXIR], indirect=True) +def test_symbol_tree_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer): + """Tests that request_full_symbol_tree ignores the configured directory.""" + root = ls_with_ignored_dirs.request_full_symbol_tree()[0] + root_children = root["children"] + children_names = {child["name"] for child in root_children} + + # Should have lib and test directories, but not scripts or ignored_dir + expected_dirs = {"lib", "test"} + assert expected_dirs.issubset(children_names), f"Expected {expected_dirs} to be in {children_names}" + assert "scripts" not in children_names, f"scripts should not be in {children_names}" + assert "ignored_dir" not in children_names, f"ignored_dir should not be in {children_names}" + + +@pytest.mark.parametrize("ls_with_ignored_dirs", [Language.ELIXIR], indirect=True) +def test_find_references_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer): + """Tests that find_references ignores the configured directory.""" + # Location of User struct, which is referenced in scripts and ignored_dir + definition_file = "lib/models.ex" + + # Find the User struct definition + symbols = ls_with_ignored_dirs.request_document_symbols(definition_file) + user_symbol = None + for symbol_group in symbols: + user_symbol = next((s for s in symbol_group if "User" in s.get("name", "")), None) + if user_symbol: + break + + if not user_symbol or "selectionRange" not in user_symbol: + pytest.skip("User symbol not found for reference testing") + + sel_start = user_symbol["selectionRange"]["start"] + references = ls_with_ignored_dirs.request_references(definition_file, sel_start["line"], sel_start["character"]) + + # Assert that scripts and ignored_dir do not appear in the references + assert not any("scripts" in ref["relativePath"] for ref in references), "scripts should be ignored" + assert not any("ignored_dir" in ref["relativePath"] for ref in references), "ignored_dir should be ignored" + + +@pytest.mark.parametrize("repo_path", [Language.ELIXIR], indirect=True) +def test_refs_and_symbols_with_glob_patterns(repo_path: Path) -> None: + """Tests that refs and symbols with glob patterns are ignored.""" + ignored_paths = ["*cripts", "ignored_*"] # codespell:ignore cripts + ls = create_ls(ignored_paths=ignored_paths, repo_path=str(repo_path), language=Language.ELIXIR) + ls.start() + + try: + # Same as in the above tests + root = ls.request_full_symbol_tree()[0] + root_children = root["children"] + children_names = {child["name"] for child in root_children} + + # Should have lib and test directories, but not scripts or ignored_dir + expected_dirs = {"lib", "test"} + assert expected_dirs.issubset(children_names), f"Expected {expected_dirs} to be in {children_names}" + assert "scripts" not in children_names, f"scripts should not be in {children_names} (glob pattern)" + assert "ignored_dir" not in children_names, f"ignored_dir should not be in {children_names} (glob pattern)" + + # Test that the refs and symbols with glob patterns are ignored + definition_file = "lib/models.ex" + + # Find the User struct definition + symbols = ls.request_document_symbols(definition_file) + user_symbol = None + for symbol_group in symbols: + user_symbol = next((s for s in symbol_group if "User" in s.get("name", "")), None) + if user_symbol: + break + + if user_symbol and "selectionRange" in user_symbol: + sel_start = user_symbol["selectionRange"]["start"] + references = ls.request_references(definition_file, sel_start["line"], sel_start["character"]) + + # Assert that scripts and ignored_dir do not appear in references + assert not any("scripts" in ref["relativePath"] for ref in references), "scripts should be ignored (glob)" + assert not any("ignored_dir" in ref["relativePath"] for ref in references), "ignored_dir should be ignored (glob)" + finally: + ls.stop() + + +@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) +def test_default_ignored_directories(language_server: SolidLanguageServer): + """Test that default Elixir directories are ignored.""" + # Test that Elixir-specific directories are ignored by default + assert language_server.is_ignored_dirname("_build"), "_build should be ignored" + assert language_server.is_ignored_dirname("deps"), "deps should be ignored" + assert language_server.is_ignored_dirname(".elixir_ls"), ".elixir_ls should be ignored" + assert language_server.is_ignored_dirname("cover"), "cover should be ignored" + assert language_server.is_ignored_dirname("node_modules"), "node_modules should be ignored" + + # Test that important directories are not ignored + assert not language_server.is_ignored_dirname("lib"), "lib should not be ignored" + assert not language_server.is_ignored_dirname("test"), "test should not be ignored" + assert not language_server.is_ignored_dirname("config"), "config should not be ignored" + assert not language_server.is_ignored_dirname("priv"), "priv should not be ignored" + + +@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) +def test_symbol_tree_excludes_build_dirs(language_server: SolidLanguageServer): + """Test that symbol tree excludes build and dependency directories.""" + symbol_tree = language_server.request_full_symbol_tree() + + if symbol_tree: + root = symbol_tree[0] + children_names = {child["name"] for child in root.get("children", [])} + + # Build and dependency directories should not appear + ignored_dirs = {"_build", "deps", ".elixir_ls", "cover", "node_modules"} + found_ignored = ignored_dirs.intersection(children_names) + assert len(found_ignored) == 0, f"Found ignored directories in symbol tree: {found_ignored}" + + # Important directories should appear + important_dirs = {"lib", "test"} + found_important = important_dirs.intersection(children_names) + assert len(found_important) > 0, f"Expected to find important directories: {important_dirs}, got: {children_names}" diff --git a/test/solidlsp/elixir/test_elixir_integration.py b/test/solidlsp/elixir/test_elixir_integration.py new file mode 100644 index 00000000..25c86a8f --- /dev/null +++ b/test/solidlsp/elixir/test_elixir_integration.py @@ -0,0 +1,157 @@ +""" +Integration tests for Elixir language server with test repository. + +These tests verify that the language server works correctly with a real Elixir project +and can perform advanced operations like cross-file symbol resolution. +""" + +import os +from pathlib import Path + +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language + +from . import NEXTLS_UNAVAILABLE, NEXTLS_UNAVAILABLE_REASON + +# These marks will be applied to all tests in this module +pytestmark = [pytest.mark.elixir, pytest.mark.skipif(NEXTLS_UNAVAILABLE, reason=f"Next LS not available: {NEXTLS_UNAVAILABLE_REASON}")] + + +class TestElixirIntegration: + """Integration tests for Elixir language server with test repository.""" + + @pytest.fixture + def elixir_test_repo_path(self): + """Get the path to the Elixir test repository.""" + test_dir = Path(__file__).parent.parent.parent + return str(test_dir / "resources" / "repos" / "elixir" / "test_repo") + + def test_elixir_repo_structure(self, elixir_test_repo_path): + """Test that the Elixir test repository has the expected structure.""" + repo_path = Path(elixir_test_repo_path) + + # Check that key files exist + assert (repo_path / "mix.exs").exists(), "mix.exs should exist" + assert (repo_path / "lib" / "test_repo.ex").exists(), "main module should exist" + assert (repo_path / "lib" / "utils.ex").exists(), "utils module should exist" + assert (repo_path / "lib" / "models.ex").exists(), "models module should exist" + assert (repo_path / "lib" / "services.ex").exists(), "services module should exist" + assert (repo_path / "lib" / "examples.ex").exists(), "examples module should exist" + assert (repo_path / "test" / "test_repo_test.exs").exists(), "test file should exist" + assert (repo_path / "test" / "models_test.exs").exists(), "models test should exist" + + @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) + def test_cross_file_symbol_resolution(self, language_server: SolidLanguageServer): + """Test that symbols can be resolved across different files.""" + # Test that User struct from models.ex can be found when referenced in services.ex + services_file = os.path.join("lib", "services.ex") + + # Find where User is referenced in services.ex + content = language_server.retrieve_full_file_content(services_file) + lines = content.split("\n") + user_reference_line = None + for i, line in enumerate(lines): + if "alias TestRepo.Models.{User" in line: + user_reference_line = i + break + + if user_reference_line is None: + pytest.skip("Could not find User reference in services.ex") + + # Try to find the definition + defining_symbol = language_server.request_defining_symbol(services_file, user_reference_line, 30) + + if defining_symbol and "location" in defining_symbol: + # Should point to models.ex + assert "models.ex" in defining_symbol["location"]["uri"] + + @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) + def test_comprehensive_symbol_search(self, language_server: SolidLanguageServer): + """Test comprehensive symbol search across the entire project.""" + # Search for all function definitions + function_pattern = r"def\s+\w+\s*[\(\s]" + function_matches = language_server.search_files_for_pattern(function_pattern) + + # Should find functions across multiple files + if function_matches: + files_with_functions = set() + for match in function_matches: + if match.source_file_path: + files_with_functions.add(os.path.basename(match.source_file_path)) + + # Should find functions in multiple files + expected_files = {"models.ex", "services.ex", "examples.ex", "utils.ex", "test_repo.ex"} + found_files = expected_files.intersection(files_with_functions) + assert len(found_files) > 0, f"Expected functions in {expected_files}, found in {files_with_functions}" + + # Search for struct definitions + struct_pattern = r"defstruct\s+\[" + struct_matches = language_server.search_files_for_pattern(struct_pattern) + + if struct_matches: + # Should find structs primarily in models.ex + models_structs = [m for m in struct_matches if m.source_file_path and "models.ex" in m.source_file_path] + assert len(models_structs) > 0, "Should find struct definitions in models.ex" + + @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) + def test_module_hierarchy_understanding(self, language_server: SolidLanguageServer): + """Test that the language server understands Elixir module hierarchy.""" + models_file = os.path.join("lib", "models.ex") + symbols = language_server.request_document_symbols(models_file) + + if symbols: + # Flatten symbol structure + all_symbols = [] + for symbol_group in symbols: + if isinstance(symbol_group, list): + all_symbols.extend(symbol_group) + else: + all_symbols.append(symbol_group) + + symbol_names = [s.get("name", "") for s in all_symbols] + + # Should understand nested module structure + expected_modules = ["TestRepo.Models", "User", "Item", "Order"] + found_modules = [name for name in expected_modules if any(name in symbol_name for symbol_name in symbol_names)] + assert len(found_modules) > 0, f"Expected modules {expected_modules}, found symbols {symbol_names}" + + def test_file_extension_matching(self): + """Test that the Elixir language recognizes the correct file extensions.""" + language = Language.ELIXIR + matcher = language.get_source_fn_matcher() + + # Test Elixir file extensions + assert matcher.is_relevant_filename("lib/test_repo.ex") + assert matcher.is_relevant_filename("test/test_repo_test.exs") + assert matcher.is_relevant_filename("config/config.exs") + assert matcher.is_relevant_filename("mix.exs") + assert matcher.is_relevant_filename("lib/models.ex") + assert matcher.is_relevant_filename("lib/services.ex") + + # Test non-Elixir files + assert not matcher.is_relevant_filename("README.md") + assert not matcher.is_relevant_filename("lib/test_repo.py") + assert not matcher.is_relevant_filename("package.json") + assert not matcher.is_relevant_filename("Cargo.toml") + + @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) + def test_protocol_and_implementation_understanding(self, language_server: SolidLanguageServer): + """Test that the language server understands Elixir protocols and implementations.""" + # Search for protocol definitions + protocol_pattern = r"defprotocol\s+\w+" + protocol_matches = language_server.search_files_for_pattern(protocol_pattern, paths_include_glob="**/models.ex") + + if protocol_matches: + # Should find the Serializable protocol + serializable_matches = [m for m in protocol_matches if "Serializable" in str(m)] + assert len(serializable_matches) > 0, "Should find Serializable protocol definition" + + # Search for protocol implementations + impl_pattern = r"defimpl\s+\w+" + impl_matches = language_server.search_files_for_pattern(impl_pattern, paths_include_glob="**/models.ex") + + if impl_matches: + # Should find multiple implementations + assert len(impl_matches) >= 3, f"Should find at least 3 protocol implementations, found {len(impl_matches)}" diff --git a/test/solidlsp/elixir/test_elixir_symbol_retrieval.py b/test/solidlsp/elixir/test_elixir_symbol_retrieval.py new file mode 100644 index 00000000..a6c31b6d --- /dev/null +++ b/test/solidlsp/elixir/test_elixir_symbol_retrieval.py @@ -0,0 +1,342 @@ +""" +Tests for the Elixir language server symbol-related functionality. + +These tests focus on the following methods: +- request_containing_symbol +- request_referencing_symbols +- request_defining_symbol +""" + +import os + +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from solidlsp.ls_types import SymbolKind + +from . import NEXTLS_UNAVAILABLE, NEXTLS_UNAVAILABLE_REASON + +# These marks will be applied to all tests in this module +pytestmark = [pytest.mark.elixir, pytest.mark.skipif(NEXTLS_UNAVAILABLE, reason=f"Next LS not available: {NEXTLS_UNAVAILABLE_REASON}")] + + +class TestElixirLanguageServerSymbols: + """Test the Elixir language server's symbol-related functionality.""" + + @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) + def test_request_containing_symbol_function(self, language_server: SolidLanguageServer) -> None: + """Test request_containing_symbol for a function.""" + # Test for a position inside the create_user function + file_path = os.path.join("lib", "services.ex") + + # Find the create_user function in the file + content = language_server.retrieve_full_file_content(file_path) + lines = content.split("\n") + create_user_line = None + for i, line in enumerate(lines): + if "def create_user(" in line: + create_user_line = i + 2 # Go inside the function body + break + + if create_user_line is None: + pytest.skip("Could not find create_user function") + + containing_symbol = language_server.request_containing_symbol(file_path, create_user_line, 10, include_body=True) + + # Verify that we found the containing symbol + if containing_symbol: + # Next LS returns the full function signature instead of just the function name + assert containing_symbol["name"] == "def create_user(pid, id, name, email, roles \\\\ [])" + assert containing_symbol["kind"] == SymbolKind.Method or containing_symbol["kind"] == SymbolKind.Function + if "body" in containing_symbol: + assert "def create_user" in containing_symbol["body"] + + @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) + def test_request_containing_symbol_module(self, language_server: SolidLanguageServer) -> None: + """Test request_containing_symbol for a module.""" + # Test for a position inside the UserService module but outside any function + file_path = os.path.join("lib", "services.ex") + + # Find the UserService module definition + content = language_server.retrieve_full_file_content(file_path) + lines = content.split("\n") + user_service_line = None + for i, line in enumerate(lines): + if "defmodule UserService do" in line: + user_service_line = i + 1 # Go inside the module + break + + if user_service_line is None: + pytest.skip("Could not find UserService module") + + containing_symbol = language_server.request_containing_symbol(file_path, user_service_line, 5) + + # Verify that we found the containing symbol + if containing_symbol: + assert "UserService" in containing_symbol["name"] + assert containing_symbol["kind"] == SymbolKind.Module or containing_symbol["kind"] == SymbolKind.Class + + @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) + def test_request_containing_symbol_nested(self, language_server: SolidLanguageServer) -> None: + """Test request_containing_symbol with nested scopes.""" + # Test for a position inside a function which is inside a module + file_path = os.path.join("lib", "services.ex") + + # Find a function inside UserService + content = language_server.retrieve_full_file_content(file_path) + lines = content.split("\n") + function_body_line = None + for i, line in enumerate(lines): + if "def create_user(" in line: + function_body_line = i + 3 # Go deeper into the function body + break + + if function_body_line is None: + pytest.skip("Could not find function body") + + containing_symbol = language_server.request_containing_symbol(file_path, function_body_line, 15) + + # Verify that we found the innermost containing symbol (the function) + if containing_symbol: + expected_names = ["create_user", "UserService"] + assert any(name in containing_symbol["name"] for name in expected_names) + + @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) + def test_request_containing_symbol_none(self, language_server: SolidLanguageServer) -> None: + """Test request_containing_symbol for a position with no containing symbol.""" + # Test for a position outside any function/module (e.g., in module doc) + file_path = os.path.join("lib", "services.ex") + # Line 1-3 are likely in module documentation or imports + containing_symbol = language_server.request_containing_symbol(file_path, 2, 10) + + # Should return None or an empty dictionary, or the top-level module + # This is acceptable behavior for module-level positions + assert containing_symbol is None or containing_symbol == {} or "TestRepo.Services" in str(containing_symbol) + + @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) + def test_request_referencing_symbols_struct(self, language_server: SolidLanguageServer) -> None: + """Test request_referencing_symbols for a struct.""" + # Test referencing symbols for User struct + file_path = os.path.join("lib", "models.ex") + + symbols = language_server.request_document_symbols(file_path) + user_symbol = None + for symbol_group in symbols: + user_symbol = next((s for s in symbol_group if "User" in s.get("name", "")), None) + if user_symbol: + break + + if not user_symbol or "selectionRange" not in user_symbol: + pytest.skip("User symbol or its selectionRange not found") + + sel_start = user_symbol["selectionRange"]["start"] + ref_symbols = [ + ref.symbol for ref in language_server.request_referencing_symbols(file_path, sel_start["line"], sel_start["character"]) + ] + + if ref_symbols: + services_references = [ + symbol + for symbol in ref_symbols + if "location" in symbol and "uri" in symbol["location"] and "services.ex" in symbol["location"]["uri"] + ] + # We expect some references from services.ex + assert len(services_references) >= 0 # At least attempt to find references + + @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) + def test_request_referencing_symbols_none(self, language_server: SolidLanguageServer) -> None: + """Test request_referencing_symbols for a position with no symbol.""" + file_path = os.path.join("lib", "services.ex") + # Line 3 is likely a blank line or comment + try: + ref_symbols = [ref.symbol for ref in language_server.request_referencing_symbols(file_path, 3, 0)] + # If we get here, make sure we got an empty result + assert ref_symbols == [] or ref_symbols is None + except Exception: + # The method might raise an exception for invalid positions + # which is acceptable behavior + pass + + # Tests for request_defining_symbol + @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) + def test_request_defining_symbol_function_call(self, language_server: SolidLanguageServer) -> None: + """Test request_defining_symbol for a function call.""" + # Find a place where User.new is called in services.ex + file_path = os.path.join("lib", "services.ex") + content = language_server.retrieve_full_file_content(file_path) + lines = content.split("\n") + user_new_call_line = None + for i, line in enumerate(lines): + if "User.new(" in line: + user_new_call_line = i + break + + if user_new_call_line is None: + pytest.skip("Could not find User.new call") + + # Try to find the definition of User.new + defining_symbol = language_server.request_defining_symbol(file_path, user_new_call_line, 15) + + if defining_symbol: + assert defining_symbol.get("name") == "new" or "User" in defining_symbol.get("name", "") + if "location" in defining_symbol and "uri" in defining_symbol["location"]: + assert "models.ex" in defining_symbol["location"]["uri"] + + @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) + def test_request_defining_symbol_struct_usage(self, language_server: SolidLanguageServer) -> None: + """Test request_defining_symbol for a struct usage.""" + # Find a place where User struct is used in services.ex + file_path = os.path.join("lib", "services.ex") + content = language_server.retrieve_full_file_content(file_path) + lines = content.split("\n") + user_usage_line = None + for i, line in enumerate(lines): + if "alias TestRepo.Models.{User" in line: + user_usage_line = i + break + + if user_usage_line is None: + pytest.skip("Could not find User struct usage") + + defining_symbol = language_server.request_defining_symbol(file_path, user_usage_line, 30) + + if defining_symbol: + assert "User" in defining_symbol.get("name", "") + + @pytest.mark.xfail( + reason="Known intermittent bug in Next LS v0.23.3: Protocol.UndefinedError for :timeout atom. " + "Occurs in CI environments but may pass locally. " + "See https://github.com/elixir-tools/next-ls/issues/543", + strict=False, + ) + @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) + def test_request_defining_symbol_none(self, language_server: SolidLanguageServer) -> None: + """Test request_defining_symbol for a position with no symbol.""" + # Test for a position with no symbol (e.g., whitespace or comment) + file_path = os.path.join("lib", "services.ex") + # Line 3 is likely a blank line + defining_symbol = language_server.request_defining_symbol(file_path, 3, 0) + + # Should return None or empty + assert defining_symbol is None or defining_symbol == {} + + @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) + def test_symbol_methods_integration(self, language_server: SolidLanguageServer) -> None: + """Test integration between different symbol methods.""" + file_path = os.path.join("lib", "models.ex") + + # Find User struct definition + content = language_server.retrieve_full_file_content(file_path) + lines = content.split("\n") + user_struct_line = None + for i, line in enumerate(lines): + if "defmodule User do" in line: + user_struct_line = i + break + + if user_struct_line is None: + pytest.skip("Could not find User struct") + + # Test containing symbol + containing = language_server.request_containing_symbol(file_path, user_struct_line + 5, 10) + + if containing: + # Test that we can find references to this symbol + if "location" in containing and "range" in containing["location"]: + start_pos = containing["location"]["range"]["start"] + refs = [ + ref.symbol for ref in language_server.request_referencing_symbols(file_path, start_pos["line"], start_pos["character"]) + ] + # We should find some references or none (both are valid outcomes) + assert isinstance(refs, list) + + @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) + def test_symbol_tree_structure(self, language_server: SolidLanguageServer) -> None: + """Test that symbol tree structure is correctly built.""" + symbol_tree = language_server.request_full_symbol_tree() + + # Should get a tree structure + assert len(symbol_tree) > 0 + + # Should have our test repository structure + root = symbol_tree[0] + assert "children" in root + + # Look for lib directory + lib_dir = None + for child in root["children"]: + if child["name"] == "lib": + lib_dir = child + break + + if lib_dir: + # Next LS returns module names instead of file names (e.g., 'services' instead of 'services.ex') + file_names = [child["name"] for child in lib_dir.get("children", [])] + expected_modules = ["models", "services", "examples", "utils", "test_repo"] + found_modules = [name for name in expected_modules if name in file_names] + assert len(found_modules) > 0, f"Expected to find some modules from {expected_modules}, but got {file_names}" + + @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) + def test_request_dir_overview(self, language_server: SolidLanguageServer) -> None: + """Test request_dir_overview functionality.""" + lib_overview = language_server.request_dir_overview("lib") + + # Should get an overview of the lib directory + assert lib_overview is not None + # Next LS returns keys like 'lib/services.ex' instead of just 'lib' + overview_keys = list(lib_overview.keys()) if hasattr(lib_overview, "keys") else [] + lib_files = [key for key in overview_keys if key.startswith("lib/")] + assert len(lib_files) > 0, f"Expected to find lib/ files in overview keys: {overview_keys}" + + # Should contain information about our modules + overview_text = str(lib_overview).lower() + expected_terms = ["models", "services", "user", "item"] + found_terms = [term for term in expected_terms if term in overview_text] + assert len(found_terms) > 0, f"Expected to find some terms from {expected_terms} in overview" + + # @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) + # def test_request_document_overview(self, language_server: SolidLanguageServer) -> None: + # """Test request_document_overview functionality.""" + # # COMMENTED OUT: Next LS document overview doesn't contain expected terms + # # Next LS return value: [('TestRepo.Models', 2, 0, 0)] - only module info, no detailed content + # # Expected terms like 'user', 'item', 'order', 'struct', 'defmodule' are not present + # # This appears to be a limitation of Next LS document overview functionality + # # + # file_path = os.path.join("lib", "models.ex") + # doc_overview = language_server.request_document_overview(file_path) + # + # # Should get an overview of the models.ex file + # assert doc_overview is not None + # + # # Should contain information about our structs and functions + # overview_text = str(doc_overview).lower() + # expected_terms = ["user", "item", "order", "struct", "defmodule"] + # found_terms = [term for term in expected_terms if term in overview_text] + # assert len(found_terms) > 0, f"Expected to find some terms from {expected_terms} in overview" + + @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) + def test_containing_symbol_of_module_attribute(self, language_server: SolidLanguageServer) -> None: + """Test containing symbol for module attributes.""" + file_path = os.path.join("lib", "models.ex") + + # Find a module attribute like @type or @doc + content = language_server.retrieve_full_file_content(file_path) + lines = content.split("\n") + attribute_line = None + for i, line in enumerate(lines): + if line.strip().startswith("@type") or line.strip().startswith("@doc"): + attribute_line = i + break + + if attribute_line is None: + pytest.skip("Could not find module attribute") + + containing_symbol = language_server.request_containing_symbol(file_path, attribute_line, 5) + + if containing_symbol: + # Should be contained within a module + assert "name" in containing_symbol + # The containing symbol should be a module + expected_names = ["User", "Item", "Order", "TestRepo.Models"] + assert any(name in containing_symbol["name"] for name in expected_names) diff --git a/test/solidlsp/terraform/test_terraform_basic.py b/test/solidlsp/terraform/test_terraform_basic.py new file mode 100644 index 00000000..ff20ac66 --- /dev/null +++ b/test/solidlsp/terraform/test_terraform_basic.py @@ -0,0 +1,53 @@ +""" +Basic integration tests for the Terraform language server functionality. + +These tests validate the functionality of the language server APIs +like request_references using the test repository. +""" + +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language + + +@pytest.mark.terraform +class TestLanguageServerBasics: + """Test basic functionality of the Terraform language server.""" + + @pytest.mark.parametrize("language_server", [Language.TERRAFORM], indirect=True) + def test_basic_definition(self, language_server: SolidLanguageServer) -> None: + """Test basic definition lookup functionality.""" + # Simple test to verify the language server is working + file_path = "main.tf" + # Just try to get document symbols - this should work without hanging + symbols = language_server.request_document_symbols(file_path) + assert len(symbols) > 0, "Should find at least some symbols in main.tf" + + @pytest.mark.parametrize("language_server", [Language.TERRAFORM], indirect=True) + def test_request_references_aws_instance(self, language_server: SolidLanguageServer) -> None: + """Test request_references on an aws_instance resource.""" + # Get references to an aws_instance resource in main.tf + file_path = "main.tf" + # Find aws_instance resources + symbols = language_server.request_document_symbols(file_path) + aws_instance_symbol = next((s for s in symbols[0] if s.get("name") == 'resource "aws_instance" "web_server"'), None) + if not aws_instance_symbol or "selectionRange" not in aws_instance_symbol: + raise AssertionError("aws_instance symbol or its selectionRange not found") + sel_start = aws_instance_symbol["selectionRange"]["start"] + references = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + assert len(references) >= 1, "aws_instance should be referenced at least once" + + @pytest.mark.parametrize("language_server", [Language.TERRAFORM], indirect=True) + def test_request_references_variable(self, language_server: SolidLanguageServer) -> None: + """Test request_references on a variable.""" + # Get references to a variable in variables.tf + file_path = "variables.tf" + # Find variable definitions + symbols = language_server.request_document_symbols(file_path) + var_symbol = next((s for s in symbols[0] if s.get("name") == 'variable "instance_type"'), None) + if not var_symbol or "selectionRange" not in var_symbol: + raise AssertionError("variable symbol or its selectionRange not found") + sel_start = var_symbol["selectionRange"]["start"] + references = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + assert len(references) >= 1, "variable should be referenced at least once"