@antmanler/claude-code-acp 0.12.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +222 -0
- package/README.md +53 -0
- package/dist/acp-agent.js +908 -0
- package/dist/index.js +20 -0
- package/dist/lib.js +6 -0
- package/dist/mcp-server.js +731 -0
- package/dist/settings.js +422 -0
- package/dist/tests/acp-agent.test.js +753 -0
- package/dist/tests/extract-lines.test.js +79 -0
- package/dist/tests/replace-and-calculate-location.test.js +266 -0
- package/dist/tests/settings.test.js +462 -0
- package/dist/tools.js +555 -0
- package/dist/utils.js +150 -0
- package/package.json +73 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { extractLinesWithByteLimit } from "../utils.js";
|
|
3
|
+
describe("extractLinesWithByteLimit", () => {
|
|
4
|
+
const simpleContent = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5";
|
|
5
|
+
it("should extract all lines when under byte limit", () => {
|
|
6
|
+
const result = extractLinesWithByteLimit(simpleContent, 1000);
|
|
7
|
+
expect(result.content).toBe(simpleContent);
|
|
8
|
+
expect(result.wasLimited).toBe(false);
|
|
9
|
+
expect(result.linesRead).toBe(5);
|
|
10
|
+
});
|
|
11
|
+
it("should limit output when exceeding byte limit", () => {
|
|
12
|
+
// Create content that will exceed byte limit
|
|
13
|
+
const longLine = "x".repeat(100);
|
|
14
|
+
const manyLines = Array(10).fill(longLine).join("\n");
|
|
15
|
+
const result = extractLinesWithByteLimit(manyLines, 250);
|
|
16
|
+
expect(result.wasLimited).toBe(true);
|
|
17
|
+
expect(result.linesRead).toBe(2);
|
|
18
|
+
});
|
|
19
|
+
it("should handle empty content", () => {
|
|
20
|
+
const result = extractLinesWithByteLimit("", 1000);
|
|
21
|
+
expect(result.content).toBe("");
|
|
22
|
+
expect(result.wasLimited).toBe(false);
|
|
23
|
+
expect(result.linesRead).toBe(1); // We read the one empty line
|
|
24
|
+
});
|
|
25
|
+
it("should handle single line file", () => {
|
|
26
|
+
const singleLine = "This is a single line without newline";
|
|
27
|
+
const result = extractLinesWithByteLimit(singleLine, 1000);
|
|
28
|
+
expect(result.content).toBe(singleLine);
|
|
29
|
+
expect(result.wasLimited).toBe(false);
|
|
30
|
+
expect(result.linesRead).toBe(1);
|
|
31
|
+
});
|
|
32
|
+
it("should correctly count bytes with multi-byte characters", () => {
|
|
33
|
+
const unicodeContent = "Hello 世界\n你好 World\nEmoji: 🌍\nNormal line";
|
|
34
|
+
const result = extractLinesWithByteLimit(unicodeContent, 1000);
|
|
35
|
+
expect(result.content).toBe(unicodeContent);
|
|
36
|
+
expect(result.linesRead).toBe(4);
|
|
37
|
+
});
|
|
38
|
+
it("should stop at byte limit even with one more line available", () => {
|
|
39
|
+
// Create lines where adding one more would exceed limit
|
|
40
|
+
const line1 = "a".repeat(40);
|
|
41
|
+
const line2 = "b".repeat(40);
|
|
42
|
+
const line3 = "c".repeat(40);
|
|
43
|
+
const content = `${line1}\n${line2}\n${line3}`;
|
|
44
|
+
const result = extractLinesWithByteLimit(content, 85);
|
|
45
|
+
expect(result.content).toBe(`${line1}\n${line2}\n`);
|
|
46
|
+
expect(result.wasLimited).toBe(true);
|
|
47
|
+
expect(result.linesRead).toBe(2);
|
|
48
|
+
});
|
|
49
|
+
it("should read exactly to limit when possible", () => {
|
|
50
|
+
const exactContent = "12345\n67890\n12345"; // 17 bytes total
|
|
51
|
+
const result = extractLinesWithByteLimit(exactContent, 17);
|
|
52
|
+
expect(result.content).toBe(exactContent);
|
|
53
|
+
expect(result.wasLimited).toBe(false);
|
|
54
|
+
expect(result.linesRead).toBe(3);
|
|
55
|
+
});
|
|
56
|
+
it("should handle Windows-style line endings", () => {
|
|
57
|
+
const windowsContent = "Line 1\r\nLine 2\r\nLine 3";
|
|
58
|
+
const result = extractLinesWithByteLimit(windowsContent, 1000);
|
|
59
|
+
// Note: split("\n") will keep the \r characters
|
|
60
|
+
expect(result.content).toBe("Line 1\r\nLine 2\r\nLine 3");
|
|
61
|
+
expect(result.linesRead).toBe(3);
|
|
62
|
+
});
|
|
63
|
+
it("should handle very large files efficiently", () => {
|
|
64
|
+
// Create a 100KB file
|
|
65
|
+
const largeLine = "x".repeat(1000);
|
|
66
|
+
const largeContent = Array(110).fill(largeLine).join("\n");
|
|
67
|
+
const result = extractLinesWithByteLimit(largeContent, 50000);
|
|
68
|
+
expect(result.wasLimited).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
it("should allow at least one line even if it exceeds byte limit", () => {
|
|
71
|
+
const veryLongLine = "x".repeat(100000); // 100KB line
|
|
72
|
+
const result = extractLinesWithByteLimit(veryLongLine, 50000);
|
|
73
|
+
// Should return the line even though it exceeds the byte limit
|
|
74
|
+
// because we always allow at least one line if no lines have been added yet
|
|
75
|
+
expect(result.content).toBe(veryLongLine);
|
|
76
|
+
expect(result.linesRead).toBe(1);
|
|
77
|
+
expect(result.wasLimited).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { replaceAndCalculateLocation } from "../mcp-server.js";
|
|
3
|
+
describe("replaceAndCalculateLocation", () => {
|
|
4
|
+
it("should replace first occurrence and return correct line number", () => {
|
|
5
|
+
const content = "line 1\nline 2 with text\nline 3 with text\nline 4";
|
|
6
|
+
const result = replaceAndCalculateLocation(content, [{ oldText: "text", newText: "replaced" }]);
|
|
7
|
+
expect(result.newContent).toBe("line 1\nline 2 with replaced\nline 3 with text\nline 4");
|
|
8
|
+
expect(result.lineNumbers).toEqual([1]); // Line 2 (0-based indexing)
|
|
9
|
+
});
|
|
10
|
+
it("should replace all occurrences when replaceAll is true", () => {
|
|
11
|
+
const content = "line 1\nline 2 with text\nline 3 with text\nline 4";
|
|
12
|
+
const result = replaceAndCalculateLocation(content, [
|
|
13
|
+
{ oldText: "text", newText: "replaced", replaceAll: true },
|
|
14
|
+
]);
|
|
15
|
+
expect(result.newContent).toBe("line 1\nline 2 with replaced\nline 3 with replaced\nline 4");
|
|
16
|
+
expect(result.lineNumbers).toEqual([1, 2]); // Lines 2 and 3 (0-based)
|
|
17
|
+
});
|
|
18
|
+
it("should return empty line numbers array when no match found", () => {
|
|
19
|
+
const content = "line 1\nline 2\nline 3";
|
|
20
|
+
expect(() => {
|
|
21
|
+
replaceAndCalculateLocation(content, [{ oldText: "notfound", newText: "replaced" }]);
|
|
22
|
+
}).toThrow('The provided `old_string` does not appear in the file: "notfound"');
|
|
23
|
+
});
|
|
24
|
+
it("should handle Windows line endings (CRLF)", () => {
|
|
25
|
+
const content = "line 1\r\nline 2 with text\r\nline 3 with text\r\nline 4";
|
|
26
|
+
const result = replaceAndCalculateLocation(content, [
|
|
27
|
+
{ oldText: "text", newText: "replaced", replaceAll: true },
|
|
28
|
+
]);
|
|
29
|
+
expect(result.newContent).toBe("line 1\r\nline 2 with replaced\r\nline 3 with replaced\r\nline 4");
|
|
30
|
+
expect(result.lineNumbers).toEqual([1, 2]);
|
|
31
|
+
});
|
|
32
|
+
it("should handle old Mac line endings (CR)", () => {
|
|
33
|
+
const content = "line 1\rline 2 with text\rline 3 with text\rline 4";
|
|
34
|
+
const result = replaceAndCalculateLocation(content, [
|
|
35
|
+
{ oldText: "text", newText: "replaced", replaceAll: true },
|
|
36
|
+
]);
|
|
37
|
+
expect(result.newContent).toBe("line 1\rline 2 with replaced\rline 3 with replaced\rline 4");
|
|
38
|
+
expect(result.lineNumbers).toEqual([1, 2]);
|
|
39
|
+
});
|
|
40
|
+
it("should handle mixed line endings", () => {
|
|
41
|
+
const content = "line 1\nline 2 with text\r\nline 3 with text\rline 4";
|
|
42
|
+
const result = replaceAndCalculateLocation(content, [
|
|
43
|
+
{ oldText: "text", newText: "replaced", replaceAll: true },
|
|
44
|
+
]);
|
|
45
|
+
expect(result.newContent).toBe("line 1\nline 2 with replaced\r\nline 3 with replaced\rline 4");
|
|
46
|
+
expect(result.lineNumbers).toEqual([1, 2]);
|
|
47
|
+
});
|
|
48
|
+
it("should handle text at the beginning of file", () => {
|
|
49
|
+
const content = "text at start\nline 2\nline 3";
|
|
50
|
+
const result = replaceAndCalculateLocation(content, [{ oldText: "text", newText: "replaced" }]);
|
|
51
|
+
expect(result.newContent).toBe("replaced at start\nline 2\nline 3");
|
|
52
|
+
expect(result.lineNumbers).toEqual([0]); // First line (0-based)
|
|
53
|
+
});
|
|
54
|
+
it("should handle text at the end of file", () => {
|
|
55
|
+
const content = "line 1\nline 2\nlast line with text";
|
|
56
|
+
const result = replaceAndCalculateLocation(content, [{ oldText: "text", newText: "replaced" }]);
|
|
57
|
+
expect(result.newContent).toBe("line 1\nline 2\nlast line with replaced");
|
|
58
|
+
expect(result.lineNumbers).toEqual([2]); // Last line (0-based)
|
|
59
|
+
});
|
|
60
|
+
it("should handle multiple occurrences on the same line", () => {
|
|
61
|
+
const content = "line 1\ntext text text\nline 3";
|
|
62
|
+
const result = replaceAndCalculateLocation(content, [
|
|
63
|
+
{ oldText: "text", newText: "replaced", replaceAll: true },
|
|
64
|
+
]);
|
|
65
|
+
expect(result.newContent).toBe("line 1\nreplaced replaced replaced\nline 3");
|
|
66
|
+
expect(result.lineNumbers).toEqual([1]); // All on line 2 (0-based), deduplicated
|
|
67
|
+
});
|
|
68
|
+
it("should handle empty file content", () => {
|
|
69
|
+
expect(() => {
|
|
70
|
+
replaceAndCalculateLocation("", [{ oldText: "text", newText: "replaced" }]);
|
|
71
|
+
}).toThrowError('The provided `old_string` does not appear in the file: "text".\n\nNo edits were applied.');
|
|
72
|
+
});
|
|
73
|
+
it("should handle empty search text", () => {
|
|
74
|
+
// Test with replaceAll false (default)
|
|
75
|
+
const content1 = "line 1\nline 2\nline 3";
|
|
76
|
+
expect(() => {
|
|
77
|
+
replaceAndCalculateLocation(content1, [{ oldText: "", newText: "replaced" }]);
|
|
78
|
+
}).toThrowError("The provided `old_string` is empty.\n\nNo edits were applied.");
|
|
79
|
+
// Test with replaceAll true on single line
|
|
80
|
+
const content2 = "abc";
|
|
81
|
+
expect(() => {
|
|
82
|
+
replaceAndCalculateLocation(content2, [{ oldText: "", newText: "X", replaceAll: true }]);
|
|
83
|
+
}).toThrowError("The provided `old_string` is empty.\n\nNo edits were applied.");
|
|
84
|
+
// Test with replaceAll true on multiline content
|
|
85
|
+
const content3 = "ab\ncd";
|
|
86
|
+
expect(() => {
|
|
87
|
+
replaceAndCalculateLocation(content3, [{ oldText: "", newText: "X", replaceAll: true }]);
|
|
88
|
+
}).toThrowError("The provided `old_string` is empty.\n\nNo edits were applied.");
|
|
89
|
+
});
|
|
90
|
+
it("should handle single line content", () => {
|
|
91
|
+
const content = "single line with text here";
|
|
92
|
+
const result = replaceAndCalculateLocation(content, [{ oldText: "text", newText: "replaced" }]);
|
|
93
|
+
expect(result.newContent).toBe("single line with replaced here");
|
|
94
|
+
expect(result.lineNumbers).toEqual([0]); // Line 1 (0-based)
|
|
95
|
+
});
|
|
96
|
+
it("should handle special characters in search text", () => {
|
|
97
|
+
const content = "line 1\nline with $special.chars*\nline 3";
|
|
98
|
+
const result = replaceAndCalculateLocation(content, [
|
|
99
|
+
{ oldText: "$special.chars*", newText: "replaced" },
|
|
100
|
+
]);
|
|
101
|
+
expect(result.newContent).toBe("line 1\nline with replaced\nline 3");
|
|
102
|
+
expect(result.lineNumbers).toEqual([1]);
|
|
103
|
+
});
|
|
104
|
+
it("should handle newlines in search text", () => {
|
|
105
|
+
const content = "line 1\nline 2\nline 3";
|
|
106
|
+
const result = replaceAndCalculateLocation(content, [
|
|
107
|
+
{ oldText: "1\nline", newText: "1\nmodified line" },
|
|
108
|
+
]);
|
|
109
|
+
expect(result.newContent).toBe("line 1\nmodified line 2\nline 3");
|
|
110
|
+
expect(result.lineNumbers).toEqual([0]); // Match starts at line 1 (0-based)
|
|
111
|
+
});
|
|
112
|
+
it("should handle newlines in replacement text", () => {
|
|
113
|
+
const content = "line 1\nline 2\nline 3";
|
|
114
|
+
const result = replaceAndCalculateLocation(content, [
|
|
115
|
+
{ oldText: "line 2", newText: "line 2a\nline 2b" },
|
|
116
|
+
]);
|
|
117
|
+
expect(result.newContent).toBe("line 1\nline 2a\nline 2b\nline 3");
|
|
118
|
+
expect(result.lineNumbers).toEqual([1]);
|
|
119
|
+
});
|
|
120
|
+
it("should handle very long lines", () => {
|
|
121
|
+
const longLine = "a".repeat(10000) + "text" + "b".repeat(10000);
|
|
122
|
+
const content = `line 1\n${longLine}\nline 3`;
|
|
123
|
+
const result = replaceAndCalculateLocation(content, [{ oldText: "text", newText: "replaced" }]);
|
|
124
|
+
expect(result.newContent).toBe(`line 1\n${"a".repeat(10000)}replaced${"b".repeat(10000)}\nline 3`);
|
|
125
|
+
expect(result.lineNumbers).toEqual([1]);
|
|
126
|
+
});
|
|
127
|
+
it("should handle many lines", () => {
|
|
128
|
+
const lines = Array.from({ length: 1000 }, (_, i) => `line ${i}`);
|
|
129
|
+
lines[500] = "line with text";
|
|
130
|
+
const content = lines.join("\n");
|
|
131
|
+
const result = replaceAndCalculateLocation(content, [{ oldText: "text", newText: "replaced" }]);
|
|
132
|
+
expect(result.lineNumbers).toEqual([500]);
|
|
133
|
+
expect(result.newContent).includes("line with replaced");
|
|
134
|
+
});
|
|
135
|
+
it("should handle overlapping matches correctly", () => {
|
|
136
|
+
const content = "aaaa\nbbbb\ncccc";
|
|
137
|
+
const result = replaceAndCalculateLocation(content, [
|
|
138
|
+
{ oldText: "aa", newText: "xx", replaceAll: true },
|
|
139
|
+
]);
|
|
140
|
+
// Should replace non-overlapping occurrences
|
|
141
|
+
expect(result.newContent).toBe("xxxx\nbbbb\ncccc");
|
|
142
|
+
expect(result.lineNumbers).toEqual([0]); // Two matches on first line, deduplicated
|
|
143
|
+
});
|
|
144
|
+
it("should preserve content after replacement with different lengths", () => {
|
|
145
|
+
const content = "short\nmedium line\nlong line here";
|
|
146
|
+
// Replacing with longer text
|
|
147
|
+
const result1 = replaceAndCalculateLocation(content, [
|
|
148
|
+
{ oldText: "short", newText: "very very long text" },
|
|
149
|
+
]);
|
|
150
|
+
expect(result1.newContent).toBe("very very long text\nmedium line\nlong line here");
|
|
151
|
+
expect(result1.lineNumbers).toEqual([0]);
|
|
152
|
+
// Replacing with shorter text
|
|
153
|
+
const result2 = replaceAndCalculateLocation(content, [
|
|
154
|
+
{ oldText: "medium line", newText: "m" },
|
|
155
|
+
]);
|
|
156
|
+
expect(result2.newContent).toBe("short\nm\nlong line here");
|
|
157
|
+
expect(result2.lineNumbers).toEqual([1]);
|
|
158
|
+
});
|
|
159
|
+
it("should handle consecutive replacements", () => {
|
|
160
|
+
const content = "text1 text2 text3";
|
|
161
|
+
const result = replaceAndCalculateLocation(content, [
|
|
162
|
+
{ oldText: "text", newText: "replaced", replaceAll: true },
|
|
163
|
+
]);
|
|
164
|
+
expect(result.newContent).toBe("replaced1 replaced2 replaced3");
|
|
165
|
+
expect(result.lineNumbers).toEqual([0]); // All on first line, deduplicated
|
|
166
|
+
});
|
|
167
|
+
it("should handle case-sensitive replacements", () => {
|
|
168
|
+
const content = "Text TEXT text";
|
|
169
|
+
const result = replaceAndCalculateLocation(content, [
|
|
170
|
+
{ oldText: "text", newText: "replaced", replaceAll: true },
|
|
171
|
+
]);
|
|
172
|
+
// Should only replace exact matches
|
|
173
|
+
expect(result.newContent).toBe("Text TEXT replaced");
|
|
174
|
+
expect(result.lineNumbers).toEqual([0]); // Only lowercase "text" matched
|
|
175
|
+
});
|
|
176
|
+
it("should handle replacement at exact end of line", () => {
|
|
177
|
+
const content = "line ends with text\nnext line";
|
|
178
|
+
const result = replaceAndCalculateLocation(content, [{ oldText: "text", newText: "replaced" }]);
|
|
179
|
+
expect(result.newContent).toBe("line ends with replaced\nnext line");
|
|
180
|
+
expect(result.lineNumbers).toEqual([0]);
|
|
181
|
+
});
|
|
182
|
+
it("should handle replacement spanning multiple lines", () => {
|
|
183
|
+
const content = "line 1\nspan\nacross\nlines\nline 5";
|
|
184
|
+
const result = replaceAndCalculateLocation(content, [
|
|
185
|
+
{ oldText: "span\nacross\nlines", newText: "single" },
|
|
186
|
+
]);
|
|
187
|
+
expect(result.newContent).toBe("line 1\nsingle\nline 5");
|
|
188
|
+
expect(result.lineNumbers).toEqual([1]); // Match starts at line 2 (0-based)
|
|
189
|
+
});
|
|
190
|
+
it("should default replaceAll to false", () => {
|
|
191
|
+
const content = "match match match";
|
|
192
|
+
// Without passing replaceAll parameter
|
|
193
|
+
const result = replaceAndCalculateLocation(content, [
|
|
194
|
+
{ oldText: "match", newText: "replaced" },
|
|
195
|
+
]);
|
|
196
|
+
expect(result.newContent).toBe("replaced match match");
|
|
197
|
+
expect(result.lineNumbers).toEqual([0]); // Only first match
|
|
198
|
+
});
|
|
199
|
+
it("should handle multiple edits with correct line numbers", () => {
|
|
200
|
+
const content = "line 1\nfoo bar\nline 3\nfoo baz\nline 5\nbar foo\nline 7";
|
|
201
|
+
const result = replaceAndCalculateLocation(content, [
|
|
202
|
+
{ oldText: "foo", newText: "FOO", replaceAll: true },
|
|
203
|
+
{ oldText: "bar", newText: "BAR", replaceAll: false },
|
|
204
|
+
{ oldText: "line 3", newText: "LINE THREE" },
|
|
205
|
+
]);
|
|
206
|
+
// Should replace:
|
|
207
|
+
// - All "foo" occurrences (lines 1, 3, 5 in original)
|
|
208
|
+
// - First "bar" occurrence (line 1 after first edit)
|
|
209
|
+
// - "line 3" (line 2 after previous edits)
|
|
210
|
+
expect(result.newContent).toBe("line 1\nFOO BAR\nLINE THREE\nFOO baz\nline 5\nbar FOO\nline 7");
|
|
211
|
+
// Line numbers reflect position in final content, deduplicated and sorted
|
|
212
|
+
expect(result.lineNumbers).toEqual([
|
|
213
|
+
1, // FOO and BAR on line 1
|
|
214
|
+
2, // LINE THREE on line 2
|
|
215
|
+
3, // FOO on line 3
|
|
216
|
+
5, // FOO on line 5
|
|
217
|
+
]);
|
|
218
|
+
});
|
|
219
|
+
it("should handle multiple edits with overlapping text", () => {
|
|
220
|
+
const content = "hello world\nhello hello\nworld hello world";
|
|
221
|
+
const result = replaceAndCalculateLocation(content, [
|
|
222
|
+
{ oldText: "hello", newText: "hi", replaceAll: false },
|
|
223
|
+
{ oldText: "world", newText: "earth", replaceAll: true },
|
|
224
|
+
{ oldText: "hello", newText: "greetings", replaceAll: true },
|
|
225
|
+
]);
|
|
226
|
+
// First edit replaces first "hello" only
|
|
227
|
+
// Second edit replaces all "world" in the modified content
|
|
228
|
+
// Third edit replaces remaining "hello"s in the modified content
|
|
229
|
+
expect(result.newContent).toBe("hi earth\ngreetings greetings\nearth greetings earth");
|
|
230
|
+
// Line numbers reflect position in final content, deduplicated and sorted
|
|
231
|
+
expect(result.lineNumbers).toEqual([
|
|
232
|
+
0, // hi and earth on line 0
|
|
233
|
+
1, // greetings (multiple) on line 1
|
|
234
|
+
2, // earth and greetings on line 2
|
|
235
|
+
]);
|
|
236
|
+
});
|
|
237
|
+
it("should handle complex scenarios with marker-like text", () => {
|
|
238
|
+
// Test that our marker approach doesn't get confused by similar text
|
|
239
|
+
const content = "line 1\n__MARKER__text\nline 3\n__REPLACE_MARKER_abc__\nline 5";
|
|
240
|
+
const result = replaceAndCalculateLocation(content, [
|
|
241
|
+
{ oldText: "__MARKER__", newText: "REPLACED", replaceAll: true },
|
|
242
|
+
{ oldText: "text", newText: "content" },
|
|
243
|
+
{ oldText: "__REPLACE_MARKER_abc__", newText: "DONE" },
|
|
244
|
+
]);
|
|
245
|
+
expect(result.newContent).toBe("line 1\nREPLACEDcontent\nline 3\nDONE\nline 5");
|
|
246
|
+
// Line numbers should be correct even with marker-like text
|
|
247
|
+
expect(result.lineNumbers).toEqual([
|
|
248
|
+
1, // REPLACED on line 1
|
|
249
|
+
3, // DONE on line 3
|
|
250
|
+
]);
|
|
251
|
+
});
|
|
252
|
+
it("should handle edits where newText matches subsequent oldText", () => {
|
|
253
|
+
const content = "foo\nbar\nbaz";
|
|
254
|
+
const result = replaceAndCalculateLocation(content, [
|
|
255
|
+
{ oldText: "foo", newText: "bar" },
|
|
256
|
+
{ oldText: "bar", newText: "baz", replaceAll: true },
|
|
257
|
+
{ oldText: "baz", newText: "qux", replaceAll: true },
|
|
258
|
+
]);
|
|
259
|
+
// First edit: foo -> bar
|
|
260
|
+
// Second edit: both original bar and new bar -> baz
|
|
261
|
+
// Third edit: all baz instances -> qux
|
|
262
|
+
expect(result.newContent).toBe("qux\nqux\nqux");
|
|
263
|
+
// All replacements end up on their respective lines
|
|
264
|
+
expect(result.lineNumbers).toEqual([0, 1, 2]);
|
|
265
|
+
});
|
|
266
|
+
});
|