From 2087bd3d1b1c521af025234d69df60306309078d Mon Sep 17 00:00:00 2001 From: Yangze Luo Date: Sun, 3 Aug 2025 13:26:07 +0800 Subject: [PATCH 1/6] Add swift lsp support --- .github/workflows/pytest.yml | 34 +- .gitignore | 4 + README.md | 1 + pyproject.toml | 3 +- .../language_servers/sourcekit_lsp.py | 561 ++++++++++++++++++ src/solidlsp/ls.py | 5 + src/solidlsp/ls_config.py | 3 + .../repos/swift/test_repo/Package.swift | 16 + .../repos/swift/test_repo/src/main.swift | 62 ++ .../repos/swift/test_repo/src/utils.swift | 20 + test/solidlsp/swift/test_swift_basic.py | 228 +++++++ 11 files changed, 933 insertions(+), 4 deletions(-) create mode 100644 src/solidlsp/language_servers/sourcekit_lsp.py create mode 100644 test/resources/repos/swift/test_repo/Package.swift create mode 100644 test/resources/repos/swift/test_repo/src/main.swift create mode 100644 test/resources/repos/swift/test_repo/src/utils.swift create mode 100644 test/solidlsp/swift/test_swift_basic.py diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 91dcd610..dc4be450 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -27,7 +27,7 @@ jobs: python-version: "${{ matrix.python-version }}" - uses: actions/setup-go@v5 with: - go-version: '>=1.17.0' + go-version: ">=1.17.0" # 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 @@ -47,8 +47,8 @@ jobs: if: runner.os != 'Windows' uses: erlef/setup-beam@v1 with: - elixir-version: '1.18.4' - otp-version: '26.1' + elixir-version: "1.18.4" + otp-version: "26.1" - name: Prepare java uses: actions/setup-java@v3 @@ -64,6 +64,34 @@ jobs: with: terraform_version: "1.5.0" terraform_wrapper: false + - name: Install Swift with swiftly (macOS) + if: runner.os == 'macOS' + run: | + echo "=== Installing swiftly on macOS ===" + curl -O https://download.swift.org/swiftly/darwin/swiftly.pkg && \ + installer -pkg swiftly.pkg -target CurrentUserHomeDirectory && \ + ~/.swiftly/bin/swiftly init --quiet-shell-followup && \ + . "${SWIFTLY_HOME_DIR:-$HOME/.swiftly}/env.sh" && \ + hash -r + swiftly install --use 6.1.2 + swiftly use 6.1.2 + echo "~/.swiftly/bin" >> $GITHUB_PATH + echo "Swiftly installed successfully" + - name: Install Swift with swiftly (Ubuntu) + if: runner.os == 'Linux' + run: | + echo "=== Installing swiftly on Ubuntu ===" + curl -O https://download.swift.org/swiftly/linux/swiftly-$(uname -m).tar.gz && \ + tar zxf swiftly-$(uname -m).tar.gz && \ + ./swiftly init --quiet-shell-followup && \ + . "${SWIFTLY_HOME_DIR:-$HOME/.local/share/swiftly}/env.sh" && \ + hash -r + swiftly install --use 6.1.2 + swiftly use 6.1.2 + sudo apt-get -y install libcurl4-openssl-dev + echo "=== Adding Swift toolchain to PATH ===" + echo "$HOME/.local/share/swiftly/bin" >> $GITHUB_PATH + echo "Swiftly installed successfully!" - name: Install uv shell: bash run: curl -LsSf https://astral.sh/uv/install.sh | sh diff --git a/.gitignore b/.gitignore index 64c7e71e..fdbd1c9f 100644 --- a/.gitignore +++ b/.gitignore @@ -227,3 +227,7 @@ tmp/ /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 + +# Swift +/test/resources/repos/swift/test_repo/.build +/test/resources/repos/swift/test_repo/.swiftpm diff --git a/README.md b/README.md index 1b6777cb..14a7d839 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ With Serena, we provide * 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) + * Swift (requires Swift and sourcekit-lsp to be installed) * indirect support (may require some code changes/manual installation) for: * Ruby (untested) * Kotlin (untested) diff --git a/pyproject.toml b/pyproject.toml index 8872384c..fc20b70f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ explicit = true [project.scripts] serena = "serena.cli:top_level" serena-mcp-server = "serena.cli:start_mcp_server" -index-project = "serena.cli:index_project" # deprecated +index-project = "serena.cli:index_project" # deprecated [project.license] text = "MIT" @@ -259,6 +259,7 @@ markers = [ "csharp: language server running for C#", "elixir: language server running for Elixir", "terraform: language server running for Terraform", + "swift: language server running for Swift", "snapshot: snapshot tests for symbolic editing operations", ] diff --git a/src/solidlsp/language_servers/sourcekit_lsp.py b/src/solidlsp/language_servers/sourcekit_lsp.py new file mode 100644 index 00000000..dac6cfe4 --- /dev/null +++ b/src/solidlsp/language_servers/sourcekit_lsp.py @@ -0,0 +1,561 @@ +import logging +import os +import pathlib +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.lsp_protocol_handler import lsp_types +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.settings import SolidLSPSettings +from solidlsp.lsp_protocol_handler import lsp_types as LSPTypes + + +class SourceKitLSP(SolidLanguageServer): + """ + Provides Swift specific instantiation of the LanguageServer class using sourcekit-lsp. + """ + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + # For Swift projects, we should ignore: + # - .build: Swift Package Manager build artifacts + # - .swiftpm: Swift Package Manager metadata + # - node_modules: if the project has JavaScript components + # - dist/build: common output directories + return super().is_ignored_dirname(dirname) or dirname in [".build", ".swiftpm", "node_modules", "dist", "build"] + + @staticmethod + def _get_swift_version(): + """Get the installed Swift version or None if not found.""" + try: + result = subprocess.run(["swift", "--version"], capture_output=True, text=True, check=False) + if result.returncode == 0: + return result.stdout.strip() + except FileNotFoundError: + return None + return None + + @staticmethod + def _get_sourcekit_lsp_version(): + """Get the installed sourcekit-lsp version or None if not found.""" + try: + result = subprocess.run(["sourcekit-lsp", "-h"], capture_output=True, text=True, check=False) + if result.returncode == 0: + return result.stdout.strip() + except FileNotFoundError: + return None + return None + + @staticmethod + def _setup_runtime_dependency(): + """ + Check if required Swift runtime dependencies are available. + Raises RuntimeError with helpful message if dependencies are missing. + """ + swift_version = SourceKitLSP._get_swift_version() + if not swift_version: + raise RuntimeError( + "Swift is not installed. Please install Swift from https://swift.org/download/ and make sure it is added to your PATH." + ) + + sourcekit_lsp_version = SourceKitLSP._get_sourcekit_lsp_version() + if not sourcekit_lsp_version: + raise RuntimeError( + "Found a Swift version but sourcekit-lsp is not installed.\n" + "Please install sourcekit-lsp as described in https://github.com/apple/sourcekit-lsp#installation\n\n" + "After installation, make sure it is added to your PATH." + ) + + return True + + def __init__( + self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings + ): + self._setup_runtime_dependency() + + super().__init__( + config, + logger, + repository_root_path, + ProcessLaunchInfo(cmd="sourcekit-lsp", cwd=repository_root_path), + "swift", + solidlsp_settings, + ) + 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 Swift Language Server. + """ + root_uri = pathlib.Path(repository_absolute_path).as_uri() + + initialize_params = { + "capabilities": { + "general": { + "markdown": { + "parser": "marked", + "version": "1.1.0" + }, + "positionEncodings": [ + "utf-16" + ], + "regularExpressions": { + "engine": "ECMAScript", + "version": "ES2020" + }, + "staleRequestSupport": { + "cancel": True, + "retryOnContentModified": [ + "textDocument/semanticTokens/full", + "textDocument/semanticTokens/range", + "textDocument/semanticTokens/full/delta" + ] + } + }, + "notebookDocument": { + "synchronization": { + "dynamicRegistration": True, + "executionSummarySupport": True + } + }, + "textDocument": { + "callHierarchy": { + "dynamicRegistration": True + }, + "codeAction": { + "codeActionLiteralSupport": { + "codeActionKind": { + "valueSet": [ + "", + "quickfix", + "refactor", + "refactor.extract", + "refactor.inline", + "refactor.rewrite", + "source", + "source.organizeImports" + ] + } + }, + "dataSupport": True, + "disabledSupport": True, + "dynamicRegistration": True, + "honorsChangeAnnotations": True, + "isPreferredSupport": True, + "resolveSupport": { + "properties": [ + "edit" + ] + } + }, + "codeLens": { + "dynamicRegistration": True + }, + "colorProvider": { + "dynamicRegistration": True + }, + "completion": { + "completionItem": { + "commitCharactersSupport": True, + "deprecatedSupport": True, + "documentationFormat": [ + "markdown", + "plaintext" + ], + "insertReplaceSupport": True, + "insertTextModeSupport": { + "valueSet": [ + 1, + 2 + ] + }, + "labelDetailsSupport": True, + "preselectSupport": True, + "resolveSupport": { + "properties": [ + "documentation", + "detail", + "additionalTextEdits" + ] + }, + "snippetSupport": True, + "tagSupport": { + "valueSet": [ + 1 + ] + } + }, + "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", + "data" + ] + }, + "contextSupport": True, + "dynamicRegistration": True, + "insertTextMode": 2 + }, + "declaration": { + "dynamicRegistration": True, + "linkSupport": True + }, + "definition": { + "dynamicRegistration": True, + "linkSupport": True + }, + "diagnostic": { + "dynamicRegistration": True, + "relatedDocumentSupport": False + }, + "documentHighlight": { + "dynamicRegistration": True + }, + "documentLink": { + "dynamicRegistration": True, + "tooltipSupport": True + }, + "documentSymbol": { + "dynamicRegistration": True, + "hierarchicalDocumentSymbolSupport": True, + "labelSupport": 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 + ] + } + }, + "foldingRange": { + "dynamicRegistration": True, + "foldingRange": { + "collapsedText": False + }, + "foldingRangeKind": { + "valueSet": [ + "comment", + "imports", + "region" + ] + }, + "lineFoldingOnly": True, + "rangeLimit": 5000 + }, + "formatting": { + "dynamicRegistration": True + }, + "hover": { + "contentFormat": [ + "markdown", + "plaintext" + ], + "dynamicRegistration": True + }, + "implementation": { + "dynamicRegistration": True, + "linkSupport": True + }, + "inlayHint": { + "dynamicRegistration": True, + "resolveSupport": { + "properties": [ + "tooltip", + "textEdits", + "label.tooltip", + "label.location", + "label.command" + ] + } + }, + "inlineValue": { + "dynamicRegistration": True + }, + "linkedEditingRange": { + "dynamicRegistration": True + }, + "onTypeFormatting": { + "dynamicRegistration": True + }, + "publishDiagnostics": { + "codeDescriptionSupport": True, + "dataSupport": True, + "relatedInformation": True, + "tagSupport": { + "valueSet": [ + 1, + 2 + ] + }, + "versionSupport": False + }, + "rangeFormatting": { + "dynamicRegistration": True, + "rangesSupport": True + }, + "references": { + "dynamicRegistration": True + }, + "rename": { + "dynamicRegistration": True, + "honorsChangeAnnotations": True, + "prepareSupport": True, + "prepareSupportDefaultBehavior": 1 + }, + "selectionRange": { + "dynamicRegistration": True + }, + "semanticTokens": { + "augmentsSyntaxTokens": True, + "dynamicRegistration": True, + "formats": [ + "relative" + ], + "multilineTokenSupport": False, + "overlappingTokenSupport": False, + "requests": { + "full": { + "delta": True + }, + "range": True + }, + "serverCancelSupport": True, + "tokenModifiers": [ + "declaration", + "definition", + "readonly", + "static", + "deprecated", + "abstract", + "async", + "modification", + "documentation", + "defaultLibrary" + ], + "tokenTypes": [ + "namespace", + "type", + "class", + "enum", + "interface", + "struct", + "typeParameter", + "parameter", + "variable", + "property", + "enumMember", + "event", + "function", + "method", + "macro", + "keyword", + "modifier", + "comment", + "string", + "number", + "regexp", + "operator", + "decorator" + ] + }, + "signatureHelp": { + "contextSupport": True, + "dynamicRegistration": True, + "signatureInformation": { + "activeParameterSupport": True, + "documentationFormat": [ + "markdown", + "plaintext" + ], + "parameterInformation": { + "labelOffsetSupport": True + } + } + }, + "synchronization": { + "didSave": True, + "dynamicRegistration": True, + "willSave": True, + "willSaveWaitUntil": True + }, + "typeDefinition": { + "dynamicRegistration": True, + "linkSupport": True + }, + "typeHierarchy": { + "dynamicRegistration": True + } + }, + "window": { + "showDocument": { + "support": True + }, + "showMessage": { + "messageActionItem": { + "additionalPropertiesSupport": True + } + }, + "workDoneProgress": True + }, + "workspace": { + "applyEdit": True, + "codeLens": { + "refreshSupport": True + }, + "configuration": True, + "diagnostics": { + "refreshSupport": True + }, + "didChangeConfiguration": { + "dynamicRegistration": True + }, + "didChangeWatchedFiles": { + "dynamicRegistration": True, + "relativePatternSupport": True + }, + "executeCommand": { + "dynamicRegistration": True + }, + "fileOperations": { + "didCreate": True, + "didDelete": True, + "didRename": True, + "dynamicRegistration": True, + "willCreate": True, + "willDelete": True, + "willRename": True + }, + "foldingRange": { + "refreshSupport": True + }, + "inlayHint": { + "refreshSupport": True + }, + "inlineValue": { + "refreshSupport": True + }, + "semanticTokens": { + "refreshSupport": False + }, + "symbol": { + "dynamicRegistration": True, + "resolveSupport": { + "properties": [ + "location.range" + ] + }, + "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 + ] + } + }, + "workspaceEdit": { + "changeAnnotationSupport": { + "groupsOnLabel": True + }, + "documentChanges": True, + "failureHandling": "textOnlyTransactional", + "normalizesLineEndings": True, + "resourceOperations": [ + "create", + "rename", + "delete" + ] + }, + "workspaceFolders": True + } + }, + "clientInfo": { + "name": "Visual Studio Code", + "version": "1.102.2" + }, + "initializationOptions": { + "backgroundIndexing": True, + "backgroundPreparationMode": "enabled", + "textDocument/codeLens": { + "supportedCommands": { + "swift.debug": "swift.debug", + "swift.run": "swift.run" + } + }, + "window/didChangeActiveDocument": True, + "workspace/getReferenceDocument": True, + "workspace/peekDocuments": True + }, + "locale": "en", + "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 sourcekit-lsp 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 sourcekit-lsp 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"] + self.logger.log(f"SourceKit LSP capabilities: {list(capabilities.keys())}", logging.INFO) + + assert "textDocumentSync" in capabilities, "textDocumentSync capability missing" + assert "definitionProvider" in capabilities, "definitionProvider capability missing" + + self.server.notify.initialized({}) + self.completions_available.set() + + # sourcekit-lsp server is typically ready immediately after initialization + self.server_ready.set() + self.server_ready.wait() \ No newline at end of file diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index ded2645a..5e82b2ef 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -226,6 +226,11 @@ class SolidLanguageServer(ABC): ls = TerraformLS(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings) + elif config.code_language == Language.SWIFT: + from solidlsp.language_servers.sourcekit_lsp import SourceKitLSP + + ls = SourceKitLSP(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings) + else: logger.log(f"Language {config.code_language} is not supported", logging.ERROR) raise SolidLSPException(f"Language {config.code_language} is not supported") diff --git a/src/solidlsp/ls_config.py b/src/solidlsp/ls_config.py index efb55a87..363027c8 100644 --- a/src/solidlsp/ls_config.py +++ b/src/solidlsp/ls_config.py @@ -42,6 +42,7 @@ class Language(str, Enum): CLOJURE = "clojure" ELIXIR = "elixir" TERRAFORM = "terraform" + SWIFT = "swift" # Experimental or deprecated Language Servers TYPESCRIPT_VTS = "typescript_vts" """Use the typescript language server through the natively bundled vscode extension via https://github.com/yioneko/vtsls""" @@ -103,6 +104,8 @@ class Language(str, Enum): return FilenameMatcher("*.ex", "*.exs") case self.TERRAFORM: return FilenameMatcher("*.tf", "*.tfvars", "*.tfstate") + case self.SWIFT: + return FilenameMatcher("*.swift") case _: raise ValueError(f"Unhandled language: {self}") diff --git a/test/resources/repos/swift/test_repo/Package.swift b/test/resources/repos/swift/test_repo/Package.swift new file mode 100644 index 00000000..13a02704 --- /dev/null +++ b/test/resources/repos/swift/test_repo/Package.swift @@ -0,0 +1,16 @@ +// swift-tools-version: 5.9 +import PackageDescription + +let package = Package( + name: "test_repo", + products: [ + .library( + name: "test_repo", + targets: ["test_repo"]), + ], + targets: [ + .target( + name: "test_repo", + dependencies: []), + ] +) \ No newline at end of file diff --git a/test/resources/repos/swift/test_repo/src/main.swift b/test/resources/repos/swift/test_repo/src/main.swift new file mode 100644 index 00000000..84630774 --- /dev/null +++ b/test/resources/repos/swift/test_repo/src/main.swift @@ -0,0 +1,62 @@ +import Foundation + +// Main entry point +func main() { + let calculator = Calculator() + let result = calculator.add(5, 3) + print("Result: \(result)") + + let user = User(name: "Alice", age: 30) + user.greet() + + let area = Utils.calculateArea(radius: 5.0) + print("Circle area: \(area)") +} + +class Calculator { + func add(_ a: Int, _ b: Int) -> Int { + return a + b + } + + func multiply(_ a: Int, _ b: Int) -> Int { + return a * b + } +} + +struct User { + let name: String + let age: Int + + func greet() { + print("Hello, my name is \(name) and I am \(age) years old.") + } + + func isAdult() -> Bool { + return age >= 18 + } +} + +enum Status { + case active + case inactive + case pending +} + +protocol Drawable { + func draw() +} + +class Circle: Drawable { + let radius: Double + + init(radius: Double) { + self.radius = radius + } + + func draw() { + print("Drawing a circle with radius \(radius)") + } +} + +// Call main +main() \ No newline at end of file diff --git a/test/resources/repos/swift/test_repo/src/utils.swift b/test/resources/repos/swift/test_repo/src/utils.swift new file mode 100644 index 00000000..cf1422c7 --- /dev/null +++ b/test/resources/repos/swift/test_repo/src/utils.swift @@ -0,0 +1,20 @@ +import Foundation + +public struct Utils { + public static func formatDate(_ date: Date) -> String { + let formatter = DateFormatter() + formatter.dateStyle = .medium + return formatter.string(from: date) + } + + public static func calculateArea(radius: Double) -> Double { + return Double.pi * radius * radius + } +} + +public extension String { + func isValidEmail() -> Bool { + let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}" + return NSPredicate(format: "SELF MATCHES %@", emailRegex).evaluate(with: self) + } +} \ No newline at end of file diff --git a/test/solidlsp/swift/test_swift_basic.py b/test/solidlsp/swift/test_swift_basic.py new file mode 100644 index 00000000..464553e9 --- /dev/null +++ b/test/solidlsp/swift/test_swift_basic.py @@ -0,0 +1,228 @@ +""" +Basic integration tests for the Swift language server functionality. + +These tests validate the functionality of the language server APIs +like request_references using the Swift test repository. +""" + +import os +import sys +import time +import platform + +import pytest + +from serena.project import Project +from serena.text_utils import LineType +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language + + +# Skip Swift tests on Windows due to complex GitHub Actions configuration +WINDOWS_SKIP = platform.system() == "Windows" +WINDOWS_SKIP_REASON = "GitHub Actions configuration for Swift on Windows is complex, skipping for now." + +pytestmark = [pytest.mark.swift, pytest.mark.skipif(WINDOWS_SKIP, reason=WINDOWS_SKIP_REASON)] + +class TestSwiftLanguageServerBasics: + """Test basic functionality of the Swift language server.""" + @pytest.mark.parametrize("language_server", [Language.SWIFT], indirect=True) + def test_goto_definition_calculator_class(self, language_server: SolidLanguageServer) -> None: + """Test goto_definition on Calculator class usage.""" + file_path = os.path.join("src", "main.swift") + + # Find the Calculator usage at line 5: let calculator = Calculator() + # Position should be at the "Calculator()" call + definitions = language_server.request_definition(file_path, 4, 23) # Position at Calculator() call + assert isinstance(definitions, list), "Definitions should be a list" + assert len(definitions) > 0, "Should find definition for Calculator class" + + # Verify the definition points to the Calculator class definition + calculator_def = definitions[0] + assert calculator_def.get("uri", "").endswith("main.swift"), "Definition should be in main.swift" + + # The Calculator class is defined starting at line 16 + start_line = calculator_def.get("range", {}).get("start", {}).get("line") + assert start_line == 15, f"Calculator class definition should be at line 16, got {start_line + 1}" + + @pytest.mark.parametrize("language_server", [Language.SWIFT], indirect=True) + def test_goto_definition_user_struct(self, language_server: SolidLanguageServer) -> None: + """Test goto_definition on User struct usage.""" + file_path = os.path.join("src", "main.swift") + + # Find the User usage at line 9: let user = User(name: "Alice", age: 30) + # Position should be at the "User(...)" call + definitions = language_server.request_definition(file_path, 8, 18) # Position at User(...) call + assert isinstance(definitions, list), "Definitions should be a list" + assert len(definitions) > 0, "Should find definition for User struct" + + # Verify the definition points to the User struct definition + user_def = definitions[0] + assert user_def.get("uri", "").endswith("main.swift"), "Definition should be in main.swift" + + # The User struct is defined starting at line 26 + start_line = user_def.get("range", {}).get("start", {}).get("line") + assert start_line == 25, f"User struct definition should be at line 26, got {start_line + 1}" + + @pytest.mark.parametrize("language_server", [Language.SWIFT], indirect=True) + def test_goto_definition_calculator_method(self, language_server: SolidLanguageServer) -> None: + """Test goto_definition on Calculator method usage.""" + file_path = os.path.join("src", "main.swift") + + # Find the add method usage at line 6: let result = calculator.add(5, 3) + # Position should be at the "add" method call + definitions = language_server.request_definition(file_path, 5, 28) # Position at add method call + assert isinstance(definitions, list), "Definitions should be a list" + + # Verify the definition points to the add method definition + add_def = definitions[0] + assert add_def.get("uri", "").endswith("main.swift"), "Definition should be in main.swift" + + # The add method is defined starting at line 17 + start_line = add_def.get("range", {}).get("start", {}).get("line") + assert start_line == 16, f"add method definition should be at line 17, got {start_line + 1}" + + @pytest.mark.parametrize("language_server", [Language.SWIFT], indirect=True) + def test_goto_definition_cross_file(self, language_server: SolidLanguageServer) -> None: + """Test goto_definition across files - Utils struct.""" + utils_file = os.path.join("src", "utils.swift") + + # First, let's check if Utils is used anywhere (it might not be in this simple test) + # We'll test goto_definition on Utils struct itself + symbols = language_server.request_document_symbols(utils_file) + utils_symbol = next((s for s in symbols[0] if s.get("name") == "Utils"), None) + + sel_start = utils_symbol["selectionRange"]["start"] + definitions = language_server.request_definition(utils_file, sel_start["line"], sel_start["character"]) + assert isinstance(definitions, list), "Definitions should be a list" + + # Should find the Utils struct definition itself + utils_def = definitions[0] + assert utils_def.get("uri", "").endswith("utils.swift"), "Definition should be in utils.swift" + + @pytest.mark.parametrize("language_server", [Language.SWIFT], indirect=True) + def test_request_references_calculator_class(self, language_server: SolidLanguageServer) -> None: + # SourceKit LSP needs a short initialization period after startup + # before it can provide accurate reference information. This sleep + # prevents race conditions where references might not be available yet. + time.sleep(5) + """Test request_references on the Calculator class.""" + # Get references to the Calculator class in main.swift + file_path = os.path.join("src", "main.swift") + symbols = language_server.request_document_symbols(file_path) + + calculator_symbol = next((s for s in symbols[0] if s.get("name") == "Calculator"), None) + + sel_start = calculator_symbol["selectionRange"]["start"] + references = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + assert isinstance(references, list), "References should be a list" + assert len(references) > 0, "Calculator class should be referenced" + + # Validate that Calculator is referenced in the main function + calculator_refs = [ref for ref in references if ref.get("uri", "").endswith("main.swift")] + assert len(calculator_refs) > 0, "Calculator class should be referenced in main.swift" + + # Check that one reference is at line 5 (let calculator = Calculator()) + line_5_refs = [ref for ref in calculator_refs if ref.get("range", {}).get("start", {}).get("line") == 4] + assert len(line_5_refs) > 0, "Calculator should be referenced at line 5" + + @pytest.mark.parametrize("language_server", [Language.SWIFT], indirect=True) + def test_request_references_user_struct(self, language_server: SolidLanguageServer) -> None: + """Test request_references on the User struct.""" + # Get references to the User struct in main.swift + file_path = os.path.join("src", "main.swift") + symbols = language_server.request_document_symbols(file_path) + + user_symbol = next((s for s in symbols[0] if s.get("name") == "User"), None) + + sel_start = user_symbol["selectionRange"]["start"] + references = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + assert isinstance(references, list), "References should be a list" + + # Validate that User is referenced in the main function + user_refs = [ref for ref in references if ref.get("uri", "").endswith("main.swift")] + assert len(user_refs) > 0, "User struct should be referenced in main.swift" + + # Check that one reference is at line 9 (let user = User(...)) + line_9_refs = [ref for ref in user_refs if ref.get("range", {}).get("start", {}).get("line") == 8] + assert len(line_9_refs) > 0, "User should be referenced at line 9" + + @pytest.mark.parametrize("language_server", [Language.SWIFT], indirect=True) + def test_request_references_utils_struct(self, language_server: SolidLanguageServer) -> None: + """Test request_references on the Utils struct.""" + # Get references to the Utils struct in utils.swift + file_path = os.path.join("src", "utils.swift") + symbols = language_server.request_document_symbols(file_path) + utils_symbol = next((s for s in symbols[0] if s.get("name") == "Utils"), None) + if not utils_symbol or "selectionRange" not in utils_symbol: + raise AssertionError("Utils symbol or its selectionRange not found") + sel_start = utils_symbol["selectionRange"]["start"] + references = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + assert isinstance(references, list), "References should be a list" + assert len(references) > 0, "Utils struct should be referenced" + + # Validate that Utils is referenced in main.swift + utils_refs = [ref for ref in references if ref.get("uri", "").endswith("main.swift")] + assert len(utils_refs) > 0, "Utils struct should be referenced in main.swift" + + # Check that one reference is at line 12 (Utils.calculateArea call) + line_12_refs = [ref for ref in utils_refs if ref.get("range", {}).get("start", {}).get("line") == 11] + assert len(line_12_refs) > 0, "Utils should be referenced at line 12" + + + +class TestSwiftProjectBasics: + @pytest.mark.parametrize("project", [Language.SWIFT], indirect=True) + def test_retrieve_content_around_line(self, project: Project) -> None: + """Test retrieve_content_around_line functionality with various scenarios.""" + file_path = os.path.join("src", "main.swift") + + # Scenario 1: Find Calculator class definition + calculator_line = None + for line_num in range(1, 50): # Search first 50 lines + try: + line_content = project.retrieve_content_around_line(file_path, line_num) + if line_content.lines and "class Calculator" in line_content.lines[0].line_content: + calculator_line = line_num + break + except: + continue + + assert calculator_line is not None, "Calculator class not found" + line_calc = project.retrieve_content_around_line(file_path, calculator_line) + assert len(line_calc.lines) == 1 + assert "class Calculator" in line_calc.lines[0].line_content + assert line_calc.lines[0].line_number == calculator_line + assert line_calc.lines[0].match_type == LineType.MATCH + + # Scenario 2: Context above and below Calculator class + with_context_around_calculator = project.retrieve_content_around_line(file_path, calculator_line, 2, 2) + assert len(with_context_around_calculator.lines) == 5 + assert "class Calculator" in with_context_around_calculator.matched_lines[0].line_content + assert with_context_around_calculator.num_matched_lines == 1 + + # Scenario 3: Search for struct definitions + struct_pattern = r"struct\s+\w+" + matches = project.search_source_files_for_pattern(struct_pattern) + assert len(matches) > 0, "Should find struct definitions" + # Should find User struct + user_matches = [m for m in matches if "User" in str(m)] + assert len(user_matches) > 0, "Should find User struct" + + # Scenario 4: Search for class definitions + class_pattern = r"class\s+\w+" + matches = project.search_source_files_for_pattern(class_pattern) + assert len(matches) > 0, "Should find class definitions" + # Should find Calculator and Circle classes + calculator_matches = [m for m in matches if "Calculator" in str(m)] + circle_matches = [m for m in matches if "Circle" in str(m)] + assert len(calculator_matches) > 0, "Should find Calculator class" + assert len(circle_matches) > 0, "Should find Circle class" + + # Scenario 5: Search for enum definitions + enum_pattern = r"enum\s+\w+" + matches = project.search_source_files_for_pattern(enum_pattern) + assert len(matches) > 0, "Should find enum definitions" + # Should find Status enum + status_matches = [m for m in matches if "Status" in str(m)] + assert len(status_matches) > 0, "Should find Status enum" \ No newline at end of file From 6d28c7d7223e0584a7b5a8ec00a36e5d9c56cc80 Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Thu, 7 Aug 2025 17:34:51 +0200 Subject: [PATCH 2/6] Minor simplification of sourcekit_lsp.py, added sleeping at startup --- .../language_servers/sourcekit_lsp.py | 407 +++++------------- test/solidlsp/swift/test_swift_basic.py | 60 ++- 2 files changed, 125 insertions(+), 342 deletions(-) diff --git a/src/solidlsp/language_servers/sourcekit_lsp.py b/src/solidlsp/language_servers/sourcekit_lsp.py index dac6cfe4..f566b693 100644 --- a/src/solidlsp/language_servers/sourcekit_lsp.py +++ b/src/solidlsp/language_servers/sourcekit_lsp.py @@ -3,17 +3,16 @@ import os import pathlib import subprocess import threading +import time 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 import lsp_types from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings -from solidlsp.lsp_protocol_handler import lsp_types as LSPTypes class SourceKitLSP(SolidLanguageServer): @@ -31,53 +30,25 @@ class SourceKitLSP(SolidLanguageServer): return super().is_ignored_dirname(dirname) or dirname in [".build", ".swiftpm", "node_modules", "dist", "build"] @staticmethod - def _get_swift_version(): - """Get the installed Swift version or None if not found.""" - try: - result = subprocess.run(["swift", "--version"], capture_output=True, text=True, check=False) - if result.returncode == 0: - return result.stdout.strip() - except FileNotFoundError: - return None - return None - - @staticmethod - def _get_sourcekit_lsp_version(): - """Get the installed sourcekit-lsp version or None if not found.""" + def _get_sourcekit_lsp_version() -> str: + """Get the installed sourcekit-lsp version or raise error if sourcekit was not found.""" try: result = subprocess.run(["sourcekit-lsp", "-h"], capture_output=True, text=True, check=False) if result.returncode == 0: return result.stdout.strip() - except FileNotFoundError: - return None - return None - - @staticmethod - def _setup_runtime_dependency(): - """ - Check if required Swift runtime dependencies are available. - Raises RuntimeError with helpful message if dependencies are missing. - """ - swift_version = SourceKitLSP._get_swift_version() - if not swift_version: + else: + raise Exception(f"`sourcekit-lsp -h` resulted in: {result}") + except Exception as e: raise RuntimeError( - "Swift is not installed. Please install Swift from https://swift.org/download/ and make sure it is added to your PATH." - ) - - sourcekit_lsp_version = SourceKitLSP._get_sourcekit_lsp_version() - if not sourcekit_lsp_version: - raise RuntimeError( - "Found a Swift version but sourcekit-lsp is not installed.\n" - "Please install sourcekit-lsp as described in https://github.com/apple/sourcekit-lsp#installation\n\n" - "After installation, make sure it is added to your PATH." - ) - - return True + "Could not find sourcekit-lsp, please install it as described in https://github.com/apple/sourcekit-lsp#installation" + "And make sure it is available on your PATH." + ) from e def __init__( self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings ): - self._setup_runtime_dependency() + sourcekit_version = self._get_sourcekit_lsp_version() + logger.log(f"Starting sourcekit lsp with version: {sourcekit_version}", logging.INFO) super().__init__( config, @@ -96,40 +67,25 @@ class SourceKitLSP(SolidLanguageServer): Returns the initialize params for the Swift Language Server. """ root_uri = pathlib.Path(repository_absolute_path).as_uri() - + initialize_params = { "capabilities": { "general": { - "markdown": { - "parser": "marked", - "version": "1.1.0" - }, - "positionEncodings": [ - "utf-16" - ], - "regularExpressions": { - "engine": "ECMAScript", - "version": "ES2020" - }, + "markdown": {"parser": "marked", "version": "1.1.0"}, + "positionEncodings": ["utf-16"], + "regularExpressions": {"engine": "ECMAScript", "version": "ES2020"}, "staleRequestSupport": { "cancel": True, "retryOnContentModified": [ "textDocument/semanticTokens/full", "textDocument/semanticTokens/range", - "textDocument/semanticTokens/full/delta" - ] - } - }, - "notebookDocument": { - "synchronization": { - "dynamicRegistration": True, - "executionSummarySupport": True - } - }, - "textDocument": { - "callHierarchy": { - "dynamicRegistration": True + "textDocument/semanticTokens/full/delta", + ], }, + }, + "notebookDocument": {"synchronization": {"dynamicRegistration": True, "executionSummarySupport": True}}, + "textDocument": { + "callHierarchy": {"dynamicRegistration": True}, "codeAction": { "codeActionLiteralSupport": { "codeActionKind": { @@ -141,7 +97,7 @@ class SourceKitLSP(SolidLanguageServer): "refactor.inline", "refactor.rewrite", "source", - "source.organizeImports" + "source.organizeImports", ] } }, @@ -150,193 +106,85 @@ class SourceKitLSP(SolidLanguageServer): "dynamicRegistration": True, "honorsChangeAnnotations": True, "isPreferredSupport": True, - "resolveSupport": { - "properties": [ - "edit" - ] - } - }, - "codeLens": { - "dynamicRegistration": True - }, - "colorProvider": { - "dynamicRegistration": True + "resolveSupport": {"properties": ["edit"]}, }, + "codeLens": {"dynamicRegistration": True}, + "colorProvider": {"dynamicRegistration": True}, "completion": { "completionItem": { "commitCharactersSupport": True, "deprecatedSupport": True, - "documentationFormat": [ - "markdown", - "plaintext" - ], + "documentationFormat": ["markdown", "plaintext"], "insertReplaceSupport": True, - "insertTextModeSupport": { - "valueSet": [ - 1, - 2 - ] - }, + "insertTextModeSupport": {"valueSet": [1, 2]}, "labelDetailsSupport": True, "preselectSupport": True, - "resolveSupport": { - "properties": [ - "documentation", - "detail", - "additionalTextEdits" - ] - }, + "resolveSupport": {"properties": ["documentation", "detail", "additionalTextEdits"]}, "snippetSupport": True, - "tagSupport": { - "valueSet": [ - 1 - ] - } + "tagSupport": {"valueSet": [1]}, }, "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", - "data" - ] + "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", "data"]}, "contextSupport": True, "dynamicRegistration": True, - "insertTextMode": 2 - }, - "declaration": { - "dynamicRegistration": True, - "linkSupport": True - }, - "definition": { - "dynamicRegistration": True, - "linkSupport": True - }, - "diagnostic": { - "dynamicRegistration": True, - "relatedDocumentSupport": False - }, - "documentHighlight": { - "dynamicRegistration": True - }, - "documentLink": { - "dynamicRegistration": True, - "tooltipSupport": True + "insertTextMode": 2, }, + "declaration": {"dynamicRegistration": True, "linkSupport": True}, + "definition": {"dynamicRegistration": True, "linkSupport": True}, + "diagnostic": {"dynamicRegistration": True, "relatedDocumentSupport": False}, + "documentHighlight": {"dynamicRegistration": True}, + "documentLink": {"dynamicRegistration": True, "tooltipSupport": True}, "documentSymbol": { "dynamicRegistration": True, "hierarchicalDocumentSymbolSupport": True, "labelSupport": 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 - ] + "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 - ] - } + "tagSupport": {"valueSet": [1]}, }, "foldingRange": { "dynamicRegistration": True, - "foldingRange": { - "collapsedText": False - }, - "foldingRangeKind": { - "valueSet": [ - "comment", - "imports", - "region" - ] - }, + "foldingRange": {"collapsedText": False}, + "foldingRangeKind": {"valueSet": ["comment", "imports", "region"]}, "lineFoldingOnly": True, - "rangeLimit": 5000 - }, - "formatting": { - "dynamicRegistration": True - }, - "hover": { - "contentFormat": [ - "markdown", - "plaintext" - ], - "dynamicRegistration": True - }, - "implementation": { - "dynamicRegistration": True, - "linkSupport": True + "rangeLimit": 5000, }, + "formatting": {"dynamicRegistration": True}, + "hover": {"contentFormat": ["markdown", "plaintext"], "dynamicRegistration": True}, + "implementation": {"dynamicRegistration": True, "linkSupport": True}, "inlayHint": { "dynamicRegistration": True, - "resolveSupport": { - "properties": [ - "tooltip", - "textEdits", - "label.tooltip", - "label.location", - "label.command" - ] - } - }, - "inlineValue": { - "dynamicRegistration": True - }, - "linkedEditingRange": { - "dynamicRegistration": True - }, - "onTypeFormatting": { - "dynamicRegistration": True + "resolveSupport": {"properties": ["tooltip", "textEdits", "label.tooltip", "label.location", "label.command"]}, }, + "inlineValue": {"dynamicRegistration": True}, + "linkedEditingRange": {"dynamicRegistration": True}, + "onTypeFormatting": {"dynamicRegistration": True}, "publishDiagnostics": { "codeDescriptionSupport": True, "dataSupport": True, "relatedInformation": True, - "tagSupport": { - "valueSet": [ - 1, - 2 - ] - }, - "versionSupport": False - }, - "rangeFormatting": { - "dynamicRegistration": True, - "rangesSupport": True - }, - "references": { - "dynamicRegistration": True + "tagSupport": {"valueSet": [1, 2]}, + "versionSupport": False, }, + "rangeFormatting": {"dynamicRegistration": True, "rangesSupport": True}, + "references": {"dynamicRegistration": True}, "rename": { "dynamicRegistration": True, "honorsChangeAnnotations": True, "prepareSupport": True, - "prepareSupportDefaultBehavior": 1 - }, - "selectionRange": { - "dynamicRegistration": True + "prepareSupportDefaultBehavior": 1, }, + "selectionRange": {"dynamicRegistration": True}, "semanticTokens": { "augmentsSyntaxTokens": True, "dynamicRegistration": True, - "formats": [ - "relative" - ], + "formats": ["relative"], "multilineTokenSupport": False, "overlappingTokenSupport": False, - "requests": { - "full": { - "delta": True - }, - "range": True - }, + "requests": {"full": {"delta": True}, "range": True}, "serverCancelSupport": True, "tokenModifiers": [ "declaration", @@ -348,7 +196,7 @@ class SourceKitLSP(SolidLanguageServer): "async", "modification", "documentation", - "defaultLibrary" + "defaultLibrary", ], "tokenTypes": [ "namespace", @@ -373,67 +221,35 @@ class SourceKitLSP(SolidLanguageServer): "number", "regexp", "operator", - "decorator" - ] + "decorator", + ], }, "signatureHelp": { "contextSupport": True, "dynamicRegistration": True, "signatureInformation": { "activeParameterSupport": True, - "documentationFormat": [ - "markdown", - "plaintext" - ], - "parameterInformation": { - "labelOffsetSupport": True - } - } + "documentationFormat": ["markdown", "plaintext"], + "parameterInformation": {"labelOffsetSupport": True}, + }, }, - "synchronization": { - "didSave": True, - "dynamicRegistration": True, - "willSave": True, - "willSaveWaitUntil": True - }, - "typeDefinition": { - "dynamicRegistration": True, - "linkSupport": True - }, - "typeHierarchy": { - "dynamicRegistration": True - } + "synchronization": {"didSave": True, "dynamicRegistration": True, "willSave": True, "willSaveWaitUntil": True}, + "typeDefinition": {"dynamicRegistration": True, "linkSupport": True}, + "typeHierarchy": {"dynamicRegistration": True}, }, "window": { - "showDocument": { - "support": True - }, - "showMessage": { - "messageActionItem": { - "additionalPropertiesSupport": True - } - }, - "workDoneProgress": True + "showDocument": {"support": True}, + "showMessage": {"messageActionItem": {"additionalPropertiesSupport": True}}, + "workDoneProgress": True, }, "workspace": { "applyEdit": True, - "codeLens": { - "refreshSupport": True - }, + "codeLens": {"refreshSupport": True}, "configuration": True, - "diagnostics": { - "refreshSupport": True - }, - "didChangeConfiguration": { - "dynamicRegistration": True - }, - "didChangeWatchedFiles": { - "dynamicRegistration": True, - "relativePatternSupport": True - }, - "executeCommand": { - "dynamicRegistration": True - }, + "diagnostics": {"refreshSupport": True}, + "didChangeConfiguration": {"dynamicRegistration": True}, + "didChangeWatchedFiles": {"dynamicRegistration": True, "relativePatternSupport": True}, + "executeCommand": {"dynamicRegistration": True}, "fileOperations": { "didCreate": True, "didDelete": True, @@ -441,70 +257,38 @@ class SourceKitLSP(SolidLanguageServer): "dynamicRegistration": True, "willCreate": True, "willDelete": True, - "willRename": True - }, - "foldingRange": { - "refreshSupport": True - }, - "inlayHint": { - "refreshSupport": True - }, - "inlineValue": { - "refreshSupport": True - }, - "semanticTokens": { - "refreshSupport": False + "willRename": True, }, + "foldingRange": {"refreshSupport": True}, + "inlayHint": {"refreshSupport": True}, + "inlineValue": {"refreshSupport": True}, + "semanticTokens": {"refreshSupport": False}, "symbol": { "dynamicRegistration": True, - "resolveSupport": { - "properties": [ - "location.range" - ] - }, + "resolveSupport": {"properties": ["location.range"]}, "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 - ] + "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 - ] - } + "tagSupport": {"valueSet": [1]}, }, "workspaceEdit": { - "changeAnnotationSupport": { - "groupsOnLabel": True - }, + "changeAnnotationSupport": {"groupsOnLabel": True}, "documentChanges": True, "failureHandling": "textOnlyTransactional", "normalizesLineEndings": True, - "resourceOperations": [ - "create", - "rename", - "delete" - ] + "resourceOperations": ["create", "rename", "delete"], }, - "workspaceFolders": True - } - }, - "clientInfo": { - "name": "Visual Studio Code", - "version": "1.102.2" + "workspaceFolders": True, + }, }, + "clientInfo": {"name": "Visual Studio Code", "version": "1.102.2"}, "initializationOptions": { "backgroundIndexing": True, "backgroundPreparationMode": "enabled", - "textDocument/codeLens": { - "supportedCommands": { - "swift.debug": "swift.debug", - "swift.run": "swift.run" - } - }, + "textDocument/codeLens": {"supportedCommands": {"swift.debug": "swift.debug", "swift.run": "swift.run"}}, "window/didChangeActiveDocument": True, "workspace/getReferenceDocument": True, - "workspace/peekDocuments": True + "workspace/peekDocuments": True, }, "locale": "en", "processId": os.getpid(), @@ -517,7 +301,7 @@ class SourceKitLSP(SolidLanguageServer): } ], } - + return initialize_params def _start_server(self): @@ -549,13 +333,18 @@ class SourceKitLSP(SolidLanguageServer): capabilities = init_response["capabilities"] self.logger.log(f"SourceKit LSP capabilities: {list(capabilities.keys())}", logging.INFO) - + assert "textDocumentSync" in capabilities, "textDocumentSync capability missing" assert "definitionProvider" in capabilities, "definitionProvider capability missing" self.server.notify.initialized({}) self.completions_available.set() - # sourcekit-lsp server is typically ready immediately after initialization self.server_ready.set() - self.server_ready.wait() \ No newline at end of file + self.server_ready.wait() + + # SourceKit LSP needs a short initialization period 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 + time.sleep(5) diff --git a/test/solidlsp/swift/test_swift_basic.py b/test/solidlsp/swift/test_swift_basic.py index 464553e9..67dfe686 100644 --- a/test/solidlsp/swift/test_swift_basic.py +++ b/test/solidlsp/swift/test_swift_basic.py @@ -6,8 +6,6 @@ like request_references using the Swift test repository. """ import os -import sys -import time import platform import pytest @@ -17,30 +15,31 @@ from serena.text_utils import LineType from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language - # Skip Swift tests on Windows due to complex GitHub Actions configuration WINDOWS_SKIP = platform.system() == "Windows" WINDOWS_SKIP_REASON = "GitHub Actions configuration for Swift on Windows is complex, skipping for now." pytestmark = [pytest.mark.swift, pytest.mark.skipif(WINDOWS_SKIP, reason=WINDOWS_SKIP_REASON)] + class TestSwiftLanguageServerBasics: """Test basic functionality of the Swift language server.""" + @pytest.mark.parametrize("language_server", [Language.SWIFT], indirect=True) def test_goto_definition_calculator_class(self, language_server: SolidLanguageServer) -> None: """Test goto_definition on Calculator class usage.""" file_path = os.path.join("src", "main.swift") - + # Find the Calculator usage at line 5: let calculator = Calculator() # Position should be at the "Calculator()" call definitions = language_server.request_definition(file_path, 4, 23) # Position at Calculator() call assert isinstance(definitions, list), "Definitions should be a list" assert len(definitions) > 0, "Should find definition for Calculator class" - + # Verify the definition points to the Calculator class definition calculator_def = definitions[0] assert calculator_def.get("uri", "").endswith("main.swift"), "Definition should be in main.swift" - + # The Calculator class is defined starting at line 16 start_line = calculator_def.get("range", {}).get("start", {}).get("line") assert start_line == 15, f"Calculator class definition should be at line 16, got {start_line + 1}" @@ -49,17 +48,17 @@ class TestSwiftLanguageServerBasics: def test_goto_definition_user_struct(self, language_server: SolidLanguageServer) -> None: """Test goto_definition on User struct usage.""" file_path = os.path.join("src", "main.swift") - + # Find the User usage at line 9: let user = User(name: "Alice", age: 30) # Position should be at the "User(...)" call definitions = language_server.request_definition(file_path, 8, 18) # Position at User(...) call assert isinstance(definitions, list), "Definitions should be a list" assert len(definitions) > 0, "Should find definition for User struct" - + # Verify the definition points to the User struct definition user_def = definitions[0] assert user_def.get("uri", "").endswith("main.swift"), "Definition should be in main.swift" - + # The User struct is defined starting at line 26 start_line = user_def.get("range", {}).get("start", {}).get("line") assert start_line == 25, f"User struct definition should be at line 26, got {start_line + 1}" @@ -68,16 +67,16 @@ class TestSwiftLanguageServerBasics: def test_goto_definition_calculator_method(self, language_server: SolidLanguageServer) -> None: """Test goto_definition on Calculator method usage.""" file_path = os.path.join("src", "main.swift") - + # Find the add method usage at line 6: let result = calculator.add(5, 3) # Position should be at the "add" method call definitions = language_server.request_definition(file_path, 5, 28) # Position at add method call assert isinstance(definitions, list), "Definitions should be a list" - + # Verify the definition points to the add method definition add_def = definitions[0] assert add_def.get("uri", "").endswith("main.swift"), "Definition should be in main.swift" - + # The add method is defined starting at line 17 start_line = add_def.get("range", {}).get("start", {}).get("line") assert start_line == 16, f"add method definition should be at line 17, got {start_line + 1}" @@ -86,42 +85,38 @@ class TestSwiftLanguageServerBasics: def test_goto_definition_cross_file(self, language_server: SolidLanguageServer) -> None: """Test goto_definition across files - Utils struct.""" utils_file = os.path.join("src", "utils.swift") - + # First, let's check if Utils is used anywhere (it might not be in this simple test) # We'll test goto_definition on Utils struct itself symbols = language_server.request_document_symbols(utils_file) utils_symbol = next((s for s in symbols[0] if s.get("name") == "Utils"), None) - + sel_start = utils_symbol["selectionRange"]["start"] definitions = language_server.request_definition(utils_file, sel_start["line"], sel_start["character"]) assert isinstance(definitions, list), "Definitions should be a list" - + # Should find the Utils struct definition itself utils_def = definitions[0] assert utils_def.get("uri", "").endswith("utils.swift"), "Definition should be in utils.swift" @pytest.mark.parametrize("language_server", [Language.SWIFT], indirect=True) def test_request_references_calculator_class(self, language_server: SolidLanguageServer) -> None: - # SourceKit LSP needs a short initialization period after startup - # before it can provide accurate reference information. This sleep - # prevents race conditions where references might not be available yet. - time.sleep(5) """Test request_references on the Calculator class.""" # Get references to the Calculator class in main.swift file_path = os.path.join("src", "main.swift") symbols = language_server.request_document_symbols(file_path) - + calculator_symbol = next((s for s in symbols[0] if s.get("name") == "Calculator"), None) - + sel_start = calculator_symbol["selectionRange"]["start"] references = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) assert isinstance(references, list), "References should be a list" assert len(references) > 0, "Calculator class should be referenced" - + # Validate that Calculator is referenced in the main function calculator_refs = [ref for ref in references if ref.get("uri", "").endswith("main.swift")] assert len(calculator_refs) > 0, "Calculator class should be referenced in main.swift" - + # Check that one reference is at line 5 (let calculator = Calculator()) line_5_refs = [ref for ref in calculator_refs if ref.get("range", {}).get("start", {}).get("line") == 4] assert len(line_5_refs) > 0, "Calculator should be referenced at line 5" @@ -132,17 +127,17 @@ class TestSwiftLanguageServerBasics: # Get references to the User struct in main.swift file_path = os.path.join("src", "main.swift") symbols = language_server.request_document_symbols(file_path) - + user_symbol = next((s for s in symbols[0] if s.get("name") == "User"), None) - + sel_start = user_symbol["selectionRange"]["start"] references = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) assert isinstance(references, list), "References should be a list" - + # Validate that User is referenced in the main function user_refs = [ref for ref in references if ref.get("uri", "").endswith("main.swift")] assert len(user_refs) > 0, "User struct should be referenced in main.swift" - + # Check that one reference is at line 9 (let user = User(...)) line_9_refs = [ref for ref in user_refs if ref.get("range", {}).get("start", {}).get("line") == 8] assert len(line_9_refs) > 0, "User should be referenced at line 9" @@ -160,17 +155,16 @@ class TestSwiftLanguageServerBasics: references = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) assert isinstance(references, list), "References should be a list" assert len(references) > 0, "Utils struct should be referenced" - + # Validate that Utils is referenced in main.swift utils_refs = [ref for ref in references if ref.get("uri", "").endswith("main.swift")] assert len(utils_refs) > 0, "Utils struct should be referenced in main.swift" - + # Check that one reference is at line 12 (Utils.calculateArea call) line_12_refs = [ref for ref in utils_refs if ref.get("range", {}).get("start", {}).get("line") == 11] assert len(line_12_refs) > 0, "Utils should be referenced at line 12" - class TestSwiftProjectBasics: @pytest.mark.parametrize("project", [Language.SWIFT], indirect=True) def test_retrieve_content_around_line(self, project: Project) -> None: @@ -187,7 +181,7 @@ class TestSwiftProjectBasics: break except: continue - + assert calculator_line is not None, "Calculator class not found" line_calc = project.retrieve_content_around_line(file_path, calculator_line) assert len(line_calc.lines) == 1 @@ -209,7 +203,7 @@ class TestSwiftProjectBasics: user_matches = [m for m in matches if "User" in str(m)] assert len(user_matches) > 0, "Should find User struct" - # Scenario 4: Search for class definitions + # Scenario 4: Search for class definitions class_pattern = r"class\s+\w+" matches = project.search_source_files_for_pattern(class_pattern) assert len(matches) > 0, "Should find class definitions" @@ -225,4 +219,4 @@ class TestSwiftProjectBasics: assert len(matches) > 0, "Should find enum definitions" # Should find Status enum status_matches = [m for m in matches if "Status" in str(m)] - assert len(status_matches) > 0, "Should find Status enum" \ No newline at end of file + assert len(status_matches) > 0, "Should find Status enum" From d41ca01513ded599012de28728c73efe0535dfb5 Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Thu, 7 Aug 2025 22:21:14 +0200 Subject: [PATCH 3/6] Sleep before requesting references, not after start --- src/solidlsp/language_servers/sourcekit_lsp.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/solidlsp/language_servers/sourcekit_lsp.py b/src/solidlsp/language_servers/sourcekit_lsp.py index f566b693..4a47ffb9 100644 --- a/src/solidlsp/language_servers/sourcekit_lsp.py +++ b/src/solidlsp/language_servers/sourcekit_lsp.py @@ -7,6 +7,7 @@ import time from overrides import override +from solidlsp import ls_types from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger @@ -60,6 +61,7 @@ class SourceKitLSP(SolidLanguageServer): ) self.server_ready = threading.Event() self.request_id = 0 + self._did_sleep_before_requesting_references = False @staticmethod def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: @@ -343,8 +345,14 @@ class SourceKitLSP(SolidLanguageServer): self.server_ready.set() self.server_ready.wait() + @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 # 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 - time.sleep(5) + 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) + self._did_sleep_before_requesting_references = True + return super().request_references(relative_file_path, line, column) From b0032ddf9a89c6b09ccc1f3d7f06f9529707eebc Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Thu, 7 Aug 2025 22:21:38 +0200 Subject: [PATCH 4/6] Install swift with GH action instead of custom code --- .github/workflows/pytest.yml | 31 +++---------------------------- 1 file changed, 3 insertions(+), 28 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 49d8e857..88424dcb 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -70,34 +70,9 @@ jobs: with: terraform_version: "1.5.0" terraform_wrapper: false - - name: Install Swift with swiftly (macOS) - if: runner.os == 'macOS' - run: | - echo "=== Installing swiftly on macOS ===" - curl -O https://download.swift.org/swiftly/darwin/swiftly.pkg && \ - installer -pkg swiftly.pkg -target CurrentUserHomeDirectory && \ - ~/.swiftly/bin/swiftly init --quiet-shell-followup && \ - . "${SWIFTLY_HOME_DIR:-$HOME/.swiftly}/env.sh" && \ - hash -r - swiftly install --use 6.1.2 - swiftly use 6.1.2 - echo "~/.swiftly/bin" >> $GITHUB_PATH - echo "Swiftly installed successfully" - - name: Install Swift with swiftly (Ubuntu) - if: runner.os == 'Linux' - run: | - echo "=== Installing swiftly on Ubuntu ===" - curl -O https://download.swift.org/swiftly/linux/swiftly-$(uname -m).tar.gz && \ - tar zxf swiftly-$(uname -m).tar.gz && \ - ./swiftly init --quiet-shell-followup && \ - . "${SWIFTLY_HOME_DIR:-$HOME/.local/share/swiftly}/env.sh" && \ - hash -r - swiftly install --use 6.1.2 - swiftly use 6.1.2 - sudo apt-get -y install libcurl4-openssl-dev - echo "=== Adding Swift toolchain to PATH ===" - echo "$HOME/.local/share/swiftly/bin" >> $GITHUB_PATH - echo "Swiftly installed successfully!" + - name: Install swift + if: runner.os != 'Windows' + uses: swift-actions/setup-swift@v2 - name: Install Ruby uses: ruby/setup-ruby@v1 with: From 80ff428012da7b898ef52cf644bb46bddc91a3f1 Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Thu, 7 Aug 2025 22:45:24 +0200 Subject: [PATCH 5/6] Install xtools explictly after swift install action --- .github/workflows/pytest.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 88424dcb..04ca150f 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -73,6 +73,10 @@ jobs: - name: Install swift if: runner.os != 'Windows' uses: swift-actions/setup-swift@v2 + # Installation of swift screws with installation of ruby on macOS for some reason + - name: Install Xcode Command Line Tools + run: xcode-select --install || true + if: runner.os == 'macOS' - name: Install Ruby uses: ruby/setup-ruby@v1 with: From 4fe88b1817efc56514e8bb889be1eda13166b47d Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Fri, 8 Aug 2025 00:00:21 +0200 Subject: [PATCH 6/6] Revert back to custom code for installing swift --- .github/workflows/pytest.yml | 39 ++++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 04ca150f..41c3f186 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -70,13 +70,40 @@ jobs: with: terraform_version: "1.5.0" terraform_wrapper: false - - name: Install swift - if: runner.os != 'Windows' - uses: swift-actions/setup-swift@v2 - # Installation of swift screws with installation of ruby on macOS for some reason - - name: Install Xcode Command Line Tools - run: xcode-select --install || true + # - name: Install swift + # if: runner.os != 'Windows' + # uses: swift-actions/setup-swift@v2 + # Installation of swift with the action screws with installation of ruby on macOS for some reason + # We can try again when version 3 of the action is released, where they will also use swiftly + # Until then, we use custom code to install swift. Sourcekit-lsp is installed automatically with swift + - name: Install Swift with swiftly (macOS) if: runner.os == 'macOS' + run: | + echo "=== Installing swiftly on macOS ===" + curl -O https://download.swift.org/swiftly/darwin/swiftly.pkg && \ + installer -pkg swiftly.pkg -target CurrentUserHomeDirectory && \ + ~/.swiftly/bin/swiftly init --quiet-shell-followup && \ + . "${SWIFTLY_HOME_DIR:-$HOME/.swiftly}/env.sh" && \ + hash -r + swiftly install --use 6.1.2 + swiftly use 6.1.2 + echo "~/.swiftly/bin" >> $GITHUB_PATH + echo "Swiftly installed successfully" + - name: Install Swift with swiftly (Ubuntu) + if: runner.os == 'Linux' + run: | + echo "=== Installing swiftly on Ubuntu ===" + curl -O https://download.swift.org/swiftly/linux/swiftly-$(uname -m).tar.gz && \ + tar zxf swiftly-$(uname -m).tar.gz && \ + ./swiftly init --quiet-shell-followup && \ + . "${SWIFTLY_HOME_DIR:-$HOME/.local/share/swiftly}/env.sh" && \ + hash -r + swiftly install --use 6.1.2 + swiftly use 6.1.2 + sudo apt-get -y install libcurl4-openssl-dev + echo "=== Adding Swift toolchain to PATH ===" + echo "$HOME/.local/share/swiftly/bin" >> $GITHUB_PATH + echo "Swiftly installed successfully!" - name: Install Ruby uses: ruby/setup-ruby@v1 with: