mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-15 02:17:26 +00:00
Merge pull request #239 from thomhurst/feature/csharp-language-server
Microsoft.CodeAnalysis.LanguageServer for C#
This commit is contained in:
20 files changed
+1108
-29
No files matched your search
@@ -218,3 +218,6 @@ tmp/
|
||||
|
||||
.serena/
|
||||
.vscode/
|
||||
|
||||
# Claude settings
|
||||
.claude/settings.local.json
|
||||
@@ -17,6 +17,7 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
* allows the language server to be initialized in the background, making the MCP server respond to requests
|
||||
immediately upon startup,
|
||||
* ensures that all tool executions are fully synchronized (executed linearly).
|
||||
* Better support for C# by switching from `omnisharp` to Microsoft's official C# language server.
|
||||
|
||||
Fixes:
|
||||
* Fix `ExecuteShellCommandTool` and `GetCurrentConfigTool` hanging on Windows
|
||||
|
||||
@@ -66,16 +66,16 @@ than existing solutions that charge a premium.
|
||||
Language servers provide support for a wide range of programming languages.
|
||||
With Serena, we provide
|
||||
* direct, out-of-the-box support for:
|
||||
* Python
|
||||
* Python
|
||||
* TypeScript/Javascript
|
||||
* PHP
|
||||
* Go (need to install go and gopls first)
|
||||
* Rust
|
||||
* C/C++
|
||||
* Java (_Note_: startup is slow, initial startup especially so. There seem to be issues with java on macos)
|
||||
* C# (requires dotnet to be installed. We switched the underlying language server recently, please report any issues you encounter)
|
||||
* Java (_Note_: startup is slow, initial startup especially so. There may be issues with java on macos and linux, we are working on it.)
|
||||
* indirect support (may require some code changes/manual installation) for:
|
||||
* Ruby (untested)
|
||||
* C# (untested)
|
||||
* Kotlin (untested)
|
||||
* Dart (untested)
|
||||
|
||||
|
||||
Executable → Regular
File mode changed.
@@ -155,6 +155,7 @@ select = [
|
||||
"YTT",
|
||||
]
|
||||
ignore = [
|
||||
"PLC0415",
|
||||
"RUF002",
|
||||
"RUF005",
|
||||
"SIM118",
|
||||
@@ -235,6 +236,7 @@ max-complexity = 20
|
||||
"scripts/**" = ["D103"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = "--snapshot-patch-pycharm-diff"
|
||||
markers = [
|
||||
"clojure: language server running for Clojure",
|
||||
"python: language server running for Python",
|
||||
@@ -243,6 +245,7 @@ markers = [
|
||||
"rust: language server running for Rust",
|
||||
"typescript: language server running for TypeScript",
|
||||
"php: language server running for PHP",
|
||||
"csharp: language server running for C#",
|
||||
"snapshot: snapshot tests for symbolic editing operations",
|
||||
"isolated_process: test runs with process isolated agent",
|
||||
]
|
||||
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
[pytest]
|
||||
addopts = --snapshot-patch-pycharm-diff
|
||||
# this shouldn't be necessary since it's already in pyproject.toml
|
||||
# but tests show lots of warnings without it
|
||||
markers =
|
||||
clojure: language server running for Clojure
|
||||
python: language server running for Python
|
||||
go: language server running for Go
|
||||
java: language server running for Java
|
||||
rust: language server running for Rust
|
||||
typescript: language server running for TypeScript
|
||||
php: language server running for PHP
|
||||
snapshot: snapshot tests for symbolic editing operations
|
||||
isolated_process: test runs with process isolated agent
|
||||
@@ -0,0 +1,772 @@
|
||||
"""
|
||||
CSharp Language Server using Microsoft.CodeAnalysis.LanguageServer (Official Roslyn-based LSP server)
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import tarfile
|
||||
import tempfile
|
||||
import threading
|
||||
import urllib.request
|
||||
import zipfile
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
from overrides import override
|
||||
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_exceptions import LanguageServerException
|
||||
from solidlsp.ls_logger import LanguageServerLogger
|
||||
from solidlsp.ls_utils import PathUtils
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
|
||||
|
||||
@dataclass
|
||||
class RuntimeDependency:
|
||||
"""Represents a runtime dependency for the C# language server."""
|
||||
|
||||
id: str
|
||||
description: str
|
||||
platform_id: str
|
||||
archive_type: str
|
||||
binary_name: str
|
||||
package_name: str | None = None
|
||||
package_version: str | None = None
|
||||
extract_path: str | None = None
|
||||
url: str | None = None
|
||||
|
||||
|
||||
# Runtime dependencies configuration
|
||||
RUNTIME_DEPENDENCIES = [
|
||||
RuntimeDependency(
|
||||
id="CSharpLanguageServer",
|
||||
description="Microsoft.CodeAnalysis.LanguageServer for Windows (x64)",
|
||||
package_name="Microsoft.CodeAnalysis.LanguageServer.win-x64",
|
||||
package_version="5.0.0-1.25277.114",
|
||||
platform_id="win-x64",
|
||||
archive_type="nupkg",
|
||||
binary_name="Microsoft.CodeAnalysis.LanguageServer.dll",
|
||||
extract_path="content/LanguageServer/win-x64",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="CSharpLanguageServer",
|
||||
description="Microsoft.CodeAnalysis.LanguageServer for Windows (ARM64)",
|
||||
package_name="Microsoft.CodeAnalysis.LanguageServer.win-arm64",
|
||||
package_version="5.0.0-1.25277.114",
|
||||
platform_id="win-arm64",
|
||||
archive_type="nupkg",
|
||||
binary_name="Microsoft.CodeAnalysis.LanguageServer.dll",
|
||||
extract_path="content/LanguageServer/win-arm64",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="CSharpLanguageServer",
|
||||
description="Microsoft.CodeAnalysis.LanguageServer for macOS (x64)",
|
||||
package_name="Microsoft.CodeAnalysis.LanguageServer.osx-x64",
|
||||
package_version="5.0.0-1.25277.114",
|
||||
platform_id="osx-x64",
|
||||
archive_type="nupkg",
|
||||
binary_name="Microsoft.CodeAnalysis.LanguageServer.dll",
|
||||
extract_path="content/LanguageServer/osx-x64",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="CSharpLanguageServer",
|
||||
description="Microsoft.CodeAnalysis.LanguageServer for macOS (ARM64)",
|
||||
package_name="Microsoft.CodeAnalysis.LanguageServer.osx-arm64",
|
||||
package_version="5.0.0-1.25277.114",
|
||||
platform_id="osx-arm64",
|
||||
archive_type="nupkg",
|
||||
binary_name="Microsoft.CodeAnalysis.LanguageServer.dll",
|
||||
extract_path="content/LanguageServer/osx-arm64",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="CSharpLanguageServer",
|
||||
description="Microsoft.CodeAnalysis.LanguageServer for Linux (x64)",
|
||||
package_name="Microsoft.CodeAnalysis.LanguageServer.linux-x64",
|
||||
package_version="5.0.0-1.25277.114",
|
||||
platform_id="linux-x64",
|
||||
archive_type="nupkg",
|
||||
binary_name="Microsoft.CodeAnalysis.LanguageServer.dll",
|
||||
extract_path="content/LanguageServer/linux-x64",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="CSharpLanguageServer",
|
||||
description="Microsoft.CodeAnalysis.LanguageServer for Linux (ARM64)",
|
||||
package_name="Microsoft.CodeAnalysis.LanguageServer.linux-arm64",
|
||||
package_version="5.0.0-1.25277.114",
|
||||
platform_id="linux-arm64",
|
||||
archive_type="nupkg",
|
||||
binary_name="Microsoft.CodeAnalysis.LanguageServer.dll",
|
||||
extract_path="content/LanguageServer/linux-arm64",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="DotNetRuntime",
|
||||
description=".NET 9 Runtime for Windows (x64)",
|
||||
url="https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-win-x64.zip",
|
||||
platform_id="win-x64",
|
||||
archive_type="zip",
|
||||
binary_name="dotnet.exe",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="DotNetRuntime",
|
||||
description=".NET 9 Runtime for Linux (x64)",
|
||||
url="https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-linux-x64.tar.gz",
|
||||
platform_id="linux-x64",
|
||||
archive_type="tar.gz",
|
||||
binary_name="dotnet",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="DotNetRuntime",
|
||||
description=".NET 9 Runtime for macOS (x64)",
|
||||
url="https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-osx-x64.tar.gz",
|
||||
platform_id="osx-x64",
|
||||
archive_type="tar.gz",
|
||||
binary_name="dotnet",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="DotNetRuntime",
|
||||
description=".NET 9 Runtime for macOS (ARM64)",
|
||||
url="https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-osx-arm64.tar.gz",
|
||||
platform_id="osx-arm64",
|
||||
archive_type="tar.gz",
|
||||
binary_name="dotnet",
|
||||
),
|
||||
]
|
||||
|
||||
# NuGet sources for package downloads
|
||||
NUGET_SOURCES = [
|
||||
"https://api.nuget.org/v3/index.json",
|
||||
"https://pkgs.dev.azure.com/azure-public/vside/_packaging/vs-impl/nuget/v3/index.json",
|
||||
"https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json",
|
||||
"https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json",
|
||||
]
|
||||
|
||||
|
||||
def breadth_first_file_scan(root_dir):
|
||||
"""
|
||||
Perform a breadth-first scan of files in the given directory.
|
||||
Yields file paths in breadth-first order.
|
||||
"""
|
||||
queue = [root_dir]
|
||||
while queue:
|
||||
current_dir = queue.pop(0)
|
||||
try:
|
||||
for item in os.listdir(current_dir):
|
||||
if item.startswith("."):
|
||||
continue
|
||||
item_path = os.path.join(current_dir, item)
|
||||
if os.path.isdir(item_path):
|
||||
queue.append(item_path)
|
||||
elif os.path.isfile(item_path):
|
||||
yield item_path
|
||||
except (PermissionError, OSError):
|
||||
# Skip directories we can't access
|
||||
pass
|
||||
|
||||
|
||||
def find_solution_or_project_file(root_dir) -> str | None:
|
||||
"""
|
||||
Find the first .sln file in breadth-first order.
|
||||
If no .sln file is found, look for a .csproj file.
|
||||
"""
|
||||
sln_file = None
|
||||
csproj_file = None
|
||||
|
||||
for filename in breadth_first_file_scan(root_dir):
|
||||
if filename.endswith(".sln") and sln_file is None:
|
||||
sln_file = filename
|
||||
elif filename.endswith(".csproj") and csproj_file is None:
|
||||
csproj_file = filename
|
||||
|
||||
# If we found a .sln file, return it immediately
|
||||
if sln_file:
|
||||
return sln_file
|
||||
|
||||
# If no .sln file was found, return the first .csproj file
|
||||
return csproj_file
|
||||
|
||||
|
||||
class CSharpLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Creates a CSharpLanguageServer instance. This class is not meant to be instantiated directly.
|
||||
Use LanguageServer.create() instead.
|
||||
"""
|
||||
dotnet_path, language_server_path, cache_dir = self.setup_runtime_dependencies(logger, config)
|
||||
|
||||
# Find solution or project file
|
||||
solution_or_project = find_solution_or_project_file(repository_root_path)
|
||||
|
||||
# Build command - Microsoft.CodeAnalysis.LanguageServer requires specific parameters
|
||||
cmd_parts = [dotnet_path, language_server_path]
|
||||
|
||||
# Required parameters
|
||||
cmd_parts.extend(["--stdio"]) # Use stdio for communication
|
||||
|
||||
# Set log level based on logger settings
|
||||
if logger.logger.level <= logging.DEBUG:
|
||||
cmd_parts.extend(["--logLevel", "Information"])
|
||||
else:
|
||||
cmd_parts.extend(["--logLevel", "Warning"])
|
||||
|
||||
# Create log directory
|
||||
log_dir = cache_dir / "logs"
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
cmd_parts.extend(["--extensionLogDirectory", str(log_dir)])
|
||||
|
||||
# The language server will discover the solution/project from the workspace root
|
||||
if solution_or_project:
|
||||
logger.log(f"Found solution/project file: {solution_or_project}", logging.INFO)
|
||||
else:
|
||||
logger.log("No .sln or .csproj file found, language server will attempt auto-discovery", logging.WARNING)
|
||||
|
||||
# Join command parts with spaces
|
||||
# ProcessLaunchInfo expects a simple string command, not shell-quoted
|
||||
cmd = cmd_parts
|
||||
|
||||
logger.log(f"Language server command: {cmd}", logging.DEBUG)
|
||||
|
||||
super().__init__(
|
||||
config,
|
||||
logger,
|
||||
repository_root_path,
|
||||
ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path),
|
||||
"csharp",
|
||||
)
|
||||
|
||||
self.initialization_complete = threading.Event()
|
||||
|
||||
@override
|
||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||
return super().is_ignored_dirname(dirname) or dirname in ["bin", "obj", "packages", ".vs"]
|
||||
|
||||
def setup_runtime_dependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> tuple[str, str, Path]:
|
||||
"""
|
||||
Set up .NET 9 runtime and Microsoft.CodeAnalysis.LanguageServer using runtime dependencies.
|
||||
Returns a tuple of (dotnet_path, language_server_dll_path, cache_dir).
|
||||
"""
|
||||
# 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":
|
||||
runtime_id = "linux-x64" if machine in ["x86_64", "amd64"] else "linux-arm64"
|
||||
else:
|
||||
raise LanguageServerException(f"Unsupported platform: {system} {machine}")
|
||||
|
||||
# Set up cache directory
|
||||
cache_dir = Path.home() / ".cache" / "serena" / "language-servers" / "csharp"
|
||||
cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Find the appropriate language server dependency
|
||||
lang_server_dep = None
|
||||
dotnet_runtime_dep = None
|
||||
|
||||
for dep in RUNTIME_DEPENDENCIES:
|
||||
if dep.id == "CSharpLanguageServer" and dep.platform_id == runtime_id:
|
||||
lang_server_dep = dep
|
||||
elif dep.id == "DotNetRuntime" and dep.platform_id == runtime_id:
|
||||
dotnet_runtime_dep = dep
|
||||
|
||||
if not lang_server_dep:
|
||||
raise LanguageServerException(f"No C# language server dependency found for platform {runtime_id}")
|
||||
if not dotnet_runtime_dep:
|
||||
raise LanguageServerException(f"No .NET runtime dependency found for platform {runtime_id}")
|
||||
|
||||
# First, ensure we have .NET 9 runtime
|
||||
dotnet_path = self._ensure_dotnet_runtime_from_config(logger, cache_dir, dotnet_runtime_dep)
|
||||
|
||||
# Then set up the language server
|
||||
package_name = lang_server_dep.package_name
|
||||
package_version = lang_server_dep.package_version
|
||||
|
||||
server_dir = cache_dir / f"{package_name}.{package_version}"
|
||||
server_dll = server_dir / lang_server_dep.binary_name
|
||||
|
||||
if server_dll.exists():
|
||||
logger.log(f"Using cached Microsoft.CodeAnalysis.LanguageServer from {server_dll}", logging.INFO)
|
||||
return dotnet_path, str(server_dll), cache_dir
|
||||
|
||||
# Download the language server 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(
|
||||
"Neither nuget nor dotnet CLI is available. Please install .NET SDK from https://dotnet.microsoft.com/download"
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
# Try to download directly from the NuGet API first
|
||||
direct_download_url = f"https://api.nuget.org/v3-flatcontainer/{package_name.lower()}/{package_version}/{package_name.lower()}.{package_version}.nupkg"
|
||||
|
||||
package_path = None
|
||||
try:
|
||||
nupkg_path = temp_path / f"{package_name}.{package_version}.nupkg"
|
||||
logger.log(f"Attempting direct download from {direct_download_url}", logging.INFO)
|
||||
|
||||
urllib.request.urlretrieve(direct_download_url, nupkg_path)
|
||||
|
||||
# Extract the nupkg (it's a zip file)
|
||||
package_path = temp_path / f"{package_name}.{package_version}"
|
||||
with zipfile.ZipFile(nupkg_path, "r") as zip_ref:
|
||||
zip_ref.extractall(package_path)
|
||||
|
||||
logger.log("Successfully downloaded and extracted package", logging.INFO)
|
||||
|
||||
except Exception as e:
|
||||
logger.log(f"Direct download failed: {e}, falling back to package manager", logging.WARNING)
|
||||
package_path = None
|
||||
|
||||
if package_path is None and dotnet_cmd:
|
||||
# Use dotnet restore to download the package
|
||||
project_content = f"""<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="{package_name}" Version="{package_version}" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<RestoreAdditionalProjectSources>
|
||||
{';'.join(NUGET_SOURCES)}
|
||||
</RestoreAdditionalProjectSources>
|
||||
</PropertyGroup>
|
||||
</Project>"""
|
||||
|
||||
project_file = temp_path / "temp.csproj"
|
||||
project_file.write_text(project_content)
|
||||
|
||||
try:
|
||||
# Use dotnet restore with no dependencies
|
||||
subprocess.run(
|
||||
[
|
||||
dotnet_cmd,
|
||||
"restore",
|
||||
str(project_file),
|
||||
"--packages",
|
||||
str(temp_path),
|
||||
"--no-dependencies",
|
||||
"--ignore-failed-sources",
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
package_path = temp_path / package_name.lower() / package_version
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.log(f"Dotnet restore stdout: {e.stdout}", logging.ERROR)
|
||||
logger.log(f"Dotnet restore stderr: {e.stderr}", logging.ERROR)
|
||||
raise LanguageServerException(f"Failed to download package: stdout={e.stdout}, stderr={e.stderr}")
|
||||
|
||||
elif package_path is None and nuget_cmd:
|
||||
# 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}")
|
||||
|
||||
if package_path is None or not package_path.exists():
|
||||
raise LanguageServerException("Failed to download Microsoft.CodeAnalysis.LanguageServer package")
|
||||
|
||||
# Extract the language server files
|
||||
extract_path = lang_server_dep.extract_path or "lib/net9.0"
|
||||
source_dir = package_path / extract_path
|
||||
|
||||
if not source_dir.exists():
|
||||
# Try alternative locations
|
||||
for possible_dir in [
|
||||
package_path / "tools" / "net9.0" / "any",
|
||||
package_path / "lib" / "net9.0",
|
||||
package_path / "contentFiles" / "any" / "net9.0",
|
||||
]:
|
||||
if possible_dir.exists():
|
||||
source_dir = possible_dir
|
||||
break
|
||||
else:
|
||||
raise LanguageServerException(f"Could not find language server files in package. Searched in {package_path}")
|
||||
|
||||
# Copy files to cache directory
|
||||
server_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
shutil.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":
|
||||
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 dotnet_path, str(server_dll), cache_dir
|
||||
|
||||
def _ensure_dotnet_runtime_from_config(self, logger: LanguageServerLogger, cache_dir: Path, runtime_dep: RuntimeDependency) -> str:
|
||||
"""
|
||||
Ensure .NET 9 runtime is available using runtime dependency configuration.
|
||||
Returns the path to the dotnet executable.
|
||||
"""
|
||||
# Check if dotnet is already available in system
|
||||
system_dotnet = shutil.which("dotnet")
|
||||
if system_dotnet:
|
||||
# Check if it's .NET 9
|
||||
try:
|
||||
result = subprocess.run([system_dotnet, "--list-runtimes"], capture_output=True, text=True, check=True)
|
||||
if "Microsoft.NETCore.App 9." in result.stdout:
|
||||
logger.log("Found system .NET 9 runtime", logging.INFO)
|
||||
return system_dotnet
|
||||
except subprocess.CalledProcessError:
|
||||
pass
|
||||
|
||||
# Download .NET 9 runtime using config
|
||||
dotnet_dir = cache_dir / "dotnet-runtime-9.0"
|
||||
dotnet_exe = dotnet_dir / runtime_dep.binary_name
|
||||
|
||||
if dotnet_exe.exists():
|
||||
# Verify it still works
|
||||
try:
|
||||
subprocess.run([str(dotnet_exe), "--info"], capture_output=True, check=True)
|
||||
logger.log(f"Using cached .NET runtime from {dotnet_exe}", logging.INFO)
|
||||
return str(dotnet_exe)
|
||||
except subprocess.CalledProcessError:
|
||||
logger.log("Cached .NET runtime is corrupted, re-downloading", logging.WARNING)
|
||||
shutil.rmtree(dotnet_dir, ignore_errors=True)
|
||||
|
||||
# Download .NET runtime
|
||||
logger.log("Downloading .NET 9 runtime...", logging.INFO)
|
||||
dotnet_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
url = runtime_dep.url
|
||||
archive_type = runtime_dep.archive_type
|
||||
|
||||
# Download the runtime
|
||||
download_path = dotnet_dir / f"dotnet-runtime.{archive_type}"
|
||||
try:
|
||||
logger.log(f"Downloading from {url}", logging.DEBUG)
|
||||
urllib.request.urlretrieve(url, download_path)
|
||||
|
||||
# Extract the archive
|
||||
if archive_type == "zip":
|
||||
with zipfile.ZipFile(download_path, "r") as zip_ref:
|
||||
zip_ref.extractall(dotnet_dir)
|
||||
else:
|
||||
# tar.gz
|
||||
with tarfile.open(download_path, "r:gz") as tar_ref:
|
||||
tar_ref.extractall(dotnet_dir)
|
||||
|
||||
# Remove the archive
|
||||
download_path.unlink()
|
||||
|
||||
# Make dotnet executable on Unix
|
||||
if platform.system().lower() != "windows":
|
||||
dotnet_exe.chmod(dotnet_exe.stat().st_mode | stat.S_IEXEC)
|
||||
|
||||
logger.log(f"Successfully installed .NET 9 runtime to {dotnet_exe}", logging.INFO)
|
||||
return str(dotnet_exe)
|
||||
|
||||
except Exception as e:
|
||||
raise LanguageServerException(f"Failed to download .NET 9 runtime from {url}: {e}") from e
|
||||
|
||||
def _get_initialize_params(self) -> InitializeParams:
|
||||
"""
|
||||
Returns the initialize params for the Microsoft.CodeAnalysis.LanguageServer.
|
||||
"""
|
||||
root_uri = PathUtils.path_to_uri(self.repository_root_path)
|
||||
root_name = os.path.basename(self.repository_root_path)
|
||||
return cast(
|
||||
InitializeParams,
|
||||
{
|
||||
"workspaceFolders": [{"uri": root_uri, "name": root_name}],
|
||||
"processId": os.getpid(),
|
||||
"rootPath": self.repository_root_path,
|
||||
"rootUri": root_uri,
|
||||
"capabilities": {
|
||||
"window": {
|
||||
"workDoneProgress": True,
|
||||
"showMessage": {"messageActionItem": {"additionalPropertiesSupport": True}},
|
||||
"showDocument": {"support": True},
|
||||
},
|
||||
"workspace": {
|
||||
"applyEdit": True,
|
||||
"workspaceEdit": {"documentChanges": True},
|
||||
"didChangeConfiguration": {"dynamicRegistration": True},
|
||||
"didChangeWatchedFiles": {"dynamicRegistration": True},
|
||||
"symbol": {
|
||||
"dynamicRegistration": True,
|
||||
"symbolKind": {
|
||||
"valueSet": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26]
|
||||
},
|
||||
},
|
||||
"executeCommand": {"dynamicRegistration": True},
|
||||
"configuration": True,
|
||||
"workspaceFolders": True,
|
||||
"workDoneProgress": 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,
|
||||
"preselectSupport": True,
|
||||
},
|
||||
"completionItemKind": {
|
||||
"valueSet": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 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},
|
||||
},
|
||||
},
|
||||
"definition": {"dynamicRegistration": True},
|
||||
"references": {"dynamicRegistration": True},
|
||||
"documentHighlight": {"dynamicRegistration": True},
|
||||
"documentSymbol": {
|
||||
"dynamicRegistration": True,
|
||||
"symbolKind": {
|
||||
"valueSet": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26]
|
||||
},
|
||||
"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},
|
||||
"publishDiagnostics": {"relatedInformation": True},
|
||||
"foldingRange": {"dynamicRegistration": True, "rangeLimit": 5000, "lineFoldingOnly": True},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
def _start_server(self):
|
||||
def do_nothing(params):
|
||||
return
|
||||
|
||||
def window_log_message(msg):
|
||||
"""Log messages from the language server."""
|
||||
message_text = msg.get("message", "")
|
||||
level = msg.get("type", 4) # Default to Log level
|
||||
|
||||
# Map LSP message types to Python logging levels
|
||||
level_map = {1: logging.ERROR, 2: logging.WARNING, 3: logging.INFO, 4: logging.DEBUG} # Error # Warning # Info # Log
|
||||
|
||||
self.logger.log(f"LSP: {message_text}", level_map.get(level, logging.DEBUG))
|
||||
|
||||
# Also print important messages to console
|
||||
if level <= 3: # Error, Warning, or Info
|
||||
print(f"C# Language Server: {message_text}")
|
||||
|
||||
def handle_progress(params):
|
||||
"""Handle progress notifications from the language server."""
|
||||
token = params.get("token", "")
|
||||
value = params.get("value", {})
|
||||
|
||||
# Log raw progress for debugging
|
||||
self.logger.log(f"Progress notification received: {params}", logging.DEBUG)
|
||||
|
||||
# Handle different progress notification types
|
||||
kind = value.get("kind")
|
||||
|
||||
if kind == "begin":
|
||||
title = value.get("title", "Operation in progress")
|
||||
message = value.get("message", "")
|
||||
percentage = value.get("percentage")
|
||||
|
||||
if percentage is not None:
|
||||
self.logger.log(f"Progress [{token}]: {title} - {message} ({percentage}%)", logging.INFO)
|
||||
print(f"C# Language Server: {title} - {message} ({percentage}%)")
|
||||
else:
|
||||
self.logger.log(f"Progress [{token}]: {title} - {message}", logging.INFO)
|
||||
print(f"C# Language Server: {title} - {message}")
|
||||
|
||||
elif kind == "report":
|
||||
message = value.get("message", "")
|
||||
percentage = value.get("percentage")
|
||||
|
||||
if percentage is not None:
|
||||
self.logger.log(f"Progress [{token}]: {message} ({percentage}%)", logging.INFO)
|
||||
if percentage % 10 == 0: # Print every 10%
|
||||
print(f"C# Language Server: {message} ({percentage}%)")
|
||||
elif message:
|
||||
self.logger.log(f"Progress [{token}]: {message}", logging.INFO)
|
||||
|
||||
elif kind == "end":
|
||||
message = value.get("message", "Operation completed")
|
||||
self.logger.log(f"Progress [{token}]: {message}", logging.INFO)
|
||||
print(f"C# Language Server: {message}")
|
||||
|
||||
def handle_workspace_configuration(params):
|
||||
"""Handle workspace/configuration requests from the server."""
|
||||
items = params.get("items", [])
|
||||
result = []
|
||||
|
||||
for item in items:
|
||||
section = item.get("section", "")
|
||||
|
||||
# Provide default values based on the configuration section
|
||||
if section.startswith(("dotnet", "csharp")):
|
||||
# Default configuration for C# settings
|
||||
if "enable" in section or "show" in section or "suppress" in section or "navigate" in section:
|
||||
# Boolean settings
|
||||
result.append(False)
|
||||
elif "scope" in section:
|
||||
# Scope settings - use appropriate enum values
|
||||
if "analyzer_diagnostics_scope" in section:
|
||||
result.append("openFiles") # BackgroundAnalysisScope
|
||||
elif "compiler_diagnostics_scope" in section:
|
||||
result.append("openFiles") # CompilerDiagnosticsScope
|
||||
else:
|
||||
result.append("openFiles")
|
||||
elif section == "dotnet_member_insertion_location":
|
||||
# ImplementTypeInsertionBehavior enum
|
||||
result.append("with_other_members_of_the_same_kind")
|
||||
elif section == "dotnet_property_generation_behavior":
|
||||
# ImplementTypePropertyGenerationBehavior enum
|
||||
result.append("prefer_throwing_properties")
|
||||
elif "location" in section or "behavior" in section:
|
||||
# Other enum settings - return null to avoid parsing errors
|
||||
result.append(None)
|
||||
else:
|
||||
# Default for other dotnet/csharp settings
|
||||
result.append(None)
|
||||
elif section == "tab_width" or section == "indent_size":
|
||||
# Tab and indent settings
|
||||
result.append(4)
|
||||
elif section == "insert_final_newline":
|
||||
# Editor settings
|
||||
result.append(True)
|
||||
else:
|
||||
# Unknown configuration - return null
|
||||
result.append(None)
|
||||
|
||||
return result
|
||||
|
||||
def handle_work_done_progress_create(params):
|
||||
"""Handle work done progress create requests."""
|
||||
# Just acknowledge the request
|
||||
return
|
||||
|
||||
def handle_register_capability(params):
|
||||
"""Handle client/registerCapability requests."""
|
||||
# Just acknowledge the request - we don't need to track these for now
|
||||
return
|
||||
|
||||
# Set up notification handlers
|
||||
self.server.on_notification("window/logMessage", window_log_message)
|
||||
self.server.on_notification("$/progress", handle_progress)
|
||||
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
|
||||
self.server.on_request("workspace/configuration", handle_workspace_configuration)
|
||||
self.server.on_request("window/workDoneProgress/create", handle_work_done_progress_create)
|
||||
self.server.on_request("client/registerCapability", handle_register_capability)
|
||||
|
||||
self.logger.log("Starting Microsoft.CodeAnalysis.LanguageServer process", logging.INFO)
|
||||
|
||||
try:
|
||||
self.server.start()
|
||||
except Exception as e:
|
||||
self.logger.log(f"Failed to start language server process: {e}", logging.ERROR)
|
||||
raise LanguageServerException(f"Failed to start C# language server: {e}")
|
||||
|
||||
# Send initialization
|
||||
initialize_params = self._get_initialize_params()
|
||||
|
||||
self.logger.log("Sending initialize request to language server", logging.INFO)
|
||||
try:
|
||||
init_response = self.server.send.initialize(initialize_params)
|
||||
self.logger.log(f"Received initialize response: {init_response}", logging.DEBUG)
|
||||
except Exception as e:
|
||||
raise LanguageServerException(f"Failed to initialize C# language server for {self.repository_root_path}: {e}") from e
|
||||
|
||||
# Verify required capabilities
|
||||
capabilities = init_response.get("capabilities", {})
|
||||
required_capabilities = [
|
||||
"textDocumentSync",
|
||||
"definitionProvider",
|
||||
"referencesProvider",
|
||||
"documentSymbolProvider",
|
||||
]
|
||||
missing = [cap for cap in required_capabilities if cap not in capabilities]
|
||||
if missing:
|
||||
raise RuntimeError(
|
||||
f"Language server is missing required capabilities: {', '.join(missing)}. "
|
||||
"Initialization failed. Please ensure the correct version of Microsoft.CodeAnalysis.LanguageServer is installed and the .NET runtime is working."
|
||||
)
|
||||
|
||||
# Complete initialization
|
||||
self.server.notify.initialized({})
|
||||
self.initialization_complete.set()
|
||||
self.completions_available.set()
|
||||
|
||||
self.logger.log(
|
||||
"Microsoft.CodeAnalysis.LanguageServer initialized and ready\n"
|
||||
"Waiting for language server to index project files...\n"
|
||||
"This may take a while for large projects",
|
||||
logging.INFO,
|
||||
)
|
||||
+43
-6
@@ -6,6 +6,7 @@ import os
|
||||
import pathlib
|
||||
import pickle
|
||||
import re
|
||||
import subprocess
|
||||
import threading
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import defaultdict
|
||||
@@ -135,9 +136,14 @@ class SolidLanguageServer(ABC):
|
||||
ls = RustAnalyzer(config, logger, repository_root_path)
|
||||
|
||||
elif config.code_language == Language.CSHARP:
|
||||
from solidlsp.language_servers.omnisharp.omnisharp import OmniSharp
|
||||
# You can also switch to OmniSharp if you prefer that
|
||||
# But OmniSharp seems to crash and has some issues with newer C# versions
|
||||
# from solidlsp.language_servers.omnisharp.omnisharp import OmniSharp
|
||||
# ls = OmniSharp(config, logger, repository_root_path)
|
||||
|
||||
ls = OmniSharp(config, logger, repository_root_path)
|
||||
from solidlsp.language_servers.csharp_language_server.csharp_language_server import CSharpLanguageServer
|
||||
|
||||
ls = CSharpLanguageServer(config, logger, repository_root_path)
|
||||
|
||||
elif config.code_language == Language.TYPESCRIPT:
|
||||
from solidlsp.language_servers.typescript_language_server.typescript_language_server import (
|
||||
@@ -343,15 +349,46 @@ class SolidLanguageServer(ABC):
|
||||
# Stage 1: Graceful Termination Request
|
||||
# Send LSP shutdown and close stdin to signal no more input.
|
||||
try:
|
||||
self.server.shutdown()
|
||||
self.logger.log("Sending LSP shutdown request...", logging.DEBUG)
|
||||
# Use a thread to timeout the LSP shutdown call since it can hang
|
||||
shutdown_thread = threading.Thread(target=self.server.shutdown)
|
||||
shutdown_thread.daemon = True
|
||||
shutdown_thread.start()
|
||||
shutdown_thread.join(timeout=2.0) # 2 second timeout for LSP shutdown
|
||||
|
||||
if shutdown_thread.is_alive():
|
||||
self.logger.log("LSP shutdown request timed out, proceeding to terminate...", logging.DEBUG)
|
||||
else:
|
||||
self.logger.log("LSP shutdown request completed.", logging.DEBUG)
|
||||
|
||||
if process.stdin and not process.stdin.is_closing():
|
||||
process.stdin.close()
|
||||
except Exception:
|
||||
pass # Ignore errors here, we are proceeding to terminate anyway.
|
||||
self.logger.log("Stage 1 shutdown complete.", logging.DEBUG)
|
||||
except Exception as e:
|
||||
self.logger.log(f"Exception during graceful shutdown: {e}", logging.DEBUG)
|
||||
# Ignore errors here, we are proceeding to terminate anyway.
|
||||
|
||||
# Stage 2: Terminate and Concurrently Drain stdout/stderr
|
||||
# Stage 2: Terminate and Wait for Process to Exit
|
||||
self.logger.log(f"Terminating process {process.pid}, current status: {process.poll()}", logging.DEBUG)
|
||||
process.terminate()
|
||||
|
||||
# Stage 3: Wait for process termination with timeout
|
||||
try:
|
||||
self.logger.log(f"Waiting for process {process.pid} to terminate...", logging.DEBUG)
|
||||
exit_code = process.wait(timeout=timeout)
|
||||
self.logger.log(f"Language server process terminated successfully with exit code {exit_code}.", logging.INFO)
|
||||
except subprocess.TimeoutExpired:
|
||||
# If termination failed, forcefully kill the process
|
||||
self.logger.log(f"Process {process.pid} termination timed out, killing process forcefully...", logging.WARNING)
|
||||
process.kill()
|
||||
try:
|
||||
exit_code = process.wait(timeout=2.0)
|
||||
self.logger.log(f"Language server process killed successfully with exit code {exit_code}.", logging.INFO)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.logger.log(f"Process {process.pid} could not be killed within timeout.", logging.ERROR)
|
||||
except Exception as e:
|
||||
self.logger.log(f"Error during process shutdown: {e}", logging.ERROR)
|
||||
|
||||
@contextmanager
|
||||
def start_server(self) -> Iterator["SolidLanguageServer"]:
|
||||
self.start()
|
||||
|
||||
@@ -2,6 +2,7 @@ import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
@@ -147,9 +148,15 @@ class SolidLanguageServerHandler:
|
||||
child_proc_env = os.environ.copy()
|
||||
child_proc_env.update(self.process_launch_info.env)
|
||||
|
||||
cmd = self.process_launch_info.cmd
|
||||
is_windows = platform.system() == "Windows"
|
||||
if not isinstance(cmd, str) and not is_windows:
|
||||
# Since we are using the shell, we need to convert the command list to a single string
|
||||
# on Linux/macOS
|
||||
cmd = " ".join(cmd)
|
||||
log.info("Starting language server process via command: %s", self.process_launch_info.cmd)
|
||||
self.process = subprocess.Popen(
|
||||
self.process_launch_info.cmd,
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stdin=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
|
||||
@@ -28,7 +28,7 @@ class LanguageServerLogger:
|
||||
"""
|
||||
|
||||
def __init__(self, json_format: bool = False, log_level: int = logging.INFO) -> None:
|
||||
self.logger = logging.getLogger("multilspy")
|
||||
self.logger = logging.getLogger("solidlsp")
|
||||
self.logger.setLevel(log_level)
|
||||
self.json_format = json_format
|
||||
|
||||
|
||||
@@ -131,7 +131,8 @@ class PathUtils:
|
||||
from urlparse import urlparse
|
||||
parsed = urlparse(uri)
|
||||
host = f"{os.path.sep}{os.path.sep}{parsed.netloc}{os.path.sep}"
|
||||
return os.path.normpath(os.path.join(host, url2pathname(unquote(parsed.path))))
|
||||
path = os.path.normpath(os.path.join(host, url2pathname(unquote(parsed.path))))
|
||||
return path
|
||||
|
||||
@staticmethod
|
||||
def path_to_uri(path: str) -> str:
|
||||
@@ -152,7 +153,8 @@ class PathUtils:
|
||||
returns `None` otherwise.
|
||||
"""
|
||||
if PurePath(path).drive == PurePath(base_path).drive:
|
||||
return str(PurePath(os.path.relpath(path, base_path)))
|
||||
rel_path = str(PurePath(os.path.relpath(path, base_path)))
|
||||
return rel_path
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ class ProcessLaunchInfo:
|
||||
"""
|
||||
|
||||
# The command to launch the process
|
||||
cmd: str
|
||||
cmd: str | list[str]
|
||||
|
||||
# The environment variables to set for the process
|
||||
env: dict[str, str] = dataclasses.field(default_factory=dict)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.5.2.0
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{0C88DD14-F956-CE84-757C-A364CCF449FC}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "resources", "resources", "{EF7103B4-4C75-1E6D-A485-A154A88D107A}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "repos", "repos", "{E2326EEF-E677-6A44-0935-7677816F09E7}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "csharp", "csharp", "{C21E6CE7-177A-86D9-040F-A317F18B6DBF}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestProject", "test\resources\repos\csharp\test_repo\TestProject.csproj", "{A4D04E18-760A-73F9-3303-0542F6298C84}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{A4D04E18-760A-73F9-3303-0542F6298C84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A4D04E18-760A-73F9-3303-0542F6298C84}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A4D04E18-760A-73F9-3303-0542F6298C84}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A4D04E18-760A-73F9-3303-0542F6298C84}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{EF7103B4-4C75-1E6D-A485-A154A88D107A} = {0C88DD14-F956-CE84-757C-A364CCF449FC}
|
||||
{E2326EEF-E677-6A44-0935-7677816F09E7} = {EF7103B4-4C75-1E6D-A485-A154A88D107A}
|
||||
{C21E6CE7-177A-86D9-040F-A317F18B6DBF} = {E2326EEF-E677-6A44-0935-7677816F09E7}
|
||||
{A4D04E18-760A-73F9-3303-0542F6298C84} = {C21E6CE7-177A-86D9-040F-A317F18B6DBF}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {BDCA748E-D888-4BAF-BF24-DAC683113BFC}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -0,0 +1,37 @@
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio temporary files
|
||||
.vs/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# Files built by Visual Studio
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# Build results
|
||||
*.dll
|
||||
*.exe
|
||||
*.pdb
|
||||
|
||||
# NuGet
|
||||
*.nupkg
|
||||
*.snupkg
|
||||
packages/
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace TestProject.Models
|
||||
{
|
||||
public class Person
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int Age { get; set; }
|
||||
public string Email { get; set; }
|
||||
|
||||
public Person(string name, int age, string email)
|
||||
{
|
||||
Name = name;
|
||||
Age = age;
|
||||
Email = email;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Name} ({Age}) - {Email}";
|
||||
}
|
||||
|
||||
public bool IsAdult()
|
||||
{
|
||||
return Age >= 18;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
|
||||
namespace TestProject
|
||||
{
|
||||
class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
Console.WriteLine("Hello, World!");
|
||||
|
||||
var calculator = new Calculator();
|
||||
int result = calculator.Add(5, 3);
|
||||
Console.WriteLine($"5 + 3 = {result}");
|
||||
}
|
||||
}
|
||||
|
||||
public class Calculator
|
||||
{
|
||||
public int Add(int a, int b)
|
||||
{
|
||||
return a + b;
|
||||
}
|
||||
|
||||
public int Subtract(int a, int b)
|
||||
{
|
||||
return a - b;
|
||||
}
|
||||
|
||||
public int Multiply(int a, int b)
|
||||
{
|
||||
return a * b;
|
||||
}
|
||||
|
||||
public double Divide(int a, int b)
|
||||
{
|
||||
if (b == 0)
|
||||
{
|
||||
throw new DivideByZeroException("Cannot divide by zero");
|
||||
}
|
||||
return (double)a / b;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -32,7 +32,16 @@ def serena_config():
|
||||
"""Create an in-memory configuration for tests with test repositories pre-registered."""
|
||||
# Create test projects for all supported languages
|
||||
test_projects = []
|
||||
for language in [Language.PYTHON, Language.GO, Language.JAVA, Language.RUST, Language.TYPESCRIPT, Language.PHP, Language.CLOJURE]:
|
||||
for language in [
|
||||
Language.PYTHON,
|
||||
Language.GO,
|
||||
Language.JAVA,
|
||||
Language.RUST,
|
||||
Language.TYPESCRIPT,
|
||||
Language.PHP,
|
||||
Language.CSHARP,
|
||||
Language.CLOJURE,
|
||||
]:
|
||||
repo_path = get_repo_path(language)
|
||||
if repo_path.exists():
|
||||
project_name = f"test_repo_{language}"
|
||||
@@ -102,6 +111,7 @@ class TestSerenaAgent:
|
||||
clj.CORE_PATH,
|
||||
marks=[pytest.mark.clojure, pytest.mark.skipif(clj.CLI_FAIL, reason=f"Clojure CLI not available: {clj.CLI_FAIL}")],
|
||||
),
|
||||
pytest.param(Language.CSHARP, "Calculator", "Class", "Program.cs", marks=pytest.mark.csharp),
|
||||
],
|
||||
indirect=["serena_agent"],
|
||||
)
|
||||
@@ -147,6 +157,7 @@ class TestSerenaAgent:
|
||||
clj.UTILS_PATH,
|
||||
marks=[pytest.mark.clojure, pytest.mark.skipif(clj.CLI_FAIL, reason=f"Clojure CLI not available: {clj.CLI_FAIL}")],
|
||||
),
|
||||
pytest.param(Language.CSHARP, "Calculator", "Program.cs", "Program.cs", marks=pytest.mark.csharp),
|
||||
],
|
||||
indirect=["serena_agent"],
|
||||
)
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp import SolidLanguageServer
|
||||
from solidlsp.ls_config import Language
|
||||
from solidlsp.ls_utils import SymbolUtils
|
||||
|
||||
|
||||
@pytest.mark.csharp
|
||||
class TestCSharpLanguageServer:
|
||||
@pytest.mark.parametrize("language_server", [Language.CSHARP], indirect=True)
|
||||
def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test finding symbols in the full symbol tree."""
|
||||
symbols = language_server.request_full_symbol_tree()
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, "Program"), "Program class not found in symbol tree"
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, "Calculator"), "Calculator class not found in symbol tree"
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, "Add"), "Add method not found in symbol tree"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.CSHARP], indirect=True)
|
||||
def test_get_document_symbols(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test getting document symbols from a C# file."""
|
||||
file_path = os.path.join("Program.cs")
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
|
||||
# Check that we have symbols
|
||||
assert len(symbols) > 0
|
||||
|
||||
# Flatten the symbols if they're nested
|
||||
if isinstance(symbols[0], list):
|
||||
symbols = symbols[0]
|
||||
|
||||
# Look for expected classes
|
||||
class_names = [s.get("name") for s in symbols if s.get("kind") == 5] # 5 is class
|
||||
assert "Program" in class_names
|
||||
assert "Calculator" in class_names
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.CSHARP], indirect=True)
|
||||
def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test finding references using symbol selection range."""
|
||||
file_path = os.path.join("Program.cs")
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
add_symbol = None
|
||||
# Handle nested symbol structure
|
||||
symbol_list = symbols[0] if symbols and isinstance(symbols[0], list) else symbols
|
||||
for sym in symbol_list:
|
||||
if sym.get("name") == "Add":
|
||||
add_symbol = sym
|
||||
break
|
||||
assert add_symbol is not None, "Could not find 'Add' method symbol in Program.cs"
|
||||
sel_start = add_symbol["selectionRange"]["start"]
|
||||
refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
|
||||
assert any(
|
||||
"Program.cs" in ref.get("relativePath", "") for ref in refs
|
||||
), "Program.cs should reference Add method (tried all positions in selectionRange)"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.CSHARP], indirect=True)
|
||||
def test_nested_namespace_symbols(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test getting symbols from nested namespace."""
|
||||
file_path = os.path.join("Models", "Person.cs")
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
|
||||
# Check that we have symbols
|
||||
assert len(symbols) > 0
|
||||
|
||||
# Flatten the symbols if they're nested
|
||||
if isinstance(symbols[0], list):
|
||||
symbols = symbols[0]
|
||||
|
||||
# Check that we have the Person class
|
||||
assert any(s.get("name") == "Person" and s.get("kind") == 5 for s in symbols)
|
||||
|
||||
# Check for properties and methods
|
||||
symbol_names = [s.get("name") for s in symbols]
|
||||
assert "Name" in symbol_names
|
||||
assert "Age" in symbol_names
|
||||
assert "Email" in symbol_names
|
||||
assert "ToString" in symbol_names
|
||||
assert "IsAdult" in symbol_names
|
||||
@@ -209,6 +209,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "execnet"
|
||||
version = "2.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bb/ff/b4c0dc78fbe20c3e59c0c7334de0c27eb4001a2b2017999af398bf730817/execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3", size = 166524 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc", size = 40612 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "executing"
|
||||
version = "2.2.0"
|
||||
@@ -832,6 +841,19 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/de/afa024cbe022b1b318a3d224125aa24939e99b4ff6f22e0ba639a2eaee47/pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e", size = 363797 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-xdist"
|
||||
version = "3.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "execnet" },
|
||||
{ name = "pytest" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/49/dc/865845cfe987b21658e871d16e0a24e871e00884c545f246dd8f6f69edda/pytest_xdist-3.7.0.tar.gz", hash = "sha256:f9248c99a7c15b7d2f90715df93610353a485827bc06eefb6566d23f6400f126", size = 87550 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/b2/0e802fde6f1c5b2f7ae7e9ad42b83fd4ecebac18a8a8c2f2f14e39dce6e1/pytest_xdist-3.7.0-py3-none-any.whl", hash = "sha256:7d3fbd255998265052435eb9daa4e99b62e6fb9cfb6efd1f858d4d8c0c7f0ca0", size = 46142 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.1.0"
|
||||
@@ -1012,6 +1034,7 @@ dev = [
|
||||
{ name = "mypy" },
|
||||
{ name = "poethepoet" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-xdist" },
|
||||
{ name = "ruff" },
|
||||
{ name = "syrupy" },
|
||||
{ name = "toml-sort" },
|
||||
@@ -1042,6 +1065,7 @@ requires-dist = [
|
||||
{ name = "pydantic", specifier = ">=2.10.6" },
|
||||
{ name = "pyright", specifier = ">=1.1.396,<2" },
|
||||
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.2" },
|
||||
{ name = "pytest-xdist", marker = "extra == 'dev'", specifier = ">=3.5.0" },
|
||||
{ name = "python-dotenv", specifier = ">=1.0.0,<2" },
|
||||
{ name = "pyyaml", specifier = ">=6.0.2" },
|
||||
{ name = "requests", specifier = ">=2.32.3,<3" },
|
||||
|
||||
Reference in new issue
Block a user