Merge pull request #1708 from ebarkhordar/fix/search-text-exclusive-end-line

fix(search): do not mark the following line as matched when a match ends at a line break
This commit is contained in:
Michael Panchenko authored and GitHub committed 2026-07-17 10:27:28 +02:00
commit ad2fdf1fa6
3 files changed
+59

No files matched your search

+6
View File
@@ -10,6 +10,12 @@ Status of the `main` branch. Changes prior to the next official version change w
the `line_ending` setting is meant to be the single point of line-ending translation on write. The
fallback now normalizes line endings to LF, consistently with the primary path.
* Tools:
- Fix: `search_for_pattern` marked one line too many as matched whenever a match ended with a line
break, because the match's exclusive end index was mapped to a line number directly and therefore
resolved to the start of the following line. The line the match ends on is now determined correctly,
which also keeps `context_lines_after` aligned.
* JetBrains:
- Allow external files from dependencies (specified via references like "<ext:FileUtil.class|472e0a13>") to be
- read via `ReadFileTool`
+4
View File
@@ -187,6 +187,10 @@ def search_text(
# Find the line numbers for the start and end positions
start_line_num = TextUtils.get_line_from_index(content, start_pos)
end_line_num = TextUtils.get_line_from_index(content, end_pos)
if end_line_num > start_line_num and TextUtils.get_line_col_from_index(content, end_pos)[1] == 0:
# `end_pos` is exclusive, so if it is at the start of a line, the match ends with the
# preceding line's newline and does not extend into the line that `end_pos` points to
end_line_num -= 1
# Calculate the range of lines to include in the context
context_start = max(0, start_line_num - context_lines_before)
+49
View File
@@ -103,6 +103,55 @@ class TestSearchText:
assert "return a * c" in first_match.lines[1].line_content
assert "elif b > a:" in first_match.lines[2].line_content
@pytest.mark.parametrize(
("pattern", "expected_matched_lines"),
[
# matches ending with a newline: the trailing newline terminates the last matched line
# and must not pull in the line that follows
(r"alpha\n", [0]),
(r"alpha\nbeta\n", [0, 1]),
(r"gamma\n", [2]),
# controls: matches ending mid-line were always correct
(r"alpha", [0]),
(r"beta", [1]),
(r"alpha\nbeta", [0, 1]),
],
)
def test_search_text_match_ending_at_line_break(self, pattern: str, expected_matched_lines: list[int]):
"""The lines marked as matched are exactly the lines the matched text occupies."""
content = "alpha\nbeta\ngamma\n"
matches = search_text(pattern, content=content)
assert len(matches) == 1
matched_lines = [line.line_number for line in matches[0].lines if line.match_type == LineType.MATCH]
assert matched_lines == expected_matched_lines
assert matches[0].num_matched_lines == len(expected_matched_lines)
@pytest.mark.parametrize(
("pattern", "expected_matched_lines"),
[
(r"alpha\r\n", [0]),
(r"alpha\r", [0]),
(r"alpha\r\nbeta\r\n", [0, 1]),
(r"alpha", [0]),
],
)
def test_search_text_match_ending_at_line_break_crlf(self, pattern: str, expected_matched_lines: list[int]):
"""As above for CRLF content, where the end index can fall inside the two-character line break.
The content is passed directly rather than read from a file, since the file read paths translate
line endings and CR would not reach this function.
"""
content = "alpha\r\nbeta\r\ngamma\r\n"
matches = search_text(pattern, content=content)
assert len(matches) == 1
matched_lines = [line.line_number for line in matches[0].lines if line.match_type == LineType.MATCH]
assert matched_lines == expected_matched_lines
assert matches[0].num_matched_lines == len(expected_matched_lines)
def test_search_text_with_multiline_match(self):
"""Test searching with multiline pattern matching."""
content = """