diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index c5dd340f..cfb46229 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" - name: Ensure cached directory exist before calling cache-related actions shell: bash run: | @@ -53,8 +53,9 @@ 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 with: @@ -69,6 +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 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: 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 1d8d1d3d..f37dc3e9 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ With Serena, we provide * Clojure * Bash (Requires Node.js and npm for bash-language-server installation) * 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 4363567b..33ffe5d0 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" @@ -260,6 +260,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", "bash: language server running for Bash", "snapshot: snapshot tests for symbolic editing operations", "ruby: language server running for Ruby", diff --git a/src/solidlsp/language_servers/sourcekit_lsp.py b/src/solidlsp/language_servers/sourcekit_lsp.py new file mode 100644 index 00000000..4a47ffb9 --- /dev/null +++ b/src/solidlsp/language_servers/sourcekit_lsp.py @@ -0,0 +1,358 @@ +import logging +import os +import pathlib +import subprocess +import threading +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 +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.settings import SolidLSPSettings + + +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_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() + else: + raise Exception(f"`sourcekit-lsp -h` resulted in: {result}") + except Exception as e: + raise RuntimeError( + "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 + ): + sourcekit_version = self._get_sourcekit_lsp_version() + logger.log(f"Starting sourcekit lsp with version: {sourcekit_version}", logging.INFO) + + 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 + self._did_sleep_before_requesting_references = False + + @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() + + 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 + 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) diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index edda2d6e..53ed740a 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) + elif config.code_language == Language.BASH: from solidlsp.language_servers.bash_language_server import BashLanguageServer diff --git a/src/solidlsp/ls_config.py b/src/solidlsp/ls_config.py index 3965d066..4bed832e 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" BASH = "bash" # Experimental or deprecated Language Servers TYPESCRIPT_VTS = "typescript_vts" @@ -104,6 +105,8 @@ class Language(str, Enum): return FilenameMatcher("*.ex", "*.exs") case self.TERRAFORM: return FilenameMatcher("*.tf", "*.tfvars", "*.tfstate") + case self.SWIFT: + return FilenameMatcher("*.swift") case self.BASH: return FilenameMatcher("*.sh", "*.bash") case _: 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..67dfe686 --- /dev/null +++ b/test/solidlsp/swift/test_swift_basic.py @@ -0,0 +1,222 @@ +""" +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 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: + """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"