chatmcp-cli 0.1.0__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 (228) hide show
  1. aider/__init__.py +20 -0
  2. aider/__main__.py +4 -0
  3. aider/_version.py +21 -0
  4. aider/analytics.py +250 -0
  5. aider/args.py +926 -0
  6. aider/args_formatter.py +228 -0
  7. aider/coders/__init__.py +34 -0
  8. aider/coders/architect_coder.py +48 -0
  9. aider/coders/architect_prompts.py +40 -0
  10. aider/coders/ask_coder.py +9 -0
  11. aider/coders/ask_prompts.py +35 -0
  12. aider/coders/base_coder.py +2483 -0
  13. aider/coders/base_prompts.py +60 -0
  14. aider/coders/chat_chunks.py +64 -0
  15. aider/coders/context_coder.py +53 -0
  16. aider/coders/context_prompts.py +75 -0
  17. aider/coders/editblock_coder.py +657 -0
  18. aider/coders/editblock_fenced_coder.py +10 -0
  19. aider/coders/editblock_fenced_prompts.py +143 -0
  20. aider/coders/editblock_func_coder.py +141 -0
  21. aider/coders/editblock_func_prompts.py +27 -0
  22. aider/coders/editblock_prompts.py +174 -0
  23. aider/coders/editor_diff_fenced_coder.py +9 -0
  24. aider/coders/editor_diff_fenced_prompts.py +11 -0
  25. aider/coders/editor_editblock_coder.py +8 -0
  26. aider/coders/editor_editblock_prompts.py +18 -0
  27. aider/coders/editor_whole_coder.py +8 -0
  28. aider/coders/editor_whole_prompts.py +10 -0
  29. aider/coders/help_coder.py +16 -0
  30. aider/coders/help_prompts.py +46 -0
  31. aider/coders/patch_coder.py +706 -0
  32. aider/coders/patch_prompts.py +161 -0
  33. aider/coders/search_replace.py +757 -0
  34. aider/coders/shell.py +37 -0
  35. aider/coders/single_wholefile_func_coder.py +102 -0
  36. aider/coders/single_wholefile_func_prompts.py +27 -0
  37. aider/coders/udiff_coder.py +429 -0
  38. aider/coders/udiff_prompts.py +115 -0
  39. aider/coders/udiff_simple.py +14 -0
  40. aider/coders/udiff_simple_prompts.py +25 -0
  41. aider/coders/wholefile_coder.py +144 -0
  42. aider/coders/wholefile_func_coder.py +134 -0
  43. aider/coders/wholefile_func_prompts.py +27 -0
  44. aider/coders/wholefile_prompts.py +67 -0
  45. aider/commands.py +1665 -0
  46. aider/copypaste.py +72 -0
  47. aider/deprecated.py +126 -0
  48. aider/diffs.py +128 -0
  49. aider/dump.py +29 -0
  50. aider/editor.py +147 -0
  51. aider/exceptions.py +107 -0
  52. aider/format_settings.py +26 -0
  53. aider/gui.py +545 -0
  54. aider/help.py +163 -0
  55. aider/help_pats.py +19 -0
  56. aider/history.py +143 -0
  57. aider/io.py +1175 -0
  58. aider/linter.py +304 -0
  59. aider/llm.py +47 -0
  60. aider/main.py +1267 -0
  61. aider/mdstream.py +243 -0
  62. aider/models.py +1286 -0
  63. aider/onboarding.py +428 -0
  64. aider/openrouter.py +128 -0
  65. aider/prompts.py +64 -0
  66. aider/queries/tree-sitter-language-pack/README.md +7 -0
  67. aider/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
  68. aider/queries/tree-sitter-language-pack/c-tags.scm +9 -0
  69. aider/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
  70. aider/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
  71. aider/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
  72. aider/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
  73. aider/queries/tree-sitter-language-pack/d-tags.scm +26 -0
  74. aider/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
  75. aider/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
  76. aider/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
  77. aider/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
  78. aider/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
  79. aider/queries/tree-sitter-language-pack/go-tags.scm +42 -0
  80. aider/queries/tree-sitter-language-pack/java-tags.scm +20 -0
  81. aider/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
  82. aider/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
  83. aider/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
  84. aider/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
  85. aider/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
  86. aider/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
  87. aider/queries/tree-sitter-language-pack/python-tags.scm +14 -0
  88. aider/queries/tree-sitter-language-pack/r-tags.scm +21 -0
  89. aider/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
  90. aider/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
  91. aider/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
  92. aider/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
  93. aider/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
  94. aider/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
  95. aider/queries/tree-sitter-languages/README.md +23 -0
  96. aider/queries/tree-sitter-languages/c-tags.scm +9 -0
  97. aider/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
  98. aider/queries/tree-sitter-languages/cpp-tags.scm +15 -0
  99. aider/queries/tree-sitter-languages/dart-tags.scm +91 -0
  100. aider/queries/tree-sitter-languages/elisp-tags.scm +8 -0
  101. aider/queries/tree-sitter-languages/elixir-tags.scm +54 -0
  102. aider/queries/tree-sitter-languages/elm-tags.scm +19 -0
  103. aider/queries/tree-sitter-languages/go-tags.scm +30 -0
  104. aider/queries/tree-sitter-languages/hcl-tags.scm +77 -0
  105. aider/queries/tree-sitter-languages/java-tags.scm +20 -0
  106. aider/queries/tree-sitter-languages/javascript-tags.scm +88 -0
  107. aider/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
  108. aider/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
  109. aider/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
  110. aider/queries/tree-sitter-languages/php-tags.scm +26 -0
  111. aider/queries/tree-sitter-languages/python-tags.scm +12 -0
  112. aider/queries/tree-sitter-languages/ql-tags.scm +26 -0
  113. aider/queries/tree-sitter-languages/ruby-tags.scm +64 -0
  114. aider/queries/tree-sitter-languages/rust-tags.scm +60 -0
  115. aider/queries/tree-sitter-languages/scala-tags.scm +65 -0
  116. aider/queries/tree-sitter-languages/typescript-tags.scm +41 -0
  117. aider/reasoning_tags.py +82 -0
  118. aider/repo.py +623 -0
  119. aider/repomap.py +847 -0
  120. aider/report.py +200 -0
  121. aider/resources/__init__.py +3 -0
  122. aider/resources/model-metadata.json +468 -0
  123. aider/resources/model-settings.yml +1767 -0
  124. aider/run_cmd.py +132 -0
  125. aider/scrape.py +284 -0
  126. aider/sendchat.py +61 -0
  127. aider/special.py +203 -0
  128. aider/urls.py +17 -0
  129. aider/utils.py +338 -0
  130. aider/versioncheck.py +113 -0
  131. aider/voice.py +187 -0
  132. aider/waiting.py +221 -0
  133. aider/watch.py +318 -0
  134. aider/watch_prompts.py +12 -0
  135. aider/website/Gemfile +8 -0
  136. aider/website/_includes/blame.md +162 -0
  137. aider/website/_includes/get-started.md +22 -0
  138. aider/website/_includes/help-tip.md +5 -0
  139. aider/website/_includes/help.md +24 -0
  140. aider/website/_includes/install.md +5 -0
  141. aider/website/_includes/keys.md +4 -0
  142. aider/website/_includes/model-warnings.md +67 -0
  143. aider/website/_includes/multi-line.md +22 -0
  144. aider/website/_includes/python-m-aider.md +5 -0
  145. aider/website/_includes/recording.css +228 -0
  146. aider/website/_includes/recording.md +34 -0
  147. aider/website/_includes/replit-pipx.md +9 -0
  148. aider/website/_includes/works-best.md +1 -0
  149. aider/website/_sass/custom/custom.scss +103 -0
  150. aider/website/docs/config/adv-model-settings.md +1881 -0
  151. aider/website/docs/config/aider_conf.md +527 -0
  152. aider/website/docs/config/api-keys.md +90 -0
  153. aider/website/docs/config/dotenv.md +478 -0
  154. aider/website/docs/config/editor.md +127 -0
  155. aider/website/docs/config/model-aliases.md +103 -0
  156. aider/website/docs/config/options.md +843 -0
  157. aider/website/docs/config/reasoning.md +209 -0
  158. aider/website/docs/config.md +44 -0
  159. aider/website/docs/faq.md +378 -0
  160. aider/website/docs/git.md +76 -0
  161. aider/website/docs/index.md +47 -0
  162. aider/website/docs/install/codespaces.md +39 -0
  163. aider/website/docs/install/docker.md +57 -0
  164. aider/website/docs/install/optional.md +100 -0
  165. aider/website/docs/install/replit.md +8 -0
  166. aider/website/docs/install.md +115 -0
  167. aider/website/docs/languages.md +264 -0
  168. aider/website/docs/legal/contributor-agreement.md +111 -0
  169. aider/website/docs/legal/privacy.md +104 -0
  170. aider/website/docs/llms/anthropic.md +77 -0
  171. aider/website/docs/llms/azure.md +48 -0
  172. aider/website/docs/llms/bedrock.md +132 -0
  173. aider/website/docs/llms/cohere.md +34 -0
  174. aider/website/docs/llms/deepseek.md +32 -0
  175. aider/website/docs/llms/gemini.md +49 -0
  176. aider/website/docs/llms/github.md +105 -0
  177. aider/website/docs/llms/groq.md +36 -0
  178. aider/website/docs/llms/lm-studio.md +39 -0
  179. aider/website/docs/llms/ollama.md +75 -0
  180. aider/website/docs/llms/openai-compat.md +39 -0
  181. aider/website/docs/llms/openai.md +58 -0
  182. aider/website/docs/llms/openrouter.md +78 -0
  183. aider/website/docs/llms/other.md +103 -0
  184. aider/website/docs/llms/vertex.md +50 -0
  185. aider/website/docs/llms/warnings.md +10 -0
  186. aider/website/docs/llms/xai.md +53 -0
  187. aider/website/docs/llms.md +54 -0
  188. aider/website/docs/more/analytics.md +122 -0
  189. aider/website/docs/more/edit-formats.md +116 -0
  190. aider/website/docs/more/infinite-output.md +137 -0
  191. aider/website/docs/more-info.md +8 -0
  192. aider/website/docs/recordings/auto-accept-architect.md +31 -0
  193. aider/website/docs/recordings/dont-drop-original-read-files.md +35 -0
  194. aider/website/docs/recordings/index.md +21 -0
  195. aider/website/docs/recordings/model-accepts-settings.md +69 -0
  196. aider/website/docs/recordings/tree-sitter-language-pack.md +80 -0
  197. aider/website/docs/repomap.md +112 -0
  198. aider/website/docs/scripting.md +100 -0
  199. aider/website/docs/troubleshooting/aider-not-found.md +24 -0
  200. aider/website/docs/troubleshooting/edit-errors.md +76 -0
  201. aider/website/docs/troubleshooting/imports.md +62 -0
  202. aider/website/docs/troubleshooting/models-and-keys.md +54 -0
  203. aider/website/docs/troubleshooting/support.md +79 -0
  204. aider/website/docs/troubleshooting/token-limits.md +96 -0
  205. aider/website/docs/troubleshooting/warnings.md +12 -0
  206. aider/website/docs/troubleshooting.md +11 -0
  207. aider/website/docs/usage/browser.md +57 -0
  208. aider/website/docs/usage/caching.md +49 -0
  209. aider/website/docs/usage/commands.md +132 -0
  210. aider/website/docs/usage/conventions.md +119 -0
  211. aider/website/docs/usage/copypaste.md +121 -0
  212. aider/website/docs/usage/images-urls.md +48 -0
  213. aider/website/docs/usage/lint-test.md +118 -0
  214. aider/website/docs/usage/modes.md +211 -0
  215. aider/website/docs/usage/not-code.md +179 -0
  216. aider/website/docs/usage/notifications.md +87 -0
  217. aider/website/docs/usage/tips.md +79 -0
  218. aider/website/docs/usage/tutorials.md +30 -0
  219. aider/website/docs/usage/voice.md +121 -0
  220. aider/website/docs/usage/watch.md +294 -0
  221. aider/website/docs/usage.md +92 -0
  222. aider/website/share/index.md +101 -0
  223. chatmcp_cli-0.1.0.dist-info/METADATA +502 -0
  224. chatmcp_cli-0.1.0.dist-info/RECORD +228 -0
  225. chatmcp_cli-0.1.0.dist-info/WHEEL +5 -0
  226. chatmcp_cli-0.1.0.dist-info/entry_points.txt +3 -0
  227. chatmcp_cli-0.1.0.dist-info/licenses/LICENSE.txt +202 -0
  228. chatmcp_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,757 @@
1
+ #!/usr/bin/env python
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ try:
7
+ import git
8
+ except ImportError:
9
+ git = None
10
+
11
+ from diff_match_patch import diff_match_patch
12
+ from tqdm import tqdm
13
+
14
+ from aider.dump import dump
15
+ from aider.utils import GitTemporaryDirectory
16
+
17
+
18
+ class RelativeIndenter:
19
+ """Rewrites text files to have relative indentation, which involves
20
+ reformatting the leading white space on lines. This format makes
21
+ it easier to search and apply edits to pairs of code blocks which
22
+ may differ significantly in their overall level of indentation.
23
+
24
+ It removes leading white space which is shared with the preceding
25
+ line.
26
+
27
+ Original:
28
+ ```
29
+ Foo # indented 8
30
+ Bar # indented 4 more than the previous line
31
+ Baz # same indent as the previous line
32
+ Fob # same indent as the previous line
33
+ ```
34
+
35
+ Becomes:
36
+ ```
37
+ Foo # indented 8
38
+ Bar # indented 4 more than the previous line
39
+ Baz # same indent as the previous line
40
+ Fob # same indent as the previous line
41
+ ```
42
+
43
+ If the current line is *less* indented then the previous line,
44
+ uses a unicode character to indicate outdenting.
45
+
46
+ Original
47
+ ```
48
+ Foo
49
+ Bar
50
+ Baz
51
+ Fob # indented 4 less than the previous line
52
+ ```
53
+
54
+ Becomes:
55
+ ```
56
+ Foo
57
+ Bar
58
+ Baz
59
+ ←←←←Fob # indented 4 less than the previous line
60
+ ```
61
+
62
+ This is a similar original to the last one, but every line has
63
+ been uniformly outdented:
64
+ ```
65
+ Foo
66
+ Bar
67
+ Baz
68
+ Fob # indented 4 less than the previous line
69
+ ```
70
+
71
+ It becomes this result, which is very similar to the previous
72
+ result. Only the white space on the first line differs. From the
73
+ word Foo onwards, it is identical to the previous result.
74
+ ```
75
+ Foo
76
+ Bar
77
+ Baz
78
+ ←←←←Fob # indented 4 less than the previous line
79
+ ```
80
+
81
+ """
82
+
83
+ def __init__(self, texts):
84
+ """
85
+ Based on the texts, choose a unicode character that isn't in any of them.
86
+ """
87
+
88
+ chars = set()
89
+ for text in texts:
90
+ chars.update(text)
91
+
92
+ ARROW = "←"
93
+ if ARROW not in chars:
94
+ self.marker = ARROW
95
+ else:
96
+ self.marker = self.select_unique_marker(chars)
97
+
98
+ def select_unique_marker(self, chars):
99
+ for codepoint in range(0x10FFFF, 0x10000, -1):
100
+ marker = chr(codepoint)
101
+ if marker not in chars:
102
+ return marker
103
+
104
+ raise ValueError("Could not find a unique marker")
105
+
106
+ def make_relative(self, text):
107
+ """
108
+ Transform text to use relative indents.
109
+ """
110
+
111
+ if self.marker in text:
112
+ raise ValueError(f"Text already contains the outdent marker: {self.marker}")
113
+
114
+ lines = text.splitlines(keepends=True)
115
+
116
+ output = []
117
+ prev_indent = ""
118
+ for line in lines:
119
+ line_without_end = line.rstrip("\n\r")
120
+
121
+ len_indent = len(line_without_end) - len(line_without_end.lstrip())
122
+ indent = line[:len_indent]
123
+ change = len_indent - len(prev_indent)
124
+ if change > 0:
125
+ cur_indent = indent[-change:]
126
+ elif change < 0:
127
+ cur_indent = self.marker * -change
128
+ else:
129
+ cur_indent = ""
130
+
131
+ out_line = cur_indent + "\n" + line[len_indent:]
132
+ # dump(len_indent, change, out_line)
133
+ # print(out_line)
134
+ output.append(out_line)
135
+ prev_indent = indent
136
+
137
+ res = "".join(output)
138
+ return res
139
+
140
+ def make_absolute(self, text):
141
+ """
142
+ Transform text from relative back to absolute indents.
143
+ """
144
+ lines = text.splitlines(keepends=True)
145
+
146
+ output = []
147
+ prev_indent = ""
148
+ for i in range(0, len(lines), 2):
149
+ dent = lines[i].rstrip("\r\n")
150
+ non_indent = lines[i + 1]
151
+
152
+ if dent.startswith(self.marker):
153
+ len_outdent = len(dent)
154
+ cur_indent = prev_indent[:-len_outdent]
155
+ else:
156
+ cur_indent = prev_indent + dent
157
+
158
+ if not non_indent.rstrip("\r\n"):
159
+ out_line = non_indent # don't indent a blank line
160
+ else:
161
+ out_line = cur_indent + non_indent
162
+
163
+ output.append(out_line)
164
+ prev_indent = cur_indent
165
+
166
+ res = "".join(output)
167
+ if self.marker in res:
168
+ # dump(res)
169
+ raise ValueError("Error transforming text back to absolute indents")
170
+
171
+ return res
172
+
173
+
174
+ # The patches are created to change S->R.
175
+ # So all the patch offsets are relative to S.
176
+ # But O has a lot more content. So all the offsets are very wrong.
177
+ #
178
+ # But patch_apply() seems to imply that once patch N is located,
179
+ # then it adjusts the offset of the next patch.
180
+ #
181
+ # This is great, because once we sync up after a big gap the nearby
182
+ # patches are close to being located right.
183
+ # Except when indentation has been changed by GPT.
184
+ #
185
+ # It would help to use the diff trick to build map_S_offset_to_O_offset().
186
+ # Then update all the S offsets in the S->R patches to be O offsets.
187
+ # Do we also need to update the R offsets?
188
+ #
189
+ # What if this gets funky/wrong?
190
+ #
191
+
192
+
193
+ def map_patches(texts, patches, debug):
194
+ search_text, replace_text, original_text = texts
195
+
196
+ dmp = diff_match_patch()
197
+ dmp.Diff_Timeout = 5
198
+
199
+ diff_s_o = dmp.diff_main(search_text, original_text)
200
+ # diff_r_s = dmp.diff_main(replace_text, search_text)
201
+
202
+ # dmp.diff_cleanupSemantic(diff_s_o)
203
+ # dmp.diff_cleanupEfficiency(diff_s_o)
204
+
205
+ if debug:
206
+ html = dmp.diff_prettyHtml(diff_s_o)
207
+ Path("tmp.html").write_text(html)
208
+
209
+ dump(len(search_text))
210
+ dump(len(original_text))
211
+
212
+ for patch in patches:
213
+ start1 = patch.start1
214
+ start2 = patch.start2
215
+
216
+ patch.start1 = dmp.diff_xIndex(diff_s_o, start1)
217
+ patch.start2 = dmp.diff_xIndex(diff_s_o, start2)
218
+
219
+ if debug:
220
+ print()
221
+ print(start1, repr(search_text[start1 : start1 + 50]))
222
+ print(patch.start1, repr(original_text[patch.start1 : patch.start1 + 50]))
223
+ print(patch.diffs)
224
+ print()
225
+
226
+ return patches
227
+
228
+
229
+ example = """Left
230
+ Left
231
+ 4 in
232
+ 4 in
233
+ 8 in
234
+ 4 in
235
+ Left
236
+ """
237
+
238
+
239
+ def relative_indent(texts):
240
+ ri = RelativeIndenter(texts)
241
+ texts = list(map(ri.make_relative, texts))
242
+
243
+ return ri, texts
244
+
245
+
246
+ line_padding = 100
247
+
248
+
249
+ def line_pad(text):
250
+ padding = "\n" * line_padding
251
+ return padding + text + padding
252
+
253
+
254
+ def line_unpad(text):
255
+ if set(text[:line_padding] + text[-line_padding:]) != set("\n"):
256
+ return
257
+ return text[line_padding:-line_padding]
258
+
259
+
260
+ def dmp_apply(texts, remap=True):
261
+ debug = False
262
+ # debug = True
263
+
264
+ search_text, replace_text, original_text = texts
265
+
266
+ dmp = diff_match_patch()
267
+ dmp.Diff_Timeout = 5
268
+ # dmp.Diff_EditCost = 16
269
+
270
+ if remap:
271
+ dmp.Match_Threshold = 0.95
272
+ dmp.Match_Distance = 500
273
+ dmp.Match_MaxBits = 128
274
+ dmp.Patch_Margin = 32
275
+ else:
276
+ dmp.Match_Threshold = 0.5
277
+ dmp.Match_Distance = 100_000
278
+ dmp.Match_MaxBits = 32
279
+ dmp.Patch_Margin = 8
280
+
281
+ diff = dmp.diff_main(search_text, replace_text, None)
282
+ dmp.diff_cleanupSemantic(diff)
283
+ dmp.diff_cleanupEfficiency(diff)
284
+
285
+ patches = dmp.patch_make(search_text, diff)
286
+
287
+ if debug:
288
+ html = dmp.diff_prettyHtml(diff)
289
+ Path("tmp.search_replace_diff.html").write_text(html)
290
+
291
+ for d in diff:
292
+ print(d[0], repr(d[1]))
293
+
294
+ for patch in patches:
295
+ start1 = patch.start1
296
+ print()
297
+ print(start1, repr(search_text[start1 : start1 + 10]))
298
+ print(start1, repr(replace_text[start1 : start1 + 10]))
299
+ print(patch.diffs)
300
+
301
+ # dump(original_text)
302
+ # dump(search_text)
303
+
304
+ if remap:
305
+ patches = map_patches(texts, patches, debug)
306
+
307
+ patches_text = dmp.patch_toText(patches)
308
+
309
+ new_text, success = dmp.patch_apply(patches, original_text)
310
+
311
+ all_success = False not in success
312
+
313
+ if debug:
314
+ # dump(new_text)
315
+ print(patches_text)
316
+
317
+ # print(new_text)
318
+ dump(success)
319
+ dump(all_success)
320
+
321
+ # print(new_text)
322
+
323
+ if not all_success:
324
+ return
325
+
326
+ return new_text
327
+
328
+
329
+ def lines_to_chars(lines, mapping):
330
+ new_text = []
331
+ for char in lines:
332
+ new_text.append(mapping[ord(char)])
333
+
334
+ new_text = "".join(new_text)
335
+ return new_text
336
+
337
+
338
+ def dmp_lines_apply(texts):
339
+ debug = False
340
+ # debug = True
341
+
342
+ for t in texts:
343
+ assert t.endswith("\n"), t
344
+
345
+ search_text, replace_text, original_text = texts
346
+
347
+ dmp = diff_match_patch()
348
+ dmp.Diff_Timeout = 5
349
+ # dmp.Diff_EditCost = 16
350
+
351
+ dmp.Match_Threshold = 0.1
352
+ dmp.Match_Distance = 100_000
353
+ dmp.Match_MaxBits = 32
354
+ dmp.Patch_Margin = 1
355
+
356
+ all_text = search_text + replace_text + original_text
357
+ all_lines, _, mapping = dmp.diff_linesToChars(all_text, "")
358
+ assert len(all_lines) == len(all_text.splitlines())
359
+
360
+ search_num = len(search_text.splitlines())
361
+ replace_num = len(replace_text.splitlines())
362
+ original_num = len(original_text.splitlines())
363
+
364
+ search_lines = all_lines[:search_num]
365
+ replace_lines = all_lines[search_num : search_num + replace_num]
366
+ original_lines = all_lines[search_num + replace_num :]
367
+
368
+ assert len(search_lines) == search_num
369
+ assert len(replace_lines) == replace_num
370
+ assert len(original_lines) == original_num
371
+
372
+ diff_lines = dmp.diff_main(search_lines, replace_lines, None)
373
+ dmp.diff_cleanupSemantic(diff_lines)
374
+ dmp.diff_cleanupEfficiency(diff_lines)
375
+
376
+ patches = dmp.patch_make(search_lines, diff_lines)
377
+
378
+ if debug:
379
+ diff = list(diff_lines)
380
+ dmp.diff_charsToLines(diff, mapping)
381
+ # dump(diff)
382
+ html = dmp.diff_prettyHtml(diff)
383
+ Path("tmp.search_replace_diff.html").write_text(html)
384
+
385
+ for d in diff:
386
+ print(d[0], repr(d[1]))
387
+
388
+ new_lines, success = dmp.patch_apply(patches, original_lines)
389
+ new_text = lines_to_chars(new_lines, mapping)
390
+
391
+ all_success = False not in success
392
+
393
+ if debug:
394
+ # print(new_text)
395
+ dump(success)
396
+ dump(all_success)
397
+
398
+ # print(new_text)
399
+
400
+ if not all_success:
401
+ return
402
+
403
+ return new_text
404
+
405
+
406
+ def diff_lines(search_text, replace_text):
407
+ dmp = diff_match_patch()
408
+ dmp.Diff_Timeout = 5
409
+ # dmp.Diff_EditCost = 16
410
+ search_lines, replace_lines, mapping = dmp.diff_linesToChars(search_text, replace_text)
411
+
412
+ diff_lines = dmp.diff_main(search_lines, replace_lines, None)
413
+ dmp.diff_cleanupSemantic(diff_lines)
414
+ dmp.diff_cleanupEfficiency(diff_lines)
415
+
416
+ diff = list(diff_lines)
417
+ dmp.diff_charsToLines(diff, mapping)
418
+ # dump(diff)
419
+
420
+ udiff = []
421
+ for d, lines in diff:
422
+ if d < 0:
423
+ d = "-"
424
+ elif d > 0:
425
+ d = "+"
426
+ else:
427
+ d = " "
428
+ for line in lines.splitlines(keepends=True):
429
+ udiff.append(d + line)
430
+
431
+ return udiff
432
+
433
+
434
+ def search_and_replace(texts):
435
+ search_text, replace_text, original_text = texts
436
+
437
+ num = original_text.count(search_text)
438
+ # if num > 1:
439
+ # raise SearchTextNotUnique()
440
+ if num == 0:
441
+ return
442
+
443
+ new_text = original_text.replace(search_text, replace_text)
444
+
445
+ return new_text
446
+
447
+
448
+ def git_cherry_pick_osr_onto_o(texts):
449
+ search_text, replace_text, original_text = texts
450
+
451
+ with GitTemporaryDirectory() as dname:
452
+ repo = git.Repo(dname)
453
+
454
+ fname = Path(dname) / "file.txt"
455
+
456
+ # Make O->S->R
457
+ fname.write_text(original_text)
458
+ repo.git.add(str(fname))
459
+ repo.git.commit("-m", "original")
460
+ original_hash = repo.head.commit.hexsha
461
+
462
+ fname.write_text(search_text)
463
+ repo.git.add(str(fname))
464
+ repo.git.commit("-m", "search")
465
+
466
+ fname.write_text(replace_text)
467
+ repo.git.add(str(fname))
468
+ repo.git.commit("-m", "replace")
469
+ replace_hash = repo.head.commit.hexsha
470
+
471
+ # go back to O
472
+ repo.git.checkout(original_hash)
473
+
474
+ # cherry pick R onto original
475
+ try:
476
+ repo.git.cherry_pick(replace_hash, "--minimal")
477
+ except (git.exc.ODBError, git.exc.GitError):
478
+ # merge conflicts!
479
+ return
480
+
481
+ new_text = fname.read_text()
482
+ return new_text
483
+
484
+
485
+ def git_cherry_pick_sr_onto_so(texts):
486
+ search_text, replace_text, original_text = texts
487
+
488
+ with GitTemporaryDirectory() as dname:
489
+ repo = git.Repo(dname)
490
+
491
+ fname = Path(dname) / "file.txt"
492
+
493
+ fname.write_text(search_text)
494
+ repo.git.add(str(fname))
495
+ repo.git.commit("-m", "search")
496
+ search_hash = repo.head.commit.hexsha
497
+
498
+ # make search->replace
499
+ fname.write_text(replace_text)
500
+ repo.git.add(str(fname))
501
+ repo.git.commit("-m", "replace")
502
+ replace_hash = repo.head.commit.hexsha
503
+
504
+ # go back to search,
505
+ repo.git.checkout(search_hash)
506
+
507
+ # make search->original
508
+ fname.write_text(original_text)
509
+ repo.git.add(str(fname))
510
+ repo.git.commit("-m", "original")
511
+
512
+ # cherry pick replace onto original
513
+ try:
514
+ repo.git.cherry_pick(replace_hash, "--minimal")
515
+ except (git.exc.ODBError, git.exc.GitError):
516
+ # merge conflicts!
517
+ return
518
+
519
+ new_text = fname.read_text()
520
+
521
+ return new_text
522
+
523
+
524
+ class SearchTextNotUnique(ValueError):
525
+ pass
526
+
527
+
528
+ all_preprocs = [
529
+ # (strip_blank_lines, relative_indent, reverse_lines)
530
+ (False, False, False),
531
+ (True, False, False),
532
+ (False, True, False),
533
+ (True, True, False),
534
+ # (False, False, True),
535
+ # (True, False, True),
536
+ # (False, True, True),
537
+ # (True, True, True),
538
+ ]
539
+
540
+ always_relative_indent = [
541
+ (False, True, False),
542
+ (True, True, False),
543
+ # (False, True, True),
544
+ # (True, True, True),
545
+ ]
546
+
547
+ editblock_strategies = [
548
+ (search_and_replace, all_preprocs),
549
+ (git_cherry_pick_osr_onto_o, all_preprocs),
550
+ (dmp_lines_apply, all_preprocs),
551
+ ]
552
+
553
+ never_relative = [
554
+ (False, False),
555
+ (True, False),
556
+ ]
557
+
558
+ udiff_strategies = [
559
+ (search_and_replace, all_preprocs),
560
+ (git_cherry_pick_osr_onto_o, all_preprocs),
561
+ (dmp_lines_apply, all_preprocs),
562
+ ]
563
+
564
+
565
+ def flexible_search_and_replace(texts, strategies):
566
+ """Try a series of search/replace methods, starting from the most
567
+ literal interpretation of search_text. If needed, progress to more
568
+ flexible methods, which can accommodate divergence between
569
+ search_text and original_text and yet still achieve the desired
570
+ edits.
571
+ """
572
+
573
+ for strategy, preprocs in strategies:
574
+ for preproc in preprocs:
575
+ res = try_strategy(texts, strategy, preproc)
576
+ if res:
577
+ return res
578
+
579
+
580
+ def reverse_lines(text):
581
+ lines = text.splitlines(keepends=True)
582
+ lines.reverse()
583
+ return "".join(lines)
584
+
585
+
586
+ def try_strategy(texts, strategy, preproc):
587
+ preproc_strip_blank_lines, preproc_relative_indent, preproc_reverse = preproc
588
+ ri = None
589
+
590
+ if preproc_strip_blank_lines:
591
+ texts = strip_blank_lines(texts)
592
+ if preproc_relative_indent:
593
+ ri, texts = relative_indent(texts)
594
+ if preproc_reverse:
595
+ texts = list(map(reverse_lines, texts))
596
+
597
+ res = strategy(texts)
598
+
599
+ if res and preproc_reverse:
600
+ res = reverse_lines(res)
601
+
602
+ if res and preproc_relative_indent:
603
+ try:
604
+ res = ri.make_absolute(res)
605
+ except ValueError:
606
+ return
607
+
608
+ return res
609
+
610
+
611
+ def strip_blank_lines(texts):
612
+ # strip leading and trailing blank lines
613
+ texts = [text.strip("\n") + "\n" for text in texts]
614
+ return texts
615
+
616
+
617
+ def read_text(fname):
618
+ text = Path(fname).read_text()
619
+ return text
620
+
621
+
622
+ def proc(dname):
623
+ dname = Path(dname)
624
+
625
+ try:
626
+ search_text = read_text(dname / "search")
627
+ replace_text = read_text(dname / "replace")
628
+ original_text = read_text(dname / "original")
629
+ except FileNotFoundError:
630
+ return
631
+
632
+ ####
633
+
634
+ texts = search_text, replace_text, original_text
635
+
636
+ strategies = [
637
+ # (search_and_replace, all_preprocs),
638
+ # (git_cherry_pick_osr_onto_o, all_preprocs),
639
+ # (git_cherry_pick_sr_onto_so, all_preprocs),
640
+ # (dmp_apply, all_preprocs),
641
+ (dmp_lines_apply, all_preprocs),
642
+ ]
643
+
644
+ short_names = dict(
645
+ search_and_replace="sr",
646
+ git_cherry_pick_osr_onto_o="cp_o",
647
+ git_cherry_pick_sr_onto_so="cp_so",
648
+ dmp_apply="dmp",
649
+ dmp_lines_apply="dmpl",
650
+ )
651
+
652
+ patched = dict()
653
+ for strategy, preprocs in strategies:
654
+ for preproc in preprocs:
655
+ method = strategy.__name__
656
+ method = short_names[method]
657
+
658
+ strip_blank, rel_indent, rev_lines = preproc
659
+ if strip_blank or rel_indent:
660
+ method += "_"
661
+ if strip_blank:
662
+ method += "s"
663
+ if rel_indent:
664
+ method += "i"
665
+ if rev_lines:
666
+ method += "r"
667
+
668
+ res = try_strategy(texts, strategy, preproc)
669
+ patched[method] = res
670
+
671
+ results = []
672
+ for method, res in patched.items():
673
+ out_fname = dname / f"original.{method}"
674
+ if out_fname.exists():
675
+ out_fname.unlink()
676
+
677
+ if res:
678
+ out_fname.write_text(res)
679
+
680
+ correct = (dname / "correct").read_text()
681
+ if res == correct:
682
+ res = "pass"
683
+ else:
684
+ res = "WRONG"
685
+ else:
686
+ res = "fail"
687
+
688
+ results.append((method, res))
689
+
690
+ return results
691
+
692
+
693
+ def colorize_result(result):
694
+ colors = {
695
+ "pass": "\033[102;30mpass\033[0m", # Green background, black text
696
+ "WRONG": "\033[101;30mWRONG\033[0m", # Red background, black text
697
+ "fail": "\033[103;30mfail\033[0m", # Yellow background, black text
698
+ }
699
+ return colors.get(result, result) # Default to original result if not found
700
+
701
+
702
+ def main(dnames):
703
+ all_results = []
704
+ for dname in tqdm(dnames):
705
+ dname = Path(dname)
706
+ results = proc(dname)
707
+ for method, res in results:
708
+ all_results.append((dname, method, res))
709
+ # print(dname, method, colorize_result(res))
710
+
711
+ # Create a 2D table with directories along the right and methods along the top
712
+ # Collect all unique methods and directories
713
+ methods = []
714
+ for _, method, _ in all_results:
715
+ if method not in methods:
716
+ methods.append(method)
717
+
718
+ directories = dnames
719
+
720
+ # Sort directories by decreasing number of 'pass' results
721
+ pass_counts = {
722
+ dname: sum(
723
+ res == "pass" for dname_result, _, res in all_results if str(dname) == str(dname_result)
724
+ )
725
+ for dname in directories
726
+ }
727
+ directories.sort(key=lambda dname: pass_counts[dname], reverse=True)
728
+
729
+ # Create a results matrix
730
+ results_matrix = {dname: {method: "" for method in methods} for dname in directories}
731
+
732
+ # Populate the results matrix
733
+ for dname, method, res in all_results:
734
+ results_matrix[str(dname)][method] = res
735
+
736
+ # Print the 2D table
737
+ # Print the header
738
+ print("{:<20}".format("Directory"), end="")
739
+ for method in methods:
740
+ print("{:<9}".format(method), end="")
741
+ print()
742
+
743
+ # Print the rows with colorized results
744
+ for dname in directories:
745
+ print("{:<20}".format(Path(dname).name), end="")
746
+ for method in methods:
747
+ res = results_matrix[dname][method]
748
+ colorized_res = colorize_result(res)
749
+ res_l = 9 + len(colorized_res) - len(res)
750
+ fmt = "{:<" + str(res_l) + "}"
751
+ print(fmt.format(colorized_res), end="")
752
+ print()
753
+
754
+
755
+ if __name__ == "__main__":
756
+ status = main(sys.argv[1:])
757
+ sys.exit(status)