Skip to main content
Glama
elixir_tools.py15.2 kB
import logging import os import pathlib import stat import subprocess import threading from typing import Any, cast from overrides import override from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_utils import FileUtils, PlatformId, PlatformUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings from ..common import RuntimeDependency log = logging.getLogger(__name__) class ElixirTools(SolidLanguageServer): """ Provides Elixir specific instantiation of the LanguageServer class using Expert, the official Elixir language server. """ @override def _get_wait_time_for_cross_file_referencing(self) -> float: return 10.0 # Elixir projects need time to compile and index before cross-file references work @override def is_ignored_dirname(self, dirname: str) -> bool: # For Elixir projects, we should ignore: # - _build: compiled artifacts # - deps: dependencies # - node_modules: if the project has JavaScript components # - .elixir_ls: ElixirLS artifacts (in case both are present) # - cover: coverage reports # - .expert: Expert artifacts return super().is_ignored_dirname(dirname) or dirname in ["_build", "deps", "node_modules", ".elixir_ls", ".expert", "cover"] @override def is_ignored_path(self, relative_path: str, ignore_unsupported_files: bool = True) -> bool: """Check if a path should be ignored for symbol indexing.""" if relative_path.endswith("mix.exs"): # These are project configuration files, not source code with symbols to index return True return super().is_ignored_path(relative_path, ignore_unsupported_files) @classmethod def _get_elixir_version(cls) -> str | None: """Get the installed Elixir version or None if not found.""" try: result = subprocess.run(["elixir", "--version"], capture_output=True, text=True, check=False) if result.returncode == 0: return result.stdout.strip() except FileNotFoundError: return None return None @classmethod def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> str: """ Setup runtime dependencies for Expert. Downloads the Expert binary for the current platform and returns the path to the executable. """ # Check if Elixir is available first elixir_version = cls._get_elixir_version() if not elixir_version: 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." ) log.info(f"Found Elixir: {elixir_version}") # First, check if expert is already in PATH (user may have installed it manually) import shutil expert_in_path = shutil.which("expert") if expert_in_path: log.info(f"Found Expert in PATH: {expert_in_path}") return expert_in_path platform_id = PlatformUtils.get_platform_id() valid_platforms = [ PlatformId.LINUX_x64, PlatformId.LINUX_arm64, PlatformId.OSX_x64, PlatformId.OSX_arm64, PlatformId.WIN_x64, PlatformId.WIN_arm64, ] assert platform_id in valid_platforms, f"Platform {platform_id} is not supported for Expert at the moment" expert_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "expert") EXPERT_VERSION = "nightly" # Define runtime dependencies inline runtime_deps = { PlatformId.LINUX_x64: RuntimeDependency( id="expert_linux_amd64", platform_id="linux-x64", url=f"https://github.com/elixir-lang/expert/releases/download/{EXPERT_VERSION}/expert_linux_amd64", archive_type="binary", binary_name="expert_linux_amd64", extract_path="expert", ), PlatformId.LINUX_arm64: RuntimeDependency( id="expert_linux_arm64", platform_id="linux-arm64", url=f"https://github.com/elixir-lang/expert/releases/download/{EXPERT_VERSION}/expert_linux_arm64", archive_type="binary", binary_name="expert_linux_arm64", extract_path="expert", ), PlatformId.OSX_x64: RuntimeDependency( id="expert_darwin_amd64", platform_id="osx-x64", url=f"https://github.com/elixir-lang/expert/releases/download/{EXPERT_VERSION}/expert_darwin_amd64", archive_type="binary", binary_name="expert_darwin_amd64", extract_path="expert", ), PlatformId.OSX_arm64: RuntimeDependency( id="expert_darwin_arm64", platform_id="osx-arm64", url=f"https://github.com/elixir-lang/expert/releases/download/{EXPERT_VERSION}/expert_darwin_arm64", archive_type="binary", binary_name="expert_darwin_arm64", extract_path="expert", ), PlatformId.WIN_x64: RuntimeDependency( id="expert_windows_amd64", platform_id="win-x64", url=f"https://github.com/elixir-lang/expert/releases/download/{EXPERT_VERSION}/expert_windows_amd64.exe", archive_type="binary", binary_name="expert_windows_amd64.exe", extract_path="expert.exe", ), PlatformId.WIN_arm64: RuntimeDependency( id="expert_windows_arm64", platform_id="win-arm64", url=f"https://github.com/elixir-lang/expert/releases/download/{EXPERT_VERSION}/expert_windows_arm64.exe", archive_type="binary", binary_name="expert_windows_arm64.exe", extract_path="expert.exe", ), } dependency = runtime_deps[platform_id] # On Windows, use .exe extension executable_name = "expert.exe" if platform_id.value.startswith("win") else "expert" executable_path = os.path.join(expert_dir, executable_name) assert dependency.binary_name is not None binary_path = os.path.join(expert_dir, dependency.binary_name) if not os.path.exists(executable_path): log.info(f"Downloading Expert binary from {dependency.url}") assert dependency.url is not None FileUtils.download_file(dependency.url, binary_path) # Make the binary executable on Unix-like systems if not platform_id.value.startswith("win"): os.chmod(binary_path, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH) # Create a symlink with the expected name on Unix-like systems if binary_path != executable_path and not platform_id.value.startswith("win"): if os.path.exists(executable_path): os.remove(executable_path) os.symlink(os.path.basename(binary_path), executable_path) assert os.path.exists(executable_path), f"Expert executable not found at {executable_path}" log.info(f"Expert binary ready at: {executable_path}") return executable_path def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): expert_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings) super().__init__( config, repository_root_path, ProcessLaunchInfo(cmd=f"{expert_executable_path} --stdio", cwd=repository_root_path), "elixir", solidlsp_settings, ) self.server_ready = threading.Event() self.request_id = 0 # Set generous timeout for Expert which can be slow to initialize and respond self.set_request_timeout(180.0) @staticmethod def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: """ Returns the initialize params for the Expert Language Server. """ # Ensure the path is absolute abs_path = os.path.abspath(repository_absolute_path) root_uri = pathlib.Path(abs_path).as_uri() initialize_params = { "processId": os.getpid(), "locale": "en", "rootPath": abs_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}, }, "window": { "showMessage": {"messageActionItem": {"additionalPropertiesSupport": True}}, "showDocument": {"support": True}, "workDoneProgress": True, }, }, "workspaceFolders": [{"uri": root_uri, "name": os.path.basename(repository_absolute_path)}], } return cast(InitializeParams, initialize_params) def _start_server(self) -> None: """Start Expert server process""" def register_capability_handler(params: Any) -> None: log.debug(f"LSP: client/registerCapability: {params}") return def window_log_message(msg: Any) -> None: """Handle window/logMessage notifications from Expert""" message_type = msg.get("type", 4) # 1=Error, 2=Warning, 3=Info, 4=Log message_text = msg.get("message", "") # Log at appropriate level based on message type if message_type == 1: log.error(f"Expert: {message_text}") elif message_type == 2: log.warning(f"Expert: {message_text}") else: log.debug(f"Expert: {message_text}") def check_server_ready(params: Any) -> None: """ Handle $/progress notifications from Expert. Expert sends progress updates during compilation and indexing. The server is considered ready when project build completes. """ value = params.get("value", {}) kind = value.get("kind", "") title = value.get("title", "") if kind == "begin": # Track when building the project starts (not "Building engine") if title.startswith("Building ") and not title.startswith("Building engine"): self._building_project = True elif kind == "end": # Project build completion is the main readiness signal if getattr(self, "_building_project", False): log.debug("Expert project build completed - server is ready") self._building_project = False self.server_ready.set() def work_done_progress_create(params: Any) -> None: """Handle window/workDoneProgress/create requests from Expert.""" return def publish_diagnostics(params: Any) -> None: """Handle textDocument/publishDiagnostics notifications.""" return self.server.on_request("client/registerCapability", register_capability_handler) self.server.on_notification("window/logMessage", window_log_message) self.server.on_notification("$/progress", check_server_ready) self.server.on_request("window/workDoneProgress/create", work_done_progress_create) self.server.on_notification("textDocument/publishDiagnostics", publish_diagnostics) log.debug("Starting Expert server process") self.server.start() initialize_params = self._get_initialize_params(self.repository_root_path) log.debug("Sending initialize request to Expert") init_response = self.server.send.initialize(initialize_params) # Verify basic server capabilities assert "textDocumentSync" in init_response["capabilities"], f"Missing textDocumentSync in {init_response['capabilities']}" self.server.notify.initialized({}) self.completions_available.set() # Expert needs time to compile the project and build indexes on first run. # This can take 2-3+ minutes for mid-sized codebases. # After the first run, subsequent startups are much faster. ready_timeout = 300.0 # 5 minutes log.debug(f"Waiting up to {ready_timeout}s for Expert to compile and index...") if self.server_ready.wait(timeout=ready_timeout): log.debug("Expert is ready for requests") else: log.warning(f"Expert did not signal readiness within {ready_timeout}s. Proceeding with requests anyway.") self.server_ready.set() # Mark as ready anyway to allow requests

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ryota-murakami/serena'

If you have feedback or need assistance with the MCP directory API, please join our Discord server