Skip to content

Commit 8989bc2

Browse files
Merge pull request #113 from mdbenito/feature/clojure-lsp
Support for clojure via clojure-lsp
2 parents 13f6dea + f0d917b commit 8989bc2

File tree

11 files changed

+766
-8
lines changed

11 files changed

+766
-8
lines changed

.gitignore

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -397,14 +397,15 @@ FodyWeavers.xsd
397397
# JetBrains Rider
398398
*.sln.iml
399399

400-
src/multilspy/language_servers/eclipse_jdtls/static/
401-
src/multilspy/language_servers/gopls/static/
402-
src/multilspy/language_servers/omnisharp/static/
403-
src/multilspy/language_servers/rust_analyzer/static/
404-
src/multilspy/language_servers/typescript_language_server/static/
405-
src/multilspy/language_servers/dart_language_server/static/
406-
src/multilspy/language_servers/clangd_language_server/static/
400+
# Downloaded language server binaries
401+
src/multilspy/language_servers/*/static/
407402

408403
# Virtual Environment
409404
.venv/
410405
venv/
406+
407+
# clojure-related temporary files
408+
.calva/
409+
.clj-kondo/
410+
.cpcache/
411+
.lsp/

src/multilspy/language_server.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@ def create(cls, config: MultilspyConfig, logger: MultilspyLogger, repository_roo
122122
from multilspy.language_servers.clangd_language_server.clangd_language_server import ClangdLanguageServer
123123

124124
return ClangdLanguageServer(config, logger, repository_root_path)
125+
elif config.code_language == Language.CLOJURE:
126+
from multilspy.language_servers.clojure_lsp.clojure_lsp import ClojureLSP
127+
128+
return ClojureLSP(config, logger, repository_root_path)
125129
else:
126130
logger.log(f"Language {config.code_language} is not supported", logging.ERROR)
127131
raise MultilspyException(f"Language {config.code_language} is not supported")
@@ -380,7 +384,7 @@ async def request_definition(
380384

381385
if not self.server_started:
382386
self.logger.log(
383-
"find_function_definition called before Language Server started",
387+
"request_definition called before Language Server started",
384388
logging.ERROR,
385389
)
386390
raise MultilspyException("Language Server not started")
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
"""
2+
Provides Clojure specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Clojure.
3+
"""
4+
5+
import asyncio
6+
import json
7+
import logging
8+
import os
9+
import stat
10+
import pathlib
11+
from contextlib import asynccontextmanager
12+
from typing import AsyncIterator
13+
14+
from multilspy.multilspy_logger import MultilspyLogger
15+
from multilspy.language_server import LanguageServer
16+
from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo
17+
from multilspy.lsp_protocol_handler.lsp_types import InitializeParams
18+
from multilspy.multilspy_config import MultilspyConfig
19+
from multilspy.multilspy_utils import FileUtils
20+
from multilspy.multilspy_utils import PlatformUtils
21+
22+
23+
class ClojureLSP(LanguageServer):
24+
"""
25+
Provides a clojure-lsp specific instantiation of the LanguageServer class. Contains various configurations and settings specific to clojure.
26+
"""
27+
28+
def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str):
29+
"""
30+
Creates a ClojureLSP instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
31+
"""
32+
clojure_lsp_executable_path = self.setup_runtime_dependencies(logger, config)
33+
super().__init__(
34+
config,
35+
logger,
36+
repository_root_path,
37+
ProcessLaunchInfo(cmd=clojure_lsp_executable_path, cwd=repository_root_path),
38+
"clojure",
39+
)
40+
self.server_ready = asyncio.Event()
41+
self.initialize_searcher_command_available = asyncio.Event()
42+
self.resolve_main_method_available = asyncio.Event()
43+
self.service_ready_event = asyncio.Event()
44+
45+
def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> str:
46+
"""
47+
Setup runtime dependencies for clojure-lsp.
48+
"""
49+
platform_id = PlatformUtils.get_platform_id()
50+
51+
with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r", encoding="utf-8") as f:
52+
d = json.load(f)
53+
del d["_description"]
54+
55+
runtime_dependencies = d["runtimeDependencies"]
56+
runtime_dependencies = [
57+
dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value
58+
]
59+
assert len(runtime_dependencies) == 1
60+
dependency = runtime_dependencies[0]
61+
62+
clojurelsp_ls_dir = os.path.join(os.path.dirname(__file__), "static", "clojure-lsp")
63+
clojurelsp_executable_path = os.path.join(clojurelsp_ls_dir, dependency["binaryName"])
64+
if not os.path.exists(clojurelsp_ls_dir):
65+
os.makedirs(clojurelsp_ls_dir)
66+
FileUtils.download_and_extract_archive(
67+
logger, dependency["url"], clojurelsp_ls_dir, dependency["archiveType"]
68+
)
69+
assert os.path.exists(clojurelsp_executable_path)
70+
os.chmod(clojurelsp_executable_path, stat.S_IEXEC)
71+
72+
return clojurelsp_executable_path
73+
74+
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
75+
"""
76+
Returns the init params for clojure-lsp.
77+
"""
78+
with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f:
79+
d = json.load(f)
80+
81+
del d["_description"]
82+
83+
d["processId"] = os.getpid()
84+
assert d["rootPath"] == "$rootPath"
85+
d["rootPath"] = repository_absolute_path
86+
87+
assert d["rootUri"] == "$rootUri"
88+
d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri()
89+
90+
assert d["workspaceFolders"][0]["uri"] == "$uri"
91+
d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri()
92+
93+
assert d["workspaceFolders"][0]["name"] == "$name"
94+
d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path)
95+
96+
return d
97+
98+
@asynccontextmanager
99+
async def start_server(self) -> AsyncIterator["ClojureLSP"]:
100+
"""
101+
Starts the Clojure Language Server, waits for the server to be ready and yields the LanguageServer instance.
102+
103+
Usage:
104+
```
105+
async with lsp.start_server():
106+
# LanguageServer has been initialized and ready to serve requests
107+
await lsp.request_definition(...)
108+
await lsp.request_references(...)
109+
# Shutdown the LanguageServer on exit from scope
110+
# LanguageServer has been shutdown
111+
"""
112+
113+
async def register_capability_handler(params):
114+
assert "registrations" in params
115+
for registration in params["registrations"]:
116+
if registration["method"] == "workspace/executeCommand":
117+
self.initialize_searcher_command_available.set()
118+
self.resolve_main_method_available.set()
119+
return
120+
121+
async def lang_status_handler(params):
122+
# TODO: Should we wait for
123+
# server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}}
124+
# Before proceeding?
125+
if params["type"] == "ServiceReady" and params["message"] == "ServiceReady":
126+
self.service_ready_event.set()
127+
128+
async def execute_client_command_handler(params):
129+
return []
130+
131+
async def do_nothing(params):
132+
return
133+
134+
async def check_experimental_status(params):
135+
if params["quiescent"] == True:
136+
self.server_ready.set()
137+
138+
async def window_log_message(msg):
139+
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
140+
141+
self.server.on_request("client/registerCapability", register_capability_handler)
142+
self.server.on_notification("language/status", lang_status_handler)
143+
self.server.on_notification("window/logMessage", window_log_message)
144+
self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
145+
self.server.on_notification("$/progress", do_nothing)
146+
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
147+
self.server.on_notification("language/actionableNotification", do_nothing)
148+
self.server.on_notification("experimental/serverStatus", check_experimental_status)
149+
150+
async with super().start_server():
151+
self.logger.log("Starting clojure-lsp server process", logging.INFO)
152+
await self.server.start()
153+
initialize_params = self._get_initialize_params(self.repository_root_path)
154+
155+
self.logger.log(
156+
"Sending initialize request from LSP client to LSP server and awaiting response",
157+
logging.INFO,
158+
)
159+
init_response = await self.server.send.initialize(initialize_params)
160+
assert init_response["capabilities"]["textDocumentSync"]["change"] == 2
161+
assert "completionProvider" in init_response["capabilities"]
162+
# Clojure-lsp completion provider capabilities are more flexible than other servers
163+
completion_provider = init_response["capabilities"]["completionProvider"]
164+
assert completion_provider["resolveProvider"] == True
165+
assert "triggerCharacters" in completion_provider
166+
self.server.notify.initialized({})
167+
# after initialize, Clojure-lsp is ready to serve
168+
self.server_ready.set()
169+
self.completions_available.set()
170+
171+
yield self
172+
173+
await self.server.shutdown()
174+
await self.server.stop()
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
{
2+
"_description": "Parameters sent by multilspy to initialize clojure-lsp (LSP v3.17)",
3+
"processId": null,
4+
"clientInfo": {
5+
"name": "multilspy",
6+
"version": "0.1.0"
7+
},
8+
"rootUri": "$rootUri",
9+
"rootPath": "$rootPath",
10+
"capabilities": {
11+
"workspace": {
12+
"applyEdit": true,
13+
"workspaceEdit": {
14+
"documentChanges": true
15+
},
16+
"symbol": {
17+
"symbolKind": {
18+
"valueSet": [
19+
1,
20+
2,
21+
3,
22+
4,
23+
5,
24+
6,
25+
7,
26+
8,
27+
9,
28+
10,
29+
11,
30+
12,
31+
13,
32+
14,
33+
15,
34+
16,
35+
17,
36+
18,
37+
19,
38+
20,
39+
21,
40+
22,
41+
23,
42+
24,
43+
25,
44+
26
45+
]
46+
}
47+
},
48+
"workspaceFolders": true
49+
},
50+
"textDocument": {
51+
"synchronization": {
52+
"didSave": true
53+
},
54+
"publishDiagnostics": {
55+
"relatedInformation": true,
56+
"tagSupport": {
57+
"valueSet": [
58+
1,
59+
2
60+
]
61+
}
62+
},
63+
"definition": {
64+
"linkSupport": true
65+
},
66+
"references": {},
67+
"completion": {
68+
"completionItem": {
69+
"snippetSupport": true,
70+
"documentationFormat": [
71+
"markdown",
72+
"plaintext"
73+
]
74+
}
75+
},
76+
"hover": {
77+
"contentFormat": [
78+
"markdown",
79+
"plaintext"
80+
]
81+
},
82+
"documentSymbol": {
83+
"hierarchicalDocumentSymbolSupport": true,
84+
"symbolKind": {
85+
"valueSet": [
86+
1,
87+
2,
88+
3,
89+
4,
90+
5,
91+
6,
92+
7,
93+
8,
94+
9,
95+
10,
96+
11,
97+
12,
98+
13,
99+
14,
100+
15,
101+
16,
102+
17,
103+
18,
104+
19,
105+
20,
106+
21,
107+
22,
108+
23,
109+
24,
110+
25,
111+
26
112+
]
113+
}
114+
}
115+
},
116+
"general": {
117+
"positionEncodings": [
118+
"utf-16"
119+
]
120+
}
121+
},
122+
"initializationOptions": {
123+
"dependency-scheme": "jar",
124+
"text-document-sync-kind": "incremental"
125+
},
126+
"trace": "off",
127+
"workspaceFolders": [
128+
{
129+
"uri": "$uri",
130+
"name": "$name"
131+
}
132+
]
133+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"_description": "Binary distributions of clojure-lsp.",
3+
"runtimeDependencies": [
4+
{
5+
"id": "clojure-lsp",
6+
"description": "clojure-lsp for OSX (ARM64)",
7+
"url": "https://github.com/clojure-lsp/clojure-lsp/releases/latest/download/clojure-lsp-native-macos-aarch64.zip",
8+
"platformId": "osx-arm64",
9+
"archiveType": "zip",
10+
"binaryName": "clojure-lsp"
11+
},
12+
{
13+
"id": "clojure-lsp",
14+
"description": "clojure-lsp for Linux (ARM64)",
15+
"url": "https://github.com/clojure-lsp/clojure-lsp/releases/latest/download/clojure-lsp-native-linux-aarch64.zip",
16+
"platformId": "linux-arm64",
17+
"archiveType": "zip",
18+
"binaryName": "clojure-lsp"
19+
},
20+
{
21+
"id": "clojure-lsp",
22+
"description": "clojure-lsp for Linux (x64)",
23+
"url": "https://github.com/clojure-lsp/clojure-lsp/releases/latest/download/clojure-lsp-native-linux-amd64.zip",
24+
"platformId": "linux-x64",
25+
"archiveType": "zip",
26+
"binaryName": "clojure-lsp"
27+
},
28+
{
29+
"id": "clojure-lsp",
30+
"description": "clojure-lsp for Windows (x64)",
31+
"url": "https://github.com/clojure-lsp/clojure-lsp/releases/latest/download/clojure-lsp-native-windows-amd64.zip",
32+
"platformId": "win-x64",
33+
"archiveType": "zip",
34+
"binaryName": "clojure-lsp"
35+
}
36+
]
37+
}

src/multilspy/multilspy_config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class Language(str, Enum):
2121
RUBY = "ruby"
2222
DART = "dart"
2323
CPP = "cpp"
24+
CLOJURE = "clojure"
2425

2526
def __str__(self) -> str:
2627
return self.value

0 commit comments

Comments
 (0)