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