Add replace_all flag to search_replace tool
This commit is contained in:
6
main.go
6
main.go
@@ -417,6 +417,9 @@ func main() {
|
|||||||
mcp.WithString("after_pattern",
|
mcp.WithString("after_pattern",
|
||||||
mcp.Description("Pattern that must appear after the main pattern (for context-aware replacement)"),
|
mcp.Description("Pattern that must appear after the main pattern (for context-aware replacement)"),
|
||||||
),
|
),
|
||||||
|
mcp.WithBoolean("replace_all",
|
||||||
|
mcp.Description("Replace all occurrences (default: true). If false, only replace first occurrence in each file"),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
mcpServer.AddTool(searchReplaceTool, searchReplaceHandler)
|
mcpServer.AddTool(searchReplaceTool, searchReplaceHandler)
|
||||||
|
|
||||||
@@ -1051,8 +1054,9 @@ func searchReplaceHandler(ctx context.Context, request mcp.CallToolRequest) (*mc
|
|||||||
includeContext := request.GetBool("include_context", false)
|
includeContext := request.GetBool("include_context", false)
|
||||||
beforePattern := request.GetString("before_pattern", "")
|
beforePattern := request.GetString("before_pattern", "")
|
||||||
afterPattern := request.GetString("after_pattern", "")
|
afterPattern := request.GetString("after_pattern", "")
|
||||||
|
replaceAll := request.GetBool("replace_all", true)
|
||||||
|
|
||||||
result, err := searchReplace(paths, pattern, replacement, useRegex, caseInsensitive, includeContext, beforePattern, afterPattern)
|
result, err := searchReplace(paths, pattern, replacement, useRegex, caseInsensitive, includeContext, beforePattern, afterPattern, replaceAll)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mcp.NewToolResultError(fmt.Sprintf("search/replace failed: %v", err)), nil
|
return mcp.NewToolResultError(fmt.Sprintf("search/replace failed: %v", err)), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ type SearchMatch struct {
|
|||||||
EndByte int `json:"end_byte"`
|
EndByte int `json:"end_byte"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func searchReplace(paths []string, pattern string, replacement *string, useRegex, caseInsensitive bool, includeContext bool, beforePattern, afterPattern string) (*SearchReplaceResult, error) {
|
func searchReplace(paths []string, pattern string, replacement *string, useRegex, caseInsensitive bool, includeContext bool, beforePattern, afterPattern string, replaceAll bool) (*SearchReplaceResult, error) {
|
||||||
result := &SearchReplaceResult{
|
result := &SearchReplaceResult{
|
||||||
Files: []FileSearchReplaceResult{},
|
Files: []FileSearchReplaceResult{},
|
||||||
}
|
}
|
||||||
@@ -75,6 +75,34 @@ func searchReplace(paths []string, pattern string, replacement *string, useRegex
|
|||||||
}
|
}
|
||||||
|
|
||||||
replaceFunc = func(text string) string {
|
replaceFunc = func(text string) string {
|
||||||
|
if !replaceAll {
|
||||||
|
// Replace only first occurrence
|
||||||
|
loc := contextRe.FindStringSubmatchIndex(text)
|
||||||
|
if loc == nil {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
submatches := contextRe.FindStringSubmatch(text)
|
||||||
|
// Rebuild the match with the target replaced
|
||||||
|
result := ""
|
||||||
|
if beforePattern != "" && len(submatches) > 1 {
|
||||||
|
result += submatches[1] // before part
|
||||||
|
}
|
||||||
|
result += *replacement // replacement for target
|
||||||
|
if afterPattern != "" {
|
||||||
|
// The after part is at index 3 if before exists, otherwise at index 2
|
||||||
|
afterIndex := 2
|
||||||
|
if beforePattern != "" {
|
||||||
|
afterIndex = 3
|
||||||
|
}
|
||||||
|
if len(submatches) > afterIndex {
|
||||||
|
result += submatches[afterIndex]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return text[:loc[0]] + result + text[loc[1]:]
|
||||||
|
}
|
||||||
|
|
||||||
return contextRe.ReplaceAllStringFunc(text, func(match string) string {
|
return contextRe.ReplaceAllStringFunc(text, func(match string) string {
|
||||||
submatches := contextRe.FindStringSubmatch(match)
|
submatches := contextRe.FindStringSubmatch(match)
|
||||||
|
|
||||||
@@ -111,6 +139,14 @@ func searchReplace(paths []string, pattern string, replacement *string, useRegex
|
|||||||
}
|
}
|
||||||
if replacement != nil {
|
if replacement != nil {
|
||||||
replaceFunc = func(text string) string {
|
replaceFunc = func(text string) string {
|
||||||
|
if !replaceAll {
|
||||||
|
// Replace only first occurrence
|
||||||
|
loc := re.FindStringIndex(text)
|
||||||
|
if loc == nil {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
return text[:loc[0]] + string(re.ExpandString([]byte{}, *replacement, text, re.FindStringSubmatchIndex(text))) + text[loc[1]:]
|
||||||
|
}
|
||||||
return re.ReplaceAllString(text, *replacement)
|
return re.ReplaceAllString(text, *replacement)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,6 +175,13 @@ func searchReplace(paths []string, pattern string, replacement *string, useRegex
|
|||||||
}
|
}
|
||||||
if replacement != nil {
|
if replacement != nil {
|
||||||
replaceFunc = func(text string) string {
|
replaceFunc = func(text string) string {
|
||||||
|
if !replaceAll {
|
||||||
|
// Replace only first occurrence
|
||||||
|
if caseInsensitive {
|
||||||
|
return caseInsensitiveReplaceFirst(text, pattern, *replacement)
|
||||||
|
}
|
||||||
|
return strings.Replace(text, pattern, *replacement, 1)
|
||||||
|
}
|
||||||
if caseInsensitive {
|
if caseInsensitive {
|
||||||
// Case-insensitive string replacement
|
// Case-insensitive string replacement
|
||||||
return caseInsensitiveReplace(text, pattern, *replacement)
|
return caseInsensitiveReplace(text, pattern, *replacement)
|
||||||
@@ -170,7 +213,7 @@ func searchReplace(paths []string, pattern string, replacement *string, useRegex
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fileResult := processFile(filePath, searchFunc, replaceFunc, includeContext)
|
fileResult := processFile(filePath, searchFunc, replaceFunc, includeContext, replaceAll)
|
||||||
if len(fileResult.Matches) > 0 || fileResult.Replaced > 0 || fileResult.Error != "" {
|
if len(fileResult.Matches) > 0 || fileResult.Replaced > 0 || fileResult.Error != "" {
|
||||||
result.Files = append(result.Files, fileResult)
|
result.Files = append(result.Files, fileResult)
|
||||||
result.TotalMatches += len(fileResult.Matches)
|
result.TotalMatches += len(fileResult.Matches)
|
||||||
@@ -183,7 +226,7 @@ func searchReplace(paths []string, pattern string, replacement *string, useRegex
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Process single file
|
// Process single file
|
||||||
fileResult := processFile(path, searchFunc, replaceFunc, includeContext)
|
fileResult := processFile(path, searchFunc, replaceFunc, includeContext, replaceAll)
|
||||||
if len(fileResult.Matches) > 0 || fileResult.Replaced > 0 || fileResult.Error != "" {
|
if len(fileResult.Matches) > 0 || fileResult.Replaced > 0 || fileResult.Error != "" {
|
||||||
result.Files = append(result.Files, fileResult)
|
result.Files = append(result.Files, fileResult)
|
||||||
result.TotalMatches += len(fileResult.Matches)
|
result.TotalMatches += len(fileResult.Matches)
|
||||||
@@ -196,7 +239,7 @@ func searchReplace(paths []string, pattern string, replacement *string, useRegex
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func processFile(path string, searchFunc func(string) [][]int, replaceFunc func(string) string, includeContext bool) FileSearchReplaceResult {
|
func processFile(path string, searchFunc func(string) [][]int, replaceFunc func(string) string, includeContext bool, replaceAll bool) FileSearchReplaceResult {
|
||||||
result := FileSearchReplaceResult{
|
result := FileSearchReplaceResult{
|
||||||
Path: path,
|
Path: path,
|
||||||
}
|
}
|
||||||
@@ -213,7 +256,14 @@ func processFile(path string, searchFunc func(string) [][]int, replaceFunc func(
|
|||||||
// If replacement is requested, do it
|
// If replacement is requested, do it
|
||||||
if replaceFunc != nil {
|
if replaceFunc != nil {
|
||||||
matches := searchFunc(content)
|
matches := searchFunc(content)
|
||||||
result.Replaced = len(matches)
|
if replaceAll {
|
||||||
|
result.Replaced = len(matches)
|
||||||
|
} else {
|
||||||
|
// Only replacing first occurrence
|
||||||
|
if len(matches) > 0 {
|
||||||
|
result.Replaced = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if result.Replaced > 0 {
|
if result.Replaced > 0 {
|
||||||
newContent := replaceFunc(content)
|
newContent := replaceFunc(content)
|
||||||
@@ -323,6 +373,19 @@ func caseInsensitiveReplace(text, old, new string) string {
|
|||||||
return result.String()
|
return result.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func caseInsensitiveReplaceFirst(text, old, new string) string {
|
||||||
|
// Case-insensitive replacement for first occurrence only
|
||||||
|
lowerText := strings.ToLower(text)
|
||||||
|
lowerOld := strings.ToLower(old)
|
||||||
|
|
||||||
|
idx := strings.Index(lowerText, lowerOld)
|
||||||
|
if idx < 0 {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
return text[:idx] + new + text[idx+len(old):]
|
||||||
|
}
|
||||||
|
|
||||||
func isTextFile(path string) bool {
|
func isTextFile(path string) bool {
|
||||||
ext := strings.ToLower(filepath.Ext(path))
|
ext := strings.ToLower(filepath.Ext(path))
|
||||||
textExts := map[string]bool{
|
textExts := map[string]bool{
|
||||||
|
|||||||
Reference in New Issue
Block a user