pi-hashline-edit-pro 0.15.1 → 0.15.2
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/package.json +1 -1
- package/src/hashline/hash.ts +46 -24
- package/src/replace.ts +9 -1
package/package.json
CHANGED
package/src/hashline/hash.ts
CHANGED
|
@@ -124,7 +124,7 @@ export function _lineHashesPure(content: string): string[] {
|
|
|
124
124
|
export async function lineHashes(
|
|
125
125
|
content: string,
|
|
126
126
|
path?: string,
|
|
127
|
-
previous?: { content: string; hashes: string[] },
|
|
127
|
+
previous?: { content: string; hashes: string[]; removedHashes?: Set<string> },
|
|
128
128
|
): Promise<string[]> {
|
|
129
129
|
if (!path) {
|
|
130
130
|
return _lineHashesPure(content);
|
|
@@ -137,6 +137,7 @@ export async function lineHashes(
|
|
|
137
137
|
const newHashes = mapStableHashes(
|
|
138
138
|
previous.content, previous.hashes,
|
|
139
139
|
content,
|
|
140
|
+
previous.removedHashes,
|
|
140
141
|
);
|
|
141
142
|
store.snapshots[path] = { content, hashes: newHashes };
|
|
142
143
|
await saveHashStore(store);
|
|
@@ -157,42 +158,63 @@ export async function lineHashes(
|
|
|
157
158
|
}
|
|
158
159
|
|
|
159
160
|
/**
|
|
160
|
-
*
|
|
161
|
-
*
|
|
161
|
+
* Maps old hashes to new positions using hash-aware content matching.
|
|
162
|
+
* Unlike Diff.diffLines (which is content-only and cannot distinguish
|
|
163
|
+
* identical lines at different positions), this algorithm uses the
|
|
164
|
+
* removedHashes set to disambiguate: when a line appears multiple times
|
|
165
|
+
* in the old content and one occurrence was targeted by the edit,
|
|
166
|
+
* the surviving occurrence is matched to the non-removed hash.
|
|
167
|
+
* New/changed lines allocate fresh hashes with collision avoidance.
|
|
162
168
|
*/
|
|
163
169
|
function mapStableHashes(
|
|
164
170
|
oldContent: string,
|
|
165
171
|
oldHashes: string[],
|
|
166
172
|
newContent: string,
|
|
173
|
+
removedHashes?: Set<string>,
|
|
167
174
|
): string[] {
|
|
168
175
|
const newLines = newContent.split("\n");
|
|
169
176
|
const newHashes = new Array<string>(newLines.length);
|
|
170
177
|
const used = new Set<string>();
|
|
171
178
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
for (
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
if (part.added) {
|
|
183
|
-
newIdx += count;
|
|
184
|
-
} else if (part.removed) {
|
|
185
|
-
oldIdx += count;
|
|
179
|
+
// Build a map from line content to list of (index, hash) for the old content.
|
|
180
|
+
// We process occurrences left-to-right so that matching preserves order.
|
|
181
|
+
const contentMap = new Map<string, { index: number; hash: string }[]>();
|
|
182
|
+
const oldLines = oldContent.split("\n");
|
|
183
|
+
for (let i = 0; i < oldLines.length; i++) {
|
|
184
|
+
const line = oldLines[i]!;
|
|
185
|
+
const entry = { index: i, hash: oldHashes[i]! };
|
|
186
|
+
const list = contentMap.get(line);
|
|
187
|
+
if (list) {
|
|
188
|
+
list.push(entry);
|
|
186
189
|
} else {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
190
|
+
contentMap.set(line, [entry]);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Match each new line to an old occurrence by content.
|
|
195
|
+
// For lines with duplicate content, prefer occurrences whose hash
|
|
196
|
+
// was NOT targeted by the edit (those are the survivors).
|
|
197
|
+
for (let i = 0; i < newLines.length; i++) {
|
|
198
|
+
const line = newLines[i]!;
|
|
199
|
+
const candidates = contentMap.get(line);
|
|
200
|
+
if (!candidates || candidates.length === 0) continue;
|
|
201
|
+
|
|
202
|
+
// Find the best match: prefer a non-removed occurrence.
|
|
203
|
+
// If all remaining occurrences are removed, use the first one anyway
|
|
204
|
+
// (it will get a fresh hash in the fill step since its hash is in used+removed).
|
|
205
|
+
let bestIdx = 0;
|
|
206
|
+
if (removedHashes && removedHashes.size > 0) {
|
|
207
|
+
for (let j = 0; j < candidates.length; j++) {
|
|
208
|
+
if (!removedHashes.has(candidates[j]!.hash)) {
|
|
209
|
+
bestIdx = j;
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
194
212
|
}
|
|
195
213
|
}
|
|
214
|
+
|
|
215
|
+
const match = candidates.splice(bestIdx, 1)[0]!;
|
|
216
|
+
newHashes[i] = match.hash;
|
|
217
|
+
used.add(match.hash);
|
|
196
218
|
}
|
|
197
219
|
|
|
198
220
|
// Fill remaining (new/changed) lines with fresh hashes
|
package/src/replace.ts
CHANGED
|
@@ -165,10 +165,18 @@ export async function execPipeline(
|
|
|
165
165
|
|
|
166
166
|
const result = anchorResult.content;
|
|
167
167
|
|
|
168
|
-
//
|
|
168
|
+
// Collect hashes targeted by the edit for hash-aware diff disambiguation
|
|
169
|
+
const removedHashes = new Set<string>();
|
|
170
|
+
for (const edit of resolved) {
|
|
171
|
+
removedHashes.add(edit.hash_range_inclusive[0].hash);
|
|
172
|
+
removedHashes.add(edit.hash_range_inclusive[1].hash);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Compute stable result hashes using hash-aware preservation
|
|
169
176
|
const resultHashes = await lineHashes(result, absolutePath, {
|
|
170
177
|
content: originalNormalized,
|
|
171
178
|
hashes: originalHashes,
|
|
179
|
+
removedHashes,
|
|
172
180
|
});
|
|
173
181
|
|
|
174
182
|
// Format boundary warnings using stable result hashes
|