mirror of
https://github.com/lukaszraczylo/filepuff-mcp.git
synced 2026-06-15 03:01:17 +00:00
fixup! Add Docker usage instructions to README
This commit is contained in:
+22
-2
@@ -495,6 +495,11 @@ func (e *Engine) applyEdit(edit *ASTEdit, node *sitter.Node, content []byte) ([]
|
||||
case EditReplace:
|
||||
result = append(result, content[:startByte]...)
|
||||
result = append(result, []byte(newContent)...)
|
||||
// Preserve trailing newline: if selection ended with \n but replacement doesn't,
|
||||
// re-add it to prevent line merging
|
||||
if endByte > startByte && content[endByte-1] == '\n' && !strings.HasSuffix(newContent, "\n") {
|
||||
result = append(result, '\n')
|
||||
}
|
||||
result = append(result, content[endByte:]...)
|
||||
|
||||
case EditInsertBefore:
|
||||
@@ -508,9 +513,14 @@ func (e *Engine) applyEdit(edit *ASTEdit, node *sitter.Node, content []byte) ([]
|
||||
|
||||
case EditInsertAfter:
|
||||
insertion := newContent
|
||||
if !strings.HasPrefix(insertion, "\n") {
|
||||
// Ensure separation from preceding content
|
||||
if endByte > 0 && content[endByte-1] != '\n' && !strings.HasPrefix(insertion, "\n") {
|
||||
insertion = "\n" + insertion
|
||||
}
|
||||
// Ensure separation from following content
|
||||
if !strings.HasSuffix(insertion, "\n") && endByte < uint32(len(content)) && content[endByte] != '\n' {
|
||||
insertion += "\n"
|
||||
}
|
||||
result = append(result, content[:endByte]...)
|
||||
result = append(result, []byte(insertion)...)
|
||||
result = append(result, content[endByte:]...)
|
||||
@@ -833,6 +843,11 @@ func (e *Engine) applyTextEditOperation(op EditOperation, content []byte, start,
|
||||
case EditReplace:
|
||||
result = append(result, content[:start]...)
|
||||
result = append(result, []byte(indentedContent)...)
|
||||
// Preserve trailing newline: if selection ended with \n but replacement doesn't,
|
||||
// re-add it to prevent line merging
|
||||
if end > start && content[end-1] == '\n' && !strings.HasSuffix(indentedContent, "\n") {
|
||||
result = append(result, '\n')
|
||||
}
|
||||
result = append(result, content[end:]...)
|
||||
|
||||
case EditInsertBefore:
|
||||
@@ -846,9 +861,14 @@ func (e *Engine) applyTextEditOperation(op EditOperation, content []byte, start,
|
||||
|
||||
case EditInsertAfter:
|
||||
insertion := indentedContent
|
||||
if !strings.HasPrefix(insertion, "\n") {
|
||||
// Ensure separation from preceding content
|
||||
if end > 0 && content[end-1] != '\n' && !strings.HasPrefix(insertion, "\n") {
|
||||
insertion = "\n" + insertion
|
||||
}
|
||||
// Ensure separation from following content
|
||||
if !strings.HasSuffix(insertion, "\n") && end < len(content) && content[end] != '\n' {
|
||||
insertion += "\n"
|
||||
}
|
||||
result = append(result, content[:end]...)
|
||||
result = append(result, []byte(insertion)...)
|
||||
result = append(result, content[end:]...)
|
||||
|
||||
@@ -1402,3 +1402,311 @@ func TestFindLineRange(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Line merge regression tests ====================
|
||||
// These tests verify that edit operations don't merge adjacent lines.
|
||||
// Each test checks exact line-by-line output, not just substring presence.
|
||||
|
||||
func TestRegressionLineMergeReplace(t *testing.T) {
|
||||
registry := parser.NewRegistry()
|
||||
defer registry.Close()
|
||||
e := NewEngine(registry)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
content string
|
||||
edit *ASTEdit
|
||||
wantLines []string
|
||||
}{
|
||||
{
|
||||
name: "replace mid-line by line range preserves line boundaries",
|
||||
content: "line1\nline2\nline3\nline4\n",
|
||||
edit: &ASTEdit{
|
||||
Operation: EditReplace,
|
||||
Selector: ASTSelector{AtLine: 2},
|
||||
NewContent: "replaced",
|
||||
},
|
||||
wantLines: []string{"line1", "replaced", "line3", "line4", ""},
|
||||
},
|
||||
{
|
||||
name: "replace multi-line range preserves boundaries",
|
||||
content: "line1\nline2\nline3\nline4\n",
|
||||
edit: &ASTEdit{
|
||||
Operation: EditReplace,
|
||||
Selector: ASTSelector{AtLine: 2, LineEnd: 3},
|
||||
NewContent: "newA\nnewB",
|
||||
},
|
||||
wantLines: []string{"line1", "newA", "newB", "line4", ""},
|
||||
},
|
||||
{
|
||||
name: "replace last line without trailing newline",
|
||||
content: "line1\nline2\nline3",
|
||||
edit: &ASTEdit{
|
||||
Operation: EditReplace,
|
||||
Selector: ASTSelector{AtLine: 3},
|
||||
NewContent: "replaced",
|
||||
},
|
||||
wantLines: []string{"line1", "line2", "replaced"},
|
||||
},
|
||||
{
|
||||
name: "replace single line with multi-line content",
|
||||
content: "before\ntarget\nafter\n",
|
||||
edit: &ASTEdit{
|
||||
Operation: EditReplace,
|
||||
Selector: ASTSelector{AtLine: 2},
|
||||
NewContent: "new1\nnew2\nnew3",
|
||||
},
|
||||
wantLines: []string{"before", "new1", "new2", "new3", "after", ""},
|
||||
},
|
||||
{
|
||||
name: "replace by exact text preserves surrounding lines",
|
||||
content: "aaa\nbbb\nccc\n",
|
||||
edit: &ASTEdit{
|
||||
Operation: EditReplace,
|
||||
Selector: ASTSelector{Text: "bbb"},
|
||||
NewContent: "xxx",
|
||||
},
|
||||
wantLines: []string{"aaa", "xxx", "ccc", ""},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
tmpFile := filepath.Join(tmpDir, "test.txt")
|
||||
if err := os.WriteFile(tmpFile, []byte(tt.content), 0600); err != nil {
|
||||
t.Fatalf("write: %v", err)
|
||||
}
|
||||
tt.edit.File = tmpFile
|
||||
|
||||
result, err := e.Apply(context.Background(), tt.edit)
|
||||
if err != nil {
|
||||
t.Fatalf("apply: %v", err)
|
||||
}
|
||||
if !result.Success {
|
||||
t.Fatalf("not successful: %s", result.Error)
|
||||
}
|
||||
|
||||
got, _ := os.ReadFile(tmpFile)
|
||||
gotLines := strings.Split(string(got), "\n")
|
||||
if len(gotLines) != len(tt.wantLines) {
|
||||
t.Fatalf("line count = %d, want %d\ngot: %q\nwant: %q", len(gotLines), len(tt.wantLines), gotLines, tt.wantLines)
|
||||
}
|
||||
for i, want := range tt.wantLines {
|
||||
if gotLines[i] != want {
|
||||
t.Errorf("line %d = %q, want %q", i+1, gotLines[i], want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegressionLineMergeInsertAfter(t *testing.T) {
|
||||
registry := parser.NewRegistry()
|
||||
defer registry.Close()
|
||||
e := NewEngine(registry)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
content string
|
||||
edit *ASTEdit
|
||||
wantLines []string
|
||||
}{
|
||||
{
|
||||
name: "insert after mid-line doesn't merge with next line",
|
||||
content: "line1\nline2\nline3\n",
|
||||
edit: &ASTEdit{
|
||||
Operation: EditInsertAfter,
|
||||
Selector: ASTSelector{Text: "line2"},
|
||||
NewContent: "inserted",
|
||||
},
|
||||
wantLines: []string{"line1", "line2", "inserted", "line3", ""},
|
||||
},
|
||||
{
|
||||
name: "insert after last line without trailing newline",
|
||||
content: "line1\nline2\nline3",
|
||||
edit: &ASTEdit{
|
||||
Operation: EditInsertAfter,
|
||||
Selector: ASTSelector{Text: "line3"},
|
||||
NewContent: "inserted",
|
||||
},
|
||||
wantLines: []string{"line1", "line2", "line3", "inserted"},
|
||||
},
|
||||
{
|
||||
name: "insert multi-line block after line",
|
||||
content: "before\ntarget\nafter\n",
|
||||
edit: &ASTEdit{
|
||||
Operation: EditInsertAfter,
|
||||
Selector: ASTSelector{Text: "target"},
|
||||
NewContent: "new1\nnew2\nnew3",
|
||||
},
|
||||
wantLines: []string{"before", "target", "new1", "new2", "new3", "after", ""},
|
||||
},
|
||||
{
|
||||
name: "insert after by line number",
|
||||
content: "aaa\nbbb\nccc\n",
|
||||
edit: &ASTEdit{
|
||||
Operation: EditInsertAfter,
|
||||
Selector: ASTSelector{AtLine: 2},
|
||||
NewContent: "inserted",
|
||||
},
|
||||
wantLines: []string{"aaa", "bbb", "inserted", "ccc", ""},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
tmpFile := filepath.Join(tmpDir, "test.txt")
|
||||
if err := os.WriteFile(tmpFile, []byte(tt.content), 0600); err != nil {
|
||||
t.Fatalf("write: %v", err)
|
||||
}
|
||||
tt.edit.File = tmpFile
|
||||
|
||||
result, err := e.Apply(context.Background(), tt.edit)
|
||||
if err != nil {
|
||||
t.Fatalf("apply: %v", err)
|
||||
}
|
||||
if !result.Success {
|
||||
t.Fatalf("not successful: %s", result.Error)
|
||||
}
|
||||
|
||||
got, _ := os.ReadFile(tmpFile)
|
||||
gotLines := strings.Split(string(got), "\n")
|
||||
if len(gotLines) != len(tt.wantLines) {
|
||||
t.Fatalf("line count = %d, want %d\ngot: %q\nwant: %q", len(gotLines), len(tt.wantLines), gotLines, tt.wantLines)
|
||||
}
|
||||
for i, want := range tt.wantLines {
|
||||
if gotLines[i] != want {
|
||||
t.Errorf("line %d = %q, want %q", i+1, gotLines[i], want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegressionLineMergeInsertBefore(t *testing.T) {
|
||||
registry := parser.NewRegistry()
|
||||
defer registry.Close()
|
||||
e := NewEngine(registry)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
content string
|
||||
edit *ASTEdit
|
||||
wantLines []string
|
||||
}{
|
||||
{
|
||||
name: "insert before mid-line doesn't merge",
|
||||
content: "line1\nline2\nline3\n",
|
||||
edit: &ASTEdit{
|
||||
Operation: EditInsertBefore,
|
||||
Selector: ASTSelector{Text: "line2"},
|
||||
NewContent: "inserted",
|
||||
},
|
||||
wantLines: []string{"line1", "inserted", "line2", "line3", ""},
|
||||
},
|
||||
{
|
||||
name: "insert multi-line block before line",
|
||||
content: "before\ntarget\nafter\n",
|
||||
edit: &ASTEdit{
|
||||
Operation: EditInsertBefore,
|
||||
Selector: ASTSelector{Text: "target"},
|
||||
NewContent: "new1\nnew2",
|
||||
},
|
||||
wantLines: []string{"before", "new1", "new2", "target", "after", ""},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
tmpFile := filepath.Join(tmpDir, "test.txt")
|
||||
if err := os.WriteFile(tmpFile, []byte(tt.content), 0600); err != nil {
|
||||
t.Fatalf("write: %v", err)
|
||||
}
|
||||
tt.edit.File = tmpFile
|
||||
|
||||
result, err := e.Apply(context.Background(), tt.edit)
|
||||
if err != nil {
|
||||
t.Fatalf("apply: %v", err)
|
||||
}
|
||||
if !result.Success {
|
||||
t.Fatalf("not successful: %s", result.Error)
|
||||
}
|
||||
|
||||
got, _ := os.ReadFile(tmpFile)
|
||||
gotLines := strings.Split(string(got), "\n")
|
||||
if len(gotLines) != len(tt.wantLines) {
|
||||
t.Fatalf("line count = %d, want %d\ngot: %q\nwant: %q", len(gotLines), len(tt.wantLines), gotLines, tt.wantLines)
|
||||
}
|
||||
for i, want := range tt.wantLines {
|
||||
if gotLines[i] != want {
|
||||
t.Errorf("line %d = %q, want %q", i+1, gotLines[i], want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegressionLineMergeDelete(t *testing.T) {
|
||||
registry := parser.NewRegistry()
|
||||
defer registry.Close()
|
||||
e := NewEngine(registry)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
content string
|
||||
edit *ASTEdit
|
||||
wantLines []string
|
||||
}{
|
||||
{
|
||||
name: "delete mid-line preserves surrounding lines",
|
||||
content: "line1\nline2\nline3\n",
|
||||
edit: &ASTEdit{
|
||||
Operation: EditDelete,
|
||||
Selector: ASTSelector{AtLine: 2},
|
||||
},
|
||||
wantLines: []string{"line1", "line3", ""},
|
||||
},
|
||||
{
|
||||
name: "delete line range preserves surrounding lines",
|
||||
content: "line1\nline2\nline3\nline4\n",
|
||||
edit: &ASTEdit{
|
||||
Operation: EditDelete,
|
||||
Selector: ASTSelector{AtLine: 2, LineEnd: 3},
|
||||
},
|
||||
wantLines: []string{"line1", "line4", ""},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
tmpFile := filepath.Join(tmpDir, "test.txt")
|
||||
if err := os.WriteFile(tmpFile, []byte(tt.content), 0600); err != nil {
|
||||
t.Fatalf("write: %v", err)
|
||||
}
|
||||
tt.edit.File = tmpFile
|
||||
|
||||
result, err := e.Apply(context.Background(), tt.edit)
|
||||
if err != nil {
|
||||
t.Fatalf("apply: %v", err)
|
||||
}
|
||||
if !result.Success {
|
||||
t.Fatalf("not successful: %s", result.Error)
|
||||
}
|
||||
|
||||
got, _ := os.ReadFile(tmpFile)
|
||||
gotLines := strings.Split(string(got), "\n")
|
||||
if len(gotLines) != len(tt.wantLines) {
|
||||
t.Fatalf("line count = %d, want %d\ngot: %q\nwant: %q", len(gotLines), len(tt.wantLines), gotLines, tt.wantLines)
|
||||
}
|
||||
for i, want := range tt.wantLines {
|
||||
if gotLines[i] != want {
|
||||
t.Errorf("line %d = %q, want %q", i+1, gotLines[i], want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user