aider-ce 0.87.2.dev9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of aider-ce might be problematic. Click here for more details.

Files changed (264) hide show
  1. aider/__init__.py +20 -0
  2. aider/__main__.py +4 -0
  3. aider/_version.py +34 -0
  4. aider/analytics.py +258 -0
  5. aider/args.py +1014 -0
  6. aider/args_formatter.py +228 -0
  7. aider/change_tracker.py +133 -0
  8. aider/coders/__init__.py +36 -0
  9. aider/coders/architect_coder.py +48 -0
  10. aider/coders/architect_prompts.py +40 -0
  11. aider/coders/ask_coder.py +9 -0
  12. aider/coders/ask_prompts.py +35 -0
  13. aider/coders/base_coder.py +3013 -0
  14. aider/coders/base_prompts.py +87 -0
  15. aider/coders/chat_chunks.py +64 -0
  16. aider/coders/context_coder.py +53 -0
  17. aider/coders/context_prompts.py +75 -0
  18. aider/coders/editblock_coder.py +657 -0
  19. aider/coders/editblock_fenced_coder.py +10 -0
  20. aider/coders/editblock_fenced_prompts.py +143 -0
  21. aider/coders/editblock_func_coder.py +141 -0
  22. aider/coders/editblock_func_prompts.py +27 -0
  23. aider/coders/editblock_prompts.py +177 -0
  24. aider/coders/editor_diff_fenced_coder.py +9 -0
  25. aider/coders/editor_diff_fenced_prompts.py +11 -0
  26. aider/coders/editor_editblock_coder.py +9 -0
  27. aider/coders/editor_editblock_prompts.py +21 -0
  28. aider/coders/editor_whole_coder.py +9 -0
  29. aider/coders/editor_whole_prompts.py +12 -0
  30. aider/coders/help_coder.py +16 -0
  31. aider/coders/help_prompts.py +46 -0
  32. aider/coders/navigator_coder.py +2711 -0
  33. aider/coders/navigator_legacy_prompts.py +338 -0
  34. aider/coders/navigator_prompts.py +530 -0
  35. aider/coders/patch_coder.py +706 -0
  36. aider/coders/patch_prompts.py +161 -0
  37. aider/coders/search_replace.py +757 -0
  38. aider/coders/shell.py +37 -0
  39. aider/coders/single_wholefile_func_coder.py +102 -0
  40. aider/coders/single_wholefile_func_prompts.py +27 -0
  41. aider/coders/udiff_coder.py +429 -0
  42. aider/coders/udiff_prompts.py +117 -0
  43. aider/coders/udiff_simple.py +14 -0
  44. aider/coders/udiff_simple_prompts.py +25 -0
  45. aider/coders/wholefile_coder.py +144 -0
  46. aider/coders/wholefile_func_coder.py +134 -0
  47. aider/coders/wholefile_func_prompts.py +27 -0
  48. aider/coders/wholefile_prompts.py +70 -0
  49. aider/commands.py +1946 -0
  50. aider/copypaste.py +72 -0
  51. aider/deprecated.py +126 -0
  52. aider/diffs.py +128 -0
  53. aider/dump.py +29 -0
  54. aider/editor.py +147 -0
  55. aider/exceptions.py +107 -0
  56. aider/format_settings.py +26 -0
  57. aider/gui.py +545 -0
  58. aider/help.py +163 -0
  59. aider/help_pats.py +19 -0
  60. aider/history.py +178 -0
  61. aider/io.py +1257 -0
  62. aider/linter.py +304 -0
  63. aider/llm.py +47 -0
  64. aider/main.py +1297 -0
  65. aider/mcp/__init__.py +94 -0
  66. aider/mcp/server.py +119 -0
  67. aider/mdstream.py +243 -0
  68. aider/models.py +1344 -0
  69. aider/onboarding.py +428 -0
  70. aider/openrouter.py +129 -0
  71. aider/prompts.py +56 -0
  72. aider/queries/tree-sitter-language-pack/README.md +7 -0
  73. aider/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
  74. aider/queries/tree-sitter-language-pack/c-tags.scm +9 -0
  75. aider/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
  76. aider/queries/tree-sitter-language-pack/clojure-tags.scm +7 -0
  77. aider/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
  78. aider/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
  79. aider/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
  80. aider/queries/tree-sitter-language-pack/d-tags.scm +26 -0
  81. aider/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
  82. aider/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
  83. aider/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
  84. aider/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
  85. aider/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
  86. aider/queries/tree-sitter-language-pack/go-tags.scm +42 -0
  87. aider/queries/tree-sitter-language-pack/java-tags.scm +20 -0
  88. aider/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
  89. aider/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
  90. aider/queries/tree-sitter-language-pack/matlab-tags.scm +10 -0
  91. aider/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
  92. aider/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
  93. aider/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
  94. aider/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
  95. aider/queries/tree-sitter-language-pack/python-tags.scm +14 -0
  96. aider/queries/tree-sitter-language-pack/r-tags.scm +21 -0
  97. aider/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
  98. aider/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
  99. aider/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
  100. aider/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
  101. aider/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
  102. aider/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
  103. aider/queries/tree-sitter-languages/README.md +23 -0
  104. aider/queries/tree-sitter-languages/c-tags.scm +9 -0
  105. aider/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
  106. aider/queries/tree-sitter-languages/cpp-tags.scm +15 -0
  107. aider/queries/tree-sitter-languages/dart-tags.scm +91 -0
  108. aider/queries/tree-sitter-languages/elisp-tags.scm +8 -0
  109. aider/queries/tree-sitter-languages/elixir-tags.scm +54 -0
  110. aider/queries/tree-sitter-languages/elm-tags.scm +19 -0
  111. aider/queries/tree-sitter-languages/go-tags.scm +30 -0
  112. aider/queries/tree-sitter-languages/hcl-tags.scm +77 -0
  113. aider/queries/tree-sitter-languages/java-tags.scm +20 -0
  114. aider/queries/tree-sitter-languages/javascript-tags.scm +88 -0
  115. aider/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
  116. aider/queries/tree-sitter-languages/matlab-tags.scm +10 -0
  117. aider/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
  118. aider/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
  119. aider/queries/tree-sitter-languages/php-tags.scm +26 -0
  120. aider/queries/tree-sitter-languages/python-tags.scm +12 -0
  121. aider/queries/tree-sitter-languages/ql-tags.scm +26 -0
  122. aider/queries/tree-sitter-languages/ruby-tags.scm +64 -0
  123. aider/queries/tree-sitter-languages/rust-tags.scm +60 -0
  124. aider/queries/tree-sitter-languages/scala-tags.scm +65 -0
  125. aider/queries/tree-sitter-languages/typescript-tags.scm +41 -0
  126. aider/reasoning_tags.py +82 -0
  127. aider/repo.py +621 -0
  128. aider/repomap.py +988 -0
  129. aider/report.py +200 -0
  130. aider/resources/__init__.py +3 -0
  131. aider/resources/model-metadata.json +699 -0
  132. aider/resources/model-settings.yml +2046 -0
  133. aider/run_cmd.py +132 -0
  134. aider/scrape.py +284 -0
  135. aider/sendchat.py +61 -0
  136. aider/special.py +203 -0
  137. aider/tools/__init__.py +26 -0
  138. aider/tools/command.py +58 -0
  139. aider/tools/command_interactive.py +53 -0
  140. aider/tools/delete_block.py +120 -0
  141. aider/tools/delete_line.py +112 -0
  142. aider/tools/delete_lines.py +137 -0
  143. aider/tools/extract_lines.py +276 -0
  144. aider/tools/grep.py +171 -0
  145. aider/tools/indent_lines.py +155 -0
  146. aider/tools/insert_block.py +211 -0
  147. aider/tools/list_changes.py +51 -0
  148. aider/tools/ls.py +49 -0
  149. aider/tools/make_editable.py +46 -0
  150. aider/tools/make_readonly.py +29 -0
  151. aider/tools/remove.py +48 -0
  152. aider/tools/replace_all.py +77 -0
  153. aider/tools/replace_line.py +125 -0
  154. aider/tools/replace_lines.py +160 -0
  155. aider/tools/replace_text.py +125 -0
  156. aider/tools/show_numbered_context.py +101 -0
  157. aider/tools/tool_utils.py +313 -0
  158. aider/tools/undo_change.py +60 -0
  159. aider/tools/view.py +13 -0
  160. aider/tools/view_files_at_glob.py +65 -0
  161. aider/tools/view_files_matching.py +103 -0
  162. aider/tools/view_files_with_symbol.py +121 -0
  163. aider/urls.py +17 -0
  164. aider/utils.py +454 -0
  165. aider/versioncheck.py +113 -0
  166. aider/voice.py +187 -0
  167. aider/waiting.py +221 -0
  168. aider/watch.py +318 -0
  169. aider/watch_prompts.py +12 -0
  170. aider/website/Gemfile +8 -0
  171. aider/website/_includes/blame.md +162 -0
  172. aider/website/_includes/get-started.md +22 -0
  173. aider/website/_includes/help-tip.md +5 -0
  174. aider/website/_includes/help.md +24 -0
  175. aider/website/_includes/install.md +5 -0
  176. aider/website/_includes/keys.md +4 -0
  177. aider/website/_includes/model-warnings.md +67 -0
  178. aider/website/_includes/multi-line.md +22 -0
  179. aider/website/_includes/python-m-aider.md +5 -0
  180. aider/website/_includes/recording.css +228 -0
  181. aider/website/_includes/recording.md +34 -0
  182. aider/website/_includes/replit-pipx.md +9 -0
  183. aider/website/_includes/works-best.md +1 -0
  184. aider/website/_sass/custom/custom.scss +103 -0
  185. aider/website/docs/config/adv-model-settings.md +2260 -0
  186. aider/website/docs/config/aider_conf.md +548 -0
  187. aider/website/docs/config/api-keys.md +90 -0
  188. aider/website/docs/config/dotenv.md +493 -0
  189. aider/website/docs/config/editor.md +127 -0
  190. aider/website/docs/config/mcp.md +95 -0
  191. aider/website/docs/config/model-aliases.md +104 -0
  192. aider/website/docs/config/options.md +890 -0
  193. aider/website/docs/config/reasoning.md +210 -0
  194. aider/website/docs/config.md +44 -0
  195. aider/website/docs/faq.md +384 -0
  196. aider/website/docs/git.md +76 -0
  197. aider/website/docs/index.md +47 -0
  198. aider/website/docs/install/codespaces.md +39 -0
  199. aider/website/docs/install/docker.md +57 -0
  200. aider/website/docs/install/optional.md +100 -0
  201. aider/website/docs/install/replit.md +8 -0
  202. aider/website/docs/install.md +115 -0
  203. aider/website/docs/languages.md +264 -0
  204. aider/website/docs/legal/contributor-agreement.md +111 -0
  205. aider/website/docs/legal/privacy.md +104 -0
  206. aider/website/docs/llms/anthropic.md +77 -0
  207. aider/website/docs/llms/azure.md +48 -0
  208. aider/website/docs/llms/bedrock.md +132 -0
  209. aider/website/docs/llms/cohere.md +34 -0
  210. aider/website/docs/llms/deepseek.md +32 -0
  211. aider/website/docs/llms/gemini.md +49 -0
  212. aider/website/docs/llms/github.md +111 -0
  213. aider/website/docs/llms/groq.md +36 -0
  214. aider/website/docs/llms/lm-studio.md +39 -0
  215. aider/website/docs/llms/ollama.md +75 -0
  216. aider/website/docs/llms/openai-compat.md +39 -0
  217. aider/website/docs/llms/openai.md +58 -0
  218. aider/website/docs/llms/openrouter.md +78 -0
  219. aider/website/docs/llms/other.md +111 -0
  220. aider/website/docs/llms/vertex.md +50 -0
  221. aider/website/docs/llms/warnings.md +10 -0
  222. aider/website/docs/llms/xai.md +53 -0
  223. aider/website/docs/llms.md +54 -0
  224. aider/website/docs/more/analytics.md +127 -0
  225. aider/website/docs/more/edit-formats.md +116 -0
  226. aider/website/docs/more/infinite-output.md +159 -0
  227. aider/website/docs/more-info.md +8 -0
  228. aider/website/docs/recordings/auto-accept-architect.md +31 -0
  229. aider/website/docs/recordings/dont-drop-original-read-files.md +35 -0
  230. aider/website/docs/recordings/index.md +21 -0
  231. aider/website/docs/recordings/model-accepts-settings.md +69 -0
  232. aider/website/docs/recordings/tree-sitter-language-pack.md +80 -0
  233. aider/website/docs/repomap.md +112 -0
  234. aider/website/docs/scripting.md +100 -0
  235. aider/website/docs/troubleshooting/aider-not-found.md +24 -0
  236. aider/website/docs/troubleshooting/edit-errors.md +76 -0
  237. aider/website/docs/troubleshooting/imports.md +62 -0
  238. aider/website/docs/troubleshooting/models-and-keys.md +54 -0
  239. aider/website/docs/troubleshooting/support.md +79 -0
  240. aider/website/docs/troubleshooting/token-limits.md +96 -0
  241. aider/website/docs/troubleshooting/warnings.md +12 -0
  242. aider/website/docs/troubleshooting.md +11 -0
  243. aider/website/docs/usage/browser.md +57 -0
  244. aider/website/docs/usage/caching.md +49 -0
  245. aider/website/docs/usage/commands.md +133 -0
  246. aider/website/docs/usage/conventions.md +119 -0
  247. aider/website/docs/usage/copypaste.md +121 -0
  248. aider/website/docs/usage/images-urls.md +48 -0
  249. aider/website/docs/usage/lint-test.md +118 -0
  250. aider/website/docs/usage/modes.md +211 -0
  251. aider/website/docs/usage/not-code.md +179 -0
  252. aider/website/docs/usage/notifications.md +87 -0
  253. aider/website/docs/usage/tips.md +79 -0
  254. aider/website/docs/usage/tutorials.md +30 -0
  255. aider/website/docs/usage/voice.md +121 -0
  256. aider/website/docs/usage/watch.md +294 -0
  257. aider/website/docs/usage.md +102 -0
  258. aider/website/share/index.md +101 -0
  259. aider_ce-0.87.2.dev9.dist-info/METADATA +543 -0
  260. aider_ce-0.87.2.dev9.dist-info/RECORD +264 -0
  261. aider_ce-0.87.2.dev9.dist-info/WHEEL +5 -0
  262. aider_ce-0.87.2.dev9.dist-info/entry_points.txt +3 -0
  263. aider_ce-0.87.2.dev9.dist-info/licenses/LICENSE.txt +202 -0
  264. aider_ce-0.87.2.dev9.dist-info/top_level.txt +1 -0
@@ -0,0 +1,276 @@
1
+ import os
2
+ import traceback
3
+
4
+ from .tool_utils import generate_unified_diff_snippet
5
+
6
+
7
+ def _execute_extract_lines(
8
+ coder,
9
+ source_file_path,
10
+ target_file_path,
11
+ start_pattern,
12
+ end_pattern=None,
13
+ line_count=None,
14
+ near_context=None,
15
+ occurrence=1,
16
+ dry_run=False,
17
+ ):
18
+ """
19
+ Extract a range of lines from a source file and move them to a target file.
20
+
21
+ Parameters:
22
+ - coder: The Coder instance
23
+ - source_file_path: Path to the file to extract lines from
24
+ - target_file_path: Path to the file to append extracted lines to (will be created if needed)
25
+ - start_pattern: Pattern marking the start of the block to extract
26
+ - end_pattern: Optional pattern marking the end of the block
27
+ - line_count: Optional number of lines to extract (alternative to end_pattern)
28
+ - near_context: Optional text nearby to help locate the correct instance of the start_pattern
29
+ - occurrence: Which occurrence of the start_pattern to use (1-based index, or -1 for last)
30
+ - dry_run: If True, simulate the change without modifying files
31
+
32
+ Returns a result message.
33
+ """
34
+ try:
35
+ # --- Validate Source File ---
36
+ abs_source_path = coder.abs_root_path(source_file_path)
37
+ rel_source_path = coder.get_rel_fname(abs_source_path)
38
+
39
+ if not os.path.isfile(abs_source_path):
40
+ coder.io.tool_error(f"Source file '{source_file_path}' not found")
41
+ return "Error: Source file not found"
42
+
43
+ if abs_source_path not in coder.abs_fnames:
44
+ if abs_source_path in coder.abs_read_only_fnames:
45
+ coder.io.tool_error(
46
+ f"Source file '{source_file_path}' is read-only. Use MakeEditable first."
47
+ )
48
+ return "Error: Source file is read-only. Use MakeEditable first."
49
+ else:
50
+ coder.io.tool_error(f"Source file '{source_file_path}' not in context")
51
+ return "Error: Source file not in context"
52
+
53
+ # --- Validate Target File ---
54
+ abs_target_path = coder.abs_root_path(target_file_path)
55
+ rel_target_path = coder.get_rel_fname(abs_target_path)
56
+ target_exists = os.path.isfile(abs_target_path)
57
+ target_is_editable = abs_target_path in coder.abs_fnames
58
+ target_is_readonly = abs_target_path in coder.abs_read_only_fnames
59
+
60
+ if target_exists and not target_is_editable:
61
+ if target_is_readonly:
62
+ coder.io.tool_error(
63
+ f"Target file '{target_file_path}' exists but is read-only. Use MakeEditable"
64
+ " first."
65
+ )
66
+ return "Error: Target file exists but is read-only. Use MakeEditable first."
67
+ else:
68
+ # This case shouldn't happen if file exists, but handle defensively
69
+ coder.io.tool_error(
70
+ f"Target file '{target_file_path}' exists but is not in context. Add it first."
71
+ )
72
+ return "Error: Target file exists but is not in context."
73
+
74
+ # --- Read Source Content ---
75
+ source_content = coder.io.read_text(abs_source_path)
76
+ if source_content is None:
77
+ coder.io.tool_error(
78
+ f"Could not read source file '{source_file_path}' before ExtractLines operation."
79
+ )
80
+ return f"Error: Could not read source file '{source_file_path}'"
81
+
82
+ # --- Find Extraction Range ---
83
+ if end_pattern and line_count:
84
+ coder.io.tool_error("Cannot specify both end_pattern and line_count")
85
+ return "Error: Cannot specify both end_pattern and line_count"
86
+
87
+ source_lines = source_content.splitlines()
88
+ original_source_content = source_content
89
+
90
+ start_pattern_line_indices = []
91
+ for i, line in enumerate(source_lines):
92
+ if start_pattern in line:
93
+ if near_context:
94
+ context_window_start = max(0, i - 5)
95
+ context_window_end = min(len(source_lines), i + 6)
96
+ context_block = "\n".join(source_lines[context_window_start:context_window_end])
97
+ if near_context in context_block:
98
+ start_pattern_line_indices.append(i)
99
+ else:
100
+ start_pattern_line_indices.append(i)
101
+
102
+ if not start_pattern_line_indices:
103
+ err_msg = f"Start pattern '{start_pattern}' not found"
104
+ if near_context:
105
+ err_msg += f" near context '{near_context}'"
106
+ err_msg += f" in source file '{source_file_path}'."
107
+ coder.io.tool_error(err_msg)
108
+ return f"Error: {err_msg}"
109
+
110
+ num_occurrences = len(start_pattern_line_indices)
111
+ try:
112
+ occurrence = int(occurrence)
113
+ if occurrence == -1:
114
+ target_idx = num_occurrences - 1
115
+ elif occurrence > 0 and occurrence <= num_occurrences:
116
+ target_idx = occurrence - 1
117
+ else:
118
+ err_msg = (
119
+ f"Occurrence number {occurrence} is out of range for start pattern"
120
+ f" '{start_pattern}'. Found {num_occurrences} occurrences"
121
+ )
122
+ if near_context:
123
+ err_msg += f" near '{near_context}'"
124
+ err_msg += f" in '{source_file_path}'."
125
+ coder.io.tool_error(err_msg)
126
+ return f"Error: {err_msg}"
127
+ except ValueError:
128
+ coder.io.tool_error(f"Invalid occurrence value: '{occurrence}'. Must be an integer.")
129
+ return f"Error: Invalid occurrence value '{occurrence}'"
130
+
131
+ start_line = start_pattern_line_indices[target_idx]
132
+ occurrence_str = f"occurrence {occurrence} of " if num_occurrences > 1 else ""
133
+
134
+ end_line = -1
135
+ if end_pattern:
136
+ for i in range(start_line, len(source_lines)):
137
+ if end_pattern in source_lines[i]:
138
+ end_line = i
139
+ break
140
+ if end_line == -1:
141
+ err_msg = (
142
+ f"End pattern '{end_pattern}' not found after {occurrence_str}start pattern"
143
+ f" '{start_pattern}' (line {start_line + 1}) in '{source_file_path}'."
144
+ )
145
+ coder.io.tool_error(err_msg)
146
+ return f"Error: {err_msg}"
147
+ elif line_count:
148
+ try:
149
+ line_count = int(line_count)
150
+ if line_count <= 0:
151
+ raise ValueError("Line count must be positive")
152
+ end_line = min(start_line + line_count - 1, len(source_lines) - 1)
153
+ except ValueError:
154
+ coder.io.tool_error(
155
+ f"Invalid line_count value: '{line_count}'. Must be a positive integer."
156
+ )
157
+ return f"Error: Invalid line_count value '{line_count}'"
158
+ else:
159
+ end_line = start_line # Extract just the start line if no end specified
160
+
161
+ # --- Prepare Content Changes ---
162
+ extracted_lines = source_lines[start_line : end_line + 1]
163
+ new_source_lines = source_lines[:start_line] + source_lines[end_line + 1 :]
164
+ new_source_content = "\n".join(new_source_lines)
165
+
166
+ target_content = ""
167
+ if target_exists:
168
+ target_content = coder.io.read_text(abs_target_path)
169
+ if target_content is None:
170
+ coder.io.tool_error(f"Could not read existing target file '{target_file_path}'.")
171
+ return f"Error: Could not read target file '{target_file_path}'"
172
+ original_target_content = target_content # For tracking
173
+
174
+ # Append extracted lines to target content, ensuring a newline if target wasn't empty
175
+ extracted_block = "\n".join(extracted_lines)
176
+ if target_content and not target_content.endswith("\n"):
177
+ target_content += "\n" # Add newline before appending if needed
178
+ new_target_content = target_content + extracted_block
179
+
180
+ # --- Generate Diffs ---
181
+ source_diff_snippet = generate_unified_diff_snippet(
182
+ original_source_content, new_source_content, rel_source_path
183
+ )
184
+ target_insertion_line = len(target_content.splitlines()) if target_content else 0
185
+ target_diff_snippet = generate_unified_diff_snippet(
186
+ original_target_content, new_target_content, rel_target_path
187
+ )
188
+
189
+ # --- Handle Dry Run ---
190
+ if dry_run:
191
+ num_extracted = end_line - start_line + 1
192
+ target_action = "append to" if target_exists else "create"
193
+ coder.io.tool_output(
194
+ f"Dry run: Would extract {num_extracted} lines (from {occurrence_str}start pattern"
195
+ f" '{start_pattern}') in {source_file_path} and {target_action} {target_file_path}"
196
+ )
197
+ # Provide more informative dry run response with diffs
198
+ return (
199
+ f"Dry run: Would extract {num_extracted} lines from {rel_source_path} and"
200
+ f" {target_action} {rel_target_path}.\nSource Diff"
201
+ f" (Deletion):\n{source_diff_snippet}\nTarget Diff"
202
+ f" (Insertion):\n{target_diff_snippet}"
203
+ )
204
+
205
+ # --- Apply Changes (Not Dry Run) ---
206
+ coder.io.write_text(abs_source_path, new_source_content)
207
+ coder.io.write_text(abs_target_path, new_target_content)
208
+
209
+ # --- Track Changes ---
210
+ source_change_id = "TRACKING_FAILED"
211
+ target_change_id = "TRACKING_FAILED"
212
+ try:
213
+ source_metadata = {
214
+ "start_line": start_line + 1,
215
+ "end_line": end_line + 1,
216
+ "start_pattern": start_pattern,
217
+ "end_pattern": end_pattern,
218
+ "line_count": line_count,
219
+ "near_context": near_context,
220
+ "occurrence": occurrence,
221
+ "extracted_content": extracted_block,
222
+ "target_file": rel_target_path,
223
+ }
224
+ source_change_id = coder.change_tracker.track_change(
225
+ file_path=rel_source_path,
226
+ change_type="extractlines_source",
227
+ original_content=original_source_content,
228
+ new_content=new_source_content,
229
+ metadata=source_metadata,
230
+ )
231
+ except Exception as track_e:
232
+ coder.io.tool_error(f"Error tracking source change for ExtractLines: {track_e}")
233
+
234
+ try:
235
+ target_metadata = {
236
+ "insertion_line": target_insertion_line + 1,
237
+ "inserted_content": extracted_block,
238
+ "source_file": rel_source_path,
239
+ }
240
+ target_change_id = coder.change_tracker.track_change(
241
+ file_path=rel_target_path,
242
+ change_type="extractlines_target",
243
+ original_content=original_target_content,
244
+ new_content=new_target_content,
245
+ metadata=target_metadata,
246
+ )
247
+ except Exception as track_e:
248
+ coder.io.tool_error(f"Error tracking target change for ExtractLines: {track_e}")
249
+
250
+ # --- Update Context ---
251
+ coder.aider_edited_files.add(rel_source_path)
252
+ coder.aider_edited_files.add(rel_target_path)
253
+ if not target_exists:
254
+ # Add the newly created file to editable context
255
+ coder.abs_fnames.add(abs_target_path)
256
+ coder.io.tool_output(f"✨ Created and added '{target_file_path}' to editable context.")
257
+
258
+ # --- Return Result ---
259
+ num_extracted = end_line - start_line + 1
260
+ target_action = "appended to" if target_exists else "created"
261
+ coder.io.tool_output(
262
+ f"✅ Extracted {num_extracted} lines from {rel_source_path} (change_id:"
263
+ f" {source_change_id}) and {target_action} {rel_target_path} (change_id:"
264
+ f" {target_change_id})"
265
+ )
266
+ # Provide more informative success response with change IDs and diffs
267
+ return (
268
+ f"Successfully extracted {num_extracted} lines from {rel_source_path} and"
269
+ f" {target_action} {rel_target_path}.\nSource Change ID: {source_change_id}\nSource"
270
+ f" Diff (Deletion):\n{source_diff_snippet}\nTarget Change ID:"
271
+ f" {target_change_id}\nTarget Diff (Insertion):\n{target_diff_snippet}"
272
+ )
273
+
274
+ except Exception as e:
275
+ coder.io.tool_error(f"Error in ExtractLines: {str(e)}\n{traceback.format_exc()}")
276
+ return f"Error: {str(e)}"
aider/tools/grep.py ADDED
@@ -0,0 +1,171 @@
1
+ import shlex
2
+ import shutil
3
+ from pathlib import Path
4
+
5
+ from aider.run_cmd import run_cmd_subprocess
6
+
7
+
8
+ def _find_search_tool():
9
+ """Find the best available command-line search tool (rg, ag, grep)."""
10
+ if shutil.which("rg"):
11
+ return "rg", shutil.which("rg")
12
+ elif shutil.which("ag"):
13
+ return "ag", shutil.which("ag")
14
+ elif shutil.which("grep"):
15
+ return "grep", shutil.which("grep")
16
+ else:
17
+ return None, None
18
+
19
+
20
+ def _execute_grep(
21
+ coder,
22
+ pattern,
23
+ file_pattern="*",
24
+ directory=".",
25
+ use_regex=False,
26
+ case_insensitive=False,
27
+ context_before=5,
28
+ context_after=5,
29
+ ):
30
+ """
31
+ Search for lines matching a pattern in files within the project repository.
32
+ Uses rg (ripgrep), ag (the silver searcher), or grep, whichever is available.
33
+
34
+ Args:
35
+ coder: The Coder instance.
36
+ pattern (str): The pattern to search for.
37
+ file_pattern (str, optional): Glob pattern to filter files. Defaults to "*".
38
+ directory (str, optional): Directory to search within relative to repo root. Defaults to ".".
39
+ use_regex (bool, optional): Whether the pattern is a regular expression. Defaults to False.
40
+ case_insensitive (bool, optional): Whether the search should be case-insensitive. Defaults to False.
41
+ context_before (int, optional): Number of context lines to show before matches. Defaults to 5.
42
+ context_after (int, optional): Number of context lines to show after matches. Defaults to 5.
43
+
44
+ Returns:
45
+ str: Formatted result indicating success or failure, including matching lines or error message.
46
+ """
47
+ repo = coder.repo
48
+ if not repo:
49
+ coder.io.tool_error("Not in a git repository.")
50
+ return "Error: Not in a git repository."
51
+
52
+ tool_name, tool_path = _find_search_tool()
53
+ if not tool_path:
54
+ coder.io.tool_error("No search tool (rg, ag, grep) found in PATH.")
55
+ return "Error: No search tool (rg, ag, grep) found."
56
+
57
+ try:
58
+ search_dir_path = Path(repo.root) / directory
59
+ if not search_dir_path.is_dir():
60
+ coder.io.tool_error(f"Directory not found: {directory}")
61
+ return f"Error: Directory not found: {directory}"
62
+
63
+ # Build the command arguments based on the available tool
64
+ cmd_args = [tool_path]
65
+
66
+ # Common options or tool-specific equivalents
67
+ if tool_name in ["rg", "grep"]:
68
+ cmd_args.append("-n") # Line numbers for rg and grep
69
+ # ag includes line numbers by default
70
+
71
+ # Context lines (Before and After)
72
+ if context_before > 0:
73
+ # All tools use -B for lines before
74
+ cmd_args.extend(["-B", str(context_before)])
75
+ if context_after > 0:
76
+ # All tools use -A for lines after
77
+ cmd_args.extend(["-A", str(context_after)])
78
+
79
+ # Case sensitivity
80
+ if case_insensitive:
81
+ cmd_args.append("-i") # Add case-insensitivity flag for all tools
82
+
83
+ # Pattern type (regex vs fixed string)
84
+ if use_regex:
85
+ if tool_name == "grep":
86
+ cmd_args.append("-E") # Use extended regex for grep
87
+ # rg and ag use regex by default, no flag needed for basic ERE
88
+ else:
89
+ if tool_name == "rg":
90
+ cmd_args.append("-F") # Fixed strings for rg
91
+ elif tool_name == "ag":
92
+ cmd_args.append("-Q") # Literal/fixed strings for ag
93
+ elif tool_name == "grep":
94
+ cmd_args.append("-F") # Fixed strings for grep
95
+
96
+ # File filtering
97
+ if (
98
+ file_pattern != "*"
99
+ ): # Avoid adding glob if it's the default '*' which might behave differently
100
+ if tool_name == "rg":
101
+ cmd_args.extend(["-g", file_pattern])
102
+ elif tool_name == "ag":
103
+ cmd_args.extend(["-G", file_pattern])
104
+ elif tool_name == "grep":
105
+ # grep needs recursive flag when filtering
106
+ cmd_args.append("-r")
107
+ cmd_args.append(f"--include={file_pattern}")
108
+ elif tool_name == "grep":
109
+ # grep needs recursive flag even without include filter
110
+ cmd_args.append("-r")
111
+
112
+ # Directory exclusion (rg and ag respect .gitignore/.git by default)
113
+ if tool_name == "grep":
114
+ cmd_args.append("--exclude-dir=.git")
115
+
116
+ # Add pattern and directory path
117
+ cmd_args.extend([pattern, str(search_dir_path)])
118
+
119
+ # Convert list to command string for run_cmd_subprocess
120
+ command_string = shlex.join(cmd_args)
121
+
122
+ coder.io.tool_output(f"⚙️ Executing {tool_name}: {command_string}")
123
+
124
+ # Use run_cmd_subprocess for execution
125
+ # Note: rg, ag, and grep return 1 if no matches are found, which is not an error for this tool.
126
+ exit_status, combined_output = run_cmd_subprocess(
127
+ command_string, verbose=coder.verbose, cwd=coder.root # Execute in the project root
128
+ )
129
+
130
+ # Format the output for the result message
131
+ output_content = combined_output or ""
132
+
133
+ # Handle exit codes (consistent across rg, ag, grep)
134
+ if exit_status == 0:
135
+ # Limit output size if necessary
136
+ max_output_lines = 50 # Consider making this configurable
137
+ output_lines = output_content.splitlines()
138
+ if len(output_lines) > max_output_lines:
139
+ truncated_output = "\n".join(output_lines[:max_output_lines])
140
+ result_message = (
141
+ f"Found matches (truncated):\n```text\n{truncated_output}\n..."
142
+ f" ({len(output_lines) - max_output_lines} more lines)\n```"
143
+ )
144
+ elif not output_content:
145
+ # Should not happen if return code is 0, but handle defensively
146
+ coder.io.tool_warning(f"{tool_name} returned 0 but produced no output.")
147
+ result_message = "No matches found (unexpected)."
148
+ else:
149
+ result_message = f"Found matches:\n```text\n{output_content}\n```"
150
+ return result_message
151
+
152
+ elif exit_status == 1:
153
+ # Exit code 1 means no matches found - this is expected behavior, not an error.
154
+ return "No matches found."
155
+ else:
156
+ # Exit code > 1 indicates an actual error
157
+ error_message = f"{tool_name.capitalize()} command failed with exit code {exit_status}."
158
+ if output_content:
159
+ # Truncate error output as well if it's too long
160
+ error_limit = 1000 # Example limit for error output
161
+ if len(output_content) > error_limit:
162
+ output_content = output_content[:error_limit] + "\n... (error output truncated)"
163
+ error_message += f" Output:\n{output_content}"
164
+ coder.io.tool_error(error_message)
165
+ return f"Error: {error_message}"
166
+
167
+ except Exception as e:
168
+ # Add command_string to the error message if it's defined
169
+ cmd_str_info = f"'{command_string}' " if "command_string" in locals() else ""
170
+ coder.io.tool_error(f"Error executing {tool_name} command {cmd_str_info}: {str(e)}")
171
+ return f"Error executing {tool_name}: {str(e)}"
@@ -0,0 +1,155 @@
1
+ from .tool_utils import (
2
+ ToolError,
3
+ apply_change,
4
+ determine_line_range,
5
+ find_pattern_indices,
6
+ format_tool_result,
7
+ generate_unified_diff_snippet,
8
+ handle_tool_error,
9
+ select_occurrence_index,
10
+ validate_file_for_edit,
11
+ )
12
+
13
+
14
+ def _execute_indent_lines(
15
+ coder,
16
+ file_path,
17
+ start_pattern,
18
+ end_pattern=None,
19
+ line_count=None,
20
+ indent_levels=1,
21
+ near_context=None,
22
+ occurrence=1,
23
+ change_id=None,
24
+ dry_run=False,
25
+ ):
26
+ """
27
+ Indent or unindent a block of lines in a file using utility functions.
28
+
29
+ Parameters:
30
+ - coder: The Coder instance
31
+ - file_path: Path to the file to modify
32
+ - start_pattern: Pattern marking the start of the block to indent (line containing this pattern)
33
+ - end_pattern: Optional pattern marking the end of the block (line containing this pattern)
34
+ - line_count: Optional number of lines to indent (alternative to end_pattern)
35
+ - indent_levels: Number of levels to indent (positive) or unindent (negative)
36
+ - near_context: Optional text nearby to help locate the correct instance of the start_pattern
37
+ - occurrence: Which occurrence of the start_pattern to use (1-based index, or -1 for last)
38
+ - change_id: Optional ID for tracking the change
39
+ - dry_run: If True, simulate the change without modifying the file
40
+
41
+ Returns a result message.
42
+ """
43
+ tool_name = "IndentLines"
44
+ try:
45
+ # 1. Validate file and get content
46
+ abs_path, rel_path, original_content = validate_file_for_edit(coder, file_path)
47
+ lines = original_content.splitlines()
48
+
49
+ # 2. Find the start line
50
+ pattern_desc = f"Start pattern '{start_pattern}'"
51
+ if near_context:
52
+ pattern_desc += f" near context '{near_context}'"
53
+ start_pattern_indices = find_pattern_indices(lines, start_pattern, near_context)
54
+ start_line_idx = select_occurrence_index(start_pattern_indices, occurrence, pattern_desc)
55
+
56
+ # 3. Determine the end line
57
+ start_line, end_line = determine_line_range(
58
+ coder=coder,
59
+ file_path=rel_path,
60
+ lines=lines,
61
+ start_pattern_line_index=start_line_idx,
62
+ end_pattern=end_pattern,
63
+ line_count=line_count,
64
+ target_symbol=None, # IndentLines uses patterns, not symbols
65
+ pattern_desc=pattern_desc,
66
+ )
67
+
68
+ # 4. Validate and prepare indentation
69
+ try:
70
+ indent_levels = int(indent_levels)
71
+ except ValueError:
72
+ raise ToolError(f"Invalid indent_levels value: '{indent_levels}'. Must be an integer.")
73
+
74
+ indent_str = " " * 4 # Assume 4 spaces per level
75
+ modified_lines = list(lines)
76
+
77
+ # Apply indentation logic (core logic remains)
78
+ for i in range(start_line, end_line + 1):
79
+ if indent_levels > 0:
80
+ modified_lines[i] = (indent_str * indent_levels) + modified_lines[i]
81
+ elif indent_levels < 0:
82
+ spaces_to_remove = abs(indent_levels) * len(indent_str)
83
+ current_leading_spaces = len(modified_lines[i]) - len(modified_lines[i].lstrip(" "))
84
+ actual_remove = min(spaces_to_remove, current_leading_spaces)
85
+ if actual_remove > 0:
86
+ modified_lines[i] = modified_lines[i][actual_remove:]
87
+
88
+ new_content = "\n".join(modified_lines)
89
+
90
+ if original_content == new_content:
91
+ coder.io.tool_warning("No changes made: indentation would not change file")
92
+ return "Warning: No changes made (indentation would not change file)"
93
+
94
+ # 5. Generate diff for feedback
95
+ diff_snippet = generate_unified_diff_snippet(original_content, new_content, rel_path)
96
+ num_occurrences = len(start_pattern_indices)
97
+ occurrence_str = f"occurrence {occurrence} of " if num_occurrences > 1 else ""
98
+ action = "indent" if indent_levels > 0 else "unindent"
99
+ levels = abs(indent_levels)
100
+ level_text = "level" if levels == 1 else "levels"
101
+ num_lines = end_line - start_line + 1
102
+
103
+ # 6. Handle dry run
104
+ if dry_run:
105
+ dry_run_message = (
106
+ f"Dry run: Would {action} {num_lines} lines ({start_line + 1}-{end_line + 1}) by"
107
+ f" {levels} {level_text} (based on {occurrence_str}start pattern '{start_pattern}')"
108
+ f" in {file_path}."
109
+ )
110
+ return format_tool_result(
111
+ coder,
112
+ tool_name,
113
+ "",
114
+ dry_run=True,
115
+ dry_run_message=dry_run_message,
116
+ diff_snippet=diff_snippet,
117
+ )
118
+
119
+ # 7. Apply Change (Not dry run)
120
+ metadata = {
121
+ "start_line": start_line + 1,
122
+ "end_line": end_line + 1,
123
+ "start_pattern": start_pattern,
124
+ "end_pattern": end_pattern,
125
+ "line_count": line_count,
126
+ "indent_levels": indent_levels,
127
+ "near_context": near_context,
128
+ "occurrence": occurrence,
129
+ }
130
+ final_change_id = apply_change(
131
+ coder,
132
+ abs_path,
133
+ rel_path,
134
+ original_content,
135
+ new_content,
136
+ "indentlines",
137
+ metadata,
138
+ change_id,
139
+ )
140
+
141
+ # 8. Format and return result
142
+ action_past = "Indented" if indent_levels > 0 else "Unindented"
143
+ success_message = (
144
+ f"{action_past} {num_lines} lines by {levels} {level_text} (from {occurrence_str}start"
145
+ f" pattern) in {file_path}"
146
+ )
147
+ return format_tool_result(
148
+ coder, tool_name, success_message, change_id=final_change_id, diff_snippet=diff_snippet
149
+ )
150
+ except ToolError as e:
151
+ # Handle errors raised by utility functions (expected errors)
152
+ return handle_tool_error(coder, tool_name, e, add_traceback=False)
153
+ except Exception as e:
154
+ # Handle unexpected errors
155
+ return handle_tool_error(coder, tool_name, e)