mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 08:15:59 +00:00
Merge pull request #736 from oraios/name-path-overloads
Add support for overloads, extending name paths with overload indices
This commit is contained in:
56 files changed
+841
-540
No files matched your search
+2
-1
@@ -70,7 +70,8 @@ dev = [
|
||||
"sphinx_rtd_theme>=0.5.1",
|
||||
"sphinx-toolbox==3.7.0",
|
||||
"jupyter-book>=1,<2",
|
||||
"nbsphinx"
|
||||
"nbsphinx",
|
||||
"pyinstrument",
|
||||
]
|
||||
agno = ["agno>=2.2.1", "sqlalchemy>=2.0.40"]
|
||||
google = ["google-genai>=1.8.0"]
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import cProfile
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
from sensai.util import logging
|
||||
from sensai.util.logging import LogTime
|
||||
from sensai.util.profiling import profiled
|
||||
|
||||
from serena.agent import SerenaAgent
|
||||
from serena.config.serena_config import SerenaConfig
|
||||
from serena.tools import FindSymbolTool
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.configure()
|
||||
|
||||
# The profiler to use:
|
||||
# Use pyinstrument for hierarchical profiling output
|
||||
# Use cProfile to determine which functions take the most time overall (and use snakeviz to visualize)
|
||||
profiler: Literal["pyinstrument", "cprofile"] = "cprofile"
|
||||
|
||||
project_path = Path(__file__).parent.parent # Serena root
|
||||
|
||||
serena_config = SerenaConfig.from_config_file()
|
||||
serena_config.log_level = logging.INFO
|
||||
serena_config.jetbrains = False
|
||||
serena_config.gui_log_window_enabled = False
|
||||
serena_config.web_dashboard = False
|
||||
|
||||
agent = SerenaAgent(str(project_path), serena_config=serena_config)
|
||||
|
||||
# wait for language server to be ready
|
||||
agent.execute_task(lambda: log.info("Language server is ready."))
|
||||
|
||||
def tool_call():
|
||||
"""This is the function we want to profile."""
|
||||
# NOTE: We use apply (not apply_ex) to run the tool call directly on the main thread
|
||||
with LogTime("Tool call"):
|
||||
result = agent.get_tool(FindSymbolTool).apply(name_path="DQN")
|
||||
log.info("Tool result:\n%s", result)
|
||||
|
||||
if profiler == "pyinstrument":
|
||||
|
||||
@profiled(log_to_file=True)
|
||||
def profiled_tool_call():
|
||||
tool_call()
|
||||
|
||||
profiled_tool_call()
|
||||
|
||||
elif profiler == "cprofile":
|
||||
cProfile.run("tool_call()", "tool_call.pstat")
|
||||
+4
-5
@@ -537,8 +537,7 @@ class ProjectCommands(AutoRegisteringGroup):
|
||||
for i, f in enumerate(tqdm(files, desc="Indexing")):
|
||||
try:
|
||||
ls = ls_mgr.get_language_server(f)
|
||||
ls.request_document_symbols(f, include_body=False)
|
||||
ls.request_document_symbols(f, include_body=True)
|
||||
ls.request_document_symbols(f)
|
||||
language_file_counts[ls.language] += 1
|
||||
except Exception as e:
|
||||
log.error(f"Failed to index {f}, continuing.")
|
||||
@@ -599,14 +598,14 @@ class ProjectCommands(AutoRegisteringGroup):
|
||||
try:
|
||||
for ls in ls_mgr.iter_language_servers():
|
||||
click.echo(f"Indexing for language {ls.language.value} …")
|
||||
symbols, _ = ls.request_document_symbols(file, include_body=False)
|
||||
ls.request_document_symbols(file, include_body=True)
|
||||
document_symbols = ls.request_document_symbols(file)
|
||||
symbols, _ = document_symbols.get_all_symbols_and_roots()
|
||||
if verbose:
|
||||
click.echo(f"Symbols in file '{file}':")
|
||||
for symbol in symbols:
|
||||
click.echo(f" - {symbol['name']} at line {symbol['selectionRange']['start']['line']} of kind {symbol['kind']}")
|
||||
ls.save_cache()
|
||||
click.echo(f"Successfully indexed file '{file}', {len(symbols)} symbols saved to {ls.cache_path}.")
|
||||
click.echo(f"Successfully indexed file '{file}', {len(symbols)} symbols saved to cache in {ls.cache_dir}.")
|
||||
finally:
|
||||
ls_mgr.stop_all()
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ included_optional_tools:
|
||||
|
||||
tool_description_overrides:
|
||||
find_symbol: |
|
||||
Retrieves symbols matching `name_path` in a file.
|
||||
Use `depth > 0` to include children. `name_path` can be: "foo": any symbol named "foo"; "foo/bar": "bar" within "foo"; "/foo/bar": only top-level "foo/bar"
|
||||
Retrieves symbols matching `name_path_pattern` in a file.
|
||||
Use `depth > 0` to include children. `name_path_pattern` can be: "foo": any symbol named "foo"; "foo/bar": "bar" within "foo"; "/foo/bar": only top-level "foo/bar"
|
||||
replace_regex: |
|
||||
Replaces text using regular expressions. Preferred for smaller edits where symbol-level tools aren't appropriate.
|
||||
Use wildcards (.*?) to match large sections efficiently: "beginning.*?end" instead of specifying exact content.
|
||||
|
||||
+60
-41
@@ -19,6 +19,7 @@ if TYPE_CHECKING:
|
||||
from .agent import SerenaAgent
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
NAME_PATH_SEP = "/"
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -113,45 +114,65 @@ class Symbol(ToStringMixin, ABC):
|
||||
"""
|
||||
|
||||
|
||||
class LanguageServerSymbol(Symbol, ToStringMixin):
|
||||
_NAME_PATH_SEP = "/"
|
||||
|
||||
@staticmethod
|
||||
def match_name_path(
|
||||
name_path: str,
|
||||
symbol_name_path_parts: list[str],
|
||||
substring_matching: bool,
|
||||
) -> bool:
|
||||
class NamePathMatcher(ToStringMixin):
|
||||
def __init__(self, name_path_expr: str, substring_matching: bool) -> None:
|
||||
"""
|
||||
Checks if a given `name_path` matches a symbol's qualified name parts.
|
||||
See docstring of `Symbol.find` for more details.
|
||||
:param name_path_expr: the name path expression to match against
|
||||
:param substring_matching: whether to use substring matching for the last segment
|
||||
"""
|
||||
assert name_path, "name_path must not be empty"
|
||||
assert symbol_name_path_parts, "symbol_name_path_parts must not be empty"
|
||||
name_path_sep = LanguageServerSymbol._NAME_PATH_SEP
|
||||
assert name_path_expr, "name_path must not be empty"
|
||||
self._expr = name_path_expr
|
||||
self._substring_matching = substring_matching
|
||||
self._is_absolute_pattern = name_path_expr.startswith(NAME_PATH_SEP)
|
||||
self._pattern_parts = name_path_expr.lstrip(NAME_PATH_SEP).rstrip(NAME_PATH_SEP).split(NAME_PATH_SEP)
|
||||
|
||||
is_absolute_pattern = name_path.startswith(name_path_sep)
|
||||
pattern_parts = name_path.lstrip(name_path_sep).rstrip(name_path_sep).split(name_path_sep)
|
||||
# extract overload index "[idx]" if present at end of last part
|
||||
self._overload_idx: int | None = None
|
||||
last_part = self._pattern_parts[-1]
|
||||
if last_part.endswith("]") and "[" in last_part:
|
||||
bracket_idx = last_part.rfind("[")
|
||||
index_part = last_part[bracket_idx + 1 : -1]
|
||||
if index_part.isdigit():
|
||||
self._pattern_parts[-1] = last_part[:bracket_idx]
|
||||
self._overload_idx = int(index_part)
|
||||
|
||||
def _tostring_includes(self) -> list[str]:
|
||||
return ["_expr"]
|
||||
|
||||
def matches_ls_symbol(self, symbol: "LanguageServerSymbol") -> bool:
|
||||
return self.matches_components(symbol.get_name_path_parts(), symbol.overload_idx)
|
||||
|
||||
def matches_components(self, symbol_name_path_parts: list[str], overload_idx: int | None) -> bool:
|
||||
# filtering based on ancestors
|
||||
if len(pattern_parts) > len(symbol_name_path_parts):
|
||||
if len(self._pattern_parts) > len(symbol_name_path_parts):
|
||||
# can't possibly match if pattern has more parts than symbol
|
||||
return False
|
||||
if is_absolute_pattern and len(pattern_parts) != len(symbol_name_path_parts):
|
||||
if self._is_absolute_pattern and len(self._pattern_parts) != len(symbol_name_path_parts):
|
||||
# for absolute patterns, the number of parts must match exactly
|
||||
return False
|
||||
if symbol_name_path_parts[-len(pattern_parts) : -1] != pattern_parts[:-1]:
|
||||
if symbol_name_path_parts[-len(self._pattern_parts) : -1] != self._pattern_parts[:-1]:
|
||||
# ancestors must match
|
||||
return False
|
||||
|
||||
# matching the last part of the symbol name
|
||||
name_to_match = pattern_parts[-1]
|
||||
name_to_match = self._pattern_parts[-1]
|
||||
symbol_name = symbol_name_path_parts[-1]
|
||||
if substring_matching:
|
||||
return name_to_match in symbol_name
|
||||
if self._substring_matching:
|
||||
if name_to_match not in symbol_name:
|
||||
return False
|
||||
else:
|
||||
return name_to_match == symbol_name
|
||||
if name_to_match != symbol_name:
|
||||
return False
|
||||
|
||||
# check for matching overload index
|
||||
if self._overload_idx is not None:
|
||||
if overload_idx != self._overload_idx:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class LanguageServerSymbol(Symbol, ToStringMixin):
|
||||
def __init__(self, symbol_root_from_ls: UnifiedSymbolInformation) -> None:
|
||||
self.symbol_root = symbol_root_from_ls
|
||||
|
||||
@@ -173,6 +194,10 @@ class LanguageServerSymbol(Symbol, ToStringMixin):
|
||||
def symbol_kind(self) -> SymbolKind:
|
||||
return self.symbol_root["kind"]
|
||||
|
||||
@property
|
||||
def overload_idx(self) -> int | None:
|
||||
return self.symbol_root.get("overload_idx")
|
||||
|
||||
def is_neighbouring_definition_separated_by_empty_line(self) -> bool:
|
||||
return self.symbol_kind in (SymbolKind.Function, SymbolKind.Method, SymbolKind.Class, SymbolKind.Interface, SymbolKind.Struct)
|
||||
|
||||
@@ -256,9 +281,13 @@ class LanguageServerSymbol(Symbol, ToStringMixin):
|
||||
|
||||
def get_name_path(self) -> str:
|
||||
"""
|
||||
Get the name path of the symbol (e.g. "class/method/inner_function").
|
||||
Get the name path of the symbol, e.g. "class/method/inner_function" or
|
||||
"class/method[1]" (overloaded method with identifying index).
|
||||
"""
|
||||
return self._NAME_PATH_SEP.join(self.get_name_path_parts())
|
||||
name_path = NAME_PATH_SEP.join(self.get_name_path_parts())
|
||||
if "overload_idx" in self.symbol_root:
|
||||
name_path += f"[{self.symbol_root['overload_idx']}]"
|
||||
return name_path
|
||||
|
||||
def get_name_path_parts(self) -> list[str]:
|
||||
"""
|
||||
@@ -321,7 +350,7 @@ class LanguageServerSymbol(Symbol, ToStringMixin):
|
||||
For example, passing `/class` will match only against top-level symbols like `class` but not against `nested_class/class`.
|
||||
Passing `/class/method` will match against `class/method` but not `nested_class/class/method` or `method`.
|
||||
|
||||
:param name_path: the name path to match against
|
||||
:param name_path: the name path expression to match against
|
||||
:param substring_matching: whether to use substring matching (as opposed to exact matching)
|
||||
of the last segment of `name_path` against the symbol name.
|
||||
:param include_kinds: an optional sequence of ints representing the LSP symbol kind.
|
||||
@@ -329,17 +358,14 @@ class LanguageServerSymbol(Symbol, ToStringMixin):
|
||||
:param exclude_kinds: If provided, symbols of the given kinds will be excluded from the result.
|
||||
"""
|
||||
result = []
|
||||
name_path_matcher = NamePathMatcher(name_path, substring_matching)
|
||||
|
||||
def should_include(s: "LanguageServerSymbol") -> bool:
|
||||
if include_kinds is not None and s.symbol_kind not in include_kinds:
|
||||
return False
|
||||
if exclude_kinds is not None and s.symbol_kind in exclude_kinds:
|
||||
return False
|
||||
return LanguageServerSymbol.match_name_path(
|
||||
name_path=name_path,
|
||||
symbol_name_path_parts=s.get_name_path_parts(),
|
||||
substring_matching=substring_matching,
|
||||
)
|
||||
return name_path_matcher.matches_ls_symbol(s)
|
||||
|
||||
def traverse(s: "LanguageServerSymbol") -> None:
|
||||
if should_include(s):
|
||||
@@ -465,7 +491,6 @@ class LanguageServerSymbolRetriever:
|
||||
def find_by_name(
|
||||
self,
|
||||
name_path: str,
|
||||
include_body: bool = False,
|
||||
include_kinds: Sequence[SymbolKind] | None = None,
|
||||
exclude_kinds: Sequence[SymbolKind] | None = None,
|
||||
substring_matching: bool = False,
|
||||
@@ -478,7 +503,7 @@ class LanguageServerSymbolRetriever:
|
||||
"""
|
||||
symbols: list[LanguageServerSymbol] = []
|
||||
for lang_server in self._ls_manager.iter_language_servers():
|
||||
symbol_roots = lang_server.request_full_symbol_tree(within_relative_path=within_relative_path, include_body=include_body)
|
||||
symbol_roots = lang_server.request_full_symbol_tree(within_relative_path=within_relative_path)
|
||||
for root in symbol_roots:
|
||||
symbols.extend(
|
||||
LanguageServerSymbol(root).find(
|
||||
@@ -487,18 +512,12 @@ class LanguageServerSymbolRetriever:
|
||||
)
|
||||
return symbols
|
||||
|
||||
def get_document_symbols(self, relative_path: str) -> list[LanguageServerSymbol]:
|
||||
lang_server = self.get_language_server(relative_path)
|
||||
symbol_dicts, _roots = lang_server.request_document_symbols(relative_path, include_body=False)
|
||||
symbols = [LanguageServerSymbol(s) for s in symbol_dicts]
|
||||
return symbols
|
||||
|
||||
def find_by_location(self, location: LanguageServerSymbolLocation) -> LanguageServerSymbol | None:
|
||||
if location.relative_path is None:
|
||||
return None
|
||||
lang_server = self.get_language_server(location.relative_path)
|
||||
symbol_dicts, _roots = lang_server.request_document_symbols(location.relative_path, include_body=False)
|
||||
for symbol_dict in symbol_dicts:
|
||||
document_symbols = lang_server.request_document_symbols(location.relative_path)
|
||||
for symbol_dict in document_symbols.iter_symbols():
|
||||
symbol = LanguageServerSymbol(symbol_dict)
|
||||
if symbol.location == location:
|
||||
return symbol
|
||||
|
||||
@@ -6,51 +6,39 @@ from serena.tools.jetbrains_plugin_client import JetBrainsPluginClient
|
||||
|
||||
class JetBrainsFindSymbolTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional):
|
||||
"""
|
||||
Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
||||
Performs a global (or local) search for symbols using the JetBrains backend
|
||||
"""
|
||||
|
||||
def apply(
|
||||
self,
|
||||
name_path: str,
|
||||
name_path_pattern: str,
|
||||
depth: int = 0,
|
||||
relative_path: str | None = None,
|
||||
include_body: bool = False,
|
||||
max_answer_chars: int = -1,
|
||||
) -> str:
|
||||
"""
|
||||
Retrieves information on all symbols/code entities (classes, methods, etc.) based on the given `name_path`,
|
||||
which represents a pattern for the symbol's path within the symbol tree of a single file.
|
||||
The returned symbol location can be used for edits or further queries.
|
||||
Retrieves information on all symbols/code entities (classes, methods, etc.) based on the given name path pattern.
|
||||
The returned symbol information can be used for edits or further queries.
|
||||
Specify `depth > 0` to retrieve children (e.g., methods of a class).
|
||||
|
||||
The matching behavior is determined by the structure of `name_path`, which can
|
||||
either be a simple name (e.g. "method") or a name path like "class/method" (relative name path)
|
||||
or "/class/method" (absolute name path).
|
||||
Note that the name path is not a path in the file system but rather a path in the symbol tree
|
||||
**within a single file**. Thus, file or directory names should never be included in the `name_path`.
|
||||
For restricting the search to a single file or directory, pass the `relative_path` parameter.
|
||||
The retrieved symbols' `name_path` attribute will always be composed of symbol names, never file
|
||||
or directory names.
|
||||
A name path is a path in the symbol tree *within a source file*.
|
||||
For example, the method `my_method` defined in class `MyClass` would have the name path `MyClass/my_method`.
|
||||
If a symbol is overloaded (e.g., in Java), a 0-based index is appended (e.g. "MyClass/my_method[0]") to
|
||||
uniquely identify it.
|
||||
|
||||
Key aspects of the name path matching behavior:
|
||||
- The name of the retrieved symbols will match the last segment of `name_path`, while preceding segments
|
||||
will restrict the search to symbols that have a desired sequence of ancestors.
|
||||
- If there is no `/` in `name_path`, there is no restriction on the ancestor symbols.
|
||||
For example, passing `method` will match against all symbols with name paths like `method`,
|
||||
`class/method`, `class/nested_class/method`, etc.
|
||||
- If `name_path` contains at least one `/`, the matching is restricted to symbols
|
||||
with the respective ancestors. For example, passing `class/method` will match against
|
||||
`class/method` as well as `nested_class/class/method` but not `other_class/method`.
|
||||
- If `name_path` starts with a `/`, it will be treated as an absolute name path pattern, i.e.
|
||||
all ancestors are provided and must match.
|
||||
For example, passing `/class` will match only against top-level symbols named `class` but
|
||||
will not match `nested_class/class`. Passing `/class/method` will match `class/method` but
|
||||
not `outer_class/class/method`.
|
||||
To search for a symbol, you provide a name path pattern that is used to match against name paths.
|
||||
It can be
|
||||
* a simple name (e.g. "method"), which will match any symbol with that name
|
||||
* a relative path like "class/method", which will match any symbol with that name path suffix
|
||||
* an absolute name path "/class/method" (absolute name path), which requires an exact match of the full name path within the source file.
|
||||
Append an index `[i]` to match a specific overload only, e.g. "MyClass/my_method[1]".
|
||||
|
||||
:param name_path: The name path pattern to search for, see above for details.
|
||||
:param depth: Depth to retrieve descendants (e.g., 1 for class methods/attributes).
|
||||
:param relative_path: Optional. Restrict search to this file or directory.
|
||||
If None, searches entire codebase.
|
||||
:param name_path_pattern: the name path matching pattern (see above)
|
||||
:param depth: depth up to which descendants shall be retrieved (e.g. use 1 to also retrieve immediate children;
|
||||
for the case where the symbol is a class, this will return its methods).
|
||||
Default 0.
|
||||
:param relative_path: Optional. Restrict search to this file or directory. If None, searches entire codebase.
|
||||
If a directory is passed, the search will be restricted to the files in that directory.
|
||||
If a file is passed, the search will be restricted to that file.
|
||||
If you have some knowledge about the codebase, you should use this parameter, as it will significantly
|
||||
@@ -62,7 +50,7 @@ class JetBrainsFindSymbolTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional):
|
||||
"""
|
||||
with JetBrainsPluginClient.from_project(self.project) as client:
|
||||
response_dict = client.find_symbol(
|
||||
name_path=name_path,
|
||||
name_path=name_path_pattern,
|
||||
relative_path=relative_path,
|
||||
depth=depth,
|
||||
include_body=include_body,
|
||||
@@ -73,7 +61,7 @@ class JetBrainsFindSymbolTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional):
|
||||
|
||||
class JetBrainsFindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional):
|
||||
"""
|
||||
Finds symbols that reference the given symbol
|
||||
Finds symbols that reference the given symbol using the JetBrains backend
|
||||
"""
|
||||
|
||||
def apply(
|
||||
@@ -104,7 +92,7 @@ class JetBrainsFindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead, ToolMark
|
||||
|
||||
class JetBrainsGetSymbolsOverviewTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional):
|
||||
"""
|
||||
Retrieves an overview of the top-level symbols within a specified file
|
||||
Retrieves an overview of the top-level symbols within a specified file using the JetBrains backend
|
||||
"""
|
||||
|
||||
def apply(
|
||||
|
||||
@@ -79,13 +79,13 @@ class GetSymbolsOverviewTool(Tool, ToolMarkerSymbolicRead):
|
||||
|
||||
class FindSymbolTool(Tool, ToolMarkerSymbolicRead):
|
||||
"""
|
||||
Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
||||
Performs a global (or local) search using the language server backend.
|
||||
"""
|
||||
|
||||
# noinspection PyDefaultArgument
|
||||
def apply(
|
||||
self,
|
||||
name_path: str,
|
||||
name_path_pattern: str,
|
||||
depth: int = 0,
|
||||
relative_path: str = "",
|
||||
include_body: bool = False,
|
||||
@@ -95,38 +95,26 @@ class FindSymbolTool(Tool, ToolMarkerSymbolicRead):
|
||||
max_answer_chars: int = -1,
|
||||
) -> str:
|
||||
"""
|
||||
Retrieves information on all symbols/code entities (classes, methods, etc.) based on the given `name_path`,
|
||||
which represents a pattern for the symbol's path within the symbol tree of a single file.
|
||||
The returned symbol location can be used for edits or further queries.
|
||||
Specify `depth > 0` to retrieve children (e.g., methods of a class).
|
||||
Retrieves information on all symbols/code entities (classes, methods, etc.) based on the given name path pattern.
|
||||
The returned symbol information can be used for edits or further queries.
|
||||
Specify `depth > 0` to also retrieve children/descendants (e.g., methods of a class).
|
||||
|
||||
The matching behavior is determined by the structure of `name_path`, which can
|
||||
either be a simple name (e.g. "method") or a name path like "class/method" (relative name path)
|
||||
or "/class/method" (absolute name path). Note that the name path is not a path in the file system
|
||||
but rather a path in the symbol tree **within a single file**. Thus, file or directory names should never
|
||||
be included in the `name_path`. For restricting the search to a single file or directory,
|
||||
the `within_relative_path` parameter should be used instead. The retrieved symbols' `name_path` attribute
|
||||
will always be composed of symbol names, never file or directory names.
|
||||
A name path is a path in the symbol tree *within a source file*.
|
||||
For example, the method `my_method` defined in class `MyClass` would have the name path `MyClass/my_method`.
|
||||
If a symbol is overloaded (e.g., in Java), a 0-based index is appended (e.g. "MyClass/my_method[0]") to
|
||||
uniquely identify it.
|
||||
|
||||
Key aspects of the name path matching behavior:
|
||||
- Trailing slashes in `name_path` play no role and are ignored.
|
||||
- The name of the retrieved symbols will match (either exactly or as a substring)
|
||||
the last segment of `name_path`, while other segments will restrict the search to symbols that
|
||||
have a desired sequence of ancestors.
|
||||
- If there is no starting or intermediate slash in `name_path`, there is no
|
||||
restriction on the ancestor symbols. For example, passing `method` will match
|
||||
against symbols with name paths like `method`, `class/method`, `class/nested_class/method`, etc.
|
||||
- If `name_path` contains a `/` but doesn't start with a `/`, the matching is restricted to symbols
|
||||
with the same ancestors as the last segment of `name_path`. For example, passing `class/method` will match against
|
||||
`class/method` as well as `nested_class/class/method` but not `method`.
|
||||
- If `name_path` starts with a `/`, it will be treated as an absolute name path pattern, meaning
|
||||
that the first segment of it must match the first segment of the symbol's name path.
|
||||
For example, passing `/class` will match only against top-level symbols like `class` but not against `nested_class/class`.
|
||||
Passing `/class/method` will match against `class/method` but not `nested_class/class/method` or `method`.
|
||||
To search for a symbol, you provide a name path pattern that is used to match against name paths.
|
||||
It can be
|
||||
* a simple name (e.g. "method"), which will match any symbol with that name
|
||||
* a relative path like "class/method", which will match any symbol with that name path suffix
|
||||
* an absolute name path "/class/method" (absolute name path), which requires an exact match of the full name path within the source file.
|
||||
Append an index `[i]` to match a specific overload only, e.g. "MyClass/my_method[1]".
|
||||
|
||||
|
||||
:param name_path: The name path pattern to search for, see above for details.
|
||||
:param depth: Depth to retrieve descendants (e.g., 1 for class methods/attributes).
|
||||
:param name_path_pattern: the name path matching pattern (see above)
|
||||
:param depth: depth up to which descendants shall be retrieved (e.g. use 1 to also retrieve immediate children;
|
||||
for the case where the symbol is a class, this will return its methods).
|
||||
Default 0.
|
||||
:param relative_path: Optional. Restrict search to this file or directory. If None, searches entire codebase.
|
||||
If a directory is passed, the search will be restricted to the files in that directory.
|
||||
If a file is passed, the search will be restricted to that file.
|
||||
@@ -140,7 +128,8 @@ class FindSymbolTool(Tool, ToolMarkerSymbolicRead):
|
||||
If not provided, all kinds are included.
|
||||
:param exclude_kinds: Optional. List of LSP symbol kind integers to exclude. Takes precedence over `include_kinds`.
|
||||
If not provided, no kinds are excluded.
|
||||
:param substring_matching: If True, use substring matching for the last segment of `name`.
|
||||
:param substring_matching: If True, use substring matching for the last element of the pattern, such that
|
||||
"Foo/get" would match "Foo/getValue" and "Foo/getData".
|
||||
:param max_answer_chars: Max characters for the JSON result. If exceeded, no content is returned.
|
||||
-1 means the default value from the config will be used.
|
||||
:return: a list of symbols (with locations) matching the name.
|
||||
@@ -149,8 +138,7 @@ class FindSymbolTool(Tool, ToolMarkerSymbolicRead):
|
||||
parsed_exclude_kinds: Sequence[SymbolKind] | None = [SymbolKind(k) for k in exclude_kinds] if exclude_kinds else None
|
||||
symbol_retriever = self.create_language_server_symbol_retriever()
|
||||
symbols = symbol_retriever.find_by_name(
|
||||
name_path,
|
||||
include_body=include_body,
|
||||
name_path_pattern,
|
||||
include_kinds=parsed_include_kinds,
|
||||
exclude_kinds=parsed_exclude_kinds,
|
||||
substring_matching=substring_matching,
|
||||
@@ -163,7 +151,7 @@ class FindSymbolTool(Tool, ToolMarkerSymbolicRead):
|
||||
|
||||
class FindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead):
|
||||
"""
|
||||
Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
||||
Finds symbols that reference the given symbol using the language server backend
|
||||
"""
|
||||
|
||||
# noinspection PyDefaultArgument
|
||||
@@ -216,7 +204,7 @@ class FindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead):
|
||||
|
||||
class ReplaceSymbolBodyTool(Tool, ToolMarkerSymbolicEdit):
|
||||
"""
|
||||
Replaces the full definition of a symbol.
|
||||
Replaces the full definition of a symbol using the language server backend.
|
||||
"""
|
||||
|
||||
def apply(
|
||||
|
||||
@@ -680,7 +680,7 @@ class ALLanguageServer(SolidLanguageServer):
|
||||
return super().is_ignored_dirname(dirname) or dirname in al_ignore_dirs
|
||||
|
||||
@override
|
||||
def request_full_symbol_tree(self, within_relative_path: str | None = None, include_body: bool = False) -> list[dict]:
|
||||
def request_full_symbol_tree(self, within_relative_path: str | None = None) -> list[dict]:
|
||||
"""
|
||||
Override to handle AL's requirement of opening files before requesting symbols.
|
||||
|
||||
@@ -713,7 +713,7 @@ class ALLanguageServer(SolidLanguageServer):
|
||||
|
||||
if os.path.isfile(within_abs_path):
|
||||
# Single file case - use parent class implementation
|
||||
_, root_nodes = self.request_document_symbols(within_relative_path, include_body=include_body)
|
||||
root_nodes = self.request_document_symbols(within_relative_path).root_symbols
|
||||
return root_nodes
|
||||
|
||||
# Directory case - scan within this directory
|
||||
@@ -755,7 +755,7 @@ class ALLanguageServer(SolidLanguageServer):
|
||||
try:
|
||||
# Use our overridden request_document_symbols which handles opening
|
||||
self.logger.log(f"AL: Getting symbols for {relative_path}", logging.DEBUG)
|
||||
all_syms, root_syms = self.request_document_symbols(relative_path, include_body=include_body)
|
||||
all_syms, root_syms = self.request_document_symbols(relative_path).get_all_symbols_and_roots()
|
||||
|
||||
if root_syms:
|
||||
# Create a file-level symbol containing the document symbols
|
||||
|
||||
@@ -9,9 +9,8 @@ import pathlib
|
||||
import shutil
|
||||
import threading
|
||||
|
||||
from solidlsp import ls_types
|
||||
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls import DocumentSymbols, LSPFileBuffer, SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_logger import LanguageServerLogger
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
@@ -194,37 +193,24 @@ class BashLanguageServer(SolidLanguageServer):
|
||||
else:
|
||||
self.logger.log("Bash server initialization complete", logging.INFO)
|
||||
|
||||
def request_document_symbols(
|
||||
self, relative_file_path: str, include_body: bool = False
|
||||
) -> tuple[list[ls_types.UnifiedSymbolInformation], list[ls_types.UnifiedSymbolInformation]]:
|
||||
"""
|
||||
Request document symbols from bash-language-server via LSP.
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
Args:
|
||||
relative_file_path: Path to the bash file relative to repository root
|
||||
include_body: Whether to include function bodies in symbol information
|
||||
|
||||
Returns:
|
||||
Tuple of (all_symbols, root_symbols) detected by the LSP server
|
||||
|
||||
"""
|
||||
self.logger.log(f"Requesting document symbols via LSP for {relative_file_path}", logging.DEBUG)
|
||||
|
||||
# Use the standard LSP approach - bash-language-server handles all function syntaxes correctly
|
||||
all_symbols, root_symbols = super().request_document_symbols(relative_file_path, include_body)
|
||||
document_symbols = super().request_document_symbols(relative_file_path, file_buffer=file_buffer)
|
||||
|
||||
# Log detection results for debugging
|
||||
functions = [s for s in all_symbols if s.get("kind") == 12]
|
||||
functions = [s for s in document_symbols.iter_symbols() if s.get("kind") == 12]
|
||||
self.logger.log(
|
||||
f"LSP function detection for {relative_file_path}: Found {len(functions)} functions",
|
||||
logging.INFO,
|
||||
)
|
||||
|
||||
return all_symbols, root_symbols
|
||||
return document_symbols
|
||||
@@ -13,7 +13,7 @@ from pathlib import PurePath
|
||||
|
||||
from overrides import override
|
||||
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls import GenericDocumentSymbol, SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_logger import LanguageServerLogger
|
||||
from solidlsp.ls_utils import FileUtils, PlatformUtils
|
||||
@@ -109,41 +109,39 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
# TODO: Add "self.runtime_dependency_paths.jre_home_path"/bin to $PATH as well
|
||||
proc_env = {"syntaxserver": "false", "JAVA_HOME": self.runtime_dependency_paths.jre_home_path}
|
||||
proc_cwd = repository_root_path
|
||||
cmd = " ".join(
|
||||
[
|
||||
jre_path,
|
||||
"--add-modules=ALL-SYSTEM",
|
||||
"--add-opens",
|
||||
"java.base/java.util=ALL-UNNAMED",
|
||||
"--add-opens",
|
||||
"java.base/java.lang=ALL-UNNAMED",
|
||||
"--add-opens",
|
||||
"java.base/sun.nio.fs=ALL-UNNAMED",
|
||||
"-Declipse.application=org.eclipse.jdt.ls.core.id1",
|
||||
"-Dosgi.bundles.defaultStartLevel=4",
|
||||
"-Declipse.product=org.eclipse.jdt.ls.core.product",
|
||||
"-Djava.import.generatesMetadataFilesAtProjectRoot=false",
|
||||
"-Dfile.encoding=utf8",
|
||||
"-noverify",
|
||||
"-XX:+UseParallelGC",
|
||||
"-XX:GCTimeRatio=4",
|
||||
"-XX:AdaptiveSizePolicyWeight=90",
|
||||
"-Dsun.zip.disableMemoryMapping=true",
|
||||
"-Djava.lsp.joinOnCompletion=true",
|
||||
"-Xmx3G",
|
||||
"-Xms100m",
|
||||
"-Xlog:disable",
|
||||
"-Dlog.level=ALL",
|
||||
f'"-javaagent:{lombok_jar_path}"',
|
||||
f'"-Djdt.core.sharedIndexLocation={shared_cache_location}"',
|
||||
"-jar",
|
||||
f'"{jdtls_launcher_jar}"',
|
||||
"-configuration",
|
||||
f'"{jdtls_config_path}"',
|
||||
"-data",
|
||||
f'"{data_dir}"',
|
||||
]
|
||||
)
|
||||
cmd = [
|
||||
jre_path,
|
||||
"--add-modules=ALL-SYSTEM",
|
||||
"--add-opens",
|
||||
"java.base/java.util=ALL-UNNAMED",
|
||||
"--add-opens",
|
||||
"java.base/java.lang=ALL-UNNAMED",
|
||||
"--add-opens",
|
||||
"java.base/sun.nio.fs=ALL-UNNAMED",
|
||||
"-Declipse.application=org.eclipse.jdt.ls.core.id1",
|
||||
"-Dosgi.bundles.defaultStartLevel=4",
|
||||
"-Declipse.product=org.eclipse.jdt.ls.core.product",
|
||||
"-Djava.import.generatesMetadataFilesAtProjectRoot=false",
|
||||
"-Dfile.encoding=utf8",
|
||||
"-noverify",
|
||||
"-XX:+UseParallelGC",
|
||||
"-XX:GCTimeRatio=4",
|
||||
"-XX:AdaptiveSizePolicyWeight=90",
|
||||
"-Dsun.zip.disableMemoryMapping=true",
|
||||
"-Djava.lsp.joinOnCompletion=true",
|
||||
"-Xmx3G",
|
||||
"-Xms100m",
|
||||
"-Xlog:disable",
|
||||
"-Dlog.level=ALL",
|
||||
f"-javaagent:{lombok_jar_path}",
|
||||
f"-Djdt.core.sharedIndexLocation={shared_cache_location}",
|
||||
"-jar",
|
||||
f"{jdtls_launcher_jar}",
|
||||
"-configuration",
|
||||
f"{jdtls_config_path}",
|
||||
"-data",
|
||||
f"{data_dir}",
|
||||
]
|
||||
|
||||
self.service_ready_event = threading.Event()
|
||||
self.intellicode_enable_command_available = threading.Event()
|
||||
@@ -789,3 +787,23 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
|
||||
# TODO: Add comments about why we wait here, and how this can be optimized
|
||||
self.service_ready_event.wait()
|
||||
|
||||
def _request_document_symbols(self, relative_file_path: str) -> list[GenericDocumentSymbol]:
|
||||
result = super()._request_document_symbols(relative_file_path)
|
||||
|
||||
# JDTLS sometimes returns symbol names with type information to handle overloads,
|
||||
# e.g. "myMethod(int) <T>", but we want overloads to be handled via overload_idx,
|
||||
# which requires the name to be just "myMethod".
|
||||
|
||||
def fix_name(symbol: GenericDocumentSymbol):
|
||||
if "(" in symbol["name"]:
|
||||
symbol["name"] = symbol["name"][: symbol["name"].index("(")]
|
||||
children = symbol.get("children")
|
||||
if children:
|
||||
for child in children:
|
||||
fix_name(child)
|
||||
|
||||
for root_symbol in result:
|
||||
fix_name(root_symbol)
|
||||
|
||||
return result
|
||||
@@ -12,7 +12,7 @@ import threading
|
||||
from overrides import override
|
||||
|
||||
from solidlsp import ls_types
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls import DocumentSymbols, LSPFileBuffer, SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_logger import LanguageServerLogger
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
@@ -138,24 +138,20 @@ class FortranLanguageServer(SolidLanguageServer):
|
||||
return symbol
|
||||
|
||||
@override
|
||||
def request_document_symbols(
|
||||
self, relative_file_path: str, include_body: bool = False
|
||||
) -> tuple[list[ls_types.UnifiedSymbolInformation], list[ls_types.UnifiedSymbolInformation]]:
|
||||
"""
|
||||
Override to fix fortls's incorrect selectionRange bug.
|
||||
def request_document_symbols(self, relative_file_path: str, file_buffer: LSPFileBuffer | None = None) -> DocumentSymbols:
|
||||
# Override to fix fortls's incorrect selectionRange bug.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# This override:
|
||||
# 1. Gets symbols from fortls via parent implementation
|
||||
# 2. Parses each symbol's line to find the correct identifier position
|
||||
# 3. Fixes selectionRange for all symbols recursively
|
||||
# 4. Returns corrected symbols
|
||||
|
||||
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.
|
||||
|
||||
This override:
|
||||
1. Gets symbols from fortls via parent implementation
|
||||
2. Parses each symbol's line to find the correct identifier position
|
||||
3. Fixes selectionRange for all symbols recursively
|
||||
4. Returns corrected symbols
|
||||
|
||||
"""
|
||||
# Get symbols from fortls (with incorrect selectionRange)
|
||||
all_symbols, root_symbols = super().request_document_symbols(relative_file_path, include_body)
|
||||
document_symbols = super().request_document_symbols(relative_file_path, file_buffer=file_buffer)
|
||||
|
||||
# Get file content for parsing
|
||||
with self.open_file(relative_file_path) as file_data:
|
||||
@@ -173,10 +169,9 @@ class FortranLanguageServer(SolidLanguageServer):
|
||||
return fixed
|
||||
|
||||
# Apply fix to all symbols
|
||||
fixed_all_symbols = [fix_symbol_and_children(sym) for sym in all_symbols]
|
||||
fixed_root_symbols = [fix_symbol_and_children(sym) for sym in root_symbols]
|
||||
fixed_root_symbols = [fix_symbol_and_children(sym) for sym in document_symbols.root_symbols]
|
||||
|
||||
return fixed_all_symbols, fixed_root_symbols
|
||||
return DocumentSymbols(fixed_root_symbols)
|
||||
|
||||
@staticmethod
|
||||
def _check_fortls_installation():
|
||||
|
||||
@@ -16,7 +16,7 @@ from pathlib import Path
|
||||
from overrides import override
|
||||
|
||||
from solidlsp import ls_types
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls import DocumentSymbols, LSPFileBuffer, SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_logger import LanguageServerLogger
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
@@ -70,17 +70,13 @@ class NixLanguageServer(SolidLanguageServer):
|
||||
return symbol
|
||||
|
||||
@override
|
||||
def request_document_symbols(
|
||||
self, relative_file_path: str, include_body: bool = False
|
||||
) -> tuple[list[ls_types.UnifiedSymbolInformation], list[ls_types.UnifiedSymbolInformation]]:
|
||||
"""
|
||||
Override to extend Nix symbol ranges to include trailing semicolons.
|
||||
def request_document_symbols(self, relative_file_path: str, file_buffer: LSPFileBuffer | None = None) -> 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.
|
||||
|
||||
nixd provides expression-level ranges (excluding semicolons) but serena needs
|
||||
statement-level ranges (including semicolons) for proper symbol replacement.
|
||||
"""
|
||||
# Get symbols from parent implementation
|
||||
all_symbols, root_symbols = super().request_document_symbols(relative_file_path, include_body)
|
||||
document_symbols = super().request_document_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)
|
||||
@@ -97,10 +93,9 @@ class NixLanguageServer(SolidLanguageServer):
|
||||
return extended
|
||||
|
||||
# Apply range extension to all symbols
|
||||
extended_all_symbols = [extend_symbol_and_children(sym) for sym in all_symbols]
|
||||
extended_root_symbols = [extend_symbol_and_children(sym) for sym in root_symbols]
|
||||
extended_root_symbols = [extend_symbol_and_children(sym) for sym in document_symbols.root_symbols]
|
||||
|
||||
return extended_all_symbols, extended_root_symbols
|
||||
return DocumentSymbols(extended_root_symbols)
|
||||
|
||||
@override
|
||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||
|
||||
+385
-204
@@ -4,7 +4,6 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import pickle
|
||||
import shutil
|
||||
import subprocess
|
||||
import threading
|
||||
@@ -18,6 +17,7 @@ from time import sleep
|
||||
from typing import Self, Union, cast
|
||||
|
||||
import pathspec
|
||||
from sensai.util.pickle import getstate, load_pickle
|
||||
|
||||
from serena.text_utils import MatchedConsecutiveLines
|
||||
from serena.util.file_system import match_path
|
||||
@@ -31,15 +31,17 @@ from solidlsp.ls_utils import FileUtils, PathUtils, TextUtils
|
||||
from solidlsp.lsp_protocol_handler import lsp_types
|
||||
from solidlsp.lsp_protocol_handler import lsp_types as LSPTypes
|
||||
from solidlsp.lsp_protocol_handler.lsp_constants import LSPConstants
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, LocationLink, SymbolKind
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, LocationLink
|
||||
from solidlsp.lsp_protocol_handler.server import (
|
||||
LSPError,
|
||||
ProcessLaunchInfo,
|
||||
StringDict,
|
||||
)
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
from solidlsp.util.cache import load_cache, save_cache
|
||||
|
||||
GenericDocumentSymbol = Union[LSPTypes.DocumentSymbol, LSPTypes.SymbolInformation, ls_types.UnifiedSymbolInformation]
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclasses.dataclass(kw_only=True)
|
||||
@@ -77,6 +79,50 @@ class LSPFileBuffer:
|
||||
def __post_init__(self):
|
||||
self.content_hash = hashlib.md5(self.contents.encode("utf-8")).hexdigest()
|
||||
|
||||
def split_lines(self) -> list[str]:
|
||||
"""Splits the contents of the file into lines."""
|
||||
return self.contents.split("\n")
|
||||
|
||||
|
||||
class DocumentSymbols:
|
||||
# IMPORTANT: Instances of this class are persisted in the high-level document symbol cache
|
||||
|
||||
def __init__(self, root_symbols: list[ls_types.UnifiedSymbolInformation]):
|
||||
self.root_symbols = root_symbols
|
||||
self._all_symbols: list[ls_types.UnifiedSymbolInformation] | None = None
|
||||
|
||||
def __getstate__(self):
|
||||
return getstate(DocumentSymbols, self, transient_properties=["_all_symbols"])
|
||||
|
||||
def iter_symbols(self) -> Iterator[ls_types.UnifiedSymbolInformation]:
|
||||
"""
|
||||
Iterate over all symbols in the document symbol tree.
|
||||
Yields symbols in a depth-first manner.
|
||||
"""
|
||||
if self._all_symbols is not None:
|
||||
yield from self._all_symbols
|
||||
return
|
||||
|
||||
def traverse(s: ls_types.UnifiedSymbolInformation):
|
||||
yield s
|
||||
for child in s.get("children", []):
|
||||
yield from traverse(child)
|
||||
|
||||
for root_symbol in self.root_symbols:
|
||||
yield from traverse(root_symbol)
|
||||
|
||||
def get_all_symbols_and_roots(self) -> tuple[list[ls_types.UnifiedSymbolInformation], list[ls_types.UnifiedSymbolInformation]]:
|
||||
"""
|
||||
This function returns all symbols in the document as a flat list and the root symbols.
|
||||
It exists to facilitate migration from previous versions, where this was the return interface of
|
||||
the LS method that obtained document symbols.
|
||||
|
||||
:return: A tuple containing a list of all symbols in the document and a list of root symbols.
|
||||
"""
|
||||
if self._all_symbols is None:
|
||||
self._all_symbols = list(self.iter_symbols())
|
||||
return self._all_symbols, self.root_symbols
|
||||
|
||||
|
||||
class SolidLanguageServer(ABC):
|
||||
"""
|
||||
@@ -85,6 +131,17 @@ class SolidLanguageServer(ABC):
|
||||
"""
|
||||
|
||||
CACHE_FOLDER_NAME = "cache"
|
||||
RAW_DOCUMENT_SYMBOLS_CACHE_VERSION = 1
|
||||
"""
|
||||
global version identifier for raw symbol caches; an LS-specific version is defined separately and combined with this.
|
||||
This should be incremented whenever there is a change in the way raw document symbols are stored.
|
||||
If the result of a language server changes in a way that affects the raw document symbols,
|
||||
the LS-specific version should be incremented instead.
|
||||
"""
|
||||
RAW_DOCUMENT_SYMBOL_CACHE_FILENAME = "raw_document_symbols.pkl"
|
||||
RAW_DOCUMENT_SYMBOL_CACHE_FILENAME_LEGACY_FALLBACK = "document_symbols_cache_v23-06-25.pkl"
|
||||
DOCUMENT_SYMBOL_CACHE_VERSION = 3
|
||||
DOCUMENT_SYMBOL_CACHE_FILENAME = "document_symbols.pkl"
|
||||
|
||||
# To be overridden and extended by subclasses
|
||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||
@@ -161,6 +218,7 @@ class SolidLanguageServer(ABC):
|
||||
process_launch_info: ProcessLaunchInfo,
|
||||
language_id: str,
|
||||
solidlsp_settings: SolidLSPSettings,
|
||||
cache_version_raw_document_symbols: int = 1,
|
||||
):
|
||||
"""
|
||||
Initializes a LanguageServer instance.
|
||||
@@ -174,6 +232,10 @@ class SolidLanguageServer(ABC):
|
||||
This parameter is the command to launch the language server process.
|
||||
The command must pass appropriate flags to the binary, so that it runs in the stdio mode,
|
||||
as opposed to HTTP, TCP modes supported by some language servers.
|
||||
:param cache_version_raw_document_symbols: the version, for caching, of the raw document symbols coming
|
||||
from this specific language server. This should be incremented by subclasses calling this constructor
|
||||
whenever the format of the raw document symbols changes (typically because the language server
|
||||
improves/fixes its output).
|
||||
"""
|
||||
self._solidlsp_settings = solidlsp_settings
|
||||
lang = self.get_language_enum_instance()
|
||||
@@ -194,14 +256,22 @@ class SolidLanguageServer(ABC):
|
||||
self.open_file_buffers: dict[str, LSPFileBuffer] = {}
|
||||
self.language = Language(language_id)
|
||||
|
||||
# load cache first to prevent any racing conditions due to asyncio stuff
|
||||
self._document_symbols_cache: dict[
|
||||
str, tuple[str, tuple[list[ls_types.UnifiedSymbolInformation], list[ls_types.UnifiedSymbolInformation]]]
|
||||
] = {}
|
||||
"""Maps file paths to a tuple of (file_content_hash, result_of_request_document_symbols)"""
|
||||
self._cache_lock = threading.Lock()
|
||||
self._cache_has_changed: bool = False
|
||||
self.load_cache()
|
||||
# initialise symbol caches
|
||||
self.cache_dir = (
|
||||
Path(self.repository_root_path) / self._solidlsp_settings.project_data_relative_path / self.CACHE_FOLDER_NAME / self.language_id
|
||||
)
|
||||
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
# * raw document symbols cache
|
||||
self._ls_specific_raw_document_symbols_cache_version = cache_version_raw_document_symbols
|
||||
self._raw_document_symbols_cache: dict[str, tuple[str, list[GenericDocumentSymbol]]] = {}
|
||||
"""maps relative file paths to a tuple of (file_content_hash, raw_root_symbols)"""
|
||||
self._raw_document_symbols_cache_is_modified: bool = False
|
||||
self._load_raw_document_symbols_cache()
|
||||
# * high-level document symbols cache
|
||||
self._document_symbols_cache: dict[str, tuple[str, DocumentSymbols]] = {}
|
||||
"""maps relative file paths to a tuple of (file_content_hash, document_symbols)"""
|
||||
self._document_symbols_cache_is_modified: bool = False
|
||||
self._load_document_symbols_cache()
|
||||
|
||||
self.server_started = False
|
||||
self.completions_available = threading.Event()
|
||||
@@ -427,6 +497,20 @@ class SolidLanguageServer(ABC):
|
||||
)
|
||||
del self.open_file_buffers[uri]
|
||||
|
||||
@contextmanager
|
||||
def _open_file_context(self, relative_file_path: str, file_buffer: LSPFileBuffer | None = None) -> Iterator[LSPFileBuffer]:
|
||||
"""
|
||||
Internal context manager to open a file, optionally reusing an existing file buffer.
|
||||
|
||||
:param relative_file_path: the relative path of the file to open.
|
||||
:param file_buffer: an optional existing file buffer to reuse.
|
||||
"""
|
||||
if file_buffer is not None:
|
||||
yield file_buffer
|
||||
else:
|
||||
with self.open_file(relative_file_path) as fb:
|
||||
yield fb
|
||||
|
||||
def insert_text_at_position(self, relative_file_path: str, line: int, column: int, text_to_be_inserted: str) -> ls_types.Position:
|
||||
"""
|
||||
Insert text at the given line and column in the given file and return
|
||||
@@ -836,136 +920,174 @@ 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(
|
||||
self, relative_file_path: str, include_body: bool = False
|
||||
) -> tuple[list[ls_types.UnifiedSymbolInformation], list[ls_types.UnifiedSymbolInformation]]:
|
||||
def _request_document_symbols(self, relative_file_path: str, file_data: LSPFileBuffer | None) -> list[GenericDocumentSymbol]:
|
||||
"""
|
||||
Raise a [textDocument/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. Wait for the response and return the result.
|
||||
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.
|
||||
|
||||
: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.
|
||||
"""
|
||||
|
||||
def get_cached_raw_document_symbols(cache_key: str, fd: LSPFileBuffer) -> list[GenericDocumentSymbol] | None:
|
||||
file_hash_and_result = self._raw_document_symbols_cache.get(cache_key)
|
||||
if file_hash_and_result is not None:
|
||||
file_hash, result = file_hash_and_result
|
||||
if file_hash == fd.content_hash:
|
||||
log.debug("Returning cached raw document symbols for %s", relative_file_path)
|
||||
return result
|
||||
else:
|
||||
log.debug("Document content for %s has changed (raw symbol cache is not up-to-date)", relative_file_path)
|
||||
else:
|
||||
log.debug("No cache hit for raw document symbols symbols in %s", relative_file_path)
|
||||
return None
|
||||
|
||||
def get_raw_document_symbols(fd: LSPFileBuffer) -> list[GenericDocumentSymbol]:
|
||||
# check for cached result
|
||||
cache_key = relative_file_path
|
||||
response = get_cached_raw_document_symbols(cache_key, fd)
|
||||
if response is not None:
|
||||
return response
|
||||
|
||||
# no cached result, query language server
|
||||
self.logger.log(f"Requesting document symbols for {relative_file_path} from the Language Server", logging.DEBUG)
|
||||
response = self.server.send.document_symbol(
|
||||
{"textDocument": {"uri": pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri()}}
|
||||
)
|
||||
|
||||
# update cache
|
||||
self._raw_document_symbols_cache[cache_key] = (file_data.content_hash, response)
|
||||
self._raw_document_symbols_cache_is_modified = True
|
||||
|
||||
return response
|
||||
|
||||
if file_data is not None:
|
||||
return get_raw_document_symbols(file_data)
|
||||
else:
|
||||
with self.open_file(relative_file_path) as opened_file_data:
|
||||
return get_raw_document_symbols(opened_file_data)
|
||||
|
||||
def request_document_symbols(self, relative_file_path: str, file_buffer: LSPFileBuffer | None = None) -> DocumentSymbols:
|
||||
"""
|
||||
Retrieves the collection of symbols in the given file
|
||||
|
||||
:param relative_file_path: The relative path of the file that has the symbols
|
||||
:param include_body: whether to include the body of the symbols in the result.
|
||||
:return: A list of symbols in the file, and a list of root symbols that represent the tree structure of the symbols.
|
||||
All symbols will have a location, a children, and a parent attribute,
|
||||
:param file_buffer: an optional file buffer if the file is already opened.
|
||||
:return: the collection of symbols in the file.
|
||||
All contained symbols will have a location, children, and a parent attribute,
|
||||
where the parent attribute is None for root symbols.
|
||||
Note that this is slightly different from the call to request_full_symbol_tree,
|
||||
where the parent attribute will be the file symbol which in turn may have a package symbol as parent.
|
||||
If you need a symbol tree that contains file symbols as well, you should use `request_full_symbol_tree` instead.
|
||||
"""
|
||||
# TODO: it's kinda dumb to not use the cache if include_body is False after include_body was True once
|
||||
# Should be fixed in the future, it's a small performance optimization
|
||||
cache_key = f"{relative_file_path}-{include_body}"
|
||||
with self.open_file(relative_file_path) as file_data:
|
||||
with self._cache_lock:
|
||||
file_hash_and_result = self._document_symbols_cache.get(cache_key)
|
||||
if file_hash_and_result is not None:
|
||||
file_hash, result = file_hash_and_result
|
||||
if file_hash == file_data.content_hash:
|
||||
self.logger.log(f"Returning cached document symbols for {relative_file_path}", logging.DEBUG)
|
||||
return result
|
||||
else:
|
||||
self.logger.log(f"Content for {relative_file_path} has changed. Will overwrite in-memory cache", logging.DEBUG)
|
||||
with self._open_file_context(relative_file_path, file_buffer) as file_data:
|
||||
# check if the desired result is cached
|
||||
cache_key = relative_file_path
|
||||
file_hash_and_result = self._document_symbols_cache.get(cache_key)
|
||||
if file_hash_and_result is not None:
|
||||
file_hash, document_symbols = file_hash_and_result
|
||||
if file_hash == file_data.content_hash:
|
||||
log.debug("Returning cached document symbols for %s", relative_file_path)
|
||||
return document_symbols
|
||||
else:
|
||||
self.logger.log(f"No cache hit for symbols with {include_body=} in {relative_file_path}", logging.DEBUG)
|
||||
log.debug("Cached document symbol content for %s has changed", relative_file_path)
|
||||
else:
|
||||
log.debug("No cache hit for document symbols in %s", relative_file_path)
|
||||
|
||||
self.logger.log(f"Requesting document symbols for {relative_file_path} from the Language Server", logging.DEBUG)
|
||||
response = self.server.send.document_symbol(
|
||||
{"textDocument": {"uri": pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri()}}
|
||||
)
|
||||
if response is None:
|
||||
self.logger.log(
|
||||
# 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",
|
||||
logging.WARNING,
|
||||
)
|
||||
return [], []
|
||||
assert isinstance(response, list), f"Unexpected response from Language Server: {response}"
|
||||
self.logger.log(
|
||||
f"Received {len(response)} document symbols for {relative_file_path} from the Language Server",
|
||||
logging.DEBUG,
|
||||
)
|
||||
return DocumentSymbols([])
|
||||
|
||||
def turn_item_into_symbol_with_children(item: GenericDocumentSymbol):
|
||||
item = cast(ls_types.UnifiedSymbolInformation, item)
|
||||
absolute_path = os.path.join(self.repository_root_path, relative_file_path)
|
||||
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)
|
||||
|
||||
# handle missing entries in location
|
||||
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
|
||||
if include_body:
|
||||
item["body"] = self.retrieve_symbol_body(item)
|
||||
# handle missing selectionRange
|
||||
if "selectionRange" not in item:
|
||||
if "range" in item:
|
||||
item["selectionRange"] = item["range"]
|
||||
else:
|
||||
item["selectionRange"] = item["location"]["range"]
|
||||
children = item.get(LSPConstants.CHILDREN, [])
|
||||
for child in children:
|
||||
child["parent"] = item
|
||||
item[LSPConstants.CHILDREN] = children
|
||||
file_lines = file_data.split_lines()
|
||||
|
||||
flat_all_symbol_list: list[ls_types.UnifiedSymbolInformation] = []
|
||||
root_nodes: list[ls_types.UnifiedSymbolInformation] = []
|
||||
for root_item in response:
|
||||
if "range" not in root_item and "location" not in root_item:
|
||||
if root_item["kind"] in [SymbolKind.File, SymbolKind.Module]:
|
||||
...
|
||||
def convert_to_unified_symbol(original_symbol_dict: GenericDocumentSymbol) -> 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).
|
||||
|
||||
# mutation is more convenient than creating a new dict,
|
||||
# so we cast and rename the var after the mutating call to turn_item_into_symbol_with_children
|
||||
# which turned and item into a "symbol"
|
||||
turn_item_into_symbol_with_children(root_item)
|
||||
root_symbol = cast(ls_types.UnifiedSymbolInformation, root_item)
|
||||
root_symbol["parent"] = None
|
||||
: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)
|
||||
|
||||
root_nodes.append(root_symbol)
|
||||
assert isinstance(root_symbol, dict)
|
||||
assert LSPConstants.NAME in root_symbol
|
||||
assert LSPConstants.KIND in root_symbol
|
||||
# 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
|
||||
|
||||
if LSPConstants.CHILDREN in root_symbol:
|
||||
# TODO: l_tree should be a list of TreeRepr. Define the following function to return TreeRepr as well
|
||||
if "body" not in item:
|
||||
item["body"] = self.retrieve_symbol_body(item, file_lines=file_lines)
|
||||
|
||||
def visit_tree_nodes_and_build_tree_repr(node: GenericDocumentSymbol) -> list[ls_types.UnifiedSymbolInformation]:
|
||||
node = cast(ls_types.UnifiedSymbolInformation, node)
|
||||
l: list[ls_types.UnifiedSymbolInformation] = []
|
||||
turn_item_into_symbol_with_children(node)
|
||||
assert LSPConstants.CHILDREN in node
|
||||
children = node[LSPConstants.CHILDREN]
|
||||
l.append(node)
|
||||
for child in children:
|
||||
l.extend(visit_tree_nodes_and_build_tree_repr(child))
|
||||
return l
|
||||
# handle missing selectionRange
|
||||
if "selectionRange" not in item:
|
||||
if "range" in item:
|
||||
item["selectionRange"] = item["range"]
|
||||
else:
|
||||
item["selectionRange"] = item["location"]["range"]
|
||||
|
||||
flat_all_symbol_list.extend(visit_tree_nodes_and_build_tree_repr(root_symbol))
|
||||
else:
|
||||
flat_all_symbol_list.append(ls_types.UnifiedSymbolInformation(**root_symbol))
|
||||
return item
|
||||
|
||||
result = flat_all_symbol_list, root_nodes
|
||||
self.logger.log(f"Caching document symbols for {relative_file_path}", logging.DEBUG)
|
||||
with self._cache_lock:
|
||||
self._document_symbols_cache[cache_key] = (file_data.content_hash, result)
|
||||
self._cache_has_changed = True
|
||||
return result
|
||||
def convert_symbols_with_common_parent(
|
||||
symbols: list[GenericDocumentSymbol], 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.
|
||||
"""
|
||||
total_name_counts = defaultdict(lambda: 0)
|
||||
for symbol in symbols:
|
||||
total_name_counts[symbol["name"]] += 1
|
||||
name_counts = 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)
|
||||
else:
|
||||
usymbol["children"] = []
|
||||
unified_symbols.append(usymbol)
|
||||
return unified_symbols
|
||||
|
||||
def request_full_symbol_tree(
|
||||
self, within_relative_path: str | None = None, include_body: bool = False
|
||||
) -> list[ls_types.UnifiedSymbolInformation]:
|
||||
unified_root_symbols = convert_symbols_with_common_parent(root_symbols, None)
|
||||
document_symbols = DocumentSymbols(unified_root_symbols)
|
||||
|
||||
# update cache
|
||||
log.debug("Updating cached document symbols for %s", relative_file_path)
|
||||
self._document_symbols_cache[cache_key] = (file_data.content_hash, document_symbols)
|
||||
self._document_symbols_cache_is_modified = True
|
||||
|
||||
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.
|
||||
Note: this may be slow the first time it is called, especially if `within_relative_path` is not used to restrict the search.
|
||||
@@ -980,8 +1102,6 @@ class SolidLanguageServer(ABC):
|
||||
:param within_relative_path: pass a relative path to only consider symbols within this path.
|
||||
If a file is passed, only the symbols within this file will be considered.
|
||||
If a directory is passed, all files within this directory will be considered.
|
||||
:param include_body: whether to include the body of the symbols in the result.
|
||||
|
||||
:return: A list of root symbols representing the top-level packages/modules in the project.
|
||||
"""
|
||||
if within_relative_path is not None:
|
||||
@@ -990,13 +1110,10 @@ class SolidLanguageServer(ABC):
|
||||
raise FileNotFoundError(f"File or directory not found: {within_abs_path}")
|
||||
if os.path.isfile(within_abs_path):
|
||||
if self.is_ignored_path(within_relative_path):
|
||||
self.logger.log(
|
||||
f"You passed a file explicitly, but it is ignored. This is probably an error. File: {within_relative_path}",
|
||||
logging.ERROR,
|
||||
)
|
||||
log.error("You passed a file explicitly, but it is ignored. This is probably an error. File: %s", within_relative_path)
|
||||
return []
|
||||
else:
|
||||
_, root_nodes = self.request_document_symbols(within_relative_path, include_body=include_body)
|
||||
root_nodes = self.request_document_symbols(within_relative_path).root_symbols
|
||||
return root_nodes
|
||||
|
||||
# Helper function to recursively process directories
|
||||
@@ -1005,7 +1122,7 @@ class SolidLanguageServer(ABC):
|
||||
abs_dir_path = os.path.realpath(abs_dir_path)
|
||||
|
||||
if self.is_ignored_path(str(Path(abs_dir_path).relative_to(self.repository_root_path))):
|
||||
self.logger.log(f"Skipping directory: {rel_dir_path}\n(because it should be ignored)", logging.DEBUG)
|
||||
log.debug("Skipping directory: %s (because it should be ignored)", rel_dir_path)
|
||||
return []
|
||||
|
||||
result = []
|
||||
@@ -1038,14 +1155,16 @@ class SolidLanguageServer(ABC):
|
||||
)
|
||||
except ValueError as e:
|
||||
# Typically happens when the path is not under the repository root (e.g., symlink pointing outside)
|
||||
self.logger.log(
|
||||
f"Skipping path {contained_dir_or_file_abs_path}; likely outside of the repository root {self.repository_root_path} [cause: {e}]",
|
||||
logging.WARNING,
|
||||
log.warning(
|
||||
"Skipping path %s; likely outside of the repository root %s [cause: %s]",
|
||||
contained_dir_or_file_abs_path,
|
||||
self.repository_root_path,
|
||||
e,
|
||||
)
|
||||
continue
|
||||
|
||||
if self.is_ignored_path(contained_dir_or_file_rel_path):
|
||||
self.logger.log(f"Skipping item: {contained_dir_or_file_rel_path}\n(because it should be ignored)", logging.DEBUG)
|
||||
log.debug("Skipping item: %s (because it should be ignored)", contained_dir_or_file_rel_path)
|
||||
continue
|
||||
|
||||
if os.path.isdir(contained_dir_or_file_abs_path):
|
||||
@@ -1055,28 +1174,28 @@ class SolidLanguageServer(ABC):
|
||||
child["parent"] = package_symbol
|
||||
|
||||
elif os.path.isfile(contained_dir_or_file_abs_path):
|
||||
_, file_root_nodes = self.request_document_symbols(contained_dir_or_file_rel_path, include_body=include_body)
|
||||
with self._open_file_context(contained_dir_or_file_rel_path) as file_data:
|
||||
document_symbols = self.request_document_symbols(contained_dir_or_file_rel_path, file_data)
|
||||
file_root_nodes = document_symbols.root_symbols
|
||||
|
||||
# Create file symbol, link with children
|
||||
file_rel_path = str(Path(contained_dir_or_file_abs_path).resolve().relative_to(self.repository_root_path))
|
||||
with self.open_file(file_rel_path) as file_data:
|
||||
fileRange = self._get_range_from_file_content(file_data.contents)
|
||||
file_symbol = ls_types.UnifiedSymbolInformation( # type: ignore
|
||||
name=os.path.splitext(contained_dir_or_file_name)[0],
|
||||
kind=ls_types.SymbolKind.File,
|
||||
range=fileRange,
|
||||
selectionRange=fileRange,
|
||||
location=ls_types.Location(
|
||||
uri=str(pathlib.Path(contained_dir_or_file_abs_path).as_uri()),
|
||||
range=fileRange,
|
||||
absolutePath=str(contained_dir_or_file_abs_path),
|
||||
relativePath=str(Path(contained_dir_or_file_abs_path).resolve().relative_to(self.repository_root_path)),
|
||||
),
|
||||
children=file_root_nodes,
|
||||
parent=package_symbol,
|
||||
)
|
||||
for child in file_root_nodes:
|
||||
child["parent"] = file_symbol
|
||||
# Create file symbol, link with children
|
||||
file_range = self._get_range_from_file_content(file_data.contents)
|
||||
file_symbol = ls_types.UnifiedSymbolInformation( # type: ignore
|
||||
name=os.path.splitext(contained_dir_or_file_name)[0],
|
||||
kind=ls_types.SymbolKind.File,
|
||||
range=file_range,
|
||||
selectionRange=file_range,
|
||||
location=ls_types.Location(
|
||||
uri=str(pathlib.Path(contained_dir_or_file_abs_path).as_uri()),
|
||||
range=file_range,
|
||||
absolutePath=str(contained_dir_or_file_abs_path),
|
||||
relativePath=str(Path(contained_dir_or_file_abs_path).resolve().relative_to(self.repository_root_path)),
|
||||
),
|
||||
children=file_root_nodes,
|
||||
parent=package_symbol,
|
||||
)
|
||||
for child in file_root_nodes:
|
||||
child["parent"] = file_symbol
|
||||
|
||||
# Link file symbol with package
|
||||
package_symbol["children"].append(file_symbol)
|
||||
@@ -1164,8 +1283,7 @@ class SolidLanguageServer(ABC):
|
||||
"""
|
||||
:return: the top-level symbols in the given file.
|
||||
"""
|
||||
_, document_roots = self.request_document_symbols(relative_file_path)
|
||||
return document_roots
|
||||
return self.request_document_symbols(relative_file_path).root_symbols
|
||||
|
||||
def request_overview(self, within_relative_path: str) -> dict[str, list[UnifiedSymbolInformation]]:
|
||||
"""
|
||||
@@ -1213,7 +1331,12 @@ class SolidLanguageServer(ABC):
|
||||
|
||||
return ls_types.Hover(**response)
|
||||
|
||||
def retrieve_symbol_body(self, symbol: ls_types.UnifiedSymbolInformation | LSPTypes.DocumentSymbol | LSPTypes.SymbolInformation) -> str:
|
||||
def retrieve_symbol_body(
|
||||
self,
|
||||
symbol: ls_types.UnifiedSymbolInformation | LSPTypes.DocumentSymbol | LSPTypes.SymbolInformation,
|
||||
file_lines: list[str] | None = None,
|
||||
file_buffer: LSPFileBuffer | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Load the body of the given symbol. If the body is already contained in the symbol, just return it.
|
||||
"""
|
||||
@@ -1225,9 +1348,10 @@ class SolidLanguageServer(ABC):
|
||||
symbol_start_line = symbol["location"]["range"]["start"]["line"]
|
||||
symbol_end_line = symbol["location"]["range"]["end"]["line"]
|
||||
assert "relativePath" in symbol["location"]
|
||||
symbol_file = self.retrieve_full_file_content(symbol["location"]["relativePath"])
|
||||
symbol_lines = symbol_file.split("\n")
|
||||
symbol_body = "\n".join(symbol_lines[symbol_start_line : symbol_end_line + 1])
|
||||
if file_lines is None:
|
||||
with self._open_file_context(symbol["location"]["relativePath"], file_buffer) as f:
|
||||
file_lines = f.split_lines()
|
||||
symbol_body = "\n".join(file_lines[symbol_start_line : symbol_end_line + 1])
|
||||
|
||||
# remove leading indentation
|
||||
symbol_start_column = symbol["location"]["range"]["start"]["character"]
|
||||
@@ -1302,8 +1426,8 @@ class SolidLanguageServer(ABC):
|
||||
ref_text = file_data.contents.split("\n")[ref_line]
|
||||
if "." in ref_text:
|
||||
containing_symbol_name = ref_text.split(".")[0]
|
||||
all_symbols, _ = self.request_document_symbols(ref_path)
|
||||
for symbol in all_symbols:
|
||||
document_symbols = self.request_document_symbols(ref_path)
|
||||
for symbol in document_symbols.iter_symbols():
|
||||
if symbol["name"] == containing_symbol_name and symbol["kind"] == ls_types.SymbolKind.Variable:
|
||||
containing_symbol = copy(symbol)
|
||||
containing_symbol["location"] = ref
|
||||
@@ -1424,12 +1548,12 @@ class SolidLanguageServer(ABC):
|
||||
)
|
||||
return None
|
||||
|
||||
symbols, _ = self.request_document_symbols(relative_file_path)
|
||||
document_symbols = self.request_document_symbols(relative_file_path)
|
||||
|
||||
# make jedi and pyright api compatible
|
||||
# the former has no location, the later has no range
|
||||
# we will just always add location of the desired format to all symbols
|
||||
for symbol in symbols:
|
||||
for symbol in document_symbols.iter_symbols():
|
||||
if "location" not in symbol:
|
||||
range = symbol["range"]
|
||||
location = ls_types.Location(
|
||||
@@ -1467,10 +1591,10 @@ class SolidLanguageServer(ABC):
|
||||
# Only consider containers that are not one-liners (otherwise we may get imports)
|
||||
candidate_containers = [
|
||||
s
|
||||
for s in symbols
|
||||
for s in document_symbols.iter_symbols()
|
||||
if s["kind"] in container_symbol_kinds and s["location"]["range"]["start"]["line"] != s["location"]["range"]["end"]["line"]
|
||||
]
|
||||
var_containers = [s for s in symbols if s["kind"] == ls_types.SymbolKind.Variable]
|
||||
var_containers = [s for s in document_symbols.iter_symbols() if s["kind"] == ls_types.SymbolKind.Variable]
|
||||
candidate_containers.extend(var_containers)
|
||||
|
||||
if not candidate_containers:
|
||||
@@ -1557,56 +1681,113 @@ class SolidLanguageServer(ABC):
|
||||
|
||||
return defining_symbol
|
||||
|
||||
@property
|
||||
def cache_path(self) -> Path:
|
||||
"""
|
||||
The path to the cache file for the document symbols.
|
||||
"""
|
||||
return (
|
||||
Path(self.repository_root_path)
|
||||
/ self._solidlsp_settings.project_data_relative_path
|
||||
/ self.CACHE_FOLDER_NAME
|
||||
/ self.language_id
|
||||
/ "document_symbols_cache_v23-06-25.pkl"
|
||||
)
|
||||
def _save_raw_document_symbols_cache(self):
|
||||
cache_file = self.cache_dir / self.RAW_DOCUMENT_SYMBOL_CACHE_FILENAME
|
||||
|
||||
def save_cache(self):
|
||||
with self._cache_lock:
|
||||
if not self._cache_has_changed:
|
||||
self.logger.log("No changes to document symbols cache, skipping save", logging.DEBUG)
|
||||
return
|
||||
|
||||
self.logger.log(f"Saving updated document symbols cache to {self.cache_path}", logging.INFO)
|
||||
self.cache_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
with open(self.cache_path, "wb") as f:
|
||||
pickle.dump(self._document_symbols_cache, f)
|
||||
self._cache_has_changed = False
|
||||
except Exception as e:
|
||||
self.logger.log(
|
||||
f"Failed to save document symbols cache to {self.cache_path}: {e}. "
|
||||
"Note: this may have resulted in a corrupted cache file.",
|
||||
logging.ERROR,
|
||||
)
|
||||
|
||||
def load_cache(self):
|
||||
if not self.cache_path.exists():
|
||||
if not self._raw_document_symbols_cache_is_modified:
|
||||
log.debug("No changes to raw document symbols cache, skipping save")
|
||||
return
|
||||
|
||||
with self._cache_lock:
|
||||
self.logger.log(f"Loading document symbols cache from {self.cache_path}", logging.INFO)
|
||||
log.info("Saving updated raw document symbols cache to %s", cache_file)
|
||||
try:
|
||||
save_cache(str(cache_file), self._raw_document_symbols_cache_version(), self._raw_document_symbols_cache)
|
||||
self._raw_document_symbols_cache_is_modified = False
|
||||
except Exception as e:
|
||||
log.error(
|
||||
"Failed to save raw document symbols cache to %s: %s. Note: this may have resulted in a corrupted cache file.",
|
||||
cache_file,
|
||||
e,
|
||||
)
|
||||
|
||||
def _raw_document_symbols_cache_version(self):
|
||||
return (self.RAW_DOCUMENT_SYMBOLS_CACHE_VERSION, self._ls_specific_raw_document_symbols_cache_version)
|
||||
|
||||
def _load_raw_document_symbols_cache(self):
|
||||
cache_file = self.cache_dir / self.RAW_DOCUMENT_SYMBOL_CACHE_FILENAME
|
||||
|
||||
if not cache_file.exists():
|
||||
# check for legacy cache to load to migrate
|
||||
legacy_cache_file = self.cache_dir / self.RAW_DOCUMENT_SYMBOL_CACHE_FILENAME_LEGACY_FALLBACK
|
||||
if legacy_cache_file.exists():
|
||||
try:
|
||||
legacy_cache: dict[
|
||||
str, tuple[str, tuple[list[ls_types.UnifiedSymbolInformation], list[ls_types.UnifiedSymbolInformation]]]
|
||||
] = load_pickle(legacy_cache_file)
|
||||
log.info("Migrating legacy document symbols cache with %d entries", len(legacy_cache))
|
||||
num_symbols_migrated = 0
|
||||
migrated_cache = {}
|
||||
for cache_key, (file_hash, (all_symbols, root_symbols)) in legacy_cache.items():
|
||||
if cache_key.endswith("-True"): # include_body=True
|
||||
new_cache_key = cache_key[:-5]
|
||||
migrated_cache[new_cache_key] = (file_hash, root_symbols)
|
||||
num_symbols_migrated += len(all_symbols)
|
||||
log.info("Migrated %d document symbols from legacy cache", num_symbols_migrated)
|
||||
self._raw_document_symbols_cache = migrated_cache
|
||||
self._raw_document_symbols_cache_is_modified = True
|
||||
self._save_raw_document_symbols_cache()
|
||||
legacy_cache_file.unlink()
|
||||
return
|
||||
except Exception as e:
|
||||
log.error("Error during cache migration: %s", e)
|
||||
return
|
||||
|
||||
# load existing cache (if any)
|
||||
if cache_file.exists():
|
||||
log.info("Loading document symbols cache from %s", cache_file)
|
||||
try:
|
||||
with open(self.cache_path, "rb") as f:
|
||||
self._document_symbols_cache = pickle.load(f)
|
||||
self.logger.log(f"Loaded {len(self._document_symbols_cache)} document symbols from cache.", logging.INFO)
|
||||
saved_cache = load_cache(str(cache_file), self._raw_document_symbols_cache_version())
|
||||
if saved_cache is not None:
|
||||
self._raw_document_symbols_cache = saved_cache
|
||||
self.logger.log(
|
||||
f"Loaded {len(self._raw_document_symbols_cache)} entries from raw document symbols cache.", logging.INFO
|
||||
)
|
||||
except Exception as e:
|
||||
# cache often becomes corrupt, so just skip loading it
|
||||
self.logger.log(
|
||||
f"Failed to load document symbols cache from {self.cache_path}: {e}. Possible cause: the cache file is corrupted. "
|
||||
"Check for any errors related to saving the cache in the logs.",
|
||||
logging.ERROR,
|
||||
# cache can become corrupt, so just skip loading it
|
||||
log.warning(
|
||||
"Failed to load raw document symbols cache from %s (%s); Ignoring cache.",
|
||||
cache_file,
|
||||
e,
|
||||
)
|
||||
|
||||
def _save_document_symbols_cache(self):
|
||||
cache_file = self.cache_dir / self.DOCUMENT_SYMBOL_CACHE_FILENAME
|
||||
|
||||
if not self._document_symbols_cache_is_modified:
|
||||
log.debug("No changes to document symbols cache, skipping save")
|
||||
return
|
||||
|
||||
log.info("Saving updated document symbols cache to %s", cache_file)
|
||||
try:
|
||||
save_cache(str(cache_file), self.DOCUMENT_SYMBOL_CACHE_VERSION, self._document_symbols_cache)
|
||||
self._document_symbols_cache_is_modified = False
|
||||
except Exception as e:
|
||||
log.error(
|
||||
"Failed to save document symbols cache to %s: %s. Note: this may have resulted in a corrupted cache file.",
|
||||
cache_file,
|
||||
e,
|
||||
)
|
||||
|
||||
def _load_document_symbols_cache(self):
|
||||
cache_file = self.cache_dir / self.DOCUMENT_SYMBOL_CACHE_FILENAME
|
||||
if cache_file.exists():
|
||||
log.info("Loading document symbols cache from %s", cache_file)
|
||||
try:
|
||||
saved_cache = load_cache(str(cache_file), self.DOCUMENT_SYMBOL_CACHE_VERSION)
|
||||
if saved_cache is not None:
|
||||
self._document_symbols_cache = saved_cache
|
||||
self.logger.log(f"Loaded {len(self._document_symbols_cache)} entries from document symbols cache.", logging.INFO)
|
||||
except Exception as e:
|
||||
# cache can become corrupt, so just skip loading it
|
||||
log.warning(
|
||||
"Failed to load document symbols cache from %s (%s); Ignoring cache.",
|
||||
cache_file,
|
||||
e,
|
||||
)
|
||||
|
||||
def save_cache(self):
|
||||
self._save_raw_document_symbols_cache()
|
||||
self._save_document_symbols_cache()
|
||||
|
||||
def request_workspace_symbol(self, query: str) -> list[ls_types.UnifiedSymbolInformation] | None:
|
||||
"""
|
||||
Raise a [workspace/symbol](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_symbol) request to the Language Server
|
||||
|
||||
@@ -33,7 +33,7 @@ from solidlsp.lsp_protocol_handler.server import (
|
||||
make_request,
|
||||
make_response,
|
||||
)
|
||||
from solidlsp.util.subprocess_util import subprocess_kwargs
|
||||
from solidlsp.util.subprocess_util import quote_arg, subprocess_kwargs
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -189,7 +189,7 @@ class SolidLanguageServerHandler:
|
||||
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)
|
||||
cmd = " ".join(map(quote_arg, cmd))
|
||||
log.info("Starting language server process via command: %s", self.process_launch_info.cmd)
|
||||
kwargs = subprocess_kwargs()
|
||||
kwargs["start_new_session"] = self.start_independent_lsp_process
|
||||
|
||||
@@ -243,6 +243,14 @@ class UnifiedSymbolInformation(TypedDict):
|
||||
All symbols except the root packages will have a parent.
|
||||
"""
|
||||
|
||||
overload_idx: NotRequired[int]
|
||||
"""
|
||||
The overload index of the symbol, if applicable. If a symbol does not have overloads, this field is omitted.
|
||||
If the symbol is an overloaded function or method (same symbol name with the same parent),
|
||||
this index indicates which overload it is. The index is 0-based.
|
||||
Added for Serena, not part of the LSP.
|
||||
"""
|
||||
|
||||
|
||||
class MarkupKind(Enum):
|
||||
"""Describes the content type that a client supports in various
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import logging
|
||||
from typing import Any, Optional
|
||||
|
||||
from sensai.util.pickle import dump_pickle, load_pickle
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def load_cache(path: str, version: Any) -> Optional[Any]:
|
||||
data = load_pickle(path)
|
||||
if not isinstance(data, dict) or "__cache_version" not in data:
|
||||
log.info("Cache is outdated (expected version %s). Ignoring cache at %s", version, path)
|
||||
return None
|
||||
saved_version = data["__cache_version"]
|
||||
if saved_version != version:
|
||||
log.info("Cache is outdated (expected version %s, got %s). Ignoring cache at %s", version, saved_version, path)
|
||||
return None
|
||||
return data["obj"]
|
||||
|
||||
|
||||
def save_cache(path: str, version: Any, obj: Any) -> None:
|
||||
data = {"__cache_version": version, "obj": obj}
|
||||
dump_pickle(data, path)
|
||||
@@ -11,3 +11,12 @@ def subprocess_kwargs():
|
||||
if platform.system() == "Windows":
|
||||
kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW # type: ignore
|
||||
return kwargs
|
||||
|
||||
|
||||
def quote_arg(arg: str) -> str:
|
||||
"""
|
||||
Adds quotes around an argument if it contains spaces.
|
||||
"""
|
||||
if " " not in arg:
|
||||
return arg
|
||||
return f'"{arg}"'
|
||||
@@ -86,7 +86,7 @@ class TestSerenaAgent:
|
||||
def test_find_symbol(self, serena_agent, symbol_name: str, expected_kind: str, expected_file: str):
|
||||
agent = serena_agent
|
||||
find_symbol_tool = agent.get_tool(FindSymbolTool)
|
||||
result = find_symbol_tool.apply_ex(name_path=symbol_name)
|
||||
result = find_symbol_tool.apply_ex(name_path_pattern=symbol_name)
|
||||
|
||||
symbols = json.loads(result)
|
||||
assert any(
|
||||
@@ -138,7 +138,7 @@ class TestSerenaAgent:
|
||||
|
||||
# Find the symbol location first
|
||||
find_symbol_tool = agent.get_tool(FindSymbolTool)
|
||||
result = find_symbol_tool.apply_ex(name_path=symbol_name, relative_path=def_file)
|
||||
result = find_symbol_tool.apply_ex(name_path_pattern=symbol_name, relative_path=def_file)
|
||||
|
||||
time.sleep(1)
|
||||
symbols = json.loads(result)
|
||||
@@ -233,7 +233,7 @@ class TestSerenaAgent:
|
||||
|
||||
find_symbol_tool = agent.get_tool(FindSymbolTool)
|
||||
result = find_symbol_tool.apply_ex(
|
||||
name_path=name_path,
|
||||
name_path_pattern=name_path,
|
||||
depth=0,
|
||||
relative_path=None,
|
||||
include_body=False,
|
||||
@@ -277,7 +277,7 @@ class TestSerenaAgent:
|
||||
|
||||
find_symbol_tool = agent.get_tool(FindSymbolTool)
|
||||
result = find_symbol_tool.apply_ex(
|
||||
name_path=name_path,
|
||||
name_path_pattern=name_path,
|
||||
depth=0,
|
||||
substring_matching=True,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
|
||||
from src.serena.symbol import LanguageServerSymbol
|
||||
from serena.symbol import NamePathMatcher
|
||||
|
||||
|
||||
class TestSymbolNameMatching:
|
||||
@@ -77,7 +77,7 @@ class TestSymbolNameMatching:
|
||||
)
|
||||
def test_match_simple_name(self, name_path_pattern, symbol_name_path_parts, is_substring_match, expected):
|
||||
"""Tests matching for simple names (no '/' in pattern)."""
|
||||
result = LanguageServerSymbol.match_name_path(name_path_pattern, symbol_name_path_parts, is_substring_match)
|
||||
result = NamePathMatcher(name_path_pattern, is_substring_match).matches_components(symbol_name_path_parts, None)
|
||||
error_msg = self._create_assertion_error_message(name_path_pattern, symbol_name_path_parts, is_substring_match, expected, result)
|
||||
assert result == expected, error_msg
|
||||
|
||||
@@ -157,6 +157,22 @@ class TestSymbolNameMatching:
|
||||
)
|
||||
def test_match_name_path_pattern_path_len_2(self, name_path_pattern, symbol_name_path_parts, is_substring_match, expected):
|
||||
"""Tests matching for qualified names (e.g. 'module/class/func')."""
|
||||
result = LanguageServerSymbol.match_name_path(name_path_pattern, symbol_name_path_parts, is_substring_match)
|
||||
result = NamePathMatcher(name_path_pattern, is_substring_match).matches_components(symbol_name_path_parts, None)
|
||||
error_msg = self._create_assertion_error_message(name_path_pattern, symbol_name_path_parts, is_substring_match, expected, result)
|
||||
assert result == expected, error_msg
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name_path_pattern, symbol_name_path_parts, symbol_overload_idx, expected",
|
||||
[
|
||||
pytest.param("bar/foo", ["bar", "foo"], 0, True, id="R: 'bar/foo' matches ['bar', 'foo'] with overload_index=0"),
|
||||
pytest.param("bar/foo", ["bar", "foo"], 1, True, id="R: 'bar/foo' matches ['bar', 'foo'] with overload_index=1"),
|
||||
pytest.param("bar/foo[0]", ["bar", "foo"], 0, True, id="R: 'bar/foo[0]' matches ['bar', 'foo'] with overload_index=0"),
|
||||
pytest.param("bar/foo[1]", ["bar", "foo"], 0, False, id="R: 'bar/foo[1]' does not match ['bar', 'foo'] with overload_index=0"),
|
||||
],
|
||||
)
|
||||
def test_match_name_path_pattern_with_overload_idx(self, name_path_pattern, symbol_name_path_parts, symbol_overload_idx, expected):
|
||||
"""Tests matching for qualified names (e.g. 'module/class/func')."""
|
||||
matcher = NamePathMatcher(name_path_pattern, False)
|
||||
result = matcher.matches_components(symbol_name_path_parts, symbol_overload_idx)
|
||||
error_msg = self._create_assertion_error_message(name_path_pattern, symbol_name_path_parts, False, expected, result)
|
||||
assert result == expected, error_msg
|
||||
@@ -43,7 +43,7 @@ class TestALLanguageServer:
|
||||
def test_find_table_fields(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test that AL Language Server can find fields within a table."""
|
||||
file_path = os.path.join("src", "Tables", "Customer.Table.al")
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
|
||||
# AL tables should have their fields as child symbols
|
||||
customer_table = None
|
||||
@@ -76,7 +76,7 @@ class TestALLanguageServer:
|
||||
def test_find_procedures(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test that AL Language Server can find procedures in codeunits."""
|
||||
file_path = os.path.join("src", "Codeunits", "CustomerMgt.Codeunit.al")
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
|
||||
# Find the codeunit symbol - AL returns 'Codeunit 50000 CustomerMgt'
|
||||
codeunit_symbol = None
|
||||
@@ -100,7 +100,7 @@ class TestALLanguageServer:
|
||||
"""Test that AL Language Server can find references to symbols."""
|
||||
# Find references to the Customer table from the CustomerMgt codeunit
|
||||
table_file = os.path.join("src", "Tables", "Customer.Table.al")
|
||||
symbols = language_server.request_document_symbols(table_file)
|
||||
symbols = language_server.request_document_symbols(table_file).get_all_symbols_and_roots()
|
||||
|
||||
# Find the Customer table symbol
|
||||
customer_symbol = None
|
||||
|
||||
@@ -25,7 +25,7 @@ class TestBashLanguageServerBasics:
|
||||
def test_bash_request_document_symbols(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test request_document_symbols for bash files."""
|
||||
# Test getting symbols from main.sh
|
||||
all_symbols, _root_symbols = language_server.request_document_symbols("main.sh", include_body=False)
|
||||
all_symbols, _root_symbols = language_server.request_document_symbols("main.sh").get_all_symbols_and_roots()
|
||||
|
||||
# Extract function symbols (LSP Symbol Kind 12)
|
||||
function_symbols = [symbol for symbol in all_symbols if symbol.get("kind") == 12]
|
||||
@@ -41,7 +41,7 @@ class TestBashLanguageServerBasics:
|
||||
def test_bash_request_document_symbols_with_body(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test request_document_symbols with body extraction."""
|
||||
# Test with include_body=True
|
||||
all_symbols, _root_symbols = language_server.request_document_symbols("main.sh", include_body=True)
|
||||
all_symbols, _root_symbols = language_server.request_document_symbols("main.sh").get_all_symbols_and_roots()
|
||||
|
||||
function_symbols = [symbol for symbol in all_symbols if symbol.get("kind") == 12]
|
||||
|
||||
@@ -58,7 +58,7 @@ class TestBashLanguageServerBasics:
|
||||
def test_bash_utils_functions(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test function detection in utils.sh file."""
|
||||
# Test with utils.sh as well
|
||||
utils_all_symbols, _utils_root_symbols = language_server.request_document_symbols("utils.sh", include_body=False)
|
||||
utils_all_symbols, _utils_root_symbols = language_server.request_document_symbols("utils.sh").get_all_symbols_and_roots()
|
||||
|
||||
utils_function_symbols = [symbol for symbol in utils_all_symbols if symbol.get("kind") == 12]
|
||||
utils_function_names = [symbol["name"] for symbol in utils_function_symbols]
|
||||
@@ -84,12 +84,12 @@ class TestBashLanguageServerBasics:
|
||||
def test_bash_function_syntax_patterns(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test that LSP detects different bash function syntax patterns correctly."""
|
||||
# Test main.sh (has both 'function' keyword and traditional syntax)
|
||||
main_all_symbols, _main_root_symbols = language_server.request_document_symbols("main.sh", include_body=False)
|
||||
main_all_symbols, _main_root_symbols = language_server.request_document_symbols("main.sh").get_all_symbols_and_roots()
|
||||
main_functions = [symbol for symbol in main_all_symbols if symbol.get("kind") == 12]
|
||||
main_function_names = [func["name"] for func in main_functions]
|
||||
|
||||
# Test utils.sh (all use 'function' keyword)
|
||||
utils_all_symbols, _utils_root_symbols = language_server.request_document_symbols("utils.sh", include_body=False)
|
||||
utils_all_symbols, _utils_root_symbols = language_server.request_document_symbols("utils.sh").get_all_symbols_and_roots()
|
||||
utils_functions = [symbol for symbol in utils_all_symbols if symbol.get("kind") == 12]
|
||||
utils_function_names = [func["name"] for func in utils_functions]
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ class TestLanguageServerBasics:
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
|
||||
def test_document_symbols(self, language_server: SolidLanguageServer):
|
||||
symbols, _ = language_server.request_document_symbols(CORE_PATH)
|
||||
symbols, _ = language_server.request_document_symbols(CORE_PATH).get_all_symbols_and_roots()
|
||||
|
||||
assert isinstance(symbols, list) and len(symbols) >= 4, "greet, add, multiply, -main functions"
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ class TestCSharpLanguageServer:
|
||||
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)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
|
||||
# Check that we have symbols
|
||||
assert len(symbols) > 0
|
||||
@@ -49,7 +49,7 @@ class TestCSharpLanguageServer:
|
||||
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)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
add_symbol = None
|
||||
# Handle nested symbol structure
|
||||
symbol_list = symbols[0] if symbols and isinstance(symbols[0], list) else symbols
|
||||
@@ -68,7 +68,7 @@ class TestCSharpLanguageServer:
|
||||
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)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
|
||||
# Check that we have symbols
|
||||
assert len(symbols) > 0
|
||||
@@ -93,7 +93,7 @@ class TestCSharpLanguageServer:
|
||||
"""Test finding references to Calculator.Subtract method across files."""
|
||||
# First, find the Subtract method in Program.cs
|
||||
file_path = os.path.join("Program.cs")
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
|
||||
# Flatten the symbols if they're nested
|
||||
symbol_list = symbols[0] if symbols and isinstance(symbols[0], list) else symbols
|
||||
|
||||
@@ -171,7 +171,7 @@ class TestDartLanguageServer:
|
||||
def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test finding references using symbol selection range."""
|
||||
file_path = os.path.join("lib", "main.dart")
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
|
||||
# Handle nested symbol structure - symbols can be nested in lists
|
||||
symbol_list = symbols[0] if symbols and isinstance(symbols[0], list) else symbols
|
||||
@@ -281,7 +281,7 @@ class TestDartLanguageServer:
|
||||
def test_request_document_symbols(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test getting document symbols from a Dart file."""
|
||||
file_path = os.path.join("lib", "main.dart")
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
|
||||
# Check that we have symbols
|
||||
assert len(symbols) > 0
|
||||
@@ -308,7 +308,7 @@ class TestDartLanguageServer:
|
||||
def test_request_referencing_symbols_comprehensive(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test comprehensive referencing symbols functionality."""
|
||||
file_path = os.path.join("lib", "main.dart")
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
|
||||
# Handle nested symbol structure
|
||||
symbol_list = symbols[0] if symbols and isinstance(symbols[0], list) else symbols
|
||||
@@ -339,7 +339,7 @@ class TestDartLanguageServer:
|
||||
helper_file_path = os.path.join("lib", "helper.dart")
|
||||
|
||||
# Test finding references to subtract function from helper.dart in main.dart
|
||||
helper_symbols = language_server.request_document_symbols(helper_file_path)
|
||||
helper_symbols = language_server.request_document_symbols(helper_file_path).get_all_symbols_and_roots()
|
||||
symbol_list = helper_symbols[0] if helper_symbols and isinstance(helper_symbols[0], list) else helper_symbols
|
||||
|
||||
subtract_symbol = next((s for s in symbol_list if s.get("name") == "subtract"), None)
|
||||
|
||||
@@ -25,7 +25,7 @@ class TestElixirBasic:
|
||||
def test_request_references_function_definition(self, language_server: SolidLanguageServer):
|
||||
"""Test finding references to a function definition."""
|
||||
file_path = os.path.join("lib", "models.ex")
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
|
||||
# Find the User module's 'new' function
|
||||
user_new_symbol = None
|
||||
@@ -54,7 +54,7 @@ class TestElixirBasic:
|
||||
def test_request_references_create_user_function(self, language_server: SolidLanguageServer):
|
||||
"""Test finding references to create_user function."""
|
||||
file_path = os.path.join("lib", "services.ex")
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
|
||||
# Find the UserService module's 'create_user' function
|
||||
create_user_symbol = None
|
||||
@@ -79,7 +79,7 @@ class TestElixirBasic:
|
||||
def test_request_referencing_symbols_function(self, language_server: SolidLanguageServer):
|
||||
"""Test finding symbols that reference a specific function."""
|
||||
file_path = os.path.join("lib", "models.ex")
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
|
||||
# Find the User module's 'new' function
|
||||
user_new_symbol = None
|
||||
@@ -103,10 +103,10 @@ class TestElixirBasic:
|
||||
def test_timeout_enumeration_bug(self, language_server: SolidLanguageServer):
|
||||
"""Test that enumeration doesn't timeout (regression test)."""
|
||||
# This should complete without timing out
|
||||
symbols = language_server.request_document_symbols("lib/models.ex")
|
||||
symbols = language_server.request_document_symbols("lib/models.ex").get_all_symbols_and_roots()
|
||||
assert symbols is not None
|
||||
|
||||
# Test multiple symbol requests in succession
|
||||
for _ in range(3):
|
||||
symbols = language_server.request_document_symbols("lib/services.ex")
|
||||
symbols = language_server.request_document_symbols("lib/services.ex").get_all_symbols_and_roots()
|
||||
assert symbols is not None
|
||||
@@ -46,7 +46,7 @@ def test_find_references_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer):
|
||||
definition_file = "lib/models.ex"
|
||||
|
||||
# Find the User struct definition
|
||||
symbols = ls_with_ignored_dirs.request_document_symbols(definition_file)
|
||||
symbols = ls_with_ignored_dirs.request_document_symbols(definition_file).get_all_symbols_and_roots()
|
||||
user_symbol = None
|
||||
for symbol_group in symbols:
|
||||
user_symbol = next((s for s in symbol_group if "User" in s.get("name", "")), None)
|
||||
@@ -87,7 +87,7 @@ def test_refs_and_symbols_with_glob_patterns(repo_path: Path) -> None:
|
||||
definition_file = "lib/models.ex"
|
||||
|
||||
# Find the User struct definition
|
||||
symbols = ls.request_document_symbols(definition_file)
|
||||
symbols = ls.request_document_symbols(definition_file).get_all_symbols_and_roots()
|
||||
user_symbol = None
|
||||
for symbol_group in symbols:
|
||||
user_symbol = next((s for s in symbol_group if "User" in s.get("name", "")), None)
|
||||
|
||||
@@ -72,7 +72,7 @@ class TestElixirIntegration:
|
||||
def test_module_hierarchy_understanding(self, language_server: SolidLanguageServer):
|
||||
"""Test that the language server understands Elixir module hierarchy."""
|
||||
models_file = os.path.join("lib", "models.ex")
|
||||
symbols = language_server.request_document_symbols(models_file)
|
||||
symbols = language_server.request_document_symbols(models_file).get_all_symbols_and_roots()
|
||||
|
||||
if symbols:
|
||||
# Flatten symbol structure
|
||||
|
||||
@@ -120,7 +120,7 @@ class TestElixirLanguageServerSymbols:
|
||||
# Test referencing symbols for User struct
|
||||
file_path = os.path.join("lib", "models.ex")
|
||||
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
user_symbol = None
|
||||
for symbol_group in symbols:
|
||||
user_symbol = next((s for s in symbol_group if "User" in s.get("name", "")), None)
|
||||
|
||||
@@ -20,7 +20,7 @@ class TestElmLanguageServer:
|
||||
@pytest.mark.parametrize("language_server", [Language.ELM], indirect=True)
|
||||
def test_find_references_within_file(self, language_server: SolidLanguageServer) -> None:
|
||||
file_path = os.path.join("Main.elm")
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
greet_symbol = None
|
||||
for sym in symbols[0]:
|
||||
if sym.get("name") == "greet":
|
||||
@@ -35,7 +35,7 @@ class TestElmLanguageServer:
|
||||
def test_find_references_across_files(self, language_server: SolidLanguageServer) -> None:
|
||||
# Test formatMessage function which is defined in Utils.elm and used in Main.elm
|
||||
utils_path = os.path.join("Utils.elm")
|
||||
symbols = language_server.request_document_symbols(utils_path)
|
||||
symbols = language_server.request_document_symbols(utils_path).get_all_symbols_and_roots()
|
||||
formatMessage_symbol = None
|
||||
for sym in symbols[0]:
|
||||
if sym.get("name") == "formatMessage":
|
||||
|
||||
@@ -29,7 +29,7 @@ class TestErlangLanguageServerBasics:
|
||||
"""Test document symbols retrieval for Erlang files."""
|
||||
try:
|
||||
file_path = "hello.erl"
|
||||
symbols_tuple = language_server.request_document_symbols(file_path)
|
||||
symbols_tuple = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
assert isinstance(symbols_tuple, tuple)
|
||||
assert len(symbols_tuple) == 2
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ def test_find_references_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer):
|
||||
definition_file = "include/records.hrl"
|
||||
|
||||
# Find the user record definition
|
||||
symbols = ls_with_ignored_dirs.request_document_symbols(definition_file)
|
||||
symbols = ls_with_ignored_dirs.request_document_symbols(definition_file).get_all_symbols_and_roots()
|
||||
user_symbol = None
|
||||
for symbol_group in symbols:
|
||||
user_symbol = next((s for s in symbol_group if "user" in s.get("name", "").lower()), None)
|
||||
@@ -107,7 +107,7 @@ def test_refs_and_symbols_with_glob_patterns(repo_path: Path) -> None:
|
||||
definition_file = "include/records.hrl"
|
||||
|
||||
# Find the user record definition
|
||||
symbols = ls.request_document_symbols(definition_file)
|
||||
symbols = ls.request_document_symbols(definition_file).get_all_symbols_and_roots()
|
||||
user_symbol = None
|
||||
for symbol_group in symbols:
|
||||
user_symbol = next((s for s in symbol_group if "user" in s.get("name", "").lower()), None)
|
||||
@@ -203,7 +203,7 @@ def test_document_symbols_ignores_dirs(ls_with_ignored_dirs: SolidLanguageServer
|
||||
# Try to get symbols from a file in ignored directory (should not find it)
|
||||
try:
|
||||
ignored_file = "ignored_dir/ignored_module.erl"
|
||||
symbols = ls_with_ignored_dirs.request_document_symbols(ignored_file)
|
||||
symbols = ls_with_ignored_dirs.request_document_symbols(ignored_file).get_all_symbols_and_roots()
|
||||
# If we get here, the file was found - symbols should be empty or None
|
||||
if symbols:
|
||||
assert len(symbols) == 0, "Should not find symbols in ignored directory"
|
||||
|
||||
@@ -126,7 +126,7 @@ class TestErlangLanguageServerSymbols:
|
||||
# Test referencing symbols for user record
|
||||
file_path = os.path.join("include", "records.hrl")
|
||||
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
user_symbol = None
|
||||
for symbol_group in symbols:
|
||||
user_symbol = next((s for s in symbol_group if "user" in s.get("name", "")), None)
|
||||
@@ -156,7 +156,7 @@ class TestErlangLanguageServerSymbols:
|
||||
# Test referencing symbols for create_user function
|
||||
file_path = os.path.join("src", "models.erl")
|
||||
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
create_user_symbol = None
|
||||
for symbol_group in symbols:
|
||||
create_user_symbol = next((s for s in symbol_group if "create_user" in s.get("name", "")), None)
|
||||
@@ -418,7 +418,7 @@ class TestErlangLanguageServerSymbols:
|
||||
# Test that we can find references to models module functions in services.erl
|
||||
file_path = os.path.join("src", "models.erl")
|
||||
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
create_user_symbol = None
|
||||
for symbol_group in symbols:
|
||||
create_user_symbol = next((s for s in symbol_group if "create_user" in s.get("name", "")), None)
|
||||
|
||||
@@ -41,12 +41,12 @@ class TestFortranLanguageServer:
|
||||
def test_request_document_symbols(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test that document symbols can be retrieved from Fortran files."""
|
||||
# Test main.f90 - should have a program symbol
|
||||
main_symbols, _ = language_server.request_document_symbols("main.f90")
|
||||
main_symbols, _ = language_server.request_document_symbols("main.f90").get_all_symbols_and_roots()
|
||||
program_names = [s.get("name") for s in main_symbols]
|
||||
assert "test_program" in program_names, f"Program 'test_program' not found in main.f90. Found: {program_names}"
|
||||
|
||||
# Test modules/math_utils.f90 - should have module and function symbols
|
||||
module_symbols, _ = language_server.request_document_symbols("modules/math_utils.f90")
|
||||
module_symbols, _ = language_server.request_document_symbols("modules/math_utils.f90").get_all_symbols_and_roots()
|
||||
all_names = [s.get("name") for s in module_symbols]
|
||||
assert "math_utils" in all_names, f"Module 'math_utils' not found. Found: {all_names}"
|
||||
assert "add_numbers" in all_names, f"Function 'add_numbers' not found. Found: {all_names}"
|
||||
@@ -60,7 +60,7 @@ class TestFortranLanguageServer:
|
||||
This tests the LSP textDocument/references capability.
|
||||
"""
|
||||
file_path = "modules/math_utils.f90"
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
|
||||
# Find the add_numbers function
|
||||
add_numbers_symbol = None
|
||||
@@ -125,7 +125,7 @@ class TestFortranLanguageServer:
|
||||
"""
|
||||
# Get the add_numbers function symbol from math_utils.f90
|
||||
file_path = "modules/math_utils.f90"
|
||||
symbols, _ = language_server.request_document_symbols(file_path)
|
||||
symbols, _ = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
|
||||
# Find the add_numbers function
|
||||
add_numbers_symbol = None
|
||||
@@ -230,7 +230,7 @@ class TestFortranLanguageServer:
|
||||
fortls returns these as SymbolKind.Class (11) for types and SymbolKind.Interface (5) for interfaces.
|
||||
"""
|
||||
file_path = "modules/geometry.f90"
|
||||
symbols, _ = language_server.request_document_symbols(file_path)
|
||||
symbols, _ = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
|
||||
# Find type and interface symbols
|
||||
type_names = []
|
||||
|
||||
@@ -19,7 +19,7 @@ class TestGoLanguageServer:
|
||||
@pytest.mark.parametrize("language_server", [Language.GO], indirect=True)
|
||||
def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None:
|
||||
file_path = os.path.join("main.go")
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
helper_symbol = None
|
||||
for sym in symbols[0]:
|
||||
if sym.get("name") == "Helper":
|
||||
|
||||
@@ -34,7 +34,7 @@ class TestHaskellLanguageServer:
|
||||
- All exported functions with correct names
|
||||
- Module structure
|
||||
"""
|
||||
all_symbols, _ = language_server.request_document_symbols("src/Calculator.hs")
|
||||
all_symbols, _ = language_server.request_document_symbols("src/Calculator.hs").get_all_symbols_and_roots()
|
||||
symbol_names = {s["name"] for s in all_symbols}
|
||||
|
||||
# Verify exact set of expected top-level symbols
|
||||
@@ -71,7 +71,7 @@ class TestHaskellLanguageServer:
|
||||
Verifies Serena identifies all helper functions that are imported
|
||||
and used by Calculator module.
|
||||
"""
|
||||
all_symbols, _ = language_server.request_document_symbols("src/Helper.hs")
|
||||
all_symbols, _ = language_server.request_document_symbols("src/Helper.hs").get_all_symbols_and_roots()
|
||||
symbol_names = {s["name"] for s in all_symbols}
|
||||
|
||||
# Verify expected helper functions (module name may also appear)
|
||||
@@ -97,7 +97,7 @@ class TestHaskellLanguageServer:
|
||||
|
||||
Verifies Serena can identify cross-module dependencies.
|
||||
"""
|
||||
all_symbols, _ = language_server.request_document_symbols("app/Main.hs")
|
||||
all_symbols, _ = language_server.request_document_symbols("app/Main.hs").get_all_symbols_and_roots()
|
||||
symbol_names = {s["name"] for s in all_symbols}
|
||||
|
||||
# Main.hs should have the main function
|
||||
|
||||
@@ -25,7 +25,7 @@ class TestJavaLanguageServer:
|
||||
|
||||
# Dynamically determine the correct line/column for the 'Model' class name
|
||||
file_path = os.path.join("src", "main", "java", "test_repo", "Model.java")
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
model_symbol = None
|
||||
for sym in symbols[0]:
|
||||
if sym.get("name") == "Model" and sym.get("kind") == 5: # 5 = Class
|
||||
|
||||
@@ -11,7 +11,7 @@ class TestJuliaLanguageServer:
|
||||
"""
|
||||
Test if we can find the top-level symbols in the main.jl file.
|
||||
"""
|
||||
all_symbols, _ = language_server.request_document_symbols("main.jl")
|
||||
all_symbols, _ = language_server.request_document_symbols("main.jl").get_all_symbols_and_roots()
|
||||
symbol_names = {s["name"] for s in all_symbols}
|
||||
assert "calculate_sum" in symbol_names
|
||||
assert "main" in symbol_names
|
||||
|
||||
@@ -25,7 +25,7 @@ class TestKotlinLanguageServer:
|
||||
|
||||
# Dynamically determine the correct line/column for the 'Model' class name
|
||||
file_path = os.path.join("src", "main", "kotlin", "test_repo", "Model.kt")
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
model_symbol = None
|
||||
for sym in symbols[0]:
|
||||
print(sym)
|
||||
|
||||
@@ -19,7 +19,7 @@ class TestLuaLanguageServer:
|
||||
@pytest.mark.parametrize("language_server", [Language.LUA], indirect=True)
|
||||
def test_find_symbols_in_calculator(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test finding specific functions in calculator.lua."""
|
||||
symbols = language_server.request_document_symbols("src/calculator.lua")
|
||||
symbols = language_server.request_document_symbols("src/calculator.lua").get_all_symbols_and_roots()
|
||||
|
||||
assert symbols is not None
|
||||
assert len(symbols) > 0
|
||||
@@ -49,7 +49,7 @@ class TestLuaLanguageServer:
|
||||
@pytest.mark.parametrize("language_server", [Language.LUA], indirect=True)
|
||||
def test_find_symbols_in_utils(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test finding specific functions in utils.lua."""
|
||||
symbols = language_server.request_document_symbols("src/utils.lua")
|
||||
symbols = language_server.request_document_symbols("src/utils.lua").get_all_symbols_and_roots()
|
||||
|
||||
assert symbols is not None
|
||||
assert len(symbols) > 0
|
||||
@@ -84,7 +84,7 @@ class TestLuaLanguageServer:
|
||||
@pytest.mark.parametrize("language_server", [Language.LUA], indirect=True)
|
||||
def test_find_symbols_in_main(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test finding functions in main.lua."""
|
||||
symbols = language_server.request_document_symbols("main.lua")
|
||||
symbols = language_server.request_document_symbols("main.lua").get_all_symbols_and_roots()
|
||||
|
||||
assert symbols is not None
|
||||
assert len(symbols) > 0
|
||||
@@ -107,7 +107,7 @@ class TestLuaLanguageServer:
|
||||
@pytest.mark.parametrize("language_server", [Language.LUA], indirect=True)
|
||||
def test_cross_file_references_calculator_add(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test finding cross-file references to calculator.add function."""
|
||||
symbols = language_server.request_document_symbols("src/calculator.lua")
|
||||
symbols = language_server.request_document_symbols("src/calculator.lua").get_all_symbols_and_roots()
|
||||
|
||||
assert symbols is not None
|
||||
symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
|
||||
@@ -163,7 +163,7 @@ class TestLuaLanguageServer:
|
||||
@pytest.mark.parametrize("language_server", [Language.LUA], indirect=True)
|
||||
def test_cross_file_references_utils_trim(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test finding cross-file references to utils.trim function."""
|
||||
symbols = language_server.request_document_symbols("src/utils.lua")
|
||||
symbols = language_server.request_document_symbols("src/utils.lua").get_all_symbols_and_roots()
|
||||
|
||||
assert symbols is not None
|
||||
symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
|
||||
@@ -245,7 +245,7 @@ class TestLuaLanguageServer:
|
||||
def test_references_between_test_and_source(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test finding references from test files to source files."""
|
||||
# Check if test_calculator.lua references calculator module
|
||||
test_symbols = language_server.request_document_symbols("tests/test_calculator.lua")
|
||||
test_symbols = language_server.request_document_symbols("tests/test_calculator.lua").get_all_symbols_and_roots()
|
||||
|
||||
assert test_symbols is not None
|
||||
assert len(test_symbols) > 0
|
||||
|
||||
@@ -25,7 +25,7 @@ class TestMarkdownLanguageServerBasics:
|
||||
def test_markdown_request_document_symbols(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test request_document_symbols for markdown files."""
|
||||
# Test getting symbols from README.md
|
||||
all_symbols, _root_symbols = language_server.request_document_symbols("README.md", include_body=False)
|
||||
all_symbols, _root_symbols = language_server.request_document_symbols("README.md").get_all_symbols_and_roots()
|
||||
|
||||
# Extract heading symbols (LSP Symbol Kind 15 is String, but marksman uses kind 15 for headings)
|
||||
# Note: Different markdown LSPs may use different symbol kinds for headings
|
||||
@@ -38,7 +38,7 @@ class TestMarkdownLanguageServerBasics:
|
||||
@pytest.mark.parametrize("language_server", [Language.MARKDOWN], indirect=True)
|
||||
def test_markdown_request_symbols_from_guide(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test symbol detection in guide.md file."""
|
||||
all_symbols, _root_symbols = language_server.request_document_symbols("guide.md", include_body=False)
|
||||
all_symbols, _root_symbols = language_server.request_document_symbols("guide.md").get_all_symbols_and_roots()
|
||||
|
||||
# At least some headings should be found
|
||||
assert len(all_symbols) > 0, f"Should find headings in guide.md, found {len(all_symbols)}"
|
||||
@@ -46,7 +46,7 @@ class TestMarkdownLanguageServerBasics:
|
||||
@pytest.mark.parametrize("language_server", [Language.MARKDOWN], indirect=True)
|
||||
def test_markdown_request_symbols_from_api(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test symbol detection in api.md file."""
|
||||
all_symbols, _root_symbols = language_server.request_document_symbols("api.md", include_body=False)
|
||||
all_symbols, _root_symbols = language_server.request_document_symbols("api.md").get_all_symbols_and_roots()
|
||||
|
||||
# Should detect headings from api.md
|
||||
assert len(all_symbols) > 0, f"Should find headings in api.md, found {len(all_symbols)}"
|
||||
@@ -55,7 +55,7 @@ class TestMarkdownLanguageServerBasics:
|
||||
def test_markdown_request_document_symbols_with_body(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test request_document_symbols with body extraction."""
|
||||
# Test with include_body=True
|
||||
all_symbols, _root_symbols = language_server.request_document_symbols("README.md", include_body=True)
|
||||
all_symbols, _root_symbols = language_server.request_document_symbols("README.md").get_all_symbols_and_roots()
|
||||
|
||||
# Should have found some symbols
|
||||
assert len(all_symbols) > 0, "Should find symbols in README.md"
|
||||
|
||||
@@ -22,7 +22,7 @@ class TestNixLanguageServer:
|
||||
@pytest.mark.parametrize("language_server", [Language.NIX], indirect=True)
|
||||
def test_find_symbols_in_default_nix(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test finding specific symbols in default.nix."""
|
||||
symbols = language_server.request_document_symbols("default.nix")
|
||||
symbols = language_server.request_document_symbols("default.nix").get_all_symbols_and_roots()
|
||||
|
||||
assert symbols is not None
|
||||
assert len(symbols) > 0
|
||||
@@ -42,7 +42,7 @@ class TestNixLanguageServer:
|
||||
@pytest.mark.parametrize("language_server", [Language.NIX], indirect=True)
|
||||
def test_find_symbols_in_utils(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test finding symbols in lib/utils.nix."""
|
||||
symbols = language_server.request_document_symbols("lib/utils.nix")
|
||||
symbols = language_server.request_document_symbols("lib/utils.nix").get_all_symbols_and_roots()
|
||||
|
||||
assert symbols is not None
|
||||
assert len(symbols) > 0
|
||||
@@ -58,7 +58,7 @@ class TestNixLanguageServer:
|
||||
@pytest.mark.parametrize("language_server", [Language.NIX], indirect=True)
|
||||
def test_find_symbols_in_flake(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test finding symbols in flake.nix."""
|
||||
symbols = language_server.request_document_symbols("flake.nix")
|
||||
symbols = language_server.request_document_symbols("flake.nix").get_all_symbols_and_roots()
|
||||
|
||||
assert symbols is not None
|
||||
assert len(symbols) > 0
|
||||
@@ -72,7 +72,7 @@ class TestNixLanguageServer:
|
||||
@pytest.mark.parametrize("language_server", [Language.NIX], indirect=True)
|
||||
def test_find_symbols_in_module(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test finding symbols in a NixOS module."""
|
||||
symbols = language_server.request_document_symbols("modules/example.nix")
|
||||
symbols = language_server.request_document_symbols("modules/example.nix").get_all_symbols_and_roots()
|
||||
|
||||
assert symbols is not None
|
||||
assert len(symbols) > 0
|
||||
@@ -86,7 +86,7 @@ class TestNixLanguageServer:
|
||||
@pytest.mark.parametrize("language_server", [Language.NIX], indirect=True)
|
||||
def test_find_references_within_file(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test finding references within the same file."""
|
||||
symbols = language_server.request_document_symbols("default.nix")
|
||||
symbols = language_server.request_document_symbols("default.nix").get_all_symbols_and_roots()
|
||||
|
||||
assert symbols is not None
|
||||
symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
|
||||
@@ -154,7 +154,7 @@ class TestNixLanguageServer:
|
||||
def test_verify_imports_exist(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Verify that our test files have proper imports set up."""
|
||||
# Verify that default.nix imports utils from lib/utils.nix
|
||||
symbols = language_server.request_document_symbols("default.nix")
|
||||
symbols = language_server.request_document_symbols("default.nix").get_all_symbols_and_roots()
|
||||
|
||||
assert symbols is not None
|
||||
symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
|
||||
@@ -164,7 +164,7 @@ class TestNixLanguageServer:
|
||||
assert "makeGreeting" in symbol_names, "makeGreeting should be found in default.nix"
|
||||
|
||||
# Verify lib/utils.nix has the expected structure
|
||||
utils_symbols = language_server.request_document_symbols("lib/utils.nix")
|
||||
utils_symbols = language_server.request_document_symbols("lib/utils.nix").get_all_symbols_and_roots()
|
||||
assert utils_symbols is not None
|
||||
utils_list = utils_symbols[0] if isinstance(utils_symbols, tuple) else utils_symbols
|
||||
utils_names = {sym.get("name") for sym in utils_list if isinstance(sym, dict)}
|
||||
|
||||
@@ -31,7 +31,7 @@ class TestPerlLanguageServer:
|
||||
def test_document_symbols(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test that document symbols are correctly identified."""
|
||||
# Request document symbols
|
||||
all_symbols, _ = language_server.request_document_symbols("main.pl", include_body=False)
|
||||
all_symbols, _ = language_server.request_document_symbols("main.pl").get_all_symbols_and_roots()
|
||||
|
||||
assert all_symbols, "Expected to find symbols in main.pl"
|
||||
assert len(all_symbols) > 0, "Expected at least one symbol"
|
||||
|
||||
@@ -26,7 +26,7 @@ class TestLanguageServerBasics:
|
||||
file_path = os.path.join("test_repo", "models.py")
|
||||
# Line 31 contains the User class definition
|
||||
# Use selectionRange only
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
user_symbol = next((s for s in symbols[0] if s.get("name") == "User"), None)
|
||||
if not user_symbol or "selectionRange" not in user_symbol:
|
||||
raise AssertionError("User symbol or its selectionRange not found")
|
||||
@@ -41,7 +41,7 @@ class TestLanguageServerBasics:
|
||||
file_path = os.path.join("test_repo", "models.py")
|
||||
# Line 56 contains the Item class definition
|
||||
# Use selectionRange only
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
item_symbol = next((s for s in symbols[0] if s.get("name") == "Item"), None)
|
||||
if not item_symbol or "selectionRange" not in item_symbol:
|
||||
raise AssertionError("Item symbol or its selectionRange not found")
|
||||
@@ -57,7 +57,7 @@ class TestLanguageServerBasics:
|
||||
file_path = os.path.join("test_repo", "services.py")
|
||||
# Line 24 contains the get_user method with id parameter
|
||||
# Use selectionRange only
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
get_user_symbol = next((s for s in symbols[0] if s.get("name") == "get_user"), None)
|
||||
if not get_user_symbol or "selectionRange" not in get_user_symbol:
|
||||
raise AssertionError("get_user symbol or its selectionRange not found")
|
||||
@@ -71,7 +71,7 @@ class TestLanguageServerBasics:
|
||||
file_path = os.path.join("test_repo", "services.py")
|
||||
# Line 15 contains the create_user method definition
|
||||
# Use selectionRange only
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
create_user_symbol = next((s for s in symbols[0] if s.get("name") == "create_user"), None)
|
||||
if not create_user_symbol or "selectionRange" not in create_user_symbol:
|
||||
raise AssertionError("create_user symbol or its selectionRange not found")
|
||||
|
||||
@@ -107,7 +107,7 @@ class TestLanguageServerSymbols:
|
||||
# Test referencing symbols for create_user function
|
||||
file_path = os.path.join("test_repo", "services.py")
|
||||
# Line 15 contains the create_user function definition
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
create_user_symbol = next((s for s in symbols[0] if s.get("name") == "create_user"), None)
|
||||
if not create_user_symbol or "selectionRange" not in create_user_symbol:
|
||||
raise AssertionError("create_user symbol or its selectionRange not found")
|
||||
@@ -131,7 +131,7 @@ class TestLanguageServerSymbols:
|
||||
# Test referencing symbols for User class
|
||||
file_path = os.path.join("test_repo", "models.py")
|
||||
# Line 31 contains the User class definition
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
user_symbol = next((s for s in symbols[0] if s.get("name") == "User"), None)
|
||||
if not user_symbol or "selectionRange" not in user_symbol:
|
||||
raise AssertionError("User symbol or its selectionRange not found")
|
||||
@@ -152,7 +152,7 @@ class TestLanguageServerSymbols:
|
||||
# Test referencing symbols for id parameter in get_user
|
||||
file_path = os.path.join("test_repo", "services.py")
|
||||
# Line 24 contains the get_user method with id parameter
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
get_user_symbol = next((s for s in symbols[0] if s.get("name") == "get_user"), None)
|
||||
if not get_user_symbol or "selectionRange" not in get_user_symbol:
|
||||
raise AssertionError("get_user symbol or its selectionRange not found")
|
||||
@@ -390,7 +390,9 @@ class TestLanguageServerSymbols:
|
||||
if "location" in user_management_node and "relativePath" in user_management_node["location"]:
|
||||
user_management_rel_path = user_management_node["location"]["relativePath"]
|
||||
assert user_management_rel_path == os.path.join("examples", "user_management.py")
|
||||
_, user_management_roots = language_server.request_document_symbols(os.path.join("examples", "user_management.py"))
|
||||
_, user_management_roots = language_server.request_document_symbols(
|
||||
os.path.join("examples", "user_management.py")
|
||||
).get_all_symbols_and_roots()
|
||||
assert user_management_roots == user_management_node["children"]
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
|
||||
@@ -411,7 +413,9 @@ class TestLanguageServerSymbols:
|
||||
if "location" in user_management_node and "relativePath" in user_management_node["location"]:
|
||||
user_management_rel_path = user_management_node["location"]["relativePath"]
|
||||
assert user_management_rel_path == os.path.join("examples", "user_management.py")
|
||||
_, user_management_roots = language_server.request_document_symbols(os.path.join("examples", "user_management.py"))
|
||||
_, user_management_roots = language_server.request_document_symbols(
|
||||
os.path.join("examples", "user_management.py")
|
||||
).get_all_symbols_and_roots()
|
||||
assert user_management_roots == user_management_node["children"]
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
|
||||
|
||||
@@ -27,7 +27,7 @@ class TestRLanguageServer:
|
||||
@pytest.mark.parametrize("language_server", [Language.R], indirect=True)
|
||||
def test_symbol_retrieval(self, language_server: SolidLanguageServer):
|
||||
"""Test R document symbol extraction."""
|
||||
all_symbols, _root_symbols = language_server.request_document_symbols(os.path.join("R", "utils.R"))
|
||||
all_symbols, _root_symbols = language_server.request_document_symbols(os.path.join("R", "utils.R")).get_all_symbols_and_roots()
|
||||
|
||||
# Should find the three exported functions
|
||||
function_symbols = [s for s in all_symbols if s.get("kind") == 12] # Function kind
|
||||
|
||||
@@ -21,7 +21,7 @@ class TestRegoLanguageServer:
|
||||
def test_request_document_symbols_authz(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test that document symbols can be retrieved from authz.rego."""
|
||||
file_path = os.path.join("policies", "authz.rego")
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
|
||||
assert symbols is not None
|
||||
assert len(symbols) > 0
|
||||
@@ -40,7 +40,7 @@ class TestRegoLanguageServer:
|
||||
def test_request_document_symbols_helpers(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test that document symbols can be retrieved from helpers.rego."""
|
||||
file_path = os.path.join("utils", "helpers.rego")
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
|
||||
assert symbols is not None
|
||||
assert len(symbols) > 0
|
||||
@@ -71,7 +71,7 @@ class TestRegoLanguageServer:
|
||||
file_path = os.path.join("policies", "authz.rego")
|
||||
|
||||
# Get document symbols
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
|
||||
|
||||
# Find the is_admin symbol which references admin_roles
|
||||
@@ -98,7 +98,7 @@ class TestRegoLanguageServer:
|
||||
file_path = os.path.join("policies", "authz.rego")
|
||||
|
||||
# Get document symbols
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
|
||||
|
||||
# Find the allow symbol
|
||||
@@ -123,7 +123,7 @@ class TestRegoLanguageServer:
|
||||
def test_find_symbols_validation(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test finding symbols in validation.rego which has imports."""
|
||||
file_path = os.path.join("policies", "validation.rego")
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
|
||||
assert symbols is not None
|
||||
assert len(symbols) > 0
|
||||
|
||||
@@ -20,7 +20,7 @@ class TestRubyLanguageServer:
|
||||
@pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
|
||||
def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None:
|
||||
file_path = os.path.join("main.rb")
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
helper_symbol = None
|
||||
for sym in symbols[0]:
|
||||
if sym.get("name") == "helper_function":
|
||||
|
||||
@@ -89,7 +89,7 @@ class TestRubyLanguageServerSymbols:
|
||||
"""Test request_containing_symbol for a module context."""
|
||||
# Test that we can find the Services module in document symbols
|
||||
file_path = os.path.join("services.rb")
|
||||
symbols, _roots = language_server.request_document_symbols(file_path)
|
||||
symbols, _roots = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
|
||||
# Verify Services module appears in document symbols
|
||||
services_module = None
|
||||
@@ -141,7 +141,7 @@ class TestRubyLanguageServerSymbols:
|
||||
# Test referencing symbols for create_user method
|
||||
file_path = os.path.join("services.rb")
|
||||
# Line containing the create_user method definition
|
||||
symbols, _roots = language_server.request_document_symbols(file_path)
|
||||
symbols, _roots = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
create_user_symbol = None
|
||||
|
||||
# Find create_user method in the document symbols (Ruby returns flat list)
|
||||
@@ -169,7 +169,7 @@ class TestRubyLanguageServerSymbols:
|
||||
# Test referencing symbols for User class
|
||||
file_path = os.path.join("models.rb")
|
||||
# Find User class in document symbols
|
||||
symbols, _roots = language_server.request_document_symbols(file_path)
|
||||
symbols, _roots = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
user_symbol = None
|
||||
|
||||
for symbol in symbols:
|
||||
@@ -285,7 +285,7 @@ class TestRubyLanguageServerSymbols:
|
||||
def test_document_symbols_detailed(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test document symbols for detailed Ruby file structure."""
|
||||
file_path = os.path.join("models.rb")
|
||||
symbols, roots = language_server.request_document_symbols(file_path)
|
||||
symbols, roots = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
|
||||
# Verify we have symbols
|
||||
assert len(symbols) > 0 or len(roots) > 0
|
||||
@@ -310,7 +310,7 @@ class TestRubyLanguageServerSymbols:
|
||||
def test_module_and_class_hierarchy(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test symbol detection for modules and nested class hierarchies."""
|
||||
file_path = os.path.join("nested.rb")
|
||||
symbols, roots = language_server.request_document_symbols(file_path)
|
||||
symbols, roots = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
|
||||
# Verify we can detect the nested structure
|
||||
assert len(symbols) > 0 or len(roots) > 0
|
||||
@@ -363,7 +363,7 @@ class TestRubyLanguageServerSymbols:
|
||||
# Test referencing symbols for a method parameter in get_user method
|
||||
file_path = os.path.join("services.rb")
|
||||
# Find get_user method and test parameter references
|
||||
symbols, _roots = language_server.request_document_symbols(file_path)
|
||||
symbols, _roots = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
get_user_symbol = None
|
||||
|
||||
for symbol in symbols:
|
||||
|
||||
@@ -31,7 +31,7 @@ class TestRust2024EditionLanguageServer:
|
||||
def test_find_references_raw(self) -> None:
|
||||
# Test finding references to the 'add' function defined in main.rs
|
||||
file_path = os.path.join("src", "main.rs")
|
||||
symbols = self.language_server.request_document_symbols(file_path)
|
||||
symbols = self.language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
add_symbol = None
|
||||
for sym in symbols[0]:
|
||||
if sym.get("name") == "add":
|
||||
@@ -53,7 +53,7 @@ class TestRust2024EditionLanguageServer:
|
||||
def test_find_referencing_symbols_multiply(self) -> None:
|
||||
# Find references to 'multiply' function defined in lib.rs
|
||||
file_path = os.path.join("src", "lib.rs")
|
||||
symbols = self.language_server.request_document_symbols(file_path)
|
||||
symbols = self.language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
multiply_symbol = None
|
||||
for sym in symbols[0]:
|
||||
if sym.get("name") == "multiply":
|
||||
@@ -69,7 +69,7 @@ class TestRust2024EditionLanguageServer:
|
||||
def test_find_calculator_struct_and_impl(self) -> None:
|
||||
# Test finding the Calculator struct and its impl block
|
||||
file_path = os.path.join("src", "lib.rs")
|
||||
symbols = self.language_server.request_document_symbols(file_path)
|
||||
symbols = self.language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
|
||||
# Find the Calculator struct
|
||||
calculator_struct = None
|
||||
|
||||
@@ -13,7 +13,7 @@ class TestRustLanguageServer:
|
||||
def test_find_references_raw(self, language_server: SolidLanguageServer) -> None:
|
||||
# Directly test the request_references method for the add function
|
||||
file_path = os.path.join("src", "lib.rs")
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
add_symbol = None
|
||||
for sym in symbols[0]:
|
||||
if sym.get("name") == "add":
|
||||
@@ -37,7 +37,7 @@ class TestRustLanguageServer:
|
||||
def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None:
|
||||
# Find references to 'add' defined in lib.rs, should be referenced from main.rs
|
||||
file_path = os.path.join("src", "lib.rs")
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
add_symbol = None
|
||||
for sym in symbols[0]:
|
||||
if sym.get("name") == "add":
|
||||
|
||||
@@ -29,7 +29,7 @@ def scala_ls():
|
||||
|
||||
def test_scala_document_symbols(scala_ls):
|
||||
"""Test document symbols for Main.scala"""
|
||||
symbols, _ = scala_ls.request_document_symbols(MAIN_FILE_PATH)
|
||||
symbols, _ = scala_ls.request_document_symbols(MAIN_FILE_PATH).get_all_symbols_and_roots()
|
||||
symbol_names = [s["name"] for s in symbols]
|
||||
assert symbol_names[0] == "com.example"
|
||||
assert symbol_names[1] == "Main"
|
||||
|
||||
@@ -88,7 +88,7 @@ class TestSwiftLanguageServerBasics:
|
||||
|
||||
# First, let's check if Utils is used anywhere (it might not be in this simple test)
|
||||
# We'll test goto_definition on Utils struct itself
|
||||
symbols = language_server.request_document_symbols(utils_file)
|
||||
symbols = language_server.request_document_symbols(utils_file).get_all_symbols_and_roots()
|
||||
utils_symbol = next((s for s in symbols[0] if s.get("name") == "Utils"), None)
|
||||
|
||||
sel_start = utils_symbol["selectionRange"]["start"]
|
||||
@@ -104,7 +104,7 @@ class TestSwiftLanguageServerBasics:
|
||||
"""Test request_references on the Calculator class."""
|
||||
# Get references to the Calculator class in main.swift
|
||||
file_path = os.path.join("src", "main.swift")
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
|
||||
calculator_symbol = next((s for s in symbols[0] if s.get("name") == "Calculator"), None)
|
||||
|
||||
@@ -126,7 +126,7 @@ class TestSwiftLanguageServerBasics:
|
||||
"""Test request_references on the User struct."""
|
||||
# Get references to the User struct in main.swift
|
||||
file_path = os.path.join("src", "main.swift")
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
|
||||
user_symbol = next((s for s in symbols[0] if s.get("name") == "User"), None)
|
||||
|
||||
@@ -147,7 +147,7 @@ class TestSwiftLanguageServerBasics:
|
||||
"""Test request_references on the Utils struct."""
|
||||
# Get references to the Utils struct in utils.swift
|
||||
file_path = os.path.join("src", "utils.swift")
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
utils_symbol = next((s for s in symbols[0] if s.get("name") == "Utils"), None)
|
||||
if not utils_symbol or "selectionRange" not in utils_symbol:
|
||||
raise AssertionError("Utils symbol or its selectionRange not found")
|
||||
|
||||
@@ -21,7 +21,7 @@ class TestLanguageServerBasics:
|
||||
# Simple test to verify the language server is working
|
||||
file_path = "main.tf"
|
||||
# Just try to get document symbols - this should work without hanging
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
assert len(symbols) > 0, "Should find at least some symbols in main.tf"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.TERRAFORM], indirect=True)
|
||||
@@ -30,7 +30,7 @@ class TestLanguageServerBasics:
|
||||
# Get references to an aws_instance resource in main.tf
|
||||
file_path = "main.tf"
|
||||
# Find aws_instance resources
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
aws_instance_symbol = next((s for s in symbols[0] if s.get("name") == 'resource "aws_instance" "web_server"'), None)
|
||||
if not aws_instance_symbol or "selectionRange" not in aws_instance_symbol:
|
||||
raise AssertionError("aws_instance symbol or its selectionRange not found")
|
||||
@@ -44,7 +44,7 @@ class TestLanguageServerBasics:
|
||||
# Get references to a variable in variables.tf
|
||||
file_path = "variables.tf"
|
||||
# Find variable definitions
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
var_symbol = next((s for s in symbols[0] if s.get("name") == 'variable "instance_type"'), None)
|
||||
if not var_symbol or "selectionRange" not in var_symbol:
|
||||
raise AssertionError("variable symbol or its selectionRange not found")
|
||||
|
||||
@@ -19,7 +19,7 @@ class TestTypescriptLanguageServer:
|
||||
@pytest.mark.parametrize("language_server", [Language.TYPESCRIPT], indirect=True)
|
||||
def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None:
|
||||
file_path = os.path.join("index.ts")
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
helper_symbol = None
|
||||
for sym in symbols[0]:
|
||||
if sym.get("name") == "helperFunction":
|
||||
|
||||
@@ -30,7 +30,7 @@ class TestZigLanguageServer:
|
||||
def test_find_symbols_in_main(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test finding specific symbols in main.zig."""
|
||||
file_path = os.path.join("src", "main.zig")
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
|
||||
assert symbols is not None
|
||||
assert len(symbols) > 0
|
||||
@@ -47,7 +47,7 @@ class TestZigLanguageServer:
|
||||
def test_find_symbols_in_calculator(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test finding Calculator struct and its methods."""
|
||||
file_path = os.path.join("src", "calculator.zig")
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
|
||||
assert symbols is not None
|
||||
assert len(symbols) > 0
|
||||
@@ -90,7 +90,7 @@ class TestZigLanguageServer:
|
||||
def test_find_symbols_in_math_utils(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test finding functions in math_utils.zig."""
|
||||
file_path = os.path.join("src", "math_utils.zig")
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
|
||||
assert symbols is not None
|
||||
assert len(symbols) > 0
|
||||
@@ -106,7 +106,7 @@ class TestZigLanguageServer:
|
||||
def test_find_references_within_file(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test finding references within the same file."""
|
||||
file_path = os.path.join("src", "calculator.zig")
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
|
||||
symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
|
||||
|
||||
@@ -163,7 +163,7 @@ class TestZigLanguageServer:
|
||||
time.sleep(1)
|
||||
|
||||
# Find Calculator struct
|
||||
symbols = language_server.request_document_symbols(os.path.join("src", "calculator.zig"))
|
||||
symbols = language_server.request_document_symbols(os.path.join("src", "calculator.zig")).get_all_symbols_and_roots()
|
||||
symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
|
||||
|
||||
calculator_symbol = None
|
||||
@@ -207,7 +207,7 @@ class TestZigLanguageServer:
|
||||
"""
|
||||
# Find references to Calculator from calculator.zig
|
||||
file_path = os.path.join("src", "calculator.zig")
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
|
||||
|
||||
calculator_symbol = None
|
||||
@@ -291,7 +291,7 @@ class TestZigLanguageServer:
|
||||
def test_verify_cross_file_imports(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Verify that our test files have proper cross-file imports."""
|
||||
# Verify main.zig imports
|
||||
main_symbols = language_server.request_document_symbols(os.path.join("src", "main.zig"))
|
||||
main_symbols = language_server.request_document_symbols(os.path.join("src", "main.zig")).get_all_symbols_and_roots()
|
||||
assert main_symbols is not None
|
||||
main_list = main_symbols[0] if isinstance(main_symbols, tuple) else main_symbols
|
||||
main_names = {sym.get("name") for sym in main_list if isinstance(sym, dict)}
|
||||
@@ -301,14 +301,14 @@ class TestZigLanguageServer:
|
||||
assert "greeting" in main_names, "greeting function should be in main.zig"
|
||||
|
||||
# Verify calculator.zig exports Calculator
|
||||
calc_symbols = language_server.request_document_symbols(os.path.join("src", "calculator.zig"))
|
||||
calc_symbols = language_server.request_document_symbols(os.path.join("src", "calculator.zig")).get_all_symbols_and_roots()
|
||||
assert calc_symbols is not None
|
||||
calc_list = calc_symbols[0] if isinstance(calc_symbols, tuple) else calc_symbols
|
||||
calc_names = {sym.get("name") for sym in calc_list if isinstance(sym, dict)}
|
||||
assert "Calculator" in calc_names, "Calculator struct should be in calculator.zig"
|
||||
|
||||
# Verify math_utils.zig exports functions
|
||||
math_symbols = language_server.request_document_symbols(os.path.join("src", "math_utils.zig"))
|
||||
math_symbols = language_server.request_document_symbols(os.path.join("src", "math_utils.zig")).get_all_symbols_and_roots()
|
||||
assert math_symbols is not None
|
||||
math_list = math_symbols[0] if isinstance(math_symbols, tuple) else math_symbols
|
||||
math_names = {sym.get("name") for sym in math_list if isinstance(sym, dict)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
revision = 2
|
||||
requires-python = "==3.11.*"
|
||||
|
||||
[[package]]
|
||||
@@ -1524,6 +1524,22 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyinstrument"
|
||||
version = "5.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/80/ce/824ee634994e612156f7b84eaf50b8523c676ebfed8d8dd12939a82f4c15/pyinstrument-5.1.1.tar.gz", hash = "sha256:bc401cda990b3c1cfe8e0e0473cbd605df3c63b73478a89ac4ab108f2184baa8", size = 264730, upload-time = "2025-08-12T11:35:43.426Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/52/65/dc0fcc6122e6f484ffa48260d1023e1fe90e53da8d69a7de656205af91ba/pyinstrument-5.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5272cff6eea21163b2105f6a80c907315e0f567720621e6d5672dc01bf71ee48", size = 130287, upload-time = "2025-08-12T11:34:32.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/96/8a6cab312342f1ad7322b459dcbdb2e041251b7bbe4c1d0dd38ccbf2ae20/pyinstrument-5.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d4e7dc5a4aee37a44ff2e63db3127f2044dd95edcae240cb95915adbf223d4be", size = 122879, upload-time = "2025-08-12T11:34:33.506Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/89/1ad1ae703951832a34ec6b32354c6df1d0690b0333b7b8396c92afcdc23e/pyinstrument-5.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:602d55121df1d88aeb6d8ebc801597fdcb9718f78d602ae81458d65c56f25d24", size = 146412, upload-time = "2025-08-12T11:34:34.975Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/70/588ec07eeaa5d5d4aff2d47c8010379e9b39a7aee7e3fda625a0ce4f5838/pyinstrument-5.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63b5a788ff955e0597bc95463e64d5fa3747017524fdc02a0f5d12d5117cf2b9", size = 144885, upload-time = "2025-08-12T11:34:36.546Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/8f/119ad44d454edb927434bb53d8a69904ecaa47ec56f95c688c557a9590de/pyinstrument-5.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7f66038ec55a12e5510689240cdc745f8e98c90b93363f745106976e5cfb7397", size = 145530, upload-time = "2025-08-12T11:34:37.726Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/31/d53b20d1967ba78574807c5940251c0f238e8e3515ee9c4f20207eb09199/pyinstrument-5.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:110a08b6e7fa9542eb37c337e79467913d364a03bc2062f85566ba96bc82f54e", size = 144708, upload-time = "2025-08-12T11:34:38.855Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/18/78ccb35dd265e83c0b7404a501004387b2e2390a0a44b6e4a004307931d1/pyinstrument-5.1.1-cp311-cp311-win32.whl", hash = "sha256:a223d5e9226ccede5bf2fbd4d13ce0aeb5120501b633ba85290ed94df37d3623", size = 124151, upload-time = "2025-08-12T11:34:40.392Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/c0/3eb94aa2c2f0b8d49e290d4df662e21e707eb4a23f3b938239634fbfdc17/pyinstrument-5.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:16ce582a7b56287d338a8b59688458341aab5c6abda970ba50b2f7b3fd69f89d", size = 124942, upload-time = "2025-08-12T11:34:41.564Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyright"
|
||||
version = "1.1.403"
|
||||
@@ -1875,6 +1891,7 @@ dev = [
|
||||
{ name = "mypy" },
|
||||
{ name = "nbsphinx" },
|
||||
{ name = "poethepoet" },
|
||||
{ name = "pyinstrument" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-xdist" },
|
||||
{ name = "ruff" },
|
||||
@@ -1912,6 +1929,7 @@ requires-dist = [
|
||||
{ name = "poethepoet", marker = "extra == 'dev'", specifier = ">=0.20.0" },
|
||||
{ name = "psutil", specifier = ">=7.0.0" },
|
||||
{ name = "pydantic", specifier = ">=2.10.6" },
|
||||
{ name = "pyinstrument", marker = "extra == 'dev'" },
|
||||
{ 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" },
|
||||
|
||||
Reference in new issue
Block a user