Refactoring: added base method ls_resources_dir, used in C# LS

This commit is contained in:
Michael Panchenko
2025-07-06 18:28:22 +02:00
parent 45541e2ea2
commit 7e06148648
3 changed files with 65 additions and 71 deletions
@@ -8,6 +8,7 @@ import os
import platform
import shutil
import stat
import subprocess
import tarfile
import threading
import urllib.request
@@ -27,12 +28,12 @@ from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
@dataclass
@dataclass(kw_only=True)
class RuntimeDependency:
"""Represents a runtime dependency for the C# language server."""
id: str
description: str
description: str | None
platform_id: str
archive_type: str
binary_name: str
@@ -194,13 +195,13 @@ class CSharpLanguageServer(SolidLanguageServer):
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._ensure_server_installed(logger, config)
dotnet_path, language_server_path = self._ensure_server_installed(logger, config)
# Find solution or project file
solution_or_project = find_solution_or_project_file(repository_root_path)
# Create log directory
log_dir = cache_dir / "logs"
log_dir = Path(self.ls_resources_dir()) / "logs"
log_dir.mkdir(parents=True, exist_ok=True)
# Build command using dotnet directly
@@ -228,29 +229,18 @@ class CSharpLanguageServer(SolidLanguageServer):
def is_ignored_dirname(self, dirname: str) -> bool:
return super().is_ignored_dirname(dirname) or dirname in ["bin", "obj", "packages", ".vs"]
@staticmethod
def _ensure_server_installed(logger: LanguageServerLogger, config: LanguageServerConfig) -> tuple[str, str, Path]:
@classmethod
def _ensure_server_installed(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> tuple[str, str]:
"""
Ensure .NET runtime and Microsoft.CodeAnalysis.LanguageServer are available.
Returns a tuple of (dotnet_path, language_server_dll_path, cache_dir).
Returns a tuple of (dotnet_path, language_server_dll_path).
"""
# Determine platform runtime ID
runtime_id = CSharpLanguageServer._get_runtime_id()
# Set up cache directory
cache_dir = Path.home() / ".cache" / "serena" / "language-servers" / "csharp"
cache_dir.mkdir(parents=True, exist_ok=True)
# Get runtime dependencies for this platform
lang_server_dep, dotnet_runtime_dep = CSharpLanguageServer._get_runtime_dependencies(runtime_id)
dotnet_path = CSharpLanguageServer._ensure_dotnet_runtime(logger, dotnet_runtime_dep)
server_dll_path = CSharpLanguageServer._ensure_language_server(logger, lang_server_dep)
# Ensure .NET runtime is available
dotnet_path = CSharpLanguageServer._ensure_dotnet_runtime(logger, cache_dir, dotnet_runtime_dep)
# Ensure language server is available
server_dll_path = CSharpLanguageServer._ensure_language_server(logger, cache_dir, lang_server_dep)
return dotnet_path, server_dll_path, cache_dir
return dotnet_path, server_dll_path
@staticmethod
def _get_runtime_id() -> str:
@@ -287,15 +277,13 @@ class CSharpLanguageServer(SolidLanguageServer):
return lang_server_dep, dotnet_runtime_dep
@staticmethod
def _ensure_dotnet_runtime(logger: LanguageServerLogger, cache_dir: Path, runtime_dep: RuntimeDependency) -> str:
def _ensure_dotnet_runtime(logger: LanguageServerLogger, runtime_dep: RuntimeDependency) -> str:
"""Ensure .NET runtime is available and return the dotnet executable path."""
# Check if dotnet is already available in system
# Check if dotnet is already available on the system
system_dotnet = shutil.which("dotnet")
if system_dotnet:
# Check if it's .NET 9
try:
import subprocess
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)
@@ -304,15 +292,15 @@ class CSharpLanguageServer(SolidLanguageServer):
pass
# Download .NET 9 runtime using config
return CSharpLanguageServer._ensure_dotnet_runtime_from_config(logger, cache_dir, runtime_dep)
return CSharpLanguageServer._ensure_dotnet_runtime_from_config(logger, runtime_dep)
@staticmethod
def _ensure_language_server(logger: LanguageServerLogger, cache_dir: Path, lang_server_dep: RuntimeDependency) -> str:
@classmethod
def _ensure_language_server(cls, logger: LanguageServerLogger, lang_server_dep: RuntimeDependency) -> str:
"""Ensure language server is available and return the DLL path."""
package_name = lang_server_dep.package_name
package_version = lang_server_dep.package_version
server_dir = cache_dir / f"{package_name}.{package_version}"
server_dir = Path(cls.ls_resources_dir()) / f"{package_name}.{package_version}"
server_dll = server_dir / lang_server_dep.binary_name
if server_dll.exists():
@@ -321,7 +309,7 @@ class CSharpLanguageServer(SolidLanguageServer):
# Download and install the language server
logger.log(f"Downloading {package_name} version {package_version}...", logging.INFO)
package_path = CSharpLanguageServer._download_nuget_package_direct(logger, package_name, package_version, cache_dir)
package_path = CSharpLanguageServer._download_nuget_package_direct(logger, package_name, package_version)
# Extract and install
CSharpLanguageServer._extract_language_server(lang_server_dep, package_path, server_dir)
@@ -359,8 +347,8 @@ class CSharpLanguageServer(SolidLanguageServer):
server_dir.mkdir(parents=True, exist_ok=True)
shutil.copytree(source_dir, server_dir, dirs_exist_ok=True)
@staticmethod
def _download_nuget_package_direct(logger: LanguageServerLogger, package_name: str, package_version: str, cache_dir: Path) -> Path:
@classmethod
def _download_nuget_package_direct(cls, logger: LanguageServerLogger, package_name: str, package_version: str) -> Path:
"""
Download a NuGet package directly from the Azure NuGet feed.
Returns the path to the extracted package directory.
@@ -368,7 +356,7 @@ class CSharpLanguageServer(SolidLanguageServer):
azure_feed_url = "https://pkgs.dev.azure.com/azure-public/vside/_packaging/vs-impl/nuget/v3/index.json"
# Create temporary directory for package download
temp_dir = cache_dir / "temp_downloads"
temp_dir = Path(cls.ls_resources_dir()) / "temp_downloads"
temp_dir.mkdir(parents=True, exist_ok=True)
try:
@@ -416,19 +404,17 @@ class CSharpLanguageServer(SolidLanguageServer):
f"Failed to download package {package_name} version {package_version} from Azure NuGet feed: {e}"
) from e
@staticmethod
def _ensure_dotnet_runtime_from_config(logger: LanguageServerLogger, cache_dir: Path, runtime_dep: RuntimeDependency) -> str:
@classmethod
def _ensure_dotnet_runtime_from_config(cls, logger: LanguageServerLogger, 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
# Check if dotnet is already available on the system
system_dotnet = shutil.which("dotnet")
if system_dotnet:
# Check if it's .NET 9
try:
import subprocess
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)
@@ -437,7 +423,7 @@ class CSharpLanguageServer(SolidLanguageServer):
pass
# Download .NET 9 runtime using config
dotnet_dir = cache_dir / "dotnet-runtime-9.0"
dotnet_dir = Path(cls.ls_resources_dir()) / "dotnet-runtime-9.0"
dotnet_exe = dotnet_dir / runtime_dep.binary_name
if dotnet_exe.exists():
+11
View File
@@ -89,6 +89,17 @@ class SolidLanguageServer(ABC):
"""
return dirname.startswith(".")
@classmethod
def ls_resources_dir(cls, mkdir: bool = True) -> str:
"""
Returns the directory where the language server resources are downloaded.
This is used to store language server binaries, configuration files, etc.
"""
result = os.path.join(os.path.dirname(__file__), "language_servers", "static", cls.__name__)
if mkdir:
os.makedirs(result, exist_ok=True)
return result
@classmethod
def create(
cls, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, timeout: float | None = None
+29 -32
View File
@@ -200,51 +200,48 @@ class TestCSharpSolutionProjectOpening:
@patch("solidlsp.language_servers.csharp_language_server.CSharpLanguageServer._start_server")
def test_csharp_language_server_logs_solution_discovery(self, mock_start_server, mock_ensure_server_installed):
"""Test that CSharpLanguageServer logs solution/project discovery during initialization."""
with tempfile.TemporaryDirectory() as cache_dir:
# Mock the server installation
mock_ensure_server_installed.return_value = ("/usr/bin/dotnet", "/path/to/server.dll", Path(cache_dir))
mock_ensure_server_installed.return_value = ("/usr/bin/dotnet", "/path/to/server.dll")
# Create test directory with solution file
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
solution_file = temp_path / "TestSolution.sln"
solution_file.touch()
# Create test directory with solution file
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
solution_file = temp_path / "TestSolution.sln"
solution_file.touch()
# Mock logger to capture log messages
mock_logger = Mock()
mock_config = Mock(spec=LanguageServerConfig)
mock_config.ignored_paths = []
# Mock logger to capture log messages
mock_logger = Mock()
mock_config = Mock(spec=LanguageServerConfig)
mock_config.ignored_paths = []
# Create CSharpLanguageServer instance
CSharpLanguageServer(mock_config, mock_logger, str(temp_path))
# Create CSharpLanguageServer instance
CSharpLanguageServer(mock_config, mock_logger, str(temp_path))
# Verify that logger was called with solution file discovery
mock_logger.log.assert_any_call(f"Found solution/project file: {solution_file}", 20) # logging.INFO
# Verify that logger was called with solution file discovery
mock_logger.log.assert_any_call(f"Found solution/project file: {solution_file}", 20) # logging.INFO
@patch("solidlsp.language_servers.csharp_language_server.CSharpLanguageServer._ensure_server_installed")
@patch("solidlsp.language_servers.csharp_language_server.CSharpLanguageServer._start_server")
def test_csharp_language_server_logs_no_solution_warning(self, mock_start_server, mock_ensure_server_installed):
"""Test that CSharpLanguageServer logs warning when no solution/project files are found."""
with tempfile.TemporaryDirectory() as cache_dir:
# Mock the server installation
mock_ensure_server_installed.return_value = ("/usr/bin/dotnet", "/path/to/server.dll", Path(cache_dir))
# Mock the server installation
mock_ensure_server_installed.return_value = ("/usr/bin/dotnet", "/path/to/server.dll")
# Create empty test directory
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Create empty test directory
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Mock logger to capture log messages
mock_logger = Mock()
mock_config = Mock(spec=LanguageServerConfig)
mock_config.ignored_paths = []
# Mock logger to capture log messages
mock_logger = Mock()
mock_config = Mock(spec=LanguageServerConfig)
mock_config.ignored_paths = []
# Create CSharpLanguageServer instance
CSharpLanguageServer(mock_config, mock_logger, str(temp_path))
# Create CSharpLanguageServer instance
CSharpLanguageServer(mock_config, mock_logger, str(temp_path))
# Verify that logger was called with warning about no solution/project files
mock_logger.log.assert_any_call(
"No .sln or .csproj file found, language server will attempt auto-discovery", 30 # logging.WARNING
)
# Verify that logger was called with warning about no solution/project files
mock_logger.log.assert_any_call(
"No .sln or .csproj file found, language server will attempt auto-discovery", 30 # logging.WARNING
)
def test_solution_and_project_opening_with_real_test_repo(self):
"""Test solution and project opening with the actual C# test repository."""