201 lines
6.7 KiB
C#
201 lines
6.7 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace AxCopilot.Services.Agent;
|
|
|
|
public class FileEditTool : IAgentTool
|
|
{
|
|
public string Name => "file_edit";
|
|
|
|
public string Description => "Edit a file by replacing an exact string match. Set replace_all=true to replace all occurrences; otherwise old_string must be unique.";
|
|
|
|
public ToolParameterSchema Parameters
|
|
{
|
|
get
|
|
{
|
|
ToolParameterSchema obj = new ToolParameterSchema
|
|
{
|
|
Properties = new Dictionary<string, ToolProperty>
|
|
{
|
|
["path"] = new ToolProperty
|
|
{
|
|
Type = "string",
|
|
Description = "File path to edit"
|
|
},
|
|
["old_string"] = new ToolProperty
|
|
{
|
|
Type = "string",
|
|
Description = "Exact string to find and replace"
|
|
},
|
|
["new_string"] = new ToolProperty
|
|
{
|
|
Type = "string",
|
|
Description = "Replacement string"
|
|
},
|
|
["replace_all"] = new ToolProperty
|
|
{
|
|
Type = "boolean",
|
|
Description = "Replace all occurrences (default false). If false, old_string must be unique."
|
|
}
|
|
}
|
|
};
|
|
int num = 3;
|
|
List<string> list = new List<string>(num);
|
|
CollectionsMarshal.SetCount(list, num);
|
|
Span<string> span = CollectionsMarshal.AsSpan(list);
|
|
span[0] = "path";
|
|
span[1] = "old_string";
|
|
span[2] = "new_string";
|
|
obj.Required = list;
|
|
return obj;
|
|
}
|
|
}
|
|
|
|
public async Task<ToolResult> ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct)
|
|
{
|
|
string path = args.GetProperty("path").GetString() ?? "";
|
|
string oldStr = args.GetProperty("old_string").GetString() ?? "";
|
|
string newStr = args.GetProperty("new_string").GetString() ?? "";
|
|
JsonElement ra;
|
|
bool replaceAll = args.TryGetProperty("replace_all", out ra) && ra.GetBoolean();
|
|
string fullPath = FileReadTool.ResolvePath(path, context.WorkFolder);
|
|
if (!context.IsPathAllowed(fullPath))
|
|
{
|
|
return ToolResult.Fail("경로 접근 차단: " + fullPath);
|
|
}
|
|
if (!File.Exists(fullPath))
|
|
{
|
|
return ToolResult.Fail("파일이 존재하지 않습니다: " + fullPath);
|
|
}
|
|
if (!(await context.CheckWritePermissionAsync(Name, fullPath)))
|
|
{
|
|
return ToolResult.Fail("쓰기 권한 거부: " + fullPath);
|
|
}
|
|
try
|
|
{
|
|
string content = await File.ReadAllTextAsync(fullPath, Encoding.UTF8, ct);
|
|
int count = CountOccurrences(content, oldStr);
|
|
if (count == 0)
|
|
{
|
|
return ToolResult.Fail("old_string을 파일에서 찾을 수 없습니다.");
|
|
}
|
|
if (!replaceAll && count > 1)
|
|
{
|
|
return ToolResult.Fail($"old_string이 {count}번 발견됩니다. replace_all=true로 전체 교체하거나, 고유한 문자열을 지정하세요.");
|
|
}
|
|
string diffPreview = GenerateDiff(content, oldStr, newStr, fullPath);
|
|
string updated = content.Replace(oldStr, newStr);
|
|
await File.WriteAllTextAsync(fullPath, updated, Encoding.UTF8, ct);
|
|
string msg = ((replaceAll && count > 1) ? $"파일 수정 완료: {fullPath} ({count}곳 전체 교체)" : ("파일 수정 완료: " + fullPath));
|
|
return ToolResult.Ok(msg + "\n\n" + diffPreview, fullPath);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return ToolResult.Fail("파일 수정 실패: " + ex.Message);
|
|
}
|
|
}
|
|
|
|
private static string GenerateDiff(string content, string oldStr, string newStr, string filePath)
|
|
{
|
|
string[] array = content.Split('\n');
|
|
int num = content.IndexOf(oldStr, StringComparison.Ordinal);
|
|
if (num < 0)
|
|
{
|
|
return "";
|
|
}
|
|
int num2 = content.Substring(0, num).Count((char c) => c == '\n');
|
|
string[] array2 = oldStr.Split('\n');
|
|
string[] array3 = newStr.Split('\n');
|
|
StringBuilder stringBuilder = new StringBuilder();
|
|
string fileName = Path.GetFileName(filePath);
|
|
StringBuilder stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder3 = stringBuilder2;
|
|
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(13, 1, stringBuilder2);
|
|
handler.AppendLiteral("--- ");
|
|
handler.AppendFormatted(fileName);
|
|
handler.AppendLiteral(" (before)");
|
|
stringBuilder3.AppendLine(ref handler);
|
|
stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder4 = stringBuilder2;
|
|
handler = new StringBuilder.AppendInterpolatedStringHandler(12, 1, stringBuilder2);
|
|
handler.AppendLiteral("+++ ");
|
|
handler.AppendFormatted(fileName);
|
|
handler.AppendLiteral(" (after)");
|
|
stringBuilder4.AppendLine(ref handler);
|
|
int num3 = Math.Max(0, num2 - 2);
|
|
int num4 = Math.Min(array.Length - 1, num2 + array2.Length - 1 + 2);
|
|
stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder5 = stringBuilder2;
|
|
handler = new StringBuilder.AppendInterpolatedStringHandler(8, 2, stringBuilder2);
|
|
handler.AppendLiteral("@@ -");
|
|
handler.AppendFormatted(num3 + 1);
|
|
handler.AppendLiteral(",");
|
|
handler.AppendFormatted(num4 - num3 + 1);
|
|
handler.AppendLiteral(" @@");
|
|
stringBuilder5.AppendLine(ref handler);
|
|
for (int num5 = num3; num5 < num2; num5++)
|
|
{
|
|
stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder6 = stringBuilder2;
|
|
handler = new StringBuilder.AppendInterpolatedStringHandler(1, 1, stringBuilder2);
|
|
handler.AppendLiteral(" ");
|
|
handler.AppendFormatted(array[num5].TrimEnd('\r'));
|
|
stringBuilder6.AppendLine(ref handler);
|
|
}
|
|
string[] array4 = array2;
|
|
foreach (string text in array4)
|
|
{
|
|
stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder7 = stringBuilder2;
|
|
handler = new StringBuilder.AppendInterpolatedStringHandler(1, 1, stringBuilder2);
|
|
handler.AppendLiteral("-");
|
|
handler.AppendFormatted(text.TrimEnd('\r'));
|
|
stringBuilder7.AppendLine(ref handler);
|
|
}
|
|
string[] array5 = array3;
|
|
foreach (string text2 in array5)
|
|
{
|
|
stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder8 = stringBuilder2;
|
|
handler = new StringBuilder.AppendInterpolatedStringHandler(1, 1, stringBuilder2);
|
|
handler.AppendLiteral("+");
|
|
handler.AppendFormatted(text2.TrimEnd('\r'));
|
|
stringBuilder8.AppendLine(ref handler);
|
|
}
|
|
int num8 = num2 + array2.Length;
|
|
for (int num9 = num8; num9 <= num4 && num9 < array.Length; num9++)
|
|
{
|
|
stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder9 = stringBuilder2;
|
|
handler = new StringBuilder.AppendInterpolatedStringHandler(1, 1, stringBuilder2);
|
|
handler.AppendLiteral(" ");
|
|
handler.AppendFormatted(array[num9].TrimEnd('\r'));
|
|
stringBuilder9.AppendLine(ref handler);
|
|
}
|
|
return stringBuilder.ToString().TrimEnd();
|
|
}
|
|
|
|
private static int CountOccurrences(string text, string search)
|
|
{
|
|
if (string.IsNullOrEmpty(search))
|
|
{
|
|
return 0;
|
|
}
|
|
int num = 0;
|
|
int startIndex = 0;
|
|
while ((startIndex = text.IndexOf(search, startIndex, StringComparison.Ordinal)) != -1)
|
|
{
|
|
num++;
|
|
startIndex += search.Length;
|
|
}
|
|
return num;
|
|
}
|
|
}
|