pi-hashline-edit-pro 4.5.2 → 5.0.0
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/README.md +43 -25
- package/index.ts +15 -2
- package/package.json +1 -1
- package/prompts/insert-guidelines.md +1 -1
- package/prompts/insert-snippet.md +1 -1
- package/prompts/insert.md +1 -1
- package/prompts/move-guidelines.md +1 -1
- package/prompts/replace-guidelines.md +2 -4
- package/prompts/replace-snippet.md +1 -1
- package/prompts/replace-within-guidelines.md +3 -0
- package/prompts/replace-within-snippet.md +1 -0
- package/prompts/replace-within.md +5 -0
- package/prompts/replace.md +3 -2
- package/prompts/undo-last-change-guidelines.md +1 -1
- package/prompts/undo-last-change-snippet.md +1 -1
- package/prompts/undo-last-change.md +1 -1
- package/src/batch.ts +29 -7
- package/src/commit.ts +16 -13
- package/src/config-ui.ts +2 -1
- package/src/config.ts +6 -1
- package/src/constants.ts +6 -0
- package/src/copy-move.ts +15 -11
- package/src/edit-common.ts +66 -15
- package/src/edit-fidelity.ts +108 -0
- package/src/grep.ts +6 -5
- package/src/hashline/apply.ts +7 -5
- package/src/hashline/index.ts +2 -0
- package/src/hashline/parse.ts +15 -4
- package/src/hashline/resolve.ts +67 -38
- package/src/insert.ts +21 -34
- package/src/payload-contract.ts +155 -28
- package/src/replace-render.ts +4 -4
- package/src/replace-response.ts +3 -1
- package/src/replace-within.ts +227 -0
- package/src/replace.ts +26 -15
- package/src/utils.ts +28 -14
- package/prompts/grep-guidelines.md +0 -2
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { execPipeline, noteAnchorError, previewFromPipe, previewError, type ReplaceDetails } from "./replace";
|
|
4
|
+
import { commitEdit } from "./commit";
|
|
5
|
+
import { readNormFile, type NormFile } from "./file-reader";
|
|
6
|
+
import { fmtRegion, MAX_HASH_LINES, parseHashRef, resolveAnchorLine, stripAnchorRow, type Anchor } from "./hashline";
|
|
7
|
+
import { withAnchorSession } from "./anchor-registry";
|
|
8
|
+
import { loadP, loadGuide } from "./prompts";
|
|
9
|
+
import {
|
|
10
|
+
assertReplaceWithinReq,
|
|
11
|
+
buildReplaceWithinToolSchema,
|
|
12
|
+
getReplaceWithinInput,
|
|
13
|
+
normalizeReplaceWithinRequest,
|
|
14
|
+
type ReplaceWithinReq,
|
|
15
|
+
} from "./payload-contract";
|
|
16
|
+
import { literalEscapeHint, splitLines } from "./utils";
|
|
17
|
+
import { toLF } from "./normalize";
|
|
18
|
+
import { MAX_RANGE_STALE_LINES } from "./constants";
|
|
19
|
+
import {
|
|
20
|
+
DEFAULT_EDIT_FLAGS,
|
|
21
|
+
editRenderResultWrapper,
|
|
22
|
+
editToolBase,
|
|
23
|
+
queuedEdit,
|
|
24
|
+
resolveEditTargetWithRequirement,
|
|
25
|
+
throwIfStrictInput,
|
|
26
|
+
tryResolveEditTarget,
|
|
27
|
+
withReplaceWithinPrompts,
|
|
28
|
+
type EditToolFlags,
|
|
29
|
+
} from "./edit-common";
|
|
30
|
+
import { makeRenderCall, type RPreview, type RRState } from "./replace-render";
|
|
31
|
+
|
|
32
|
+
interface WithinRefs {
|
|
33
|
+
from: Anchor;
|
|
34
|
+
to: Anchor;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface WithinPlan {
|
|
38
|
+
editParams: { remove_from: string; remove_to: string; replacement_lines: string };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function formatWithinRange(start: number, end: number): string {
|
|
42
|
+
return start === end ? `line ${start}` : `lines ${start}-${end}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function offsetLineNumber(rangeLines: string[], offset: number): number {
|
|
46
|
+
let cursor = 0;
|
|
47
|
+
for (let index = 0; index < rangeLines.length; index++) {
|
|
48
|
+
const length = rangeLines[index]!.length;
|
|
49
|
+
if (offset <= cursor + length) return index + 1;
|
|
50
|
+
cursor += length + 1;
|
|
51
|
+
}
|
|
52
|
+
return rangeLines.length;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function matchOffsets(text: string, oldText: string): number[] {
|
|
56
|
+
const offsets: number[] = [];
|
|
57
|
+
let from = 0;
|
|
58
|
+
for (;;) {
|
|
59
|
+
const found = text.indexOf(oldText, from);
|
|
60
|
+
if (found < 0) return offsets;
|
|
61
|
+
offsets.push(found);
|
|
62
|
+
from = found + 1;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function notFoundMessage(displayPath: string, start: number, end: number, fileHashes: string[], fileLines: string[]): string {
|
|
67
|
+
const rangeLength = end - start + 1;
|
|
68
|
+
const shownCount = Math.min(rangeLength, MAX_RANGE_STALE_LINES);
|
|
69
|
+
const shown = fmtRegion(fileHashes.slice(start - 1, start - 1 + shownCount), fileLines.slice(start - 1, start - 1 + shownCount));
|
|
70
|
+
const more = rangeLength > shownCount ? `\n[The range has ${rangeLength} lines; showing the first ${shownCount}.]` : "";
|
|
71
|
+
return `[E_SUBSTRING_NOT_FOUND] "replace_old" was not found in ${formatWithinRange(start, end)} of ${displayPath}. Current rows:\n\n${shown}${more}\n\nCopy replace_old exactly from the served row (comparison uses LF breaks and excludes the last line's terminator) and retry.`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function ambiguousMessage(displayPath: string, start: number, end: number, matchLines: number[]): string {
|
|
75
|
+
const shownCount = 8;
|
|
76
|
+
const shown = matchLines.slice(0, shownCount).join(", ");
|
|
77
|
+
const more = matchLines.length > shownCount ? ` (+${matchLines.length - shownCount} more)` : "";
|
|
78
|
+
return `[E_SUBSTRING_AMBIGUOUS] "replace_old" occurs ${matchLines.length} times in ${formatWithinRange(start, end)} of ${displayPath} (matching lines ${shown}${more}). Narrow replace_from/replace_to to one line, or extend replace_old so it matches exactly once.`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function parseWithinAnchors(req: ReplaceWithinReq): { refs: WithinRefs; warnings: string[] } {
|
|
82
|
+
const warnings: string[] = [];
|
|
83
|
+
const from = stripAnchorRow(req.replace_from.trim(), "replace_from entry", warnings);
|
|
84
|
+
const to = stripAnchorRow(req.replace_to.trim(), "replace_to entry", warnings);
|
|
85
|
+
return { refs: { from: parseHashRef(from), to: parseHashRef(to) }, warnings };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function buildReplaceWithinEdit(req: ReplaceWithinReq, refs: WithinRefs, preload: NormFile, displayPath: string): WithinPlan {
|
|
89
|
+
const fileLines = splitLines(preload.normalized);
|
|
90
|
+
const fromLine = resolveAnchorLine(refs.from, fileLines, preload.fileHashes, displayPath);
|
|
91
|
+
const toLine = resolveAnchorLine(refs.to, fileLines, preload.fileHashes, displayPath);
|
|
92
|
+
const start = Math.min(fromLine, toLine);
|
|
93
|
+
const end = Math.max(fromLine, toLine);
|
|
94
|
+
const rangeLines = fileLines.slice(start - 1, end);
|
|
95
|
+
const rangeText = rangeLines.join("\n");
|
|
96
|
+
const oldText = toLF(req.replace_old);
|
|
97
|
+
const offsets = matchOffsets(rangeText, oldText);
|
|
98
|
+
if (offsets.length === 0) {
|
|
99
|
+
throw new Error(notFoundMessage(displayPath, start, end, preload.fileHashes, fileLines));
|
|
100
|
+
}
|
|
101
|
+
if (offsets.length > 1) {
|
|
102
|
+
throw new Error(ambiguousMessage(displayPath, start, end, offsets.map((offset) => start - 1 + offsetLineNumber(rangeLines, offset))));
|
|
103
|
+
}
|
|
104
|
+
const matchOffset = offsets[0]!;
|
|
105
|
+
const replacement = rangeText.slice(0, matchOffset) + req.replace_new + rangeText.slice(matchOffset + oldText.length);
|
|
106
|
+
const startRef = fromLine <= toLine ? refs.from : refs.to;
|
|
107
|
+
const endRef = fromLine <= toLine ? refs.to : refs.from;
|
|
108
|
+
return {
|
|
109
|
+
editParams: {
|
|
110
|
+
remove_from: startRef.hash,
|
|
111
|
+
remove_to: endRef.hash,
|
|
112
|
+
replacement_lines: replacement,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function replaceWithinPreview(request: unknown, cwd: string, signal?: AbortSignal): Promise<RPreview> {
|
|
118
|
+
try {
|
|
119
|
+
const normalized = normalizeReplaceWithinRequest(request);
|
|
120
|
+
assertReplaceWithinReq(normalized);
|
|
121
|
+
const req = normalized;
|
|
122
|
+
const { refs, warnings } = parseWithinAnchors(req);
|
|
123
|
+
await throwIfStrictInput(warnings);
|
|
124
|
+
const targetPath = await resolveEditTargetWithRequirement({
|
|
125
|
+
removeFrom: req.replace_from,
|
|
126
|
+
removeTo: req.replace_to,
|
|
127
|
+
providedPath: req.path,
|
|
128
|
+
cwd,
|
|
129
|
+
});
|
|
130
|
+
const preload = await readNormFile(targetPath, cwd, {
|
|
131
|
+
accessMode: constants.R_OK,
|
|
132
|
+
maxLines: MAX_HASH_LINES,
|
|
133
|
+
noPersist: true,
|
|
134
|
+
allocation: "shadow",
|
|
135
|
+
signal,
|
|
136
|
+
});
|
|
137
|
+
const plan = buildReplaceWithinEdit(req, refs, preload, targetPath);
|
|
138
|
+
const pipe = await execPipeline(targetPath, plan.editParams, cwd, {
|
|
139
|
+
accessMode: constants.R_OK,
|
|
140
|
+
noPersist: true,
|
|
141
|
+
preloadedNorm: preload,
|
|
142
|
+
signal,
|
|
143
|
+
});
|
|
144
|
+
return previewFromPipe(pipe);
|
|
145
|
+
} catch (error: unknown) {
|
|
146
|
+
if (signal?.aborted) throw error;
|
|
147
|
+
return previewError(error);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
type ReplaceWithinToolDef = ToolDefinition<any, ReplaceDetails, RRState> & { renderShell?: "default" | "self" };
|
|
152
|
+
|
|
153
|
+
export function buildReplaceWithinToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): ReplaceWithinToolDef {
|
|
154
|
+
const prompted = withReplaceWithinPrompts({
|
|
155
|
+
description: loadP("../prompts/replace-within.md"),
|
|
156
|
+
snippet: loadP("../prompts/replace-within-snippet.md"),
|
|
157
|
+
guidelines: loadGuide("../prompts/replace-within-guidelines.md"),
|
|
158
|
+
}, flags);
|
|
159
|
+
return {
|
|
160
|
+
name: "replace_within",
|
|
161
|
+
label: "Replace Within",
|
|
162
|
+
description: prompted.description,
|
|
163
|
+
promptSnippet: prompted.snippet,
|
|
164
|
+
promptGuidelines: prompted.guidelines,
|
|
165
|
+
...editToolBase,
|
|
166
|
+
prepareArguments: normalizeReplaceWithinRequest,
|
|
167
|
+
parameters: buildReplaceWithinToolSchema(flags.requirePath),
|
|
168
|
+
renderCall: makeRenderCall(replaceWithinPreview, {
|
|
169
|
+
getInput: getReplaceWithinInput,
|
|
170
|
+
toolName: "replace_within",
|
|
171
|
+
resolveTarget: (input) => (typeof input.replace_from === "string" ? tryResolveEditTarget(input.replace_from, input.replace_to) : undefined),
|
|
172
|
+
}),
|
|
173
|
+
renderResult: editRenderResultWrapper,
|
|
174
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
175
|
+
return withAnchorSession(ctx, async () => {
|
|
176
|
+
const normalized = normalizeReplaceWithinRequest(params);
|
|
177
|
+
assertReplaceWithinReq(normalized);
|
|
178
|
+
const req = normalized;
|
|
179
|
+
const { refs, warnings } = parseWithinAnchors(req);
|
|
180
|
+
await throwIfStrictInput(warnings);
|
|
181
|
+
const hints = [
|
|
182
|
+
literalEscapeHint([req.replace_old], "replace_old"),
|
|
183
|
+
literalEscapeHint([req.replace_new], "replace_new"),
|
|
184
|
+
].filter((hint): hint is string => hint !== undefined);
|
|
185
|
+
const targetPath = await resolveEditTargetWithRequirement({
|
|
186
|
+
removeFrom: req.replace_from,
|
|
187
|
+
removeTo: req.replace_to,
|
|
188
|
+
providedPath: req.path,
|
|
189
|
+
cwd: ctx.cwd,
|
|
190
|
+
});
|
|
191
|
+
return queuedEdit(targetPath, ctx.cwd, signal, async (absolutePath, mutationTargetPath) => {
|
|
192
|
+
const preload = await readNormFile(targetPath, ctx.cwd, {
|
|
193
|
+
signal,
|
|
194
|
+
accessMode: constants.R_OK | constants.W_OK,
|
|
195
|
+
maxLines: MAX_HASH_LINES,
|
|
196
|
+
});
|
|
197
|
+
let plan: WithinPlan;
|
|
198
|
+
try {
|
|
199
|
+
plan = buildReplaceWithinEdit(req, refs, preload, targetPath);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
await noteAnchorError(preload.absolutePath, error);
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
const pipe = await execPipeline(targetPath, plan.editParams, ctx.cwd, {
|
|
205
|
+
accessMode: constants.R_OK | constants.W_OK,
|
|
206
|
+
signal,
|
|
207
|
+
preloadedNorm: preload,
|
|
208
|
+
});
|
|
209
|
+
return commitEdit(pipe, {
|
|
210
|
+
path: pipe.path,
|
|
211
|
+
absolutePath,
|
|
212
|
+
mutationTargetPath,
|
|
213
|
+
editAnchors: [plan.editParams.remove_from, plan.editParams.remove_to],
|
|
214
|
+
prefixWarnings: [...warnings, ...hints],
|
|
215
|
+
signal,
|
|
216
|
+
verb: "replaced",
|
|
217
|
+
noopNoun: "Replacement",
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function regReplaceWithin(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FLAGS): void {
|
|
226
|
+
pi.registerTool(buildReplaceWithinToolDef(flags));
|
|
227
|
+
}
|
package/src/replace.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
type LineEnding,
|
|
10
10
|
} from "./replace-diff";
|
|
11
11
|
import { readNormFile, type NormFile } from "./file-reader";
|
|
12
|
-
import { editToolSchema, buildEditToolSchema, type ReqParams, assertReq, normReq } from "./payload-contract";
|
|
12
|
+
import { editToolSchema, buildEditToolSchema, type ReqParams, type RawReqParams, assertReq, normReq } from "./payload-contract";
|
|
13
13
|
import { literalEscapeHint, splitLines } from "./utils";
|
|
14
14
|
import { loadP, loadGuide } from "./prompts";
|
|
15
15
|
import { type FileIdentity } from "./fs-write";
|
|
@@ -83,6 +83,7 @@ export interface ExecPipelineOptions {
|
|
|
83
83
|
served?: ReadonlyMap<string, string>;
|
|
84
84
|
allowEmpty?: boolean;
|
|
85
85
|
stripWarning?: StripWarningLocation;
|
|
86
|
+
endingOverrides?: (LineEnding | undefined)[];
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
export function hashSpan(hashes: string[], from: string, to: string): [number, number] | undefined {
|
|
@@ -120,28 +121,35 @@ function countLineChanges(
|
|
|
120
121
|
totalRemovedLines,
|
|
121
122
|
};
|
|
122
123
|
}
|
|
123
|
-
|
|
124
|
-
export function buildReplaceHEdit(params: ReqParams): { edit: HEdit; warnings: string[] } {
|
|
124
|
+
export function buildReplaceHEdit(params: RawReqParams): { edit: HEdit; warnings: string[] } {
|
|
125
125
|
const editWarnings: string[] = [];
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
replacement_lines: params.replacement_lines,
|
|
131
|
-
},
|
|
132
|
-
editWarnings,
|
|
133
|
-
);
|
|
126
|
+
const anchors = { remove_from: params.remove_from, remove_to: params.remove_to };
|
|
127
|
+
const edit = typeof params.replacement_lines === "string"
|
|
128
|
+
? resEdit({ ...anchors, replacement_lines: params.replacement_lines }, editWarnings)
|
|
129
|
+
: resEdit({ ...anchors, replacement_lines: params.replacement_lines }, editWarnings);
|
|
134
130
|
return { edit, warnings: editWarnings };
|
|
135
131
|
}
|
|
136
132
|
|
|
133
|
+
function withEndingOverrides(edit: HEdit, overrides: (LineEnding | undefined)[] | undefined): HEdit {
|
|
134
|
+
if (overrides === undefined) return edit;
|
|
135
|
+
const length = Math.max(edit.content_separators?.length ?? 0, overrides.length);
|
|
136
|
+
const separators: (LineEnding | undefined)[] = new Array(length);
|
|
137
|
+
for (let index = 0; index < length; index++) {
|
|
138
|
+
separators[index] = overrides[index] ?? edit.content_separators?.[index];
|
|
139
|
+
}
|
|
140
|
+
if (separators.every((ending) => ending === undefined)) return edit;
|
|
141
|
+
return { ...edit, content_separators: separators };
|
|
142
|
+
}
|
|
143
|
+
|
|
137
144
|
export async function execPipeline(
|
|
138
145
|
targetPath: string,
|
|
139
|
-
params:
|
|
146
|
+
params: RawReqParams,
|
|
140
147
|
cwd: string,
|
|
141
148
|
options?: ExecPipelineOptions,
|
|
142
149
|
): Promise<PipelineResult> {
|
|
143
150
|
|
|
144
151
|
const { edit, warnings: editWarnings } = buildReplaceHEdit(params);
|
|
152
|
+
const anchoredEdit = withEndingOverrides(edit, options?.endingOverrides);
|
|
145
153
|
const hashStore = options?.store ?? await loadHashStore();
|
|
146
154
|
const preResolvedPath = await resolveTarget(toCwd(targetPath, cwd));
|
|
147
155
|
const served = options?.served ?? servedForPath(preResolvedPath);
|
|
@@ -154,7 +162,7 @@ export async function execPipeline(
|
|
|
154
162
|
try {
|
|
155
163
|
anchorResult = applyEdit(
|
|
156
164
|
originalNormalized,
|
|
157
|
-
|
|
165
|
+
anchoredEdit,
|
|
158
166
|
options?.signal,
|
|
159
167
|
originalHashes,
|
|
160
168
|
displayPath,
|
|
@@ -202,12 +210,15 @@ export async function execPipeline(
|
|
|
202
210
|
totalRemovedLines,
|
|
203
211
|
identity,
|
|
204
212
|
...(pipeSpans ? { spans: pipeSpans } : {}),
|
|
205
|
-
...(
|
|
213
|
+
...(anchoredEdit.content_separators !== undefined ? { contentSeparators: anchoredEdit.content_separators } : {}),
|
|
206
214
|
};
|
|
207
215
|
}
|
|
208
216
|
|
|
209
217
|
export function previewFromPipe(pipe: PipelineResult): RPreview {
|
|
210
218
|
if (pipe.originalNormalized === pipe.result) {
|
|
219
|
+
if (pipe.contentSeparators !== undefined) {
|
|
220
|
+
return { diff: "", path: pipe.path };
|
|
221
|
+
}
|
|
211
222
|
return {
|
|
212
223
|
error: `No changes made to ${pipe.path}. The edit produced identical content.`,
|
|
213
224
|
path: pipe.path,
|
|
@@ -274,7 +285,7 @@ export function buildToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): ToolDef
|
|
|
274
285
|
const canonical = normReq(params);
|
|
275
286
|
assertReq(canonical);
|
|
276
287
|
const normalizedParams = canonical;
|
|
277
|
-
const literalEscape = literalEscapeHint(normalizedParams.replacement_lines, "replacement_lines");
|
|
288
|
+
const literalEscape = literalEscapeHint([normalizedParams.replacement_lines], "replacement_lines");
|
|
278
289
|
const targetPath = await resolveEditTargetWithRequirement({
|
|
279
290
|
removeFrom: normalizedParams.remove_from,
|
|
280
291
|
removeTo: normalizedParams.remove_to,
|
package/src/utils.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { NUL_CONTENT_MSG, MAX_BYTES } from "./constants";
|
|
2
2
|
import { HASH_CLASS } from "./hashline/alphabet";
|
|
3
|
+
import { splitWithEndings } from "./line-endings";
|
|
4
|
+
import type { LineEnding } from "./normalize";
|
|
3
5
|
|
|
4
6
|
export function isRec(value: unknown): value is Record<string, unknown> {
|
|
5
7
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -342,24 +344,36 @@ export function decodeStringArray(value: unknown, warnings?: string[], label = "
|
|
|
342
344
|
return undefined;
|
|
343
345
|
}
|
|
344
346
|
|
|
345
|
-
function
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
347
|
+
function legacyLinesToText(lines: string[]): string {
|
|
348
|
+
const decoded = decodeStringArray(lines) ?? lines;
|
|
349
|
+
const parsedLines: string[] = [];
|
|
350
|
+
const parsedSeparators: (LineEnding | undefined)[] = [];
|
|
351
|
+
for (const element of decoded) {
|
|
352
|
+
const parsed = splitWithEndings(element);
|
|
353
|
+
const lineCount = element.endsWith("\n") || element.endsWith("\r") ? parsed.lines.length - 1 : parsed.lines.length;
|
|
354
|
+
for (let index = 0; index < lineCount; index++) {
|
|
355
|
+
parsedLines.push(parsed.lines[index]!);
|
|
356
|
+
parsedSeparators.push(parsed.endings[index]);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
let text = "";
|
|
360
|
+
for (let index = 0; index < parsedLines.length; index++) {
|
|
361
|
+
const line = parsedLines[index]!;
|
|
362
|
+
const separator = parsedSeparators[index];
|
|
363
|
+
text += line;
|
|
364
|
+
if (separator !== undefined) text += separator;
|
|
365
|
+
else if (index < parsedLines.length - 1) text += "\n";
|
|
366
|
+
else if (line === "") text += "\n";
|
|
367
|
+
}
|
|
368
|
+
return text;
|
|
356
369
|
}
|
|
357
370
|
|
|
358
371
|
function normalizeEditLines(record: Record<string, unknown>): void {
|
|
359
372
|
for (const key of ["replacement_lines", "lines"]) {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
373
|
+
const value = record[key];
|
|
374
|
+
if (Array.isArray(value) && value.every((entry): entry is string => typeof entry === "string")) {
|
|
375
|
+
record[key] = legacyLinesToText(value);
|
|
376
|
+
}
|
|
363
377
|
}
|
|
364
378
|
}
|
|
365
379
|
|