cecli-dev 0.93.1__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 (366) hide show
  1. cecli/__init__.py +20 -0
  2. cecli/__main__.py +4 -0
  3. cecli/_version.py +34 -0
  4. cecli/args.py +1092 -0
  5. cecli/args_formatter.py +228 -0
  6. cecli/change_tracker.py +133 -0
  7. cecli/coders/__init__.py +38 -0
  8. cecli/coders/agent_coder.py +1872 -0
  9. cecli/coders/architect_coder.py +63 -0
  10. cecli/coders/ask_coder.py +8 -0
  11. cecli/coders/base_coder.py +3993 -0
  12. cecli/coders/chat_chunks.py +116 -0
  13. cecli/coders/context_coder.py +52 -0
  14. cecli/coders/copypaste_coder.py +269 -0
  15. cecli/coders/editblock_coder.py +656 -0
  16. cecli/coders/editblock_fenced_coder.py +9 -0
  17. cecli/coders/editblock_func_coder.py +140 -0
  18. cecli/coders/editor_diff_fenced_coder.py +8 -0
  19. cecli/coders/editor_editblock_coder.py +8 -0
  20. cecli/coders/editor_whole_coder.py +8 -0
  21. cecli/coders/help_coder.py +15 -0
  22. cecli/coders/patch_coder.py +705 -0
  23. cecli/coders/search_replace.py +757 -0
  24. cecli/coders/shell.py +37 -0
  25. cecli/coders/single_wholefile_func_coder.py +101 -0
  26. cecli/coders/udiff_coder.py +428 -0
  27. cecli/coders/udiff_simple.py +12 -0
  28. cecli/coders/wholefile_coder.py +143 -0
  29. cecli/coders/wholefile_func_coder.py +133 -0
  30. cecli/commands/__init__.py +192 -0
  31. cecli/commands/add.py +226 -0
  32. cecli/commands/agent.py +51 -0
  33. cecli/commands/architect.py +46 -0
  34. cecli/commands/ask.py +44 -0
  35. cecli/commands/chat_mode.py +0 -0
  36. cecli/commands/clear.py +37 -0
  37. cecli/commands/code.py +46 -0
  38. cecli/commands/command_prefix.py +44 -0
  39. cecli/commands/commit.py +52 -0
  40. cecli/commands/context.py +47 -0
  41. cecli/commands/context_blocks.py +124 -0
  42. cecli/commands/context_management.py +51 -0
  43. cecli/commands/copy.py +62 -0
  44. cecli/commands/copy_context.py +81 -0
  45. cecli/commands/core.py +287 -0
  46. cecli/commands/diff.py +68 -0
  47. cecli/commands/drop.py +217 -0
  48. cecli/commands/editor.py +78 -0
  49. cecli/commands/exit.py +55 -0
  50. cecli/commands/git.py +57 -0
  51. cecli/commands/help.py +140 -0
  52. cecli/commands/history_search.py +40 -0
  53. cecli/commands/lint.py +109 -0
  54. cecli/commands/list_sessions.py +56 -0
  55. cecli/commands/load.py +85 -0
  56. cecli/commands/load_session.py +48 -0
  57. cecli/commands/load_skill.py +68 -0
  58. cecli/commands/ls.py +75 -0
  59. cecli/commands/map.py +37 -0
  60. cecli/commands/map_refresh.py +35 -0
  61. cecli/commands/model.py +118 -0
  62. cecli/commands/models.py +41 -0
  63. cecli/commands/multiline_mode.py +38 -0
  64. cecli/commands/paste.py +91 -0
  65. cecli/commands/quit.py +32 -0
  66. cecli/commands/read_only.py +267 -0
  67. cecli/commands/read_only_stub.py +270 -0
  68. cecli/commands/reasoning_effort.py +70 -0
  69. cecli/commands/remove_skill.py +68 -0
  70. cecli/commands/report.py +40 -0
  71. cecli/commands/reset.py +88 -0
  72. cecli/commands/run.py +99 -0
  73. cecli/commands/save.py +49 -0
  74. cecli/commands/save_session.py +43 -0
  75. cecli/commands/settings.py +69 -0
  76. cecli/commands/test.py +58 -0
  77. cecli/commands/think_tokens.py +74 -0
  78. cecli/commands/tokens.py +207 -0
  79. cecli/commands/undo.py +145 -0
  80. cecli/commands/utils/__init__.py +0 -0
  81. cecli/commands/utils/base_command.py +131 -0
  82. cecli/commands/utils/helpers.py +142 -0
  83. cecli/commands/utils/registry.py +53 -0
  84. cecli/commands/utils/save_load_manager.py +98 -0
  85. cecli/commands/voice.py +78 -0
  86. cecli/commands/weak_model.py +123 -0
  87. cecli/commands/web.py +87 -0
  88. cecli/deprecated_args.py +185 -0
  89. cecli/diffs.py +129 -0
  90. cecli/dump.py +29 -0
  91. cecli/editor.py +147 -0
  92. cecli/exceptions.py +115 -0
  93. cecli/format_settings.py +26 -0
  94. cecli/help.py +119 -0
  95. cecli/help_pats.py +19 -0
  96. cecli/helpers/__init__.py +9 -0
  97. cecli/helpers/copypaste.py +123 -0
  98. cecli/helpers/coroutines.py +8 -0
  99. cecli/helpers/file_searcher.py +142 -0
  100. cecli/helpers/model_providers.py +552 -0
  101. cecli/helpers/plugin_manager.py +81 -0
  102. cecli/helpers/profiler.py +162 -0
  103. cecli/helpers/requests.py +77 -0
  104. cecli/helpers/similarity.py +98 -0
  105. cecli/helpers/skills.py +577 -0
  106. cecli/history.py +186 -0
  107. cecli/io.py +1782 -0
  108. cecli/linter.py +304 -0
  109. cecli/llm.py +101 -0
  110. cecli/main.py +1280 -0
  111. cecli/mcp/__init__.py +154 -0
  112. cecli/mcp/oauth.py +250 -0
  113. cecli/mcp/server.py +278 -0
  114. cecli/mdstream.py +243 -0
  115. cecli/models.py +1255 -0
  116. cecli/onboarding.py +301 -0
  117. cecli/prompts/__init__.py +0 -0
  118. cecli/prompts/agent.yml +71 -0
  119. cecli/prompts/architect.yml +35 -0
  120. cecli/prompts/ask.yml +31 -0
  121. cecli/prompts/base.yml +99 -0
  122. cecli/prompts/context.yml +60 -0
  123. cecli/prompts/copypaste.yml +5 -0
  124. cecli/prompts/editblock.yml +143 -0
  125. cecli/prompts/editblock_fenced.yml +106 -0
  126. cecli/prompts/editblock_func.yml +25 -0
  127. cecli/prompts/editor_diff_fenced.yml +115 -0
  128. cecli/prompts/editor_editblock.yml +121 -0
  129. cecli/prompts/editor_whole.yml +46 -0
  130. cecli/prompts/help.yml +37 -0
  131. cecli/prompts/patch.yml +110 -0
  132. cecli/prompts/single_wholefile_func.yml +24 -0
  133. cecli/prompts/udiff.yml +106 -0
  134. cecli/prompts/udiff_simple.yml +13 -0
  135. cecli/prompts/utils/__init__.py +0 -0
  136. cecli/prompts/utils/prompt_registry.py +167 -0
  137. cecli/prompts/utils/system.py +56 -0
  138. cecli/prompts/wholefile.yml +50 -0
  139. cecli/prompts/wholefile_func.yml +24 -0
  140. cecli/queries/tree-sitter-language-pack/README.md +7 -0
  141. cecli/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
  142. cecli/queries/tree-sitter-language-pack/c-tags.scm +12 -0
  143. cecli/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
  144. cecli/queries/tree-sitter-language-pack/clojure-tags.scm +12 -0
  145. cecli/queries/tree-sitter-language-pack/commonlisp-tags.scm +127 -0
  146. cecli/queries/tree-sitter-language-pack/cpp-tags.scm +18 -0
  147. cecli/queries/tree-sitter-language-pack/csharp-tags.scm +32 -0
  148. cecli/queries/tree-sitter-language-pack/d-tags.scm +26 -0
  149. cecli/queries/tree-sitter-language-pack/dart-tags.scm +97 -0
  150. cecli/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
  151. cecli/queries/tree-sitter-language-pack/elixir-tags.scm +59 -0
  152. cecli/queries/tree-sitter-language-pack/elm-tags.scm +22 -0
  153. cecli/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
  154. cecli/queries/tree-sitter-language-pack/go-tags.scm +49 -0
  155. cecli/queries/tree-sitter-language-pack/java-tags.scm +26 -0
  156. cecli/queries/tree-sitter-language-pack/javascript-tags.scm +96 -0
  157. cecli/queries/tree-sitter-language-pack/lua-tags.scm +39 -0
  158. cecli/queries/tree-sitter-language-pack/matlab-tags.scm +10 -0
  159. cecli/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
  160. cecli/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +101 -0
  161. cecli/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
  162. cecli/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
  163. cecli/queries/tree-sitter-language-pack/python-tags.scm +24 -0
  164. cecli/queries/tree-sitter-language-pack/r-tags.scm +27 -0
  165. cecli/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
  166. cecli/queries/tree-sitter-language-pack/ruby-tags.scm +69 -0
  167. cecli/queries/tree-sitter-language-pack/rust-tags.scm +63 -0
  168. cecli/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
  169. cecli/queries/tree-sitter-language-pack/swift-tags.scm +54 -0
  170. cecli/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
  171. cecli/queries/tree-sitter-languages/README.md +24 -0
  172. cecli/queries/tree-sitter-languages/c-tags.scm +12 -0
  173. cecli/queries/tree-sitter-languages/c_sharp-tags.scm +52 -0
  174. cecli/queries/tree-sitter-languages/cpp-tags.scm +18 -0
  175. cecli/queries/tree-sitter-languages/dart-tags.scm +92 -0
  176. cecli/queries/tree-sitter-languages/elisp-tags.scm +8 -0
  177. cecli/queries/tree-sitter-languages/elixir-tags.scm +59 -0
  178. cecli/queries/tree-sitter-languages/elm-tags.scm +22 -0
  179. cecli/queries/tree-sitter-languages/fortran-tags.scm +18 -0
  180. cecli/queries/tree-sitter-languages/go-tags.scm +36 -0
  181. cecli/queries/tree-sitter-languages/haskell-tags.scm +5 -0
  182. cecli/queries/tree-sitter-languages/hcl-tags.scm +77 -0
  183. cecli/queries/tree-sitter-languages/java-tags.scm +26 -0
  184. cecli/queries/tree-sitter-languages/javascript-tags.scm +96 -0
  185. cecli/queries/tree-sitter-languages/julia-tags.scm +60 -0
  186. cecli/queries/tree-sitter-languages/kotlin-tags.scm +30 -0
  187. cecli/queries/tree-sitter-languages/matlab-tags.scm +10 -0
  188. cecli/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
  189. cecli/queries/tree-sitter-languages/ocaml_interface-tags.scm +104 -0
  190. cecli/queries/tree-sitter-languages/php-tags.scm +32 -0
  191. cecli/queries/tree-sitter-languages/python-tags.scm +22 -0
  192. cecli/queries/tree-sitter-languages/ql-tags.scm +26 -0
  193. cecli/queries/tree-sitter-languages/ruby-tags.scm +69 -0
  194. cecli/queries/tree-sitter-languages/rust-tags.scm +63 -0
  195. cecli/queries/tree-sitter-languages/scala-tags.scm +64 -0
  196. cecli/queries/tree-sitter-languages/typescript-tags.scm +44 -0
  197. cecli/queries/tree-sitter-languages/zig-tags.scm +20 -0
  198. cecli/reasoning_tags.py +82 -0
  199. cecli/repo.py +626 -0
  200. cecli/repomap.py +1368 -0
  201. cecli/report.py +260 -0
  202. cecli/resources/__init__.py +3 -0
  203. cecli/resources/model-metadata.json +25751 -0
  204. cecli/resources/model-settings.yml +2394 -0
  205. cecli/resources/providers.json +67 -0
  206. cecli/run_cmd.py +143 -0
  207. cecli/scrape.py +295 -0
  208. cecli/sendchat.py +250 -0
  209. cecli/sessions.py +281 -0
  210. cecli/special.py +203 -0
  211. cecli/tools/__init__.py +72 -0
  212. cecli/tools/command.py +103 -0
  213. cecli/tools/command_interactive.py +113 -0
  214. cecli/tools/context_manager.py +175 -0
  215. cecli/tools/delete_block.py +154 -0
  216. cecli/tools/delete_line.py +120 -0
  217. cecli/tools/delete_lines.py +144 -0
  218. cecli/tools/extract_lines.py +281 -0
  219. cecli/tools/finished.py +35 -0
  220. cecli/tools/git_branch.py +132 -0
  221. cecli/tools/git_diff.py +49 -0
  222. cecli/tools/git_log.py +43 -0
  223. cecli/tools/git_remote.py +39 -0
  224. cecli/tools/git_show.py +37 -0
  225. cecli/tools/git_status.py +32 -0
  226. cecli/tools/grep.py +242 -0
  227. cecli/tools/indent_lines.py +195 -0
  228. cecli/tools/insert_block.py +263 -0
  229. cecli/tools/list_changes.py +71 -0
  230. cecli/tools/load_skill.py +51 -0
  231. cecli/tools/ls.py +77 -0
  232. cecli/tools/remove_skill.py +51 -0
  233. cecli/tools/replace_all.py +113 -0
  234. cecli/tools/replace_line.py +135 -0
  235. cecli/tools/replace_lines.py +180 -0
  236. cecli/tools/replace_text.py +186 -0
  237. cecli/tools/show_numbered_context.py +137 -0
  238. cecli/tools/thinking.py +52 -0
  239. cecli/tools/undo_change.py +82 -0
  240. cecli/tools/update_todo_list.py +148 -0
  241. cecli/tools/utils/base_tool.py +64 -0
  242. cecli/tools/utils/helpers.py +359 -0
  243. cecli/tools/utils/output.py +119 -0
  244. cecli/tools/utils/registry.py +145 -0
  245. cecli/tools/view_files_matching.py +138 -0
  246. cecli/tools/view_files_with_symbol.py +117 -0
  247. cecli/tui/__init__.py +83 -0
  248. cecli/tui/app.py +971 -0
  249. cecli/tui/io.py +566 -0
  250. cecli/tui/styles.tcss +117 -0
  251. cecli/tui/widgets/__init__.py +19 -0
  252. cecli/tui/widgets/completion_bar.py +331 -0
  253. cecli/tui/widgets/file_list.py +76 -0
  254. cecli/tui/widgets/footer.py +165 -0
  255. cecli/tui/widgets/input_area.py +320 -0
  256. cecli/tui/widgets/key_hints.py +16 -0
  257. cecli/tui/widgets/output.py +354 -0
  258. cecli/tui/widgets/status_bar.py +279 -0
  259. cecli/tui/worker.py +160 -0
  260. cecli/urls.py +16 -0
  261. cecli/utils.py +499 -0
  262. cecli/versioncheck.py +90 -0
  263. cecli/voice.py +90 -0
  264. cecli/waiting.py +38 -0
  265. cecli/watch.py +316 -0
  266. cecli/watch_prompts.py +12 -0
  267. cecli/website/Gemfile +8 -0
  268. cecli/website/_includes/blame.md +162 -0
  269. cecli/website/_includes/get-started.md +22 -0
  270. cecli/website/_includes/help-tip.md +5 -0
  271. cecli/website/_includes/help.md +24 -0
  272. cecli/website/_includes/install.md +5 -0
  273. cecli/website/_includes/keys.md +4 -0
  274. cecli/website/_includes/model-warnings.md +67 -0
  275. cecli/website/_includes/multi-line.md +22 -0
  276. cecli/website/_includes/python-m-aider.md +5 -0
  277. cecli/website/_includes/recording.css +228 -0
  278. cecli/website/_includes/recording.md +34 -0
  279. cecli/website/_includes/replit-pipx.md +9 -0
  280. cecli/website/_includes/works-best.md +1 -0
  281. cecli/website/_sass/custom/custom.scss +103 -0
  282. cecli/website/docs/config/adv-model-settings.md +2498 -0
  283. cecli/website/docs/config/agent-mode.md +320 -0
  284. cecli/website/docs/config/aider_conf.md +548 -0
  285. cecli/website/docs/config/api-keys.md +90 -0
  286. cecli/website/docs/config/custom-commands.md +187 -0
  287. cecli/website/docs/config/dotenv.md +493 -0
  288. cecli/website/docs/config/editor.md +127 -0
  289. cecli/website/docs/config/mcp.md +210 -0
  290. cecli/website/docs/config/model-aliases.md +173 -0
  291. cecli/website/docs/config/options.md +890 -0
  292. cecli/website/docs/config/reasoning.md +210 -0
  293. cecli/website/docs/config/skills.md +172 -0
  294. cecli/website/docs/config/tui.md +126 -0
  295. cecli/website/docs/config.md +44 -0
  296. cecli/website/docs/faq.md +379 -0
  297. cecli/website/docs/git.md +76 -0
  298. cecli/website/docs/index.md +47 -0
  299. cecli/website/docs/install/codespaces.md +39 -0
  300. cecli/website/docs/install/docker.md +48 -0
  301. cecli/website/docs/install/optional.md +100 -0
  302. cecli/website/docs/install/replit.md +8 -0
  303. cecli/website/docs/install.md +115 -0
  304. cecli/website/docs/languages.md +264 -0
  305. cecli/website/docs/legal/contributor-agreement.md +111 -0
  306. cecli/website/docs/legal/privacy.md +104 -0
  307. cecli/website/docs/llms/anthropic.md +77 -0
  308. cecli/website/docs/llms/azure.md +48 -0
  309. cecli/website/docs/llms/bedrock.md +132 -0
  310. cecli/website/docs/llms/cohere.md +34 -0
  311. cecli/website/docs/llms/deepseek.md +32 -0
  312. cecli/website/docs/llms/gemini.md +49 -0
  313. cecli/website/docs/llms/github.md +111 -0
  314. cecli/website/docs/llms/groq.md +36 -0
  315. cecli/website/docs/llms/lm-studio.md +39 -0
  316. cecli/website/docs/llms/ollama.md +75 -0
  317. cecli/website/docs/llms/openai-compat.md +39 -0
  318. cecli/website/docs/llms/openai.md +58 -0
  319. cecli/website/docs/llms/openrouter.md +78 -0
  320. cecli/website/docs/llms/other.md +117 -0
  321. cecli/website/docs/llms/vertex.md +50 -0
  322. cecli/website/docs/llms/warnings.md +10 -0
  323. cecli/website/docs/llms/xai.md +53 -0
  324. cecli/website/docs/llms.md +54 -0
  325. cecli/website/docs/more/analytics.md +127 -0
  326. cecli/website/docs/more/edit-formats.md +116 -0
  327. cecli/website/docs/more/infinite-output.md +192 -0
  328. cecli/website/docs/more-info.md +8 -0
  329. cecli/website/docs/recordings/auto-accept-architect.md +31 -0
  330. cecli/website/docs/recordings/dont-drop-original-read-files.md +35 -0
  331. cecli/website/docs/recordings/index.md +21 -0
  332. cecli/website/docs/recordings/model-accepts-settings.md +69 -0
  333. cecli/website/docs/recordings/tree-sitter-language-pack.md +80 -0
  334. cecli/website/docs/repomap.md +112 -0
  335. cecli/website/docs/scripting.md +100 -0
  336. cecli/website/docs/sessions.md +213 -0
  337. cecli/website/docs/troubleshooting/aider-not-found.md +24 -0
  338. cecli/website/docs/troubleshooting/edit-errors.md +76 -0
  339. cecli/website/docs/troubleshooting/imports.md +62 -0
  340. cecli/website/docs/troubleshooting/models-and-keys.md +54 -0
  341. cecli/website/docs/troubleshooting/support.md +79 -0
  342. cecli/website/docs/troubleshooting/token-limits.md +96 -0
  343. cecli/website/docs/troubleshooting/warnings.md +12 -0
  344. cecli/website/docs/troubleshooting.md +11 -0
  345. cecli/website/docs/usage/browser.md +57 -0
  346. cecli/website/docs/usage/caching.md +49 -0
  347. cecli/website/docs/usage/commands.md +133 -0
  348. cecli/website/docs/usage/conventions.md +119 -0
  349. cecli/website/docs/usage/copypaste.md +136 -0
  350. cecli/website/docs/usage/images-urls.md +48 -0
  351. cecli/website/docs/usage/lint-test.md +118 -0
  352. cecli/website/docs/usage/modes.md +211 -0
  353. cecli/website/docs/usage/not-code.md +179 -0
  354. cecli/website/docs/usage/notifications.md +87 -0
  355. cecli/website/docs/usage/tips.md +79 -0
  356. cecli/website/docs/usage/tutorials.md +30 -0
  357. cecli/website/docs/usage/voice.md +121 -0
  358. cecli/website/docs/usage/watch.md +294 -0
  359. cecli/website/docs/usage.md +102 -0
  360. cecli/website/share/index.md +101 -0
  361. cecli_dev-0.93.1.dist-info/METADATA +549 -0
  362. cecli_dev-0.93.1.dist-info/RECORD +366 -0
  363. cecli_dev-0.93.1.dist-info/WHEEL +5 -0
  364. cecli_dev-0.93.1.dist-info/entry_points.txt +4 -0
  365. cecli_dev-0.93.1.dist-info/licenses/LICENSE.txt +202 -0
  366. cecli_dev-0.93.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,656 @@
1
+ import difflib
2
+ import math
3
+ import re
4
+ import sys
5
+ from difflib import SequenceMatcher
6
+ from pathlib import Path
7
+
8
+ from cecli import utils
9
+
10
+ from ..dump import dump # noqa: F401
11
+ from .base_coder import Coder
12
+
13
+
14
+ class EditBlockCoder(Coder):
15
+ """A coder that uses search/replace blocks for code modifications."""
16
+
17
+ edit_format = "diff"
18
+ prompt_format = "editblock"
19
+
20
+ def get_edits(self):
21
+ content = self.partial_response_content
22
+
23
+ # might raise ValueError for malformed ORIG/UPD blocks
24
+ edits = list(
25
+ find_original_update_blocks(
26
+ content,
27
+ self.fence,
28
+ self.get_inchat_relative_files(),
29
+ )
30
+ )
31
+
32
+ self.shell_commands += [edit[1] for edit in edits if edit[0] is None]
33
+ edits = [edit for edit in edits if edit[0] is not None]
34
+
35
+ return edits
36
+
37
+ def apply_edits_dry_run(self, edits):
38
+ return self.apply_edits(edits, dry_run=True)
39
+
40
+ def apply_edits(self, edits, dry_run=False):
41
+ failed = []
42
+ passed = []
43
+ updated_edits = []
44
+
45
+ for edit in edits:
46
+ path, original, updated = edit
47
+ full_path = self.abs_root_path(path)
48
+ new_content = None
49
+
50
+ if Path(full_path).exists():
51
+ content = self.io.read_text(full_path)
52
+ new_content = do_replace(full_path, content, original, updated, self.fence)
53
+
54
+ # If the edit failed, and
55
+ # this is not a "create a new file" with an empty original...
56
+ # https://github.com/Aider-AI/aider/issues/2258
57
+ if not new_content and original.strip():
58
+ # try patching any of the other files in the chat
59
+ for full_path in self.abs_fnames:
60
+ content = self.io.read_text(full_path)
61
+ new_content = do_replace(full_path, content, original, updated, self.fence)
62
+ if new_content:
63
+ path = self.get_rel_fname(full_path)
64
+ break
65
+
66
+ updated_edits.append((path, original, updated))
67
+
68
+ if new_content:
69
+ if not dry_run:
70
+ self.io.write_text(full_path, new_content)
71
+ passed.append(edit)
72
+ else:
73
+ failed.append(edit)
74
+
75
+ if dry_run:
76
+ return updated_edits
77
+
78
+ if not failed:
79
+ return
80
+
81
+ blocks = "block" if len(failed) == 1 else "blocks"
82
+
83
+ res = f"# {len(failed)} SEARCH/REPLACE {blocks} failed to match!\n"
84
+ for edit in failed:
85
+ path, original, updated = edit
86
+
87
+ full_path = self.abs_root_path(path)
88
+ content = self.io.read_text(full_path)
89
+
90
+ res += f"""
91
+ ## SearchReplaceNoExactMatch: This SEARCH block failed to exactly match lines in {path}
92
+ <<<<<<< SEARCH
93
+ {original}=======
94
+ {updated}>>>>>>> REPLACE
95
+
96
+ """
97
+ did_you_mean = find_similar_lines(original, content)
98
+ if did_you_mean:
99
+ res += f"""Did you mean to match some of these actual lines from {path}?
100
+
101
+ {self.fence[0]}
102
+ {did_you_mean}
103
+ {self.fence[1]}
104
+
105
+ """
106
+
107
+ if updated in content and updated:
108
+ res += f"""Are you sure you need this SEARCH/REPLACE block?
109
+ The REPLACE lines are already in {path}!
110
+
111
+ """
112
+ res += (
113
+ "The SEARCH section must exactly match an existing block of lines including all white"
114
+ " space, comments, indentation, docstrings, etc\n"
115
+ )
116
+ if passed:
117
+ pblocks = "block" if len(passed) == 1 else "blocks"
118
+ res += f"""
119
+ # The other {len(passed)} SEARCH/REPLACE {pblocks} were applied successfully.
120
+ Don't re-send them.
121
+ Just reply with fixed versions of the {blocks} above that failed to match.
122
+ """
123
+ raise ValueError(res)
124
+
125
+
126
+ def prep(content):
127
+ if content and not content.endswith("\n"):
128
+ content += "\n"
129
+ lines = content.splitlines(keepends=True)
130
+ return content, lines
131
+
132
+
133
+ def perfect_or_whitespace(whole_lines, part_lines, replace_lines):
134
+ # Try for a perfect match
135
+ res = perfect_replace(whole_lines, part_lines, replace_lines)
136
+ if res:
137
+ return res
138
+
139
+ # Try being flexible about leading whitespace
140
+ res = replace_part_with_missing_leading_whitespace(whole_lines, part_lines, replace_lines)
141
+ if res:
142
+ return res
143
+
144
+
145
+ def perfect_replace(whole_lines, part_lines, replace_lines):
146
+ part_tup = tuple(part_lines)
147
+ part_len = len(part_lines)
148
+
149
+ for i in range(len(whole_lines) - part_len + 1):
150
+ whole_tup = tuple(whole_lines[i : i + part_len])
151
+ if part_tup == whole_tup:
152
+ res = whole_lines[:i] + replace_lines + whole_lines[i + part_len :]
153
+ return "".join(res)
154
+
155
+
156
+ def replace_most_similar_chunk(whole, part, replace):
157
+ """Best efforts to find the `part` lines in `whole` and replace them with `replace`"""
158
+
159
+ whole, whole_lines = prep(whole)
160
+ part, part_lines = prep(part)
161
+ replace, replace_lines = prep(replace)
162
+
163
+ res = perfect_or_whitespace(whole_lines, part_lines, replace_lines)
164
+ if res:
165
+ return res
166
+
167
+ # drop leading empty line, GPT sometimes adds them spuriously (issue #25)
168
+ if len(part_lines) > 2 and not part_lines[0].strip():
169
+ skip_blank_line_part_lines = part_lines[1:]
170
+ res = perfect_or_whitespace(whole_lines, skip_blank_line_part_lines, replace_lines)
171
+ if res:
172
+ return res
173
+
174
+ # Try to handle when it elides code with ...
175
+ try:
176
+ res = try_dotdotdots(whole, part, replace)
177
+ if res:
178
+ return res
179
+ except ValueError:
180
+ pass
181
+
182
+ return
183
+ # Try fuzzy matching
184
+ res = replace_closest_edit_distance(whole_lines, part, part_lines, replace_lines)
185
+ if res:
186
+ return res
187
+
188
+
189
+ def try_dotdotdots(whole, part, replace):
190
+ """
191
+ See if the edit block has ... lines.
192
+ If not, return none.
193
+
194
+ If yes, try and do a perfect edit with the ... chunks.
195
+ If there's a mismatch or otherwise imperfect edit, raise ValueError.
196
+
197
+ If perfect edit succeeds, return the updated whole.
198
+ """
199
+
200
+ dots_re = re.compile(r"(^\s*\.\.\.\n)", re.MULTILINE | re.DOTALL)
201
+
202
+ part_pieces = re.split(dots_re, part)
203
+ replace_pieces = re.split(dots_re, replace)
204
+
205
+ if len(part_pieces) != len(replace_pieces):
206
+ raise ValueError("Unpaired ... in SEARCH/REPLACE block")
207
+
208
+ if len(part_pieces) == 1:
209
+ # no dots in this edit block, just return None
210
+ return
211
+
212
+ # Compare odd strings in part_pieces and replace_pieces
213
+ all_dots_match = all(part_pieces[i] == replace_pieces[i] for i in range(1, len(part_pieces), 2))
214
+
215
+ if not all_dots_match:
216
+ raise ValueError("Unmatched ... in SEARCH/REPLACE block")
217
+
218
+ part_pieces = [part_pieces[i] for i in range(0, len(part_pieces), 2)]
219
+ replace_pieces = [replace_pieces[i] for i in range(0, len(replace_pieces), 2)]
220
+
221
+ pairs = zip(part_pieces, replace_pieces)
222
+ for part, replace in pairs:
223
+ if not part and not replace:
224
+ continue
225
+
226
+ if not part and replace:
227
+ if not whole.endswith("\n"):
228
+ whole += "\n"
229
+ whole += replace
230
+ continue
231
+
232
+ if whole.count(part) == 0:
233
+ raise ValueError
234
+ if whole.count(part) > 1:
235
+ raise ValueError
236
+
237
+ whole = whole.replace(part, replace, 1)
238
+
239
+ return whole
240
+
241
+
242
+ def replace_part_with_missing_leading_whitespace(whole_lines, part_lines, replace_lines):
243
+ # GPT often messes up leading whitespace.
244
+ # It usually does it uniformly across the ORIG and UPD blocks.
245
+ # Either omitting all leading whitespace, or including only some of it.
246
+
247
+ # Outdent everything in part_lines and replace_lines by the max fixed amount possible
248
+ leading = [len(p) - len(p.lstrip()) for p in part_lines if p.strip()] + [
249
+ len(p) - len(p.lstrip()) for p in replace_lines if p.strip()
250
+ ]
251
+
252
+ if leading and min(leading):
253
+ num_leading = min(leading)
254
+ part_lines = [p[num_leading:] if p.strip() else p for p in part_lines]
255
+ replace_lines = [p[num_leading:] if p.strip() else p for p in replace_lines]
256
+
257
+ # can we find an exact match not including the leading whitespace
258
+ num_part_lines = len(part_lines)
259
+
260
+ for i in range(len(whole_lines) - num_part_lines + 1):
261
+ add_leading = match_but_for_leading_whitespace(
262
+ whole_lines[i : i + num_part_lines], part_lines
263
+ )
264
+
265
+ if add_leading is None:
266
+ continue
267
+
268
+ replace_lines = [add_leading + rline if rline.strip() else rline for rline in replace_lines]
269
+ whole_lines = whole_lines[:i] + replace_lines + whole_lines[i + num_part_lines :]
270
+ return "".join(whole_lines)
271
+
272
+ return None
273
+
274
+
275
+ def match_but_for_leading_whitespace(whole_lines, part_lines):
276
+ num = len(whole_lines)
277
+
278
+ # does the non-whitespace all agree?
279
+ if not all(whole_lines[i].lstrip() == part_lines[i].lstrip() for i in range(num)):
280
+ return
281
+
282
+ # are they all offset the same?
283
+ add = set(
284
+ whole_lines[i][: len(whole_lines[i]) - len(part_lines[i])]
285
+ for i in range(num)
286
+ if whole_lines[i].strip()
287
+ )
288
+
289
+ if len(add) != 1:
290
+ return
291
+
292
+ return add.pop()
293
+
294
+
295
+ def replace_closest_edit_distance(whole_lines, part, part_lines, replace_lines):
296
+ similarity_thresh = 0.8
297
+
298
+ max_similarity = 0
299
+ most_similar_chunk_start = -1
300
+ most_similar_chunk_end = -1
301
+
302
+ scale = 0.1
303
+ min_len = math.floor(len(part_lines) * (1 - scale))
304
+ max_len = math.ceil(len(part_lines) * (1 + scale))
305
+
306
+ for length in range(min_len, max_len):
307
+ for i in range(len(whole_lines) - length + 1):
308
+ chunk = whole_lines[i : i + length]
309
+ chunk = "".join(chunk)
310
+
311
+ similarity = SequenceMatcher(None, chunk, part).ratio()
312
+
313
+ if similarity > max_similarity and similarity:
314
+ max_similarity = similarity
315
+ most_similar_chunk_start = i
316
+ most_similar_chunk_end = i + length
317
+
318
+ if max_similarity < similarity_thresh:
319
+ return
320
+
321
+ modified_whole = (
322
+ whole_lines[:most_similar_chunk_start]
323
+ + replace_lines
324
+ + whole_lines[most_similar_chunk_end:]
325
+ )
326
+ modified_whole = "".join(modified_whole)
327
+
328
+ return modified_whole
329
+
330
+
331
+ DEFAULT_FENCE = ("`" * 3, "`" * 3)
332
+
333
+
334
+ def strip_quoted_wrapping(res, fname=None, fence=DEFAULT_FENCE):
335
+ """
336
+ Given an input string which may have extra "wrapping" around it, remove the wrapping.
337
+ For example:
338
+
339
+ filename.ext
340
+ ```
341
+ We just want this content
342
+ Not the filename and triple quotes
343
+ ```
344
+ """
345
+ if not res:
346
+ return res
347
+
348
+ res = res.splitlines()
349
+
350
+ if fname and res[0].strip().endswith(Path(fname).name):
351
+ res = res[1:]
352
+
353
+ if res[0].startswith(fence[0]) and res[-1].startswith(fence[1]):
354
+ res = res[1:-1]
355
+
356
+ res = "\n".join(res)
357
+ if res and res[-1] != "\n":
358
+ res += "\n"
359
+
360
+ return res
361
+
362
+
363
+ def do_replace(fname, content, before_text, after_text, fence=None):
364
+ before_text = strip_quoted_wrapping(before_text, fname, fence)
365
+ after_text = strip_quoted_wrapping(after_text, fname, fence)
366
+ fname = Path(fname)
367
+
368
+ # does it want to make a new file?
369
+ if not fname.exists() and not before_text.strip():
370
+ fname.touch()
371
+ content = ""
372
+
373
+ if content is None:
374
+ return
375
+
376
+ if not before_text.strip():
377
+ # append to existing file, or start a new file
378
+ new_content = content + after_text
379
+ else:
380
+ new_content = replace_most_similar_chunk(content, before_text, after_text)
381
+
382
+ return new_content
383
+
384
+
385
+ HEAD = r"^<{5,9} SEARCH>?\s*$"
386
+ DIVIDER = r"^={5,9}\s*$"
387
+ UPDATED = r"^>{5,9} REPLACE\s*$"
388
+
389
+ HEAD_ERR = "<<<<<<< SEARCH"
390
+ DIVIDER_ERR = "======="
391
+ UPDATED_ERR = ">>>>>>> REPLACE"
392
+
393
+ separators = "|".join([HEAD, DIVIDER, UPDATED])
394
+
395
+ split_re = re.compile(r"^((?:" + separators + r")[ ]*\n)", re.MULTILINE | re.DOTALL)
396
+
397
+
398
+ missing_filename_err = (
399
+ "Bad/missing filename. The filename must be alone on the line before the opening fence"
400
+ " {fence[0]}"
401
+ )
402
+
403
+ # Always be willing to treat triple-backticks as a fence when searching for filenames
404
+ triple_backticks = "`" * 3
405
+
406
+
407
+ def strip_filename(filename, fence):
408
+ filename = filename.strip()
409
+
410
+ if filename == "...":
411
+ return
412
+
413
+ start_fence = fence[0]
414
+ if filename.startswith(start_fence):
415
+ candidate = filename[len(start_fence) :]
416
+ if candidate and ("." in candidate or "/" in candidate):
417
+ return candidate
418
+ return
419
+
420
+ if filename.startswith(triple_backticks):
421
+ candidate = filename[len(triple_backticks) :]
422
+ if candidate and ("." in candidate or "/" in candidate):
423
+ return candidate
424
+ return
425
+
426
+ filename = filename.rstrip(":")
427
+ filename = filename.lstrip("#")
428
+ filename = filename.strip()
429
+ filename = filename.strip("`")
430
+ filename = filename.strip("*")
431
+
432
+ # https://github.com/Aider-AI/aider/issues/1158
433
+ # filename = filename.replace("\\_", "_")
434
+
435
+ return filename
436
+
437
+
438
+ def find_original_update_blocks(content, fence=DEFAULT_FENCE, valid_fnames=None):
439
+ lines = content.splitlines(keepends=True)
440
+ i = 0
441
+ current_filename = None
442
+
443
+ head_pattern = re.compile(HEAD)
444
+ divider_pattern = re.compile(DIVIDER)
445
+ updated_pattern = re.compile(UPDATED)
446
+
447
+ while i < len(lines):
448
+ line = lines[i]
449
+
450
+ # Check for shell code blocks
451
+ shell_starts = [
452
+ "```bash",
453
+ "```sh",
454
+ "```shell",
455
+ "```cmd",
456
+ "```batch",
457
+ "```powershell",
458
+ "```ps1",
459
+ "```zsh",
460
+ "```fish",
461
+ "```ksh",
462
+ "```csh",
463
+ "```tcsh",
464
+ ]
465
+
466
+ # Check if the next line or the one after that is an editblock
467
+ next_is_editblock = (
468
+ i + 1 < len(lines)
469
+ and head_pattern.match(lines[i + 1].strip())
470
+ or i + 2 < len(lines)
471
+ and head_pattern.match(lines[i + 2].strip())
472
+ )
473
+
474
+ if any(line.strip().startswith(start) for start in shell_starts) and not next_is_editblock:
475
+ shell_content = []
476
+ i += 1
477
+ while i < len(lines) and not lines[i].strip().startswith("```"):
478
+ shell_content.append(lines[i])
479
+ i += 1
480
+ if i < len(lines) and lines[i].strip().startswith("```"):
481
+ i += 1 # Skip the closing ```
482
+
483
+ yield None, "".join(shell_content)
484
+ continue
485
+
486
+ # Check for SEARCH/REPLACE blocks
487
+ if head_pattern.match(line.strip()):
488
+ try:
489
+ # if next line after HEAD exists and is DIVIDER, it's a new file
490
+ if i + 1 < len(lines) and divider_pattern.match(lines[i + 1].strip()):
491
+ filename = find_filename(lines[max(0, i - 3) : i], fence, None)
492
+ else:
493
+ filename = find_filename(lines[max(0, i - 3) : i], fence, valid_fnames)
494
+
495
+ if not filename:
496
+ if current_filename:
497
+ filename = current_filename
498
+ else:
499
+ raise ValueError(missing_filename_err.format(fence=fence))
500
+
501
+ current_filename = filename
502
+
503
+ original_text = []
504
+ i += 1
505
+ while i < len(lines) and not divider_pattern.match(lines[i].strip()):
506
+ original_text.append(lines[i])
507
+ i += 1
508
+
509
+ if i >= len(lines) or not divider_pattern.match(lines[i].strip()):
510
+ raise ValueError(f"Expected `{DIVIDER_ERR}`")
511
+
512
+ updated_text = []
513
+ i += 1
514
+ while i < len(lines) and not (
515
+ updated_pattern.match(lines[i].strip())
516
+ or divider_pattern.match(lines[i].strip())
517
+ ):
518
+ updated_text.append(lines[i])
519
+ i += 1
520
+
521
+ if i >= len(lines) or not (
522
+ updated_pattern.match(lines[i].strip())
523
+ or divider_pattern.match(lines[i].strip())
524
+ ):
525
+ raise ValueError(f"Expected `{UPDATED_ERR}` or `{DIVIDER_ERR}`")
526
+
527
+ yield filename, "".join(original_text), "".join(updated_text)
528
+
529
+ except ValueError as e:
530
+ processed = "".join(lines[: i + 1])
531
+ err = e.args[0]
532
+ raise ValueError(f"{processed}\n^^^ {err}")
533
+
534
+ i += 1
535
+
536
+
537
+ def find_filename(lines, fence, valid_fnames):
538
+ """
539
+ Deepseek Coder v2 has been doing this:
540
+
541
+
542
+ ```python
543
+ word_count.py
544
+ ```
545
+ ```python
546
+ <<<<<<< SEARCH
547
+ ...
548
+
549
+ This is a more flexible search back for filenames.
550
+ """
551
+
552
+ if valid_fnames is None:
553
+ valid_fnames = []
554
+
555
+ # Go back through the 3 preceding lines
556
+ lines.reverse()
557
+ lines = lines[:3]
558
+
559
+ filenames = []
560
+ for line in lines:
561
+ # If we find a filename, done
562
+ filename = strip_filename(line, fence)
563
+ if filename:
564
+ filenames.append(filename)
565
+
566
+ # Only continue as long as we keep seeing fences
567
+ if not line.startswith(fence[0]) and not line.startswith(triple_backticks):
568
+ break
569
+
570
+ if not filenames:
571
+ return
572
+
573
+ # pick the *best* filename found
574
+
575
+ # Check for exact match first
576
+ for fname in filenames:
577
+ if fname in valid_fnames:
578
+ return fname
579
+
580
+ # Check for partial match (basename match)
581
+ for fname in filenames:
582
+ for vfn in valid_fnames:
583
+ if fname == Path(vfn).name:
584
+ return vfn
585
+
586
+ # Perform fuzzy matching with valid_fnames
587
+ for fname in filenames:
588
+ close_matches = difflib.get_close_matches(fname, valid_fnames, n=1, cutoff=0.8)
589
+ if len(close_matches) == 1:
590
+ return close_matches[0]
591
+
592
+ # If no fuzzy match, look for a file w/extension
593
+ for fname in filenames:
594
+ if "." in fname:
595
+ return fname
596
+
597
+ if filenames:
598
+ return filenames[0]
599
+
600
+
601
+ def find_similar_lines(search_lines, content_lines, threshold=0.6):
602
+ search_lines = search_lines.splitlines()
603
+ content_lines = content_lines.splitlines()
604
+
605
+ best_ratio = 0
606
+ best_match = None
607
+
608
+ for i in range(len(content_lines) - len(search_lines) + 1):
609
+ chunk = content_lines[i : i + len(search_lines)]
610
+ ratio = SequenceMatcher(None, search_lines, chunk).ratio()
611
+ if ratio > best_ratio:
612
+ best_ratio = ratio
613
+ best_match = chunk
614
+ best_match_i = i
615
+
616
+ if best_ratio < threshold:
617
+ return ""
618
+
619
+ if best_match[0] == search_lines[0] and best_match[-1] == search_lines[-1]:
620
+ return "\n".join(best_match)
621
+
622
+ N = 5
623
+ best_match_end = min(len(content_lines), best_match_i + len(search_lines) + N)
624
+ best_match_i = max(0, best_match_i - N)
625
+
626
+ best = content_lines[best_match_i:best_match_end]
627
+ return "\n".join(best)
628
+
629
+
630
+ def main():
631
+ history_md = Path(sys.argv[1]).read_text()
632
+ if not history_md:
633
+ return
634
+
635
+ messages = utils.split_chat_history_markdown(history_md)
636
+
637
+ for msg in messages:
638
+ msg = msg["content"]
639
+ edits = list(find_original_update_blocks(msg))
640
+
641
+ for fname, before, after in edits:
642
+ # Compute diff
643
+ diff = difflib.unified_diff(
644
+ before.splitlines(keepends=True),
645
+ after.splitlines(keepends=True),
646
+ fromfile="before",
647
+ tofile="after",
648
+ )
649
+ diff = "".join(diff)
650
+ dump(before)
651
+ dump(after)
652
+ dump(diff)
653
+
654
+
655
+ if __name__ == "__main__":
656
+ main()
@@ -0,0 +1,9 @@
1
+ from ..dump import dump # noqa: F401
2
+ from .editblock_coder import EditBlockCoder
3
+
4
+
5
+ class EditBlockFencedCoder(EditBlockCoder):
6
+ """A coder that uses fenced search/replace blocks for code modifications."""
7
+
8
+ edit_format = "diff-fenced"
9
+ prompt_format = "editblock_fenced"