Skip to content

Commit 5de2030

Browse files
committed
Инструменты: актуализация схем, хинты, исправления
Схемы: - EditFileTool: operations[]/edits[] — полные sub-schemas, operation в top-level - FileReadTool: force в bulk items - GitCombinedTool: init в описании command - BatchToolsTool: убраны несуществующие affectedFiles[0] ссылки - InitTool: sub-tasks документация, параметр description для merge Исправления: - EditFileTool: autoIndent пробрасывается в operations[] через topLevelAutoIndent - BatchToolsTool: $LAST/$PREV_END работают после nts_file_manage(create) - ContextTool: фантомный nts_worker_finish заменён на nts_todo(action='close')
1 parent f93cbf2 commit 5de2030

13 files changed

Lines changed: 434 additions & 139 deletions

File tree

app/src/main/java/ru/nts/tools/mcp/tools/editing/EditFileTool.java

Lines changed: 98 additions & 24 deletions
Large diffs are not rendered by default.

app/src/main/java/ru/nts/tools/mcp/tools/editing/ProjectReplaceTool.java

Lines changed: 46 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ public JsonNode execute(JsonNode params) throws Exception {
149149

150150
Pattern pattern = isRegex ? Pattern.compile(query, Pattern.MULTILINE | Pattern.DOTALL) : null;
151151

152-
List<ReplaceTask> tasks = new ArrayList<>();
152+
List<ReplaceTask> tasks = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>();
153153
int totalOccurrences = 0;
154154

155155
Charset forcedCharset = null;
@@ -159,56 +159,52 @@ public JsonNode execute(JsonNode params) throws Exception {
159159
} catch (Exception ignored) {}
160160
}
161161

162-
// 1. Предварительное сканирование
162+
// 1. Предварительное сканирование — collect candidates then parallel scan
163+
List<Path> candidates;
164+
final PathMatcher incMatcher = includeMatcher;
165+
final PathMatcher excMatcher = excludeMatcher;
163166
try (Stream<Path> walk = Files.walk(root)) {
164-
Iterable<Path> iterable = walk.filter(p -> Files.isRegularFile(p) && !PathSanitizer.isProtected(p))::iterator;
165-
for (Path p : iterable) {
166-
try {
167-
Path relPath = root.relativize(p);
168-
// Нормализация пути для матчера (замена \ на /)
169-
Path normalizedRelPath = Path.of(relPath.toString().replace('\\', '/'));
170-
171-
if (includeMatcher != null && !includeMatcher.matches(normalizedRelPath)) {
172-
continue;
173-
}
174-
if (excludeMatcher != null && excludeMatcher.matches(normalizedRelPath)) {
175-
continue;
176-
}
167+
candidates = walk
168+
.filter(p -> Files.isRegularFile(p) && !PathSanitizer.isProtected(p))
169+
.filter(p -> {
170+
Path rel = Path.of(root.relativize(p).toString().replace('\\', '/'));
171+
if (incMatcher != null && !incMatcher.matches(rel)) return false;
172+
if (excMatcher != null && excMatcher.matches(rel)) return false;
173+
return true;
174+
})
175+
.toList();
176+
}
177177

178-
// Защита от огромных файлов
179-
PathSanitizer.checkFileSize(p);
180-
181-
// Эффективное чтение с детекцией кодировки и проверкой на бинарность
182-
EncodingUtils.TextFileContent fileData = (forcedCharset != null)
183-
? EncodingUtils.readTextFile(p, forcedCharset)
184-
: EncodingUtils.readTextFile(p);
185-
186-
String content = fileData.content();
187-
int count = 0;
188-
189-
if (isRegex) {
190-
Matcher m = pattern.matcher(content);
191-
while (m.find()) {
192-
count++;
178+
// Parallel scan: file reads + pattern matching across all CPU cores
179+
final Charset fc = forcedCharset;
180+
final boolean regex = isRegex;
181+
var found = candidates.parallelStream()
182+
.map(p -> {
183+
try {
184+
PathSanitizer.checkFileSize(p);
185+
EncodingUtils.TextFileContent fileData = (fc != null)
186+
? EncodingUtils.readTextFile(p, fc) : EncodingUtils.readTextFile(p);
187+
String content = fileData.content();
188+
int count = 0;
189+
if (regex) {
190+
Matcher m = pattern.matcher(content);
191+
while (m.find()) count++;
192+
} else {
193+
int idx = content.indexOf(query);
194+
while (idx >= 0) { count++; idx = content.indexOf(query, idx + query.length()); }
193195
}
194-
} else {
195-
int idx = content.indexOf(query);
196-
while (idx >= 0) {
197-
count++;
198-
idx = content.indexOf(query, idx + query.length());
196+
if (count > 0) {
197+
Charset outputCharset = (fc != null) ? fc : fileData.charset();
198+
return new ReplaceTask(p, content, outputCharset, count);
199199
}
200-
}
200+
} catch (Exception ignored) {}
201+
return null;
202+
})
203+
.filter(java.util.Objects::nonNull)
204+
.toList();
201205

202-
if (count > 0) {
203-
Charset outputCharset = (forcedCharset != null) ? forcedCharset : fileData.charset();
204-
tasks.add(new ReplaceTask(p, content, outputCharset, count));
205-
totalOccurrences += count;
206-
}
207-
} catch (Exception ignored) {
208-
// Игнорируем бинарные файлы, ошибки доступа или слишком большие файлы в процессе массового сканирования
209-
}
210-
}
211-
}
206+
tasks.addAll(found);
207+
for (ReplaceTask t : tasks) totalOccurrences += t.count();
212208

213209
if (tasks.isEmpty()) {
214210
return createResponse("No matches found. No files modified.");
@@ -227,7 +223,7 @@ public JsonNode execute(JsonNode params) throws Exception {
227223
}
228224

229225
// 4. Выполнение замены в транзакции
230-
List<AffectedFile> affectedFiles = new ArrayList<>();
226+
List<AffectedFile> affectedFiles = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>();
231227

232228
TransactionManager.startTransaction("Project Replace: '" + query + "' -> '" + replacement + "'", instruction);
233229
try {
@@ -245,7 +241,7 @@ public JsonNode execute(JsonNode params) throws Exception {
245241
// (до записи, для консистентности)
246242
byte[] contentBytes = newContent.getBytes(task.charset);
247243
long crc = calculateCRC32FromBytes(contentBytes);
248-
int lineCount = newContent.split("\n", -1).length;
244+
int lineCount = newContent.split("\r?\n", -1).length;
249245

250246
FileUtils.safeWrite(task.path, newContent, task.charset);
251247

@@ -307,7 +303,7 @@ private JsonNode generateDryRunDiff(List<ReplaceTask> tasks, String query, Strin
307303
}
308304

309305
Path relPath = root.relativize(task.path);
310-
String[] originalLines = task.originalContent.split("\n", -1);
306+
String[] originalLines = task.originalContent.split("\r?\n", -1);
311307

312308
// Применяем замену для генерации diff
313309
String newContent;
@@ -316,7 +312,7 @@ private JsonNode generateDryRunDiff(List<ReplaceTask> tasks, String query, Strin
316312
} else {
317313
newContent = task.originalContent.replace(query, replacement);
318314
}
319-
String[] newLines = newContent.split("\n", -1);
315+
String[] newLines = newContent.split("\r?\n", -1);
320316

321317
sb.append("--- a/").append(relPath).append("\n");
322318
sb.append("+++ b/").append(relPath).append("\n");

app/src/main/java/ru/nts/tools/mcp/tools/external/GitCombinedTool.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public JsonNode getInputSchema() {
7979
"Operation: 'cmd' (run git command), 'diff' (show changes), 'commit_task' (auto-commit). Required.");
8080

8181
props.putObject("command").put("type", "string").put("description",
82-
"For 'cmd': Git subcommand. Allowed: status, diff, log, add, commit, rev-parse, branch. " +
82+
"For 'cmd': Git subcommand. Allowed: status, diff, log, add, commit, rev-parse, branch, init. " +
8383
"Example: command='status' or command='log' args='--oneline -5'");
8484

8585
props.putObject("args").put("type", "string").put("description",
@@ -123,7 +123,8 @@ public JsonNode execute(JsonNode params) throws Exception {
123123
private void checkGitRepository() {
124124
Path gitDir = PathSanitizer.getRoot().resolve(".git");
125125
if (!Files.exists(gitDir)) {
126-
throw new NtsException(NtsErrorCode.DIRECTORY_NOT_FOUND, "path", ".git");
126+
throw new NtsException(NtsErrorCode.NOT_A_GIT_REPOSITORY,
127+
"path", PathSanitizer.getRoot().toString());
127128
}
128129
}
129130

@@ -142,7 +143,7 @@ private JsonNode executeCmd(JsonNode params) throws Exception {
142143
checkGitRepository();
143144
}
144145

145-
List<String> command = new ArrayList<>();
146+
List<String> command = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>();
146147
command.add("git");
147148
command.add(subCmd);
148149

app/src/main/java/ru/nts/tools/mcp/tools/external/GradleTool.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ private String findSystemGradle(boolean isWindows) {
334334
}
335335

336336
private JsonNode executeGradleTask(String gradleExe, String task, String extraArgs, long timeout) throws Exception {
337-
List<String> command = new ArrayList<>();
337+
List<String> command = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>();
338338
command.add(gradleExe);
339339
command.add(task);
340340

@@ -505,7 +505,7 @@ private String extractTestName(String testInfo) {
505505

506506
private List<String> buildInitCommand(String gradleExe, String initType, String initDsl, String extraArgs,
507507
GradleVersion gradleVersion) {
508-
List<String> command = new ArrayList<>();
508+
List<String> command = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>();
509509
command.add(gradleExe);
510510
command.add("init");
511511

@@ -526,7 +526,7 @@ private List<String> buildInitCommand(String gradleExe, String initType, String
526526
}
527527

528528
private List<String> splitArgs(String rawArgs) {
529-
List<String> args = new ArrayList<>();
529+
List<String> args = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>();
530530
if (rawArgs == null || rawArgs.isBlank()) {
531531
return args;
532532
}

app/src/main/java/ru/nts/tools/mcp/tools/external/VerifyTool.java

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -133,12 +133,18 @@ private JsonNode executeSyntax(JsonNode params) {
133133
"ok", null, null, false, false, List.of(), CoverageWarning.none());
134134
}
135135

136-
List<FileCheckResult> results = new ArrayList<>();
137-
for (String affected : affectedPaths) {
138-
Path path = Path.of(affected);
139-
SyntaxCheckResult result = SyntaxChecker.check(path);
140-
results.add(new FileCheckResult(path, result));
141-
}
136+
// Parallel syntax check — each file is independent, SyntaxChecker is stateless
137+
List<FileCheckResult> results = affectedPaths.parallelStream()
138+
.map(affected -> {
139+
Path path = Path.of(affected);
140+
try {
141+
return new FileCheckResult(path, SyntaxChecker.check(path));
142+
} catch (Exception e) {
143+
return new FileCheckResult(path, new SyntaxCheckResult(List.of()));
144+
}
145+
})
146+
.collect(java.util.stream.Collectors.toCollection(
147+
it.unimi.dsi.fastutil.objects.ObjectArrayList::new));
142148

143149
return formatSyntaxResult(results);
144150
}
@@ -215,7 +221,7 @@ private JsonNode executeGradle(JsonNode params, String task, String defaultArgs)
215221
diagnostics);
216222
}
217223

218-
List<String> command = new ArrayList<>();
224+
List<String> command = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>();
219225
command.add(wrapperFile.getAbsolutePath());
220226
command.add(task);
221227
if (!defaultArgs.isBlank()) {

app/src/main/java/ru/nts/tools/mcp/tools/fs/FileReadTool.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,8 @@ public JsonNode getInputSchema() {
181181
bulkProps.putObject("contextRange").put("type", "integer").put("description", "Lines before AND after the anchor match");
182182
bulkProps.putObject("token").put("type", "string").put("description", "Pass previous token to check if unchanged");
183183
bulkProps.putObject("encoding").put("type", "string").put("description", "Force specific encoding");
184+
bulkProps.putObject("force").put("type", "boolean").put("description",
185+
"Bypass UNCHANGED optimization and always return content.");
184186
var bulkRanges = bulkProps.putObject("ranges");
185187
bulkRanges.put("type", "array").put("description", "Read multiple non-contiguous sections");
186188
var bulkRangeItem = bulkRanges.putObject("items").put("type", "object");
@@ -390,7 +392,7 @@ private JsonNode executeRead(Path path, JsonNode params) throws Exception {
390392

391393
private JsonNode executeReadRanges(Path path, JsonNode rangesNode, String[] lines, long crc32, int lineCount, String encodingLabel, boolean hasExternalChange) {
392394
StringBuilder sb = new StringBuilder();
393-
List<String> tokens = new ArrayList<>();
395+
List<String> tokens = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>();
394396

395397
if (hasExternalChange) {
396398
sb.append(EXTERNAL_CHANGE_TIP);
@@ -504,7 +506,7 @@ private JsonNode executeInfo(Path path) throws IOException {
504506
EncodingUtils.DetectionInfo detection = EncodingUtils.detectEncodingInfo(path);
505507
Charset charset = detection.charset();
506508
long crc32 = calculateCRC32(path);
507-
List<String> head = new ArrayList<>();
509+
List<String> head = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>();
508510
long lineCount = 0;
509511

510512
// Для больших файлов показываем больше строк (обычно там imports)

0 commit comments

Comments
 (0)