From 8ea2f866ce05c8e3149232544f063ab7c7d883b6 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 5 Jul 2025 21:35:43 +0200 Subject: [PATCH] Add JetBrainsFindSymbolTool --- src/serena/tools/__init__.py | 1 + src/serena/tools/jetbrains_plugin_client.py | 5 +- src/serena/tools/jetbrains_tools.py | 70 +++++++++++++++++++++ 3 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 src/serena/tools/jetbrains_tools.py diff --git a/src/serena/tools/__init__.py b/src/serena/tools/__init__.py index 4e05360c..c88a7e53 100644 --- a/src/serena/tools/__init__.py +++ b/src/serena/tools/__init__.py @@ -6,3 +6,4 @@ from .memory_tools import * from .cmd_tools import * from .config_tools import * from .workflow_tools import * +from .jetbrains_tools import * diff --git a/src/serena/tools/jetbrains_plugin_client.py b/src/serena/tools/jetbrains_plugin_client.py index 76bfe78a..db87894e 100644 --- a/src/serena/tools/jetbrains_plugin_client.py +++ b/src/serena/tools/jetbrains_plugin_client.py @@ -66,17 +66,18 @@ class JetBrainsPluginClient: def heartbeat(self) -> dict[str, Any]: return self._make_request("GET", "/heartbeat") - def find_symbol(self, name_path: str, include_body: bool = False, depth: int = 0) -> dict[str, Any]: + def find_symbol(self, name_path: str, relative_path: str | None = None, include_body: bool = False, depth: int = 0) -> dict[str, Any]: """ Find symbols by name. :param name_path: the name path to match + :param relative_path: the relative path to which to restrict the search :param include_body: whether to include symbol body content :param depth: depth of children to include (0 = no children) :return: Dictionary containing 'symbols' list with matching symbols """ - request_data = {"name": name_path, "includeBody": include_body, "depth": depth} + request_data = {"namePath": name_path, "relativePath": relative_path, "includeBody": include_body, "depth": depth} return self._make_request("POST", "/findSymbol", request_data) diff --git a/src/serena/tools/jetbrains_tools.py b/src/serena/tools/jetbrains_tools.py new file mode 100644 index 00000000..d7d276e2 --- /dev/null +++ b/src/serena/tools/jetbrains_tools.py @@ -0,0 +1,70 @@ +import json + +from serena.tools import TOOL_DEFAULT_MAX_ANSWER_LENGTH, Tool +from serena.tools.jetbrains_plugin_client import JetBrainsPluginClient + + +class JetBrainsFindSymbolTool(Tool): + """ + Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). + """ + + def apply( + self, + name_path: str, + depth: int = 0, + relative_path: str | None = None, + include_body: bool = False, + max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH, + ) -> 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). + + 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. + + 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`. + + :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. + 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 + speed up the search as well as reduce the number of results. + :param include_body: If True, include the symbol's source code. Use judiciously. + :param max_answer_chars: Max characters for the JSON result. If exceeded, no content is returned. + :return: JSON string: a list of symbols (with locations) matching the name. + """ + with JetBrainsPluginClient() as client: + response_dict = client.find_symbol( + name_path=name_path, + relative_path=relative_path, + depth=depth, + include_body=include_body, + ) + result = json.dumps(response_dict) + return self._limit_length(result, max_answer_chars)