mirror of
https://github.com/tiennm99/serena.git
synced 2026-09-02 16:20:41 +00:00
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:
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)
|
||||
|
||||
Reference in New Issue
Block a user