From c7ac81b895b3bce0c5a8c8a629b9c0a929c37da8 Mon Sep 17 00:00:00 2001 From: Miguel de Benito Delgado Date: Sat, 7 Jun 2025 18:22:21 +0000 Subject: [PATCH 01/25] WIP: preliminary support for clojure-lsp --- .../clojure_lsp/clojure_lsp.py | 178 ++++++++++ .../clojure_lsp/initialize_params.json | 332 ++++++++++++++++++ .../clojure_lsp/runtime_dependencies.json | 37 ++ 3 files changed, 547 insertions(+) create mode 100644 src/multilspy/language_servers/clojure_lsp/clojure_lsp.py create mode 100644 src/multilspy/language_servers/clojure_lsp/initialize_params.json create mode 100644 src/multilspy/language_servers/clojure_lsp/runtime_dependencies.json diff --git a/src/multilspy/language_servers/clojure_lsp/clojure_lsp.py b/src/multilspy/language_servers/clojure_lsp/clojure_lsp.py new file mode 100644 index 00000000..796bc2bd --- /dev/null +++ b/src/multilspy/language_servers/clojure_lsp/clojure_lsp.py @@ -0,0 +1,178 @@ +""" +Provides Rust specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Rust. +""" + +import asyncio +import json +import logging +import os +import stat +import pathlib +from contextlib import asynccontextmanager +from typing import AsyncIterator + +from overrides import override + +from multilspy.multilspy_logger import MultilspyLogger +from multilspy.language_server import LanguageServer +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo +from multilspy.lsp_protocol_handler.lsp_types import InitializeParams +from multilspy.multilspy_config import MultilspyConfig +from multilspy.multilspy_utils import FileUtils +from multilspy.multilspy_utils import PlatformUtils + + +class ClojureLSP(LanguageServer): + """ + Provides a clojure-lsp specific instantiation of the LanguageServer class. Contains various configurations and settings specific to clojure. + """ + + def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, 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 = asyncio.Event() + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + return super().is_ignored_dirname(dirname) or dirname in ["target"] + + def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> str: + """ + Setup runtime dependencies for clojure-lsp. + """ + platform_id = PlatformUtils.get_platform_id() + + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r", encoding="utf-8") as f: + d = json.load(f) + del d["_description"] + + runtime_dependencies = d["runtimeDependencies"] + runtime_dependencies = [ + dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value + ] + assert len(runtime_dependencies) == 1 + 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_ls_dir): + os.makedirs(clojurelsp_ls_dir) + FileUtils.download_and_extract_archive( + logger, dependency["url"], clojurelsp_ls_dir, dependency["archiveType"] + ) + assert os.path.exists(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. + """ + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: + d = json.load(f) + + del d["_description"] + + d["processId"] = os.getpid() + assert d["rootPath"] == "$rootPath" + d["rootPath"] = repository_absolute_path + + assert d["rootUri"] == "$rootUri" + d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["uri"] == "$uri" + d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["name"] == "$name" + d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) + + return d + + @asynccontextmanager + async def start_server(self) -> AsyncIterator["ClojureLSP"]: + """ + Starts the Clojure Language Server, waits for the server to be ready and yields the LanguageServer instance. + + Usage: + ``` + async with lsp.start_server(): + # LanguageServer has been initialized and ready to serve requests + await lsp.request_definition(...) + await lsp.request_references(...) + # Shutdown the LanguageServer on exit from scope + # LanguageServer has been shutdown + """ + + async 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 + + async 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() + + async def execute_client_command_handler(params): + return [] + + async def do_nothing(params): + return + + async def check_experimental_status(params): + if params["quiescent"] == True: + self.server_ready.set() + + async 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) + + async with super().start_server(): + self.logger.log("Starting clojure-lsp server process", logging.INFO) + await 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 = await self.server.send.initialize(initialize_params) + assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 + assert "completionProvider" in init_response["capabilities"] + assert init_response["capabilities"]["completionProvider"] == { + "resolveProvider": True, + "triggerCharacters": [":", ".", "'", "("], + "completionItem": {"labelDetailsSupport": True}, + } + self.server.notify.initialized({}) + self.completions_available.set() + + await self.server_ready.wait() + + yield self + + await self.server.shutdown() + await self.server.stop() diff --git a/src/multilspy/language_servers/clojure_lsp/initialize_params.json b/src/multilspy/language_servers/clojure_lsp/initialize_params.json new file mode 100644 index 00000000..a63ef8e1 --- /dev/null +++ b/src/multilspy/language_servers/clojure_lsp/initialize_params.json @@ -0,0 +1,332 @@ +{ + "_description": "Parameters sent by multilspy to initialize clojure-lsp (LSP v3.17)", + "processId": "os.getpid()", + "clientInfo": { + "name": "multilspy", + "version": "0.1.0" + }, + "locale": "en", + "rootPath": "$rootPath", + "rootUri": "$rootUri", + "capabilities": { + "workspace": { + "applyEdit": true, + "workspaceEdit": { + "documentChanges": true, + "resourceOperations": [ + "create", + "rename", + "delete" + ] + }, + "configuration": true, + "didChangeWatchedFiles": { + "dynamicRegistration": true + }, + "symbol": { + "dynamicRegistration": 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 + ] + } + }, + "executeCommand": { + "dynamicRegistration": true + }, + "workspaceFolders": true, + "semanticTokens": { + "refreshSupport": true + }, + "codeLens": { + "refreshSupport": true + }, + "fileOperations": { + "dynamicRegistration": true, + "didRename": true, + "willRename": true + } + }, + "textDocument": { + "publishDiagnostics": { + "relatedInformation": true, + "tagSupport": { + "valueSet": [ + 1, + 2 + ] + }, + "codeDescriptionSupport": true, + "dataSupport": true + }, + "synchronization": { + "dynamicRegistration": true, + "didSave": true + }, + "completion": { + "dynamicRegistration": true, + "contextSupport": true, + "completionItem": { + "snippetSupport": true, + "documentationFormat": [ + "markdown", + "plaintext" + ], + "deprecatedSupport": true, + "preselectSupport": true, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "insertReplaceSupport": true, + "resolveSupport": { + "properties": [ + "documentation", + "detail", + "additionalTextEdits" + ] + } + }, + "completionItemKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25 + ] + } + }, + "hover": { + "dynamicRegistration": true, + "contentFormat": [ + "markdown", + "plaintext" + ] + }, + "signatureHelp": { + "dynamicRegistration": true, + "signatureInformation": { + "documentationFormat": [ + "markdown", + "plaintext" + ], + "parameterInformation": { + "labelOffsetSupport": true + } + } + }, + "declaration": { + "dynamicRegistration": true, + "linkSupport": true + }, + "definition": { + "dynamicRegistration": true, + "linkSupport": true + }, + "implementation": { + "dynamicRegistration": true, + "linkSupport": true + }, + "references": { + "dynamicRegistration": true + }, + "documentHighlight": { + "dynamicRegistration": true + }, + "documentSymbol": { + "dynamicRegistration": 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 + ] + }, + "hierarchicalDocumentSymbolSupport": true + }, + "codeAction": { + "dynamicRegistration": true, + "isPreferredSupport": true, + "resolveSupport": { + "properties": [ + "edit" + ] + }, + "codeActionLiteralSupport": { + "codeActionKind": { + "valueSet": [ + "", + "quickfix", + "refactor", + "refactor.extract", + "refactor.inline", + "refactor.rewrite", + "source", + "source.organizeImports" + ] + } + } + }, + "codeLens": { + "dynamicRegistration": true + }, + "formatting": { + "dynamicRegistration": true + }, + "rangeFormatting": { + "dynamicRegistration": true + }, + "rename": { + "dynamicRegistration": true, + "prepareSupport": true + }, + "foldingRange": { + "dynamicRegistration": true, + "lineFoldingOnly": true + }, + "selectionRange": { + "dynamicRegistration": true + }, + "semanticTokens": { + "dynamicRegistration": true, + "tokenTypes": [ + "namespace", + "class", + "function", + "method", + "variable", + "keyword", + "string", + "comment", + "number", + "regexp", + "operator", + "macro" + ], + "tokenModifiers": [ + "declaration", + "definition", + "readonly", + "static", + "deprecated", + "documentation" + ], + "formats": [ + "relative" + ], + "requests": { + "range": true, + "full": { + "delta": false + } + } + }, + "linkedEditingRange": { + "dynamicRegistration": true + }, + "callHierarchy": { + "dynamicRegistration": true + } + }, + "window": { + "workDoneProgress": true, + "showMessage": { + "messageActionItem": { + "additionalPropertiesSupport": true + } + } + }, + "general": { + "positionEncodings": [ + "utf-16" + ] + }, + "experimental": { + "testTree": true, + "projectTree": true + } + }, + "initializationOptions": { + "dependency-scheme": "jar", + "document-formatting?": true, + "document-range-formatting?": true, + "text-document-sync-kind": "incremental", + "semantic-tokens?": true + }, + "trace": "verbose", + "workspaceFolders": [ + { + "uri": "$uri", + "name": "$name" + } + ] +} \ No newline at end of file diff --git a/src/multilspy/language_servers/clojure_lsp/runtime_dependencies.json b/src/multilspy/language_servers/clojure_lsp/runtime_dependencies.json new file mode 100644 index 00000000..e93c2bac --- /dev/null +++ b/src/multilspy/language_servers/clojure_lsp/runtime_dependencies.json @@ -0,0 +1,37 @@ +{ + "_description": "Binary distributions of clojure-lsp.", + "runtimeDependencies": [ + { + "id": "clojure-lsp", + "description": "clojure-lsp for OSX (ARM64)", + "url": "https://github.com/clojure-lsp/clojure-lsp/releases/latest/download/clojure-lsp-native-macos-aarch64.zip", + "platformId": "osx-arm64", + "archiveType": "zip", + "binaryName": "clojure-lsp" + }, + { + "id": "clojure-lsp", + "description": "clojure-lsp for Linux (ARM64)", + "url": "https://github.com/clojure-lsp/clojure-lsp/releases/latest/download/clojure-lsp-native-linux-aarch64.zip", + "platformId": "linux-arm64", + "archiveType": "zip", + "binaryName": "clojure-lsp" + }, + { + "id": "clojure-lsp", + "description": "clojure-lsp for Linux (x64)", + "url": "https://github.com/clojure-lsp/clojure-lsp/releases/latest/download/clojure-lsp-native-linux-amd64.zip", + "platformId": "linux-x64", + "archiveType": "zip", + "binaryName": "clojure-lsp" + }, + { + "id": "clojure-lsp", + "description": "clojure-lsp for Windows (x64)", + "url": "https://github.com/clojure-lsp/clojure-lsp/releases/latest/download/clojure-lsp-native-windows-amd64.zip", + "platformId": "win-x64", + "archiveType": "zip", + "binaryName": "clojure-lsp" + } + ] +} \ No newline at end of file From 211b72042cbf8f5d983cea8b8ed63c342db06eeb Mon Sep 17 00:00:00 2001 From: Miguel de Benito Delgado Date: Sat, 7 Jun 2025 18:47:19 +0000 Subject: [PATCH 02/25] Support for clojure via clojure-lsp --- src/multilspy/language_server.py | 4 + .../clojure_lsp/clojure_lsp.py | 24 +- .../clojure_lsp/initialize_params.json | 227 ++---------------- src/multilspy/multilspy_config.py | 3 + 4 files changed, 31 insertions(+), 227 deletions(-) diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index dcf36ef2..d21ff6a3 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -197,6 +197,10 @@ class LanguageServer: from multilspy.language_servers.intelephense.intelephense import Intelephense return Intelephense(config, logger, repository_root_path) + elif config.code_language == Language.CLOJURE: + from multilspy.language_servers.clojure_lsp.clojure_lsp import ClojureLSP + + return ClojureLSP(config, logger, repository_root_path) else: logger.log(f"Language {config.code_language} is not supported", logging.ERROR) raise MultilspyException(f"Language {config.code_language} is not supported") diff --git a/src/multilspy/language_servers/clojure_lsp/clojure_lsp.py b/src/multilspy/language_servers/clojure_lsp/clojure_lsp.py index 796bc2bd..5fab0172 100644 --- a/src/multilspy/language_servers/clojure_lsp/clojure_lsp.py +++ b/src/multilspy/language_servers/clojure_lsp/clojure_lsp.py @@ -1,5 +1,5 @@ """ -Provides Rust specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Rust. +Provides Clojure specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Clojure. """ import asyncio @@ -11,8 +11,6 @@ import pathlib from contextlib import asynccontextmanager from typing import AsyncIterator -from overrides import override - from multilspy.multilspy_logger import MultilspyLogger from multilspy.language_server import LanguageServer from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo @@ -40,11 +38,10 @@ class ClojureLSP(LanguageServer): "clojure", ) self.server_ready = asyncio.Event() + self.initialize_searcher_command_available = asyncio.Event() + self.resolve_main_method_available = asyncio.Event() + self.service_ready_event = asyncio.Event() - @override - def is_ignored_dirname(self, dirname: str) -> bool: - return super().is_ignored_dirname(dirname) or dirname in ["target"] - def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> str: """ Setup runtime dependencies for clojure-lsp. @@ -162,16 +159,15 @@ class ClojureLSP(LanguageServer): init_response = await self.server.send.initialize(initialize_params) assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 assert "completionProvider" in init_response["capabilities"] - assert init_response["capabilities"]["completionProvider"] == { - "resolveProvider": True, - "triggerCharacters": [":", ".", "'", "("], - "completionItem": {"labelDetailsSupport": True}, - } + # 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() - await self.server_ready.wait() - yield self await self.server.shutdown() diff --git a/src/multilspy/language_servers/clojure_lsp/initialize_params.json b/src/multilspy/language_servers/clojure_lsp/initialize_params.json index a63ef8e1..808c1eb4 100644 --- a/src/multilspy/language_servers/clojure_lsp/initialize_params.json +++ b/src/multilspy/language_servers/clojure_lsp/initialize_params.json @@ -1,30 +1,19 @@ { "_description": "Parameters sent by multilspy to initialize clojure-lsp (LSP v3.17)", - "processId": "os.getpid()", + "processId": null, "clientInfo": { "name": "multilspy", "version": "0.1.0" }, - "locale": "en", - "rootPath": "$rootPath", "rootUri": "$rootUri", + "rootPath": "$rootPath", "capabilities": { "workspace": { "applyEdit": true, "workspaceEdit": { - "documentChanges": true, - "resourceOperations": [ - "create", - "rename", - "delete" - ] - }, - "configuration": true, - "didChangeWatchedFiles": { - "dynamicRegistration": true + "documentChanges": true }, "symbol": { - "dynamicRegistration": true, "symbolKind": { "valueSet": [ 1, @@ -56,23 +45,12 @@ ] } }, - "executeCommand": { - "dynamicRegistration": true - }, - "workspaceFolders": true, - "semanticTokens": { - "refreshSupport": true - }, - "codeLens": { - "refreshSupport": true - }, - "fileOperations": { - "dynamicRegistration": true, - "didRename": true, - "willRename": true - } + "workspaceFolders": true }, "textDocument": { + "synchronization": { + "didSave": true + }, "publishDiagnostics": { "relatedInformation": true, "tagSupport": { @@ -80,107 +58,29 @@ 1, 2 ] - }, - "codeDescriptionSupport": true, - "dataSupport": true + } }, - "synchronization": { - "dynamicRegistration": true, - "didSave": true + "definition": { + "linkSupport": true }, + "references": {}, "completion": { - "dynamicRegistration": true, - "contextSupport": true, "completionItem": { "snippetSupport": true, "documentationFormat": [ "markdown", "plaintext" - ], - "deprecatedSupport": true, - "preselectSupport": true, - "tagSupport": { - "valueSet": [ - 1 - ] - }, - "insertReplaceSupport": true, - "resolveSupport": { - "properties": [ - "documentation", - "detail", - "additionalTextEdits" - ] - } - }, - "completionItemKind": { - "valueSet": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25 ] } }, "hover": { - "dynamicRegistration": true, "contentFormat": [ "markdown", "plaintext" ] }, - "signatureHelp": { - "dynamicRegistration": true, - "signatureInformation": { - "documentationFormat": [ - "markdown", - "plaintext" - ], - "parameterInformation": { - "labelOffsetSupport": true - } - } - }, - "declaration": { - "dynamicRegistration": true, - "linkSupport": true - }, - "definition": { - "dynamicRegistration": true, - "linkSupport": true - }, - "implementation": { - "dynamicRegistration": true, - "linkSupport": true - }, - "references": { - "dynamicRegistration": true - }, - "documentHighlight": { - "dynamicRegistration": true - }, "documentSymbol": { - "dynamicRegistration": true, + "hierarchicalDocumentSymbolSupport": true, "symbolKind": { "valueSet": [ 1, @@ -210,98 +110,6 @@ 25, 26 ] - }, - "hierarchicalDocumentSymbolSupport": true - }, - "codeAction": { - "dynamicRegistration": true, - "isPreferredSupport": true, - "resolveSupport": { - "properties": [ - "edit" - ] - }, - "codeActionLiteralSupport": { - "codeActionKind": { - "valueSet": [ - "", - "quickfix", - "refactor", - "refactor.extract", - "refactor.inline", - "refactor.rewrite", - "source", - "source.organizeImports" - ] - } - } - }, - "codeLens": { - "dynamicRegistration": true - }, - "formatting": { - "dynamicRegistration": true - }, - "rangeFormatting": { - "dynamicRegistration": true - }, - "rename": { - "dynamicRegistration": true, - "prepareSupport": true - }, - "foldingRange": { - "dynamicRegistration": true, - "lineFoldingOnly": true - }, - "selectionRange": { - "dynamicRegistration": true - }, - "semanticTokens": { - "dynamicRegistration": true, - "tokenTypes": [ - "namespace", - "class", - "function", - "method", - "variable", - "keyword", - "string", - "comment", - "number", - "regexp", - "operator", - "macro" - ], - "tokenModifiers": [ - "declaration", - "definition", - "readonly", - "static", - "deprecated", - "documentation" - ], - "formats": [ - "relative" - ], - "requests": { - "range": true, - "full": { - "delta": false - } - } - }, - "linkedEditingRange": { - "dynamicRegistration": true - }, - "callHierarchy": { - "dynamicRegistration": true - } - }, - "window": { - "workDoneProgress": true, - "showMessage": { - "messageActionItem": { - "additionalPropertiesSupport": true } } }, @@ -309,20 +117,13 @@ "positionEncodings": [ "utf-16" ] - }, - "experimental": { - "testTree": true, - "projectTree": true } }, "initializationOptions": { "dependency-scheme": "jar", - "document-formatting?": true, - "document-range-formatting?": true, - "text-document-sync-kind": "incremental", - "semantic-tokens?": true + "text-document-sync-kind": "incremental" }, - "trace": "verbose", + "trace": "off", "workspaceFolders": [ { "uri": "$uri", diff --git a/src/multilspy/multilspy_config.py b/src/multilspy/multilspy_config.py index 2b4f7ca4..3d34bae8 100644 --- a/src/multilspy/multilspy_config.py +++ b/src/multilspy/multilspy_config.py @@ -38,6 +38,7 @@ class Language(str, Enum): DART = "dart" CPP = "cpp" PHP = "php" + CLOJURE = "clojure" def __str__(self) -> str: return self.value @@ -68,6 +69,8 @@ class Language(str, Enum): return FilenameMatcher("*.dart") case self.PHP: return FilenameMatcher("*.php") + case self.CLOJURE: + return FilenameMatcher("*.clj", "*.cljs", "*.cljc", "*.edn") case _: raise ValueError(f"Unhandled language: {self}") From 5058ca73ef58a4834b1c9d545a735726d8b9c6c7 Mon Sep 17 00:00:00 2001 From: Miguel de Benito Delgado Date: Sat, 28 Jun 2025 08:34:30 +0000 Subject: [PATCH 03/25] Typo in log message --- src/multilspy/language_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index d21ff6a3..0fc968fd 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -521,7 +521,7 @@ class LanguageServer: 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 MultilspyException("Language Server not started") From fd3c63370359f5a0f65bf258b03074f22fc6a515 Mon Sep 17 00:00:00 2001 From: Miguel de Benito Delgado Date: Sat, 28 Jun 2025 08:36:23 +0000 Subject: [PATCH 04/25] Add clojure temp files to gitignore --- .gitignore | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitignore b/.gitignore index ce9c54f5..fb789675 100644 --- a/.gitignore +++ b/.gitignore @@ -200,9 +200,16 @@ pylint.html /*.yml !*.template.yml /agent-ui + # dynamic LS installations /src/multilspy/language_servers/*/static +# clojure-lsp temporary files +.calva/ +.clj-kondo/ +.cpcache/ +.lsp/ + # Cache is in .multilspy **/.multilspy/** From b8436c5c4a87c455fdbc1fb830ef930fee2e883f Mon Sep 17 00:00:00 2001 From: Miguel de Benito Delgado Date: Sat, 28 Jun 2025 08:37:41 +0000 Subject: [PATCH 05/25] Add clojure test repo --- .../repos/clojure/test_repo/deps.edn | 5 ++++ .../clojure/test_repo/src/test_app/core.clj | 23 +++++++++++++++++++ .../clojure/test_repo/src/test_app/utils.clj | 17 ++++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 test/resources/repos/clojure/test_repo/deps.edn create mode 100644 test/resources/repos/clojure/test_repo/src/test_app/core.clj create mode 100644 test/resources/repos/clojure/test_repo/src/test_app/utils.clj diff --git a/test/resources/repos/clojure/test_repo/deps.edn b/test/resources/repos/clojure/test_repo/deps.edn new file mode 100644 index 00000000..d0680352 --- /dev/null +++ b/test/resources/repos/clojure/test_repo/deps.edn @@ -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"}}}}} diff --git a/test/resources/repos/clojure/test_repo/src/test_app/core.clj b/test/resources/repos/clojure/test_repo/src/test_app/core.clj new file mode 100644 index 00000000..20f5e000 --- /dev/null +++ b/test/resources/repos/clojure/test_repo/src/test_app/core.clj @@ -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))) diff --git a/test/resources/repos/clojure/test_repo/src/test_app/utils.clj b/test/resources/repos/clojure/test_repo/src/test_app/utils.clj new file mode 100644 index 00000000..868bcb54 --- /dev/null +++ b/test/resources/repos/clojure/test_repo/src/test_app/utils.clj @@ -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)) From b58b3de0a240783f9d8fd60c2c58c23308dd10e7 Mon Sep 17 00:00:00 2001 From: Miguel de Benito Delgado Date: Sat, 28 Jun 2025 08:45:33 +0000 Subject: [PATCH 06/25] Test basic clojure support --- test/multilspy/clojure/test_clojure_basic.py | 120 +++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 test/multilspy/clojure/test_clojure_basic.py diff --git a/test/multilspy/clojure/test_clojure_basic.py b/test/multilspy/clojure/test_clojure_basic.py new file mode 100644 index 00000000..99a7e8d3 --- /dev/null +++ b/test/multilspy/clojure/test_clojure_basic.py @@ -0,0 +1,120 @@ +import pytest + +from multilspy.language_server import SyncLanguageServer +from multilspy.multilspy_config import Language +from multilspy.multilspy_types import Position + +@pytest.mark.clojure +class TestLanguageServerBasics: + + @pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True) + def test_basic_definition(self, language_server: SyncLanguageServer): + """ + Test finding definition of 'greet' function call in core.clj + """ + + filepath = "src/test_app/core.clj" + result = language_server.request_definition(filepath, 20, 12) # Position of 'greet' in (greet "World") + + assert isinstance(result, list) + assert len(result) >= 1 + + definition = result[0] + assert definition["relativePath"] == "src/test_app/core.clj" + 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: SyncLanguageServer): + """ + Test finding references to 'multiply' function from core.clj + """ + filepath = "src/test_app/core.clj" + result = language_server.request_references(filepath, 12, 6) + + assert isinstance(result, list) + assert len(result) >= 2 # Should find definition + usage in utils.clj + + usage_found = any( + item["relativePath"] == "src/test_app/utils.clj" 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: SyncLanguageServer): + filepath = "src/test_app/utils.clj" + with language_server.open_file(filepath): + # After "core/" in calculate-area + result = language_server.request_completions(filepath, 6, 8) + + assert isinstance(result, list) + assert len(result) > 0 + + # Should find multiply and other core functions + completion_texts = [item["completionText"] for item in result] + assert any("multiply" in text for text in completion_texts) + + + @pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True) + def test_document_symbols(self, language_server: SyncLanguageServer): + filepath = "src/test_app/core.clj" + symbols, _ = language_server.request_document_symbols(filepath) + + assert isinstance(symbols, list) + assert 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: SyncLanguageServer): + # Test hover on greet function + filepath = "src/test_app/core.clj" + result = language_server.request_hover(filepath, 2, 7) # Position on 'greet' function name + + if result is not None: + 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() + + + @pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True) + def test_workspace_symbols(self, language_server: SyncLanguageServer): + # Search for functions containing "add" + result = language_server.request_workspace_symbol("add") + + if result is not None: + assert isinstance(result, list) + + # Should find the 'add' function + symbol_names = [symbol["name"] for symbol in result] + assert any("add" in name.lower() for name in symbol_names) + + @pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True) + def test_namespace_functions(self, language_server: SyncLanguageServer): + """Test definition lookup for core/greet usage in utils.clj""" + + filepath = "src/test_app/utils.clj" + # Position of 'greet' in core/greet call + result = language_server.request_definition(filepath, 11, 25) + + assert isinstance(result, list) + assert len(result) >= 1 + + definition = result[0] + assert definition["relativePath"] == "src/test_app/core.clj",\ + "Should find the definition of greet in core.clj" + From 528f037501a3db0118097b6d6a8ea710f4052e2d Mon Sep 17 00:00:00 2001 From: Miguel de Benito Delgado Date: Sat, 28 Jun 2025 11:11:08 +0000 Subject: [PATCH 07/25] Additional basic tests for clojure --- test/multilspy/clojure/test_clojure_basic.py | 154 +++++++++++++++++-- 1 file changed, 137 insertions(+), 17 deletions(-) diff --git a/test/multilspy/clojure/test_clojure_basic.py b/test/multilspy/clojure/test_clojure_basic.py index 99a7e8d3..29a46814 100644 --- a/test/multilspy/clojure/test_clojure_basic.py +++ b/test/multilspy/clojure/test_clojure_basic.py @@ -2,7 +2,8 @@ import pytest from multilspy.language_server import SyncLanguageServer from multilspy.multilspy_config import Language -from multilspy.multilspy_types import Position +from multilspy.multilspy_types import UnifiedSymbolInformation + @pytest.mark.clojure class TestLanguageServerBasics: @@ -12,7 +13,6 @@ class TestLanguageServerBasics: """ Test finding definition of 'greet' function call in core.clj """ - filepath = "src/test_app/core.clj" result = language_server.request_definition(filepath, 20, 12) # Position of 'greet' in (greet "World") @@ -81,14 +81,16 @@ class TestLanguageServerBasics: filepath = "src/test_app/core.clj" result = language_server.request_hover(filepath, 2, 7) # Position on 'greet' function name - if result is not None: - 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() + 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) @@ -96,17 +98,38 @@ class TestLanguageServerBasics: # Search for functions containing "add" result = language_server.request_workspace_symbol("add") - if result is not None: - assert isinstance(result, list) - - # Should find the 'add' function - symbol_names = [symbol["name"] for symbol in result] - assert any("add" in name.lower() for name in symbol_names) + + 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: SyncLanguageServer): + """Test retrieving content around specific lines""" + filepath = "src/test_app/core.clj" + + # Test retrieving content around the greet function definition (line 2) + result = language_server.retrieve_content_around_line(filepath, 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(filepath, 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: SyncLanguageServer): """Test definition lookup for core/greet usage in utils.clj""" - filepath = "src/test_app/utils.clj" # Position of 'greet' in core/greet call result = language_server.request_definition(filepath, 11, 25) @@ -118,3 +141,100 @@ class TestLanguageServerBasics: assert definition["relativePath"] == "src/test_app/core.clj",\ "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: SyncLanguageServer): + 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: SyncLanguageServer): + """Test references to multiply function with content""" + filepath = "src/test_app/core.clj" + result = language_server.request_references_with_content(filepath, 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: SyncLanguageServer): + """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: SyncLanguageServer): + """Test finding symbols that reference a given symbol + Finds references to the 'multiply' function. + """ + filepath = "src/test_app/core.clj" + result = language_server.request_referencing_symbols(filepath, 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}" From d073735b6ea90b6c98212e91c2d62f708b6b7f76 Mon Sep 17 00:00:00 2001 From: Miguel de Benito Delgado Date: Sat, 28 Jun 2025 11:12:57 +0000 Subject: [PATCH 08/25] Typo in contributing file --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ced77bd7..54e67a4a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -72,4 +72,4 @@ There are also some tips from the multilspy admin [here](https://github.com/micr ⚠️ Important: The LSP allows for lot of optional fields and symbols, so the language servers may differ in some details, even if they follow the LSP. Therefore you should include some code of the new language in `test/resources` and add tests for symbolic read operations on that code. Have a look -at `test/multilspy/test_symbol_retrieval.py` for an example of such tests for the python LS. +at `test/multilspy/python/test_symbol_retrieval.py` for an example of such tests for the python LS. From 0b280eca98439070a7be295c7bfd879d1f44fbc0 Mon Sep 17 00:00:00 2001 From: Miguel de Benito Delgado Date: Sat, 28 Jun 2025 11:14:26 +0000 Subject: [PATCH 09/25] Add clojure pytest marker to config --- pyproject.toml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3ce1ed4c..0c81ab92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -252,11 +252,12 @@ max-complexity = 20 [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", + "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", ] From 31cee4da6341c6b221ea1943034a8b25083bbb8b Mon Sep 17 00:00:00 2001 From: Miguel de Benito Delgado Date: Sat, 28 Jun 2025 11:29:58 +0000 Subject: [PATCH 10/25] Tell overeager codespell to stfu --- src/solidlsp/ls_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/solidlsp/ls_config.py b/src/solidlsp/ls_config.py index c6126128..5609ec06 100644 --- a/src/solidlsp/ls_config.py +++ b/src/solidlsp/ls_config.py @@ -73,7 +73,7 @@ class Language(str, Enum): case self.PHP: return FilenameMatcher("*.php") case self.CLOJURE: - return FilenameMatcher("*.clj", "*.cljs", "*.cljc", "*.edn") + return FilenameMatcher("*.clj", "*.cljs", "*.cljc", "*.edn") # codespell:ignore edn case _: raise ValueError(f"Unhandled language: {self}") From 5cedba39e3a18afe790b63f5d156df627661c5a4 Mon Sep 17 00:00:00 2001 From: Miguel de Benito Delgado Date: Sat, 28 Jun 2025 11:45:47 +0000 Subject: [PATCH 11/25] Finish renaming and moving files after move to solidlsp --- .../clojure_lsp/clojure_lsp.py | 92 +++++++++---------- .../clojure_lsp/initialize_params.json | 2 +- .../clojure_lsp/runtime_dependencies.json | 0 src/solidlsp/ls.py | 2 +- .../clojure/test_clojure_basic.py | 30 +++--- 5 files changed, 59 insertions(+), 67 deletions(-) rename src/{multilspy => solidlsp}/language_servers/clojure_lsp/clojure_lsp.py (65%) rename src/{multilspy => solidlsp}/language_servers/clojure_lsp/initialize_params.json (99%) rename src/{multilspy => solidlsp}/language_servers/clojure_lsp/runtime_dependencies.json (100%) rename test/{multilspy => solidlsp}/clojure/test_clojure_basic.py (92%) diff --git a/src/multilspy/language_servers/clojure_lsp/clojure_lsp.py b/src/solidlsp/language_servers/clojure_lsp/clojure_lsp.py similarity index 65% rename from src/multilspy/language_servers/clojure_lsp/clojure_lsp.py rename to src/solidlsp/language_servers/clojure_lsp/clojure_lsp.py index 5fab0172..7422e77d 100644 --- a/src/multilspy/language_servers/clojure_lsp/clojure_lsp.py +++ b/src/solidlsp/language_servers/clojure_lsp/clojure_lsp.py @@ -2,30 +2,28 @@ Provides Clojure specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Clojure. """ -import asyncio +import threading import json import logging import os import stat import pathlib -from contextlib import asynccontextmanager -from typing import AsyncIterator -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -from multilspy.lsp_protocol_handler.lsp_types import InitializeParams -from multilspy.multilspy_config import MultilspyConfig -from multilspy.multilspy_utils import FileUtils -from multilspy.multilspy_utils import PlatformUtils +from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.ls import SolidLanguageServer +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_utils import FileUtils +from solidlsp.ls_utils import PlatformUtils -class ClojureLSP(LanguageServer): +class ClojureLSP(SolidLanguageServer): """ Provides a clojure-lsp specific instantiation of the LanguageServer class. Contains various configurations and settings specific to clojure. """ - def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + 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. """ @@ -37,12 +35,12 @@ class ClojureLSP(LanguageServer): ProcessLaunchInfo(cmd=clojure_lsp_executable_path, cwd=repository_root_path), "clojure", ) - self.server_ready = asyncio.Event() - self.initialize_searcher_command_available = asyncio.Event() - self.resolve_main_method_available = asyncio.Event() - self.service_ready_event = asyncio.Event() + 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: MultilspyLogger, config: MultilspyConfig) -> str: + def setup_runtime_dependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> str: """ Setup runtime dependencies for clojure-lsp. """ @@ -95,14 +93,13 @@ class ClojureLSP(LanguageServer): return d - @asynccontextmanager - async def start_server(self) -> AsyncIterator["ClojureLSP"]: + def _start_server(self): """ Starts the Clojure Language Server, waits for the server to be ready and yields the LanguageServer instance. Usage: ``` - async with lsp.start_server(): + with lsp.start_server(): # LanguageServer has been initialized and ready to serve requests await lsp.request_definition(...) await lsp.request_references(...) @@ -110,7 +107,7 @@ class ClojureLSP(LanguageServer): # LanguageServer has been shutdown """ - async def register_capability_handler(params): + def register_capability_handler(params): assert "registrations" in params for registration in params["registrations"]: if registration["method"] == "workspace/executeCommand": @@ -118,24 +115,24 @@ class ClojureLSP(LanguageServer): self.resolve_main_method_available.set() return - async def lang_status_handler(params): + 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() - async def execute_client_command_handler(params): + def execute_client_command_handler(params): return [] - async def do_nothing(params): + def do_nothing(params): return - async def check_experimental_status(params): + def check_experimental_status(params): if params["quiescent"] == True: self.server_ready.set() - async def window_log_message(msg): + def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) self.server.on_request("client/registerCapability", register_capability_handler) @@ -147,28 +144,23 @@ class ClojureLSP(LanguageServer): self.server.on_notification("language/actionableNotification", do_nothing) self.server.on_notification("experimental/serverStatus", check_experimental_status) - async with super().start_server(): - self.logger.log("Starting clojure-lsp server process", logging.INFO) - await self.server.start() - initialize_params = self._get_initialize_params(self.repository_root_path) + 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 = await 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() - - yield self - - await self.server.shutdown() - await self.server.stop() + 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() diff --git a/src/multilspy/language_servers/clojure_lsp/initialize_params.json b/src/solidlsp/language_servers/clojure_lsp/initialize_params.json similarity index 99% rename from src/multilspy/language_servers/clojure_lsp/initialize_params.json rename to src/solidlsp/language_servers/clojure_lsp/initialize_params.json index 808c1eb4..3ab9e2a5 100644 --- a/src/multilspy/language_servers/clojure_lsp/initialize_params.json +++ b/src/solidlsp/language_servers/clojure_lsp/initialize_params.json @@ -2,7 +2,7 @@ "_description": "Parameters sent by multilspy to initialize clojure-lsp (LSP v3.17)", "processId": null, "clientInfo": { - "name": "multilspy", + "name": "solidlsp", "version": "0.1.0" }, "rootUri": "$rootUri", diff --git a/src/multilspy/language_servers/clojure_lsp/runtime_dependencies.json b/src/solidlsp/language_servers/clojure_lsp/runtime_dependencies.json similarity index 100% rename from src/multilspy/language_servers/clojure_lsp/runtime_dependencies.json rename to src/solidlsp/language_servers/clojure_lsp/runtime_dependencies.json diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index c26e32bd..e80ef0e1 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -172,7 +172,7 @@ class SolidLanguageServer(ABC): ls = Intelephense(config, logger, repository_root_path) elif config.code_language == Language.CLOJURE: - from multilspy.language_servers.clojure_lsp.clojure_lsp import ClojureLSP + from solidlsp.language_servers.clojure_lsp.clojure_lsp import ClojureLSP ls = ClojureLSP(config, logger, repository_root_path) diff --git a/test/multilspy/clojure/test_clojure_basic.py b/test/solidlsp/clojure/test_clojure_basic.py similarity index 92% rename from test/multilspy/clojure/test_clojure_basic.py rename to test/solidlsp/clojure/test_clojure_basic.py index 29a46814..3d8e57d9 100644 --- a/test/multilspy/clojure/test_clojure_basic.py +++ b/test/solidlsp/clojure/test_clojure_basic.py @@ -1,15 +1,15 @@ import pytest -from multilspy.language_server import SyncLanguageServer -from multilspy.multilspy_config import Language -from multilspy.multilspy_types import UnifiedSymbolInformation +from solidlsp.ls import SolidLanguageServer +from solidlsp.ls_config import Language +from solidlsp.ls_types import UnifiedSymbolInformation @pytest.mark.clojure class TestLanguageServerBasics: @pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True) - def test_basic_definition(self, language_server: SyncLanguageServer): + def test_basic_definition(self, language_server: SolidLanguageServer): """ Test finding definition of 'greet' function call in core.clj """ @@ -26,7 +26,7 @@ class TestLanguageServerBasics: @pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True) - def test_cross_file_references(self, language_server: SyncLanguageServer): + def test_cross_file_references(self, language_server: SolidLanguageServer): """ Test finding references to 'multiply' function from core.clj """ @@ -45,7 +45,7 @@ class TestLanguageServerBasics: @pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True) - def test_completions(self, language_server: SyncLanguageServer): + def test_completions(self, language_server: SolidLanguageServer): filepath = "src/test_app/utils.clj" with language_server.open_file(filepath): # After "core/" in calculate-area @@ -60,7 +60,7 @@ class TestLanguageServerBasics: @pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True) - def test_document_symbols(self, language_server: SyncLanguageServer): + def test_document_symbols(self, language_server: SolidLanguageServer): filepath = "src/test_app/core.clj" symbols, _ = language_server.request_document_symbols(filepath) @@ -76,7 +76,7 @@ class TestLanguageServerBasics: @pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True) - def test_hover(self, language_server: SyncLanguageServer): + def test_hover(self, language_server: SolidLanguageServer): # Test hover on greet function filepath = "src/test_app/core.clj" result = language_server.request_hover(filepath, 2, 7) # Position on 'greet' function name @@ -94,7 +94,7 @@ class TestLanguageServerBasics: @pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True) - def test_workspace_symbols(self, language_server: SyncLanguageServer): + def test_workspace_symbols(self, language_server: SolidLanguageServer): # Search for functions containing "add" result = language_server.request_workspace_symbol("add") @@ -108,7 +108,7 @@ class TestLanguageServerBasics: 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: SyncLanguageServer): + def test_retrieve_content_around_line(self, language_server: SolidLanguageServer): """Test retrieving content around specific lines""" filepath = "src/test_app/core.clj" @@ -128,7 +128,7 @@ class TestLanguageServerBasics: 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: SyncLanguageServer): + def test_namespace_functions(self, language_server: SolidLanguageServer): """Test definition lookup for core/greet usage in utils.clj""" filepath = "src/test_app/utils.clj" # Position of 'greet' in core/greet call @@ -142,7 +142,7 @@ class TestLanguageServerBasics: "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: SyncLanguageServer): + 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" @@ -158,7 +158,7 @@ class TestLanguageServerBasics: 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: SyncLanguageServer): + def test_request_references_with_content(self, language_server: SolidLanguageServer): """Test references to multiply function with content""" filepath = "src/test_app/core.clj" result = language_server.request_references_with_content(filepath, 12, 6, 3) @@ -181,7 +181,7 @@ class TestLanguageServerBasics: assert "calculate-area" in utils_content @pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True) - def test_request_full_symbol_tree(self, language_server: SyncLanguageServer): + 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. """ @@ -222,7 +222,7 @@ class TestLanguageServerBasics: @pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True) - def test_request_referencing_symbols(self, language_server: SyncLanguageServer): + def test_request_referencing_symbols(self, language_server: SolidLanguageServer): """Test finding symbols that reference a given symbol Finds references to the 'multiply' function. """ From f24ee4259242fd9708c8313a6e22a6c065c4da80 Mon Sep 17 00:00:00 2001 From: Miguel de Benito Delgado Date: Sat, 28 Jun 2025 14:00:21 +0200 Subject: [PATCH 12/25] De-fluff --- .../clojure_lsp/clojure_lsp.py | 17 ++-- test/solidlsp/clojure/test_clojure_basic.py | 85 +++++++++---------- 2 files changed, 48 insertions(+), 54 deletions(-) diff --git a/src/solidlsp/language_servers/clojure_lsp/clojure_lsp.py b/src/solidlsp/language_servers/clojure_lsp/clojure_lsp.py index 7422e77d..4c6f1944 100644 --- a/src/solidlsp/language_servers/clojure_lsp/clojure_lsp.py +++ b/src/solidlsp/language_servers/clojure_lsp/clojure_lsp.py @@ -2,20 +2,19 @@ Provides Clojure specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Clojure. """ -import threading import json import logging import os -import stat import pathlib +import stat +import threading -from solidlsp.ls_logger import LanguageServerLogger from solidlsp.ls import SolidLanguageServer -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo -from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.ls_config import LanguageServerConfig -from solidlsp.ls_utils import FileUtils -from solidlsp.ls_utils import PlatformUtils +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 class ClojureLSP(SolidLanguageServer): @@ -46,7 +45,7 @@ class ClojureLSP(SolidLanguageServer): """ platform_id = PlatformUtils.get_platform_id() - with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r", encoding="utf-8") as f: + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), encoding="utf-8") as f: d = json.load(f) del d["_description"] @@ -73,7 +72,7 @@ class ClojureLSP(SolidLanguageServer): """ Returns the init params for clojure-lsp. """ - with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), encoding="utf-8") as f: d = json.load(f) del d["_description"] diff --git a/test/solidlsp/clojure/test_clojure_basic.py b/test/solidlsp/clojure/test_clojure_basic.py index 3d8e57d9..20f6772c 100644 --- a/test/solidlsp/clojure/test_clojure_basic.py +++ b/test/solidlsp/clojure/test_clojure_basic.py @@ -1,13 +1,12 @@ import pytest -from solidlsp.ls import SolidLanguageServer +from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_types import UnifiedSymbolInformation @pytest.mark.clojure class TestLanguageServerBasics: - @pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True) def test_basic_definition(self, language_server: SolidLanguageServer): """ @@ -18,7 +17,7 @@ class TestLanguageServerBasics: assert isinstance(result, list) assert len(result) >= 1 - + definition = result[0] assert definition["relativePath"] == "src/test_app/core.clj" assert definition["range"]["start"]["line"] == 2, \ @@ -43,7 +42,6 @@ class TestLanguageServerBasics: ) 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): filepath = "src/test_app/utils.clj" @@ -58,7 +56,6 @@ class TestLanguageServerBasics: completion_texts = [item["completionText"] for item in result] assert any("multiply" in text for text in completion_texts) - @pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True) def test_document_symbols(self, language_server: SolidLanguageServer): filepath = "src/test_app/core.clj" @@ -70,15 +67,14 @@ class TestLanguageServerBasics: # 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 - filepath = "src/test_app/core.clj" + filepath = "src/test_app/core.clj" result = language_server.request_hover(filepath, 2, 7) # Position on 'greet' function name assert result is not None, "Hover should return information for greet function" @@ -92,7 +88,6 @@ class TestLanguageServerBasics: 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" @@ -111,18 +106,18 @@ class TestLanguageServerBasics: def test_retrieve_content_around_line(self, language_server: SolidLanguageServer): """Test retrieving content around specific lines""" filepath = "src/test_app/core.clj" - + # Test retrieving content around the greet function definition (line 2) result = language_server.retrieve_content_around_line(filepath, 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) + + # Test retrieving content around multiply function (around line 13) result = language_server.retrieve_content_around_line(filepath, 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" @@ -131,12 +126,12 @@ class TestLanguageServerBasics: def test_namespace_functions(self, language_server: SolidLanguageServer): """Test definition lookup for core/greet usage in utils.clj""" filepath = "src/test_app/utils.clj" - # Position of 'greet' in core/greet call + # Position of 'greet' in core/greet call result = language_server.request_definition(filepath, 11, 25) assert isinstance(result, list) assert len(result) >= 1 - + definition = result[0] assert definition["relativePath"] == "src/test_app/core.clj",\ "Should find the definition of greet in core.clj" @@ -144,38 +139,38 @@ class TestLanguageServerBasics: @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""" filepath = "src/test_app/core.clj" result = language_server.request_references_with_content(filepath, 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 @@ -186,55 +181,55 @@ class TestLanguageServerBasics: 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: + 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)}") + 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. - """ - filepath = "src/test_app/core.clj" - result = language_server.request_referencing_symbols(filepath, 12, 6) + """ + filepath = "src/test_app/core.clj" + result = language_server.request_referencing_symbols(filepath, 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"]: + 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}" From e7f5acd7d907af93e05b862c652ff3f2abf7d2c5 Mon Sep 17 00:00:00 2001 From: Miguel de Benito Delgado Date: Sat, 28 Jun 2025 20:32:37 +0200 Subject: [PATCH 13/25] Fail on missing or incompatible clojure executable --- .../clojure_lsp/clojure_lsp.py | 54 ++++++++++--------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/src/solidlsp/language_servers/clojure_lsp/clojure_lsp.py b/src/solidlsp/language_servers/clojure_lsp/clojure_lsp.py index 4c6f1944..be0dfea4 100644 --- a/src/solidlsp/language_servers/clojure_lsp/clojure_lsp.py +++ b/src/solidlsp/language_servers/clojure_lsp/clojure_lsp.py @@ -6,7 +6,9 @@ import json import logging import os import pathlib +import shutil import stat +import subprocess import threading from solidlsp.ls import SolidLanguageServer @@ -17,6 +19,26 @@ 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. @@ -38,11 +60,10 @@ class ClojureLSP(SolidLanguageServer): 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. - """ + """Setup runtime dependencies for clojure-lsp.""" + verify_clojure_cli() platform_id = PlatformUtils.get_platform_id() with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), encoding="utf-8") as f: @@ -50,19 +71,15 @@ class ClojureLSP(SolidLanguageServer): del d["_description"] runtime_dependencies = d["runtimeDependencies"] - runtime_dependencies = [ - dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value - ] + runtime_dependencies = [dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value] assert len(runtime_dependencies) == 1 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_ls_dir): - os.makedirs(clojurelsp_ls_dir) - FileUtils.download_and_extract_archive( - logger, dependency["url"], clojurelsp_ls_dir, dependency["archiveType"] - ) + os.makedirs(clojurelsp_ls_dir) + FileUtils.download_and_extract_archive(logger, dependency["url"], clojurelsp_ls_dir, dependency["archiveType"]) assert os.path.exists(clojurelsp_executable_path) os.chmod(clojurelsp_executable_path, stat.S_IEXEC) @@ -93,19 +110,6 @@ class ClojureLSP(SolidLanguageServer): return d def _start_server(self): - """ - Starts the Clojure Language Server, waits for the server to be ready and yields the LanguageServer instance. - - Usage: - ``` - with lsp.start_server(): - # LanguageServer has been initialized and ready to serve requests - await lsp.request_definition(...) - await lsp.request_references(...) - # Shutdown the LanguageServer on exit from scope - # LanguageServer has been shutdown - """ - def register_capability_handler(params): assert "registrations" in params for registration in params["registrations"]: @@ -145,7 +149,7 @@ class ClojureLSP(SolidLanguageServer): 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( From 151df419aebadc1433403fbcc9b3dc7e8834c307 Mon Sep 17 00:00:00 2001 From: Miguel de Benito Delgado Date: Sat, 28 Jun 2025 20:37:29 +0200 Subject: [PATCH 14/25] Skip clojure tests on incompatible or missing cli --- test/solidlsp/clojure/__init__.py | 12 ++++++++++++ test/solidlsp/clojure/test_clojure_basic.py | 3 +++ 2 files changed, 15 insertions(+) create mode 100644 test/solidlsp/clojure/__init__.py diff --git a/test/solidlsp/clojure/__init__.py b/test/solidlsp/clojure/__init__.py new file mode 100644 index 00000000..d1ad386b --- /dev/null +++ b/test/solidlsp/clojure/__init__.py @@ -0,0 +1,12 @@ +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) + + +CLOJURE_CLI_FAIL = _test_clojure_cli() diff --git a/test/solidlsp/clojure/test_clojure_basic.py b/test/solidlsp/clojure/test_clojure_basic.py index 20f6772c..a292512d 100644 --- a/test/solidlsp/clojure/test_clojure_basic.py +++ b/test/solidlsp/clojure/test_clojure_basic.py @@ -4,8 +4,11 @@ from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_types import UnifiedSymbolInformation +from . import CLOJURE_CLI_FAIL + @pytest.mark.clojure +@pytest.mark.skipif(CLOJURE_CLI_FAIL, reason=f"Clojure CLI not available: {CLOJURE_CLI_FAIL}") class TestLanguageServerBasics: @pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True) def test_basic_definition(self, language_server: SolidLanguageServer): From a99707cb1736d94b913e15a0014e3d9db2457c1c Mon Sep 17 00:00:00 2001 From: Miguel de Benito Delgado Date: Sat, 28 Jun 2025 20:48:37 +0200 Subject: [PATCH 15/25] Add java+clojure to the environment for the pytest gh workflow --- .github/workflows/pytest.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 64dd29fa..14f38131 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -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 \ No newline at end of file + run: uv run poe test From b95cc78cbdce0755efd3cd3ea475dc73ad44e7f8 Mon Sep 17 00:00:00 2001 From: Miguel de Benito Delgado Date: Sat, 28 Jun 2025 20:48:50 +0200 Subject: [PATCH 16/25] Cosmetic --- test/solidlsp/clojure/test_clojure_basic.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/test/solidlsp/clojure/test_clojure_basic.py b/test/solidlsp/clojure/test_clojure_basic.py index a292512d..f667bd5e 100644 --- a/test/solidlsp/clojure/test_clojure_basic.py +++ b/test/solidlsp/clojure/test_clojure_basic.py @@ -35,8 +35,8 @@ class TestLanguageServerBasics: filepath = "src/test_app/core.clj" result = language_server.request_references(filepath, 12, 6) - assert isinstance(result, list) - assert len(result) >= 2 # Should find definition + usage in utils.clj + assert isinstance(result, list) and len(result) >= 2, \ + "Should find definition + usage in utils.clj" usage_found = any( item["relativePath"] == "src/test_app/utils.clj" and @@ -52,20 +52,19 @@ class TestLanguageServerBasics: # After "core/" in calculate-area result = language_server.request_completions(filepath, 6, 8) - assert isinstance(result, list) - assert len(result) > 0 + assert isinstance(result, list) and len(result) > 0 - # Should find multiply and other core functions completion_texts = [item["completionText"] for item in result] - assert any("multiply" in text for text in completion_texts) + 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): filepath = "src/test_app/core.clj" symbols, _ = language_server.request_document_symbols(filepath) - assert isinstance(symbols, list) - assert len(symbols) >= 4 # greet, add, multiply, -main functions + 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] From d097171a1abb7ac2d0d2694537252c84c02aa489 Mon Sep 17 00:00:00 2001 From: Miguel de Benito Delgado Date: Sat, 28 Jun 2025 21:12:16 +0200 Subject: [PATCH 17/25] Use pathlib in tests --- test/solidlsp/clojure/test_clojure_basic.py | 53 +++++++++------------ 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/test/solidlsp/clojure/test_clojure_basic.py b/test/solidlsp/clojure/test_clojure_basic.py index f667bd5e..46e60e7f 100644 --- a/test/solidlsp/clojure/test_clojure_basic.py +++ b/test/solidlsp/clojure/test_clojure_basic.py @@ -1,3 +1,5 @@ +from pathlib import Path + import pytest from solidlsp.ls import SolidLanguageServer @@ -10,19 +12,22 @@ from . import CLOJURE_CLI_FAIL @pytest.mark.clojure @pytest.mark.skipif(CLOJURE_CLI_FAIL, reason=f"Clojure CLI not available: {CLOJURE_CLI_FAIL}") class TestLanguageServerBasics: + test_app = Path("src") / "test_app" + core_path = str(test_app / "core.clj") + utils_path = str(test_app / "utils.clj") + @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 """ - filepath = "src/test_app/core.clj" - result = language_server.request_definition(filepath, 20, 12) # Position of 'greet' in (greet "World") + result = language_server.request_definition(self.core_path, 20, 12) # Position of 'greet' in (greet "World") assert isinstance(result, list) assert len(result) >= 1 definition = result[0] - assert definition["relativePath"] == "src/test_app/core.clj" + assert definition["relativePath"] == self.core_path assert definition["range"]["start"]["line"] == 2, \ "Should find the definition of greet function at line 2" @@ -32,36 +37,30 @@ class TestLanguageServerBasics: """ Test finding references to 'multiply' function from core.clj """ - filepath = "src/test_app/core.clj" - result = language_server.request_references(filepath, 12, 6) + result = language_server.request_references(self.core_path, 12, 6) - assert isinstance(result, list) and len(result) >= 2, \ - "Should find definition + usage in utils.clj" + assert isinstance(result, list) and len(result) >= 2, "Should find definition + usage in utils.clj" usage_found = any( - item["relativePath"] == "src/test_app/utils.clj" and - item["range"]["start"]["line"] == 6 # multiply usage in calculate-area + item["relativePath"] == self.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): - filepath = "src/test_app/utils.clj" - with language_server.open_file(filepath): + with language_server.open_file(self.utils_path): # After "core/" in calculate-area - result = language_server.request_completions(filepath, 6, 8) + result = language_server.request_completions(self.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/'" + 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): - filepath = "src/test_app/core.clj" - symbols, _ = language_server.request_document_symbols(filepath) + symbols, _ = language_server.request_document_symbols(self.core_path) assert isinstance(symbols, list) and len(symbols) >= 4, \ "greet, add, multiply, -main functions" @@ -75,9 +74,8 @@ class TestLanguageServerBasics: @pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True) def test_hover(self, language_server: SolidLanguageServer): - # Test hover on greet function - filepath = "src/test_app/core.clj" - result = language_server.request_hover(filepath, 2, 7) # Position on 'greet' function name + """Test hover on greet function""" + result = language_server.request_hover(self.core_path, 2, 7) assert result is not None, "Hover should return information for greet function" assert "contents" in result @@ -107,10 +105,8 @@ class TestLanguageServerBasics: @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""" - filepath = "src/test_app/core.clj" - # Test retrieving content around the greet function definition (line 2) - result = language_server.retrieve_content_around_line(filepath, 2, 2) + result = language_server.retrieve_content_around_line(self.core_path, 2, 2) assert result is not None, "Should retrieve content around line 2" content_str = result.to_display_string() @@ -118,7 +114,7 @@ class TestLanguageServerBasics: 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(filepath, 13, 1) + result = language_server.retrieve_content_around_line(self.core_path, 13, 1) assert result is not None, "Should retrieve content around line 13" content_str = result.to_display_string() @@ -127,15 +123,14 @@ class TestLanguageServerBasics: @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""" - filepath = "src/test_app/utils.clj" # Position of 'greet' in core/greet call - result = language_server.request_definition(filepath, 11, 25) + result = language_server.request_definition(self.utils_path, 11, 25) assert isinstance(result, list) assert len(result) >= 1 definition = result[0] - assert definition["relativePath"] == "src/test_app/core.clj",\ + assert definition["relativePath"] == self.core_path, \ "Should find the definition of greet in core.clj" @pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True) @@ -157,8 +152,7 @@ class TestLanguageServerBasics: @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""" - filepath = "src/test_app/core.clj" - result = language_server.request_references_with_content(filepath, 12, 6, 3) + result = language_server.request_references_with_content(self.core_path, 12, 6, 3) assert result is not None, "Should find references with content" assert isinstance(result, list) @@ -224,8 +218,7 @@ class TestLanguageServerBasics: """Test finding symbols that reference a given symbol Finds references to the 'multiply' function. """ - filepath = "src/test_app/core.clj" - result = language_server.request_referencing_symbols(filepath, 12, 6) + result = language_server.request_referencing_symbols(self.core_path, 12, 6) assert isinstance(result, list) and len(result) > 0, \ "Should find at least one referencing symbol" found_relevant_references = False From c3dd421ee81943a84d74f29adeb44492332c12dc Mon Sep 17 00:00:00 2001 From: Miguel de Benito Delgado Date: Sat, 28 Jun 2025 21:26:21 +0200 Subject: [PATCH 18/25] Add pytest markers to pytest.ini (?) --- pytest.ini | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pytest.ini b/pytest.ini index 22e867b9..a9d2f112 100644 --- a/pytest.ini +++ b/pytest.ini @@ -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 \ No newline at end of file From 62f5985aa5956a08811e043686fe0d2b6e359f99 Mon Sep 17 00:00:00 2001 From: Miguel de Benito Delgado Date: Sun, 29 Jun 2025 10:07:00 +0200 Subject: [PATCH 19/25] Declare InitializeParams inside LS class --- .../clojure_lsp/clojure_lsp.py | 60 +++++--- .../clojure_lsp/initialize_params.json | 133 ------------------ 2 files changed, 38 insertions(+), 155 deletions(-) delete mode 100644 src/solidlsp/language_servers/clojure_lsp/initialize_params.json diff --git a/src/solidlsp/language_servers/clojure_lsp/clojure_lsp.py b/src/solidlsp/language_servers/clojure_lsp/clojure_lsp.py index be0dfea4..b402832d 100644 --- a/src/solidlsp/language_servers/clojure_lsp/clojure_lsp.py +++ b/src/solidlsp/language_servers/clojure_lsp/clojure_lsp.py @@ -86,28 +86,44 @@ class ClojureLSP(SolidLanguageServer): return clojurelsp_executable_path def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: - """ - Returns the init params for clojure-lsp. - """ - with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), encoding="utf-8") as f: - d = json.load(f) - - del d["_description"] - - d["processId"] = os.getpid() - assert d["rootPath"] == "$rootPath" - d["rootPath"] = repository_absolute_path - - assert d["rootUri"] == "$rootUri" - d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() - - assert d["workspaceFolders"][0]["uri"] == "$uri" - d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri() - - assert d["workspaceFolders"][0]["name"] == "$name" - d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) - - return d + """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): diff --git a/src/solidlsp/language_servers/clojure_lsp/initialize_params.json b/src/solidlsp/language_servers/clojure_lsp/initialize_params.json deleted file mode 100644 index 3ab9e2a5..00000000 --- a/src/solidlsp/language_servers/clojure_lsp/initialize_params.json +++ /dev/null @@ -1,133 +0,0 @@ -{ - "_description": "Parameters sent by multilspy to initialize clojure-lsp (LSP v3.17)", - "processId": null, - "clientInfo": { - "name": "solidlsp", - "version": "0.1.0" - }, - "rootUri": "$rootUri", - "rootPath": "$rootPath", - "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": "$uri", - "name": "$name" - } - ] -} \ No newline at end of file From b307ff10cac6c1d78528ed9b5f6ac64210d8a2cf Mon Sep 17 00:00:00 2001 From: Miguel de Benito Delgado Date: Sun, 29 Jun 2025 10:07:21 +0200 Subject: [PATCH 20/25] Fix executable name for windows --- .../language_servers/clojure_lsp/runtime_dependencies.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/solidlsp/language_servers/clojure_lsp/runtime_dependencies.json b/src/solidlsp/language_servers/clojure_lsp/runtime_dependencies.json index e93c2bac..234c56d9 100644 --- a/src/solidlsp/language_servers/clojure_lsp/runtime_dependencies.json +++ b/src/solidlsp/language_servers/clojure_lsp/runtime_dependencies.json @@ -31,7 +31,7 @@ "url": "https://github.com/clojure-lsp/clojure-lsp/releases/latest/download/clojure-lsp-native-windows-amd64.zip", "platformId": "win-x64", "archiveType": "zip", - "binaryName": "clojure-lsp" + "binaryName": "clojure-lsp.exe" } ] } \ No newline at end of file From 8ac9d79076188d124f46e670c604412ea7f7a6b6 Mon Sep 17 00:00:00 2001 From: Miguel de Benito Delgado Date: Sun, 29 Jun 2025 10:22:48 +0200 Subject: [PATCH 21/25] Declare runtime deps inside clojure LS class --- .../clojure_lsp/clojure_lsp.py | 37 +++++++++++++++---- .../clojure_lsp/runtime_dependencies.json | 37 ------------------- 2 files changed, 30 insertions(+), 44 deletions(-) delete mode 100644 src/solidlsp/language_servers/clojure_lsp/runtime_dependencies.json diff --git a/src/solidlsp/language_servers/clojure_lsp/clojure_lsp.py b/src/solidlsp/language_servers/clojure_lsp/clojure_lsp.py index b402832d..9569a16b 100644 --- a/src/solidlsp/language_servers/clojure_lsp/clojure_lsp.py +++ b/src/solidlsp/language_servers/clojure_lsp/clojure_lsp.py @@ -2,7 +2,6 @@ Provides Clojure specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Clojure. """ -import json import logging import os import pathlib @@ -44,6 +43,34 @@ 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. @@ -66,12 +93,7 @@ class ClojureLSP(SolidLanguageServer): verify_clojure_cli() platform_id = PlatformUtils.get_platform_id() - with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), encoding="utf-8") as f: - d = json.load(f) - del d["_description"] - - runtime_dependencies = d["runtimeDependencies"] - runtime_dependencies = [dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value] + runtime_dependencies = [dependency for dependency in self.runtime_dependencies if dependency["platformId"] == platform_id.value] assert len(runtime_dependencies) == 1 dependency = runtime_dependencies[0] @@ -79,6 +101,7 @@ class ClojureLSP(SolidLanguageServer): clojurelsp_executable_path = os.path.join(clojurelsp_ls_dir, dependency["binaryName"]) if not os.path.exists(clojurelsp_ls_dir): os.makedirs(clojurelsp_ls_dir) + self.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"]) assert os.path.exists(clojurelsp_executable_path) os.chmod(clojurelsp_executable_path, stat.S_IEXEC) diff --git a/src/solidlsp/language_servers/clojure_lsp/runtime_dependencies.json b/src/solidlsp/language_servers/clojure_lsp/runtime_dependencies.json deleted file mode 100644 index 234c56d9..00000000 --- a/src/solidlsp/language_servers/clojure_lsp/runtime_dependencies.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "_description": "Binary distributions of clojure-lsp.", - "runtimeDependencies": [ - { - "id": "clojure-lsp", - "description": "clojure-lsp for OSX (ARM64)", - "url": "https://github.com/clojure-lsp/clojure-lsp/releases/latest/download/clojure-lsp-native-macos-aarch64.zip", - "platformId": "osx-arm64", - "archiveType": "zip", - "binaryName": "clojure-lsp" - }, - { - "id": "clojure-lsp", - "description": "clojure-lsp for Linux (ARM64)", - "url": "https://github.com/clojure-lsp/clojure-lsp/releases/latest/download/clojure-lsp-native-linux-aarch64.zip", - "platformId": "linux-arm64", - "archiveType": "zip", - "binaryName": "clojure-lsp" - }, - { - "id": "clojure-lsp", - "description": "clojure-lsp for Linux (x64)", - "url": "https://github.com/clojure-lsp/clojure-lsp/releases/latest/download/clojure-lsp-native-linux-amd64.zip", - "platformId": "linux-x64", - "archiveType": "zip", - "binaryName": "clojure-lsp" - }, - { - "id": "clojure-lsp", - "description": "clojure-lsp for Windows (x64)", - "url": "https://github.com/clojure-lsp/clojure-lsp/releases/latest/download/clojure-lsp-native-windows-amd64.zip", - "platformId": "win-x64", - "archiveType": "zip", - "binaryName": "clojure-lsp.exe" - } - ] -} \ No newline at end of file From 3bd8c4a944b143628d41f8e5699fb77f917629d0 Mon Sep 17 00:00:00 2001 From: Miguel de Benito Delgado Date: Sun, 29 Jun 2025 10:41:17 +0200 Subject: [PATCH 22/25] Factor a couple of constants for clojure tests out --- test/solidlsp/clojure/__init__.py | 7 ++- test/solidlsp/clojure/test_clojure_basic.py | 60 ++++++++------------- 2 files changed, 29 insertions(+), 38 deletions(-) diff --git a/test/solidlsp/clojure/__init__.py b/test/solidlsp/clojure/__init__.py index d1ad386b..ed57686d 100644 --- a/test/solidlsp/clojure/__init__.py +++ b/test/solidlsp/clojure/__init__.py @@ -1,3 +1,5 @@ +from pathlib import Path + from solidlsp.language_servers.clojure_lsp.clojure_lsp import verify_clojure_cli @@ -9,4 +11,7 @@ def _test_clojure_cli() -> str | bool: return str(e) -CLOJURE_CLI_FAIL = _test_clojure_cli() +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") diff --git a/test/solidlsp/clojure/test_clojure_basic.py b/test/solidlsp/clojure/test_clojure_basic.py index 46e60e7f..e50ad0b2 100644 --- a/test/solidlsp/clojure/test_clojure_basic.py +++ b/test/solidlsp/clojure/test_clojure_basic.py @@ -1,57 +1,49 @@ -from pathlib import Path - import pytest from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_types import UnifiedSymbolInformation -from . import CLOJURE_CLI_FAIL +from . import CLI_FAIL, CORE_PATH, UTILS_PATH @pytest.mark.clojure -@pytest.mark.skipif(CLOJURE_CLI_FAIL, reason=f"Clojure CLI not available: {CLOJURE_CLI_FAIL}") +@pytest.mark.skipif(CLI_FAIL, reason=f"Clojure CLI not available: {CLI_FAIL}") class TestLanguageServerBasics: - test_app = Path("src") / "test_app" - core_path = str(test_app / "core.clj") - utils_path = str(test_app / "utils.clj") - @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(self.core_path, 20, 12) # Position of 'greet' in (greet "World") + 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"] == self.core_path - assert definition["range"]["start"]["line"] == 2, \ - "Should find the definition of greet function at line 2" - + 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(self.core_path, 12, 6) + 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"] == self.utils_path and item["range"]["start"]["line"] == 6 # multiply usage in calculate-area + 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(self.utils_path): + with language_server.open_file(UTILS_PATH): # After "core/" in calculate-area - result = language_server.request_completions(self.utils_path, 6, 8) + result = language_server.request_completions(UTILS_PATH, 6, 8) assert isinstance(result, list) and len(result) > 0 @@ -60,10 +52,9 @@ class TestLanguageServerBasics: @pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True) def test_document_symbols(self, language_server: SolidLanguageServer): - symbols, _ = language_server.request_document_symbols(self.core_path) + symbols, _ = language_server.request_document_symbols(CORE_PATH) - assert isinstance(symbols, list) and len(symbols) >= 4, \ - "greet, add, multiply, -main functions" + 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] @@ -75,7 +66,7 @@ class TestLanguageServerBasics: @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(self.core_path, 2, 7) + 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 @@ -93,20 +84,17 @@ class TestLanguageServerBasics: # 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'" - + 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}" - + 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(self.core_path, 2, 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() @@ -114,7 +102,7 @@ class TestLanguageServerBasics: 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(self.core_path, 13, 1) + 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() @@ -124,14 +112,13 @@ class TestLanguageServerBasics: 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(self.utils_path, 11, 25) + result = language_server.request_definition(UTILS_PATH, 11, 25) assert isinstance(result, list) assert len(result) >= 1 definition = result[0] - assert definition["relativePath"] == self.core_path, \ - "Should find the definition of greet in core.clj" + 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): @@ -152,7 +139,7 @@ class TestLanguageServerBasics: @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(self.core_path, 12, 6, 3) + 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) @@ -218,9 +205,8 @@ class TestLanguageServerBasics: """Test finding symbols that reference a given symbol Finds references to the 'multiply' function. """ - result = language_server.request_referencing_symbols(self.core_path, 12, 6) - assert isinstance(result, list) and len(result) > 0, \ - "Should find at least one referencing symbol" + 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"]: From 254526d80d65c96d4f11dbe592fec2b9bbaa6267 Mon Sep 17 00:00:00 2001 From: Miguel de Benito Delgado Date: Sun, 29 Jun 2025 10:43:14 +0200 Subject: [PATCH 23/25] Test serena tools find symbol and find symbol references for clojure --- test/serena/test_serena_agent.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/test/serena/test_serena_agent.py b/test/serena/test_serena_agent.py index 4e297853..2db5537b 100644 --- a/test/serena/test_serena_agent.py +++ b/test/serena/test_serena_agent.py @@ -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"], ) From 612912b79154b1ee082927fe0e077d52ce9d9879 Mon Sep 17 00:00:00 2001 From: Miguel de Benito Delgado Date: Sun, 29 Jun 2025 10:51:27 +0200 Subject: [PATCH 24/25] Fix logger use and improve detection of failed downloads in clojure setup --- src/solidlsp/language_servers/clojure_lsp/clojure_lsp.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/solidlsp/language_servers/clojure_lsp/clojure_lsp.py b/src/solidlsp/language_servers/clojure_lsp/clojure_lsp.py index 9569a16b..7ac6c8a7 100644 --- a/src/solidlsp/language_servers/clojure_lsp/clojure_lsp.py +++ b/src/solidlsp/language_servers/clojure_lsp/clojure_lsp.py @@ -99,9 +99,9 @@ class ClojureLSP(SolidLanguageServer): 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_ls_dir): - os.makedirs(clojurelsp_ls_dir) - self.logger.log(f"Downloading and extracting clojure-lsp from {dependency['url']} to {clojurelsp_ls_dir}", logging.INFO) + 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"]) assert os.path.exists(clojurelsp_executable_path) os.chmod(clojurelsp_executable_path, stat.S_IEXEC) From cbb393dd72b77300866459cae5841f0030724217 Mon Sep 17 00:00:00 2001 From: Miguel de Benito Delgado Date: Sun, 29 Jun 2025 11:42:49 +0200 Subject: [PATCH 25/25] Raise instead of assert in a couple of places --- src/solidlsp/language_servers/clojure_lsp/clojure_lsp.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/solidlsp/language_servers/clojure_lsp/clojure_lsp.py b/src/solidlsp/language_servers/clojure_lsp/clojure_lsp.py index 7ac6c8a7..e83b540d 100644 --- a/src/solidlsp/language_servers/clojure_lsp/clojure_lsp.py +++ b/src/solidlsp/language_servers/clojure_lsp/clojure_lsp.py @@ -94,7 +94,8 @@ class ClojureLSP(SolidLanguageServer): platform_id = PlatformUtils.get_platform_id() runtime_dependencies = [dependency for dependency in self.runtime_dependencies if dependency["platformId"] == platform_id.value] - assert len(runtime_dependencies) == 1 + 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") @@ -103,9 +104,9 @@ class ClojureLSP(SolidLanguageServer): 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"]) - assert os.path.exists(clojurelsp_executable_path) + 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: