diff --git a/src/serena/text_utils.py b/src/serena/text_utils.py index 338b295..75cc7dd 100644 --- a/src/serena/text_utils.py +++ b/src/serena/text_utils.py @@ -153,19 +153,28 @@ def search_text( # Convert pattern to a compiled regex if it's a string if is_glob and isinstance(pattern, str): - # Convert glob pattern to regex - # Escape all regex special characters except * and ? - regex_special_chars = r"\^$.|+()[{" - escaped_pattern = "" - for char in pattern: - if char in regex_special_chars: - escaped_pattern += "\\" + char - elif char == "*": - escaped_pattern += ".*" - elif char == "?": - escaped_pattern += "." - else: - escaped_pattern += char + # Convert glob pattern with optional backslash escaping to regex + def glob_to_regex(glob_pat: str) -> str: + regex_parts: list[str] = [] + i = 0 + while i < len(glob_pat): + ch = glob_pat[i] + if ch == "*": + regex_parts.append(".*") + elif ch == "?": + regex_parts.append(".") + elif ch == "\\": + i += 1 + if i < len(glob_pat): + regex_parts.append(re.escape(glob_pat[i])) + else: + regex_parts.append("\\") + else: + regex_parts.append(re.escape(ch)) + i += 1 + return "".join(regex_parts) + + escaped_pattern = glob_to_regex(pattern) # For glob patterns, don't anchor with ^ and $ to allow partial line matches compiled_pattern = re.compile(escaped_pattern) elif isinstance(pattern, str): diff --git a/test/serena/test_text_utils.py b/test/serena/test_text_utils.py index 1fa6929..860ce6a 100644 --- a/test/serena/test_text_utils.py +++ b/test/serena/test_text_utils.py @@ -182,6 +182,24 @@ class TestSearchText: assert any("isinstance(item, dict)" in line for line in instance_matches) assert any("isinstance(item, list)" in line for line in instance_matches) + def test_search_text_glob_with_special_chars(self): + """Glob patterns containing regex special characters should match literally.""" + content = """ + def func_square(): + print("value[42]") + + def func_curly(): + print("value{bar}") + """ + + matches_square = search_text(r"*\[42\]*", content=content, is_glob=True) + assert len(matches_square) == 1 + assert "[42]" in matches_square[0].lines[0].line_content + + matches_curly = search_text("*{bar}*", content=content, is_glob=True) + assert len(matches_curly) == 1 + assert "{bar}" in matches_curly[0].lines[0].line_content + def test_search_text_no_matches(self): """Test searching with a pattern that doesn't match anything.""" content = """