janito 1.10.0__py3-none-any.whl → 1.11.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.
- janito/__init__.py +1 -1
- janito/agent/conversation_api.py +178 -90
- janito/agent/conversation_ui.py +1 -1
- janito/agent/llm_conversation_history.py +12 -0
- janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +19 -4
- janito/agent/tools/__init__.py +2 -0
- janito/agent/tools/create_directory.py +1 -1
- janito/agent/tools/create_file.py +1 -1
- janito/agent/tools/fetch_url.py +1 -1
- janito/agent/tools/find_files.py +26 -13
- janito/agent/tools/get_file_outline/core.py +1 -1
- janito/agent/tools/get_file_outline/python_outline.py +139 -95
- janito/agent/tools/get_lines.py +92 -63
- janito/agent/tools/move_file.py +58 -32
- janito/agent/tools/open_url.py +31 -0
- janito/agent/tools/python_command_runner.py +85 -86
- janito/agent/tools/python_file_runner.py +85 -86
- janito/agent/tools/python_stdin_runner.py +87 -88
- janito/agent/tools/remove_directory.py +1 -1
- janito/agent/tools/remove_file.py +1 -1
- janito/agent/tools/replace_file.py +2 -2
- janito/agent/tools/replace_text_in_file.py +193 -149
- janito/agent/tools/run_bash_command.py +1 -1
- janito/agent/tools/run_powershell_command.py +4 -0
- janito/agent/tools/search_text/__init__.py +1 -0
- janito/agent/tools/search_text/core.py +176 -0
- janito/agent/tools/search_text/match_lines.py +58 -0
- janito/agent/tools/search_text/pattern_utils.py +65 -0
- janito/agent/tools/search_text/traverse_directory.py +132 -0
- janito/agent/tools/validate_file_syntax/core.py +41 -30
- janito/agent/tools/validate_file_syntax/html_validator.py +21 -5
- janito/agent/tools/validate_file_syntax/markdown_validator.py +77 -34
- janito/agent/tools_utils/gitignore_utils.py +25 -2
- janito/agent/tools_utils/utils.py +7 -1
- janito/cli/config_commands.py +112 -109
- janito/shell/main.py +51 -8
- janito/shell/session/config.py +83 -75
- janito/shell/ui/interactive.py +97 -73
- janito/termweb/static/editor.css +32 -29
- janito/termweb/static/editor.css.bak +140 -22
- janito/termweb/static/editor.html +12 -7
- janito/termweb/static/editor.html.bak +16 -11
- janito/termweb/static/editor.js +94 -40
- janito/termweb/static/editor.js.bak +97 -65
- janito/termweb/static/index.html +1 -2
- janito/termweb/static/index.html.bak +1 -1
- janito/termweb/static/termweb.css +1 -22
- janito/termweb/static/termweb.css.bak +6 -4
- janito/termweb/static/termweb.js +0 -6
- janito/termweb/static/termweb.js.bak +1 -2
- {janito-1.10.0.dist-info → janito-1.11.1.dist-info}/METADATA +1 -1
- {janito-1.10.0.dist-info → janito-1.11.1.dist-info}/RECORD +56 -51
- {janito-1.10.0.dist-info → janito-1.11.1.dist-info}/WHEEL +1 -1
- janito/agent/tools/search_text.py +0 -254
- {janito-1.10.0.dist-info → janito-1.11.1.dist-info}/entry_points.txt +0 -0
- {janito-1.10.0.dist-info → janito-1.11.1.dist-info}/licenses/LICENSE +0 -0
- {janito-1.10.0.dist-info → janito-1.11.1.dist-info}/top_level.txt +0 -0
@@ -2,6 +2,8 @@ from janito.agent.tool_base import ToolBase
|
|
2
2
|
from janito.agent.tools_utils.action_type import ActionType
|
3
3
|
from janito.agent.tool_registry import register_tool
|
4
4
|
from janito.i18n import tr
|
5
|
+
import shutil
|
6
|
+
import re
|
5
7
|
|
6
8
|
|
7
9
|
@register_tool(name="replace_text_in_file")
|
@@ -9,6 +11,10 @@ class ReplaceTextInFileTool(ToolBase):
|
|
9
11
|
"""
|
10
12
|
Replace exact occurrences of a given text in a file.
|
11
13
|
|
14
|
+
Note:
|
15
|
+
To avoid syntax errors, ensure your replacement text is pre-indented as needed, matching the indentation of the
|
16
|
+
search text in its original location.
|
17
|
+
|
12
18
|
Args:
|
13
19
|
file_path (str): Path to the file to modify.
|
14
20
|
search_text (str): The exact text to search for (including indentation).
|
@@ -36,9 +42,151 @@ class ReplaceTextInFileTool(ToolBase):
|
|
36
42
|
action = "(all)" if replace_all else ""
|
37
43
|
search_lines = len(search_text.splitlines())
|
38
44
|
replace_lines = len(replacement_text.splitlines())
|
45
|
+
info_msg = self._format_info_msg(
|
46
|
+
disp_path,
|
47
|
+
search_lines,
|
48
|
+
replace_lines,
|
49
|
+
action,
|
50
|
+
search_text,
|
51
|
+
replacement_text,
|
52
|
+
file_path,
|
53
|
+
)
|
54
|
+
self.report_info(ActionType.WRITE, info_msg)
|
55
|
+
try:
|
56
|
+
content = self._read_file_content(file_path)
|
57
|
+
match_lines = self._find_match_lines(content, search_text)
|
58
|
+
occurrences = content.count(search_text)
|
59
|
+
replaced_count, new_content = self._replace_content(
|
60
|
+
content, search_text, replacement_text, replace_all, occurrences
|
61
|
+
)
|
62
|
+
file_changed = new_content != content
|
63
|
+
backup_path = file_path + ".bak"
|
64
|
+
if backup and file_changed:
|
65
|
+
self._backup_file(file_path, backup_path)
|
66
|
+
if file_changed:
|
67
|
+
self._write_file_content(file_path, new_content)
|
68
|
+
warning, concise_warning = self._handle_warnings(
|
69
|
+
replaced_count, file_changed, occurrences
|
70
|
+
)
|
71
|
+
if warning:
|
72
|
+
self.report_warning(warning)
|
73
|
+
if concise_warning:
|
74
|
+
return concise_warning
|
75
|
+
self._report_success(match_lines)
|
76
|
+
line_delta_str = self._get_line_delta_str(content, new_content)
|
77
|
+
match_info, details = self._format_match_details(
|
78
|
+
replaced_count,
|
79
|
+
match_lines,
|
80
|
+
search_lines,
|
81
|
+
replace_lines,
|
82
|
+
line_delta_str,
|
83
|
+
replace_all,
|
84
|
+
)
|
85
|
+
return self._format_final_msg(
|
86
|
+
file_path, warning, backup_path, match_info, details
|
87
|
+
)
|
88
|
+
except Exception as e:
|
89
|
+
self.report_error(tr(" \u274c Error"))
|
90
|
+
return tr("Error replacing text: {error}", error=e)
|
91
|
+
|
92
|
+
def _read_file_content(self, file_path):
|
93
|
+
"""Read the entire content of the file."""
|
94
|
+
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
|
95
|
+
return f.read()
|
96
|
+
|
97
|
+
def _find_match_lines(self, content, search_text):
|
98
|
+
"""Find all line numbers where search_text occurs in content."""
|
99
|
+
lines = content.splitlines(keepends=True)
|
100
|
+
joined = "".join(lines)
|
101
|
+
match_lines = []
|
102
|
+
idx = 0
|
103
|
+
while True:
|
104
|
+
idx = joined.find(search_text, idx)
|
105
|
+
if idx == -1:
|
106
|
+
break
|
107
|
+
upto = joined[:idx]
|
108
|
+
line_no = upto.count("\n") + 1
|
109
|
+
match_lines.append(line_no)
|
110
|
+
idx += 1 if not search_text else len(search_text)
|
111
|
+
return match_lines
|
112
|
+
|
113
|
+
def _replace_content(
|
114
|
+
self, content, search_text, replacement_text, replace_all, occurrences
|
115
|
+
):
|
116
|
+
"""Replace occurrences of search_text with replacement_text in content."""
|
117
|
+
if replace_all:
|
118
|
+
replaced_count = content.count(search_text)
|
119
|
+
new_content = content.replace(search_text, replacement_text)
|
120
|
+
else:
|
121
|
+
if occurrences > 1:
|
122
|
+
return 0, content # No changes made, not unique
|
123
|
+
replaced_count = 1 if occurrences == 1 else 0
|
124
|
+
new_content = content.replace(search_text, replacement_text, 1)
|
125
|
+
return replaced_count, new_content
|
126
|
+
|
127
|
+
def _backup_file(self, file_path, backup_path):
|
128
|
+
"""Create a backup of the file."""
|
129
|
+
shutil.copy2(file_path, backup_path)
|
130
|
+
|
131
|
+
def _write_file_content(self, file_path, content):
|
132
|
+
"""Write content to the file."""
|
133
|
+
with open(file_path, "w", encoding="utf-8", errors="replace") as f:
|
134
|
+
f.write(content)
|
135
|
+
|
136
|
+
def _handle_warnings(self, replaced_count, file_changed, occurrences):
|
137
|
+
"""Handle and return warnings and concise warnings if needed."""
|
138
|
+
warning = ""
|
139
|
+
concise_warning = None
|
140
|
+
if replaced_count == 0:
|
141
|
+
warning = tr(" [Warning: Search text not found in file]")
|
142
|
+
if not file_changed:
|
143
|
+
self.report_warning(tr(" \u2139\ufe0f No changes made. [not found]"))
|
144
|
+
concise_warning = tr(
|
145
|
+
"No changes made. The search text was not found. Expand your search context with surrounding lines if needed."
|
146
|
+
)
|
147
|
+
if occurrences > 1 and replaced_count == 0:
|
148
|
+
self.report_warning(tr(" \u2139\ufe0f No changes made. [not unique]"))
|
149
|
+
concise_warning = tr(
|
150
|
+
"No changes made. The search text is not unique. Expand your search context with surrounding lines to ensure uniqueness."
|
151
|
+
)
|
152
|
+
return warning, concise_warning
|
153
|
+
|
154
|
+
def _report_success(self, match_lines):
|
155
|
+
"""Report success with line numbers where replacements occurred."""
|
156
|
+
if match_lines:
|
157
|
+
lines_str = ", ".join(str(line_no) for line_no in match_lines)
|
158
|
+
self.report_success(
|
159
|
+
tr(" \u2705 replaced at {lines_str}", lines_str=lines_str)
|
160
|
+
)
|
161
|
+
else:
|
162
|
+
self.report_success(tr(" \u2705 replaced (lines unknown)"))
|
163
|
+
|
164
|
+
def _get_line_delta_str(self, content, new_content):
|
165
|
+
"""Return a string describing the net line change after replacement."""
|
166
|
+
total_lines_before = content.count("\n") + 1
|
167
|
+
total_lines_after = new_content.count("\n") + 1
|
168
|
+
line_delta = total_lines_after - total_lines_before
|
169
|
+
if line_delta > 0:
|
170
|
+
return f" (+{line_delta} lines)"
|
171
|
+
elif line_delta < 0:
|
172
|
+
return f" ({line_delta} lines)"
|
173
|
+
else:
|
174
|
+
return " (no net line change)"
|
175
|
+
|
176
|
+
def _format_info_msg(
|
177
|
+
self,
|
178
|
+
disp_path,
|
179
|
+
search_lines,
|
180
|
+
replace_lines,
|
181
|
+
action,
|
182
|
+
search_text,
|
183
|
+
replacement_text,
|
184
|
+
file_path,
|
185
|
+
):
|
186
|
+
"""Format the info message for the operation."""
|
39
187
|
if replace_lines == 0:
|
40
|
-
|
41
|
-
"📝
|
188
|
+
return tr(
|
189
|
+
"📝 Replace in {disp_path} del {search_lines} lines {action}",
|
42
190
|
disp_path=disp_path,
|
43
191
|
search_lines=search_lines,
|
44
192
|
action=action,
|
@@ -48,7 +196,7 @@ class ReplaceTextInFileTool(ToolBase):
|
|
48
196
|
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
|
49
197
|
_content = f.read()
|
50
198
|
_new_content = _content.replace(
|
51
|
-
search_text, replacement_text, -1 if
|
199
|
+
search_text, replacement_text, -1 if action else 1
|
52
200
|
)
|
53
201
|
_total_lines_before = _content.count("\n") + 1
|
54
202
|
_total_lines_after = _new_content.count("\n") + 1
|
@@ -61,158 +209,54 @@ class ReplaceTextInFileTool(ToolBase):
|
|
61
209
|
delta_str = f"{_line_delta} lines"
|
62
210
|
else:
|
63
211
|
delta_str = "+0"
|
64
|
-
|
65
|
-
"📝
|
212
|
+
return tr(
|
213
|
+
"📝 Replace in {disp_path} {delta_str} {action}",
|
66
214
|
disp_path=disp_path,
|
67
215
|
delta_str=delta_str,
|
68
216
|
action=action,
|
69
217
|
)
|
70
|
-
self.report_info(
|
71
|
-
ActionType.WRITE,
|
72
|
-
info_msg + (" ..." if not info_msg.rstrip().endswith("...") else ""),
|
73
|
-
)
|
74
|
-
try:
|
75
|
-
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
|
76
|
-
content = f.read()
|
77
|
-
|
78
|
-
def find_match_lines(content, search_text):
|
79
|
-
lines = content.splitlines(keepends=True)
|
80
|
-
joined = "".join(lines)
|
81
|
-
match_lines = []
|
82
|
-
idx = 0
|
83
|
-
while True:
|
84
|
-
idx = joined.find(search_text, idx)
|
85
|
-
if idx == -1:
|
86
|
-
break
|
87
|
-
upto = joined[:idx]
|
88
|
-
line_no = upto.count("\n") + 1
|
89
|
-
match_lines.append(line_no)
|
90
|
-
idx += 1 if not search_text else len(search_text)
|
91
|
-
return match_lines
|
92
|
-
|
93
|
-
match_lines = find_match_lines(content, search_text)
|
94
|
-
if replace_all:
|
95
|
-
replaced_count = content.count(search_text)
|
96
|
-
new_content = content.replace(search_text, replacement_text)
|
97
|
-
else:
|
98
|
-
occurrences = content.count(search_text)
|
99
|
-
if occurrences > 1:
|
100
|
-
self.report_warning(tr(" ℹ️ No changes made. [not unique]"))
|
101
|
-
warning_detail = tr(
|
102
|
-
"The search text is not unique. Expand your search context with surrounding lines to ensure uniqueness."
|
103
|
-
)
|
104
|
-
return tr(
|
105
|
-
"No changes made. {warning_detail}",
|
106
|
-
warning_detail=warning_detail,
|
107
|
-
)
|
108
|
-
replaced_count = 1 if occurrences == 1 else 0
|
109
|
-
new_content = content.replace(search_text, replacement_text, 1)
|
110
|
-
import shutil
|
111
|
-
|
112
|
-
backup_path = file_path + ".bak"
|
113
|
-
if backup and new_content != content:
|
114
|
-
shutil.copy2(file_path, backup_path)
|
115
|
-
if new_content != content:
|
116
|
-
with open(file_path, "w", encoding="utf-8", errors="replace") as f:
|
117
|
-
f.write(new_content)
|
118
|
-
file_changed = True
|
119
|
-
else:
|
120
|
-
file_changed = False
|
121
|
-
warning = ""
|
122
|
-
if replaced_count == 0:
|
123
|
-
warning = tr(" [Warning: Search text not found in file]")
|
124
|
-
if not file_changed:
|
125
|
-
self.report_warning(tr(" ℹ️ No changes made. [not found]"))
|
126
|
-
concise_warning = tr(
|
127
|
-
"The search text was not found. Expand your search context with surrounding lines if needed."
|
128
|
-
)
|
129
|
-
return tr(
|
130
|
-
"No changes made. {concise_warning}",
|
131
|
-
concise_warning=concise_warning,
|
132
|
-
)
|
133
|
-
if match_lines:
|
134
|
-
lines_str = ", ".join(str(line_no) for line_no in match_lines)
|
135
|
-
self.report_success(
|
136
|
-
tr(" ✅ replaced at {lines_str}", lines_str=lines_str)
|
137
|
-
)
|
138
|
-
else:
|
139
|
-
self.report_success(tr(" ✅ replaced (lines unknown)"))
|
140
218
|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
)
|
157
|
-
indent_warning = ""
|
158
|
-
if search_indent != replace_indent:
|
159
|
-
indent_warning = tr(
|
160
|
-
" [Warning: Indentation mismatch between search and replacement text: '{search_indent}' vs '{replace_indent}']",
|
161
|
-
search_indent=search_indent,
|
162
|
-
replace_indent=replace_indent,
|
163
|
-
)
|
164
|
-
total_lines_before = content.count("\n") + 1
|
165
|
-
total_lines_after = new_content.count("\n") + 1
|
166
|
-
line_delta = total_lines_after - total_lines_before
|
167
|
-
line_delta_str = (
|
168
|
-
f" (+{line_delta} lines)"
|
169
|
-
if line_delta > 0
|
170
|
-
else (
|
171
|
-
f" ({line_delta} lines)"
|
172
|
-
if line_delta < 0
|
173
|
-
else " (no net line change)"
|
174
|
-
)
|
175
|
-
)
|
176
|
-
if replaced_count > 0:
|
177
|
-
if replace_all:
|
178
|
-
match_info = tr(
|
179
|
-
"Matches found at lines: {lines}. ",
|
180
|
-
lines=", ".join(str(line) for line in match_lines),
|
181
|
-
)
|
182
|
-
else:
|
183
|
-
match_info = (
|
184
|
-
tr("Match found at line {line}. ", line=match_lines[0])
|
185
|
-
if match_lines
|
186
|
-
else ""
|
187
|
-
)
|
188
|
-
details = tr(
|
189
|
-
"Replaced {replaced_count} occurrence(s) at above line(s): {search_lines} lines replaced with {replace_lines} lines each.{line_delta_str}",
|
190
|
-
replaced_count=replaced_count,
|
191
|
-
search_lines=search_lines,
|
192
|
-
replace_lines=replace_lines,
|
193
|
-
line_delta_str=line_delta_str,
|
219
|
+
def _format_match_details(
|
220
|
+
self,
|
221
|
+
replaced_count,
|
222
|
+
match_lines,
|
223
|
+
search_lines,
|
224
|
+
replace_lines,
|
225
|
+
line_delta_str,
|
226
|
+
replace_all,
|
227
|
+
):
|
228
|
+
"""Format match info and details for the final message."""
|
229
|
+
if replaced_count > 0:
|
230
|
+
if replace_all:
|
231
|
+
match_info = tr(
|
232
|
+
"Matches found at lines: {lines}. ",
|
233
|
+
lines=", ".join(str(line) for line in match_lines),
|
194
234
|
)
|
195
235
|
else:
|
196
|
-
match_info =
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
"Text replaced in {file_path}{warning}{indent_warning} (backup at {backup_path})\n{warning_detail}",
|
201
|
-
file_path=file_path,
|
202
|
-
warning=warning,
|
203
|
-
indent_warning=indent_warning,
|
204
|
-
backup_path=backup_path,
|
205
|
-
warning_detail=warning_detail,
|
236
|
+
match_info = (
|
237
|
+
tr("Match found at line {line}. ", line=match_lines[0])
|
238
|
+
if match_lines
|
239
|
+
else ""
|
206
240
|
)
|
207
|
-
|
208
|
-
"
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
match_info=match_info,
|
214
|
-
details=details,
|
241
|
+
details = tr(
|
242
|
+
"Replaced {replaced_count} occurrence(s) at above line(s): {search_lines} lines replaced with {replace_lines} lines each.{line_delta_str}",
|
243
|
+
replaced_count=replaced_count,
|
244
|
+
search_lines=search_lines,
|
245
|
+
replace_lines=replace_lines,
|
246
|
+
line_delta_str=line_delta_str,
|
215
247
|
)
|
216
|
-
|
217
|
-
|
218
|
-
|
248
|
+
else:
|
249
|
+
match_info = ""
|
250
|
+
details = ""
|
251
|
+
return match_info, details
|
252
|
+
|
253
|
+
def _format_final_msg(self, file_path, warning, backup_path, match_info, details):
|
254
|
+
"""Format the final status message."""
|
255
|
+
return tr(
|
256
|
+
"Text replaced in {file_path}{warning} (backup at {backup_path}). {match_info}{details}",
|
257
|
+
file_path=file_path,
|
258
|
+
warning=warning,
|
259
|
+
backup_path=backup_path,
|
260
|
+
match_info=match_info,
|
261
|
+
details=details,
|
262
|
+
)
|
@@ -34,7 +34,7 @@ class RunBashCommandTool(ToolBase):
|
|
34
34
|
return tr("Warning: Empty command provided. Operation skipped.")
|
35
35
|
self.report_info(
|
36
36
|
ActionType.EXECUTE,
|
37
|
-
tr("🖥️
|
37
|
+
tr("🖥️ Run bash command: {command} ...\n", command=command),
|
38
38
|
)
|
39
39
|
if requires_user_input:
|
40
40
|
self.report_warning(
|
@@ -3,6 +3,7 @@ from janito.agent.tools_utils.action_type import ActionType
|
|
3
3
|
from janito.agent.tool_registry import register_tool
|
4
4
|
from janito.i18n import tr
|
5
5
|
import subprocess
|
6
|
+
import os
|
6
7
|
import tempfile
|
7
8
|
import threading
|
8
9
|
|
@@ -44,6 +45,8 @@ class RunPowerShellCommandTool(ToolBase):
|
|
44
45
|
return True
|
45
46
|
|
46
47
|
def _launch_process(self, shell_exe, command_with_encoding):
|
48
|
+
env = os.environ.copy()
|
49
|
+
env["PYTHONIOENCODING"] = "utf-8"
|
47
50
|
return subprocess.Popen(
|
48
51
|
[
|
49
52
|
shell_exe,
|
@@ -59,6 +62,7 @@ class RunPowerShellCommandTool(ToolBase):
|
|
59
62
|
bufsize=1,
|
60
63
|
universal_newlines=True,
|
61
64
|
encoding="utf-8",
|
65
|
+
env=env,
|
62
66
|
)
|
63
67
|
|
64
68
|
def _stream_output(self, stream, file_obj, report_func, count_func, counter):
|
@@ -0,0 +1 @@
|
|
1
|
+
from .core import SearchTextTool
|
@@ -0,0 +1,176 @@
|
|
1
|
+
from janito.agent.tool_base import ToolBase
|
2
|
+
from janito.agent.tools_utils.action_type import ActionType
|
3
|
+
from janito.agent.tool_registry import register_tool
|
4
|
+
from janito.agent.tools_utils.utils import pluralize, display_path
|
5
|
+
from janito.i18n import tr
|
6
|
+
import os
|
7
|
+
from .pattern_utils import prepare_pattern, format_result, summarize_total
|
8
|
+
from .match_lines import read_file_lines
|
9
|
+
from .traverse_directory import traverse_directory
|
10
|
+
|
11
|
+
|
12
|
+
@register_tool(name="search_text")
|
13
|
+
class SearchTextTool(ToolBase):
|
14
|
+
"""
|
15
|
+
Search for a text pattern (regex or plain string) in all files within one or more directories or file paths and return matching lines or counts. Respects .gitignore.
|
16
|
+
Args:
|
17
|
+
paths (str): String of one or more paths (space-separated) to search in. Each path can be a directory or a file.
|
18
|
+
pattern (str): Regex pattern or plain text substring to search for in files. Must not be empty. Tries regex first, falls back to substring if regex is invalid.
|
19
|
+
is_regex (bool): If True, treat pattern as a regular expression. If False, treat as plain text (default).
|
20
|
+
max_depth (int, optional): Maximum directory depth to search. If 0 (default), search is recursive with no depth limit. If >0, limits recursion to that depth. Setting max_depth=1 disables recursion (only top-level directory). Ignored for file paths.
|
21
|
+
max_results (int, optional): Maximum number of results to return. Defaults to 100. 0 means no limit.
|
22
|
+
count_only (bool): If True, return only the count of matches per file and total, not the matching lines. Default is False.
|
23
|
+
Returns:
|
24
|
+
str: If count_only is False, matching lines from files as a newline-separated string, each formatted as 'filepath:lineno: line'.
|
25
|
+
If count_only is True, returns per-file and total match counts.
|
26
|
+
If max_results is reached, appends a note to the output.
|
27
|
+
"""
|
28
|
+
|
29
|
+
def _handle_file(
|
30
|
+
self,
|
31
|
+
search_path,
|
32
|
+
pattern,
|
33
|
+
regex,
|
34
|
+
use_regex,
|
35
|
+
max_results,
|
36
|
+
total_results,
|
37
|
+
count_only,
|
38
|
+
):
|
39
|
+
if count_only:
|
40
|
+
match_count, dir_limit_reached, _ = read_file_lines(
|
41
|
+
search_path,
|
42
|
+
pattern,
|
43
|
+
regex,
|
44
|
+
use_regex,
|
45
|
+
True,
|
46
|
+
max_results,
|
47
|
+
total_results,
|
48
|
+
)
|
49
|
+
per_file_counts = [(search_path, match_count)] if match_count > 0 else []
|
50
|
+
return [], dir_limit_reached, per_file_counts
|
51
|
+
else:
|
52
|
+
dir_output, dir_limit_reached, match_count_list = read_file_lines(
|
53
|
+
search_path,
|
54
|
+
pattern,
|
55
|
+
regex,
|
56
|
+
use_regex,
|
57
|
+
False,
|
58
|
+
max_results,
|
59
|
+
total_results,
|
60
|
+
)
|
61
|
+
per_file_counts = (
|
62
|
+
[(search_path, len(match_count_list))]
|
63
|
+
if match_count_list and len(match_count_list) > 0
|
64
|
+
else []
|
65
|
+
)
|
66
|
+
return dir_output, dir_limit_reached, per_file_counts
|
67
|
+
|
68
|
+
def _handle_path(
|
69
|
+
self,
|
70
|
+
search_path,
|
71
|
+
pattern,
|
72
|
+
regex,
|
73
|
+
use_regex,
|
74
|
+
max_depth,
|
75
|
+
max_results,
|
76
|
+
total_results,
|
77
|
+
count_only,
|
78
|
+
):
|
79
|
+
info_str = tr(
|
80
|
+
"\U0001f50d Search {search_type} '{pattern}' in '{disp_path}'",
|
81
|
+
search_type=("regex" if use_regex else "text"),
|
82
|
+
pattern=pattern,
|
83
|
+
disp_path=display_path(search_path),
|
84
|
+
)
|
85
|
+
if max_depth > 0:
|
86
|
+
info_str += tr(" [max_depth={max_depth}]", max_depth=max_depth)
|
87
|
+
if count_only:
|
88
|
+
info_str += " [count]"
|
89
|
+
self.report_info(ActionType.READ, info_str)
|
90
|
+
if os.path.isfile(search_path):
|
91
|
+
dir_output, dir_limit_reached, per_file_counts = self._handle_file(
|
92
|
+
search_path,
|
93
|
+
pattern,
|
94
|
+
regex,
|
95
|
+
use_regex,
|
96
|
+
max_results,
|
97
|
+
total_results,
|
98
|
+
count_only,
|
99
|
+
)
|
100
|
+
else:
|
101
|
+
if count_only:
|
102
|
+
per_file_counts, dir_limit_reached, _ = traverse_directory(
|
103
|
+
search_path,
|
104
|
+
pattern,
|
105
|
+
regex,
|
106
|
+
use_regex,
|
107
|
+
max_depth,
|
108
|
+
max_results,
|
109
|
+
total_results,
|
110
|
+
True,
|
111
|
+
)
|
112
|
+
dir_output = []
|
113
|
+
else:
|
114
|
+
dir_output, dir_limit_reached, per_file_counts = traverse_directory(
|
115
|
+
search_path,
|
116
|
+
pattern,
|
117
|
+
regex,
|
118
|
+
use_regex,
|
119
|
+
max_depth,
|
120
|
+
max_results,
|
121
|
+
total_results,
|
122
|
+
False,
|
123
|
+
)
|
124
|
+
count = sum(count for _, count in per_file_counts)
|
125
|
+
file_word = pluralize("match", count)
|
126
|
+
self.report_success(
|
127
|
+
tr(" \u2705 {count} {file_word}", count=count, file_word=file_word)
|
128
|
+
)
|
129
|
+
return info_str, dir_output, dir_limit_reached, per_file_counts
|
130
|
+
|
131
|
+
def run(
|
132
|
+
self,
|
133
|
+
paths: str,
|
134
|
+
pattern: str,
|
135
|
+
is_regex: bool = False,
|
136
|
+
max_depth: int = 0,
|
137
|
+
max_results: int = 100,
|
138
|
+
count_only: bool = False,
|
139
|
+
) -> str:
|
140
|
+
regex, use_regex, error_msg = prepare_pattern(
|
141
|
+
pattern, is_regex, self.report_error, self.report_warning
|
142
|
+
)
|
143
|
+
if error_msg:
|
144
|
+
return error_msg
|
145
|
+
paths_list = paths.split()
|
146
|
+
results = []
|
147
|
+
all_per_file_counts = []
|
148
|
+
for search_path in paths_list:
|
149
|
+
info_str, dir_output, dir_limit_reached, per_file_counts = (
|
150
|
+
self._handle_path(
|
151
|
+
search_path,
|
152
|
+
pattern,
|
153
|
+
regex,
|
154
|
+
use_regex,
|
155
|
+
max_depth,
|
156
|
+
max_results,
|
157
|
+
0,
|
158
|
+
count_only,
|
159
|
+
)
|
160
|
+
)
|
161
|
+
if count_only:
|
162
|
+
all_per_file_counts.extend(per_file_counts)
|
163
|
+
result_str = format_result(
|
164
|
+
pattern,
|
165
|
+
use_regex,
|
166
|
+
dir_output,
|
167
|
+
dir_limit_reached,
|
168
|
+
count_only,
|
169
|
+
per_file_counts,
|
170
|
+
)
|
171
|
+
results.append(info_str + "\n" + result_str)
|
172
|
+
if dir_limit_reached:
|
173
|
+
break
|
174
|
+
if count_only:
|
175
|
+
results.append(summarize_total(all_per_file_counts))
|
176
|
+
return "\n\n".join(results)
|
@@ -0,0 +1,58 @@
|
|
1
|
+
import re
|
2
|
+
from janito.agent.tools_utils.gitignore_utils import GitignoreFilter
|
3
|
+
import os
|
4
|
+
|
5
|
+
|
6
|
+
def is_binary_file(path, blocksize=1024):
|
7
|
+
try:
|
8
|
+
with open(path, "rb") as f:
|
9
|
+
chunk = f.read(blocksize)
|
10
|
+
if b"\0" in chunk:
|
11
|
+
return True
|
12
|
+
text_characters = bytearray(
|
13
|
+
{7, 8, 9, 10, 12, 13, 27} | set(range(0x20, 0x100))
|
14
|
+
)
|
15
|
+
nontext = chunk.translate(None, text_characters)
|
16
|
+
if len(nontext) / max(1, len(chunk)) > 0.3:
|
17
|
+
return True
|
18
|
+
except Exception:
|
19
|
+
return True
|
20
|
+
return False
|
21
|
+
|
22
|
+
|
23
|
+
def match_line(line, pattern, regex, use_regex):
|
24
|
+
if use_regex:
|
25
|
+
return regex and regex.search(line)
|
26
|
+
return pattern in line
|
27
|
+
|
28
|
+
|
29
|
+
def should_limit(max_results, total_results, match_count, count_only, dir_output):
|
30
|
+
if max_results > 0:
|
31
|
+
current_count = total_results + (match_count if count_only else len(dir_output))
|
32
|
+
return current_count >= max_results
|
33
|
+
return False
|
34
|
+
|
35
|
+
|
36
|
+
def read_file_lines(
|
37
|
+
path, pattern, regex, use_regex, count_only, max_results, total_results
|
38
|
+
):
|
39
|
+
dir_output = []
|
40
|
+
dir_limit_reached = False
|
41
|
+
match_count = 0
|
42
|
+
if not is_binary_file(path):
|
43
|
+
try:
|
44
|
+
open_kwargs = {"mode": "r", "encoding": "utf-8"}
|
45
|
+
with open(path, **open_kwargs) as f:
|
46
|
+
for lineno, line in enumerate(f, 1):
|
47
|
+
if match_line(line, pattern, regex, use_regex):
|
48
|
+
match_count += 1
|
49
|
+
if not count_only:
|
50
|
+
dir_output.append(f"{path}:{lineno}: {line.rstrip()}")
|
51
|
+
if should_limit(
|
52
|
+
max_results, total_results, match_count, count_only, dir_output
|
53
|
+
):
|
54
|
+
dir_limit_reached = True
|
55
|
+
break
|
56
|
+
except Exception:
|
57
|
+
pass
|
58
|
+
return match_count, dir_limit_reached, dir_output
|