From ac7547308efca7029d674dd9786629db5900784c Mon Sep 17 00:00:00 2001 From: Claude Assistant Date: Wed, 25 Jun 2025 22:16:39 +0100 Subject: [PATCH] Replace csharp-ls with official Microsoft.CodeAnalysis.LanguageServer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit updates the C# language server implementation to use the official Microsoft.CodeAnalysis.LanguageServer package instead of csharp-ls. Key changes: - Download and use platform-specific Microsoft.CodeAnalysis.LanguageServer packages - Support for Windows, Linux, and macOS with appropriate runtime IDs - Automatic package download and caching in ~/.cache/serena/language-servers/ - Uses stdio communication by default - Enhanced initialization with workspace folders support The new implementation provides better stability and feature parity with VS Code's official C# extension since it uses the same underlying language server. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../csharp_language_server.py | 271 ++++++++++++------ 1 file changed, 180 insertions(+), 91 deletions(-) diff --git a/src/solidlsp/language_servers/csharp_language_server/csharp_language_server.py b/src/solidlsp/language_servers/csharp_language_server/csharp_language_server.py index baab351..bde0438 100644 --- a/src/solidlsp/language_servers/csharp_language_server/csharp_language_server.py +++ b/src/solidlsp/language_servers/csharp_language_server/csharp_language_server.py @@ -65,8 +65,8 @@ def find_solution_or_project_file(root_dir) -> str | None: class CSharpLanguageServer(SolidLanguageServer): """ - Provides C# specific instantiation of the LanguageServer class using csharp-ls. - csharp-ls is a Roslyn-based LSP server that provides modern C# language features. + Provides C# specific instantiation of the LanguageServer class using Microsoft.CodeAnalysis.LanguageServer. + This is the official Roslyn-based language server from Microsoft. """ def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): @@ -74,28 +74,23 @@ class CSharpLanguageServer(SolidLanguageServer): Creates a CSharpLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - csharp_ls_path = self.setup_runtime_dependencies(logger, config) + language_server_path = self.setup_runtime_dependencies(logger, config) # Find solution or project file solution_or_project = find_solution_or_project_file(repository_root_path) - # Build command - cmd_parts = [csharp_ls_path] + # Build command - Microsoft.CodeAnalysis.LanguageServer uses stdio by default + cmd_parts = ["dotnet", language_server_path] # Add logging level if debug is enabled if logger.logger.level <= logging.DEBUG: - cmd_parts.extend(["--loglevel", "info"]) - else: - cmd_parts.extend(["--loglevel", "error"]) + cmd_parts.extend(["--logLevel", "Information"]) - # Add solution file if found + # The language server will discover the solution/project from the workspace root if solution_or_project: - # Extract relative path from repository root - rel_path = os.path.relpath(solution_or_project, repository_root_path) - cmd_parts.extend(["--solution", rel_path]) - logger.log(f"Using solution/project file: {rel_path}", logging.INFO) + logger.log(f"Found solution/project file: {solution_or_project}", logging.INFO) else: - logger.log("No .sln or .csproj file found, csharp-ls will attempt auto-discovery", logging.WARNING) + logger.log("No .sln or .csproj file found, language server will attempt auto-discovery", logging.WARNING) cmd = " ".join(cmd_parts) @@ -115,87 +110,157 @@ class CSharpLanguageServer(SolidLanguageServer): def setup_runtime_dependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> str: """ - Set up csharp-ls by ensuring it's installed as a dotnet tool. - Returns the path to the csharp-ls executable. + Set up Microsoft.CodeAnalysis.LanguageServer by downloading the NuGet package. + Returns the path to the language server DLL. """ - # First check if csharp-ls is already available in PATH - csharp_ls_path = shutil.which("csharp-ls") - if csharp_ls_path: - logger.log(f"Found csharp-ls in PATH: {csharp_ls_path}", logging.INFO) - return csharp_ls_path + import platform + import tempfile + from pathlib import Path - # Check if dotnet is available - dotnet_path = shutil.which("dotnet") - if not dotnet_path: + # Determine the runtime ID based on the platform + system = platform.system().lower() + machine = platform.machine().lower() + + # Map platform info to runtime ID + if system == "windows": + runtime_id = "win-x64" if machine in ["amd64", "x86_64"] else "win-arm64" + elif system == "darwin": + runtime_id = "osx-x64" if machine in ["x86_64"] else "osx-arm64" + elif system == "linux": + # Check if we're on musl or glibc + # For now, assume glibc (most common) + runtime_id = "linux-x64" if machine in ["x86_64", "amd64"] else "linux-arm64" + else: + # Fallback to neutral package + runtime_id = "neutral" + + # Package configuration + package_name = f"Microsoft.CodeAnalysis.LanguageServer.{runtime_id}" + package_version = "4.13.0-2.final" # Latest stable version as of search + + # Check if already downloaded + cache_dir = Path.home() / ".cache" / "serena" / "language-servers" / "csharp" + cache_dir.mkdir(parents=True, exist_ok=True) + + server_dir = cache_dir / f"{package_name}.{package_version}" + server_dll = server_dir / "Microsoft.CodeAnalysis.LanguageServer.dll" + + if server_dll.exists(): + logger.log(f"Using cached Microsoft.CodeAnalysis.LanguageServer from {server_dll}", logging.INFO) + return str(server_dll) + + # Download the package + logger.log(f"Downloading {package_name} version {package_version}...", logging.INFO) + + # Check if nuget or dotnet is available + nuget_cmd = shutil.which("nuget") + dotnet_cmd = shutil.which("dotnet") + + if not nuget_cmd and not dotnet_cmd: raise LanguageServerException( - "dotnet SDK is not installed or not in PATH. " - "Please install the .NET SDK from https://dotnet.microsoft.com/download" + "Neither nuget nor dotnet CLI is available. " + "Please install .NET SDK from https://dotnet.microsoft.com/download" ) - # Check if csharp-ls is installed as a global tool - try: - result = subprocess.run( - ["dotnet", "tool", "list", "-g"], - capture_output=True, - text=True, - check=True - ) - if "csharp-ls" in result.stdout: - # csharp-ls is installed, but not in PATH - # Try to find it in the default dotnet tools directory - home = os.path.expanduser("~") - possible_paths = [ - os.path.join(home, ".dotnet", "tools", "csharp-ls"), - os.path.join(home, ".dotnet", "tools", "csharp-ls.exe"), - ] - for path in possible_paths: - if os.path.exists(path): - logger.log(f"Found csharp-ls at: {path}", logging.INFO) - return path - except subprocess.CalledProcessError: - pass - - # Install csharp-ls as a global tool - logger.log("Installing csharp-ls as a global dotnet tool...", logging.INFO) - try: - subprocess.run( - ["dotnet", "tool", "install", "-g", "csharp-ls"], - check=True, - capture_output=True, - text=True - ) - logger.log("Successfully installed csharp-ls", logging.INFO) - except subprocess.CalledProcessError as e: - # It might already be installed but failed to update - if "is already installed" in e.stderr: - logger.log("csharp-ls is already installed", logging.INFO) + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + if dotnet_cmd: + # Use dotnet restore to download the package + project_content = f""" + + net8.0 + + + + +""" + + project_file = temp_path / "temp.csproj" + project_file.write_text(project_content) + + try: + # Restore the package + subprocess.run( + [dotnet_cmd, "restore", str(project_file), "--packages", str(temp_path)], + check=True, + capture_output=True, + text=True + ) + + # Find the downloaded package + package_path = temp_path / package_name.lower() / package_version + + except subprocess.CalledProcessError as e: + raise LanguageServerException(f"Failed to download package: {e.stderr}") + else: - raise LanguageServerException(f"Failed to install csharp-ls: {e.stderr}") - - # After installation, try to find it again - csharp_ls_path = shutil.which("csharp-ls") - if csharp_ls_path: - return csharp_ls_path - - # Try the default locations again - home = os.path.expanduser("~") - possible_paths = [ - os.path.join(home, ".dotnet", "tools", "csharp-ls"), - os.path.join(home, ".dotnet", "tools", "csharp-ls.exe"), - ] - for path in possible_paths: - if os.path.exists(path): - logger.log(f"Found csharp-ls at: {path}", logging.INFO) - return path - - raise LanguageServerException( - "Failed to find csharp-ls after installation. " - "Please ensure ~/.dotnet/tools is in your PATH" - ) + # Use nuget to download the package + try: + subprocess.run( + [ + nuget_cmd, "install", package_name, + "-Version", package_version, + "-OutputDirectory", str(temp_path), + "-NonInteractive" + ], + check=True, + capture_output=True, + text=True + ) + + # Find the downloaded package + package_path = temp_path / f"{package_name}.{package_version}" + + except subprocess.CalledProcessError as e: + raise LanguageServerException(f"Failed to download package: {e.stderr}") + + # Extract the language server files + if runtime_id == "neutral": + # For neutral package, files are in lib/net8.0 + source_dir = package_path / "lib" / "net8.0" + else: + # For runtime-specific packages, files are in content/LanguageServer/{runtime-id} + source_dir = package_path / "content" / "LanguageServer" / runtime_id + + if not source_dir.exists(): + # Try alternative locations + for possible_dir in [ + package_path / "tools" / "net8.0" / "any", + package_path / "lib" / "net8.0", + package_path / "contentFiles" / "any" / "net8.0" + ]: + if possible_dir.exists(): + source_dir = possible_dir + break + else: + raise LanguageServerException( + f"Could not find language server files in package. " + f"Searched in {package_path}" + ) + + # Copy files to cache directory + server_dir.mkdir(parents=True, exist_ok=True) + + import shutil as shutil_module + shutil_module.copytree(source_dir, server_dir, dirs_exist_ok=True) + + if not server_dll.exists(): + raise LanguageServerException( + "Microsoft.CodeAnalysis.LanguageServer.dll not found after extraction" + ) + + # Make the DLL executable on Unix-like systems + if system != "windows": + import stat + server_dll.chmod(server_dll.stat().st_mode | stat.S_IEXEC) + + logger.log(f"Successfully installed Microsoft.CodeAnalysis.LanguageServer to {server_dll}", logging.INFO) + return str(server_dll) def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: """ - Returns the initialize params for the csharp-ls Language Server. + Returns the initialize params for the Microsoft.CodeAnalysis.LanguageServer. """ initialize_params: InitializeParams = { # type: ignore "processId": os.getpid(), @@ -214,6 +279,8 @@ class CSharpLanguageServer(SolidLanguageServer): }, }, "executeCommand": {"dynamicRegistration": True}, + "configuration": True, + "workspaceFolders": True, }, "textDocument": { "synchronization": { @@ -277,6 +344,11 @@ class CSharpLanguageServer(SolidLanguageServer): "onTypeFormatting": {"dynamicRegistration": True}, "rename": {"dynamicRegistration": True}, "publishDiagnostics": {"relatedInformation": True}, + "foldingRange": { + "dynamicRegistration": True, + "rangeLimit": 5000, + "lineFoldingOnly": True + }, }, }, "workspaceFolders": [ @@ -288,7 +360,7 @@ class CSharpLanguageServer(SolidLanguageServer): def _start_server(self): """ - Starts the csharp-ls Language Server. + Starts the Microsoft.CodeAnalysis.LanguageServer. Usage: ``` @@ -307,20 +379,37 @@ class CSharpLanguageServer(SolidLanguageServer): def window_log_message(msg): """Log messages from the language server.""" message_text = msg.get("message", "") - self.logger.log(f"LSP: window/logMessage: {message_text}", logging.INFO) + level = msg.get("type", 4) # Default to Log level + + # Map LSP message types to Python logging levels + level_map = { + 1: logging.ERROR, # Error + 2: logging.WARNING, # Warning + 3: logging.INFO, # Info + 4: logging.DEBUG # Log + } + + self.logger.log(f"LSP: {message_text}", level_map.get(level, logging.DEBUG)) + + def handle_workspace_configuration(params): + """Handle workspace/configuration requests from the server.""" + # Return empty configuration for now + items = params.get("items", []) + return [{}] * len(items) # Set up notification handlers 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("workspace/configuration", handle_workspace_configuration) - self.logger.log("Starting csharp-ls server process", logging.INFO) + self.logger.log("Starting Microsoft.CodeAnalysis.LanguageServer process", logging.INFO) self.server.start() # Send initialization initialize_params = self._get_initialize_params(self.repository_root_path) - self.logger.log("Sending initialize request to csharp-ls server", logging.INFO) + self.logger.log("Sending initialize request to language server", logging.INFO) init_response = self.server.send.initialize(initialize_params) self.logger.log(f"Received initialize response: {init_response}", logging.DEBUG) @@ -336,4 +425,4 @@ class CSharpLanguageServer(SolidLanguageServer): self.initialization_complete.set() self.completions_available.set() - self.logger.log("csharp-ls server initialized and ready", logging.INFO) + self.logger.log("Microsoft.CodeAnalysis.LanguageServer initialized and ready", logging.INFO)