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,160 @@
1
+ import os
2
+
3
+ from .tool_utils import (
4
+ ToolError,
5
+ apply_change,
6
+ format_tool_result,
7
+ generate_unified_diff_snippet,
8
+ handle_tool_error,
9
+ )
10
+
11
+
12
+ def _execute_replace_lines(
13
+ coder, file_path, start_line, end_line, new_content, change_id=None, dry_run=False
14
+ ):
15
+ """
16
+ Replace a range of lines identified by line numbers.
17
+ Useful for fixing errors identified by error messages or linters.
18
+
19
+ Parameters:
20
+ - file_path: Path to the file to modify
21
+ - start_line: The first line number to replace (1-based)
22
+ - end_line: The last line number to replace (1-based)
23
+ - new_content: New content for the lines (can be multi-line)
24
+ - change_id: Optional ID for tracking the change
25
+ - dry_run: If True, simulate the change without modifying the file
26
+
27
+ Returns a result message.
28
+ """
29
+ tool_name = "ReplaceLines"
30
+ try:
31
+ # Get absolute file path
32
+ abs_path = coder.abs_root_path(file_path)
33
+ rel_path = coder.get_rel_fname(abs_path)
34
+
35
+ # Check if file exists
36
+ if not os.path.isfile(abs_path):
37
+ raise ToolError(f"File '{file_path}' not found")
38
+
39
+ # Check if file is in editable context
40
+ if abs_path not in coder.abs_fnames:
41
+ if abs_path in coder.abs_read_only_fnames:
42
+ raise ToolError(f"File '{file_path}' is read-only. Use MakeEditable first.")
43
+ else:
44
+ raise ToolError(f"File '{file_path}' not in context")
45
+
46
+ # Reread file content immediately before modification
47
+ file_content = coder.io.read_text(abs_path)
48
+ if file_content is None:
49
+ raise ToolError(f"Could not read file '{file_path}'")
50
+
51
+ # Convert line numbers to integers if needed
52
+ try:
53
+ start_line = int(start_line)
54
+ except ValueError:
55
+ raise ToolError(f"Invalid start_line value: '{start_line}'. Must be an integer.")
56
+
57
+ try:
58
+ end_line = int(end_line)
59
+ except ValueError:
60
+ raise ToolError(f"Invalid end_line value: '{end_line}'. Must be an integer.")
61
+
62
+ # Split into lines
63
+ lines = file_content.splitlines()
64
+
65
+ # Convert 1-based line numbers to 0-based indices
66
+ start_idx = start_line - 1
67
+ end_idx = end_line - 1
68
+
69
+ # Validate line numbers
70
+ if start_idx < 0 or start_idx >= len(lines):
71
+ raise ToolError(
72
+ f"Start line {start_line} is out of range for file '{file_path}' (has"
73
+ f" {len(lines)} lines)."
74
+ )
75
+
76
+ if end_idx < start_idx or end_idx >= len(lines):
77
+ raise ToolError(
78
+ f"End line {end_line} is out of range for file '{file_path}' (must be >= start line"
79
+ f" {start_line} and <= {len(lines)})."
80
+ )
81
+
82
+ # Store original content for change tracking
83
+ original_content = file_content
84
+ replaced_lines = lines[start_idx : end_idx + 1]
85
+
86
+ # Split the new content into lines
87
+ new_lines = new_content.splitlines()
88
+
89
+ # Perform the replacement
90
+ new_full_lines = lines[:start_idx] + new_lines + lines[end_idx + 1 :]
91
+ new_content_full = "\n".join(new_full_lines)
92
+
93
+ if original_content == new_content_full:
94
+ coder.io.tool_warning("No changes made: new content is identical to original")
95
+ return "Warning: No changes made (new content identical to original)"
96
+
97
+ # Generate diff snippet
98
+ diff_snippet = generate_unified_diff_snippet(original_content, new_content_full, rel_path)
99
+
100
+ # Create a readable diff for the lines replacement
101
+ diff = f"Lines {start_line}-{end_line}:\n"
102
+ # Add removed lines with - prefix
103
+ for line in replaced_lines:
104
+ diff += f"- {line}\n"
105
+ # Add separator
106
+ diff += "---\n"
107
+ # Add new lines with + prefix
108
+ for line in new_lines:
109
+ diff += f"+ {line}\n"
110
+
111
+ # Handle dry run
112
+ if dry_run:
113
+ dry_run_message = f"Dry run: Would replace lines {start_line}-{end_line} in {file_path}"
114
+ return format_tool_result(
115
+ coder,
116
+ tool_name,
117
+ "",
118
+ dry_run=True,
119
+ dry_run_message=dry_run_message,
120
+ diff_snippet=diff_snippet,
121
+ )
122
+
123
+ # --- Apply Change (Not dry run) ---
124
+ metadata = {
125
+ "start_line": start_line,
126
+ "end_line": end_line,
127
+ "replaced_lines": replaced_lines,
128
+ "new_lines": new_lines,
129
+ }
130
+
131
+ final_change_id = apply_change(
132
+ coder,
133
+ abs_path,
134
+ rel_path,
135
+ original_content,
136
+ new_content_full,
137
+ "replacelines",
138
+ metadata,
139
+ change_id,
140
+ )
141
+
142
+ coder.aider_edited_files.add(rel_path)
143
+ replaced_count = end_line - start_line + 1
144
+ new_count = len(new_lines)
145
+
146
+ # Format and return result
147
+ success_message = (
148
+ f"Replaced lines {start_line}-{end_line} ({replaced_count} lines) with {new_count} new"
149
+ f" lines in {file_path}"
150
+ )
151
+ return format_tool_result(
152
+ coder, tool_name, success_message, change_id=final_change_id, diff_snippet=diff_snippet
153
+ )
154
+
155
+ except ToolError as e:
156
+ # Handle errors raised by utility functions (expected errors)
157
+ return handle_tool_error(coder, tool_name, e, add_traceback=False)
158
+ except Exception as e:
159
+ # Handle unexpected errors
160
+ return handle_tool_error(coder, tool_name, e)
@@ -0,0 +1,125 @@
1
+ from .tool_utils import (
2
+ ToolError,
3
+ apply_change,
4
+ format_tool_result,
5
+ generate_unified_diff_snippet,
6
+ handle_tool_error,
7
+ validate_file_for_edit,
8
+ )
9
+
10
+
11
+ def _execute_replace_text(
12
+ coder,
13
+ file_path,
14
+ find_text,
15
+ replace_text,
16
+ near_context=None,
17
+ occurrence=1,
18
+ change_id=None,
19
+ dry_run=False,
20
+ ):
21
+ """
22
+ Replace specific text with new text, optionally using nearby context for disambiguation.
23
+ Uses utility functions for validation, finding occurrences, and applying changes.
24
+ """
25
+ tool_name = "ReplaceText"
26
+ try:
27
+ # 1. Validate file and get content
28
+ abs_path, rel_path, original_content = validate_file_for_edit(coder, file_path)
29
+
30
+ # 2. Find occurrences using helper function
31
+ # Note: _find_occurrences is currently on the Coder class, not in tool_utils
32
+ occurrences = coder._find_occurrences(original_content, find_text, near_context)
33
+
34
+ if not occurrences:
35
+ err_msg = f"Text '{find_text}' not found"
36
+ if near_context:
37
+ err_msg += f" near context '{near_context}'"
38
+ err_msg += f" in file '{file_path}'."
39
+ raise ToolError(err_msg)
40
+
41
+ # 3. Select the occurrence index
42
+ num_occurrences = len(occurrences)
43
+ try:
44
+ occurrence = int(occurrence)
45
+ if occurrence == -1:
46
+ if num_occurrences == 0:
47
+ raise ToolError(f"Text '{find_text}' not found, cannot select last occurrence.")
48
+ target_idx = num_occurrences - 1
49
+ elif 1 <= occurrence <= num_occurrences:
50
+ target_idx = occurrence - 1 # Convert 1-based to 0-based
51
+ else:
52
+ err_msg = (
53
+ f"Occurrence number {occurrence} is out of range. Found"
54
+ f" {num_occurrences} occurrences of '{find_text}'"
55
+ )
56
+ if near_context:
57
+ err_msg += f" near '{near_context}'"
58
+ err_msg += f" in '{file_path}'."
59
+ raise ToolError(err_msg)
60
+ except ValueError:
61
+ raise ToolError(f"Invalid occurrence value: '{occurrence}'. Must be an integer.")
62
+
63
+ start_index = occurrences[target_idx]
64
+
65
+ # 4. Perform the replacement
66
+ new_content = (
67
+ original_content[:start_index]
68
+ + replace_text
69
+ + original_content[start_index + len(find_text) :]
70
+ )
71
+
72
+ if original_content == new_content:
73
+ coder.io.tool_warning("No changes made: replacement text is identical to original")
74
+ return "Warning: No changes made (replacement identical to original)"
75
+
76
+ # 5. Generate diff for feedback
77
+ # Note: _generate_diff_snippet is currently on the Coder class
78
+ diff_snippet = generate_unified_diff_snippet(original_content, new_content, rel_path)
79
+ occurrence_str = f"occurrence {occurrence}" if num_occurrences > 1 else "text"
80
+
81
+ # 6. Handle dry run
82
+ if dry_run:
83
+ dry_run_message = (
84
+ f"Dry run: Would replace {occurrence_str} of '{find_text}' in {file_path}."
85
+ )
86
+ return format_tool_result(
87
+ coder,
88
+ tool_name,
89
+ "",
90
+ dry_run=True,
91
+ dry_run_message=dry_run_message,
92
+ diff_snippet=diff_snippet,
93
+ )
94
+
95
+ # 7. Apply Change (Not dry run)
96
+ metadata = {
97
+ "start_index": start_index,
98
+ "find_text": find_text,
99
+ "replace_text": replace_text,
100
+ "near_context": near_context,
101
+ "occurrence": occurrence,
102
+ }
103
+ final_change_id = apply_change(
104
+ coder,
105
+ abs_path,
106
+ rel_path,
107
+ original_content,
108
+ new_content,
109
+ "replacetext",
110
+ metadata,
111
+ change_id,
112
+ )
113
+
114
+ # 8. Format and return result
115
+ success_message = f"Replaced {occurrence_str} in {file_path}"
116
+ return format_tool_result(
117
+ coder, tool_name, success_message, change_id=final_change_id, diff_snippet=diff_snippet
118
+ )
119
+
120
+ except ToolError as e:
121
+ # Handle errors raised by utility functions or explicitly raised here
122
+ return handle_tool_error(coder, tool_name, e, add_traceback=False)
123
+ except Exception as e:
124
+ # Handle unexpected errors
125
+ return handle_tool_error(coder, tool_name, e)
@@ -0,0 +1,101 @@
1
+ import os
2
+
3
+ from .tool_utils import ToolError, handle_tool_error, resolve_paths
4
+
5
+
6
+ def execute_show_numbered_context(
7
+ coder, file_path, pattern=None, line_number=None, context_lines=3
8
+ ):
9
+ """
10
+ Displays numbered lines from file_path centered around a target location
11
+ (pattern or line_number), without adding the file to context.
12
+ Uses utility functions for path resolution and error handling.
13
+ """
14
+ tool_name = "ShowNumberedContext"
15
+ try:
16
+ # 1. Validate arguments
17
+ if not (pattern is None) ^ (line_number is None):
18
+ raise ToolError("Provide exactly one of 'pattern' or 'line_number'.")
19
+
20
+ # 2. Resolve path
21
+ abs_path, rel_path = resolve_paths(coder, file_path)
22
+ if not os.path.exists(abs_path):
23
+ # Check existence after resolving, as resolve_paths doesn't guarantee existence
24
+ raise ToolError(f"File not found: {file_path}")
25
+
26
+ # 3. Read file content
27
+ content = coder.io.read_text(abs_path)
28
+ if content is None:
29
+ raise ToolError(f"Could not read file: {file_path}")
30
+ lines = content.splitlines()
31
+ num_lines = len(lines)
32
+
33
+ # 4. Determine center line index
34
+ center_line_idx = -1
35
+ found_by = ""
36
+
37
+ if line_number is not None:
38
+ try:
39
+ line_number_int = int(line_number)
40
+ if 1 <= line_number_int <= num_lines:
41
+ center_line_idx = line_number_int - 1 # Convert to 0-based index
42
+ found_by = f"line {line_number_int}"
43
+ else:
44
+ raise ToolError(
45
+ f"Line number {line_number_int} is out of range (1-{num_lines}) for"
46
+ f" {file_path}."
47
+ )
48
+ except ValueError:
49
+ raise ToolError(f"Invalid line number '{line_number}'. Must be an integer.")
50
+
51
+ elif pattern is not None:
52
+ # TODO: Update this section for multiline pattern support later
53
+ first_match_line_idx = -1
54
+ for i, line in enumerate(lines):
55
+ if pattern in line:
56
+ first_match_line_idx = i
57
+ break
58
+
59
+ if first_match_line_idx != -1:
60
+ center_line_idx = first_match_line_idx
61
+ found_by = f"pattern '{pattern}' on line {center_line_idx + 1}"
62
+ else:
63
+ raise ToolError(f"Pattern '{pattern}' not found in {file_path}.")
64
+
65
+ if center_line_idx == -1:
66
+ # Should not happen if logic above is correct, but as a safeguard
67
+ raise ToolError("Internal error: Could not determine center line.")
68
+
69
+ # 5. Calculate context window
70
+ try:
71
+ context_lines_int = int(context_lines)
72
+ if context_lines_int < 0:
73
+ raise ValueError("Context lines must be non-negative")
74
+ except ValueError:
75
+ coder.io.tool_warning(
76
+ f"Invalid context_lines value '{context_lines}', using default 3."
77
+ )
78
+ context_lines_int = 3
79
+
80
+ start_line_idx = max(0, center_line_idx - context_lines_int)
81
+ end_line_idx = min(num_lines - 1, center_line_idx + context_lines_int)
82
+
83
+ # 6. Format output
84
+ # Use rel_path for user-facing messages
85
+ output_lines = [f"Displaying context around {found_by} in {rel_path}:"]
86
+ max_line_num_width = len(str(end_line_idx + 1)) # Width for padding
87
+
88
+ for i in range(start_line_idx, end_line_idx + 1):
89
+ line_num_str = str(i + 1).rjust(max_line_num_width)
90
+ output_lines.append(f"{line_num_str} | {lines[i]}")
91
+
92
+ # Log success and return the formatted context directly
93
+ coder.io.tool_output(f"Successfully retrieved context for {rel_path}")
94
+ return "\n".join(output_lines)
95
+
96
+ except ToolError as e:
97
+ # Handle expected errors raised by utility functions or validation
98
+ return handle_tool_error(coder, tool_name, e, add_traceback=False)
99
+ except Exception as e:
100
+ # Handle unexpected errors during processing
101
+ return handle_tool_error(coder, tool_name, e)
@@ -0,0 +1,313 @@
1
+ import difflib
2
+ import os
3
+ import re
4
+ import traceback
5
+
6
+
7
+ class ToolError(Exception):
8
+ """Custom exception for tool-specific errors that should be reported to the LLM."""
9
+
10
+ pass
11
+
12
+
13
+ def resolve_paths(coder, file_path):
14
+ """Resolves absolute and relative paths for a given file path."""
15
+ try:
16
+ abs_path = coder.abs_root_path(file_path)
17
+ rel_path = coder.get_rel_fname(abs_path)
18
+ return abs_path, rel_path
19
+ except Exception as e:
20
+ # Wrap unexpected errors during path resolution
21
+ raise ToolError(f"Error resolving path '{file_path}': {e}")
22
+
23
+
24
+ def validate_file_for_edit(coder, file_path):
25
+ """
26
+ Validates if a file exists, is in context, and is editable.
27
+ Reads and returns original content if valid.
28
+ Raises ToolError on failure.
29
+
30
+ Returns:
31
+ tuple: (absolute_path, relative_path, original_content)
32
+ """
33
+ abs_path, rel_path = resolve_paths(coder, file_path)
34
+
35
+ if not os.path.isfile(abs_path):
36
+ raise ToolError(f"File '{file_path}' not found")
37
+
38
+ if abs_path not in coder.abs_fnames:
39
+ if abs_path in coder.abs_read_only_fnames:
40
+ raise ToolError(f"File '{file_path}' is read-only. Use MakeEditable first.")
41
+ else:
42
+ # File exists but is not in context at all
43
+ raise ToolError(f"File '{file_path}' not in context. Use View or MakeEditable first.")
44
+
45
+ # Reread content immediately before potential modification
46
+ content = coder.io.read_text(abs_path)
47
+ if content is None:
48
+ # This indicates an issue reading a file we know exists and is in context
49
+ coder.io.tool_error(
50
+ f"Internal error: Could not read file '{file_path}' which should be accessible."
51
+ )
52
+ raise ToolError(f"Could not read file '{file_path}'")
53
+
54
+ return abs_path, rel_path, content
55
+
56
+
57
+ def find_pattern_indices(lines, pattern, use_regex=False):
58
+ """Finds all line indices matching a pattern."""
59
+ indices = []
60
+ for i, line in enumerate(lines):
61
+ if (use_regex and re.search(pattern, line)) or (not use_regex and pattern in line):
62
+ indices.append(i)
63
+ return indices
64
+
65
+
66
+ def select_occurrence_index(indices, occurrence, pattern_desc="Pattern"):
67
+ """
68
+ Selects the target 0-based index from a list of indices based on the 1-based occurrence parameter.
69
+ Raises ToolError if the pattern wasn't found or the occurrence is invalid.
70
+ """
71
+ num_occurrences = len(indices)
72
+ if not indices:
73
+ raise ToolError(f"{pattern_desc} not found")
74
+
75
+ try:
76
+ occurrence = int(occurrence) # Ensure occurrence is an integer
77
+ if occurrence == -1: # Last occurrence
78
+ if num_occurrences == 0:
79
+ raise ToolError(f"{pattern_desc} not found, cannot select last occurrence.")
80
+ target_idx = num_occurrences - 1
81
+ elif 1 <= occurrence <= num_occurrences:
82
+ target_idx = occurrence - 1 # Convert 1-based to 0-based
83
+ else:
84
+ raise ToolError(
85
+ f"Occurrence number {occurrence} is out of range for {pattern_desc}. Found"
86
+ f" {num_occurrences} occurrences."
87
+ )
88
+ except ValueError:
89
+ raise ToolError(f"Invalid occurrence value: '{occurrence}'. Must be an integer.")
90
+
91
+ return indices[target_idx]
92
+
93
+
94
+ def determine_line_range(
95
+ coder,
96
+ file_path,
97
+ lines,
98
+ start_pattern_line_index=None, # Made optional
99
+ end_pattern=None,
100
+ line_count=None,
101
+ target_symbol=None,
102
+ pattern_desc="Block",
103
+ ):
104
+ """
105
+ Determines the end line index based on end_pattern or line_count.
106
+ Raises ToolError if end_pattern is not found or line_count is invalid.
107
+ """
108
+ # Parameter validation: Ensure only one targeting method is used
109
+ targeting_methods = [
110
+ target_symbol is not None,
111
+ start_pattern_line_index is not None,
112
+ # Note: line_count and end_pattern depend on start_pattern_line_index
113
+ ]
114
+ if sum(targeting_methods) > 1:
115
+ raise ToolError("Cannot specify target_symbol along with start_pattern.")
116
+ if sum(targeting_methods) == 0:
117
+ raise ToolError(
118
+ "Must specify either target_symbol or start_pattern."
119
+ ) # Or line numbers for line-based tools, handled elsewhere
120
+
121
+ if target_symbol:
122
+ if end_pattern or line_count:
123
+ raise ToolError("Cannot specify end_pattern or line_count when using target_symbol.")
124
+ try:
125
+ # Use repo_map to find the symbol's definition range
126
+ start_line, end_line = coder.repo_map.get_symbol_definition_location(
127
+ file_path, target_symbol
128
+ )
129
+ return start_line, end_line
130
+ except AttributeError: # Use specific exception
131
+ # Check if repo_map exists and is initialized before accessing methods
132
+ if not hasattr(coder, "repo_map") or coder.repo_map is None:
133
+ raise ToolError("RepoMap is not available or not initialized.")
134
+ # If repo_map exists, the error might be from get_symbol_definition_location itself
135
+ # Re-raise ToolErrors directly
136
+ raise
137
+ except ToolError as e:
138
+ # Propagate specific ToolErrors from repo_map (not found, ambiguous, etc.)
139
+ raise e
140
+ except Exception as e:
141
+ # Catch other unexpected errors during symbol lookup
142
+ raise ToolError(f"Unexpected error looking up symbol '{target_symbol}': {e}")
143
+
144
+ # --- Existing logic for pattern/line_count based targeting ---
145
+ # Ensure start_pattern_line_index is provided if not using target_symbol
146
+ if start_pattern_line_index is None:
147
+ raise ToolError(
148
+ "Internal error: start_pattern_line_index is required when not using target_symbol."
149
+ )
150
+
151
+ # Assign start_line here for the pattern-based logic path
152
+ start_line = start_pattern_line_index # Start of existing logic
153
+ start_line = start_pattern_line_index
154
+ end_line = -1
155
+
156
+ if end_pattern and line_count:
157
+ raise ToolError("Cannot specify both end_pattern and line_count")
158
+
159
+ if end_pattern:
160
+ found_end = False
161
+ # Search from the start_line onwards for the end_pattern
162
+ for i in range(start_line, len(lines)):
163
+ if end_pattern in lines[i]:
164
+ end_line = i
165
+ found_end = True
166
+ break
167
+ if not found_end:
168
+ raise ToolError(
169
+ f"End pattern '{end_pattern}' not found after start pattern on line"
170
+ f" {start_line + 1}"
171
+ )
172
+ elif line_count:
173
+ try:
174
+ line_count = int(line_count)
175
+ if line_count <= 0:
176
+ raise ValueError("Line count must be positive")
177
+ # Calculate end line index, ensuring it doesn't exceed file bounds
178
+ end_line = min(start_line + line_count - 1, len(lines) - 1)
179
+ except ValueError:
180
+ raise ToolError(
181
+ f"Invalid line_count value: '{line_count}'. Must be a positive integer."
182
+ )
183
+ else:
184
+ # If neither end_pattern nor line_count is given, the range is just the start line
185
+ end_line = start_line
186
+
187
+ return start_line, end_line
188
+
189
+
190
+ def generate_unified_diff_snippet(original_content, new_content, file_path, context_lines=3):
191
+ """
192
+ Generates a unified diff snippet between original and new content.
193
+
194
+ Args:
195
+ original_content (str): The original file content.
196
+ new_content (str): The modified file content.
197
+ file_path (str): The relative path to the file (for display in diff header).
198
+ context_lines (int): Number of context lines to show around changes.
199
+
200
+ Returns:
201
+ str: A formatted unified diff snippet, or an empty string if no changes.
202
+ """
203
+ if original_content == new_content:
204
+ return ""
205
+
206
+ original_lines = original_content.splitlines(keepends=True)
207
+ new_lines = new_content.splitlines(keepends=True)
208
+
209
+ diff = difflib.unified_diff(
210
+ original_lines,
211
+ new_lines,
212
+ fromfile=f"a/{file_path}",
213
+ tofile=f"b/{file_path}",
214
+ n=context_lines, # Number of context lines
215
+ )
216
+
217
+ # Join the diff lines, potentially skipping the header if desired,
218
+ # but let's keep it for standard format.
219
+ diff_snippet = "".join(diff)
220
+
221
+ # Ensure snippet ends with a newline for cleaner formatting in results
222
+ if diff_snippet and not diff_snippet.endswith("\n"):
223
+ diff_snippet += "\n"
224
+
225
+ return diff_snippet
226
+
227
+
228
+ def apply_change(
229
+ coder, abs_path, rel_path, original_content, new_content, change_type, metadata, change_id=None
230
+ ):
231
+ """
232
+ Writes the new content, tracks the change, and updates coder state.
233
+ Returns the final change ID. Raises ToolError on tracking failure.
234
+ """
235
+ coder.io.write_text(abs_path, new_content)
236
+ try:
237
+ final_change_id = coder.change_tracker.track_change(
238
+ file_path=rel_path,
239
+ change_type=change_type,
240
+ original_content=original_content,
241
+ new_content=new_content,
242
+ metadata=metadata,
243
+ change_id=change_id,
244
+ )
245
+ except Exception as track_e:
246
+ # Log the error but also raise ToolError to inform the LLM
247
+ coder.io.tool_error(f"Error tracking change for {change_type}: {track_e}")
248
+ raise ToolError(f"Failed to track change: {track_e}")
249
+
250
+ coder.aider_edited_files.add(rel_path)
251
+ return final_change_id
252
+
253
+
254
+ def handle_tool_error(coder, tool_name, e, add_traceback=True):
255
+ """Logs tool errors and returns a formatted error message for the LLM."""
256
+ error_message = f"Error in {tool_name}: {str(e)}"
257
+ if add_traceback:
258
+ error_message += f"\n{traceback.format_exc()}"
259
+ coder.io.tool_error(error_message)
260
+ # Return only the core error message to the LLM for brevity
261
+ return f"Error: {str(e)}"
262
+
263
+
264
+ def format_tool_result(
265
+ coder,
266
+ tool_name,
267
+ success_message,
268
+ change_id=None,
269
+ diff_snippet=None,
270
+ dry_run=False,
271
+ dry_run_message=None,
272
+ ):
273
+ """Formats the result message for tool execution."""
274
+ if dry_run:
275
+ full_message = dry_run_message or f"Dry run: Would execute {tool_name}."
276
+ if diff_snippet:
277
+ full_message += f" Diff snippet:\n{diff_snippet}"
278
+ coder.io.tool_output(full_message) # Log the dry run action
279
+ return full_message
280
+ else:
281
+ # Use the provided success message, potentially adding change_id and diff
282
+ full_message = f"✅ {success_message}"
283
+ if change_id:
284
+ full_message += f" (change_id: {change_id})"
285
+ coder.io.tool_output(full_message) # Log the success action
286
+
287
+ result_for_llm = f"Successfully executed {tool_name}."
288
+ if change_id:
289
+ result_for_llm += f" Change ID: {change_id}."
290
+ if diff_snippet:
291
+ result_for_llm += f" Diff snippet:\n{diff_snippet}"
292
+ return result_for_llm
293
+
294
+
295
+ # Example usage within a hypothetical tool:
296
+ # try:
297
+ # abs_path, rel_path, original_content = validate_file_for_edit(coder, file_path)
298
+ # # ... tool specific logic to determine new_content and metadata ...
299
+ # if dry_run:
300
+ # return format_tool_result(coder, "MyTool", "", dry_run=True, diff_snippet=diff)
301
+ #
302
+ # change_id = apply_change(coder, abs_path, rel_path, original_content, new_content, 'mytool', metadata)
303
+ # return format_tool_result(
304
+ # coder,
305
+ # "MyTool",
306
+ # f"Applied change to {file_path}",
307
+ # change_id=change_id,
308
+ # diff_snippet=diff
309
+ # )
310
+ # except ToolError as e:
311
+ # return handle_tool_error(coder, "MyTool", e, add_traceback=False) # Don't need traceback for ToolErrors
312
+ # except Exception as e:
313
+ # return handle_tool_error(coder, "MyTool", e)