@link-assistant/agent 0.0.8 → 0.0.11
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/EXAMPLES.md +80 -1
- package/MODELS.md +72 -24
- package/README.md +95 -2
- package/TOOLS.md +20 -0
- package/package.json +36 -2
- package/src/agent/agent.ts +68 -54
- package/src/auth/claude-oauth.ts +426 -0
- package/src/auth/index.ts +28 -26
- package/src/auth/plugins.ts +876 -0
- package/src/bun/index.ts +53 -43
- package/src/bus/global.ts +5 -5
- package/src/bus/index.ts +59 -53
- package/src/cli/bootstrap.js +12 -12
- package/src/cli/bootstrap.ts +6 -6
- package/src/cli/cmd/agent.ts +97 -92
- package/src/cli/cmd/auth.ts +468 -0
- package/src/cli/cmd/cmd.ts +2 -2
- package/src/cli/cmd/export.ts +41 -41
- package/src/cli/cmd/mcp.ts +210 -53
- package/src/cli/cmd/models.ts +30 -29
- package/src/cli/cmd/run.ts +269 -213
- package/src/cli/cmd/stats.ts +185 -146
- package/src/cli/error.ts +17 -13
- package/src/cli/ui.ts +78 -0
- package/src/command/index.ts +26 -26
- package/src/config/config.ts +528 -288
- package/src/config/markdown.ts +15 -15
- package/src/file/ripgrep.ts +201 -169
- package/src/file/time.ts +21 -18
- package/src/file/watcher.ts +51 -42
- package/src/file.ts +1 -1
- package/src/flag/flag.ts +26 -11
- package/src/format/formatter.ts +206 -162
- package/src/format/index.ts +61 -61
- package/src/global/index.ts +21 -21
- package/src/id/id.ts +47 -33
- package/src/index.js +554 -332
- package/src/json-standard/index.ts +173 -0
- package/src/mcp/index.ts +135 -128
- package/src/patch/index.ts +336 -267
- package/src/project/bootstrap.ts +15 -15
- package/src/project/instance.ts +43 -36
- package/src/project/project.ts +47 -47
- package/src/project/state.ts +37 -33
- package/src/provider/models-macro.ts +5 -5
- package/src/provider/models.ts +32 -32
- package/src/provider/opencode.js +19 -19
- package/src/provider/provider.ts +518 -277
- package/src/provider/transform.ts +143 -102
- package/src/server/project.ts +21 -21
- package/src/server/server.ts +111 -105
- package/src/session/agent.js +66 -60
- package/src/session/compaction.ts +136 -111
- package/src/session/index.ts +189 -156
- package/src/session/message-v2.ts +312 -268
- package/src/session/message.ts +73 -57
- package/src/session/processor.ts +180 -166
- package/src/session/prompt.ts +678 -533
- package/src/session/retry.ts +26 -23
- package/src/session/revert.ts +76 -62
- package/src/session/status.ts +26 -26
- package/src/session/summary.ts +97 -76
- package/src/session/system.ts +77 -63
- package/src/session/todo.ts +22 -16
- package/src/snapshot/index.ts +92 -76
- package/src/storage/storage.ts +157 -120
- package/src/tool/bash.ts +116 -106
- package/src/tool/batch.ts +73 -59
- package/src/tool/codesearch.ts +60 -53
- package/src/tool/edit.ts +319 -263
- package/src/tool/glob.ts +32 -28
- package/src/tool/grep.ts +72 -53
- package/src/tool/invalid.ts +7 -7
- package/src/tool/ls.ts +77 -64
- package/src/tool/multiedit.ts +30 -21
- package/src/tool/patch.ts +121 -94
- package/src/tool/read.ts +140 -122
- package/src/tool/registry.ts +38 -38
- package/src/tool/task.ts +93 -60
- package/src/tool/todo.ts +16 -16
- package/src/tool/tool.ts +45 -36
- package/src/tool/webfetch.ts +97 -74
- package/src/tool/websearch.ts +78 -64
- package/src/tool/write.ts +21 -15
- package/src/util/binary.ts +27 -19
- package/src/util/context.ts +8 -8
- package/src/util/defer.ts +7 -5
- package/src/util/error.ts +24 -19
- package/src/util/eventloop.ts +16 -10
- package/src/util/filesystem.ts +37 -33
- package/src/util/fn.ts +11 -8
- package/src/util/iife.ts +1 -1
- package/src/util/keybind.ts +44 -44
- package/src/util/lazy.ts +7 -7
- package/src/util/locale.ts +20 -16
- package/src/util/lock.ts +43 -38
- package/src/util/log.ts +95 -85
- package/src/util/queue.ts +8 -8
- package/src/util/rpc.ts +35 -23
- package/src/util/scrap.ts +4 -4
- package/src/util/signal.ts +5 -5
- package/src/util/timeout.ts +6 -6
- package/src/util/token.ts +2 -2
- package/src/util/wildcard.ts +38 -27
package/src/tool/edit.ts
CHANGED
|
@@ -3,81 +3,108 @@
|
|
|
3
3
|
// https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/utils/editCorrector.ts
|
|
4
4
|
// https://github.com/cline/cline/blob/main/evals/diff-edits/diff-apply/diff-06-26-25.ts
|
|
5
5
|
|
|
6
|
-
import z from
|
|
7
|
-
import * as path from
|
|
8
|
-
import { Tool } from
|
|
9
|
-
import { createTwoFilesPatch, diffLines } from
|
|
10
|
-
import DESCRIPTION from
|
|
11
|
-
import { File } from
|
|
12
|
-
import { Bus } from
|
|
13
|
-
import { FileTime } from
|
|
14
|
-
import { Instance } from
|
|
15
|
-
import { Snapshot } from
|
|
6
|
+
import z from 'zod';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import { Tool } from './tool';
|
|
9
|
+
import { createTwoFilesPatch, diffLines } from 'diff';
|
|
10
|
+
import DESCRIPTION from './edit.txt';
|
|
11
|
+
import { File } from '../file';
|
|
12
|
+
import { Bus } from '../bus';
|
|
13
|
+
import { FileTime } from '../file/time';
|
|
14
|
+
import { Instance } from '../project/instance';
|
|
15
|
+
import { Snapshot } from '../snapshot';
|
|
16
16
|
|
|
17
17
|
function normalizeLineEndings(text: string): string {
|
|
18
|
-
return text.replaceAll(
|
|
18
|
+
return text.replaceAll('\r\n', '\n');
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export const EditTool = Tool.define(
|
|
21
|
+
export const EditTool = Tool.define('edit', {
|
|
22
22
|
description: DESCRIPTION,
|
|
23
23
|
parameters: z.object({
|
|
24
|
-
filePath: z.string().describe(
|
|
25
|
-
oldString: z.string().describe(
|
|
26
|
-
newString: z
|
|
27
|
-
|
|
24
|
+
filePath: z.string().describe('The absolute path to the file to modify'),
|
|
25
|
+
oldString: z.string().describe('The text to replace'),
|
|
26
|
+
newString: z
|
|
27
|
+
.string()
|
|
28
|
+
.describe(
|
|
29
|
+
'The text to replace it with (must be different from oldString)'
|
|
30
|
+
),
|
|
31
|
+
replaceAll: z
|
|
32
|
+
.boolean()
|
|
33
|
+
.optional()
|
|
34
|
+
.describe('Replace all occurrences of oldString (default false)'),
|
|
28
35
|
}),
|
|
29
36
|
async execute(params, ctx) {
|
|
30
37
|
if (!params.filePath) {
|
|
31
|
-
throw new Error(
|
|
38
|
+
throw new Error('filePath is required');
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
if (params.oldString === params.newString) {
|
|
35
|
-
throw new Error(
|
|
42
|
+
throw new Error('oldString and newString must be different');
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
// No restrictions - unrestricted file editing
|
|
39
|
-
const filePath = path.isAbsolute(params.filePath)
|
|
46
|
+
const filePath = path.isAbsolute(params.filePath)
|
|
47
|
+
? params.filePath
|
|
48
|
+
: path.join(Instance.directory, params.filePath);
|
|
40
49
|
|
|
41
|
-
let diff =
|
|
42
|
-
let contentOld =
|
|
43
|
-
let contentNew =
|
|
50
|
+
let diff = '';
|
|
51
|
+
let contentOld = '';
|
|
52
|
+
let contentNew = '';
|
|
44
53
|
await (async () => {
|
|
45
|
-
if (params.oldString ===
|
|
46
|
-
contentNew = params.newString
|
|
47
|
-
diff = trimDiff(
|
|
48
|
-
|
|
54
|
+
if (params.oldString === '') {
|
|
55
|
+
contentNew = params.newString;
|
|
56
|
+
diff = trimDiff(
|
|
57
|
+
createTwoFilesPatch(filePath, filePath, contentOld, contentNew)
|
|
58
|
+
);
|
|
59
|
+
await Bun.write(filePath, params.newString);
|
|
49
60
|
await Bus.publish(File.Event.Edited, {
|
|
50
61
|
file: filePath,
|
|
51
|
-
})
|
|
52
|
-
return
|
|
62
|
+
});
|
|
63
|
+
return;
|
|
53
64
|
}
|
|
54
65
|
|
|
55
|
-
const file = Bun.file(filePath)
|
|
56
|
-
const stats = await file.stat().catch(() => {})
|
|
57
|
-
if (!stats) throw new Error(`File ${filePath} not found`)
|
|
58
|
-
if (stats.isDirectory())
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
66
|
+
const file = Bun.file(filePath);
|
|
67
|
+
const stats = await file.stat().catch(() => {});
|
|
68
|
+
if (!stats) throw new Error(`File ${filePath} not found`);
|
|
69
|
+
if (stats.isDirectory())
|
|
70
|
+
throw new Error(`Path is a directory, not a file: ${filePath}`);
|
|
71
|
+
await FileTime.assert(ctx.sessionID, filePath);
|
|
72
|
+
contentOld = await file.text();
|
|
73
|
+
contentNew = replace(
|
|
74
|
+
contentOld,
|
|
75
|
+
params.oldString,
|
|
76
|
+
params.newString,
|
|
77
|
+
params.replaceAll
|
|
78
|
+
);
|
|
62
79
|
|
|
63
80
|
diff = trimDiff(
|
|
64
|
-
createTwoFilesPatch(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
81
|
+
createTwoFilesPatch(
|
|
82
|
+
filePath,
|
|
83
|
+
filePath,
|
|
84
|
+
normalizeLineEndings(contentOld),
|
|
85
|
+
normalizeLineEndings(contentNew)
|
|
86
|
+
)
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
await file.write(contentNew);
|
|
68
90
|
await Bus.publish(File.Event.Edited, {
|
|
69
91
|
file: filePath,
|
|
70
|
-
})
|
|
71
|
-
contentNew = await file.text()
|
|
92
|
+
});
|
|
93
|
+
contentNew = await file.text();
|
|
72
94
|
diff = trimDiff(
|
|
73
|
-
createTwoFilesPatch(
|
|
74
|
-
|
|
75
|
-
|
|
95
|
+
createTwoFilesPatch(
|
|
96
|
+
filePath,
|
|
97
|
+
filePath,
|
|
98
|
+
normalizeLineEndings(contentOld),
|
|
99
|
+
normalizeLineEndings(contentNew)
|
|
100
|
+
)
|
|
101
|
+
);
|
|
102
|
+
})();
|
|
76
103
|
|
|
77
|
-
FileTime.read(ctx.sessionID, filePath)
|
|
104
|
+
FileTime.read(ctx.sessionID, filePath);
|
|
78
105
|
|
|
79
|
-
let output =
|
|
80
|
-
const diagnostics = {}
|
|
106
|
+
let output = '';
|
|
107
|
+
const diagnostics = {};
|
|
81
108
|
|
|
82
109
|
const filediff: Snapshot.FileDiff = {
|
|
83
110
|
file: filePath,
|
|
@@ -85,10 +112,10 @@ export const EditTool = Tool.define("edit", {
|
|
|
85
112
|
after: contentNew,
|
|
86
113
|
additions: 0,
|
|
87
114
|
deletions: 0,
|
|
88
|
-
}
|
|
115
|
+
};
|
|
89
116
|
for (const change of diffLines(contentOld, contentNew)) {
|
|
90
|
-
if (change.added) filediff.additions += change.count || 0
|
|
91
|
-
if (change.removed) filediff.deletions += change.count || 0
|
|
117
|
+
if (change.added) filediff.additions += change.count || 0;
|
|
118
|
+
if (change.removed) filediff.deletions += change.count || 0;
|
|
92
119
|
}
|
|
93
120
|
|
|
94
121
|
return {
|
|
@@ -99,239 +126,254 @@ export const EditTool = Tool.define("edit", {
|
|
|
99
126
|
},
|
|
100
127
|
title: `${path.relative(Instance.worktree, filePath)}`,
|
|
101
128
|
output,
|
|
102
|
-
}
|
|
129
|
+
};
|
|
103
130
|
},
|
|
104
|
-
})
|
|
131
|
+
});
|
|
105
132
|
|
|
106
|
-
export type Replacer = (
|
|
133
|
+
export type Replacer = (
|
|
134
|
+
content: string,
|
|
135
|
+
find: string
|
|
136
|
+
) => Generator<string, void, unknown>;
|
|
107
137
|
|
|
108
138
|
// Similarity thresholds for block anchor fallback matching
|
|
109
|
-
const SINGLE_CANDIDATE_SIMILARITY_THRESHOLD = 0.0
|
|
110
|
-
const MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD = 0.3
|
|
139
|
+
const SINGLE_CANDIDATE_SIMILARITY_THRESHOLD = 0.0;
|
|
140
|
+
const MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD = 0.3;
|
|
111
141
|
|
|
112
142
|
/**
|
|
113
143
|
* Levenshtein distance algorithm implementation
|
|
114
144
|
*/
|
|
115
145
|
function levenshtein(a: string, b: string): number {
|
|
116
146
|
// Handle empty strings
|
|
117
|
-
if (a ===
|
|
118
|
-
return Math.max(a.length, b.length)
|
|
147
|
+
if (a === '' || b === '') {
|
|
148
|
+
return Math.max(a.length, b.length);
|
|
119
149
|
}
|
|
120
150
|
const matrix = Array.from({ length: a.length + 1 }, (_, i) =>
|
|
121
|
-
Array.from({ length: b.length + 1 }, (_, j) =>
|
|
122
|
-
|
|
151
|
+
Array.from({ length: b.length + 1 }, (_, j) =>
|
|
152
|
+
i === 0 ? j : j === 0 ? i : 0
|
|
153
|
+
)
|
|
154
|
+
);
|
|
123
155
|
|
|
124
156
|
for (let i = 1; i <= a.length; i++) {
|
|
125
157
|
for (let j = 1; j <= b.length; j++) {
|
|
126
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1
|
|
127
|
-
matrix[i][j] = Math.min(
|
|
158
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
159
|
+
matrix[i][j] = Math.min(
|
|
160
|
+
matrix[i - 1][j] + 1,
|
|
161
|
+
matrix[i][j - 1] + 1,
|
|
162
|
+
matrix[i - 1][j - 1] + cost
|
|
163
|
+
);
|
|
128
164
|
}
|
|
129
165
|
}
|
|
130
|
-
return matrix[a.length][b.length]
|
|
166
|
+
return matrix[a.length][b.length];
|
|
131
167
|
}
|
|
132
168
|
|
|
133
169
|
export const SimpleReplacer: Replacer = function* (_content, find) {
|
|
134
|
-
yield find
|
|
135
|
-
}
|
|
170
|
+
yield find;
|
|
171
|
+
};
|
|
136
172
|
|
|
137
173
|
export const LineTrimmedReplacer: Replacer = function* (content, find) {
|
|
138
|
-
const originalLines = content.split(
|
|
139
|
-
const searchLines = find.split(
|
|
174
|
+
const originalLines = content.split('\n');
|
|
175
|
+
const searchLines = find.split('\n');
|
|
140
176
|
|
|
141
|
-
if (searchLines[searchLines.length - 1] ===
|
|
142
|
-
searchLines.pop()
|
|
177
|
+
if (searchLines[searchLines.length - 1] === '') {
|
|
178
|
+
searchLines.pop();
|
|
143
179
|
}
|
|
144
180
|
|
|
145
181
|
for (let i = 0; i <= originalLines.length - searchLines.length; i++) {
|
|
146
|
-
let matches = true
|
|
182
|
+
let matches = true;
|
|
147
183
|
|
|
148
184
|
for (let j = 0; j < searchLines.length; j++) {
|
|
149
|
-
const originalTrimmed = originalLines[i + j].trim()
|
|
150
|
-
const searchTrimmed = searchLines[j].trim()
|
|
185
|
+
const originalTrimmed = originalLines[i + j].trim();
|
|
186
|
+
const searchTrimmed = searchLines[j].trim();
|
|
151
187
|
|
|
152
188
|
if (originalTrimmed !== searchTrimmed) {
|
|
153
|
-
matches = false
|
|
154
|
-
break
|
|
189
|
+
matches = false;
|
|
190
|
+
break;
|
|
155
191
|
}
|
|
156
192
|
}
|
|
157
193
|
|
|
158
194
|
if (matches) {
|
|
159
|
-
let matchStartIndex = 0
|
|
195
|
+
let matchStartIndex = 0;
|
|
160
196
|
for (let k = 0; k < i; k++) {
|
|
161
|
-
matchStartIndex += originalLines[k].length + 1
|
|
197
|
+
matchStartIndex += originalLines[k].length + 1;
|
|
162
198
|
}
|
|
163
199
|
|
|
164
|
-
let matchEndIndex = matchStartIndex
|
|
200
|
+
let matchEndIndex = matchStartIndex;
|
|
165
201
|
for (let k = 0; k < searchLines.length; k++) {
|
|
166
|
-
matchEndIndex += originalLines[i + k].length
|
|
202
|
+
matchEndIndex += originalLines[i + k].length;
|
|
167
203
|
if (k < searchLines.length - 1) {
|
|
168
|
-
matchEndIndex += 1 // Add newline character except for the last line
|
|
204
|
+
matchEndIndex += 1; // Add newline character except for the last line
|
|
169
205
|
}
|
|
170
206
|
}
|
|
171
207
|
|
|
172
|
-
yield content.substring(matchStartIndex, matchEndIndex)
|
|
208
|
+
yield content.substring(matchStartIndex, matchEndIndex);
|
|
173
209
|
}
|
|
174
210
|
}
|
|
175
|
-
}
|
|
211
|
+
};
|
|
176
212
|
|
|
177
213
|
export const BlockAnchorReplacer: Replacer = function* (content, find) {
|
|
178
|
-
const originalLines = content.split(
|
|
179
|
-
const searchLines = find.split(
|
|
214
|
+
const originalLines = content.split('\n');
|
|
215
|
+
const searchLines = find.split('\n');
|
|
180
216
|
|
|
181
217
|
if (searchLines.length < 3) {
|
|
182
|
-
return
|
|
218
|
+
return;
|
|
183
219
|
}
|
|
184
220
|
|
|
185
|
-
if (searchLines[searchLines.length - 1] ===
|
|
186
|
-
searchLines.pop()
|
|
221
|
+
if (searchLines[searchLines.length - 1] === '') {
|
|
222
|
+
searchLines.pop();
|
|
187
223
|
}
|
|
188
224
|
|
|
189
|
-
const firstLineSearch = searchLines[0].trim()
|
|
190
|
-
const lastLineSearch = searchLines[searchLines.length - 1].trim()
|
|
191
|
-
const searchBlockSize = searchLines.length
|
|
225
|
+
const firstLineSearch = searchLines[0].trim();
|
|
226
|
+
const lastLineSearch = searchLines[searchLines.length - 1].trim();
|
|
227
|
+
const searchBlockSize = searchLines.length;
|
|
192
228
|
|
|
193
229
|
// Collect all candidate positions where both anchors match
|
|
194
|
-
const candidates: Array<{ startLine: number; endLine: number }> = []
|
|
230
|
+
const candidates: Array<{ startLine: number; endLine: number }> = [];
|
|
195
231
|
for (let i = 0; i < originalLines.length; i++) {
|
|
196
232
|
if (originalLines[i].trim() !== firstLineSearch) {
|
|
197
|
-
continue
|
|
233
|
+
continue;
|
|
198
234
|
}
|
|
199
235
|
|
|
200
236
|
// Look for the matching last line after this first line
|
|
201
237
|
for (let j = i + 2; j < originalLines.length; j++) {
|
|
202
238
|
if (originalLines[j].trim() === lastLineSearch) {
|
|
203
|
-
candidates.push({ startLine: i, endLine: j })
|
|
204
|
-
break // Only match the first occurrence of the last line
|
|
239
|
+
candidates.push({ startLine: i, endLine: j });
|
|
240
|
+
break; // Only match the first occurrence of the last line
|
|
205
241
|
}
|
|
206
242
|
}
|
|
207
243
|
}
|
|
208
244
|
|
|
209
245
|
// Return immediately if no candidates
|
|
210
246
|
if (candidates.length === 0) {
|
|
211
|
-
return
|
|
247
|
+
return;
|
|
212
248
|
}
|
|
213
249
|
|
|
214
250
|
// Handle single candidate scenario (using relaxed threshold)
|
|
215
251
|
if (candidates.length === 1) {
|
|
216
|
-
const { startLine, endLine } = candidates[0]
|
|
217
|
-
const actualBlockSize = endLine - startLine + 1
|
|
252
|
+
const { startLine, endLine } = candidates[0];
|
|
253
|
+
const actualBlockSize = endLine - startLine + 1;
|
|
218
254
|
|
|
219
|
-
let similarity = 0
|
|
220
|
-
let linesToCheck = Math.min(searchBlockSize - 2, actualBlockSize - 2) // Middle lines only
|
|
255
|
+
let similarity = 0;
|
|
256
|
+
let linesToCheck = Math.min(searchBlockSize - 2, actualBlockSize - 2); // Middle lines only
|
|
221
257
|
|
|
222
258
|
if (linesToCheck > 0) {
|
|
223
259
|
for (let j = 1; j < searchBlockSize - 1 && j < actualBlockSize - 1; j++) {
|
|
224
|
-
const originalLine = originalLines[startLine + j].trim()
|
|
225
|
-
const searchLine = searchLines[j].trim()
|
|
226
|
-
const maxLen = Math.max(originalLine.length, searchLine.length)
|
|
260
|
+
const originalLine = originalLines[startLine + j].trim();
|
|
261
|
+
const searchLine = searchLines[j].trim();
|
|
262
|
+
const maxLen = Math.max(originalLine.length, searchLine.length);
|
|
227
263
|
if (maxLen === 0) {
|
|
228
|
-
continue
|
|
264
|
+
continue;
|
|
229
265
|
}
|
|
230
|
-
const distance = levenshtein(originalLine, searchLine)
|
|
231
|
-
similarity += (1 - distance / maxLen) / linesToCheck
|
|
266
|
+
const distance = levenshtein(originalLine, searchLine);
|
|
267
|
+
similarity += (1 - distance / maxLen) / linesToCheck;
|
|
232
268
|
|
|
233
269
|
// Exit early when threshold is reached
|
|
234
270
|
if (similarity >= SINGLE_CANDIDATE_SIMILARITY_THRESHOLD) {
|
|
235
|
-
break
|
|
271
|
+
break;
|
|
236
272
|
}
|
|
237
273
|
}
|
|
238
274
|
} else {
|
|
239
275
|
// No middle lines to compare, just accept based on anchors
|
|
240
|
-
similarity = 1.0
|
|
276
|
+
similarity = 1.0;
|
|
241
277
|
}
|
|
242
278
|
|
|
243
279
|
if (similarity >= SINGLE_CANDIDATE_SIMILARITY_THRESHOLD) {
|
|
244
|
-
let matchStartIndex = 0
|
|
280
|
+
let matchStartIndex = 0;
|
|
245
281
|
for (let k = 0; k < startLine; k++) {
|
|
246
|
-
matchStartIndex += originalLines[k].length + 1
|
|
282
|
+
matchStartIndex += originalLines[k].length + 1;
|
|
247
283
|
}
|
|
248
|
-
let matchEndIndex = matchStartIndex
|
|
284
|
+
let matchEndIndex = matchStartIndex;
|
|
249
285
|
for (let k = startLine; k <= endLine; k++) {
|
|
250
|
-
matchEndIndex += originalLines[k].length
|
|
286
|
+
matchEndIndex += originalLines[k].length;
|
|
251
287
|
if (k < endLine) {
|
|
252
|
-
matchEndIndex += 1 // Add newline character except for the last line
|
|
288
|
+
matchEndIndex += 1; // Add newline character except for the last line
|
|
253
289
|
}
|
|
254
290
|
}
|
|
255
|
-
yield content.substring(matchStartIndex, matchEndIndex)
|
|
291
|
+
yield content.substring(matchStartIndex, matchEndIndex);
|
|
256
292
|
}
|
|
257
|
-
return
|
|
293
|
+
return;
|
|
258
294
|
}
|
|
259
295
|
|
|
260
296
|
// Calculate similarity for multiple candidates
|
|
261
|
-
let bestMatch: { startLine: number; endLine: number } | null = null
|
|
262
|
-
let maxSimilarity = -1
|
|
297
|
+
let bestMatch: { startLine: number; endLine: number } | null = null;
|
|
298
|
+
let maxSimilarity = -1;
|
|
263
299
|
|
|
264
300
|
for (const candidate of candidates) {
|
|
265
|
-
const { startLine, endLine } = candidate
|
|
266
|
-
const actualBlockSize = endLine - startLine + 1
|
|
301
|
+
const { startLine, endLine } = candidate;
|
|
302
|
+
const actualBlockSize = endLine - startLine + 1;
|
|
267
303
|
|
|
268
|
-
let similarity = 0
|
|
269
|
-
let linesToCheck = Math.min(searchBlockSize - 2, actualBlockSize - 2) // Middle lines only
|
|
304
|
+
let similarity = 0;
|
|
305
|
+
let linesToCheck = Math.min(searchBlockSize - 2, actualBlockSize - 2); // Middle lines only
|
|
270
306
|
|
|
271
307
|
if (linesToCheck > 0) {
|
|
272
308
|
for (let j = 1; j < searchBlockSize - 1 && j < actualBlockSize - 1; j++) {
|
|
273
|
-
const originalLine = originalLines[startLine + j].trim()
|
|
274
|
-
const searchLine = searchLines[j].trim()
|
|
275
|
-
const maxLen = Math.max(originalLine.length, searchLine.length)
|
|
309
|
+
const originalLine = originalLines[startLine + j].trim();
|
|
310
|
+
const searchLine = searchLines[j].trim();
|
|
311
|
+
const maxLen = Math.max(originalLine.length, searchLine.length);
|
|
276
312
|
if (maxLen === 0) {
|
|
277
|
-
continue
|
|
313
|
+
continue;
|
|
278
314
|
}
|
|
279
|
-
const distance = levenshtein(originalLine, searchLine)
|
|
280
|
-
similarity += 1 - distance / maxLen
|
|
315
|
+
const distance = levenshtein(originalLine, searchLine);
|
|
316
|
+
similarity += 1 - distance / maxLen;
|
|
281
317
|
}
|
|
282
|
-
similarity /= linesToCheck // Average similarity
|
|
318
|
+
similarity /= linesToCheck; // Average similarity
|
|
283
319
|
} else {
|
|
284
320
|
// No middle lines to compare, just accept based on anchors
|
|
285
|
-
similarity = 1.0
|
|
321
|
+
similarity = 1.0;
|
|
286
322
|
}
|
|
287
323
|
|
|
288
324
|
if (similarity > maxSimilarity) {
|
|
289
|
-
maxSimilarity = similarity
|
|
290
|
-
bestMatch = candidate
|
|
325
|
+
maxSimilarity = similarity;
|
|
326
|
+
bestMatch = candidate;
|
|
291
327
|
}
|
|
292
328
|
}
|
|
293
329
|
|
|
294
330
|
// Threshold judgment
|
|
295
331
|
if (maxSimilarity >= MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD && bestMatch) {
|
|
296
|
-
const { startLine, endLine } = bestMatch
|
|
297
|
-
let matchStartIndex = 0
|
|
332
|
+
const { startLine, endLine } = bestMatch;
|
|
333
|
+
let matchStartIndex = 0;
|
|
298
334
|
for (let k = 0; k < startLine; k++) {
|
|
299
|
-
matchStartIndex += originalLines[k].length + 1
|
|
335
|
+
matchStartIndex += originalLines[k].length + 1;
|
|
300
336
|
}
|
|
301
|
-
let matchEndIndex = matchStartIndex
|
|
337
|
+
let matchEndIndex = matchStartIndex;
|
|
302
338
|
for (let k = startLine; k <= endLine; k++) {
|
|
303
|
-
matchEndIndex += originalLines[k].length
|
|
339
|
+
matchEndIndex += originalLines[k].length;
|
|
304
340
|
if (k < endLine) {
|
|
305
|
-
matchEndIndex += 1
|
|
341
|
+
matchEndIndex += 1;
|
|
306
342
|
}
|
|
307
343
|
}
|
|
308
|
-
yield content.substring(matchStartIndex, matchEndIndex)
|
|
344
|
+
yield content.substring(matchStartIndex, matchEndIndex);
|
|
309
345
|
}
|
|
310
|
-
}
|
|
346
|
+
};
|
|
311
347
|
|
|
312
|
-
export const WhitespaceNormalizedReplacer: Replacer = function* (
|
|
313
|
-
|
|
314
|
-
|
|
348
|
+
export const WhitespaceNormalizedReplacer: Replacer = function* (
|
|
349
|
+
content,
|
|
350
|
+
find
|
|
351
|
+
) {
|
|
352
|
+
const normalizeWhitespace = (text: string) =>
|
|
353
|
+
text.replace(/\s+/g, ' ').trim();
|
|
354
|
+
const normalizedFind = normalizeWhitespace(find);
|
|
315
355
|
|
|
316
356
|
// Handle single line matches
|
|
317
|
-
const lines = content.split(
|
|
357
|
+
const lines = content.split('\n');
|
|
318
358
|
for (let i = 0; i < lines.length; i++) {
|
|
319
|
-
const line = lines[i]
|
|
359
|
+
const line = lines[i];
|
|
320
360
|
if (normalizeWhitespace(line) === normalizedFind) {
|
|
321
|
-
yield line
|
|
361
|
+
yield line;
|
|
322
362
|
} else {
|
|
323
363
|
// Only check for substring matches if the full line doesn't match
|
|
324
|
-
const normalizedLine = normalizeWhitespace(line)
|
|
364
|
+
const normalizedLine = normalizeWhitespace(line);
|
|
325
365
|
if (normalizedLine.includes(normalizedFind)) {
|
|
326
366
|
// Find the actual substring in the original line that matches
|
|
327
|
-
const words = find.trim().split(/\s+/)
|
|
367
|
+
const words = find.trim().split(/\s+/);
|
|
328
368
|
if (words.length > 0) {
|
|
329
|
-
const pattern = words
|
|
369
|
+
const pattern = words
|
|
370
|
+
.map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
371
|
+
.join('\\s+');
|
|
330
372
|
try {
|
|
331
|
-
const regex = new RegExp(pattern)
|
|
332
|
-
const match = line.match(regex)
|
|
373
|
+
const regex = new RegExp(pattern);
|
|
374
|
+
const match = line.match(regex);
|
|
333
375
|
if (match) {
|
|
334
|
-
yield match[0]
|
|
376
|
+
yield match[0];
|
|
335
377
|
}
|
|
336
378
|
} catch (e) {
|
|
337
379
|
// Invalid regex pattern, skip
|
|
@@ -342,234 +384,244 @@ export const WhitespaceNormalizedReplacer: Replacer = function* (content, find)
|
|
|
342
384
|
}
|
|
343
385
|
|
|
344
386
|
// Handle multi-line matches
|
|
345
|
-
const findLines = find.split(
|
|
387
|
+
const findLines = find.split('\n');
|
|
346
388
|
if (findLines.length > 1) {
|
|
347
389
|
for (let i = 0; i <= lines.length - findLines.length; i++) {
|
|
348
|
-
const block = lines.slice(i, i + findLines.length)
|
|
349
|
-
if (normalizeWhitespace(block.join(
|
|
350
|
-
yield block.join(
|
|
390
|
+
const block = lines.slice(i, i + findLines.length);
|
|
391
|
+
if (normalizeWhitespace(block.join('\n')) === normalizedFind) {
|
|
392
|
+
yield block.join('\n');
|
|
351
393
|
}
|
|
352
394
|
}
|
|
353
395
|
}
|
|
354
|
-
}
|
|
396
|
+
};
|
|
355
397
|
|
|
356
398
|
export const IndentationFlexibleReplacer: Replacer = function* (content, find) {
|
|
357
399
|
const removeIndentation = (text: string) => {
|
|
358
|
-
const lines = text.split(
|
|
359
|
-
const nonEmptyLines = lines.filter((line) => line.trim().length > 0)
|
|
360
|
-
if (nonEmptyLines.length === 0) return text
|
|
400
|
+
const lines = text.split('\n');
|
|
401
|
+
const nonEmptyLines = lines.filter((line) => line.trim().length > 0);
|
|
402
|
+
if (nonEmptyLines.length === 0) return text;
|
|
361
403
|
|
|
362
404
|
const minIndent = Math.min(
|
|
363
405
|
...nonEmptyLines.map((line) => {
|
|
364
|
-
const match = line.match(/^(\s*)/)
|
|
365
|
-
return match ? match[1].length : 0
|
|
366
|
-
})
|
|
367
|
-
)
|
|
406
|
+
const match = line.match(/^(\s*)/);
|
|
407
|
+
return match ? match[1].length : 0;
|
|
408
|
+
})
|
|
409
|
+
);
|
|
368
410
|
|
|
369
|
-
return lines
|
|
370
|
-
|
|
411
|
+
return lines
|
|
412
|
+
.map((line) => (line.trim().length === 0 ? line : line.slice(minIndent)))
|
|
413
|
+
.join('\n');
|
|
414
|
+
};
|
|
371
415
|
|
|
372
|
-
const normalizedFind = removeIndentation(find)
|
|
373
|
-
const contentLines = content.split(
|
|
374
|
-
const findLines = find.split(
|
|
416
|
+
const normalizedFind = removeIndentation(find);
|
|
417
|
+
const contentLines = content.split('\n');
|
|
418
|
+
const findLines = find.split('\n');
|
|
375
419
|
|
|
376
420
|
for (let i = 0; i <= contentLines.length - findLines.length; i++) {
|
|
377
|
-
const block = contentLines.slice(i, i + findLines.length).join(
|
|
421
|
+
const block = contentLines.slice(i, i + findLines.length).join('\n');
|
|
378
422
|
if (removeIndentation(block) === normalizedFind) {
|
|
379
|
-
yield block
|
|
423
|
+
yield block;
|
|
380
424
|
}
|
|
381
425
|
}
|
|
382
|
-
}
|
|
426
|
+
};
|
|
383
427
|
|
|
384
428
|
export const EscapeNormalizedReplacer: Replacer = function* (content, find) {
|
|
385
429
|
const unescapeString = (str: string): string => {
|
|
386
430
|
return str.replace(/\\(n|t|r|'|"|`|\\|\n|\$)/g, (match, capturedChar) => {
|
|
387
431
|
switch (capturedChar) {
|
|
388
|
-
case
|
|
389
|
-
return
|
|
390
|
-
case
|
|
391
|
-
return
|
|
392
|
-
case
|
|
393
|
-
return
|
|
432
|
+
case 'n':
|
|
433
|
+
return '\n';
|
|
434
|
+
case 't':
|
|
435
|
+
return '\t';
|
|
436
|
+
case 'r':
|
|
437
|
+
return '\r';
|
|
394
438
|
case "'":
|
|
395
|
-
return "'"
|
|
439
|
+
return "'";
|
|
396
440
|
case '"':
|
|
397
|
-
return '"'
|
|
398
|
-
case
|
|
399
|
-
return
|
|
400
|
-
case
|
|
401
|
-
return
|
|
402
|
-
case
|
|
403
|
-
return
|
|
404
|
-
case
|
|
405
|
-
return
|
|
441
|
+
return '"';
|
|
442
|
+
case '`':
|
|
443
|
+
return '`';
|
|
444
|
+
case '\\':
|
|
445
|
+
return '\\';
|
|
446
|
+
case '\n':
|
|
447
|
+
return '\n';
|
|
448
|
+
case '$':
|
|
449
|
+
return '$';
|
|
406
450
|
default:
|
|
407
|
-
return match
|
|
451
|
+
return match;
|
|
408
452
|
}
|
|
409
|
-
})
|
|
410
|
-
}
|
|
453
|
+
});
|
|
454
|
+
};
|
|
411
455
|
|
|
412
|
-
const unescapedFind = unescapeString(find)
|
|
456
|
+
const unescapedFind = unescapeString(find);
|
|
413
457
|
|
|
414
458
|
// Try direct match with unescaped find string
|
|
415
459
|
if (content.includes(unescapedFind)) {
|
|
416
|
-
yield unescapedFind
|
|
460
|
+
yield unescapedFind;
|
|
417
461
|
}
|
|
418
462
|
|
|
419
463
|
// Also try finding escaped versions in content that match unescaped find
|
|
420
|
-
const lines = content.split(
|
|
421
|
-
const findLines = unescapedFind.split(
|
|
464
|
+
const lines = content.split('\n');
|
|
465
|
+
const findLines = unescapedFind.split('\n');
|
|
422
466
|
|
|
423
467
|
for (let i = 0; i <= lines.length - findLines.length; i++) {
|
|
424
|
-
const block = lines.slice(i, i + findLines.length).join(
|
|
425
|
-
const unescapedBlock = unescapeString(block)
|
|
468
|
+
const block = lines.slice(i, i + findLines.length).join('\n');
|
|
469
|
+
const unescapedBlock = unescapeString(block);
|
|
426
470
|
|
|
427
471
|
if (unescapedBlock === unescapedFind) {
|
|
428
|
-
yield block
|
|
472
|
+
yield block;
|
|
429
473
|
}
|
|
430
474
|
}
|
|
431
|
-
}
|
|
475
|
+
};
|
|
432
476
|
|
|
433
477
|
export const MultiOccurrenceReplacer: Replacer = function* (content, find) {
|
|
434
478
|
// This replacer yields all exact matches, allowing the replace function
|
|
435
479
|
// to handle multiple occurrences based on replaceAll parameter
|
|
436
|
-
let startIndex = 0
|
|
480
|
+
let startIndex = 0;
|
|
437
481
|
|
|
438
482
|
while (true) {
|
|
439
|
-
const index = content.indexOf(find, startIndex)
|
|
440
|
-
if (index === -1) break
|
|
483
|
+
const index = content.indexOf(find, startIndex);
|
|
484
|
+
if (index === -1) break;
|
|
441
485
|
|
|
442
|
-
yield find
|
|
443
|
-
startIndex = index + find.length
|
|
486
|
+
yield find;
|
|
487
|
+
startIndex = index + find.length;
|
|
444
488
|
}
|
|
445
|
-
}
|
|
489
|
+
};
|
|
446
490
|
|
|
447
491
|
export const TrimmedBoundaryReplacer: Replacer = function* (content, find) {
|
|
448
|
-
const trimmedFind = find.trim()
|
|
492
|
+
const trimmedFind = find.trim();
|
|
449
493
|
|
|
450
494
|
if (trimmedFind === find) {
|
|
451
495
|
// Already trimmed, no point in trying
|
|
452
|
-
return
|
|
496
|
+
return;
|
|
453
497
|
}
|
|
454
498
|
|
|
455
499
|
// Try to find the trimmed version
|
|
456
500
|
if (content.includes(trimmedFind)) {
|
|
457
|
-
yield trimmedFind
|
|
501
|
+
yield trimmedFind;
|
|
458
502
|
}
|
|
459
503
|
|
|
460
504
|
// Also try finding blocks where trimmed content matches
|
|
461
|
-
const lines = content.split(
|
|
462
|
-
const findLines = find.split(
|
|
505
|
+
const lines = content.split('\n');
|
|
506
|
+
const findLines = find.split('\n');
|
|
463
507
|
|
|
464
508
|
for (let i = 0; i <= lines.length - findLines.length; i++) {
|
|
465
|
-
const block = lines.slice(i, i + findLines.length).join(
|
|
509
|
+
const block = lines.slice(i, i + findLines.length).join('\n');
|
|
466
510
|
|
|
467
511
|
if (block.trim() === trimmedFind) {
|
|
468
|
-
yield block
|
|
512
|
+
yield block;
|
|
469
513
|
}
|
|
470
514
|
}
|
|
471
|
-
}
|
|
515
|
+
};
|
|
472
516
|
|
|
473
517
|
export const ContextAwareReplacer: Replacer = function* (content, find) {
|
|
474
|
-
const findLines = find.split(
|
|
518
|
+
const findLines = find.split('\n');
|
|
475
519
|
if (findLines.length < 3) {
|
|
476
520
|
// Need at least 3 lines to have meaningful context
|
|
477
|
-
return
|
|
521
|
+
return;
|
|
478
522
|
}
|
|
479
523
|
|
|
480
524
|
// Remove trailing empty line if present
|
|
481
|
-
if (findLines[findLines.length - 1] ===
|
|
482
|
-
findLines.pop()
|
|
525
|
+
if (findLines[findLines.length - 1] === '') {
|
|
526
|
+
findLines.pop();
|
|
483
527
|
}
|
|
484
528
|
|
|
485
|
-
const contentLines = content.split(
|
|
529
|
+
const contentLines = content.split('\n');
|
|
486
530
|
|
|
487
531
|
// Extract first and last lines as context anchors
|
|
488
|
-
const firstLine = findLines[0].trim()
|
|
489
|
-
const lastLine = findLines[findLines.length - 1].trim()
|
|
532
|
+
const firstLine = findLines[0].trim();
|
|
533
|
+
const lastLine = findLines[findLines.length - 1].trim();
|
|
490
534
|
|
|
491
535
|
// Find blocks that start and end with the context anchors
|
|
492
536
|
for (let i = 0; i < contentLines.length; i++) {
|
|
493
|
-
if (contentLines[i].trim() !== firstLine) continue
|
|
537
|
+
if (contentLines[i].trim() !== firstLine) continue;
|
|
494
538
|
|
|
495
539
|
// Look for the matching last line
|
|
496
540
|
for (let j = i + 2; j < contentLines.length; j++) {
|
|
497
541
|
if (contentLines[j].trim() === lastLine) {
|
|
498
542
|
// Found a potential context block
|
|
499
|
-
const blockLines = contentLines.slice(i, j + 1)
|
|
500
|
-
const block = blockLines.join(
|
|
543
|
+
const blockLines = contentLines.slice(i, j + 1);
|
|
544
|
+
const block = blockLines.join('\n');
|
|
501
545
|
|
|
502
546
|
// Check if the middle content has reasonable similarity
|
|
503
547
|
// (simple heuristic: at least 50% of non-empty lines should match when trimmed)
|
|
504
548
|
if (blockLines.length === findLines.length) {
|
|
505
|
-
let matchingLines = 0
|
|
506
|
-
let totalNonEmptyLines = 0
|
|
549
|
+
let matchingLines = 0;
|
|
550
|
+
let totalNonEmptyLines = 0;
|
|
507
551
|
|
|
508
552
|
for (let k = 1; k < blockLines.length - 1; k++) {
|
|
509
|
-
const blockLine = blockLines[k].trim()
|
|
510
|
-
const findLine = findLines[k].trim()
|
|
553
|
+
const blockLine = blockLines[k].trim();
|
|
554
|
+
const findLine = findLines[k].trim();
|
|
511
555
|
|
|
512
556
|
if (blockLine.length > 0 || findLine.length > 0) {
|
|
513
|
-
totalNonEmptyLines
|
|
557
|
+
totalNonEmptyLines++;
|
|
514
558
|
if (blockLine === findLine) {
|
|
515
|
-
matchingLines
|
|
559
|
+
matchingLines++;
|
|
516
560
|
}
|
|
517
561
|
}
|
|
518
562
|
}
|
|
519
563
|
|
|
520
|
-
if (
|
|
521
|
-
|
|
522
|
-
|
|
564
|
+
if (
|
|
565
|
+
totalNonEmptyLines === 0 ||
|
|
566
|
+
matchingLines / totalNonEmptyLines >= 0.5
|
|
567
|
+
) {
|
|
568
|
+
yield block;
|
|
569
|
+
break; // Only match the first occurrence
|
|
523
570
|
}
|
|
524
571
|
}
|
|
525
|
-
break
|
|
572
|
+
break;
|
|
526
573
|
}
|
|
527
574
|
}
|
|
528
575
|
}
|
|
529
|
-
}
|
|
576
|
+
};
|
|
530
577
|
|
|
531
578
|
export function trimDiff(diff: string): string {
|
|
532
|
-
const lines = diff.split(
|
|
579
|
+
const lines = diff.split('\n');
|
|
533
580
|
const contentLines = lines.filter(
|
|
534
581
|
(line) =>
|
|
535
|
-
(line.startsWith(
|
|
536
|
-
!line.startsWith(
|
|
537
|
-
!line.startsWith(
|
|
538
|
-
)
|
|
582
|
+
(line.startsWith('+') || line.startsWith('-') || line.startsWith(' ')) &&
|
|
583
|
+
!line.startsWith('---') &&
|
|
584
|
+
!line.startsWith('+++')
|
|
585
|
+
);
|
|
539
586
|
|
|
540
|
-
if (contentLines.length === 0) return diff
|
|
587
|
+
if (contentLines.length === 0) return diff;
|
|
541
588
|
|
|
542
|
-
let min = Infinity
|
|
589
|
+
let min = Infinity;
|
|
543
590
|
for (const line of contentLines) {
|
|
544
|
-
const content = line.slice(1)
|
|
591
|
+
const content = line.slice(1);
|
|
545
592
|
if (content.trim().length > 0) {
|
|
546
|
-
const match = content.match(/^(\s*)/)
|
|
547
|
-
if (match) min = Math.min(min, match[1].length)
|
|
593
|
+
const match = content.match(/^(\s*)/);
|
|
594
|
+
if (match) min = Math.min(min, match[1].length);
|
|
548
595
|
}
|
|
549
596
|
}
|
|
550
|
-
if (min === Infinity || min === 0) return diff
|
|
597
|
+
if (min === Infinity || min === 0) return diff;
|
|
551
598
|
const trimmedLines = lines.map((line) => {
|
|
552
599
|
if (
|
|
553
|
-
(line.startsWith(
|
|
554
|
-
!line.startsWith(
|
|
555
|
-
!line.startsWith(
|
|
600
|
+
(line.startsWith('+') || line.startsWith('-') || line.startsWith(' ')) &&
|
|
601
|
+
!line.startsWith('---') &&
|
|
602
|
+
!line.startsWith('+++')
|
|
556
603
|
) {
|
|
557
|
-
const prefix = line[0]
|
|
558
|
-
const content = line.slice(1)
|
|
559
|
-
return prefix + content.slice(min)
|
|
604
|
+
const prefix = line[0];
|
|
605
|
+
const content = line.slice(1);
|
|
606
|
+
return prefix + content.slice(min);
|
|
560
607
|
}
|
|
561
|
-
return line
|
|
562
|
-
})
|
|
608
|
+
return line;
|
|
609
|
+
});
|
|
563
610
|
|
|
564
|
-
return trimmedLines.join(
|
|
611
|
+
return trimmedLines.join('\n');
|
|
565
612
|
}
|
|
566
613
|
|
|
567
|
-
export function replace(
|
|
614
|
+
export function replace(
|
|
615
|
+
content: string,
|
|
616
|
+
oldString: string,
|
|
617
|
+
newString: string,
|
|
618
|
+
replaceAll = false
|
|
619
|
+
): string {
|
|
568
620
|
if (oldString === newString) {
|
|
569
|
-
throw new Error(
|
|
621
|
+
throw new Error('oldString and newString must be different');
|
|
570
622
|
}
|
|
571
623
|
|
|
572
|
-
let notFound = true
|
|
624
|
+
let notFound = true;
|
|
573
625
|
|
|
574
626
|
for (const replacer of [
|
|
575
627
|
SimpleReplacer,
|
|
@@ -583,22 +635,26 @@ export function replace(content: string, oldString: string, newString: string, r
|
|
|
583
635
|
MultiOccurrenceReplacer,
|
|
584
636
|
]) {
|
|
585
637
|
for (const search of replacer(content, oldString)) {
|
|
586
|
-
const index = content.indexOf(search)
|
|
587
|
-
if (index === -1) continue
|
|
588
|
-
notFound = false
|
|
638
|
+
const index = content.indexOf(search);
|
|
639
|
+
if (index === -1) continue;
|
|
640
|
+
notFound = false;
|
|
589
641
|
if (replaceAll) {
|
|
590
|
-
return content.replaceAll(search, newString)
|
|
642
|
+
return content.replaceAll(search, newString);
|
|
591
643
|
}
|
|
592
|
-
const lastIndex = content.lastIndexOf(search)
|
|
593
|
-
if (index !== lastIndex) continue
|
|
594
|
-
return
|
|
644
|
+
const lastIndex = content.lastIndexOf(search);
|
|
645
|
+
if (index !== lastIndex) continue;
|
|
646
|
+
return (
|
|
647
|
+
content.substring(0, index) +
|
|
648
|
+
newString +
|
|
649
|
+
content.substring(index + search.length)
|
|
650
|
+
);
|
|
595
651
|
}
|
|
596
652
|
}
|
|
597
653
|
|
|
598
654
|
if (notFound) {
|
|
599
|
-
throw new Error(
|
|
655
|
+
throw new Error('oldString not found in content');
|
|
600
656
|
}
|
|
601
657
|
throw new Error(
|
|
602
|
-
|
|
603
|
-
)
|
|
658
|
+
'Found multiple matches for oldString. Provide more surrounding lines in oldString to identify the correct match.'
|
|
659
|
+
);
|
|
604
660
|
}
|