mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-11 12:29:04 +00:00
767 lines
33 KiB
Python
767 lines
33 KiB
Python
"""
|
|
CSharp Language Server using Microsoft.CodeAnalysis.LanguageServer (Official Roslyn-based LSP server)
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import platform
|
|
import shutil
|
|
import subprocess
|
|
import tarfile
|
|
import threading
|
|
import urllib.request
|
|
import zipfile
|
|
from collections.abc import Iterable
|
|
from pathlib import Path
|
|
from typing import Any, cast
|
|
|
|
from overrides import override
|
|
|
|
from solidlsp.ls import LanguageServerDependencyProvider, SolidLanguageServer
|
|
from solidlsp.ls_config import LanguageServerConfig
|
|
from solidlsp.ls_exceptions import SolidLSPException
|
|
from solidlsp.ls_utils import PathUtils
|
|
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams, InitializeResult
|
|
from solidlsp.settings import SolidLSPSettings
|
|
from solidlsp.util.zip import SafeZipExtractor
|
|
|
|
from .common import RuntimeDependency, RuntimeDependencyCollection
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
_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.25329.6",
|
|
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.25329.6",
|
|
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.25329.6",
|
|
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.25329.6",
|
|
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.25329.6",
|
|
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.25329.6",
|
|
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 Linux (ARM64)",
|
|
url="https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-linux-arm64.tar.gz",
|
|
platform_id="linux-arm64",
|
|
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",
|
|
),
|
|
RuntimeDependency(
|
|
id="DotNetRuntime",
|
|
description=".NET 9 Runtime for Windows (ARM64)",
|
|
url="https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-win-arm64.zip",
|
|
platform_id="win-arm64",
|
|
archive_type="zip",
|
|
binary_name="dotnet.exe",
|
|
),
|
|
]
|
|
|
|
|
|
def breadth_first_file_scan(root_dir: str) -> Iterable[str]:
|
|
"""
|
|
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) -> 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`,
|
|
the official Roslyn-based language server from Microsoft.
|
|
|
|
You can pass a list of runtime dependency overrides in ls_specific_settings["csharp"]["runtime_dependencies"]. This is a list of
|
|
dicts, each containing at least the "id" key, and optionally "platform_id" to uniquely identify the dependency to override.
|
|
For example, to override the URL of the .NET runtime on windows-x64, add the entry:
|
|
|
|
```
|
|
{
|
|
"id": "DotNetRuntime",
|
|
"platform_id": "win-x64",
|
|
"url": "https://example.com/custom-dotnet-runtime.zip"
|
|
}
|
|
```
|
|
|
|
See the `_RUNTIME_DEPENDENCIES` variable above for the available dependency ids and platform_ids.
|
|
"""
|
|
|
|
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
|
|
"""
|
|
Creates a CSharpLanguageServer instance. This class is not meant to be instantiated directly.
|
|
Use LanguageServer.create() instead.
|
|
"""
|
|
super().__init__(config, repository_root_path, None, "csharp", solidlsp_settings)
|
|
|
|
self.initialization_complete = threading.Event()
|
|
|
|
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
|
|
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir, self._solidlsp_settings, self.repository_root_path)
|
|
|
|
@override
|
|
def is_ignored_dirname(self, dirname: str) -> bool:
|
|
return super().is_ignored_dirname(dirname) or dirname in ["bin", "obj", "packages", ".vs"]
|
|
|
|
class DependencyProvider(LanguageServerDependencyProvider):
|
|
def __init__(
|
|
self,
|
|
custom_settings: SolidLSPSettings.CustomLSSettings,
|
|
ls_resources_dir: str,
|
|
solidlsp_settings: SolidLSPSettings,
|
|
repository_root_path: str,
|
|
):
|
|
super().__init__(custom_settings, ls_resources_dir)
|
|
self._solidlsp_settings = solidlsp_settings
|
|
self._repository_root_path = repository_root_path
|
|
self._dotnet_path, self._language_server_path = self._ensure_server_installed()
|
|
|
|
def create_launch_command(self) -> list[str] | str:
|
|
# Find solution or project file
|
|
solution_or_project = find_solution_or_project_file(self._repository_root_path)
|
|
|
|
# Create log directory
|
|
log_dir = Path(self._ls_resources_dir) / "logs"
|
|
log_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Build command using dotnet directly
|
|
cmd = [self._dotnet_path, self._language_server_path, "--logLevel=Information", f"--extensionLogDirectory={log_dir}", "--stdio"]
|
|
|
|
# The language server will discover the solution/project from the workspace root
|
|
if solution_or_project:
|
|
log.info(f"Found solution/project file: {solution_or_project}")
|
|
else:
|
|
log.warning("No .sln or .csproj file found, language server will attempt auto-discovery")
|
|
|
|
log.debug(f"Language server command: {' '.join(cmd)}")
|
|
|
|
return cmd
|
|
|
|
def _ensure_server_installed(self) -> tuple[str, str]:
|
|
"""
|
|
Ensure .NET runtime and Microsoft.CodeAnalysis.LanguageServer are available.
|
|
Returns a tuple of (dotnet_path, language_server_dll_path).
|
|
"""
|
|
runtime_dependency_overrides = cast(list[dict[str, Any]], self._custom_settings.get("runtime_dependencies", []))
|
|
|
|
log.debug("Resolving runtime dependencies")
|
|
|
|
runtime_dependencies = RuntimeDependencyCollection(
|
|
_RUNTIME_DEPENDENCIES,
|
|
overrides=runtime_dependency_overrides,
|
|
)
|
|
|
|
log.debug(
|
|
f"Available runtime dependencies: {runtime_dependencies.get_dependencies_for_current_platform}",
|
|
)
|
|
|
|
# Find the dependencies for our platform
|
|
lang_server_dep = runtime_dependencies.get_single_dep_for_current_platform("CSharpLanguageServer")
|
|
dotnet_runtime_dep = runtime_dependencies.get_single_dep_for_current_platform("DotNetRuntime")
|
|
dotnet_path = self._ensure_dotnet_runtime(dotnet_runtime_dep)
|
|
server_dll_path = self._ensure_language_server(lang_server_dep)
|
|
|
|
return dotnet_path, server_dll_path
|
|
|
|
def _ensure_dotnet_runtime(self, dotnet_runtime_dep: RuntimeDependency) -> str:
|
|
"""Ensure .NET runtime is available and return the dotnet executable path."""
|
|
# TODO: use RuntimeDependency util methods instead of custom validation/download logic
|
|
|
|
# Check if dotnet is already available on the 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:
|
|
log.info("Found system .NET 9 runtime")
|
|
return system_dotnet
|
|
except subprocess.CalledProcessError:
|
|
pass
|
|
|
|
# Download .NET 9 runtime using config
|
|
return self._ensure_dotnet_runtime_from_config(dotnet_runtime_dep)
|
|
|
|
def _ensure_language_server(self, 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 = Path(self._ls_resources_dir) / f"{package_name}.{package_version}"
|
|
assert lang_server_dep.binary_name is not None
|
|
server_dll = server_dir / lang_server_dep.binary_name
|
|
|
|
if server_dll.exists():
|
|
log.info(f"Using cached Microsoft.CodeAnalysis.LanguageServer from {server_dll}")
|
|
return str(server_dll)
|
|
|
|
# Download and install the language server
|
|
log.info(f"Downloading {package_name} version {package_version}...")
|
|
assert package_version is not None
|
|
assert package_name is not None
|
|
package_path = self._download_nuget_package_direct(package_name, package_version)
|
|
|
|
# Extract and install
|
|
self._extract_language_server(lang_server_dep, package_path, server_dir)
|
|
|
|
if not server_dll.exists():
|
|
raise SolidLSPException("Microsoft.CodeAnalysis.LanguageServer.dll not found after extraction")
|
|
|
|
# Make executable on Unix systems
|
|
if platform.system().lower() != "windows":
|
|
server_dll.chmod(0o755)
|
|
|
|
log.info(f"Successfully installed Microsoft.CodeAnalysis.LanguageServer to {server_dll}")
|
|
return str(server_dll)
|
|
|
|
@staticmethod
|
|
def _extract_language_server(lang_server_dep: RuntimeDependency, package_path: Path, server_dir: Path) -> None:
|
|
"""Extract language server files from downloaded package."""
|
|
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 SolidLSPException(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)
|
|
|
|
def _download_nuget_package_direct(self, 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.
|
|
"""
|
|
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 = Path(self._ls_resources_dir) / "temp_downloads"
|
|
temp_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
try:
|
|
# First, get the service index from the Azure feed
|
|
log.debug("Fetching NuGet service index from Azure feed...")
|
|
with urllib.request.urlopen(azure_feed_url) as response:
|
|
service_index = json.loads(response.read().decode())
|
|
|
|
# Find the package base address (for downloading packages)
|
|
package_base_address = None
|
|
for resource in service_index.get("resources", []):
|
|
if resource.get("@type") == "PackageBaseAddress/3.0.0":
|
|
package_base_address = resource.get("@id")
|
|
break
|
|
|
|
if not package_base_address:
|
|
raise SolidLSPException("Could not find package base address in Azure NuGet feed")
|
|
|
|
# Construct the download URL for the specific package
|
|
package_id_lower = package_name.lower()
|
|
package_version_lower = package_version.lower()
|
|
package_url = f"{package_base_address.rstrip('/')}/{package_id_lower}/{package_version_lower}/{package_id_lower}.{package_version_lower}.nupkg"
|
|
|
|
log.debug(f"Downloading package from: {package_url}")
|
|
|
|
# Download the .nupkg file
|
|
nupkg_file = temp_dir / f"{package_name}.{package_version}.nupkg"
|
|
urllib.request.urlretrieve(package_url, nupkg_file)
|
|
|
|
# Extract the .nupkg file (it's just a zip file)
|
|
package_extract_dir = temp_dir / f"{package_name}.{package_version}"
|
|
package_extract_dir.mkdir(exist_ok=True)
|
|
|
|
# Use SafeZipExtractor to handle long paths and skip errors
|
|
extractor = SafeZipExtractor(archive_path=nupkg_file, extract_dir=package_extract_dir, verbose=False)
|
|
extractor.extract_all()
|
|
|
|
# Clean up the nupkg file
|
|
nupkg_file.unlink()
|
|
|
|
log.info(f"Successfully downloaded and extracted {package_name} version {package_version}")
|
|
return package_extract_dir
|
|
|
|
except Exception as e:
|
|
raise SolidLSPException(
|
|
f"Failed to download package {package_name} version {package_version} from Azure NuGet feed: {e}"
|
|
) from e
|
|
|
|
def _ensure_dotnet_runtime_from_config(self, dotnet_runtime_dep: RuntimeDependency) -> str:
|
|
"""
|
|
Ensure .NET 9 runtime is available using runtime dependency configuration.
|
|
Returns the path to the dotnet executable.
|
|
"""
|
|
# TODO: use RuntimeDependency util methods instead of custom download logic
|
|
|
|
# Check if dotnet is already available on the 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:
|
|
log.info("Found system .NET 9 runtime")
|
|
return system_dotnet
|
|
except subprocess.CalledProcessError:
|
|
pass
|
|
|
|
# Download .NET 9 runtime using config
|
|
dotnet_dir = Path(self._ls_resources_dir) / "dotnet-runtime-9.0"
|
|
assert dotnet_runtime_dep.binary_name is not None, "Runtime dependency must have a binary_name"
|
|
dotnet_exe = dotnet_dir / dotnet_runtime_dep.binary_name
|
|
|
|
if dotnet_exe.exists():
|
|
log.info(f"Using cached .NET runtime from {dotnet_exe}")
|
|
return str(dotnet_exe)
|
|
|
|
# Download .NET runtime
|
|
log.info("Downloading .NET 9 runtime...")
|
|
dotnet_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
custom_dotnet_runtime_url = self._custom_settings.get("dotnet_runtime_url")
|
|
if custom_dotnet_runtime_url is not None:
|
|
log.info(f"Using custom .NET runtime url: {custom_dotnet_runtime_url}")
|
|
url = custom_dotnet_runtime_url
|
|
else:
|
|
url = dotnet_runtime_dep.url
|
|
|
|
archive_type = dotnet_runtime_dep.archive_type
|
|
|
|
# Download the runtime
|
|
download_path = dotnet_dir / f"dotnet-runtime.{archive_type}"
|
|
try:
|
|
log.debug(f"Downloading from {url}")
|
|
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(0o755)
|
|
|
|
log.info(f"Successfully installed .NET 9 runtime to {dotnet_exe}")
|
|
return str(dotnet_exe)
|
|
|
|
except Exception as e:
|
|
raise SolidLSPException(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": list(range(1, 27))},
|
|
},
|
|
"executeCommand": {"dynamicRegistration": True},
|
|
"configuration": True,
|
|
"workspaceFolders": True,
|
|
"workDoneProgress": True,
|
|
},
|
|
"textDocument": {
|
|
"synchronization": {"dynamicRegistration": True, "willSave": True, "willSaveWaitUntil": True, "didSave": True},
|
|
"hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
|
|
"signatureHelp": {
|
|
"dynamicRegistration": True,
|
|
"signatureInformation": {
|
|
"documentationFormat": ["markdown", "plaintext"],
|
|
"parameterInformation": {"labelOffsetSupport": True},
|
|
},
|
|
},
|
|
"definition": {"dynamicRegistration": True},
|
|
"references": {"dynamicRegistration": True},
|
|
"documentSymbol": {
|
|
"dynamicRegistration": True,
|
|
"symbolKind": {"valueSet": list(range(1, 27))},
|
|
"hierarchicalDocumentSymbolSupport": True,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
)
|
|
|
|
def _start_server(self) -> None:
|
|
def do_nothing(params: dict) -> None:
|
|
return
|
|
|
|
def window_log_message(msg: dict) -> None:
|
|
"""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
|
|
|
|
log.log(level_map.get(level, logging.DEBUG), f"LSP: {message_text}")
|
|
|
|
def handle_progress(params: dict) -> None:
|
|
"""Handle progress notifications from the language server."""
|
|
token = params.get("token", "")
|
|
value = params.get("value", {})
|
|
|
|
# Log raw progress for debugging
|
|
log.debug(f"Progress notification received: {params}")
|
|
|
|
# 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:
|
|
log.debug(f"Progress [{token}]: {title} - {message} ({percentage}%)")
|
|
else:
|
|
log.info(f"Progress [{token}]: {title} - {message}")
|
|
|
|
elif kind == "report":
|
|
message = value.get("message", "")
|
|
percentage = value.get("percentage")
|
|
|
|
if percentage is not None:
|
|
log.info(f"Progress [{token}]: {message} ({percentage}%)")
|
|
elif message:
|
|
log.info(f"Progress [{token}]: {message}")
|
|
|
|
elif kind == "end":
|
|
message = value.get("message", "Operation completed")
|
|
log.info(f"Progress [{token}]: {message}")
|
|
|
|
def handle_workspace_configuration(params: dict) -> list:
|
|
"""Handle workspace/configuration requests from the server."""
|
|
items = params.get("items", [])
|
|
result: list[Any] = []
|
|
|
|
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: dict) -> None:
|
|
"""Handle work done progress create requests."""
|
|
# Just acknowledge the request
|
|
return
|
|
|
|
def handle_register_capability(params: dict) -> None:
|
|
"""Handle client/registerCapability requests."""
|
|
# Just acknowledge the request - we don't need to track these for now
|
|
return
|
|
|
|
def handle_project_needs_restore(params: dict) -> None:
|
|
return
|
|
|
|
def handle_workspace_indexing_complete(params: dict) -> None:
|
|
self.completions_available.set()
|
|
|
|
# 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_notification("workspace/projectInitializationComplete", handle_workspace_indexing_complete)
|
|
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.server.on_request("workspace/_roslyn_projectNeedsRestore", handle_project_needs_restore)
|
|
|
|
log.info("Starting Microsoft.CodeAnalysis.LanguageServer process")
|
|
|
|
try:
|
|
self.server.start()
|
|
except Exception as e:
|
|
log.info(f"Failed to start language server process: {e}", logging.ERROR)
|
|
raise SolidLSPException(f"Failed to start C# language server: {e}")
|
|
|
|
# Send initialization
|
|
initialize_params = self._get_initialize_params()
|
|
|
|
log.info("Sending initialize request to language server")
|
|
try:
|
|
init_response = self.server.send.initialize(initialize_params)
|
|
log.info(f"Received initialize response: {init_response}")
|
|
except Exception as e:
|
|
raise SolidLSPException(f"Failed to initialize C# language server for {self.repository_root_path}: {e}") from e
|
|
|
|
# Apply diagnostic capabilities
|
|
self._force_pull_diagnostics(init_response)
|
|
|
|
# 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({})
|
|
|
|
# Open solution and project files
|
|
self._open_solution_and_projects()
|
|
|
|
self.initialization_complete.set()
|
|
|
|
log.info(
|
|
"Microsoft.CodeAnalysis.LanguageServer initialized and ready\n"
|
|
"Waiting for language server to index project files...\n"
|
|
"This may take a while for large projects"
|
|
)
|
|
|
|
if self.completions_available.wait(30): # Wait up to 30 seconds for indexing
|
|
log.info("Indexing complete")
|
|
else:
|
|
log.warning("Timeout waiting for indexing to complete, proceeding anyway")
|
|
self.completions_available.set()
|
|
|
|
def _force_pull_diagnostics(self, init_response: dict | InitializeResult) -> None:
|
|
"""
|
|
Apply the diagnostic capabilities hack.
|
|
Forces the server to support pull diagnostics.
|
|
"""
|
|
capabilities = init_response.get("capabilities", {})
|
|
diagnostic_provider: Any = capabilities.get("diagnosticProvider", {})
|
|
|
|
# Add the diagnostic capabilities hack
|
|
if isinstance(diagnostic_provider, dict):
|
|
diagnostic_provider.update(
|
|
{
|
|
"interFileDependencies": True,
|
|
"workDoneProgress": True,
|
|
"workspaceDiagnostics": True,
|
|
}
|
|
)
|
|
log.debug("Applied diagnostic capabilities hack for better C# diagnostics")
|
|
|
|
def _open_solution_and_projects(self) -> None:
|
|
"""
|
|
Open solution and project files using notifications.
|
|
"""
|
|
# Find solution file
|
|
solution_file = None
|
|
for filename in breadth_first_file_scan(self.repository_root_path):
|
|
if filename.endswith(".sln"):
|
|
solution_file = filename
|
|
break
|
|
|
|
# Send solution/open notification if solution file found
|
|
if solution_file:
|
|
solution_uri = PathUtils.path_to_uri(solution_file)
|
|
self.server.notify.send_notification("solution/open", {"solution": solution_uri})
|
|
log.debug(f"Opened solution file: {solution_file}")
|
|
|
|
# Find and open project files
|
|
project_files = []
|
|
for filename in breadth_first_file_scan(self.repository_root_path):
|
|
if filename.endswith(".csproj"):
|
|
project_files.append(filename)
|
|
|
|
# Send project/open notifications for each project file
|
|
if project_files:
|
|
project_uris = [PathUtils.path_to_uri(project_file) for project_file in project_files]
|
|
self.server.notify.send_notification("project/open", {"projects": project_uris})
|
|
log.debug(f"Opened project files: {project_files}")
|
|
|
|
@override
|
|
def _get_wait_time_for_cross_file_referencing(self) -> float:
|
|
return 2
|