mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 10:16:00 +00:00
Fix document symbol caching for language servers that post-process symbols
Caching happened in the base implementation before subclass overrides could post-process the result, so an override's work was either never cached or, if it mutated in place, re-applied to cached output on every hit. Restructure both symbol caches so that they wrap the subclass logic: subclasses now post-process via dedicated hooks (`_request_raw_document_symbols` and `_build_document_symbols_from_raw_symbols`), and the cached value is the fully processed result. Cache versions are bumped where the cached content changes as a consequence.
This commit is contained in:
1 parent
be609b6257
commit
9f9db76622
16 files changed
+211
-224
No files matched your search
@@ -73,6 +73,10 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
storage directory via a lock; a single instance (including across restarts) still gets the
|
||||
same directory, and a second concurrent instance gets a directory of its own instead of
|
||||
contending for the first one's (#1966)
|
||||
- Fix: document symbol caching did not account for language-server-specific post-processing of
|
||||
symbols, which was applied outside the caches; the processing of language servers that post-process
|
||||
symbols (e.g. Go, Nix, Fortran, F#, Vue) was therefore repeated on every request or, if it mutated
|
||||
symbols in place, re-applied to already processed cached results
|
||||
|
||||
CLI:
|
||||
- Fix `project index-file` command not using only the relevant language server to index the given file (#1965)
|
||||
|
||||
@@ -992,25 +992,19 @@ class ALLanguageServer(SolidLanguageServer):
|
||||
|
||||
@override
|
||||
def request_document_symbols(self, relative_file_path: str, file_buffer: LSPFileBuffer | None = None) -> DocumentSymbols:
|
||||
"""
|
||||
Override to normalize AL symbol names by stripping object type and ID metadata.
|
||||
|
||||
AL Language Server returns symbol names with full object format like
|
||||
'Table 50000 "TEST Customer"', but symbol names should be pure without metadata.
|
||||
This follows the same pattern as Java LS which strips type information from names.
|
||||
|
||||
Metadata (object type, ID) is available via the hover LSP method when using
|
||||
include_info=True in find_symbol.
|
||||
"""
|
||||
# Normalize path separators for cross-platform compatibility (backslash → forward slash)
|
||||
relative_file_path = self._normalize_path(relative_file_path)
|
||||
|
||||
# Get symbols from parent implementation
|
||||
document_symbols = super().request_document_symbols(relative_file_path, file_buffer=file_buffer)
|
||||
|
||||
return document_symbols
|
||||
return super().request_document_symbols(relative_file_path, file_buffer=file_buffer)
|
||||
|
||||
def _normalize_symbol_name(self, symbol: RawDocumentSymbol, relative_file_path: str) -> str:
|
||||
# Override to normalize AL symbol names by stripping object type and ID metadata.
|
||||
# IMPORTANT: Update _document_symbols_cache_fingerprint if this normalization logic changes.
|
||||
#
|
||||
# AL Language Server returns symbol names with full object format like
|
||||
# 'Table 50000 "TEST Customer"', but symbol names should be pure without metadata.
|
||||
# This follows the same pattern as Java LS which strips type information from names.
|
||||
#
|
||||
# Metadata (object type, ID) is available via the hover LSP method when using
|
||||
# include_info=True in find_symbol.
|
||||
original_name = symbol["name"]
|
||||
normalized_name = self._extract_al_display_name(original_name)
|
||||
|
||||
|
||||
@@ -611,22 +611,22 @@ class AngularLanguageServer(SolidLanguageServer):
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
@override
|
||||
def _request_document_symbols(
|
||||
def _request_raw_document_symbols(
|
||||
self, relative_file_path: str, file_data: LSPFileBuffer | None
|
||||
) -> list[SymbolInformation] | list[DocumentSymbol] | None:
|
||||
if self._ts_server is not None and self._is_typescript_file(relative_file_path):
|
||||
with self._ts_server.open_file(relative_file_path):
|
||||
return self._ts_server._request_document_symbols(relative_file_path, file_data=None)
|
||||
return self._ts_server._request_raw_document_symbols(relative_file_path, file_data=None)
|
||||
# ngserver returns -32601 for textDocument/documentSymbol on every .html file.
|
||||
# Route to the HTML companion which gives the structural element tree
|
||||
# (works on both plain HTML like index.html and Angular templates).
|
||||
if self._is_html_template_file(relative_file_path):
|
||||
if self._html_server is not None and self._html_server_started:
|
||||
with self._html_server.open_file(relative_file_path):
|
||||
return self._html_server._request_document_symbols(relative_file_path, file_data=None)
|
||||
return self._html_server._request_raw_document_symbols(relative_file_path, file_data=None)
|
||||
log.debug("HTML companion unavailable for %s; returning None", relative_file_path)
|
||||
return None
|
||||
return super()._request_document_symbols(relative_file_path, file_data)
|
||||
return super()._request_raw_document_symbols(relative_file_path, file_data)
|
||||
|
||||
@override
|
||||
def request_definition(self, relative_file_path: str, line: int, column: int) -> list[ls_types.Location]:
|
||||
|
||||
@@ -127,7 +127,7 @@ class AnsibleLanguageServer(SolidLanguageServer):
|
||||
return False
|
||||
|
||||
@override
|
||||
def _request_document_symbols(
|
||||
def _request_raw_document_symbols(
|
||||
self, relative_file_path: str, file_data: LSPFileBuffer | None
|
||||
) -> list[SymbolInformation] | list[DocumentSymbol] | None:
|
||||
# ansible-language-server does not implement textDocument/documentSymbol and the
|
||||
|
||||
@@ -10,10 +10,8 @@ import threading
|
||||
|
||||
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection, build_npm_install_command
|
||||
from solidlsp.ls import (
|
||||
DocumentSymbols,
|
||||
LanguageServerDependencyProvider,
|
||||
LanguageServerDependencyProviderSinglePath,
|
||||
LSPFileBuffer,
|
||||
SolidLanguageServer,
|
||||
)
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
@@ -297,22 +295,3 @@ class BashLanguageServer(SolidLanguageServer):
|
||||
self.server_ready.set()
|
||||
else:
|
||||
log.info("Bash server initialization complete")
|
||||
|
||||
def request_document_symbols(self, relative_file_path: str, file_buffer: LSPFileBuffer | None = None) -> DocumentSymbols:
|
||||
# Uses the standard LSP documentSymbol request which provides reliable function detection
|
||||
# for all bash function syntaxes including:
|
||||
# - function name() { ... } (with function keyword)
|
||||
# - name() { ... } (traditional syntax)
|
||||
# - Functions with various indentation levels
|
||||
# - Functions with comments before/after/inside
|
||||
|
||||
log.debug(f"Requesting document symbols via LSP for {relative_file_path}")
|
||||
|
||||
# Use the standard LSP approach - bash-language-server handles all function syntaxes correctly
|
||||
document_symbols = super().request_document_symbols(relative_file_path, file_buffer=file_buffer)
|
||||
|
||||
# Log detection results for debugging
|
||||
functions = [s for s in document_symbols.iter_symbols() if s.get("kind") == 12]
|
||||
log.info(f"LSP function detection for {relative_file_path}: Found {len(functions)} functions")
|
||||
|
||||
return document_symbols
|
||||
@@ -1406,10 +1406,10 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
|
||||
return best_result
|
||||
|
||||
def _request_document_symbols(
|
||||
def _request_raw_document_symbols(
|
||||
self, relative_file_path: str, file_data: LSPFileBuffer | None
|
||||
) -> list[SymbolInformation] | list[DocumentSymbol] | None:
|
||||
result = super()._request_document_symbols(relative_file_path, file_data=file_data)
|
||||
result = super()._request_raw_document_symbols(relative_file_path, file_data=file_data)
|
||||
if result is None:
|
||||
return None
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ Fortran Language Server implementation using fortls.
|
||||
|
||||
import logging
|
||||
import re
|
||||
from collections.abc import Hashable
|
||||
|
||||
from overrides import override
|
||||
|
||||
@@ -139,9 +140,16 @@ class FortranLanguageServer(SolidLanguageServer):
|
||||
return symbol
|
||||
|
||||
@override
|
||||
def request_document_symbols(self, relative_file_path: str, file_buffer: LSPFileBuffer | None = None) -> DocumentSymbols:
|
||||
def _document_symbols_cache_fingerprint(self) -> Hashable | None:
|
||||
build_document_symbols_version = 2
|
||||
return build_document_symbols_version
|
||||
|
||||
@override
|
||||
def _build_document_symbols_from_raw_symbols(self, relative_file_path: str, file_buffer: LSPFileBuffer) -> DocumentSymbols:
|
||||
# Override to fix fortls's incorrect selectionRange bug.
|
||||
#
|
||||
# IMPORTANT: Update _document_symbols_cache_fingerprint() when changing this method.
|
||||
#
|
||||
# fortls returns selectionRange pointing to line start (character 0) instead of the
|
||||
# identifier name position. This breaks MCP server features that rely on exact positions.
|
||||
#
|
||||
@@ -152,11 +160,10 @@ class FortranLanguageServer(SolidLanguageServer):
|
||||
# 4. Returns corrected symbols
|
||||
|
||||
# Get symbols from fortls (with incorrect selectionRange)
|
||||
document_symbols = super().request_document_symbols(relative_file_path, file_buffer=file_buffer)
|
||||
document_symbols = super()._build_document_symbols_from_raw_symbols(relative_file_path, file_buffer=file_buffer)
|
||||
|
||||
# Get file content for parsing
|
||||
with self.open_file(relative_file_path) as file_data:
|
||||
file_content = file_data.contents
|
||||
file_content = file_buffer.contents
|
||||
|
||||
# Fix selectionRange recursively for all symbols
|
||||
def fix_symbol_and_children(symbol: ls_types.UnifiedSymbolInformation) -> ls_types.UnifiedSymbolInformation:
|
||||
|
||||
@@ -8,6 +8,7 @@ import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import threading
|
||||
from collections.abc import Hashable
|
||||
from pathlib import Path
|
||||
|
||||
from overrides import override
|
||||
@@ -110,14 +111,19 @@ class FSharpLanguageServer(SolidLanguageServer):
|
||||
return corrected_symbol
|
||||
|
||||
@override
|
||||
def request_document_symbols(self, relative_file_path: str, file_buffer: LSPFileBuffer | None = None) -> DocumentSymbols:
|
||||
def _document_symbols_cache_fingerprint(self) -> Hashable | None:
|
||||
build_document_symbols_impl_version = 2
|
||||
return build_document_symbols_impl_version
|
||||
|
||||
@override
|
||||
def _build_document_symbols_from_raw_symbols(self, relative_file_path: str, file_buffer: LSPFileBuffer) -> DocumentSymbols:
|
||||
# Override to fix FsAutoComplete's incorrect selectionRange for module declarations (#925):
|
||||
# it points at the `module` keyword instead of the module name, so hover-by-selectionRange
|
||||
# returns the keyword's docs instead of the module's own.
|
||||
document_symbols = super().request_document_symbols(relative_file_path, file_buffer=file_buffer)
|
||||
# IMPORTANT: Update _document_symbols_cache_fingerprint() when changing this method.
|
||||
document_symbols = super()._build_document_symbols_from_raw_symbols(relative_file_path, file_buffer=file_buffer)
|
||||
|
||||
with self.open_file(relative_file_path) as file_data:
|
||||
file_content = file_data.contents
|
||||
file_content = file_buffer.contents
|
||||
|
||||
def fix_symbol_and_children(symbol: ls_types.UnifiedSymbolInformation) -> ls_types.UnifiedSymbolInformation:
|
||||
fixed = self._fix_module_selection_range(symbol, file_content)
|
||||
|
||||
@@ -170,8 +170,8 @@ class Gopls(SolidLanguageServer):
|
||||
@override
|
||||
def _document_symbols_cache_fingerprint(self) -> Hashable:
|
||||
normalize_symbol_name_version = 1
|
||||
request_document_symbols_impl_version = 2
|
||||
return normalize_symbol_name_version, request_document_symbols_impl_version
|
||||
build_document_symbols_impl_version = 3
|
||||
return normalize_symbol_name_version, build_document_symbols_impl_version
|
||||
|
||||
@override
|
||||
def _raw_document_symbols_cache_fingerprint(self) -> Hashable:
|
||||
@@ -208,13 +208,14 @@ class Gopls(SolidLanguageServer):
|
||||
return symbol["name"].rsplit(".", 1)[-1]
|
||||
|
||||
@override
|
||||
def request_document_symbols(self, relative_file_path: str, file_buffer: LSPFileBuffer | None = None) -> DocumentSymbols:
|
||||
def _build_document_symbols_from_raw_symbols(self, relative_file_path: str, file_buffer: LSPFileBuffer) -> DocumentSymbols:
|
||||
# Override to extend single `type`/`var`/`const` declaration ranges to include the leading
|
||||
# keyword. gopls excludes the keyword from such ranges (unlike `func` declarations), which
|
||||
# causes replace_symbol_body to drop the keyword from the symbol body and replacement range;
|
||||
# a natural keyword-inclusive round-trip edit would then corrupt the file (e.g. `type Foo`
|
||||
# becomes `type type Foo`). See _extend_go_symbol_range_to_include_leading_keyword.
|
||||
document_symbols = super().request_document_symbols(relative_file_path, file_buffer=file_buffer)
|
||||
# IMPORTANT: Update _document_symbols_cache_fingerprint() when changing this method.
|
||||
document_symbols = super()._build_document_symbols_from_raw_symbols(relative_file_path, file_buffer=file_buffer)
|
||||
if not document_symbols.root_symbols:
|
||||
return document_symbols
|
||||
|
||||
|
||||
@@ -169,16 +169,16 @@ class Marksman(SolidLanguageServer):
|
||||
return request_document_symbols_override_version
|
||||
|
||||
@override
|
||||
def request_document_symbols(self, relative_file_path: str, file_buffer: LSPFileBuffer | None = None) -> DocumentSymbols:
|
||||
"""Override to remap Marksman's heading symbol kinds from String to Namespace.
|
||||
def _build_document_symbols_from_raw_symbols(self, relative_file_path: str, file_buffer: LSPFileBuffer) -> DocumentSymbols:
|
||||
# Override to remap Marksman's heading symbol kinds from String to Namespace.
|
||||
#
|
||||
# Marksman LSP returns all markdown headings (h1-h6) with SymbolKind.String (15).
|
||||
# This is problematic because String (15) >= Variable (13), so headings are
|
||||
# classified as "low-level" and filtered out of symbol overviews.
|
||||
# Remapping to Namespace (3) fixes this and is semantically appropriate
|
||||
# (headings are named sections containing other content).
|
||||
|
||||
Marksman LSP returns all markdown headings (h1-h6) with SymbolKind.String (15).
|
||||
This is problematic because String (15) >= Variable (13), so headings are
|
||||
classified as "low-level" and filtered out of symbol overviews.
|
||||
Remapping to Namespace (3) fixes this and is semantically appropriate
|
||||
(headings are named sections containing other content).
|
||||
"""
|
||||
document_symbols = super().request_document_symbols(relative_file_path, file_buffer=file_buffer)
|
||||
document_symbols = super()._build_document_symbols_from_raw_symbols(relative_file_path, file_buffer=file_buffer)
|
||||
|
||||
# NOTE: When changing this method, also update the cache fingerprint method above
|
||||
|
||||
|
||||
@@ -494,7 +494,7 @@ class MatlabLanguageServer(SolidLanguageServer):
|
||||
"helperFiles", # Common helper file directories
|
||||
]
|
||||
|
||||
def _request_document_symbols(
|
||||
def _request_raw_document_symbols(
|
||||
self, relative_file_path: str, file_data: LSPFileBuffer | None
|
||||
) -> list[SymbolInformation] | list[DocumentSymbol] | None:
|
||||
"""
|
||||
@@ -504,7 +504,7 @@ class MatlabLanguageServer(SolidLanguageServer):
|
||||
particularly for script sections (cell mode markers like %%). This method
|
||||
normalizes the names to strings for compatibility with the unified symbol format.
|
||||
"""
|
||||
symbols = super()._request_document_symbols(relative_file_path, file_data)
|
||||
symbols = super()._request_raw_document_symbols(relative_file_path, file_data)
|
||||
|
||||
if symbols is None or len(symbols) == 0:
|
||||
return symbols
|
||||
|
||||
@@ -10,6 +10,7 @@ import logging
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
from collections.abc import Hashable
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@@ -184,16 +185,22 @@ class NixLanguageServer(SolidLanguageServer):
|
||||
return symbol
|
||||
|
||||
@override
|
||||
def request_document_symbols(self, relative_file_path: str, file_buffer: LSPFileBuffer | None = None) -> DocumentSymbols:
|
||||
def _document_symbols_cache_fingerprint(self) -> Hashable | None:
|
||||
build_document_symbols_version = 2
|
||||
return build_document_symbols_version
|
||||
|
||||
@override
|
||||
def _build_document_symbols_from_raw_symbols(self, relative_file_path: str, file_buffer: LSPFileBuffer) -> DocumentSymbols:
|
||||
# Override to extend Nix symbol ranges to include trailing semicolons.
|
||||
# nixd provides expression-level ranges (excluding semicolons) but serena needs
|
||||
# statement-level ranges (including semicolons) for proper symbol replacement.
|
||||
# IMPORTANT: Update _document_symbols_cache_fingerprint() when changing this method.
|
||||
|
||||
# Get symbols from parent implementation
|
||||
document_symbols = super().request_document_symbols(relative_file_path, file_buffer=file_buffer)
|
||||
document_symbols = super()._build_document_symbols_from_raw_symbols(relative_file_path, file_buffer=file_buffer)
|
||||
|
||||
# Get file content for range extension
|
||||
file_content = self.language_server.retrieve_full_file_content(relative_file_path)
|
||||
file_content = file_buffer.contents
|
||||
|
||||
# Extend ranges for all symbols recursively
|
||||
def extend_symbol_and_children(symbol: ls_types.UnifiedSymbolInformation) -> ls_types.UnifiedSymbolInformation:
|
||||
|
||||
@@ -176,6 +176,7 @@ class VueLanguageServer(SolidLanguageServer):
|
||||
ProcessLaunchInfo(cmd=vue_lsp_executable_path, cwd=repository_root_path),
|
||||
"vue",
|
||||
solidlsp_settings,
|
||||
cache_version_raw_document_symbols=2,
|
||||
)
|
||||
self.server_ready = threading.Event()
|
||||
self.initialize_searcher_command_available = threading.Event()
|
||||
@@ -868,7 +869,7 @@ class VueLanguageServer(SolidLanguageServer):
|
||||
return prefer_non_node_modules_definition(definitions)
|
||||
|
||||
@override
|
||||
def _request_document_symbols(
|
||||
def _request_raw_document_symbols(
|
||||
self, relative_file_path: str, file_data: LSPFileBuffer | None
|
||||
) -> list[SymbolInformation] | list[DocumentSymbol] | None:
|
||||
"""
|
||||
@@ -883,7 +884,7 @@ class VueLanguageServer(SolidLanguageServer):
|
||||
We filter out Property symbols that have a matching Variable with the same name
|
||||
at a different location (the definition), keeping only the definition.
|
||||
"""
|
||||
symbols = super()._request_document_symbols(relative_file_path, file_data)
|
||||
symbols = super()._request_raw_document_symbols(relative_file_path, file_data)
|
||||
|
||||
if symbols is None or len(symbols) == 0:
|
||||
return symbols
|
||||
|
||||
+138
-114
@@ -1842,24 +1842,15 @@ class SolidLanguageServer(ABC):
|
||||
|
||||
return [json.loads(json_repr) for json_repr in set(json.dumps(item, sort_keys=True) for item in completions_list)]
|
||||
|
||||
def _request_document_symbols(
|
||||
def _get_raw_document_symbols(
|
||||
self, relative_file_path: str, file_data: LSPFileBuffer | None
|
||||
) -> list[SymbolInformation] | list[DocumentSymbol] | None:
|
||||
"""
|
||||
Sends a [documentSymbol](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentSymbol)
|
||||
request to the language server to find symbols in the given file - or returns a cached result if available.
|
||||
The returned symbols are considered "raw document symbols" (in contrast to processed symbols returned by `request_document_symbols`).
|
||||
Gets the raw document symbols for the given file, either from the cache or by querying the language server.
|
||||
|
||||
NOTE: This method can be overridden in subclasses to post-process the raw results.
|
||||
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
|
||||
if the post-processing can also be done on the processed/high-level symbols.
|
||||
|
||||
:param relative_file_path: the relative path of the file that has the symbols.
|
||||
:param relative_file_path: the relative path of the file for which to retrieve the raw document symbols.
|
||||
:param file_data: the file data buffer, if already opened. If None, the file will be opened in this method.
|
||||
:return: the list of root symbols in the file.
|
||||
:return: the list of root symbols in the file
|
||||
"""
|
||||
|
||||
def get_cached_raw_document_symbols(cache_key: str, fd: LSPFileBuffer) -> list[SymbolInformation] | list[DocumentSymbol] | None:
|
||||
@@ -1879,7 +1870,7 @@ class SolidLanguageServer(ABC):
|
||||
log.debug("perf: raw_document_symbols_cache STALE path=%s", relative_file_path)
|
||||
return None
|
||||
|
||||
def get_raw_document_symbols(fd: LSPFileBuffer) -> list[SymbolInformation] | list[DocumentSymbol] | None:
|
||||
with self._open_file_context(relative_file_path, file_buffer=file_data, open_in_ls=False) as fd:
|
||||
# check for cached result
|
||||
cache_key = relative_file_path
|
||||
response = get_cached_raw_document_symbols(cache_key, fd)
|
||||
@@ -1887,8 +1878,7 @@ class SolidLanguageServer(ABC):
|
||||
return response
|
||||
|
||||
# no cached result, query language server
|
||||
log.debug(f"Requesting document symbols for {relative_file_path} from the Language Server")
|
||||
response = self.server.send.document_symbol({"textDocument": {"uri": self._resolve_file_uri(relative_file_path)}})
|
||||
response = self._request_raw_document_symbols(relative_file_path, file_data=fd)
|
||||
|
||||
# Only cache non-empty results. An empty or None response can occur when the language server
|
||||
# has not yet finished indexing or building the project (e.g. Lean 4 before `lake build`),
|
||||
@@ -1899,8 +1889,30 @@ class SolidLanguageServer(ABC):
|
||||
|
||||
return response
|
||||
|
||||
with self._open_file_context(relative_file_path, file_buffer=file_data) as fd:
|
||||
return get_raw_document_symbols(fd)
|
||||
def _request_raw_document_symbols(
|
||||
self, relative_file_path: str, file_data: LSPFileBuffer | None
|
||||
) -> list[SymbolInformation] | list[DocumentSymbol] | None:
|
||||
"""
|
||||
Sends a [documentSymbol](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentSymbol)
|
||||
request to the language server to find symbols in the given file.
|
||||
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 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 `_build_document_symbols_from_raw_symbols` method
|
||||
if the post-processing can also be done on the processed/high-level symbols.
|
||||
For symbol name normalization, override `_normalize_symbol_name` instead.
|
||||
|
||||
:param relative_file_path: the relative path of the file that has the symbols.
|
||||
:param file_data: the file data buffer, if already opened. If None, the file will be opened in this method.
|
||||
:return: the list of root symbols in the file.
|
||||
"""
|
||||
with self._open_file_context(relative_file_path, file_buffer=file_data):
|
||||
log.debug(f"Requesting document symbols for {relative_file_path} from the Language Server")
|
||||
response = self.server.send.document_symbol({"textDocument": {"uri": self._resolve_file_uri(relative_file_path)}})
|
||||
return response
|
||||
|
||||
def _normalize_symbol_name(self, symbol: RawDocumentSymbol, relative_file_path: str) -> str:
|
||||
"""
|
||||
@@ -1928,11 +1940,6 @@ class SolidLanguageServer(ABC):
|
||||
"""
|
||||
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 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.
|
||||
:return: the collection of symbols in the file.
|
||||
@@ -1956,97 +1963,8 @@ class SolidLanguageServer(ABC):
|
||||
|
||||
log.debug("Cached document symbol content for %s has changed (old hash=%s)", relative_file_path, file_hash)
|
||||
|
||||
# no cached result: request the root symbols from the language server
|
||||
root_symbols = self._request_document_symbols(relative_file_path, file_data)
|
||||
|
||||
if root_symbols is None:
|
||||
log.warning(
|
||||
f"Received None response from the Language Server for document symbols in {relative_file_path}. "
|
||||
f"This means the language server can't understand this file (possibly due to syntax errors). It may also be due to a bug or misconfiguration of the LS. "
|
||||
f"Returning empty list",
|
||||
)
|
||||
return DocumentSymbols([])
|
||||
|
||||
assert isinstance(root_symbols, list), f"Unexpected response from Language Server: {root_symbols}"
|
||||
log.debug("Received %d root symbols for %s from the language server", len(root_symbols), relative_file_path)
|
||||
|
||||
body_factory = SymbolBodyFactory(file_data)
|
||||
|
||||
def convert_to_unified_symbol(original_symbol_dict: RawDocumentSymbol) -> ls_types.UnifiedSymbolInformation:
|
||||
"""
|
||||
Converts the given symbol dictionary to the unified representation, ensuring
|
||||
that all required fields are present (except 'children' which is handled separately).
|
||||
|
||||
:param original_symbol_dict: the item to augment
|
||||
:return: the augmented item (new object)
|
||||
"""
|
||||
# noinspection PyInvalidCast
|
||||
item = cast(ls_types.UnifiedSymbolInformation, dict(original_symbol_dict))
|
||||
absolute_path = os.path.join(self.repository_root_path, relative_file_path)
|
||||
|
||||
# handle missing location and path entries
|
||||
if "location" not in item:
|
||||
uri = pathlib.Path(absolute_path).as_uri()
|
||||
assert "range" in item
|
||||
tree_location = ls_types.Location(
|
||||
uri=uri,
|
||||
range=item["range"],
|
||||
absolutePath=absolute_path,
|
||||
relativePath=relative_file_path,
|
||||
)
|
||||
item["location"] = tree_location
|
||||
location = item["location"]
|
||||
if "absolutePath" not in location:
|
||||
location["absolutePath"] = absolute_path
|
||||
if "relativePath" not in location:
|
||||
location["relativePath"] = relative_file_path
|
||||
|
||||
item["body"] = self.create_symbol_body(item, factory=body_factory)
|
||||
|
||||
# handle missing selectionRange
|
||||
if "selectionRange" not in item:
|
||||
if "range" in item:
|
||||
item["selectionRange"] = item["range"]
|
||||
else:
|
||||
item["selectionRange"] = item["location"]["range"]
|
||||
|
||||
return item
|
||||
|
||||
def convert_symbols_with_common_parent(
|
||||
symbols: list[DocumentSymbol] | list[SymbolInformation],
|
||||
parent: ls_types.UnifiedSymbolInformation | None,
|
||||
) -> list[ls_types.UnifiedSymbolInformation]:
|
||||
"""
|
||||
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:
|
||||
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:
|
||||
usymbol = convert_to_unified_symbol(symbol)
|
||||
if total_name_counts[usymbol["name"]] > 1:
|
||||
usymbol["overload_idx"] = name_counts[usymbol["name"]]
|
||||
name_counts[usymbol["name"]] += 1
|
||||
usymbol["parent"] = parent
|
||||
if "children" in usymbol:
|
||||
usymbol["children"] = convert_symbols_with_common_parent(usymbol["children"], usymbol) # type: ignore
|
||||
else:
|
||||
usymbol["children"] = []
|
||||
unified_symbols.append(usymbol)
|
||||
return unified_symbols
|
||||
|
||||
unified_root_symbols = convert_symbols_with_common_parent(root_symbols, None)
|
||||
document_symbols = DocumentSymbols(unified_root_symbols)
|
||||
# no cached result: get the raw root symbols from the language server
|
||||
document_symbols = self._build_document_symbols_from_raw_symbols(relative_file_path, file_buffer=file_data)
|
||||
|
||||
# update cache
|
||||
content_hash = file_data.content_hash
|
||||
@@ -2056,6 +1974,112 @@ class SolidLanguageServer(ABC):
|
||||
|
||||
return document_symbols
|
||||
|
||||
def _build_document_symbols_from_raw_symbols(self, relative_file_path: str, file_buffer: LSPFileBuffer) -> DocumentSymbols:
|
||||
"""
|
||||
Requests the raw symbols from the language server and builds the collection of (high-level) symbols from them.
|
||||
|
||||
NOTE: This method can be overridden in subclasses to post-process the results.
|
||||
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
|
||||
:param file_buffer: file buffer of the file
|
||||
:return: the collection of symbols in the file.
|
||||
"""
|
||||
root_symbols = self._get_raw_document_symbols(relative_file_path, file_data=file_buffer)
|
||||
|
||||
if root_symbols is None:
|
||||
log.warning(
|
||||
f"Received None response from the Language Server for document symbols in {relative_file_path}. "
|
||||
f"This means the language server can't understand this file (possibly due to syntax errors). It may also be due to a bug or misconfiguration of the LS. "
|
||||
f"Returning empty list",
|
||||
)
|
||||
return DocumentSymbols([])
|
||||
|
||||
assert isinstance(root_symbols, list), f"Unexpected response from Language Server: {root_symbols}"
|
||||
log.debug("Received %d root symbols for %s from the language server", len(root_symbols), relative_file_path)
|
||||
|
||||
body_factory = SymbolBodyFactory(file_buffer)
|
||||
|
||||
def convert_to_unified_symbol(original_symbol_dict: RawDocumentSymbol) -> ls_types.UnifiedSymbolInformation:
|
||||
"""
|
||||
Converts the given symbol dictionary to the unified representation, ensuring
|
||||
that all required fields are present (except 'children' which is handled separately).
|
||||
|
||||
:param original_symbol_dict: the item to augment
|
||||
:return: the augmented item (new object)
|
||||
"""
|
||||
# noinspection PyInvalidCast
|
||||
item = cast(ls_types.UnifiedSymbolInformation, dict(original_symbol_dict))
|
||||
absolute_path = os.path.join(self.repository_root_path, relative_file_path)
|
||||
|
||||
# handle missing location and path entries
|
||||
if "location" not in item:
|
||||
uri = pathlib.Path(absolute_path).as_uri()
|
||||
assert "range" in item
|
||||
tree_location = ls_types.Location(
|
||||
uri=uri,
|
||||
range=item["range"],
|
||||
absolutePath=absolute_path,
|
||||
relativePath=relative_file_path,
|
||||
)
|
||||
item["location"] = tree_location
|
||||
location = item["location"]
|
||||
if "absolutePath" not in location:
|
||||
location["absolutePath"] = absolute_path
|
||||
if "relativePath" not in location:
|
||||
location["relativePath"] = relative_file_path
|
||||
|
||||
item["body"] = self.create_symbol_body(item, factory=body_factory)
|
||||
|
||||
# handle missing selectionRange
|
||||
if "selectionRange" not in item:
|
||||
if "range" in item:
|
||||
item["selectionRange"] = item["range"]
|
||||
else:
|
||||
item["selectionRange"] = item["location"]["range"]
|
||||
|
||||
return item
|
||||
|
||||
def convert_symbols_with_common_parent(
|
||||
symbols: list[DocumentSymbol] | list[SymbolInformation],
|
||||
parent: ls_types.UnifiedSymbolInformation | None,
|
||||
) -> list[ls_types.UnifiedSymbolInformation]:
|
||||
"""
|
||||
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:
|
||||
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:
|
||||
usymbol = convert_to_unified_symbol(symbol)
|
||||
if total_name_counts[usymbol["name"]] > 1:
|
||||
usymbol["overload_idx"] = name_counts[usymbol["name"]]
|
||||
name_counts[usymbol["name"]] += 1
|
||||
usymbol["parent"] = parent
|
||||
if "children" in usymbol:
|
||||
usymbol["children"] = convert_symbols_with_common_parent(usymbol["children"], usymbol) # type: ignore
|
||||
else:
|
||||
usymbol["children"] = []
|
||||
unified_symbols.append(usymbol)
|
||||
return unified_symbols
|
||||
|
||||
unified_root_symbols = convert_symbols_with_common_parent(root_symbols, None)
|
||||
document_symbols = DocumentSymbols(unified_root_symbols)
|
||||
|
||||
return document_symbols
|
||||
|
||||
def request_full_symbol_tree(self, within_relative_path: str | None = None) -> list[ls_types.UnifiedSymbolInformation]:
|
||||
"""
|
||||
Will go through all files in the project or within a relative path and build a tree of symbols.
|
||||
|
||||
@@ -23,7 +23,7 @@ def test_request_document_symbols_returns_none_without_contacting_server() -> No
|
||||
to None instead of sending a request that is guaranteed to fail. Runs with no
|
||||
language server process, node, or npm required.
|
||||
"""
|
||||
assert AnsibleLanguageServer._request_document_symbols(None, "playbook.yml", None) is None
|
||||
assert AnsibleLanguageServer._request_raw_document_symbols(None, "playbook.yml", None) is None
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
|
||||
@@ -2,17 +2,13 @@
|
||||
Regression test for #925: FsAutoComplete's selectionRange for a `module <Name>` declaration
|
||||
points at the `module` keyword rather than at `<Name>`, so hovering a module reports the
|
||||
generic keyword's docs instead of the module's own (confirmed by dsyme and MischaPanch in the
|
||||
issue thread). This pins FSharpLanguageServer._fix_module_selection_range and the
|
||||
request_document_symbols override directly, without spinning up FsAutoComplete: F# language
|
||||
issue thread). This pins FSharpLanguageServer._fix_module_selection_range directly, without spinning up FsAutoComplete: F# language
|
||||
server tests are unconditionally disabled (test/conftest.py, category 1, "F# language server is
|
||||
currently unreliable"), so a live-LS test can never run here or on CI.
|
||||
"""
|
||||
|
||||
from contextlib import contextmanager
|
||||
|
||||
from solidlsp import ls_types
|
||||
from solidlsp.language_servers.fsharp_language_server import FSharpLanguageServer
|
||||
from solidlsp.ls import DocumentSymbols, SolidLanguageServer
|
||||
|
||||
|
||||
def _bare_fsharp_server() -> FSharpLanguageServer:
|
||||
@@ -98,35 +94,3 @@ class TestFixModuleSelectionRange:
|
||||
fixed = server._fix_module_selection_range(symbol, file_content)
|
||||
|
||||
assert fixed is symbol
|
||||
|
||||
|
||||
class TestRequestDocumentSymbolsWiring:
|
||||
def test_fixes_every_symbol_in_the_tree_not_just_roots(self, monkeypatch) -> None:
|
||||
"""End-to-end wiring test: the override must fix nested symbols too, mirroring the
|
||||
already-merged fortran_language_server.py recursive-fix pattern this follows.
|
||||
"""
|
||||
outer = _module_symbol("Outer", line=0, start_char=0, end_char=6)
|
||||
inner = _module_symbol("Inner", line=1, start_char=4, end_char=10)
|
||||
outer["children"] = [inner]
|
||||
|
||||
file_content = "module Outer\n module Inner =\n let x = 1\n"
|
||||
|
||||
def fake_super_request_document_symbols(self, relative_file_path, file_buffer=None):
|
||||
return DocumentSymbols([outer])
|
||||
|
||||
monkeypatch.setattr(SolidLanguageServer, "request_document_symbols", fake_super_request_document_symbols)
|
||||
|
||||
server = _bare_fsharp_server()
|
||||
|
||||
@contextmanager
|
||||
def fake_open_file(relative_file_path):
|
||||
yield type("FakeFileData", (), {"contents": file_content})()
|
||||
|
||||
monkeypatch.setattr(server, "open_file", fake_open_file)
|
||||
|
||||
result = server.request_document_symbols("Test.fs")
|
||||
|
||||
root = result.root_symbols[0]
|
||||
assert root["selectionRange"]["start"] == {"line": 0, "character": 7}
|
||||
child = root["children"][0]
|
||||
assert child["selectionRange"]["start"] == {"line": 1, "character": 11}
|
||||
Reference in new issue
Block a user