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/resources/serena_config.template.yml b/src/serena/resources/serena_config.template.yml index 082c0c32..db1d70bc 100644 --- a/src/serena/resources/serena_config.template.yml +++ b/src/serena/resources/serena_config.template.yml @@ -38,7 +38,27 @@ ls_specific_settings: {} # 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 +# No documentation on options means no options are available. +# +# Example: add ARM64 runtime dependencies for the C# language server +# ls_specific_settings: +# csharp: +# runtime_dependencies: +# - id: "DotNetRuntime" +# platform_id: "linux-arm64" +# url: "https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-linux-arm64.tar.gz" +# archive_type: "tar.gz" +# binary_name: "dotnet" +# description: ".NET 9 Runtime for Linux (ARM64)" +# - id: "CSharpLanguageServer" +# platform_id: "linux-arm64" +# package_name: "Microsoft.CodeAnalysis.LanguageServer.linux-arm64" +# package_version: "5.0.0-1.25329.6" +# archive_type: "nupkg" +# binary_name: "Microsoft.CodeAnalysis.LanguageServer.dll" +# extract_path: "content/LanguageServer/linux-arm64" +# description: "Microsoft.CodeAnalysis.LanguageServer for Linux (ARM64)" +# dotnet_runtime_url: "https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-linux-arm64.tar.gz" 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/csharp_language_server.py b/src/solidlsp/language_servers/csharp_language_server.py index e5d7fa1c..abec7552 100644 --- a/src/solidlsp/language_servers/csharp_language_server.py +++ b/src/solidlsp/language_servers/csharp_language_server.py @@ -107,6 +107,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)", @@ -176,7 +184,8 @@ class CSharpLanguageServer(SolidLanguageServer): This is 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 + - dotnet_runtime_url: overrides the .NET runtime download URL + - runtime_dependencies: list of dependency definitions merged with RUNTIME_DEPENDENCIES """ def __init__( @@ -230,7 +239,62 @@ class CSharpLanguageServer(SolidLanguageServer): 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) + + from solidlsp.runtime_config import RuntimeDependencyLoader + + 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) + if language_specific_config is None: + language_specific_config = ls_specific_settings.get(str(language_enum)) + + runtime_overrides = None + if isinstance(language_specific_config, dict): + runtime_overrides = cast(list[dict[str, object]] | None, language_specific_config.get("runtime_dependencies")) + + logger.log("Resolving runtime dependencies", logging.DEBUG) + if language_specific_config is not None: + logger.log( + f"Language-specific config for {language_enum}: {language_specific_config}", + logging.DEBUG, + ) + + all_dependencies = RuntimeDependencyLoader.load_dependencies( + base_dependencies=RUNTIME_DEPENDENCIES, + override_entries=runtime_overrides, + ) + + logger.log( + f"Available runtime dependencies: {[(dep.id, dep.platform_id) for dep in all_dependencies]}", + logging.DEBUG, + ) + + # Find the dependencies for our platform + lang_server_dep = None + dotnet_runtime_dep = None + + for dep in all_dependencies: + logger.log( + f"Inspecting dependency {dep.id} for platform {dep.platform_id}", + logging.DEBUG, + ) + 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: + logger.log( + f"No C# language server dependency matched runtime {runtime_id}", + logging.ERROR, + ) + raise SolidLSPException(f"No C# language server dependency found for platform {runtime_id}") + if not dotnet_runtime_dep: + logger.log( + f"No .NET runtime dependency matched runtime {runtime_id}", + logging.ERROR, + ) + raise SolidLSPException(f"No .NET runtime dependency found for platform {runtime_id}") 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) @@ -251,25 +315,6 @@ class CSharpLanguageServer(SolidLanguageServer): 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 diff --git a/src/solidlsp/language_servers/sourcekit_lsp.py b/src/solidlsp/language_servers/sourcekit_lsp.py index 4a47ffb9..21eb3308 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,6 +346,9 @@ 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 @@ -352,7 +356,17 @@ class SourceKitLSP(SolidLanguageServer): # prevents race conditions where references might not be available yet. # Unfortunately, sourcekit doesn't send a signal when it's really ready 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 + # Base delay: 5s local, 10s CI, minus elapsed time (minimum 1s) + base_delay = 10 if os.getenv("CI") else 5 + remaining_delay = max(1, base_delay - elapsed) + else: + # Fallback if initialization timestamp is missing + remaining_delay = 10 if os.getenv("CI") else 5 + + self.logger.log(f"Sleeping {remaining_delay:.1f}s before requesting references for the first time", logging.DEBUG) + time.sleep(remaining_delay) self._did_sleep_before_requesting_references = True return super().request_references(relative_file_path, line, column) 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/src/solidlsp/runtime_config.py b/src/solidlsp/runtime_config.py new file mode 100644 index 00000000..bca7f04a --- /dev/null +++ b/src/solidlsp/runtime_config.py @@ -0,0 +1,48 @@ +"""Runtime dependency configuration loader for language servers.""" + +from __future__ import annotations + +from collections.abc import Iterable, Mapping +from dataclasses import asdict +from typing import Optional + +from .language_servers.common import RuntimeDependency + + +class RuntimeDependencyLoader: + """Utility helpers to merge runtime dependency overrides.""" + + @staticmethod + def load_dependencies( + base_dependencies: Iterable[RuntimeDependency], + override_entries: Optional[Iterable[Mapping[str, object]]] = None, + ) -> list[RuntimeDependency]: + """Merge *override_entries* into *base_dependencies*. + + Entries are matched by the tuple ``(id, platform_id)``. If an override provides only a + subset of the ``RuntimeDependency`` fields, the remaining values will fall back to the + corresponding base dependency. + """ + # Ensure we work on a copy in order to keep the base definition immutable for callers + merged: dict[tuple[str | None, str | None], RuntimeDependency] = {(dep.id, dep.platform_id): dep for dep in base_dependencies} + + if not override_entries: + return list(merged.values()) + + for entry in override_entries: + entry_id = entry.get("id") + if entry_id is None: + raise ValueError("Runtime dependency override requires an 'id' value") + + key = (entry_id, entry.get("platform_id")) + base_dep = merged.get(key) + + if base_dep is not None: + dep_data = asdict(base_dep) + dep_data.update(entry) + else: + dep_data = dict(entry) + + merged[key] = RuntimeDependency(**dep_data) + + return list(merged.values()) diff --git a/test/resources/repos/kotlin/test_repo/.gradle/9.0.0/checksums/checksums.lock b/test/resources/repos/kotlin/test_repo/.gradle/9.0.0/checksums/checksums.lock new file mode 100644 index 00000000..48ac852d Binary files /dev/null and b/test/resources/repos/kotlin/test_repo/.gradle/9.0.0/checksums/checksums.lock differ diff --git a/test/resources/repos/kotlin/test_repo/.gradle/9.0.0/checksums/md5-checksums.bin b/test/resources/repos/kotlin/test_repo/.gradle/9.0.0/checksums/md5-checksums.bin new file mode 100644 index 00000000..70273ccc Binary files /dev/null and b/test/resources/repos/kotlin/test_repo/.gradle/9.0.0/checksums/md5-checksums.bin differ diff --git a/test/resources/repos/kotlin/test_repo/.gradle/9.0.0/checksums/sha1-checksums.bin b/test/resources/repos/kotlin/test_repo/.gradle/9.0.0/checksums/sha1-checksums.bin new file mode 100644 index 00000000..ac99f6a9 Binary files /dev/null and b/test/resources/repos/kotlin/test_repo/.gradle/9.0.0/checksums/sha1-checksums.bin differ diff --git a/test/resources/repos/kotlin/test_repo/.gradle/9.0.0/executionHistory/executionHistory.lock b/test/resources/repos/kotlin/test_repo/.gradle/9.0.0/executionHistory/executionHistory.lock new file mode 100644 index 00000000..638bd249 Binary files /dev/null and b/test/resources/repos/kotlin/test_repo/.gradle/9.0.0/executionHistory/executionHistory.lock differ diff --git a/test/resources/repos/kotlin/test_repo/.gradle/9.0.0/fileChanges/last-build.bin b/test/resources/repos/kotlin/test_repo/.gradle/9.0.0/fileChanges/last-build.bin new file mode 100644 index 00000000..f76dd238 Binary files /dev/null and b/test/resources/repos/kotlin/test_repo/.gradle/9.0.0/fileChanges/last-build.bin differ diff --git a/test/resources/repos/kotlin/test_repo/.gradle/9.0.0/fileHashes/fileHashes.lock b/test/resources/repos/kotlin/test_repo/.gradle/9.0.0/fileHashes/fileHashes.lock new file mode 100644 index 00000000..af433fed Binary files /dev/null and b/test/resources/repos/kotlin/test_repo/.gradle/9.0.0/fileHashes/fileHashes.lock differ diff --git a/test/resources/repos/kotlin/test_repo/.gradle/9.0.0/gc.properties b/test/resources/repos/kotlin/test_repo/.gradle/9.0.0/gc.properties new file mode 100644 index 00000000..e69de29b diff --git a/test/resources/repos/kotlin/test_repo/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/test/resources/repos/kotlin/test_repo/.gradle/buildOutputCleanup/buildOutputCleanup.lock new file mode 100644 index 00000000..4b1ede75 Binary files /dev/null and b/test/resources/repos/kotlin/test_repo/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/test/resources/repos/kotlin/test_repo/.gradle/buildOutputCleanup/cache.properties b/test/resources/repos/kotlin/test_repo/.gradle/buildOutputCleanup/cache.properties new file mode 100644 index 00000000..fe5bbba6 --- /dev/null +++ b/test/resources/repos/kotlin/test_repo/.gradle/buildOutputCleanup/cache.properties @@ -0,0 +1,2 @@ +#Thu Sep 18 14:46:12 MSK 2025 +gradle.version=9.0.0 diff --git a/test/resources/repos/kotlin/test_repo/.gradle/vcs-1/gc.properties b/test/resources/repos/kotlin/test_repo/.gradle/vcs-1/gc.properties new file mode 100644 index 00000000..e69de29b