Skip to content

Commit d6b4473

Browse files
authored
fix(tools): use native ReplacePattern for editor_replace tool (#39)
Use TextDocument.ReplacePattern instead of deleting the entire file content and reinserting it, which caused an empty-file intermediate state in the undo history. Also returns the number of replacements made for better LLM feedback.
1 parent 47b841e commit d6b4473

6 files changed

Lines changed: 22 additions & 17 deletions

File tree

src/CodingWithCalvin.MCPServer.Server/RpcClient.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ public Task ShutdownAsync()
116116
public Task<bool> SetSelectionAsync(string path, int startLine, int startColumn, int endLine, int endColumn)
117117
=> Proxy.SetSelectionAsync(path, startLine, startColumn, endLine, endColumn);
118118
public Task<bool> InsertTextAsync(string text) => Proxy.InsertTextAsync(text);
119-
public Task<bool> ReplaceTextAsync(string oldText, string newText) => Proxy.ReplaceTextAsync(oldText, newText);
119+
public Task<int> ReplaceTextAsync(string oldText, string newText) => Proxy.ReplaceTextAsync(oldText, newText);
120120
public Task<bool> GoToLineAsync(int line) => Proxy.GoToLineAsync(line);
121121
public Task<List<FindResult>> FindAsync(string searchText, bool matchCase, bool wholeWord)
122122
=> Proxy.FindAsync(searchText, matchCase, wholeWord);

src/CodingWithCalvin.MCPServer.Server/Tools/DocumentTools.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,8 @@ public async Task<string> ReplaceTextAsync(
164164
[Description("The exact text to find (case-sensitive).")] string oldText,
165165
[Description("The replacement text. Use empty string to delete matches.")] string newText)
166166
{
167-
var success = await _rpcClient.ReplaceTextAsync(oldText, newText);
168-
return success ? "Text replaced" : "Text not found or no active document";
167+
var count = await _rpcClient.ReplaceTextAsync(oldText, newText);
168+
return count > 0 ? $"Replaced {count} occurrence(s)" : "Text not found or no active document";
169169
}
170170

171171
[McpServerTool(Name = "editor_goto_line", Destructive = false, Idempotent = true)]

src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public interface IVisualStudioRpc
2626
Task<bool> SetSelectionAsync(string path, int startLine, int startColumn, int endLine, int endColumn);
2727

2828
Task<bool> InsertTextAsync(string text);
29-
Task<bool> ReplaceTextAsync(string oldText, string newText);
29+
Task<int> ReplaceTextAsync(string oldText, string newText);
3030
Task<bool> GoToLineAsync(int line);
3131
Task<List<FindResult>> FindAsync(string searchText, bool matchCase, bool wholeWord);
3232

src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public interface IVisualStudioService
2222
Task<bool> SetSelectionAsync(string path, int startLine, int startColumn, int endLine, int endColumn);
2323

2424
Task<bool> InsertTextAsync(string text);
25-
Task<bool> ReplaceTextAsync(string oldText, string newText);
25+
Task<int> ReplaceTextAsync(string oldText, string newText);
2626
Task<bool> GoToLineAsync(int line);
2727
Task<List<FindResult>> FindAsync(string searchText, bool matchCase = false, bool wholeWord = false);
2828

src/CodingWithCalvin.MCPServer/Services/RpcServer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ public async Task RequestShutdownAsync()
176176
public Task<bool> SetSelectionAsync(string path, int startLine, int startColumn, int endLine, int endColumn)
177177
=> _vsService.SetSelectionAsync(path, startLine, startColumn, endLine, endColumn);
178178
public Task<bool> InsertTextAsync(string text) => _vsService.InsertTextAsync(text);
179-
public Task<bool> ReplaceTextAsync(string oldText, string newText) => _vsService.ReplaceTextAsync(oldText, newText);
179+
public Task<int> ReplaceTextAsync(string oldText, string newText) => _vsService.ReplaceTextAsync(oldText, newText);
180180
public Task<bool> GoToLineAsync(int line) => _vsService.GoToLineAsync(line);
181181
public Task<List<FindResult>> FindAsync(string searchText, bool matchCase, bool wholeWord)
182182
=> _vsService.FindAsync(searchText, matchCase, wholeWord);

src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -371,35 +371,40 @@ public async Task<bool> InsertTextAsync(string text)
371371
return true;
372372
}
373373

374-
public async Task<bool> ReplaceTextAsync(string oldText, string newText)
374+
public async Task<int> ReplaceTextAsync(string oldText, string newText)
375375
{
376376
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
377377
var dte = await GetDteAsync();
378378

379379
var doc = dte.ActiveDocument;
380380
if (doc == null)
381381
{
382-
return false;
382+
return 0;
383383
}
384384

385385
var textDoc = doc.Object("TextDocument") as TextDocument;
386386
if (textDoc == null)
387387
{
388-
return false;
388+
return 0;
389389
}
390390

391-
var editPoint = textDoc.StartPoint.CreateEditPoint();
392-
var content = editPoint.GetText(textDoc.EndPoint);
393-
var newContent = content.Replace(oldText, newText);
391+
var count = 0;
392+
var searchPoint = textDoc.StartPoint.CreateEditPoint();
393+
EditPoint matchEnd = null;
394394

395-
if (content != newContent)
395+
while (searchPoint.FindPattern(oldText, (int)vsFindOptions.vsFindOptionsMatchCase, ref matchEnd))
396396
{
397-
editPoint.Delete(textDoc.EndPoint);
398-
editPoint.Insert(newContent);
399-
return true;
397+
count++;
398+
searchPoint = matchEnd;
400399
}
401400

402-
return false;
401+
if (count > 0)
402+
{
403+
TextRanges tags = null;
404+
textDoc.ReplacePattern(oldText, newText, (int)vsFindOptions.vsFindOptionsMatchCase, ref tags);
405+
}
406+
407+
return count;
403408
}
404409

405410
public async Task<bool> GoToLineAsync(int line)

0 commit comments

Comments
 (0)