Elixir: refactored to be in line with the other LS implementations

This commit is contained in:
MischaPanch authored and Michael Panchenko committed 2025-07-24 23:55:29 +02:00
1 parent b63acf2dbe
commit debcccbee2
3 files changed
+111 -197

No files matched your search

@@ -1,4 +1,3 @@
import json
import logging import logging
import os import os
import pathlib import pathlib
@@ -6,17 +5,18 @@ import stat
import subprocess import subprocess
import threading import threading
import time import time
from pathlib import PurePath
from overrides import override from overrides import override
from solidlsp.ls import SolidLanguageServer from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.ls_utils import FileUtils, PlatformUtils from solidlsp.ls_utils import FileUtils, PlatformId, PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from ..common import RuntimeDependency
class ElixirTools(SolidLanguageServer): class ElixirTools(SolidLanguageServer):
""" """
@@ -68,8 +68,8 @@ class ElixirTools(SolidLanguageServer):
return filtered_response return filtered_response
@staticmethod @classmethod
def _get_elixir_version(): def _get_elixir_version(cls):
"""Get the installed Elixir version or None if not found.""" """Get the installed Elixir version or None if not found."""
try: try:
result = subprocess.run(["elixir", "--version"], capture_output=True, text=True, check=False) result = subprocess.run(["elixir", "--version"], capture_output=True, text=True, check=False)
@@ -79,13 +79,14 @@ class ElixirTools(SolidLanguageServer):
return None return None
return None return None
def setupRuntimeDependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> str: @classmethod
def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> str:
""" """
Setup runtime dependencies for Next LS. Setup runtime dependencies for Next LS.
Downloads the Next LS binary for the current platform and returns the path to the executable. Downloads the Next LS binary for the current platform and returns the path to the executable.
""" """
# Check if Elixir is available first # Check if Elixir is available first
elixir_version = self._get_elixir_version() elixir_version = cls._get_elixir_version()
if not elixir_version: if not elixir_version:
raise RuntimeError( raise RuntimeError(
"Elixir is not installed. Please install Elixir from https://elixir-lang.org/install.html and make sure it is added to your PATH." "Elixir is not installed. Please install Elixir from https://elixir-lang.org/install.html and make sure it is added to your PATH."
@@ -93,62 +94,68 @@ class ElixirTools(SolidLanguageServer):
logger.log(f"Found Elixir: {elixir_version}", logging.INFO) logger.log(f"Found Elixir: {elixir_version}", logging.INFO)
platformId = PlatformUtils.get_platform_id() platform_id = PlatformUtils.get_platform_id()
with open(str(PurePath(os.path.dirname(__file__), "runtime_dependencies.json")), encoding="utf-8") as f:
runtimeDependencies = json.load(f)
del runtimeDependencies["_description"]
os.makedirs(str(PurePath(os.path.abspath(os.path.dirname(__file__)), "static")), exist_ok=True)
# Map platform IDs to runtime dependency keys
platform_mapping = {
"linux-x64": "linux-x64",
"osx-x64": "darwin-x64",
"osx-arm64": "darwin-arm64",
"darwin-x64": "darwin-x64",
"darwin-arm64": "darwin-arm64",
"win-x64": "win-x64",
}
platform_key = platform_mapping.get(platformId.value)
if not platform_key:
raise RuntimeError(f"Unsupported platform for Next LS: {platformId.value}")
# Check for Windows and provide a helpful error message # Check for Windows and provide a helpful error message
if platformId.value.startswith("win"): if platform_id.value.startswith("win"):
raise RuntimeError( raise RuntimeError(
"Windows is not supported by Next LS. The Next LS project does not provide Windows binaries. " "Windows is not supported by Next LS. The Next LS project does not provide Windows binaries. "
"Consider using Windows Subsystem for Linux (WSL) or a virtual machine with Linux/macOS." "Consider using Windows Subsystem for Linux (WSL) or a virtual machine with Linux/macOS."
) )
dependency = runtimeDependencies["next_ls"][platform_key] valid_platforms = [
next_ls_dir = str(PurePath(os.path.abspath(os.path.dirname(__file__)), "static", dependency["relative_extraction_path"])) PlatformId.LINUX_x64,
os.makedirs(next_ls_dir, exist_ok=True) PlatformId.OSX_x64,
PlatformId.OSX_arm64,
]
assert platform_id in valid_platforms, f"Platform {platform_id} is not supported for Next LS at the moment"
executable_path = str(PurePath(next_ls_dir, dependency["executable_name"])) next_ls_dir = os.path.join(cls.ls_resources_dir(), "next-ls")
binary_path = str(PurePath(next_ls_dir, dependency["binary_name"]))
# Define runtime dependencies inline
runtime_deps = {
PlatformId.LINUX_x64: RuntimeDependency(
id="next_ls_linux_amd64",
platform_id="linux-x64",
url="https://github.com/elixir-tools/next-ls/releases/download/v0.23.3/next_ls_linux_amd64",
archive_type="binary",
binary_name="next_ls_linux_amd64",
extract_path="next_ls",
),
PlatformId.OSX_x64: RuntimeDependency(
id="next_ls_darwin_amd64",
platform_id="osx-x64",
url="https://github.com/elixir-tools/next-ls/releases/download/v0.23.3/next_ls_darwin_amd64",
archive_type="binary",
binary_name="next_ls_darwin_amd64",
extract_path="next_ls",
),
PlatformId.OSX_arm64: RuntimeDependency(
id="next_ls_darwin_arm64",
platform_id="osx-arm64",
url="https://github.com/elixir-tools/next-ls/releases/download/v0.23.3/next_ls_darwin_arm64",
archive_type="binary",
binary_name="next_ls_darwin_arm64",
extract_path="next_ls",
),
}
dependency = runtime_deps[platform_id]
executable_path = os.path.join(next_ls_dir, "nextls")
binary_path = os.path.join(next_ls_dir, dependency.binary_name)
if not os.path.exists(executable_path): if not os.path.exists(executable_path):
logger.log(f"Downloading Next LS binary from {dependency['url']}", logging.INFO) logger.log(f"Downloading Next LS binary from {dependency.url}", logging.INFO)
FileUtils.download_file(logger, dependency["url"], binary_path) FileUtils.download_file(logger, dependency.url, binary_path)
# Make the binary executable on Unix-like systems # Make the binary executable on Unix-like systems
if not platformId.value.startswith("win"): os.chmod(binary_path, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
os.chmod(binary_path, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
# Create a symlink or copy with the expected name # Create a symlink with the expected name
if binary_path != executable_path: if binary_path != executable_path:
if os.path.exists(executable_path): if os.path.exists(executable_path):
os.remove(executable_path) os.remove(executable_path)
if platformId.value.startswith("win"): os.symlink(os.path.basename(binary_path), executable_path)
# On Windows, copy the file
import shutil
shutil.copy2(binary_path, executable_path)
else:
# On Unix-like systems, create a symlink
os.symlink(os.path.basename(binary_path), executable_path)
assert os.path.exists(executable_path), f"Next LS executable not found at {executable_path}" assert os.path.exists(executable_path), f"Next LS executable not found at {executable_path}"
@@ -156,7 +163,7 @@ class ElixirTools(SolidLanguageServer):
return executable_path return executable_path
def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str):
nextls_executable_path = self.setupRuntimeDependencies(logger, config) nextls_executable_path = self._setup_runtime_dependencies(logger, config)
super().__init__( super().__init__(
config, config,
@@ -171,29 +178,66 @@ class ElixirTools(SolidLanguageServer):
# Set generous timeout for Next LS which can be slow to initialize and respond # Set generous timeout for Next LS which can be slow to initialize and respond
self.set_request_timeout(180.0) # 60 seconds for all environments self.set_request_timeout(180.0) # 60 seconds for all environments
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: @staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
""" """
Returns the initialize params for the Next LS Language Server. Returns the initialize params for the Next LS Language Server.
""" """
with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), encoding="utf-8") as f: root_uri = pathlib.Path(repository_absolute_path).as_uri()
d = json.load(f) initialize_params = {
"processId": os.getpid(),
"locale": "en",
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"initializationOptions": {
"mix_env": "dev",
"mix_target": "host",
"experimental": {"completions": {"enable": False}},
"extensions": {"credo": {"enable": True, "cli_options": []}},
},
"capabilities": {
"textDocument": {
"synchronization": {"didSave": True, "dynamicRegistration": True},
"completion": {
"dynamicRegistration": True,
"completionItem": {"snippetSupport": True, "documentationFormat": ["markdown", "plaintext"]},
},
"definition": {"dynamicRegistration": True},
"references": {"dynamicRegistration": True},
"documentSymbol": {
"dynamicRegistration": True,
"hierarchicalDocumentSymbolSupport": True,
"symbolKind": {"valueSet": list(range(1, 27))},
},
"hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
"formatting": {"dynamicRegistration": True},
"codeAction": {
"dynamicRegistration": True,
"codeActionLiteralSupport": {
"codeActionKind": {
"valueSet": [
"quickfix",
"refactor",
"refactor.extract",
"refactor.inline",
"refactor.rewrite",
"source",
"source.organizeImports",
]
}
},
},
},
"workspace": {
"workspaceFolders": True,
"didChangeConfiguration": {"dynamicRegistration": True},
"executeCommand": {"dynamicRegistration": True},
},
},
"workspaceFolders": [{"uri": root_uri, "name": os.path.basename(repository_absolute_path)}],
}
del d["_description"] return initialize_params
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
def _start_server(self): def _start_server(self):
"""Start Next LS server process""" """Start Next LS server process"""
@@ -1,97 +0,0 @@
{
"_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize",
"processId": "os.getpid()",
"locale": "en",
"rootPath": "$rootPath",
"rootUri": "$rootUri",
"initializationOptions": {
"mix_env": "dev",
"mix_target": "host",
"experimental": {
"completions": {
"enable": false
}
},
"extensions": {
"credo": {
"enable": true,
"cli_options": []
}
}
},
"capabilities": {
"textDocument": {
"synchronization": {
"didSave": true,
"dynamicRegistration": true
},
"completion": {
"dynamicRegistration": true,
"completionItem": {
"snippetSupport": true,
"documentationFormat": [
"markdown",
"plaintext"
]
}
},
"definition": {
"dynamicRegistration": true
},
"references": {
"dynamicRegistration": true
},
"documentSymbol": {
"dynamicRegistration": true,
"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
]
}
},
"hover": {
"dynamicRegistration": true,
"contentFormat": [
"markdown",
"plaintext"
]
},
"formatting": {
"dynamicRegistration": true
},
"codeAction": {
"dynamicRegistration": true,
"codeActionLiteralSupport": {
"codeActionKind": {
"valueSet": [
"quickfix",
"refactor",
"refactor.extract",
"refactor.inline",
"refactor.rewrite",
"source",
"source.organizeImports"
]
}
}
}
},
"workspace": {
"workspaceFolders": true,
"didChangeConfiguration": {
"dynamicRegistration": true
},
"executeCommand": {
"dynamicRegistration": true
}
}
},
"workspaceFolders": [
{
"uri": "$uri",
"name": "$name"
}
]
}
@@ -1,33 +0,0 @@
{
"_description": "This file lists the runtime dependencies for the Elixir Language Server (Next LS)",
"next_ls": {
"linux-x64": {
"url": "https://github.com/elixir-tools/next-ls/releases/download/v0.23.3/next_ls_linux_amd64",
"archiveType": "binary",
"relative_extraction_path": "next_ls",
"binary_name": "next_ls_linux_amd64",
"executable_name": "nextls"
},
"darwin-x64": {
"url": "https://github.com/elixir-tools/next-ls/releases/download/v0.23.3/next_ls_darwin_amd64",
"archiveType": "binary",
"relative_extraction_path": "next_ls",
"binary_name": "next_ls_darwin_amd64",
"executable_name": "nextls"
},
"darwin-arm64": {
"url": "https://github.com/elixir-tools/next-ls/releases/download/v0.23.3/next_ls_darwin_arm64",
"archiveType": "binary",
"relative_extraction_path": "next_ls",
"binary_name": "next_ls_darwin_arm64",
"executable_name": "nextls"
},
"win-x64": {
"url": "https://github.com/elixir-tools/next-ls/releases/download/v0.23.3/next_ls_windows_amd64.exe",
"archiveType": "binary",
"relative_extraction_path": "next_ls",
"binary_name": "next_ls_windows_amd64.exe",
"executable_name": "nextls.exe"
}
}
}