Merge pull request #251 from mdbenito/feature/clojure-lsp

Support for clojure via clojure-lsp
This commit is contained in:
Miguel de Benito Delgado authored and GitHub committed 2025-06-29 11:51:47 +02:00
commit 808cc86248
13 files changed
+577 -75

No files matched your search

+10 -1
View File
@@ -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
+7
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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")
+3
View File
@@ -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))
+16 -1
View File
@@ -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"],
)
+17
View File
@@ -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")
+216
View File
@@ -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}"