From 97a6845c7870e7c4ef9dd375bb5f0de5bceb74df Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 29 Mar 2026 17:37:57 +0200 Subject: [PATCH] SolidLanguageServer: Support name normalisation in request_document_symbols via new method * Introduce method _normalize_symbol_name to support name transformation * Apply in CSharpLanguageServer, fixing old override of request_document_symbols not accounting for overload handling (invalidating high-level cache accordingly) * Apply in ALLanguageServer --- .../language_servers/al_language_server.py | 46 +++++-------- .../csharp_language_server.py | 67 +++++++------------ src/solidlsp/ls.py | 30 ++++++++- 3 files changed, 71 insertions(+), 72 deletions(-) diff --git a/src/solidlsp/language_servers/al_language_server.py b/src/solidlsp/language_servers/al_language_server.py index 3e3c4584..84c65d58 100644 --- a/src/solidlsp/language_servers/al_language_server.py +++ b/src/solidlsp/language_servers/al_language_server.py @@ -18,7 +18,7 @@ from overrides import override from solidlsp import ls_types from solidlsp.language_servers.common import quote_windows_path -from solidlsp.ls import DocumentSymbols, LSPFileBuffer, SolidLanguageServer +from solidlsp.ls import DocumentSymbols, GenericDocumentSymbol, LSPFileBuffer, SolidLanguageServer from solidlsp.ls_config import Language, LanguageServerConfig from solidlsp.ls_types import SymbolKind, UnifiedSymbolInformation from solidlsp.ls_utils import FileUtils @@ -1007,35 +1007,25 @@ class ALLanguageServer(SolidLanguageServer): # Get symbols from parent implementation document_symbols = super().request_document_symbols(relative_file_path, file_buffer=file_buffer) - # Normalize names by stripping AL object metadata, storing originals for hover - def normalize_name(symbol: UnifiedSymbolInformation) -> None: - original_name = symbol["name"] - normalized_name = self._extract_al_display_name(original_name) - - # Store original name if it was normalized (for hover injection) - # Only store if we have valid position data to avoid false matches at (0, 0) - if original_name != normalized_name: - sel_range = symbol.get("selectionRange") - if sel_range: - start = sel_range.get("start") - if start and "line" in start and "character" in start: - line = start["line"] - char = start["character"] - self._al_original_names[(relative_file_path, line, char)] = original_name - - symbol["name"] = normalized_name - - # Process children recursively - if symbol.get("children"): - for child in symbol["children"]: - normalize_name(child) - - # Apply to all root symbols - for sym in document_symbols.root_symbols: - normalize_name(sym) - return document_symbols + def _normalize_symbol_name(self, symbol: GenericDocumentSymbol, relative_file_path: str) -> str: + original_name = symbol["name"] + normalized_name = self._extract_al_display_name(original_name) + + # Store original name if it was normalized (for hover injection) + # Only store if we have valid position data to avoid false matches at (0, 0) + if original_name != normalized_name: + sel_range = symbol.get("selectionRange") + if sel_range: + start = sel_range.get("start") # type: ignore + if start and "line" in start and "character" in start: + line = start["line"] + char = start["character"] + self._al_original_names[(relative_file_path, line, char)] = original_name + + return normalized_name + @override def request_hover( self, relative_file_path: str, line: int, column: int, file_buffer: LSPFileBuffer | None = None diff --git a/src/solidlsp/language_servers/csharp_language_server.py b/src/solidlsp/language_servers/csharp_language_server.py index f5b14e18..fbd9d672 100644 --- a/src/solidlsp/language_servers/csharp_language_server.py +++ b/src/solidlsp/language_servers/csharp_language_server.py @@ -7,7 +7,7 @@ import os import platform import shutil import threading -from collections.abc import Iterable +from collections.abc import Hashable, Iterable from dataclasses import replace from pathlib import Path from typing import Any, cast @@ -15,10 +15,15 @@ from typing import Any, cast from overrides import override from serena.util.dotnet import DotNETUtil -from solidlsp.ls import DocumentSymbols, LanguageServerDependencyProvider, LSPFileBuffer, SolidLanguageServer +from solidlsp.ls import ( + GenericDocumentSymbol, + LanguageServerDependencyProvider, + LSPFileBuffer, + SolidLanguageServer, +) from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_exceptions import SolidLSPException -from solidlsp.ls_types import Hover, UnifiedSymbolInformation +from solidlsp.ls_types import Hover from solidlsp.ls_utils import FileUtils, PathUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams, InitializeResult from solidlsp.settings import SolidLSPSettings @@ -218,28 +223,6 @@ class CSharpLanguageServer(SolidLanguageServer): def is_ignored_dirname(self, dirname: str) -> bool: return super().is_ignored_dirname(dirname) or dirname in ["bin", "obj", "packages", ".vs"] - @override - def request_document_symbols(self, relative_file_path: str, file_buffer: Any = None) -> DocumentSymbols: - """ - Override to normalize Roslyn symbol names and cache originals. - - Roslyn 5.5.0+ returns symbol names with type annotations: - - Properties: "Name : string" - - Methods: "Add(int, int) : int" - - This method: - 1. Normalizes names to base form ("Name", "Add") - 2. Caches original names for rich information display - 3. Populates LSP spec's 'detail' field with type/signature info - """ - symbols = super().request_document_symbols(relative_file_path, file_buffer) - - # Normalize all symbols recursively - for symbol in symbols.iter_symbols(): - self._normalize_symbol_name(symbol, relative_file_path) - - return symbols - @override def request_hover(self, relative_file_path: str, line: int, column: int, file_buffer: LSPFileBuffer | None = None) -> Hover | None: """ @@ -265,12 +248,20 @@ class CSharpLanguageServer(SolidLanguageServer): return hover - def _normalize_symbol_name(self, symbol: UnifiedSymbolInformation, relative_file_path: str) -> None: - """ - Normalize a single symbol's name and cache the original. - Processes children recursively. - """ - original_name = symbol.get("name", "") + def _document_symbols_cache_fingerprint(self) -> Hashable | None: + normalize_symbol_name_version = 1 + return normalize_symbol_name_version + + def _normalize_symbol_name(self, symbol: GenericDocumentSymbol, relative_file_path: str) -> str: + # Roslyn 5.5.0+ returns symbol names with type annotations: + # - Properties: "Name : string" + # - Methods: "Add(int, int) : int" + # + # This method: + # 1. Normalizes names to base form ("Name", "Add") + # 2. Caches original names for rich information display + # 3. Populates LSP spec's 'detail' field with type/signature info + original_name = symbol["name"] # Extract base name and type/signature info normalized_name, type_info = self._extract_base_name_and_type(original_name) @@ -279,24 +270,18 @@ class CSharpLanguageServer(SolidLanguageServer): if original_name != normalized_name: sel_range = symbol.get("selectionRange") if sel_range: - start = sel_range.get("start") + start = sel_range.get("start") # type: ignore if start and "line" in start and "character" in start: line = start["line"] char = start["character"] cache_key = (relative_file_path, line, char) self._original_symbol_names[cache_key] = original_name - # Populate LSP spec's 'detail' field with type/signature information + # Populate 'detail' field with type/signature information (for UnifiedSymbolInformation) if type_info and "detail" not in symbol: - symbol["detail"] = type_info + symbol["detail"] = type_info # type: ignore - # Update the symbol name - symbol["name"] = normalized_name - - # Process children recursively - children = symbol.get("children", []) - for child in children: - self._normalize_symbol_name(child, relative_file_path) + return normalized_name @staticmethod def _extract_base_name_and_type(roslyn_name: str) -> tuple[str, str]: diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 6091c6ae..688889e0 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -1262,7 +1262,7 @@ class SolidLanguageServer(ABC): The returned symbols are considered "raw document symbols" (in contrast to processed symbols returned by `request_document_symbols`). NOTE: This method can be overridden in subclasses to post-process the raw results. - When doing so, be sure to update the init parameter `cache_version_raw_document_symbols` + When doing so after the initial implementation, be sure to update the init parameter `cache_version_raw_document_symbols` to a different version (add 1) to ensure that all caches are invalidated appropriately. IMPORTANT: Since rebuilding the raw document symbol cache from the language server results is potentially expensive, prefer overriding the `request_document_symbols` method @@ -1312,13 +1312,30 @@ class SolidLanguageServer(ABC): with self._open_file_context(relative_file_path, file_buffer=file_data) as fd: return get_raw_document_symbols(fd) + def _normalize_symbol_name(self, symbol: GenericDocumentSymbol, relative_file_path: str) -> str: + """ + Normalizes the name of the given symbol, e.g. by removing parameter lists from method symbols. + + Override this method in subclasses to implement language-specific normalization logic. + NOTE: When changing the override of this method after the initial LS implementation, + be sure to also override `_document_symbols_cache_fingerprint` in order to ensure that + the caches are invalidated appropriately. + + :param symbol: the symbol + :param relative_file_path: the relative path of the file the symbol is located in + :return: the normalized name of the symbol + """ + # the default implementation does not change the name + return symbol["name"] + def request_document_symbols(self, relative_file_path: str, file_buffer: LSPFileBuffer | None = None) -> DocumentSymbols: """ Retrieves the collection of symbols in the given file. NOTE: This method can be overridden in subclasses to post-process the results. - When doing so, be sure to also override `_document_symbols_cache_fingerprint` + When doing so after the initial LS implementation, be sure to also override `_document_symbols_cache_fingerprint` to ensure that the caches are invalidated appropriately. + DO NOT override this method to modify symbol names; override `_normalize_symbol_name` instead. :param relative_file_path: The relative path of the file that has the symbols :param file_buffer: an optional file buffer if the file is already opened. @@ -1410,9 +1427,16 @@ class SolidLanguageServer(ABC): Converts the given symbols into UnifiedSymbolInformation with proper parent-child relationships, adding overload indices for symbols with the same name under the same parent. """ + # apply name normalization and count occurrences of each symbol name total_name_counts: dict[str, int] = defaultdict(lambda: 0) for symbol in symbols: - total_name_counts[symbol["name"]] += 1 + name = self._normalize_symbol_name(symbol, relative_file_path=relative_file_path) + symbol["name"] = name + total_name_counts[name] += 1 + + # convert symbols to the unified representation and + # * add overload indices where necessary + # * ensure that the "parent" field is set correctly name_counts: dict[str, int] = defaultdict(lambda: 0) unified_symbols = [] for symbol in symbols: