Skip to main content
Glama
haskell_language_server.py17.8 kB
""" Provides Haskell specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Haskell. """ import logging import os import pathlib import shutil import time from typing import Any from overrides import override from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings log = logging.getLogger(__name__) class HaskellLanguageServer(SolidLanguageServer): """ Provides Haskell specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Haskell. Uses Haskell Language Server (HLS) for LSP functionality. """ @staticmethod def _ensure_hls_installed() -> str: """Ensure haskell-language-server-wrapper is available.""" # Try common locations common_paths = [ shutil.which("haskell-language-server-wrapper"), "/opt/homebrew/bin/haskell-language-server-wrapper", "/usr/local/bin/haskell-language-server-wrapper", os.path.expanduser("~/.ghcup/bin/haskell-language-server-wrapper"), os.path.expanduser("~/.cabal/bin/haskell-language-server-wrapper"), os.path.expanduser("~/.local/bin/haskell-language-server-wrapper"), ] # Check Stack programs directory stack_programs = os.path.expanduser("~/.local/share/stack/programs") if os.path.exists(stack_programs): try: for arch_dir in os.listdir(stack_programs): arch_path = os.path.join(stack_programs, arch_dir) if os.path.isdir(arch_path): try: for ghc_dir in os.listdir(arch_path): hls_path = os.path.join(arch_path, ghc_dir, "bin", "haskell-language-server-wrapper") if os.path.isfile(hls_path) and os.access(hls_path, os.X_OK): common_paths.append(hls_path) except (PermissionError, OSError): # Skip directories we can't read continue except (PermissionError, OSError): # Stack programs directory not accessible pass for path in common_paths: if path and os.path.isfile(path) and os.access(path, os.X_OK): return path raise RuntimeError( "haskell-language-server-wrapper is not installed or not in PATH.\n" "Searched locations:\n" + "\n".join(f" - {p}" for p in common_paths if p) + "\n" "Please install HLS via:\n" " - GHCup: https://www.haskell.org/ghcup/\n" " - Stack: stack install haskell-language-server\n" " - Cabal: cabal install haskell-language-server\n" " - Homebrew (macOS): brew install haskell-language-server" ) def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): """ Creates a HaskellLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ hls_executable_path = self._ensure_hls_installed() log.info(f"Using haskell-language-server at: {hls_executable_path}") # Check if there's a haskell subdirectory with Stack/Cabal project haskell_subdir = os.path.join(repository_root_path, "haskell") if os.path.exists(haskell_subdir) and ( os.path.exists(os.path.join(haskell_subdir, "stack.yaml")) or os.path.exists(os.path.join(haskell_subdir, "cabal.project")) ): working_dir = haskell_subdir log.info(f"Using Haskell project directory: {working_dir}") else: working_dir = repository_root_path # Set up environment with GHCup bin in PATH env = dict(os.environ) ghcup_bin = os.path.expanduser("~/.ghcup/bin") if ghcup_bin not in env.get("PATH", ""): env["PATH"] = f"{ghcup_bin}{os.pathsep}{env.get('PATH', '')}" super().__init__( config, repository_root_path, ProcessLaunchInfo(cmd=[hls_executable_path, "--lsp", "--cwd", working_dir], cwd=working_dir, env=env), "haskell", solidlsp_settings, ) @override def is_ignored_dirname(self, dirname: str) -> bool: return super().is_ignored_dirname(dirname) or dirname in ["dist", "dist-newstyle", ".stack-work", ".cabal-sandbox"] @staticmethod def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: """ Returns the initialize params for the Haskell Language Server. """ root_uri = pathlib.Path(repository_absolute_path).as_uri() initialize_params = { "clientInfo": {"name": "Serena", "version": "0.1.0"}, "locale": "en", "capabilities": { "workspace": { "applyEdit": True, "workspaceEdit": { "documentChanges": True, "resourceOperations": ["create", "rename", "delete"], "failureHandling": "textOnlyTransactional", "normalizesLineEndings": True, "changeAnnotationSupport": {"groupsOnLabel": True}, }, "configuration": True, "didChangeWatchedFiles": {"dynamicRegistration": True, "relativePatternSupport": True}, "symbol": { "dynamicRegistration": True, "symbolKind": {"valueSet": list(range(1, 27))}, "tagSupport": {"valueSet": [1]}, "resolveSupport": {"properties": ["location.range"]}, }, "executeCommand": {"dynamicRegistration": True}, "didChangeConfiguration": {"dynamicRegistration": True}, "workspaceFolders": True, "semanticTokens": {"refreshSupport": True}, }, "textDocument": { "publishDiagnostics": { "relatedInformation": True, "versionSupport": False, "tagSupport": {"valueSet": [1, 2]}, "codeDescriptionSupport": True, "dataSupport": True, }, "synchronization": {"dynamicRegistration": True, "willSave": True, "willSaveWaitUntil": True, "didSave": True}, "completion": { "dynamicRegistration": True, "contextSupport": True, "completionItem": { "snippetSupport": True, "commitCharactersSupport": True, "documentationFormat": ["markdown", "plaintext"], "deprecatedSupport": True, "preselectSupport": True, "tagSupport": {"valueSet": [1]}, "insertReplaceSupport": True, "resolveSupport": {"properties": ["documentation", "detail", "additionalTextEdits"]}, "insertTextModeSupport": {"valueSet": [1, 2]}, "labelDetailsSupport": True, }, "insertTextMode": 2, "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}, "activeParameterSupport": True, }, "contextSupport": True, }, "definition": {"dynamicRegistration": True, "linkSupport": True}, "references": {"dynamicRegistration": True}, "documentHighlight": {"dynamicRegistration": True}, "documentSymbol": { "dynamicRegistration": True, "symbolKind": {"valueSet": list(range(1, 27))}, "hierarchicalDocumentSymbolSupport": True, "tagSupport": {"valueSet": [1]}, "labelSupport": True, }, "codeAction": { "dynamicRegistration": True, "isPreferredSupport": True, "disabledSupport": True, "dataSupport": True, "resolveSupport": {"properties": ["edit"]}, "codeActionLiteralSupport": { "codeActionKind": { "valueSet": [ "", "quickfix", "refactor", "refactor.extract", "refactor.inline", "refactor.rewrite", "source", "source.organizeImports", ] } }, "honorsChangeAnnotations": False, }, "formatting": {"dynamicRegistration": True}, "rangeFormatting": {"dynamicRegistration": True}, "onTypeFormatting": {"dynamicRegistration": True}, "rename": { "dynamicRegistration": True, "prepareSupport": True, "prepareSupportDefaultBehavior": 1, "honorsChangeAnnotations": True, }, "documentLink": {"dynamicRegistration": True, "tooltipSupport": True}, "typeDefinition": {"dynamicRegistration": True, "linkSupport": True}, "implementation": {"dynamicRegistration": True, "linkSupport": True}, "colorProvider": {"dynamicRegistration": True}, "foldingRange": { "dynamicRegistration": True, "rangeLimit": 5000, "lineFoldingOnly": True, "foldingRangeKind": {"valueSet": ["comment", "imports", "region"]}, }, "declaration": {"dynamicRegistration": True, "linkSupport": True}, "selectionRange": {"dynamicRegistration": True}, "callHierarchy": {"dynamicRegistration": True}, "semanticTokens": { "dynamicRegistration": True, "tokenTypes": [ "namespace", "type", "class", "enum", "interface", "struct", "typeParameter", "parameter", "variable", "property", "enumMember", "event", "function", "method", "macro", "keyword", "modifier", "comment", "string", "number", "regexp", "operator", ], "tokenModifiers": [ "declaration", "definition", "readonly", "static", "deprecated", "abstract", "async", "modification", "documentation", "defaultLibrary", ], "formats": ["relative"], "requests": {"range": True, "full": {"delta": True}}, "multilineTokenSupport": False, "overlappingTokenSupport": False, }, "linkedEditingRange": {"dynamicRegistration": True}, }, "window": { "showMessage": {"messageActionItem": {"additionalPropertiesSupport": True}}, "showDocument": {"support": True}, "workDoneProgress": True, }, "general": { "staleRequestSupport": {"cancel": True, "retryOnContentModified": []}, "regularExpressions": {"engine": "ECMAScript", "version": "ES2020"}, "markdown": { "parser": "marked", "version": "1.1.0", }, "positionEncodings": ["utf-16"], }, }, "initializationOptions": { "haskell": { "formattingProvider": "ormolu", "checkProject": True, } }, "trace": "verbose", "processId": os.getpid(), "rootPath": repository_absolute_path, "rootUri": root_uri, "workspaceFolders": [ { "uri": root_uri, "name": os.path.basename(repository_absolute_path), } ], } return initialize_params # type: ignore def _start_server(self) -> None: """ Starts the Haskell Language Server """ def do_nothing(params: Any) -> None: return def window_log_message(msg: dict) -> None: log.info(f"LSP: window/logMessage: {msg}") def register_capability_handler(params: dict) -> None: """Handle dynamic capability registration from HLS""" if "registrations" in params: for registration in params.get("registrations", []): method = registration.get("method", "") log.info(f"HLS registered capability: {method}") return def workspace_configuration_handler(params: dict) -> Any: """Handle workspace/configuration requests from HLS""" log.info(f"HLS requesting configuration: {params}") # Configuration matching VS Code settings and initialization options haskell_config = { "formattingProvider": "ormolu", "checkProject": True, "plugin": {"importLens": {"codeActionsOn": False, "codeLensOn": False}, "hlint": {"codeActionsOn": False}}, } # HLS expects array of config items matching requested sections if isinstance(params, dict) and "items" in params: result = [] for item in params["items"]: section = item.get("section", "") if section == "haskell": result.append(haskell_config) else: result.append({}) log.info(f"Returning configuration: {result}") return result # Fallback: return single config return [haskell_config] self.server.on_notification("window/logMessage", window_log_message) self.server.on_notification("$/progress", do_nothing) self.server.on_notification("textDocument/publishDiagnostics", do_nothing) self.server.on_request("client/registerCapability", register_capability_handler) self.server.on_request("workspace/configuration", workspace_configuration_handler) log.info("Starting Haskell Language Server process") self.server.start() initialize_params = self._get_initialize_params(self.repository_root_path) log.info("Sending initialize request from LSP client to LSP server and awaiting response") init_response = self.server.send.initialize(initialize_params) # Log capabilities returned by HLS capabilities = init_response.get("capabilities", {}) log.info(f"HLS capabilities: {list(capabilities.keys())}") self.server.notify.initialized({}) self.completions_available.set() # Give HLS time to index the project # HLS can be slow to index, especially on first run log.info("Waiting for HLS to index project...") time.sleep(5) log.info("Haskell Language Server initialized successfully")

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/oraios/serena'

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