From d79e2c2a0f1874d3f8e74d8c9706e99ade372a1b Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Thu, 23 Apr 2026 14:50:04 +0200 Subject: [PATCH] Fix: SearchForPatternTool returned 1-based line numbers (in contrast to all other tools) Cause: Implementation of text_utils.search_text Serena's tools use 0-based line numbers, and this information is now also centrally added to the instructions (manual). --- CHANGELOG.md | 1 + .../config/prompt_templates/system_prompt.yml | 4 +++- src/serena/tools/file_tools.py | 4 ++-- src/serena/util/text_utils.py | 17 +++++++---------- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b28c8bb..678be1e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ Status of the `main` branch. Changes prior to the next official version change w - Project server: - `query_project`: Support use of project root instead of project name #1388 - `list_queryable_projects`: Return both project names and project roots + - Fix: `search_for_pattern` tool returned 1-based line numbers (in contrast to all other tools); cause: implementation of `text_utils.search_text` * JetBrains: - `Move` and `SafeDelete` tools: transform empty string to None (counteracts client errors) diff --git a/src/serena/resources/config/prompt_templates/system_prompt.yml b/src/serena/resources/config/prompt_templates/system_prompt.yml index debefcd2..35fffeda 100644 --- a/src/serena/resources/config/prompt_templates/system_prompt.yml +++ b/src/serena/resources/config/prompt_templates/system_prompt.yml @@ -41,7 +41,9 @@ prompts: The following global (not project-specific) memories are available to you: {{ global_memories_list }} {%- endif -%} {%- endif %} - + + Line numbers returned by Serena's tools are 0-based! + The context and modes of operation are described below. These determine how to interact with your user and which kinds of interactions are expected of you. diff --git a/src/serena/tools/file_tools.py b/src/serena/tools/file_tools.py index 5f27ddf9..4e7268e0 100644 --- a/src/serena/tools/file_tools.py +++ b/src/serena/tools/file_tools.py @@ -347,7 +347,6 @@ class SearchForPatternTool(Tool): is used to further restrict the search). Smartly combining the various restrictions allows you to perform very targeted searches. - :param substring_pattern: Regular expression for a substring pattern to search for :param context_lines_before: Number of lines of context to include before each match :param context_lines_after: Number of lines of context to include after each match @@ -373,7 +372,7 @@ class SearchForPatternTool(Tool): For example, for finding classes or methods from a name pattern. Setting to False is a better choice if you also want to search in non-code files, like in html or yaml files, which is why it is the default. - :return: A mapping of file paths to lists of matched consecutive lines. + :return: A mapping from file paths to matched consecutive lines (0-based line numbers). """ abs_path = os.path.join(self.get_project_root(), relative_path) if not os.path.exists(abs_path): @@ -411,6 +410,7 @@ class SearchForPatternTool(Tool): paths_include_glob=paths_include_glob, paths_exclude_glob=paths_exclude_glob, ) + # group matches by file file_to_matches: dict[str, list[str]] = defaultdict(list) for match in matches: diff --git a/src/serena/util/text_utils.py b/src/serena/util/text_utils.py index 20c7b0b7..523ba294 100644 --- a/src/serena/util/text_utils.py +++ b/src/serena/util/text_utils.py @@ -188,17 +188,16 @@ def search_text( end_pos = match.end() # Find the line numbers for the start and end positions - start_line_num = content[:start_pos].count("\n") + 1 - end_line_num = content[:end_pos].count("\n") + 1 + start_line_num = content[:start_pos].count("\n") + end_line_num = content[:end_pos].count("\n") # Calculate the range of lines to include in the context - context_start = max(1, start_line_num - context_lines_before) - context_end = min(total_lines, end_line_num + context_lines_after) + context_start = max(0, start_line_num - context_lines_before) + context_end = min(total_lines - 1, end_line_num + context_lines_after) # Create TextLine objects for the context context_lines = [] - for i in range(context_start - 1, context_end): - line_num = i + 1 + for line_num in range(context_start, context_end + 1): if context_start <= line_num < start_line_num: match_type = LineType.BEFORE_MATCH elif end_line_num < line_num <= context_end: @@ -206,7 +205,7 @@ def search_text( else: match_type = LineType.MATCH - context_lines.append(TextLine(line_number=line_num, line_content=lines[i], match_type=match_type)) + context_lines.append(TextLine(line_number=line_num, line_content=lines[line_num], match_type=match_type)) matches.append(MatchedConsecutiveLines(lines=context_lines, source_file_path=source_file_path)) else: @@ -215,7 +214,6 @@ def search_text( # Search line by line, normal compile without DOTALL compiled_pattern = re.compile(pattern) for i, line in enumerate(lines): - line_num = i + 1 if compiled_pattern.search(line): # Calculate the range of lines to include in the context context_start = max(0, i - context_lines_before) @@ -224,7 +222,6 @@ def search_text( # Create TextLine objects for the context context_lines = [] for j in range(context_start, context_end + 1): - context_line_num = j + 1 if j < i: match_type = LineType.BEFORE_MATCH elif j > i: @@ -232,7 +229,7 @@ def search_text( else: match_type = LineType.MATCH - context_lines.append(TextLine(line_number=context_line_num, line_content=lines[j], match_type=match_type)) + context_lines.append(TextLine(line_number=j, line_content=lines[j], match_type=match_type)) matches.append(MatchedConsecutiveLines(lines=context_lines, source_file_path=source_file_path))