mirror of
https://github.com/tiennm99/serena.git
synced 2026-07-30 12:23:36 +00:00
LS: new method, request_full_symbol_tree
This commit is contained in:
@@ -660,7 +660,9 @@ class LanguageServer:
|
||||
|
||||
: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. Each symbol in hierarchy starting from the roots has a children attribute.
|
||||
:return: A list of symbols in the file, and a list of root symbols that represent the tree structure of the symbols.
|
||||
Each symbol in hierarchy starting from the roots has a children attribute.
|
||||
All symbols will have a location and a children attribute.
|
||||
"""
|
||||
self.logger.log(f"Requesting document symbols for {relative_file_path} for the first time", logging.DEBUG)
|
||||
# TODO: it's kinda dumb to not use the cache if include_body is False after include_body was True once
|
||||
@@ -736,7 +738,92 @@ class LanguageServer:
|
||||
self._document_symbols_cache[cache_key] = (file_data.content_hash, result)
|
||||
self._cache_has_changed = True
|
||||
return result
|
||||
|
||||
async def request_full_symbol_tree(self, start_package_relative_path: str | None = None, include_body: bool = False) -> List[multilspy_types.UnifiedSymbolInformation]:
|
||||
"""
|
||||
Will go through all files in the project and build a tree of symbols. Note: this may be slow the first time it is called.
|
||||
|
||||
For each file, a symbol of kind Module (3) will be created. For directories, a symbol of kind Package (4) will be created.
|
||||
All symbols will have a children attribute, thereby representing the tree structure of all symbols in the project
|
||||
that are within the repository.
|
||||
Will ignore all directories that start with a dot (.) and __pycache__ directories.
|
||||
|
||||
Args:
|
||||
start_package_relative_path: if passed, only the symbols within this directory will be considered.
|
||||
include_body: whether to include the body of the symbols in the result.
|
||||
|
||||
Returns:
|
||||
A list of root symbols representing the top-level packages/modules in the project.
|
||||
"""
|
||||
if not self.server_started:
|
||||
self.logger.log(
|
||||
"request_full_symbol_tree called before Language Server started",
|
||||
logging.ERROR,
|
||||
)
|
||||
raise MultilspyException("Language Server not started")
|
||||
|
||||
# Helper function to check if a path should be ignored
|
||||
def should_ignore_path(path: str) -> bool:
|
||||
parts = path.split(os.sep)
|
||||
return any(part.startswith('.') or part == '__pycache__' for part in parts)
|
||||
|
||||
# Helper function to recursively process directories
|
||||
async def process_directory(dir_path: str) -> List[multilspy_types.UnifiedSymbolInformation]:
|
||||
if should_ignore_path(dir_path):
|
||||
return []
|
||||
|
||||
result = []
|
||||
try:
|
||||
items = os.listdir(os.path.join(self.repository_root_path, dir_path))
|
||||
except OSError:
|
||||
return []
|
||||
|
||||
# Create package symbol for directory
|
||||
package_symbol = multilspy_types.UnifiedSymbolInformation( # type: ignore
|
||||
name=os.path.basename(dir_path),
|
||||
kind=multilspy_types.SymbolKind.Package,
|
||||
location=multilspy_types.Location(
|
||||
uri=str(pathlib.Path(os.path.join(self.repository_root_path, dir_path)).as_uri()),
|
||||
range={"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 0}},
|
||||
absolutePath=str(os.path.join(self.repository_root_path, dir_path)),
|
||||
relativePath=str(Path(dir_path).resolve().relative_to(self.repository_root_path)),
|
||||
),
|
||||
children=[]
|
||||
)
|
||||
result.append(package_symbol)
|
||||
|
||||
for item in items:
|
||||
item_path = os.path.join(dir_path, item)
|
||||
abs_item_path = os.path.join(self.repository_root_path, item_path)
|
||||
|
||||
if os.path.isdir(abs_item_path):
|
||||
child_symbols = await process_directory(item_path)
|
||||
package_symbol["children"].extend(child_symbols)
|
||||
|
||||
elif os.path.isfile(abs_item_path):
|
||||
_, root_nodes = await self.request_document_symbols(item_path, include_body=include_body)
|
||||
|
||||
# Create module symbol
|
||||
module_symbol = multilspy_types.UnifiedSymbolInformation( # type: ignore
|
||||
name=os.path.splitext(item)[0],
|
||||
kind=multilspy_types.SymbolKind.Module,
|
||||
location=multilspy_types.Location(
|
||||
uri=str(pathlib.Path(abs_item_path).as_uri()),
|
||||
range={"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 0}},
|
||||
absolutePath=str(abs_item_path),
|
||||
relativePath=str(Path(item_path).resolve().relative_to(self.repository_root_path)),
|
||||
),
|
||||
children=root_nodes
|
||||
)
|
||||
|
||||
package_symbol["children"].append(module_symbol)
|
||||
|
||||
return result
|
||||
|
||||
# Start from the root or the specified directory
|
||||
start_path = start_package_relative_path or self.repository_root_path
|
||||
return await process_directory(start_path)
|
||||
|
||||
async def request_hover(self, relative_file_path: str, line: int, column: int) -> Union[multilspy_types.Hover, None]:
|
||||
"""
|
||||
Raise a [textDocument/hover](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_hover) request to the Language Server
|
||||
@@ -1188,8 +1275,8 @@ class SyncLanguageServer:
|
||||
:return: None
|
||||
"""
|
||||
self.loop = asyncio.new_event_loop()
|
||||
loop_thread = threading.Thread(target=self.loop.run_forever, daemon=True)
|
||||
loop_thread.start()
|
||||
self.loop_thread = threading.Thread(target=self.loop.run_forever, daemon=True)
|
||||
self.loop_thread.start()
|
||||
ctx = self.language_server.start_server()
|
||||
asyncio.run_coroutine_threadsafe(ctx.__aenter__(), loop=self.loop).result()
|
||||
yield self
|
||||
@@ -1280,6 +1367,28 @@ class SyncLanguageServer:
|
||||
self.language_server.request_document_symbols(relative_file_path, include_body), self.loop
|
||||
).result()
|
||||
return result
|
||||
|
||||
def request_full_symbol_tree(self, start_package_relative_path: str | None = None, include_body: bool = False) -> List[multilspy_types.UnifiedSymbolInformation]:
|
||||
"""
|
||||
Will go through all files in the project and build a tree of symbols. Note: this may be slow the first time it is called.
|
||||
|
||||
For each file, a symbol of kind Module (3) will be created. For directories, a symbol of kind Package (4) will be created.
|
||||
All symbols will have a children attribute, thereby representing the tree structure of all symbols in the project
|
||||
that are within the repository.
|
||||
Will ignore all directories that start with a dot (.) and __pycache__ directories.
|
||||
|
||||
Args:
|
||||
start_package_relative_path: if passed, only the symbols within this directory will be considered.
|
||||
include_body: whether to include the body of the symbols in the result.
|
||||
|
||||
Returns:
|
||||
A list of root symbols representing the top-level packages/modules in the project.
|
||||
"""
|
||||
result = asyncio.run_coroutine_threadsafe(
|
||||
self.language_server.request_full_symbol_tree(start_package_relative_path, include_body), self.loop
|
||||
).result()
|
||||
return result
|
||||
|
||||
|
||||
def request_hover(self, relative_file_path: str, line: int, column: int) -> Union[multilspy_types.Hover, None]:
|
||||
"""
|
||||
|
||||
@@ -293,6 +293,8 @@ class TestLanguageServerSymbols:
|
||||
|
||||
# Step 3: Verify that they refer to the same symbol
|
||||
assert defining_symbol["kind"] == containing_symbol["kind"]
|
||||
assert "location" in defining_symbol
|
||||
assert "location" in containing_symbol
|
||||
assert defining_symbol["location"]["uri"] == containing_symbol["location"]["uri"]
|
||||
|
||||
# The integration test is successful if we've gotten this far,
|
||||
@@ -320,35 +322,46 @@ class TestLanguageServerSymbols:
|
||||
warnings.warn("Could not verify container hierarchy - implementation detail")
|
||||
|
||||
def test_symbol_tree_structure(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
"""Test the symbol tree structure."""
|
||||
file_path = str(repo_path / "test_repo" / "services.py")
|
||||
symbols, root_nodes = language_server.request_document_symbols(file_path)
|
||||
assert len(symbols) > 0
|
||||
assert {root["name"] for root in root_nodes} == {
|
||||
"UserService",
|
||||
"ItemService",
|
||||
"create_service_container",
|
||||
"user_var_str",
|
||||
"user_service",
|
||||
}
|
||||
user_service_root = next(root for root in root_nodes if root["name"] == "UserService")
|
||||
assert user_service_root
|
||||
assert "children" in user_service_root
|
||||
assert {child["name"] for child in user_service_root["children"] if child["kind"] != SymbolKind.Variable} == {
|
||||
"__init__",
|
||||
"create_user",
|
||||
"get_user",
|
||||
"list_users",
|
||||
"delete_user",
|
||||
}
|
||||
"""Test that the symbol tree structure is correctly built."""
|
||||
# Get all symbols in the test file
|
||||
repo_structure = language_server.request_full_symbol_tree()
|
||||
assert len(repo_structure) == 1
|
||||
# Assert that the root symbol is the test_repo directory
|
||||
assert repo_structure[0]["name"] == "test_repo"
|
||||
assert repo_structure[0]["kind"] == SymbolKind.Package
|
||||
assert "children" in repo_structure[0]
|
||||
# Assert that the children are the top-level packages
|
||||
child_names = {child["name"] for child in repo_structure[0]["children"]}
|
||||
child_kinds = {child["kind"] for child in repo_structure[0]["children"]}
|
||||
assert child_names == {"test_repo", "custom_test", "examples", "scripts"}
|
||||
assert child_kinds == {SymbolKind.Package}
|
||||
examples_package = next(child for child in repo_structure[0]["children"] if child["name"] == "examples")
|
||||
# assert that children are __init__ and user_management
|
||||
assert {child["name"] for child in examples_package["children"]} == {"__init__", "user_management"}
|
||||
assert {child["kind"] for child in examples_package["children"]} == {SymbolKind.Module}
|
||||
|
||||
# Now recursively flatten the tree into a set of names and assert that it coincides with the set of symbol names
|
||||
all_names_in_tree = set()
|
||||
# assert that tree of user_management node is same as retrieved directly
|
||||
user_management_node = next(child for child in examples_package["children"] if child["name"] == "user_management")
|
||||
user_management_rel_path = user_management_node["location"]["relativePath"]
|
||||
assert user_management_rel_path == "examples/user_management.py"
|
||||
_, user_management_roots = language_server.request_document_symbols(str(repo_path / "examples" / "user_management.py"))
|
||||
assert user_management_roots == user_management_node["children"]
|
||||
|
||||
def flatten_tree(nodes):
|
||||
for node in nodes:
|
||||
all_names_in_tree.add(node["name"])
|
||||
flatten_tree(node.get("children", []))
|
||||
def test_symbol_tree_structure_subdir(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
"""Test that the symbol tree structure is correctly built."""
|
||||
# Get all symbols in the test file
|
||||
examples_package_roots = language_server.request_full_symbol_tree(start_package_relative_path=str(repo_path / "examples"))
|
||||
assert len(examples_package_roots) == 1
|
||||
examples_package = examples_package_roots[0]
|
||||
assert examples_package["name"] == "examples"
|
||||
assert examples_package["kind"] == SymbolKind.Package
|
||||
# assert that children are __init__ and user_management
|
||||
assert {child["name"] for child in examples_package["children"]} == {"__init__", "user_management"}
|
||||
assert {child["kind"] for child in examples_package["children"]} == {SymbolKind.Module}
|
||||
|
||||
flatten_tree(root_nodes)
|
||||
assert all_names_in_tree == {symbol["name"] for symbol in symbols}
|
||||
# assert that tree of user_management node is same as retrieved directly
|
||||
user_management_node = next(child for child in examples_package["children"] if child["name"] == "user_management")
|
||||
user_management_rel_path = user_management_node["location"]["relativePath"]
|
||||
assert user_management_rel_path == "examples/user_management.py"
|
||||
_, user_management_roots = language_server.request_document_symbols(str(repo_path / "examples" / "user_management.py"))
|
||||
assert user_management_roots == user_management_node["children"]
|
||||
|
||||
Reference in New Issue
Block a user