mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 10:16:00 +00:00
Merge pull request #251 from mdbenito/feature/clojure-lsp
Support for clojure via clojure-lsp
This commit is contained in:
13 files changed
+577
-75
No files matched your search
@@ -35,6 +35,15 @@ jobs:
|
||||
- name: Install gopls
|
||||
shell: bash
|
||||
run: go install golang.org/x/tools/gopls@latest
|
||||
- name: Prepare java
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: "17"
|
||||
- name: Install clojure tools
|
||||
uses: DeLaGuardo/setup-clojure@13.4
|
||||
with:
|
||||
cli: latest
|
||||
- name: Install uv
|
||||
shell: bash
|
||||
run: curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
@@ -55,4 +64,4 @@ jobs:
|
||||
run: uv pip install -e ".[dev]"
|
||||
- name: Test with pytest
|
||||
shell: bash
|
||||
run: uv run poe test
|
||||
run: uv run poe test
|
||||
@@ -200,10 +200,17 @@ pylint.html
|
||||
/*.yml
|
||||
!*.template.yml
|
||||
/agent-ui
|
||||
|
||||
# dynamic LS installations
|
||||
/src/multilspy/language_servers/*/static
|
||||
/src/solidlsp/language_servers/*/static
|
||||
|
||||
# clojure-lsp temporary files
|
||||
.calva/
|
||||
.clj-kondo/
|
||||
.cpcache/
|
||||
.lsp/
|
||||
|
||||
# Cache is in .multilspy
|
||||
**/.multilspy/**
|
||||
|
||||
|
||||
+36
-72
@@ -1,21 +1,17 @@
|
||||
[build-system]
|
||||
build-backend = "hatchling.build"
|
||||
requires = [
|
||||
"hatchling"
|
||||
]
|
||||
requires = ["hatchling"]
|
||||
|
||||
[project]
|
||||
name = "serena"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = [
|
||||
{name = "Oraios AI", email = "info@oraios-ai.de"}
|
||||
]
|
||||
authors = [{ name = "Oraios AI", email = "info@oraios-ai.de" }]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11, <3.12"
|
||||
classifiers = [
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3.11"
|
||||
"Programming Language :: Python :: 3.11",
|
||||
]
|
||||
dependencies = [
|
||||
"requests>=2.32.3,<3",
|
||||
@@ -59,16 +55,9 @@ dev = [
|
||||
"types-pyyaml>=6.0.12.20241230",
|
||||
"syrupy>=4.9.1",
|
||||
]
|
||||
agno = [
|
||||
"agno>=1.2.6",
|
||||
"sqlalchemy>=2.0.40",
|
||||
]
|
||||
anthropic = [
|
||||
"anthropic>=0.49.0",
|
||||
]
|
||||
google = [
|
||||
"google-genai>=1.8.0",
|
||||
]
|
||||
agno = ["agno>=1.2.6", "sqlalchemy>=2.0.40"]
|
||||
anthropic = ["anthropic>=0.49.0"]
|
||||
google = ["google-genai>=1.8.0"]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/oraios/serena"
|
||||
@@ -78,9 +67,7 @@ packages = ["src/serena", "src/interprompt", "src/solidlsp"]
|
||||
|
||||
[tool.black]
|
||||
line-length = 140
|
||||
target-version = [
|
||||
"py311"
|
||||
]
|
||||
target-version = ["py311"]
|
||||
exclude = '''
|
||||
/(
|
||||
src/solidlsp/language_servers/.*/static|src/multilspy
|
||||
@@ -122,26 +109,15 @@ _black_check = "black --check src scripts test"
|
||||
_ruff_check = "ruff check src scripts test"
|
||||
_black_format = "black src scripts test"
|
||||
_ruff_format = "ruff check --fix src scripts test"
|
||||
lint = [
|
||||
"_black_check",
|
||||
"_ruff_check",
|
||||
]
|
||||
format = [
|
||||
"_ruff_format",
|
||||
"_black_format"
|
||||
]
|
||||
lint = ["_black_check", "_ruff_check"]
|
||||
format = ["_ruff_format", "_black_format"]
|
||||
_mypy = "mypy src/serena"
|
||||
type-check = [
|
||||
"_mypy",
|
||||
]
|
||||
type-check = ["_mypy"]
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py311"
|
||||
line-length = 140
|
||||
exclude = [
|
||||
"src/solidlsp/language_servers/**/static",
|
||||
"src/multilspy",
|
||||
]
|
||||
exclude = ["src/solidlsp/language_servers/**/static", "src/multilspy"]
|
||||
|
||||
[tool.ruff.format]
|
||||
quote-style = "double"
|
||||
@@ -176,7 +152,7 @@ select = [
|
||||
"TID",
|
||||
"UP",
|
||||
"W",
|
||||
"YTT"
|
||||
"YTT",
|
||||
]
|
||||
ignore = [
|
||||
"RUF002",
|
||||
@@ -236,51 +212,39 @@ ignore = [
|
||||
"W291",
|
||||
"W293",
|
||||
"B009",
|
||||
"SIM103", # forbids multiple returns
|
||||
"SIM110", # requires use of any(...) instead of for-loop
|
||||
"G001", # forbids str.format in log statements
|
||||
"E722", # forbids unspecific except clause
|
||||
"SIM105", # forbids empty/general except clause
|
||||
"SIM113", # wants to enforce use of enumerate
|
||||
"E712", # forbids equality comparison with True/False
|
||||
"UP007", # forbids some uses of Union
|
||||
"TID252", # forbids relative imports
|
||||
"B904", # forces use of raise from other_exception
|
||||
"RUF012", # forbids mutable attributes as ClassVar
|
||||
]
|
||||
unfixable = [
|
||||
"F841",
|
||||
"F601",
|
||||
"F602",
|
||||
"B018"
|
||||
]
|
||||
extend-fixable = [
|
||||
"F401",
|
||||
"B905",
|
||||
"W291"
|
||||
"SIM103", # forbids multiple returns
|
||||
"SIM110", # requires use of any(...) instead of for-loop
|
||||
"G001", # forbids str.format in log statements
|
||||
"E722", # forbids unspecific except clause
|
||||
"SIM105", # forbids empty/general except clause
|
||||
"SIM113", # wants to enforce use of enumerate
|
||||
"E712", # forbids equality comparison with True/False
|
||||
"UP007", # forbids some uses of Union
|
||||
"TID252", # forbids relative imports
|
||||
"B904", # forces use of raise from other_exception
|
||||
"RUF012", # forbids mutable attributes as ClassVar
|
||||
]
|
||||
unfixable = ["F841", "F601", "F602", "B018"]
|
||||
extend-fixable = ["F401", "B905", "W291"]
|
||||
|
||||
[tool.ruff.lint.mccabe]
|
||||
max-complexity = 20
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"tests/**" = [
|
||||
"D103"
|
||||
]
|
||||
"scripts/**" = [
|
||||
"D103"
|
||||
]
|
||||
"tests/**" = ["D103"]
|
||||
"scripts/**" = ["D103"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
markers = [
|
||||
"python: language server running for Python",
|
||||
"go: language server running for Go",
|
||||
"java: language server running for Java",
|
||||
"rust: language server running for Rust",
|
||||
"typescript: language server running for TypeScript",
|
||||
"php: language server running for PHP",
|
||||
"snapshot: snapshot tests for symbolic editing operations",
|
||||
"isolated_process: test runs with process isolated agent",
|
||||
"clojure: language server running for Clojure",
|
||||
"python: language server running for Python",
|
||||
"go: language server running for Go",
|
||||
"java: language server running for Java",
|
||||
"rust: language server running for Rust",
|
||||
"typescript: language server running for TypeScript",
|
||||
"php: language server running for PHP",
|
||||
"snapshot: snapshot tests for symbolic editing operations",
|
||||
"isolated_process: test runs with process isolated agent",
|
||||
]
|
||||
|
||||
[tool.codespell]
|
||||
|
||||
+12
@@ -1,2 +1,14 @@
|
||||
[pytest]
|
||||
addopts = --snapshot-patch-pycharm-diff
|
||||
# this shouldn't be necessary since it's already in pyproject.toml
|
||||
# but tests show lots of warnings without it
|
||||
markers =
|
||||
clojure: language server running for Clojure
|
||||
python: language server running for Python
|
||||
go: language server running for Go
|
||||
java: language server running for Java
|
||||
rust: language server running for Rust
|
||||
typescript: language server running for TypeScript
|
||||
php: language server running for PHP
|
||||
snapshot: snapshot tests for symbolic editing operations
|
||||
isolated_process: test runs with process isolated agent
|
||||
@@ -0,0 +1,209 @@
|
||||
"""
|
||||
Provides Clojure specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Clojure.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import threading
|
||||
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_logger import LanguageServerLogger
|
||||
from solidlsp.ls_utils import FileUtils, PlatformUtils
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
|
||||
|
||||
def run_command(cmd: list, capture_output: bool = True) -> subprocess.CompletedProcess:
|
||||
return subprocess.run(
|
||||
cmd, stdout=subprocess.PIPE if capture_output else None, stderr=subprocess.STDOUT if capture_output else None, text=True, check=True
|
||||
)
|
||||
|
||||
|
||||
def verify_clojure_cli():
|
||||
install_msg = "Please install the official Clojure CLI from:\n https://clojure.org/guides/getting_started"
|
||||
if shutil.which("clojure") is None:
|
||||
raise FileNotFoundError("`clojure` not found.\n" + install_msg)
|
||||
|
||||
help_proc = run_command(["clojure", "--help"])
|
||||
if "-Aaliases" not in help_proc.stdout:
|
||||
raise RuntimeError("Detected a Clojure executable, but it does not support '-Aaliases'.\n" + install_msg)
|
||||
|
||||
spath_proc = run_command(["clojure", "-Spath"], capture_output=False)
|
||||
if spath_proc.returncode != 0:
|
||||
raise RuntimeError("`clojure -Spath` failed; please upgrade to Clojure CLI ≥ 1.10.")
|
||||
|
||||
|
||||
class ClojureLSP(SolidLanguageServer):
|
||||
"""
|
||||
Provides a clojure-lsp specific instantiation of the LanguageServer class. Contains various configurations and settings specific to clojure.
|
||||
"""
|
||||
|
||||
clojure_lsp_releases = "https://github.com/clojure-lsp/clojure-lsp/releases/latest/download"
|
||||
runtime_dependencies = [
|
||||
{
|
||||
"url": f"{clojure_lsp_releases}/clojure-lsp-native-macos-aarch64.zip",
|
||||
"platformId": "osx-arm64",
|
||||
"archiveType": "zip",
|
||||
"binaryName": "clojure-lsp",
|
||||
},
|
||||
{
|
||||
"url": f"{clojure_lsp_releases}/clojure-lsp-native-linux-aarch64.zip",
|
||||
"platformId": "linux-arm64",
|
||||
"archiveType": "zip",
|
||||
"binaryName": "clojure-lsp",
|
||||
},
|
||||
{
|
||||
"url": f"{clojure_lsp_releases}/clojure-lsp-native-linux-amd64.zip",
|
||||
"platformId": "linux-x64",
|
||||
"archiveType": "zip",
|
||||
"binaryName": "clojure-lsp",
|
||||
},
|
||||
{
|
||||
"url": f"{clojure_lsp_releases}/clojure-lsp-native-windows-amd64.zip",
|
||||
"platformId": "win-x64",
|
||||
"archiveType": "zip",
|
||||
"binaryName": "clojure-lsp.exe",
|
||||
},
|
||||
]
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str):
|
||||
"""
|
||||
Creates a ClojureLSP instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
|
||||
"""
|
||||
clojure_lsp_executable_path = self.setup_runtime_dependencies(logger, config)
|
||||
super().__init__(
|
||||
config,
|
||||
logger,
|
||||
repository_root_path,
|
||||
ProcessLaunchInfo(cmd=clojure_lsp_executable_path, cwd=repository_root_path),
|
||||
"clojure",
|
||||
)
|
||||
self.server_ready = threading.Event()
|
||||
self.initialize_searcher_command_available = threading.Event()
|
||||
self.resolve_main_method_available = threading.Event()
|
||||
self.service_ready_event = threading.Event()
|
||||
|
||||
def setup_runtime_dependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> str:
|
||||
"""Setup runtime dependencies for clojure-lsp."""
|
||||
verify_clojure_cli()
|
||||
platform_id = PlatformUtils.get_platform_id()
|
||||
|
||||
runtime_dependencies = [dependency for dependency in self.runtime_dependencies if dependency["platformId"] == platform_id.value]
|
||||
if len(runtime_dependencies) != 1:
|
||||
raise RuntimeError(f"Could not find a suitable clojure-lsp runtime dependency for platform {platform_id.value}.")
|
||||
dependency = runtime_dependencies[0]
|
||||
|
||||
clojurelsp_ls_dir = os.path.join(os.path.dirname(__file__), "static", "clojure-lsp")
|
||||
clojurelsp_executable_path = os.path.join(clojurelsp_ls_dir, dependency["binaryName"])
|
||||
if not os.path.exists(clojurelsp_executable_path):
|
||||
os.makedirs(clojurelsp_ls_dir, exist_ok=True)
|
||||
logger.log(f"Downloading and extracting clojure-lsp from {dependency['url']} to {clojurelsp_ls_dir}", logging.INFO)
|
||||
FileUtils.download_and_extract_archive(logger, dependency["url"], clojurelsp_ls_dir, dependency["archiveType"])
|
||||
if not os.path.exists(clojurelsp_executable_path):
|
||||
raise FileNotFoundError(f"Download failed? Could not find clojure-lsp executable at {clojurelsp_executable_path}")
|
||||
os.chmod(clojurelsp_executable_path, stat.S_IEXEC)
|
||||
return clojurelsp_executable_path
|
||||
|
||||
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
|
||||
"""Returns the init params for clojure-lsp."""
|
||||
root_uri = pathlib.Path(repository_absolute_path).as_uri()
|
||||
return { # type: ignore
|
||||
"processId": os.getpid(),
|
||||
"rootPath": repository_absolute_path,
|
||||
"rootUri": root_uri,
|
||||
"clientInfo": {"name": "solidlsp", "version": "0.1.0"},
|
||||
"capabilities": {
|
||||
"workspace": {
|
||||
"applyEdit": True,
|
||||
"workspaceEdit": {"documentChanges": True},
|
||||
"symbol": {
|
||||
"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]
|
||||
}
|
||||
},
|
||||
"workspaceFolders": True,
|
||||
},
|
||||
"textDocument": {
|
||||
"synchronization": {"didSave": True},
|
||||
"publishDiagnostics": {"relatedInformation": True, "tagSupport": {"valueSet": [1, 2]}},
|
||||
"definition": {"linkSupport": True},
|
||||
"references": {},
|
||||
"completion": {"completionItem": {"snippetSupport": True, "documentationFormat": ["markdown", "plaintext"]}},
|
||||
"hover": {"contentFormat": ["markdown", "plaintext"]},
|
||||
"documentSymbol": {
|
||||
"hierarchicalDocumentSymbolSupport": 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]
|
||||
},
|
||||
},
|
||||
},
|
||||
"general": {"positionEncodings": ["utf-16"]},
|
||||
},
|
||||
"initializationOptions": {"dependency-scheme": "jar", "text-document-sync-kind": "incremental"},
|
||||
"trace": "off",
|
||||
"workspaceFolders": [{"uri": root_uri, "name": os.path.basename(repository_absolute_path)}],
|
||||
}
|
||||
|
||||
def _start_server(self):
|
||||
def register_capability_handler(params):
|
||||
assert "registrations" in params
|
||||
for registration in params["registrations"]:
|
||||
if registration["method"] == "workspace/executeCommand":
|
||||
self.initialize_searcher_command_available.set()
|
||||
self.resolve_main_method_available.set()
|
||||
return
|
||||
|
||||
def lang_status_handler(params):
|
||||
# TODO: Should we wait for
|
||||
# server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}}
|
||||
# Before proceeding?
|
||||
if params["type"] == "ServiceReady" and params["message"] == "ServiceReady":
|
||||
self.service_ready_event.set()
|
||||
|
||||
def execute_client_command_handler(params):
|
||||
return []
|
||||
|
||||
def do_nothing(params):
|
||||
return
|
||||
|
||||
def check_experimental_status(params):
|
||||
if params["quiescent"] == True:
|
||||
self.server_ready.set()
|
||||
|
||||
def window_log_message(msg):
|
||||
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
|
||||
|
||||
self.server.on_request("client/registerCapability", register_capability_handler)
|
||||
self.server.on_notification("language/status", lang_status_handler)
|
||||
self.server.on_notification("window/logMessage", window_log_message)
|
||||
self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
|
||||
self.server.on_notification("$/progress", do_nothing)
|
||||
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
|
||||
self.server.on_notification("language/actionableNotification", do_nothing)
|
||||
self.server.on_notification("experimental/serverStatus", check_experimental_status)
|
||||
|
||||
self.logger.log("Starting clojure-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)
|
||||
assert init_response["capabilities"]["textDocumentSync"]["change"] == 2
|
||||
assert "completionProvider" in init_response["capabilities"]
|
||||
# Clojure-lsp completion provider capabilities are more flexible than other servers'
|
||||
completion_provider = init_response["capabilities"]["completionProvider"]
|
||||
assert completion_provider["resolveProvider"] == True
|
||||
assert "triggerCharacters" in completion_provider
|
||||
self.server.notify.initialized({})
|
||||
# after initialize, Clojure-lsp is ready to serve
|
||||
self.server_ready.set()
|
||||
self.completions_available.set()
|
||||
+6
-1
@@ -171,6 +171,11 @@ class SolidLanguageServer(ABC):
|
||||
|
||||
ls = Intelephense(config, logger, repository_root_path)
|
||||
|
||||
elif config.code_language == Language.CLOJURE:
|
||||
from solidlsp.language_servers.clojure_lsp.clojure_lsp import ClojureLSP
|
||||
|
||||
ls = ClojureLSP(config, logger, repository_root_path)
|
||||
|
||||
else:
|
||||
logger.log(f"Language {config.code_language} is not supported", logging.ERROR)
|
||||
raise LanguageServerException(f"Language {config.code_language} is not supported")
|
||||
@@ -516,7 +521,7 @@ class SolidLanguageServer(ABC):
|
||||
"""
|
||||
if not self.server_started:
|
||||
self.logger.log(
|
||||
"find_function_definition called before Language Server started",
|
||||
"request_definition called before Language Server started",
|
||||
logging.ERROR,
|
||||
)
|
||||
raise LanguageServerException("Language Server not started")
|
||||
|
||||
@@ -37,6 +37,7 @@ class Language(str, Enum):
|
||||
DART = "dart"
|
||||
CPP = "cpp"
|
||||
PHP = "php"
|
||||
CLOJURE = "clojure"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
@@ -71,6 +72,8 @@ class Language(str, Enum):
|
||||
return FilenameMatcher("*.dart")
|
||||
case self.PHP:
|
||||
return FilenameMatcher("*.php")
|
||||
case self.CLOJURE:
|
||||
return FilenameMatcher("*.clj", "*.cljs", "*.cljc", "*.edn") # codespell:ignore edn
|
||||
case _:
|
||||
raise ValueError(f"Unhandled language: {self}")
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{:paths ["src"]
|
||||
:deps {org.clojure/clojure {:mvn/version "1.11.1"}}
|
||||
:aliases
|
||||
{:test {:extra-paths ["test"]
|
||||
:extra-deps {org.clojure/test.check {:mvn/version "1.1.1"}}}}}
|
||||
@@ -0,0 +1,23 @@
|
||||
(ns test-app.core)
|
||||
|
||||
(defn greet
|
||||
"A simple greeting function"
|
||||
[name]
|
||||
(str "Hello, " name "!"))
|
||||
|
||||
(defn add
|
||||
"Adds two numbers"
|
||||
[a b]
|
||||
(+ a b))
|
||||
|
||||
(defn multiply
|
||||
"Multiplies two numbers"
|
||||
[a b]
|
||||
(* a b))
|
||||
|
||||
(defn -main
|
||||
"Main entry point"
|
||||
[& args]
|
||||
(println (greet "World"))
|
||||
(println "2 + 3 =" (add 2 3))
|
||||
(println "4 * 5 =" (multiply 4 5)))
|
||||
@@ -0,0 +1,17 @@
|
||||
(ns test-app.utils
|
||||
(:require [test-app.core :as core]))
|
||||
|
||||
(defn calculate-area
|
||||
"Calculates the area of a rectangle"
|
||||
[width height]
|
||||
(core/multiply width height))
|
||||
|
||||
(defn format-greeting
|
||||
"Formats a greeting message"
|
||||
[name]
|
||||
(str "Welcome, " (core/greet name)))
|
||||
|
||||
(defn sum-list
|
||||
"Sums a list of numbers"
|
||||
[numbers]
|
||||
(reduce core/add 0 numbers))
|
||||
@@ -5,6 +5,7 @@ from dataclasses import dataclass
|
||||
|
||||
import pytest
|
||||
|
||||
import test.solidlsp.clojure as clj
|
||||
from serena.agent import FindReferencingSymbolsTool, FindSymbolTool, Project, ProjectConfig, SerenaAgent, SerenaConfigBase
|
||||
from serena.process_isolated_agent import ProcessIsolatedSerenaAgent
|
||||
from solidlsp.ls_config import Language
|
||||
@@ -31,7 +32,7 @@ def serena_config():
|
||||
"""Create an in-memory configuration for tests with test repositories pre-registered."""
|
||||
# Create test projects for all supported languages
|
||||
test_projects = []
|
||||
for language in [Language.PYTHON, Language.GO, Language.JAVA, Language.RUST, Language.TYPESCRIPT, Language.PHP]:
|
||||
for language in [Language.PYTHON, Language.GO, Language.JAVA, Language.RUST, Language.TYPESCRIPT, Language.PHP, Language.CLOJURE]:
|
||||
repo_path = get_repo_path(language)
|
||||
if repo_path.exists():
|
||||
project_name = f"test_repo_{language}"
|
||||
@@ -94,6 +95,13 @@ class TestSerenaAgent:
|
||||
pytest.param(Language.RUST, "add", "Function", "lib.rs", marks=pytest.mark.rust),
|
||||
pytest.param(Language.TYPESCRIPT, "DemoClass", "Class", "index.ts", marks=pytest.mark.typescript),
|
||||
pytest.param(Language.PHP, "helperFunction", "Function", "helper.php", marks=pytest.mark.php),
|
||||
pytest.param(
|
||||
Language.CLOJURE,
|
||||
"greet",
|
||||
"Function",
|
||||
clj.CORE_PATH,
|
||||
marks=[pytest.mark.clojure, pytest.mark.skipif(clj.CLI_FAIL, reason=f"Clojure CLI not available: {clj.CLI_FAIL}")],
|
||||
),
|
||||
],
|
||||
indirect=["serena_agent"],
|
||||
)
|
||||
@@ -132,6 +140,13 @@ class TestSerenaAgent:
|
||||
pytest.param(Language.RUST, "add", os.path.join("src", "lib.rs"), os.path.join("src", "main.rs"), marks=pytest.mark.rust),
|
||||
pytest.param(Language.TYPESCRIPT, "helperFunction", "index.ts", "use_helper.ts", marks=pytest.mark.typescript),
|
||||
pytest.param(Language.PHP, "helperFunction", "helper.php", "index.php", marks=pytest.mark.php),
|
||||
pytest.param(
|
||||
Language.CLOJURE,
|
||||
"multiply",
|
||||
clj.CORE_PATH,
|
||||
clj.UTILS_PATH,
|
||||
marks=[pytest.mark.clojure, pytest.mark.skipif(clj.CLI_FAIL, reason=f"Clojure CLI not available: {clj.CLI_FAIL}")],
|
||||
),
|
||||
],
|
||||
indirect=["serena_agent"],
|
||||
)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
from pathlib import Path
|
||||
|
||||
from solidlsp.language_servers.clojure_lsp.clojure_lsp import verify_clojure_cli
|
||||
|
||||
|
||||
def _test_clojure_cli() -> str | bool:
|
||||
try:
|
||||
verify_clojure_cli()
|
||||
return False
|
||||
except (FileNotFoundError, RuntimeError) as e:
|
||||
return str(e)
|
||||
|
||||
|
||||
CLI_FAIL = _test_clojure_cli()
|
||||
TEST_APP_PATH = Path("src") / "test_app"
|
||||
CORE_PATH = str(TEST_APP_PATH / "core.clj")
|
||||
UTILS_PATH = str(TEST_APP_PATH / "utils.clj")
|
||||
@@ -0,0 +1,216 @@
|
||||
import pytest
|
||||
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls_config import Language
|
||||
from solidlsp.ls_types import UnifiedSymbolInformation
|
||||
|
||||
from . import CLI_FAIL, CORE_PATH, UTILS_PATH
|
||||
|
||||
|
||||
@pytest.mark.clojure
|
||||
@pytest.mark.skipif(CLI_FAIL, reason=f"Clojure CLI not available: {CLI_FAIL}")
|
||||
class TestLanguageServerBasics:
|
||||
@pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
|
||||
def test_basic_definition(self, language_server: SolidLanguageServer):
|
||||
"""
|
||||
Test finding definition of 'greet' function call in core.clj
|
||||
"""
|
||||
result = language_server.request_definition(CORE_PATH, 20, 12) # Position of 'greet' in (greet "World")
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert len(result) >= 1
|
||||
|
||||
definition = result[0]
|
||||
assert definition["relativePath"] == CORE_PATH
|
||||
assert definition["range"]["start"]["line"] == 2, "Should find the definition of greet function at line 2"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
|
||||
def test_cross_file_references(self, language_server: SolidLanguageServer):
|
||||
"""
|
||||
Test finding references to 'multiply' function from core.clj
|
||||
"""
|
||||
result = language_server.request_references(CORE_PATH, 12, 6)
|
||||
|
||||
assert isinstance(result, list) and len(result) >= 2, "Should find definition + usage in utils.clj"
|
||||
|
||||
usage_found = any(
|
||||
item["relativePath"] == UTILS_PATH and item["range"]["start"]["line"] == 6 # multiply usage in calculate-area
|
||||
for item in result
|
||||
)
|
||||
assert usage_found, "Should find multiply usage in utils.clj"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
|
||||
def test_completions(self, language_server: SolidLanguageServer):
|
||||
with language_server.open_file(UTILS_PATH):
|
||||
# After "core/" in calculate-area
|
||||
result = language_server.request_completions(UTILS_PATH, 6, 8)
|
||||
|
||||
assert isinstance(result, list) and len(result) > 0
|
||||
|
||||
completion_texts = [item["completionText"] for item in result]
|
||||
assert any("multiply" in text for text in completion_texts), "Should find 'multiply' function in completions after 'core/'"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
|
||||
def test_document_symbols(self, language_server: SolidLanguageServer):
|
||||
symbols, _ = language_server.request_document_symbols(CORE_PATH)
|
||||
|
||||
assert isinstance(symbols, list) and len(symbols) >= 4, "greet, add, multiply, -main functions"
|
||||
|
||||
# Check that we find the expected function symbols
|
||||
symbol_names = [symbol["name"] for symbol in symbols]
|
||||
expected_functions = ["greet", "add", "multiply", "-main"]
|
||||
|
||||
for func_name in expected_functions:
|
||||
assert func_name in symbol_names, f"Should find {func_name} function in symbols"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
|
||||
def test_hover(self, language_server: SolidLanguageServer):
|
||||
"""Test hover on greet function"""
|
||||
result = language_server.request_hover(CORE_PATH, 2, 7)
|
||||
|
||||
assert result is not None, "Hover should return information for greet function"
|
||||
assert "contents" in result
|
||||
# Should contain function signature or documentation
|
||||
contents = result["contents"]
|
||||
if isinstance(contents, str):
|
||||
assert "greet" in contents.lower()
|
||||
elif isinstance(contents, dict) and "value" in contents:
|
||||
assert "greet" in contents["value"].lower()
|
||||
else:
|
||||
assert False, f"Unexpected contents format: {type(contents)}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
|
||||
def test_workspace_symbols(self, language_server: SolidLanguageServer):
|
||||
# Search for functions containing "add"
|
||||
result = language_server.request_workspace_symbol("add")
|
||||
|
||||
assert isinstance(result, list) and len(result) > 0, "Should find at least one symbol containing 'add'"
|
||||
|
||||
# Should find the 'add' function
|
||||
symbol_names = [symbol["name"] for symbol in result]
|
||||
assert any("add" in name.lower() for name in symbol_names), f"Should find 'add' function in symbols: {symbol_names}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
|
||||
def test_retrieve_content_around_line(self, language_server: SolidLanguageServer):
|
||||
"""Test retrieving content around specific lines"""
|
||||
# Test retrieving content around the greet function definition (line 2)
|
||||
result = language_server.retrieve_content_around_line(CORE_PATH, 2, 2)
|
||||
|
||||
assert result is not None, "Should retrieve content around line 2"
|
||||
content_str = result.to_display_string()
|
||||
assert "greet" in content_str, "Should contain the greet function definition"
|
||||
assert "defn" in content_str, "Should contain defn keyword"
|
||||
|
||||
# Test retrieving content around multiply function (around line 13)
|
||||
result = language_server.retrieve_content_around_line(CORE_PATH, 13, 1)
|
||||
|
||||
assert result is not None, "Should retrieve content around line 13"
|
||||
content_str = result.to_display_string()
|
||||
assert "multiply" in content_str, "Should contain multiply function"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
|
||||
def test_namespace_functions(self, language_server: SolidLanguageServer):
|
||||
"""Test definition lookup for core/greet usage in utils.clj"""
|
||||
# Position of 'greet' in core/greet call
|
||||
result = language_server.request_definition(UTILS_PATH, 11, 25)
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert len(result) >= 1
|
||||
|
||||
definition = result[0]
|
||||
assert definition["relativePath"] == CORE_PATH, "Should find the definition of greet in core.clj"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
|
||||
def test_search_files_for_pattern(self, language_server: SolidLanguageServer):
|
||||
result = language_server.search_files_for_pattern("defn.*greet")
|
||||
|
||||
assert result is not None, "Pattern search should return results"
|
||||
assert len(result) > 0, "Should find at least one match for 'defn.*greet'"
|
||||
|
||||
core_matches = [match for match in result if match.source_file_path and "core.clj" in match.source_file_path]
|
||||
assert len(core_matches) > 0, "Should find greet function in core.clj"
|
||||
|
||||
result = language_server.search_files_for_pattern(":require")
|
||||
|
||||
assert result is not None, "Should find require statements"
|
||||
utils_matches = [match for match in result if match.source_file_path and "utils.clj" in match.source_file_path]
|
||||
assert len(utils_matches) > 0, "Should find require statement in utils.clj"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
|
||||
def test_request_references_with_content(self, language_server: SolidLanguageServer):
|
||||
"""Test references to multiply function with content"""
|
||||
result = language_server.request_references_with_content(CORE_PATH, 12, 6, 3)
|
||||
|
||||
assert result is not None, "Should find references with content"
|
||||
assert isinstance(result, list)
|
||||
assert len(result) >= 2, "Should find definition + usage in utils.clj"
|
||||
|
||||
for ref in result:
|
||||
assert ref.source_file_path is not None, "Each reference should have a source file path"
|
||||
content_str = ref.to_display_string()
|
||||
assert len(content_str) > 0, "Content should not be empty"
|
||||
|
||||
# Verify we find the reference in utils.clj with context
|
||||
utils_refs = [ref for ref in result if ref.source_file_path and "utils.clj" in ref.source_file_path]
|
||||
assert len(utils_refs) > 0, "Should find reference in utils.clj"
|
||||
|
||||
# The context should contain the calculate-area function
|
||||
utils_content = utils_refs[0].to_display_string()
|
||||
assert "calculate-area" in utils_content
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
|
||||
def test_request_full_symbol_tree(self, language_server: SolidLanguageServer):
|
||||
"""Test retrieving the full symbol tree for project overview
|
||||
We just check that we find some expected symbols.
|
||||
"""
|
||||
result = language_server.request_full_symbol_tree()
|
||||
|
||||
assert result is not None, "Should return symbol tree"
|
||||
assert isinstance(result, list), "Symbol tree should be a list"
|
||||
assert len(result) > 0, "Should find symbols in the project"
|
||||
|
||||
def traverse_symbols(symbols, indent=0):
|
||||
"""Recursively traverse symbols to print their structure"""
|
||||
info = []
|
||||
for s in symbols:
|
||||
name = getattr(s, "name", "NO_NAME")
|
||||
kind = getattr(s, "kind", "NO_KIND")
|
||||
info.append(f"{' ' * indent}Symbol: {name}, Kind: {kind}")
|
||||
if hasattr(s, "children") and s.children:
|
||||
info.append(" " * indent + "Children:")
|
||||
info.extend(traverse_symbols(s.children, indent + 2))
|
||||
return info
|
||||
|
||||
def list_all_symbols(symbols: list[UnifiedSymbolInformation]):
|
||||
found = []
|
||||
for symbol in symbols:
|
||||
found.append(symbol["name"])
|
||||
found.extend(list_all_symbols(symbol["children"]))
|
||||
return found
|
||||
|
||||
all_symbol_names = list_all_symbols(result)
|
||||
|
||||
expected_symbols = ["greet", "add", "multiply", "-main", "calculate-area", "format-greeting", "sum-list"]
|
||||
found_expected = [name for name in expected_symbols if any(name in symbol_name for symbol_name in all_symbol_names)]
|
||||
|
||||
if len(found_expected) < 7:
|
||||
pytest.fail(
|
||||
f"Expected to find at least 3 symbols from {expected_symbols}, but found: {found_expected}.\n"
|
||||
f"All symbol names: {all_symbol_names}\n"
|
||||
f"Symbol tree structure:\n{traverse_symbols(result)}"
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
|
||||
def test_request_referencing_symbols(self, language_server: SolidLanguageServer):
|
||||
"""Test finding symbols that reference a given symbol
|
||||
Finds references to the 'multiply' function.
|
||||
"""
|
||||
result = language_server.request_referencing_symbols(CORE_PATH, 12, 6)
|
||||
assert isinstance(result, list) and len(result) > 0, "Should find at least one referencing symbol"
|
||||
found_relevant_references = False
|
||||
for ref in result:
|
||||
if hasattr(ref, "symbol") and "calculate-area" in ref.symbol["name"]:
|
||||
found_relevant_references = True
|
||||
break
|
||||
|
||||
assert found_relevant_references, f"Should have found calculate-area referencing multiply, but got: {result}"
|
||||
Reference in new issue
Block a user