aider-ce 0.88.20__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.
Files changed (279) 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 +1056 -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/agent_coder.py +2166 -0
  10. aider/coders/agent_prompts.py +104 -0
  11. aider/coders/architect_coder.py +48 -0
  12. aider/coders/architect_prompts.py +40 -0
  13. aider/coders/ask_coder.py +9 -0
  14. aider/coders/ask_prompts.py +35 -0
  15. aider/coders/base_coder.py +3613 -0
  16. aider/coders/base_prompts.py +87 -0
  17. aider/coders/chat_chunks.py +64 -0
  18. aider/coders/context_coder.py +53 -0
  19. aider/coders/context_prompts.py +75 -0
  20. aider/coders/editblock_coder.py +657 -0
  21. aider/coders/editblock_fenced_coder.py +10 -0
  22. aider/coders/editblock_fenced_prompts.py +143 -0
  23. aider/coders/editblock_func_coder.py +141 -0
  24. aider/coders/editblock_func_prompts.py +27 -0
  25. aider/coders/editblock_prompts.py +175 -0
  26. aider/coders/editor_diff_fenced_coder.py +9 -0
  27. aider/coders/editor_diff_fenced_prompts.py +11 -0
  28. aider/coders/editor_editblock_coder.py +9 -0
  29. aider/coders/editor_editblock_prompts.py +21 -0
  30. aider/coders/editor_whole_coder.py +9 -0
  31. aider/coders/editor_whole_prompts.py +12 -0
  32. aider/coders/help_coder.py +16 -0
  33. aider/coders/help_prompts.py +46 -0
  34. aider/coders/patch_coder.py +706 -0
  35. aider/coders/patch_prompts.py +159 -0
  36. aider/coders/search_replace.py +757 -0
  37. aider/coders/shell.py +37 -0
  38. aider/coders/single_wholefile_func_coder.py +102 -0
  39. aider/coders/single_wholefile_func_prompts.py +27 -0
  40. aider/coders/udiff_coder.py +429 -0
  41. aider/coders/udiff_prompts.py +115 -0
  42. aider/coders/udiff_simple.py +14 -0
  43. aider/coders/udiff_simple_prompts.py +25 -0
  44. aider/coders/wholefile_coder.py +144 -0
  45. aider/coders/wholefile_func_coder.py +134 -0
  46. aider/coders/wholefile_func_prompts.py +27 -0
  47. aider/coders/wholefile_prompts.py +65 -0
  48. aider/commands.py +2173 -0
  49. aider/copypaste.py +72 -0
  50. aider/deprecated.py +126 -0
  51. aider/diffs.py +128 -0
  52. aider/dump.py +29 -0
  53. aider/editor.py +147 -0
  54. aider/exceptions.py +115 -0
  55. aider/format_settings.py +26 -0
  56. aider/gui.py +545 -0
  57. aider/help.py +163 -0
  58. aider/help_pats.py +19 -0
  59. aider/helpers/__init__.py +9 -0
  60. aider/helpers/similarity.py +98 -0
  61. aider/history.py +180 -0
  62. aider/io.py +1608 -0
  63. aider/linter.py +304 -0
  64. aider/llm.py +55 -0
  65. aider/main.py +1415 -0
  66. aider/mcp/__init__.py +174 -0
  67. aider/mcp/server.py +149 -0
  68. aider/mdstream.py +243 -0
  69. aider/models.py +1313 -0
  70. aider/onboarding.py +429 -0
  71. aider/openrouter.py +129 -0
  72. aider/prompts.py +56 -0
  73. aider/queries/tree-sitter-language-pack/README.md +7 -0
  74. aider/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
  75. aider/queries/tree-sitter-language-pack/c-tags.scm +9 -0
  76. aider/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
  77. aider/queries/tree-sitter-language-pack/clojure-tags.scm +7 -0
  78. aider/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
  79. aider/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
  80. aider/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
  81. aider/queries/tree-sitter-language-pack/d-tags.scm +26 -0
  82. aider/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
  83. aider/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
  84. aider/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
  85. aider/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
  86. aider/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
  87. aider/queries/tree-sitter-language-pack/go-tags.scm +42 -0
  88. aider/queries/tree-sitter-language-pack/java-tags.scm +20 -0
  89. aider/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
  90. aider/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
  91. aider/queries/tree-sitter-language-pack/matlab-tags.scm +10 -0
  92. aider/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
  93. aider/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
  94. aider/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
  95. aider/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
  96. aider/queries/tree-sitter-language-pack/python-tags.scm +14 -0
  97. aider/queries/tree-sitter-language-pack/r-tags.scm +21 -0
  98. aider/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
  99. aider/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
  100. aider/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
  101. aider/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
  102. aider/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
  103. aider/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
  104. aider/queries/tree-sitter-languages/README.md +24 -0
  105. aider/queries/tree-sitter-languages/c-tags.scm +9 -0
  106. aider/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
  107. aider/queries/tree-sitter-languages/cpp-tags.scm +15 -0
  108. aider/queries/tree-sitter-languages/dart-tags.scm +91 -0
  109. aider/queries/tree-sitter-languages/elisp-tags.scm +8 -0
  110. aider/queries/tree-sitter-languages/elixir-tags.scm +54 -0
  111. aider/queries/tree-sitter-languages/elm-tags.scm +19 -0
  112. aider/queries/tree-sitter-languages/fortran-tags.scm +15 -0
  113. aider/queries/tree-sitter-languages/go-tags.scm +30 -0
  114. aider/queries/tree-sitter-languages/haskell-tags.scm +3 -0
  115. aider/queries/tree-sitter-languages/hcl-tags.scm +77 -0
  116. aider/queries/tree-sitter-languages/java-tags.scm +20 -0
  117. aider/queries/tree-sitter-languages/javascript-tags.scm +88 -0
  118. aider/queries/tree-sitter-languages/julia-tags.scm +60 -0
  119. aider/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
  120. aider/queries/tree-sitter-languages/matlab-tags.scm +10 -0
  121. aider/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
  122. aider/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
  123. aider/queries/tree-sitter-languages/php-tags.scm +26 -0
  124. aider/queries/tree-sitter-languages/python-tags.scm +12 -0
  125. aider/queries/tree-sitter-languages/ql-tags.scm +26 -0
  126. aider/queries/tree-sitter-languages/ruby-tags.scm +64 -0
  127. aider/queries/tree-sitter-languages/rust-tags.scm +60 -0
  128. aider/queries/tree-sitter-languages/scala-tags.scm +65 -0
  129. aider/queries/tree-sitter-languages/typescript-tags.scm +41 -0
  130. aider/queries/tree-sitter-languages/zig-tags.scm +3 -0
  131. aider/reasoning_tags.py +82 -0
  132. aider/repo.py +621 -0
  133. aider/repomap.py +1174 -0
  134. aider/report.py +260 -0
  135. aider/resources/__init__.py +3 -0
  136. aider/resources/model-metadata.json +776 -0
  137. aider/resources/model-settings.yml +2068 -0
  138. aider/run_cmd.py +133 -0
  139. aider/scrape.py +293 -0
  140. aider/sendchat.py +242 -0
  141. aider/sessions.py +256 -0
  142. aider/special.py +203 -0
  143. aider/tools/__init__.py +72 -0
  144. aider/tools/command.py +105 -0
  145. aider/tools/command_interactive.py +122 -0
  146. aider/tools/delete_block.py +182 -0
  147. aider/tools/delete_line.py +155 -0
  148. aider/tools/delete_lines.py +184 -0
  149. aider/tools/extract_lines.py +341 -0
  150. aider/tools/finished.py +48 -0
  151. aider/tools/git_branch.py +129 -0
  152. aider/tools/git_diff.py +60 -0
  153. aider/tools/git_log.py +57 -0
  154. aider/tools/git_remote.py +53 -0
  155. aider/tools/git_show.py +51 -0
  156. aider/tools/git_status.py +46 -0
  157. aider/tools/grep.py +256 -0
  158. aider/tools/indent_lines.py +221 -0
  159. aider/tools/insert_block.py +288 -0
  160. aider/tools/list_changes.py +86 -0
  161. aider/tools/ls.py +93 -0
  162. aider/tools/make_editable.py +85 -0
  163. aider/tools/make_readonly.py +69 -0
  164. aider/tools/remove.py +91 -0
  165. aider/tools/replace_all.py +126 -0
  166. aider/tools/replace_line.py +173 -0
  167. aider/tools/replace_lines.py +217 -0
  168. aider/tools/replace_text.py +187 -0
  169. aider/tools/show_numbered_context.py +147 -0
  170. aider/tools/tool_utils.py +313 -0
  171. aider/tools/undo_change.py +95 -0
  172. aider/tools/update_todo_list.py +156 -0
  173. aider/tools/view.py +57 -0
  174. aider/tools/view_files_matching.py +141 -0
  175. aider/tools/view_files_with_symbol.py +129 -0
  176. aider/urls.py +17 -0
  177. aider/utils.py +456 -0
  178. aider/versioncheck.py +113 -0
  179. aider/voice.py +205 -0
  180. aider/waiting.py +38 -0
  181. aider/watch.py +318 -0
  182. aider/watch_prompts.py +12 -0
  183. aider/website/Gemfile +8 -0
  184. aider/website/_includes/blame.md +162 -0
  185. aider/website/_includes/get-started.md +22 -0
  186. aider/website/_includes/help-tip.md +5 -0
  187. aider/website/_includes/help.md +24 -0
  188. aider/website/_includes/install.md +5 -0
  189. aider/website/_includes/keys.md +4 -0
  190. aider/website/_includes/model-warnings.md +67 -0
  191. aider/website/_includes/multi-line.md +22 -0
  192. aider/website/_includes/python-m-aider.md +5 -0
  193. aider/website/_includes/recording.css +228 -0
  194. aider/website/_includes/recording.md +34 -0
  195. aider/website/_includes/replit-pipx.md +9 -0
  196. aider/website/_includes/works-best.md +1 -0
  197. aider/website/_sass/custom/custom.scss +103 -0
  198. aider/website/docs/config/adv-model-settings.md +2261 -0
  199. aider/website/docs/config/agent-mode.md +194 -0
  200. aider/website/docs/config/aider_conf.md +548 -0
  201. aider/website/docs/config/api-keys.md +90 -0
  202. aider/website/docs/config/dotenv.md +493 -0
  203. aider/website/docs/config/editor.md +127 -0
  204. aider/website/docs/config/mcp.md +95 -0
  205. aider/website/docs/config/model-aliases.md +104 -0
  206. aider/website/docs/config/options.md +890 -0
  207. aider/website/docs/config/reasoning.md +210 -0
  208. aider/website/docs/config.md +44 -0
  209. aider/website/docs/faq.md +384 -0
  210. aider/website/docs/git.md +76 -0
  211. aider/website/docs/index.md +47 -0
  212. aider/website/docs/install/codespaces.md +39 -0
  213. aider/website/docs/install/docker.md +57 -0
  214. aider/website/docs/install/optional.md +100 -0
  215. aider/website/docs/install/replit.md +8 -0
  216. aider/website/docs/install.md +115 -0
  217. aider/website/docs/languages.md +264 -0
  218. aider/website/docs/legal/contributor-agreement.md +111 -0
  219. aider/website/docs/legal/privacy.md +104 -0
  220. aider/website/docs/llms/anthropic.md +77 -0
  221. aider/website/docs/llms/azure.md +48 -0
  222. aider/website/docs/llms/bedrock.md +132 -0
  223. aider/website/docs/llms/cohere.md +34 -0
  224. aider/website/docs/llms/deepseek.md +32 -0
  225. aider/website/docs/llms/gemini.md +49 -0
  226. aider/website/docs/llms/github.md +111 -0
  227. aider/website/docs/llms/groq.md +36 -0
  228. aider/website/docs/llms/lm-studio.md +39 -0
  229. aider/website/docs/llms/ollama.md +75 -0
  230. aider/website/docs/llms/openai-compat.md +39 -0
  231. aider/website/docs/llms/openai.md +58 -0
  232. aider/website/docs/llms/openrouter.md +78 -0
  233. aider/website/docs/llms/other.md +117 -0
  234. aider/website/docs/llms/vertex.md +50 -0
  235. aider/website/docs/llms/warnings.md +10 -0
  236. aider/website/docs/llms/xai.md +53 -0
  237. aider/website/docs/llms.md +54 -0
  238. aider/website/docs/more/analytics.md +127 -0
  239. aider/website/docs/more/edit-formats.md +116 -0
  240. aider/website/docs/more/infinite-output.md +165 -0
  241. aider/website/docs/more-info.md +8 -0
  242. aider/website/docs/recordings/auto-accept-architect.md +31 -0
  243. aider/website/docs/recordings/dont-drop-original-read-files.md +35 -0
  244. aider/website/docs/recordings/index.md +21 -0
  245. aider/website/docs/recordings/model-accepts-settings.md +69 -0
  246. aider/website/docs/recordings/tree-sitter-language-pack.md +80 -0
  247. aider/website/docs/repomap.md +112 -0
  248. aider/website/docs/scripting.md +100 -0
  249. aider/website/docs/sessions.md +203 -0
  250. aider/website/docs/troubleshooting/aider-not-found.md +24 -0
  251. aider/website/docs/troubleshooting/edit-errors.md +76 -0
  252. aider/website/docs/troubleshooting/imports.md +62 -0
  253. aider/website/docs/troubleshooting/models-and-keys.md +54 -0
  254. aider/website/docs/troubleshooting/support.md +79 -0
  255. aider/website/docs/troubleshooting/token-limits.md +96 -0
  256. aider/website/docs/troubleshooting/warnings.md +12 -0
  257. aider/website/docs/troubleshooting.md +11 -0
  258. aider/website/docs/usage/browser.md +57 -0
  259. aider/website/docs/usage/caching.md +49 -0
  260. aider/website/docs/usage/commands.md +133 -0
  261. aider/website/docs/usage/conventions.md +119 -0
  262. aider/website/docs/usage/copypaste.md +121 -0
  263. aider/website/docs/usage/images-urls.md +48 -0
  264. aider/website/docs/usage/lint-test.md +118 -0
  265. aider/website/docs/usage/modes.md +211 -0
  266. aider/website/docs/usage/not-code.md +179 -0
  267. aider/website/docs/usage/notifications.md +87 -0
  268. aider/website/docs/usage/tips.md +79 -0
  269. aider/website/docs/usage/tutorials.md +30 -0
  270. aider/website/docs/usage/voice.md +121 -0
  271. aider/website/docs/usage/watch.md +294 -0
  272. aider/website/docs/usage.md +102 -0
  273. aider/website/share/index.md +101 -0
  274. aider_ce-0.88.20.dist-info/METADATA +187 -0
  275. aider_ce-0.88.20.dist-info/RECORD +279 -0
  276. aider_ce-0.88.20.dist-info/WHEEL +5 -0
  277. aider_ce-0.88.20.dist-info/entry_points.txt +2 -0
  278. aider_ce-0.88.20.dist-info/licenses/LICENSE.txt +202 -0
  279. aider_ce-0.88.20.dist-info/top_level.txt +1 -0
@@ -0,0 +1,187 @@
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
+ schema = {
11
+ "type": "function",
12
+ "function": {
13
+ "name": "ReplaceText",
14
+ "description": "Replace text in a file.",
15
+ "parameters": {
16
+ "type": "object",
17
+ "properties": {
18
+ "file_path": {"type": "string"},
19
+ "find_text": {"type": "string"},
20
+ "replace_text": {"type": "string"},
21
+ "near_context": {"type": "string"},
22
+ "occurrence": {"type": "integer", "default": 1},
23
+ "change_id": {"type": "string"},
24
+ "dry_run": {"type": "boolean", "default": False},
25
+ },
26
+ "required": ["file_path", "find_text", "replace_text"],
27
+ },
28
+ },
29
+ }
30
+
31
+ # Normalized tool name for lookup
32
+ NORM_NAME = "replacetext"
33
+
34
+
35
+ def _execute_replace_text(
36
+ coder,
37
+ file_path,
38
+ find_text,
39
+ replace_text,
40
+ near_context=None,
41
+ occurrence=1,
42
+ change_id=None,
43
+ dry_run=False,
44
+ ):
45
+ """
46
+ Replace specific text with new text, optionally using nearby context for disambiguation.
47
+ Uses utility functions for validation, finding occurrences, and applying changes.
48
+ """
49
+ tool_name = "ReplaceText"
50
+ try:
51
+ # 1. Validate file and get content
52
+ abs_path, rel_path, original_content = validate_file_for_edit(coder, file_path)
53
+
54
+ # 2. Find occurrences using helper function
55
+ # Note: _find_occurrences is currently on the Coder class, not in tool_utils
56
+ occurrences = coder._find_occurrences(original_content, find_text, near_context)
57
+
58
+ if not occurrences:
59
+ err_msg = f"Text '{find_text}' not found"
60
+ if near_context:
61
+ err_msg += f" near context '{near_context}'"
62
+ err_msg += f" in file '{file_path}'."
63
+ raise ToolError(err_msg)
64
+
65
+ # 3. Select the occurrence index
66
+ num_occurrences = len(occurrences)
67
+ try:
68
+ occurrence = int(occurrence)
69
+ if occurrence == -1:
70
+ if num_occurrences == 0:
71
+ raise ToolError(f"Text '{find_text}' not found, cannot select last occurrence.")
72
+ target_idx = num_occurrences - 1
73
+ elif 1 <= occurrence <= num_occurrences:
74
+ target_idx = occurrence - 1 # Convert 1-based to 0-based
75
+ else:
76
+ err_msg = (
77
+ f"Occurrence number {occurrence} is out of range. Found"
78
+ f" {num_occurrences} occurrences of '{find_text}'"
79
+ )
80
+ if near_context:
81
+ err_msg += f" near '{near_context}'"
82
+ err_msg += f" in '{file_path}'."
83
+ raise ToolError(err_msg)
84
+ except ValueError:
85
+ raise ToolError(f"Invalid occurrence value: '{occurrence}'. Must be an integer.")
86
+
87
+ start_index = occurrences[target_idx]
88
+
89
+ # 4. Perform the replacement
90
+ new_content = (
91
+ original_content[:start_index]
92
+ + replace_text
93
+ + original_content[start_index + len(find_text) :]
94
+ )
95
+
96
+ if original_content == new_content:
97
+ coder.io.tool_warning("No changes made: replacement text is identical to original")
98
+ return "Warning: No changes made (replacement identical to original)"
99
+
100
+ # 5. Generate diff for feedback
101
+ # Note: _generate_diff_snippet is currently on the Coder class
102
+ diff_snippet = generate_unified_diff_snippet(original_content, new_content, rel_path)
103
+ occurrence_str = f"occurrence {occurrence}" if num_occurrences > 1 else "text"
104
+
105
+ # 6. Handle dry run
106
+ if dry_run:
107
+ dry_run_message = (
108
+ f"Dry run: Would replace {occurrence_str} of '{find_text}' 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_index": start_index,
122
+ "find_text": find_text,
123
+ "replace_text": replace_text,
124
+ "near_context": near_context,
125
+ "occurrence": occurrence,
126
+ }
127
+ final_change_id = apply_change(
128
+ coder,
129
+ abs_path,
130
+ rel_path,
131
+ original_content,
132
+ new_content,
133
+ "replacetext",
134
+ metadata,
135
+ change_id,
136
+ )
137
+
138
+ coder.files_edited_by_tools.add(rel_path)
139
+ # 8. Format and return result
140
+ success_message = f"Replaced {occurrence_str} in {file_path}"
141
+ return format_tool_result(
142
+ coder, tool_name, success_message, change_id=final_change_id, diff_snippet=diff_snippet
143
+ )
144
+
145
+ except ToolError as e:
146
+ # Handle errors raised by utility functions or explicitly raised here
147
+ return handle_tool_error(coder, tool_name, e, add_traceback=False)
148
+ except Exception as e:
149
+ # Handle unexpected errors
150
+ return handle_tool_error(coder, tool_name, e)
151
+
152
+
153
+ def process_response(coder, params):
154
+ """
155
+ Process the ReplaceText tool response.
156
+
157
+ Args:
158
+ coder: The Coder instance
159
+ params: Dictionary of parameters
160
+
161
+ Returns:
162
+ str: Result message
163
+ """
164
+ file_path = params.get("file_path")
165
+ find_text = params.get("find_text")
166
+ replace_text = params.get("replace_text")
167
+ near_context = params.get("near_context")
168
+ occurrence = params.get("occurrence", 1)
169
+ change_id = params.get("change_id")
170
+ dry_run = params.get("dry_run", False)
171
+
172
+ if file_path is not None and find_text is not None and replace_text is not None:
173
+ return _execute_replace_text(
174
+ coder,
175
+ file_path,
176
+ find_text,
177
+ replace_text,
178
+ near_context,
179
+ occurrence,
180
+ change_id,
181
+ dry_run,
182
+ )
183
+ else:
184
+ return (
185
+ "Error: Missing required parameters for ReplaceText (file_path,"
186
+ " find_text, replace_text)"
187
+ )
@@ -0,0 +1,147 @@
1
+ import os
2
+
3
+ from .tool_utils import ToolError, handle_tool_error, resolve_paths
4
+
5
+ schema = {
6
+ "type": "function",
7
+ "function": {
8
+ "name": "ShowNumberedContext",
9
+ "description": "Show numbered lines of context around a pattern or line number.",
10
+ "parameters": {
11
+ "type": "object",
12
+ "properties": {
13
+ "file_path": {"type": "string"},
14
+ "pattern": {"type": "string"},
15
+ "line_number": {"type": "integer"},
16
+ "context_lines": {"type": "integer", "default": 3},
17
+ },
18
+ "required": ["file_path"],
19
+ },
20
+ },
21
+ }
22
+
23
+ # Normalized tool name for lookup
24
+ NORM_NAME = "shownumberedcontext"
25
+
26
+
27
+ def execute_show_numbered_context(
28
+ coder, file_path, pattern=None, line_number=None, context_lines=3
29
+ ):
30
+ """
31
+ Displays numbered lines from file_path centered around a target location
32
+ (pattern or line_number), without adding the file to context.
33
+ Uses utility functions for path resolution and error handling.
34
+ """
35
+ tool_name = "ShowNumberedContext"
36
+ try:
37
+ # 1. Validate arguments
38
+ if not (pattern is None) ^ (line_number is None):
39
+ raise ToolError("Provide exactly one of 'pattern' or 'line_number'.")
40
+
41
+ # 2. Resolve path
42
+ abs_path, rel_path = resolve_paths(coder, file_path)
43
+ if not os.path.exists(abs_path):
44
+ # Check existence after resolving, as resolve_paths doesn't guarantee existence
45
+ raise ToolError(f"File not found: {file_path}")
46
+
47
+ # 3. Read file content
48
+ content = coder.io.read_text(abs_path)
49
+ if content is None:
50
+ raise ToolError(f"Could not read file: {file_path}")
51
+ lines = content.splitlines()
52
+ num_lines = len(lines)
53
+
54
+ # 4. Determine center line index
55
+ center_line_idx = -1
56
+ found_by = ""
57
+
58
+ if line_number is not None:
59
+ try:
60
+ line_number_int = int(line_number)
61
+ if 1 <= line_number_int <= num_lines:
62
+ center_line_idx = line_number_int - 1 # Convert to 0-based index
63
+ found_by = f"line {line_number_int}"
64
+ else:
65
+ raise ToolError(
66
+ f"Line number {line_number_int} is out of range (1-{num_lines}) for"
67
+ f" {file_path}."
68
+ )
69
+ except ValueError:
70
+ raise ToolError(f"Invalid line number '{line_number}'. Must be an integer.")
71
+
72
+ elif pattern is not None:
73
+ # TODO: Update this section for multiline pattern support later
74
+ first_match_line_idx = -1
75
+ for i, line in enumerate(lines):
76
+ if pattern in line:
77
+ first_match_line_idx = i
78
+ break
79
+
80
+ if first_match_line_idx != -1:
81
+ center_line_idx = first_match_line_idx
82
+ found_by = f"pattern '{pattern}' on line {center_line_idx + 1}"
83
+ else:
84
+ raise ToolError(f"Pattern '{pattern}' not found in {file_path}.")
85
+
86
+ if center_line_idx == -1:
87
+ # Should not happen if logic above is correct, but as a safeguard
88
+ raise ToolError("Internal error: Could not determine center line.")
89
+
90
+ # 5. Calculate context window
91
+ try:
92
+ context_lines_int = int(context_lines)
93
+ if context_lines_int < 0:
94
+ raise ValueError("Context lines must be non-negative")
95
+ except ValueError:
96
+ coder.io.tool_warning(
97
+ f"Invalid context_lines value '{context_lines}', using default 3."
98
+ )
99
+ context_lines_int = 3
100
+
101
+ start_line_idx = max(0, center_line_idx - context_lines_int)
102
+ end_line_idx = min(num_lines - 1, center_line_idx + context_lines_int)
103
+
104
+ # 6. Format output
105
+ # Use rel_path for user-facing messages
106
+ output_lines = [f"Displaying context around {found_by} in {rel_path}:"]
107
+ max_line_num_width = len(str(end_line_idx + 1)) # Width for padding
108
+
109
+ for i in range(start_line_idx, end_line_idx + 1):
110
+ line_num_str = str(i + 1).rjust(max_line_num_width)
111
+ output_lines.append(f"{line_num_str} | {lines[i]}")
112
+
113
+ # Log success and return the formatted context directly
114
+ coder.io.tool_output(f"Successfully retrieved context for {rel_path}")
115
+ return "\n".join(output_lines)
116
+
117
+ except ToolError as e:
118
+ # Handle expected errors raised by utility functions or validation
119
+ return handle_tool_error(coder, tool_name, e, add_traceback=False)
120
+ except Exception as e:
121
+ # Handle unexpected errors during processing
122
+ return handle_tool_error(coder, tool_name, e)
123
+
124
+
125
+ def process_response(coder, params):
126
+ """
127
+ Process the ShowNumberedContext tool response.
128
+
129
+ Args:
130
+ coder: The Coder instance
131
+ params: Dictionary of parameters
132
+
133
+ Returns:
134
+ str: Result message
135
+ """
136
+ file_path = params.get("file_path")
137
+ pattern = params.get("pattern")
138
+ line_number = params.get("line_number")
139
+ context_lines = params.get("context_lines", 3)
140
+
141
+ if file_path is not None and (pattern is not None or line_number is not None):
142
+ return execute_show_numbered_context(coder, file_path, pattern, line_number, context_lines)
143
+ else:
144
+ return (
145
+ "Error: Missing required parameters for ViewNumberedContext (file_path"
146
+ " and either pattern or line_number)"
147
+ )
@@ -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)