diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index b99bf7ab..0ab82056 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -102,6 +102,10 @@ jobs: swiftly use 6.1.2 echo "~/.swiftly/bin" >> $GITHUB_PATH echo "Swiftly installed successfully" + # Verify sourcekit-lsp is working before proceeding + echo "=== Verifying sourcekit-lsp installation ===" + which sourcekit-lsp || echo "Warning: sourcekit-lsp not found in PATH" + sourcekit-lsp --help || echo "Warning: sourcekit-lsp not responding" - name: Install Swift with swiftly (Ubuntu) if: runner.os == 'Linux' run: | @@ -119,6 +123,10 @@ jobs: echo "=== Adding Swift toolchain to PATH ===" echo "$HOME/.local/share/swiftly/bin" >> $GITHUB_PATH echo "Swiftly installed successfully!" + # Verify sourcekit-lsp is working before proceeding + echo "=== Verifying sourcekit-lsp installation ===" + which sourcekit-lsp || echo "Warning: sourcekit-lsp not found in PATH" + sourcekit-lsp --help || echo "Warning: sourcekit-lsp not responding" - name: Install Ruby uses: ruby/setup-ruby@v1 with: diff --git a/src/serena/project.py b/src/serena/project.py index 9288209a..0673f05e 100644 --- a/src/serena/project.py +++ b/src/serena/project.py @@ -309,6 +309,6 @@ class Project: solidlsp_settings=SolidLSPSettings( solidlsp_dir=SERENA_MANAGED_DIR_IN_HOME, project_data_relative_path=SERENA_MANAGED_DIR_NAME, - ls_specific_settings=ls_specific_settings, + ls_specific_settings=ls_specific_settings or {}, ), ) diff --git a/src/serena/resources/serena_config.template.yml b/src/serena/resources/serena_config.template.yml index 082c0c32..cb4373e0 100644 --- a/src/serena/resources/serena_config.template.yml +++ b/src/serena/resources/serena_config.template.yml @@ -37,8 +37,9 @@ ls_specific_settings: {} # Added on 23.08.2025 # Advanced configuration option allowing to configure language server implementation specific options. Maps the language # (same entry as in project.yml) to the options. -# Have a look at the docstring of the constructors of the LS implementations within solidlsp to see which options are available. -# No documentation on options means no options are available +# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available. +# No documentation on options means no options are available. +# tool_timeout: 240 # timeout, in seconds, after which tool executions are terminated diff --git a/src/serena/symbol.py b/src/serena/symbol.py index 1322d45d..9d89c5fb 100644 --- a/src/serena/symbol.py +++ b/src/serena/symbol.py @@ -485,14 +485,14 @@ class LanguageServerSymbolRetriever: return symbols def get_document_symbols(self, relative_path: str) -> list[LanguageServerSymbol]: - symbol_dicts, roots = self._lang_server.request_document_symbols(relative_path, include_body=False) + symbol_dicts, _roots = self._lang_server.request_document_symbols(relative_path, include_body=False) symbols = [LanguageServerSymbol(s) for s in symbol_dicts] return symbols def find_by_location(self, location: LanguageServerSymbolLocation) -> LanguageServerSymbol | None: if location.relative_path is None: return None - symbol_dicts, roots = self._lang_server.request_document_symbols(location.relative_path, include_body=False) + symbol_dicts, _roots = self._lang_server.request_document_symbols(location.relative_path, include_body=False) for symbol_dict in symbol_dicts: symbol = LanguageServerSymbol(symbol_dict) if symbol.location == location: diff --git a/src/serena/tools/file_tools.py b/src/serena/tools/file_tools.py index e5b1467a..58f9520d 100644 --- a/src/serena/tools/file_tools.py +++ b/src/serena/tools/file_tools.py @@ -145,7 +145,7 @@ class FindFileTool(Tool): filename = os.path.basename(abs_path) return not fnmatch(filename, file_mask) - dirs, files = scan_directory( + _dirs, files = scan_directory( path=dir_to_scan, recursive=True, is_ignored_dir=self.project.is_ignored_path, @@ -378,7 +378,7 @@ class SearchForPatternTool(Tool): if os.path.isfile(abs_path): rel_paths_to_search = [relative_path] else: - dirs, rel_paths_to_search = scan_directory( + _dirs, rel_paths_to_search = scan_directory( path=abs_path, recursive=True, is_ignored_dir=self.project.is_ignored_path, diff --git a/src/solidlsp/language_servers/clangd_language_server.py b/src/solidlsp/language_servers/clangd_language_server.py index aeba0a0b..6f1426be 100644 --- a/src/solidlsp/language_servers/clangd_language_server.py +++ b/src/solidlsp/language_servers/clangd_language_server.py @@ -89,7 +89,7 @@ class ClangdLanguageServer(SolidLanguageServer): ) clangd_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "clangd") - dep = deps.single_for_current_platform() + dep = deps.get_single_dep_for_current_platform() clangd_executable_path = deps.binary_path(clangd_ls_dir) if not os.path.exists(clangd_executable_path): logger.log( diff --git a/src/solidlsp/language_servers/clojure_lsp.py b/src/solidlsp/language_servers/clojure_lsp.py index 9e52f6d2..36583e85 100644 --- a/src/solidlsp/language_servers/clojure_lsp.py +++ b/src/solidlsp/language_servers/clojure_lsp.py @@ -112,7 +112,7 @@ class ClojureLSP(SolidLanguageServer): """Setup runtime dependencies for clojure-lsp and return the command to start the server.""" verify_clojure_cli() deps = ClojureLSP.runtime_dependencies - dependency = deps.single_for_current_platform() + dependency = deps.get_single_dep_for_current_platform() clojurelsp_ls_dir = cls.ls_resources_dir(solidlsp_settings) clojurelsp_executable_path = deps.binary_path(clojurelsp_ls_dir) diff --git a/src/solidlsp/language_servers/common.py b/src/solidlsp/language_servers/common.py index e606e41d..a6bcbb3e 100644 --- a/src/solidlsp/language_servers/common.py +++ b/src/solidlsp/language_servers/common.py @@ -4,8 +4,9 @@ import logging import os import platform import subprocess -from collections.abc import Sequence -from dataclasses import dataclass +from collections.abc import Iterable, Mapping, Sequence +from dataclasses import dataclass, replace +from typing import Any, cast from solidlsp.ls_logger import LanguageServerLogger from solidlsp.ls_utils import FileUtils, PlatformUtils @@ -33,23 +34,47 @@ class RuntimeDependency: class RuntimeDependencyCollection: """Utility to handle installation of runtime dependencies.""" - def __init__(self, dependencies: Sequence[RuntimeDependency]): - self._dependencies = list(dependencies) + def __init__(self, dependencies: Sequence[RuntimeDependency], overrides: Iterable[Mapping[str, Any]] = ()) -> None: + """Initialize the collection with a list of dependencies and optional overrides. - 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)] + :param dependencies: List of base RuntimeDependency instances. The combination of 'id' and 'platform_id' must be unique. + :param overrides: List of dictionaries which represent overrides or additions to the base dependencies. + Each entry must contain at least the 'id' key, and optionally 'platform_id' to uniquely identify the dependency to override. + """ + self._id_and_platform_id_to_dep: dict[tuple[str, str | None], RuntimeDependency] = {} + for dep in dependencies: + dep_key = (dep.id, dep.platform_id) + if dep_key in self._id_and_platform_id_to_dep: + raise ValueError(f"Duplicate runtime dependency with id '{dep.id}' and platform_id '{dep.platform_id}':\n{dep}") + self._id_and_platform_id_to_dep[dep_key] = dep - def for_current_platform(self) -> list[RuntimeDependency]: - return self.for_platform(PlatformUtils.get_platform_id().value) + for dep_values_override in overrides: + override_key = cast(tuple[str, str | None], (dep_values_override["id"], dep_values_override.get("platform_id"))) + base_dep = self._id_and_platform_id_to_dep.get(override_key) + if base_dep is None: + new_runtime_dep = RuntimeDependency(**dep_values_override) + self._id_and_platform_id_to_dep[override_key] = new_runtime_dep + else: + self._id_and_platform_id_to_dep[override_key] = replace(base_dep, **dep_values_override) - def single_for_current_platform(self) -> RuntimeDependency: - deps = self.for_current_platform() + def get_dependencies_for_platform(self, platform_id: str) -> list[RuntimeDependency]: + return [d for d in self._id_and_platform_id_to_dep.values() if d.platform_id in (platform_id, "any", "platform-agnostic", None)] + + def get_dependencies_for_current_platform(self) -> list[RuntimeDependency]: + return self.get_dependencies_for_platform(PlatformUtils.get_platform_id().value) + + def get_single_dep_for_current_platform(self, dependency_id: str | None = None) -> RuntimeDependency: + deps = self.get_dependencies_for_current_platform() + if dependency_id is not None: + deps = [d for d in deps if d.id == dependency_id] if len(deps) != 1: - raise RuntimeError(f"Expected exactly one runtime dependency for {PlatformUtils.get_platform_id().value}, found {len(deps)}") + raise RuntimeError( + f"Expected exactly one runtime dependency for platform-{PlatformUtils.get_platform_id().value} and {dependency_id=}, found {len(deps)}" + ) return deps[0] def binary_path(self, target_dir: str) -> str: - dep = self.single_for_current_platform() + dep = self.get_single_dep_for_current_platform() if not dep.binary_name: return target_dir return os.path.join(target_dir, dep.binary_name) @@ -61,7 +86,7 @@ class RuntimeDependencyCollection: """ os.makedirs(target_dir, exist_ok=True) results: dict[str, str] = {} - for dep in self.for_current_platform(): + for dep in self.get_dependencies_for_current_platform(): if dep.url: self._install_from_url(dep, logger, target_dir) if dep.command: @@ -107,6 +132,7 @@ class RuntimeDependencyCollection: @staticmethod def _install_from_url(dep: RuntimeDependency, logger: LanguageServerLogger, target_dir: str) -> None: + assert dep.url is not 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) diff --git a/src/solidlsp/language_servers/csharp_language_server.py b/src/solidlsp/language_servers/csharp_language_server.py index e5d7fa1c..d5531fd2 100644 --- a/src/solidlsp/language_servers/csharp_language_server.py +++ b/src/solidlsp/language_servers/csharp_language_server.py @@ -13,7 +13,7 @@ import threading import urllib.request import zipfile from pathlib import Path -from typing import cast +from typing import Any, cast from overrides import override @@ -27,10 +27,9 @@ from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings from solidlsp.util.zip import SafeZipExtractor -from .common import RuntimeDependency +from .common import RuntimeDependency, RuntimeDependencyCollection -# Runtime dependencies configuration -RUNTIME_DEPENDENCIES = [ +_RUNTIME_DEPENDENCIES = [ RuntimeDependency( id="CSharpLanguageServer", description="Microsoft.CodeAnalysis.LanguageServer for Windows (x64)", @@ -107,6 +106,14 @@ RUNTIME_DEPENDENCIES = [ archive_type="tar.gz", binary_name="dotnet", ), + RuntimeDependency( + id="DotNetRuntime", + description=".NET 9 Runtime for Linux (ARM64)", + url="https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-linux-arm64.tar.gz", + platform_id="linux-arm64", + archive_type="tar.gz", + binary_name="dotnet", + ), RuntimeDependency( id="DotNetRuntime", description=".NET 9 Runtime for macOS (x64)", @@ -172,11 +179,22 @@ def find_solution_or_project_file(root_dir) -> str | None: class CSharpLanguageServer(SolidLanguageServer): """ - Provides C# specific instantiation of the LanguageServer class using Microsoft.CodeAnalysis.LanguageServer. - This is the official Roslyn-based language server from Microsoft. + Provides C# specific instantiation of the LanguageServer class using `Microsoft.CodeAnalysis.LanguageServer`, + the official Roslyn-based language server from Microsoft. - You can pass the following entries in ls_specific_settings["csharp"]: - - dotnet_runtime_url: will override the URL from RUNTIME_DEPENDENCIES + You can pass a list of runtime dependency overrides in ls_specific_settings["csharp"]. This is a list of + dicts, each containing at least the "id" key, and optionally "platform_id" to uniquely identify the dependency to override. + For example, to override the URL of the .NET runtime on windows-x64, add the entry: + + ``` + { + "id": "DotNetRuntime", + "platform_id": "win-x64", + "url": "https://example.com/custom-dotnet-runtime.zip" + } + ``` + + See the `_RUNTIME_DEPENDENCIES` variable above for the available dependency ids and platform_ids. """ def __init__( @@ -229,52 +247,43 @@ class CSharpLanguageServer(SolidLanguageServer): Ensure .NET runtime and Microsoft.CodeAnalysis.LanguageServer are available. Returns a tuple of (dotnet_path, language_server_dll_path). """ - runtime_id = CSharpLanguageServer._get_runtime_id() - lang_server_dep, dotnet_runtime_dep = CSharpLanguageServer._get_runtime_dependencies(runtime_id) + language_enum = cls.get_language_enum_instance() + ls_specific_settings = solidlsp_settings.ls_specific_settings or {} + language_specific_config = ls_specific_settings.get(language_enum, {}) + runtime_dependency_overrides = cast(list[dict[str, Any]], language_specific_config.get("runtime_dependencies", [])) + + logger.log("Resolving runtime dependencies", logging.DEBUG) + if language_specific_config: + logger.log( + f"Language-specific config for {language_enum}: {language_specific_config}", + logging.DEBUG, + ) + + runtime_dependencies = RuntimeDependencyCollection( + _RUNTIME_DEPENDENCIES, + overrides=runtime_dependency_overrides, + ) + + logger.log( + f"Available runtime dependencies: {runtime_dependencies.get_dependencies_for_current_platform}", + logging.DEBUG, + ) + + # Find the dependencies for our platform + lang_server_dep = runtime_dependencies.get_single_dep_for_current_platform("CSharpLanguageServer") + dotnet_runtime_dep = runtime_dependencies.get_single_dep_for_current_platform("DotNetRuntime") dotnet_path = CSharpLanguageServer._ensure_dotnet_runtime(logger, dotnet_runtime_dep, solidlsp_settings) server_dll_path = CSharpLanguageServer._ensure_language_server(logger, lang_server_dep, solidlsp_settings) return dotnet_path, server_dll_path - @staticmethod - def _get_runtime_id() -> str: - """Determine the runtime ID based on the platform.""" - system = platform.system().lower() - machine = platform.machine().lower() - - if system == "windows": - return "win-x64" if machine in ["amd64", "x86_64"] else "win-arm64" - elif system == "darwin": - return "osx-x64" if machine in ["x86_64"] else "osx-arm64" - elif system == "linux": - return "linux-x64" if machine in ["x86_64", "amd64"] else "linux-arm64" - else: - raise SolidLSPException(f"Unsupported platform: {system} {machine}") - - @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 - - for dep in RUNTIME_DEPENDENCIES: - if dep.id == "CSharpLanguageServer" and dep.platform_id == runtime_id: - lang_server_dep = dep - elif dep.id == "DotNetRuntime" and dep.platform_id == runtime_id: - dotnet_runtime_dep = dep - - if not lang_server_dep: - raise SolidLSPException(f"No C# language server dependency found for platform {runtime_id}") - if not dotnet_runtime_dep: - raise SolidLSPException(f"No .NET runtime dependency found for platform {runtime_id}") - - return lang_server_dep, dotnet_runtime_dep - @classmethod def _ensure_dotnet_runtime( cls, logger: LanguageServerLogger, dotnet_runtime_dep: RuntimeDependency, solidlsp_settings: SolidLSPSettings ) -> str: """Ensure .NET runtime is available and return the dotnet executable path.""" + # TODO: use RuntimeDependency util methods instead of custom validation/download logic + # Check if dotnet is already available on the system system_dotnet = shutil.which("dotnet") if system_dotnet: @@ -413,6 +422,8 @@ class CSharpLanguageServer(SolidLanguageServer): Ensure .NET 9 runtime is available using runtime dependency configuration. Returns the path to the dotnet executable. """ + # TODO: use RuntimeDependency util methods instead of custom download logic + # Check if dotnet is already available on the system system_dotnet = shutil.which("dotnet") if system_dotnet: diff --git a/src/solidlsp/language_servers/sourcekit_lsp.py b/src/solidlsp/language_servers/sourcekit_lsp.py index 4a47ffb9..2c7ba472 100644 --- a/src/solidlsp/language_servers/sourcekit_lsp.py +++ b/src/solidlsp/language_servers/sourcekit_lsp.py @@ -62,6 +62,7 @@ class SourceKitLSP(SolidLanguageServer): self.server_ready = threading.Event() self.request_id = 0 self._did_sleep_before_requesting_references = False + self._initialization_timestamp = None @staticmethod def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: @@ -345,14 +346,40 @@ class SourceKitLSP(SolidLanguageServer): self.server_ready.set() self.server_ready.wait() + # Mark initialization timestamp for smarter delay calculation + self._initialization_timestamp = time.time() + @override def request_references(self, relative_file_path: str, line: int, column: int) -> list[ls_types.Location]: - # SourceKit LSP needs a short initialization period after startup + # SourceKit LSP needs initialization + indexing time after startup # before it can provide accurate reference information. This sleep # prevents race conditions where references might not be available yet. - # Unfortunately, sourcekit doesn't send a signal when it's really ready + # CI environments need extra time for project indexing and cross-file analysis if not self._did_sleep_before_requesting_references: - self.logger.log("Sleeping 5s before requesting references for the first time", logging.DEBUG) - time.sleep(5) + # Calculate minimum delay based on how much time has passed since initialization + if self._initialization_timestamp: + elapsed = time.time() - self._initialization_timestamp + # Increased CI delay for project indexing: 15s CI, 5s local + base_delay = 15 if os.getenv("CI") else 5 + remaining_delay = max(2, base_delay - elapsed) + else: + # Fallback if initialization timestamp is missing + remaining_delay = 15 if os.getenv("CI") else 5 + + self.logger.log( + f"Sleeping {remaining_delay:.1f}s before requesting references for the first time (CI needs extra indexing time)", + logging.INFO, + ) + time.sleep(remaining_delay) self._did_sleep_before_requesting_references = True - return super().request_references(relative_file_path, line, column) + + # Get references with retry logic for CI stability + references = super().request_references(relative_file_path, line, column) + + # In CI, if no references found, retry once after additional delay + if os.getenv("CI") and not references: + self.logger.log("No references found in CI - retrying after additional 5s delay", logging.INFO) + time.sleep(5) + references = super().request_references(relative_file_path, line, column) + + return references diff --git a/src/solidlsp/language_servers/terraform_ls.py b/src/solidlsp/language_servers/terraform_ls.py index 5ee38189..b20d8653 100644 --- a/src/solidlsp/language_servers/terraform_ls.py +++ b/src/solidlsp/language_servers/terraform_ls.py @@ -108,7 +108,7 @@ class TerraformLS(SolidLanguageServer): ), ] ) - dependency = deps.single_for_current_platform() + dependency = deps.get_single_dep_for_current_platform() terraform_ls_executable_path = deps.binary_path(cls.ls_resources_dir(solidlsp_settings)) if not os.path.exists(terraform_ls_executable_path): diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 8a371df0..02561486 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -1105,7 +1105,30 @@ class SolidLanguageServer(ABC): if symbol["kind"] == ls_types.SymbolKind.File: # For file symbols, process their children (top-level symbols) for child in symbol["children"]: - path = Path(child["location"]["absolutePath"]).resolve().relative_to(self.repository_root_path) + # Handle cross-platform path resolution (fixes Docker/macOS path issues) + absolute_path = Path(child["location"]["absolutePath"]).resolve() + repository_root = Path(self.repository_root_path).resolve() + + # Try pathlib first, fallback to alternative approach if paths are incompatible + try: + path = absolute_path.relative_to(repository_root) + except ValueError: + # If paths are from different roots (e.g., /workspaces vs /Users), + # use the relativePath from location if available, or extract from absolutePath + if "relativePath" in child["location"] and child["location"]["relativePath"]: + path = Path(child["location"]["relativePath"]) + else: + # Extract relative path by finding common structure + # Example: /workspaces/.../test_repo/file.py -> test_repo/file.py + path_parts = absolute_path.parts + + # Find the last common part or use a fallback + if "test_repo" in path_parts: + test_repo_idx = path_parts.index("test_repo") + path = Path(*path_parts[test_repo_idx:]) + else: + # Last resort: use filename only + path = Path(absolute_path.name) result[str(path)].append(child) # For package/directory symbols, process their children for child in symbol["children"]: diff --git a/test/resources/repos/kotlin/test_repo/.gitignore b/test/resources/repos/kotlin/test_repo/.gitignore new file mode 100644 index 00000000..3bb19ffd --- /dev/null +++ b/test/resources/repos/kotlin/test_repo/.gitignore @@ -0,0 +1 @@ +.gradle/