mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-14 02:16:21 +00:00
Merge pull request #437 from oraios/feat/support-swift
This commit is contained in:
11 files changed
+731
-4
No files matched your search
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
+2
-1
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 _:
|
||||
|
||||
@@ -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: []),
|
||||
]
|
||||
)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
Reference in new issue
Block a user