akhaliq/anycoder
3.3k
1"""2Search/Replace utilities for applying targeted code changes.3Search/Replace utilities for applying targeted code changes.4"""5 6# Search/Replace block markers7SEARCH_START = "\u003c\u003c\u003c\u003c\u003c\u003c\u003c SEARCH"8DIVIDER = "======="9REPLACE_END = "\u003e\u003e\u003e\u003e\u003e\u003e\u003e REPLACE"10 11 12def apply_search_replace_changes(original_content: str, changes_text: str) -> str:13 """Apply search/replace changes to content (HTML, Python, JS, CSS, etc.)14 15 Args:16 original_content: The original file content to modify17 changes_text: Text containing SEARCH/REPLACE blocks18 19 Returns:20 Modified content with all search/replace blocks applied21 """22 if not changes_text.strip():23 return original_content24 25 # If the model didn't use the block markers, try a CSS-rule fallback where26 # provided blocks like `.selector { ... }` replace matching CSS rules.27 if (SEARCH_START not in changes_text) and (DIVIDER not in changes_text) and (REPLACE_END not in changes_text):28 try:29 import re # Local import to avoid global side effects30 updated_content = original_content31 replaced_any_rule = False32 # Find CSS-like rule blocks in the changes_text33 # This is a conservative matcher that looks for `selector { ... }`34 css_blocks = re.findall(r"([^{]+)\{([\s\S]*?)\}", changes_text, flags=re.MULTILINE)35 for selector_raw, body_raw in css_blocks:36 selector = selector_raw.strip()37 body = body_raw.strip()38 if not selector:39 continue40 # Build a regex to find the existing rule for this selector41 # Capture opening `{` and closing `}` to preserve them; replace inner body.42 pattern = re.compile(rf"({re.escape(selector)}\s*\{{)([\s\S]*?)(\}})")43 def _replace_rule(match):44 nonlocal replaced_any_rule45 replaced_any_rule = True46 prefix, existing_body, suffix = match.groups()47 # Preserve indentation of the existing first body line if present48 first_line_indent = ""49 for line in existing_body.splitlines():50 stripped = line.lstrip(" \t")51 if stripped:52 first_line_indent = line[: len(line) - len(stripped)]53 break54 # Re-indent provided body with the detected indent55 if body:56 new_body_lines = [first_line_indent + line if line.strip() else line for line in body.splitlines()]57 new_body_text = "\n" + "\n".join(new_body_lines) + "\n"58 else:59 new_body_text = existing_body # If empty body provided, keep existing60 return f"{prefix}{new_body_text}{suffix}"61 updated_content, num_subs = pattern.subn(_replace_rule, updated_content, count=1)62 if replaced_any_rule:63 return updated_content64 except Exception:65 # Fallback silently to the standard block-based application66 pass67 68 # Split the changes text into individual search/replace blocks69 blocks = []70 current_block = ""71 lines = changes_text.split('\n')72 73 for line in lines:74 if line.strip() == SEARCH_START:75 if current_block.strip():76 blocks.append(current_block.strip())77 current_block = line + '\n'78 elif line.strip() == REPLACE_END:79 current_block += line + '\n'80 blocks.append(current_block.strip())81 current_block = ""82 else:83 current_block += line + '\n'84 85 if current_block.strip():86 blocks.append(current_block.strip())87 88 modified_content = original_content89 90 for block in blocks:91 if not block.strip():92 continue93 94 # Parse the search/replace block95 lines = block.split('\n')96 search_lines = []97 replace_lines = []98 in_search = False99 in_replace = False100 101 for line in lines:102 if line.strip() == SEARCH_START:103 in_search = True104 in_replace = False105 elif line.strip() == DIVIDER:106 in_search = False107 in_replace = True108 elif line.strip() == REPLACE_END:109 in_replace = False110 elif in_search:111 search_lines.append(line)112 elif in_replace:113 replace_lines.append(line)114 115 # Apply the search/replace116 if search_lines:117 search_text = '\n'.join(search_lines).strip()118 replace_text = '\n'.join(replace_lines).strip()119 120 if search_text in modified_content:121 modified_content = modified_content.replace(search_text, replace_text)122 else:123 # If exact block match fails, attempt a CSS-rule fallback using the replace_text124 try:125 import re126 updated_content = modified_content127 replaced_any_rule = False128 css_blocks = re.findall(r"([^{]+)\{([\s\S]*?)\}", replace_text, flags=re.MULTILINE)129 for selector_raw, body_raw in css_blocks:130 selector = selector_raw.strip()131 body = body_raw.strip()132 if not selector:133 continue134 pattern = re.compile(rf"({re.escape(selector)}\s*\{{)([\s\S]*?)(\}})")135 def _replace_rule(match):136 nonlocal replaced_any_rule137 replaced_any_rule = True138 prefix, existing_body, suffix = match.groups()139 first_line_indent = ""140 for line in existing_body.splitlines():141 stripped = line.lstrip(" \t")142 if stripped:143 first_line_indent = line[: len(line) - len(stripped)]144 break145 if body:146 new_body_lines = [first_line_indent + line if line.strip() else line for line in body.splitlines()]147 new_body_text = "\n" + "\n".join(new_body_lines) + "\n"148 else:149 new_body_text = existing_body150 return f"{prefix}{new_body_text}{suffix}"151 updated_content, num_subs = pattern.subn(_replace_rule, updated_content, count=1)152 if replaced_any_rule:153 modified_content = updated_content154 else:155 print(f"[Search/Replace] Warning: Search text not found in content: {search_text[:100]}...")156 except Exception:157 print(f"[Search/Replace] Warning: Search text not found in content: {search_text[:100]}...")158 159 return modified_content160 161 162def has_search_replace_blocks(text: str) -> bool:163 """Check if text contains SEARCH/REPLACE block markers.164 165 Args:166 text: Text to check167 168 Returns:169 True if text contains search/replace markers, False otherwise170 """171 return (SEARCH_START in text) and (DIVIDER in text) and (REPLACE_END in text)172 173 174def parse_file_specific_changes(changes_text: str) -> dict:175 """Parse changes that specify which files to modify.176 177 Looks for patterns like:178 === components/Header.jsx ===179 \u003c\u003c\u003c\u003c\u003c\u003c\u003c SEARCH180 ...181 182 Returns:183 Dict mapping filename -> search/replace changes for that file184 """185 import re186 187 file_changes = {}188 189 # Pattern to match file sections: === filename ===190 file_pattern = re.compile(r"^===\s+([^\n=]+?)\s+===\s*$", re.MULTILINE)191 192 # Find all file sections193 matches = list(file_pattern.finditer(changes_text))194 195 if not matches:196 # No file-specific sections, treat entire text as changes197 return {"__all__": changes_text}198 199 for i, match in enumerate(matches):200 filename = match.group(1).strip()201 start_pos = match.end()202 203 # Find the end of this file's section (start of next file or end of text)204 if i + 1 < len(matches):205 end_pos = matches[i + 1].start()206 else:207 end_pos = len(changes_text)208 209 file_content = changes_text[start_pos:end_pos].strip()210 211 if file_content:212 file_changes[filename] = file_content213 214 return file_changes215 