Replace csharp-ls with official Microsoft.CodeAnalysis.LanguageServer

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 <noreply@anthropic.com>
This commit is contained in:
Claude Assistant
2025-06-25 22:16:39 +01:00
co-authored by Claude
parent eb8b118d62
commit ac7547308e
@@ -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"""<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="{package_name}" Version="{package_version}" />
</ItemGroup>
</Project>"""
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)