Skip to main content
Glama
fsharp_language_server.py16.8 kB
""" Provides F# specific instantiation of the LanguageServer class. """ import logging import os import pathlib import shutil import threading from pathlib import Path from overrides import override from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_exceptions import SolidLSPException 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 FSharpLanguageServer(SolidLanguageServer): """ Provides F# specific instantiation of the LanguageServer class using Ionide LSP (FsAutoComplete). Contains various configurations and settings specific to F# development. """ def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): """ Creates an FSharpLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ fsharp_lsp_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings) super().__init__( config, repository_root_path, ProcessLaunchInfo(cmd=fsharp_lsp_executable_path, cwd=repository_root_path), "fsharp", solidlsp_settings, ) self.server_ready = threading.Event() self.initialize_searcher_command_available = threading.Event() @override def is_ignored_dirname(self, dirname: str) -> bool: return super().is_ignored_dirname(dirname) or dirname in [ "bin", "obj", "packages", ".paket", "paket-files", ".fake", ".ionide", ] @classmethod def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> str: """ Setup runtime dependencies for F# Language Server and return the command to start the server. """ # First check if .NET SDK is installed dotnet_exe = shutil.which("dotnet") if not dotnet_exe: raise RuntimeError( ".NET SDK is not installed or not in PATH. Please install .NET SDK 8.0 or later and ensure 'dotnet' is in your PATH." ) # Verify dotnet version import subprocess try: result = subprocess.run([dotnet_exe, "--version"], capture_output=True, text=True, check=True) log.info(f"Found .NET SDK version: {result.stdout.strip()}") except subprocess.CalledProcessError: raise RuntimeError("Failed to get .NET SDK version. Please ensure .NET SDK is properly installed.") RuntimeDependencyCollection( [ RuntimeDependency( id="fsautocomplete", description="FsAutoComplete (Ionide F# Language Server)", command="dotnet tool install --tool-path ./ fsautocomplete", platform_id="any", ), ] ) # Install FsAutoComplete if not already installed fsharp_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "fsharp-lsp") fsautocomplete_path = os.path.join(fsharp_ls_dir, "fsautocomplete") # Handle Windows executable extension if os.name == "nt": fsautocomplete_path += ".exe" if not os.path.exists(fsautocomplete_path): log.info(f"FsAutoComplete executable not found at {fsautocomplete_path}. Installing...") # Ensure the directory exists os.makedirs(fsharp_ls_dir, exist_ok=True) # Install FsAutoComplete using dotnet tool install try: import subprocess result = subprocess.run( [dotnet_exe, "tool", "install", "--tool-path", fsharp_ls_dir, "fsautocomplete"], cwd=fsharp_ls_dir, capture_output=True, text=True, check=True, ) log.info("FsAutoComplete installed successfully") log.debug(f"Installation output: {result.stdout}") except subprocess.CalledProcessError as e: log.error(f"Failed to install FsAutoComplete: {e.stderr}") raise RuntimeError(f"Failed to install FsAutoComplete: {e.stderr}") if not os.path.exists(fsautocomplete_path): raise FileNotFoundError( f"FsAutoComplete executable not found at {fsautocomplete_path}, something went wrong with the installation." ) # FsAutoComplete uses --lsp flag for LSP mode return f"{fsautocomplete_path} --adaptive-lsp-server-enabled --project-graph-enabled --use-fcs-transparent-compiler" def _get_initialize_params(self) -> InitializeParams: """ Returns the initialize params for the F# Language Server. """ root_uri = pathlib.Path(self.repository_root_path).as_uri() initialize_params = { "processId": os.getpid(), "rootPath": self.repository_root_path, "rootUri": root_uri, "workspaceFolders": [{"name": "workspace", "uri": root_uri}], "capabilities": { "workspace": { "applyEdit": True, "workspaceEdit": {"documentChanges": True}, "didChangeConfiguration": {"dynamicRegistration": True}, "didChangeWatchedFiles": {"dynamicRegistration": True}, "symbol": {"dynamicRegistration": True}, "executeCommand": {"dynamicRegistration": True}, "configuration": True, "workspaceFolders": True, }, "textDocument": { "synchronization": { "dynamicRegistration": True, "willSave": True, "willSaveWaitUntil": True, "didSave": True, }, "completion": { "dynamicRegistration": True, "contextSupport": True, "completionItem": { "snippetSupport": True, "commitCharactersSupport": True, "documentationFormat": ["markdown", "plaintext"], "deprecatedSupport": True, }, }, "hover": { "dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"], }, "signatureHelp": { "dynamicRegistration": True, "signatureInformation": {"documentationFormat": ["markdown", "plaintext"]}, }, "definition": {"dynamicRegistration": True}, "references": {"dynamicRegistration": True}, "documentHighlight": {"dynamicRegistration": True}, "documentSymbol": { "dynamicRegistration": True, "symbolKind": {"valueSet": list(range(1, 26))}, # All SymbolKind values "hierarchicalDocumentSymbolSupport": True, }, "codeAction": { "dynamicRegistration": True, "codeActionLiteralSupport": { "codeActionKind": { "valueSet": [ "", "quickfix", "refactor", "refactor.extract", "refactor.inline", "refactor.rewrite", "source", "source.organizeImports", ] } }, }, "codeLens": {"dynamicRegistration": True}, "formatting": {"dynamicRegistration": True}, "rangeFormatting": {"dynamicRegistration": True}, "onTypeFormatting": {"dynamicRegistration": True}, "rename": {"dynamicRegistration": True}, "documentLink": {"dynamicRegistration": True}, "publishDiagnostics": { "relatedInformation": True, "versionSupport": False, "tagSupport": {"valueSet": [1, 2]}, }, "implementation": {"dynamicRegistration": True}, "typeDefinition": {"dynamicRegistration": True}, "colorProvider": {"dynamicRegistration": True}, "foldingRange": { "dynamicRegistration": True, "rangeLimit": 5000, "lineFoldingOnly": True, }, "declaration": {"dynamicRegistration": True}, "selectionRange": {"dynamicRegistration": True}, }, "window": { "workDoneProgress": True, }, }, "initializationOptions": { # F# specific initialization options "automaticWorkspaceInit": True, "abstractClassStubGeneration": True, "abstractClassStubGenerationObjectIdentifier": "this", "abstractClassStubGenerationMethodBody": 'failwith "Not Implemented"', "addFsiWatcher": False, "codeLenses": {"signature": {"enabled": True}, "references": {"enabled": True}}, "disableInMemoryProjectReferences": False, "dotNetRoot": self._get_dotnet_root(), "enableMSBuildProjectGraph": False, "excludeProjectDirectories": ["paket-files"], "externalAutocomplete": False, "fsac": {"attachDebugger": False, "silencedLogs": [], "conserveMemory": False, "netCoreDllPath": ""}, "fsiExtraParameters": [], "generateBinlog": False, "interfaceStubGeneration": True, "interfaceStubGenerationObjectIdentifier": "this", "interfaceStubGenerationMethodBody": 'failwith "Not Implemented"', "keywordsAutocomplete": True, "linter": True, "pipelineHints": {"enabled": True}, "recordStubGeneration": True, "recordStubGenerationBody": 'failwith "Not Implemented"', "resolveNamespaces": True, "saveOnlyOpenFiles": False, "showProjectExplorerIn": ["ionide", "solution"], "simplifyNameAnalyzer": True, "smartIndent": False, "suggestGitignore": True, "suggestSdkScripts": True, "unionCaseStubGeneration": True, "unionCaseStubGenerationBody": 'failwith "Not Implemented"', "unusedDeclarationsAnalyzer": True, "unusedOpensAnalyzer": True, "verboseLogging": False, "workspaceModePeekDeepLevel": 2, "workspacePath": self.repository_root_path, }, "trace": "off", } return initialize_params # type: ignore def _get_dotnet_root(self) -> str: """ Get the .NET root directory. """ dotnet_exe = shutil.which("dotnet") if dotnet_exe: # Try to get the installation path try: import subprocess result = subprocess.run([dotnet_exe, "--info"], capture_output=True, text=True, check=True) lines = result.stdout.split("\n") for line in lines: if "Base Path:" in line or "Base path:" in line: base_path = line.split(":", 1)[1].strip() # Get the parent directory (remove 'sdk/version' part) return str(Path(base_path).parent.parent) except (subprocess.CalledProcessError, Exception): pass # Fallback: use the directory containing dotnet executable if dotnet_exe: return str(Path(dotnet_exe).parent) return "" def _start_server(self) -> None: """ Start the F# Language Server with custom handlers. """ def handle_window_log_message(params: dict) -> None: """Handle window/logMessage from the LSP server.""" message = params.get("message", "") message_type = params.get("type", 1) # Map LSP log levels to Python logging levels level_map = {1: logging.ERROR, 2: logging.WARNING, 3: logging.INFO, 4: logging.DEBUG} level = level_map.get(message_type, logging.INFO) log.log(level, f"FsAutoComplete: {message}") def handle_window_show_message(params: dict) -> None: """Handle window/showMessage from the LSP server.""" message = params.get("message", "") message_type = params.get("type", 1) # Map LSP message types to Python logging levels level_map = {1: logging.ERROR, 2: logging.WARNING, 3: logging.INFO, 4: logging.DEBUG} level = level_map.get(message_type, logging.INFO) log.log(level, f"FsAutoComplete Message: {message}") def handle_workspace_configuration(params: dict) -> list: """Handle workspace/configuration requests from the LSP server.""" # Return empty configuration for now items = params.get("items", []) return [None] * len(items) def handle_client_register_capability(params: dict) -> None: """Handle client/registerCapability requests from the LSP server.""" # For now, just acknowledge the registration return def handle_client_unregister_capability(params: dict) -> None: """Handle client/unregisterCapability requests from the LSP server.""" # For now, just acknowledge the unregistration return def handle_work_done_progress_create(params: dict) -> None: """Handle window/workDoneProgress/create requests from the LSP server.""" # Just acknowledge the request - we don't need to track progress for now return # Register custom handlers self.server.on_notification("window/logMessage", handle_window_log_message) self.server.on_notification("window/showMessage", handle_window_show_message) self.server.on_request("workspace/configuration", handle_workspace_configuration) self.server.on_request("client/registerCapability", handle_client_register_capability) self.server.on_request("client/unregisterCapability", handle_client_unregister_capability) self.server.on_request("window/workDoneProgress/create", handle_work_done_progress_create) log.info("Starting FsAutoComplete F# language server process") try: self.server.start() except Exception as e: log.error(f"Failed to start F# language server process: {e}") raise SolidLSPException(f"Failed to start F# language server: {e}") # Send initialization initialize_params = self._get_initialize_params() log.info("Sending initialize request to F# language server") try: self.server.send.initialize(initialize_params) log.debug("Received initialize response from F# language server") except Exception as e: raise SolidLSPException(f"Failed to initialize F# language server for {self.repository_root_path}: {e}") from e # Complete initialization self.server.notify.initialized({}) log.info("F# language server initialized successfully") @override def _get_wait_time_for_cross_file_referencing(self) -> float: """ F# projects can be large and may need more time for cross-file analysis. """ return 15.0 # 15 seconds should be sufficient for most F# projects

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