pi-hashline-edit-pro 2.8.1 → 2.8.3
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 +15 -12
- package/index.ts +37 -8
- package/package.json +1 -1
- package/prompts/grep-guidelines.md +3 -3
- package/prompts/grep-snippet.md +1 -1
- package/prompts/grep.md +1 -1
- package/prompts/insert-guidelines.md +1 -1
- package/prompts/read-guidelines.md +1 -1
- package/prompts/replace.md +1 -1
- package/src/commit.ts +6 -2
- package/src/config.ts +15 -2
- package/src/constants.ts +1 -1
- package/src/grep.ts +200 -111
- package/src/hash-store/validation.ts +31 -0
- package/src/hash-store.ts +46 -2
- package/src/hashline/apply.ts +3 -3
- package/src/hashline/resolve.ts +52 -36
- package/src/read.ts +5 -3
- package/src/replace-undo.ts +6 -3
- package/src/replace.ts +2 -2
- package/src/served.ts +56 -27
- package/src/write-hook.ts +4 -4
package/src/hashline/resolve.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { abortIf, rejectUnknownFields, firstNonEmptyIndex, lastNonEmptyIndex, cl
|
|
|
2
2
|
import { HASH_SEP, HASH_RUN, stripRowPrefix, canon } from "./hash";
|
|
3
3
|
import { parseHashRef, parseText, type Anchor } from "./parse";
|
|
4
4
|
import { NEW_CONTENT_NOT_ARRAY_MSG, MAX_RANGE_STALE_LINES } from "../constants";
|
|
5
|
+
import { contentChecksum } from "./hasher";
|
|
5
6
|
|
|
6
7
|
export type RAnchor = {
|
|
7
8
|
line: number;
|
|
@@ -90,14 +91,13 @@ export function fmtMismatchWithHashes(
|
|
|
90
91
|
fileLines: string[],
|
|
91
92
|
fileHashes: string[],
|
|
92
93
|
filePath?: string,
|
|
93
|
-
): { text: string; hashes: string[] } {
|
|
94
|
+
): { text: string; hashes: string[]; servedMap: Map<string, string> } {
|
|
94
95
|
assertAligned(fileLines, fileHashes, "fmtMismatch");
|
|
95
|
-
|
|
96
96
|
const out: string[] = [];
|
|
97
97
|
const hashes: string[] = [];
|
|
98
|
+
const servedMap = new Map<string, string>();
|
|
98
99
|
const notFound = mismatches.filter((m) => m.kind === "not_found");
|
|
99
100
|
const ambiguous = mismatches.filter((m) => m.kind === "ambiguous");
|
|
100
|
-
|
|
101
101
|
const refList = notFound.map((m) => `"${m.ref.hash}"`).join(", ");
|
|
102
102
|
if (notFound.length > 0) {
|
|
103
103
|
out.push(
|
|
@@ -110,8 +110,11 @@ export function fmtMismatchWithHashes(
|
|
|
110
110
|
const to = Math.min(fileLines.length, ctx.line + 1);
|
|
111
111
|
const rows: string[] = [];
|
|
112
112
|
for (let ln = from; ln <= to; ln++) {
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
const h = fileHashes[ln - 1]!;
|
|
114
|
+
const c = fileLines[ln - 1] ?? "";
|
|
115
|
+
hashes.push(h);
|
|
116
|
+
servedMap.set(h, contentChecksum(c));
|
|
117
|
+
rows.push(` ${ln}: ${h}│${clipLine(c)}`);
|
|
115
118
|
}
|
|
116
119
|
out.push("");
|
|
117
120
|
out.push(` Current context around resolved anchor "${ctx.hash}" (line ${ctx.line}):\n${rows.join("\n")}`);
|
|
@@ -128,7 +131,12 @@ export function fmtMismatchWithHashes(
|
|
|
128
131
|
(m.candidates?.length ?? 0) > sample.length
|
|
129
132
|
? `, ... (+${(m.candidates?.length ?? 0) - sample.length} more)`
|
|
130
133
|
: "";
|
|
131
|
-
for (const line of sample)
|
|
134
|
+
for (const line of sample) {
|
|
135
|
+
const h = fileHashes[line - 1]!;
|
|
136
|
+
const c = fileLines[line - 1] ?? "";
|
|
137
|
+
hashes.push(h);
|
|
138
|
+
servedMap.set(h, contentChecksum(c));
|
|
139
|
+
}
|
|
132
140
|
const lines = sample
|
|
133
141
|
.map((line) => {
|
|
134
142
|
const content = clipLine(fileLines[line - 1] ?? "");
|
|
@@ -140,8 +148,7 @@ export function fmtMismatchWithHashes(
|
|
|
140
148
|
);
|
|
141
149
|
}
|
|
142
150
|
}
|
|
143
|
-
|
|
144
|
-
return { text: out.join("\n"), hashes };
|
|
151
|
+
return { text: out.join("\n"), hashes, servedMap };
|
|
145
152
|
}
|
|
146
153
|
|
|
147
154
|
|
|
@@ -482,45 +489,49 @@ export function valEdit(
|
|
|
482
489
|
}
|
|
483
490
|
|
|
484
491
|
export function resolveAnchorLine(
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
492
|
+
ref: Anchor,
|
|
493
|
+
fileLines: string[],
|
|
494
|
+
fileHashes: string[],
|
|
495
|
+
filePath?: string,
|
|
489
496
|
): number {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
497
|
+
const { resolved, mismatches } = valEdit(
|
|
498
|
+
{ hash_bounds: [ref, ref], content_lines: [] },
|
|
499
|
+
fileLines,
|
|
500
|
+
fileHashes,
|
|
501
|
+
[],
|
|
502
|
+
undefined,
|
|
503
|
+
);
|
|
504
|
+
if (mismatches.length > 0 || !resolved) {
|
|
505
|
+
const feedback = fmtMismatchWithHashes(
|
|
506
|
+
mismatches,
|
|
507
|
+
fileLines,
|
|
508
|
+
fileHashes,
|
|
509
|
+
filePath,
|
|
510
|
+
);
|
|
511
|
+
throw new AnchorMismatchError(feedback.text, feedback.hashes, feedback.servedMap);
|
|
512
|
+
}
|
|
513
|
+
return resolved.hash_bounds[0].line;
|
|
507
514
|
}
|
|
508
515
|
|
|
509
516
|
export class RangeStaleError extends Error {
|
|
510
517
|
readonly rangeHashes: string[];
|
|
511
|
-
|
|
518
|
+
readonly rangeServedMap: Map<string, string>;
|
|
519
|
+
constructor(message: string, rangeHashes: string[], rangeServedMap: Map<string, string>) {
|
|
512
520
|
super(message);
|
|
513
521
|
this.name = "RangeStaleError";
|
|
514
522
|
this.rangeHashes = rangeHashes;
|
|
523
|
+
this.rangeServedMap = rangeServedMap;
|
|
515
524
|
}
|
|
516
525
|
}
|
|
517
526
|
|
|
518
527
|
export class AnchorMismatchError extends Error {
|
|
519
528
|
readonly feedbackHashes: string[];
|
|
520
|
-
|
|
529
|
+
readonly feedbackMap: Map<string, string>;
|
|
530
|
+
constructor(message: string, feedbackHashes: string[], feedbackMap: Map<string, string>) {
|
|
521
531
|
super(message);
|
|
522
532
|
this.name = "AnchorMismatchError";
|
|
523
533
|
this.feedbackHashes = feedbackHashes;
|
|
534
|
+
this.feedbackMap = feedbackMap;
|
|
524
535
|
}
|
|
525
536
|
}
|
|
526
537
|
|
|
@@ -528,7 +539,7 @@ export function assertRangeServed(
|
|
|
528
539
|
resolved: RHEdit,
|
|
529
540
|
fileLines: string[],
|
|
530
541
|
fileHashes: string[],
|
|
531
|
-
served:
|
|
542
|
+
served: ReadonlyMap<string, string> | undefined,
|
|
532
543
|
filePath?: string,
|
|
533
544
|
): void {
|
|
534
545
|
assertAligned(fileLines, fileHashes, "assertRangeServed");
|
|
@@ -536,18 +547,23 @@ export function assertRangeServed(
|
|
|
536
547
|
const endLine = resolved.hash_bounds[1].line;
|
|
537
548
|
const mismatchLines: number[] = [];
|
|
538
549
|
for (let line = startLine; line <= endLine; line++) {
|
|
539
|
-
|
|
550
|
+
const hash = fileHashes[line - 1]!;
|
|
551
|
+
const content = fileLines[line - 1]!;
|
|
552
|
+
const servedContent = served?.get(hash);
|
|
553
|
+
if (servedContent === undefined || servedContent !== contentChecksum(content)) mismatchLines.push(line);
|
|
540
554
|
}
|
|
541
555
|
if (mismatchLines.length === 0) return;
|
|
542
|
-
|
|
543
556
|
const rangeLength = endLine - startLine + 1;
|
|
544
557
|
const shownLength = Math.min(rangeLength, MAX_RANGE_STALE_LINES);
|
|
545
558
|
const rows: string[] = [];
|
|
546
559
|
const shownHashes: string[] = [];
|
|
560
|
+
const shownMap = new Map<string, string>();
|
|
547
561
|
for (let line = startLine; line < startLine + shownLength; line++) {
|
|
548
562
|
const hash = fileHashes[line - 1]!;
|
|
563
|
+
const content = fileLines[line - 1]!;
|
|
549
564
|
shownHashes.push(hash);
|
|
550
|
-
|
|
565
|
+
shownMap.set(hash, contentChecksum(content));
|
|
566
|
+
rows.push(fmtRow(hash, clipLine(content)));
|
|
551
567
|
}
|
|
552
568
|
const location = filePath ? ` in ${filePath}` : "";
|
|
553
569
|
const first = mismatchLines[0]!;
|
|
@@ -561,7 +577,7 @@ export function assertRangeServed(
|
|
|
561
577
|
: "";
|
|
562
578
|
const message =
|
|
563
579
|
`[E_RANGE_STALE] ${mismatchText} what was shown. Nothing was modified. Current range with fresh anchors:\n\n${rows.join("\n")}${capHint}`;
|
|
564
|
-
throw new RangeStaleError(message, shownHashes);
|
|
580
|
+
throw new RangeStaleError(message, shownHashes, shownMap);
|
|
565
581
|
}
|
|
566
582
|
|
|
567
583
|
export { warnUnicodeEsc };
|
package/src/read.ts
CHANGED
|
@@ -13,8 +13,8 @@ import { MAX_OVERSIZED_WARNING_LINES } from "./constants";
|
|
|
13
13
|
import { readNormFile, safeSnapId } from "./file-reader";
|
|
14
14
|
import { lineHashes, fmtRegion, fmtRow, HASH_SEP, MAX_HASH_LINES } from "./hashline";
|
|
15
15
|
import { toCwd } from "./paths";
|
|
16
|
-
import { abortIf, makePrepareArguments, numberedRead, visLines } from "./utils";
|
|
17
|
-
import { recordServedSafe } from "./served";
|
|
16
|
+
import { abortIf, makePrepareArguments, numberedRead, visLines, splitLines } from "./utils";
|
|
17
|
+
import { recordServedSafe, buildServedMap } from "./served";
|
|
18
18
|
import { loadP, loadGuide } from "./prompts";
|
|
19
19
|
import { valAccess } from "./validation";
|
|
20
20
|
import { Text } from "@earendil-works/pi-tui";
|
|
@@ -236,7 +236,9 @@ export function regRead(pi: ExtensionAPI): void {
|
|
|
236
236
|
fileHashes,
|
|
237
237
|
resolvedPath,
|
|
238
238
|
);
|
|
239
|
-
|
|
239
|
+
const fileLines = splitLines(normalized);
|
|
240
|
+
const servedMap = buildServedMap(fileHashes, fileLines, preview.servedHashes);
|
|
241
|
+
await recordServedSafe(resolvedPath, servedMap, "read", new Set(fileHashes));
|
|
240
242
|
const snapshotId = await safeSnapId(absolutePath, "read");
|
|
241
243
|
const previewText =
|
|
242
244
|
hadUtf8DecodeErrors
|
package/src/replace-undo.ts
CHANGED
|
@@ -4,11 +4,11 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
4
4
|
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { Type } from "typebox";
|
|
6
6
|
import { loadHashStore, persistSnapshot, upsertUndo, getUndoEntry, deleteUndo, type UndoRecord } from "./hash-store";
|
|
7
|
-
import {
|
|
7
|
+
import { recordServed, buildServedMap, servedHashesFromDiff } from "./served";
|
|
8
8
|
import { resolveInCwd, writeAtomic, type FileIdentity } from "./fs-write";
|
|
9
9
|
import { toLF, stripBOM, restoreEndings, type LineEnding } from "./normalize";
|
|
10
10
|
import { genDiff, genPatch } from "./replace-diff";
|
|
11
|
-
import { cntDiff, errCode, makePrepareArguments } from "./utils";
|
|
11
|
+
import { cntDiff, errCode, makePrepareArguments, splitLines } from "./utils";
|
|
12
12
|
import { loadP, loadGuide } from "./prompts";
|
|
13
13
|
import { buildMetrics } from "./replace-response";
|
|
14
14
|
import { renderEditResult } from "./replace-render";
|
|
@@ -172,7 +172,10 @@ export function regUndo(pi: ExtensionAPI): void {
|
|
|
172
172
|
try {
|
|
173
173
|
const store = await loadHashStore();
|
|
174
174
|
persistSnapshot(store, mutationTargetPath, undo.content, undo.hashes);
|
|
175
|
-
|
|
175
|
+
const diffHashes = servedHashesFromDiff(undoDiff);
|
|
176
|
+
const undoLines = splitLines(undo.content);
|
|
177
|
+
const servedMap = buildServedMap(undo.hashes, undoLines, diffHashes);
|
|
178
|
+
recordServed(store, mutationTargetPath, servedMap, new Set(undo.hashes));
|
|
176
179
|
} catch (error) {
|
|
177
180
|
console.error("Failed to restore hash store snapshot after undo:", error);
|
|
178
181
|
}
|
package/src/replace.ts
CHANGED
|
@@ -119,8 +119,8 @@ function hashSpan(hashes: string[], from: string, to: string): [number, number]
|
|
|
119
119
|
}
|
|
120
120
|
async function noteAnchorError(absolutePath: string, error: unknown, scopeHashes: string[], noPersist?: boolean): Promise<void> {
|
|
121
121
|
if (noPersist === true) return;
|
|
122
|
-
if (error instanceof RangeStaleError) await recordServedSafe(absolutePath, error.
|
|
123
|
-
else if (error instanceof AnchorMismatchError) await recordServedSafe(absolutePath, error.
|
|
122
|
+
if (error instanceof RangeStaleError) await recordServedSafe(absolutePath, error.rangeServedMap, "range-stale feedback", new Set(scopeHashes));
|
|
123
|
+
else if (error instanceof AnchorMismatchError) await recordServedSafe(absolutePath, error.feedbackMap, "anchor-mismatch feedback", new Set(scopeHashes));
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
function collectRemovedHashes(
|
package/src/served.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { loadHashStore,
|
|
1
|
+
import { loadHashStore, parseStoredServed, STORE_NOT_OPEN_MESSAGE, withStore, type HashStore } from "./hash-store";
|
|
2
2
|
import { HASH_CLASS } from "./hashline/alphabet";
|
|
3
|
+
import { contentChecksum } from "./hashline/hasher";
|
|
3
4
|
|
|
4
5
|
const SERVED_DIFF_ROW_RE = new RegExp(`^[+ ](${HASH_CLASS})│`);
|
|
5
6
|
|
|
@@ -12,60 +13,88 @@ export function servedHashesFromDiff(diff: string): string[] {
|
|
|
12
13
|
return hashes;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
export function
|
|
16
|
+
export function servedMapFromDiff(diff: string): Map<string, string> {
|
|
17
|
+
const map = new Map<string, string>();
|
|
18
|
+
for (const line of diff.split("\n")) {
|
|
19
|
+
const match = SERVED_DIFF_ROW_RE.exec(line);
|
|
20
|
+
if (!match) continue;
|
|
21
|
+
const hash = match[1]!;
|
|
22
|
+
const content = line.slice(match[0].length);
|
|
23
|
+
map.set(hash, contentChecksum(content));
|
|
24
|
+
}
|
|
25
|
+
return map;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function buildServedMap(fileHashes: string[], fileLines: string[], wantedHashes: string[]): Map<string, string> {
|
|
29
|
+
const index = new Map<string, number>();
|
|
30
|
+
for (let i = 0; i < fileHashes.length; i++) index.set(fileHashes[i]!, i);
|
|
31
|
+
const map = new Map<string, string>();
|
|
32
|
+
for (const h of wantedHashes) {
|
|
33
|
+
const idx = index.get(h);
|
|
34
|
+
if (idx !== undefined) map.set(h, contentChecksum(fileLines[idx]!));
|
|
35
|
+
}
|
|
36
|
+
return map;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getServed(store: HashStore, path: string): Map<string, string> | undefined {
|
|
16
40
|
const row = store.stmts.servedGet(path);
|
|
17
|
-
const parsed =
|
|
41
|
+
const parsed = parseStoredServed(row, () => store.stmts.servedDelete(path));
|
|
18
42
|
if (!parsed) return undefined;
|
|
19
|
-
return
|
|
43
|
+
return parsed;
|
|
20
44
|
}
|
|
21
45
|
|
|
22
46
|
function computeUpdate(
|
|
23
47
|
store: HashStore,
|
|
24
48
|
path: string,
|
|
25
|
-
|
|
49
|
+
entries: Map<string, string>,
|
|
26
50
|
scope?: ReadonlySet<string>,
|
|
27
|
-
):
|
|
51
|
+
): Map<string, string> | undefined {
|
|
28
52
|
const existing = getServed(store, path);
|
|
29
|
-
if (!existing &&
|
|
30
|
-
const
|
|
53
|
+
if (!existing && entries.size === 0 && !scope) return undefined;
|
|
54
|
+
const map = existing ? new Map(existing) : new Map<string, string>();
|
|
31
55
|
let changed = false;
|
|
32
56
|
if (scope) {
|
|
33
|
-
for (const hash of
|
|
57
|
+
for (const hash of [...map.keys()]) {
|
|
34
58
|
if (!scope.has(hash)) {
|
|
35
|
-
|
|
59
|
+
map.delete(hash);
|
|
36
60
|
changed = true;
|
|
37
61
|
}
|
|
38
62
|
}
|
|
39
63
|
}
|
|
40
|
-
for (const hash of
|
|
41
|
-
|
|
42
|
-
|
|
64
|
+
for (const [hash, content] of entries) {
|
|
65
|
+
const prev = map.get(hash);
|
|
66
|
+
if (prev !== content) {
|
|
67
|
+
map.set(hash, content);
|
|
43
68
|
changed = true;
|
|
44
69
|
}
|
|
45
70
|
}
|
|
46
|
-
if (!changed) return undefined;
|
|
47
|
-
return
|
|
71
|
+
if (!changed && existing) return undefined;
|
|
72
|
+
if (map.size === 0 && (!existing || existing.size === 0)) return undefined;
|
|
73
|
+
if (!changed) return existing;
|
|
74
|
+
return map;
|
|
48
75
|
}
|
|
49
76
|
|
|
50
77
|
export function recordServed(
|
|
51
78
|
store: HashStore,
|
|
52
79
|
path: string,
|
|
53
|
-
|
|
80
|
+
entries: Map<string, string>,
|
|
54
81
|
scope?: ReadonlySet<string>,
|
|
55
82
|
): void {
|
|
56
83
|
try {
|
|
57
84
|
withStore(() => {
|
|
58
|
-
const
|
|
59
|
-
if (!
|
|
60
|
-
|
|
85
|
+
const map = computeUpdate(store, path, entries, scope);
|
|
86
|
+
if (!map) return;
|
|
87
|
+
const obj = Object.fromEntries(map);
|
|
88
|
+
store.stmts.servedUpsert(path, JSON.stringify(obj), Date.now());
|
|
61
89
|
});
|
|
62
90
|
return;
|
|
63
91
|
} catch (error) {
|
|
64
92
|
if (!(error instanceof Error && error.message === STORE_NOT_OPEN_MESSAGE)) throw error;
|
|
65
93
|
}
|
|
66
|
-
const
|
|
67
|
-
if (!
|
|
68
|
-
|
|
94
|
+
const map = computeUpdate(store, path, entries, scope);
|
|
95
|
+
if (!map) return;
|
|
96
|
+
const obj = Object.fromEntries(map);
|
|
97
|
+
store.stmts.servedUpsert(path, JSON.stringify(obj), Date.now());
|
|
69
98
|
}
|
|
70
99
|
|
|
71
100
|
export function recordServedDiff(
|
|
@@ -74,7 +103,7 @@ export function recordServedDiff(
|
|
|
74
103
|
diff: string,
|
|
75
104
|
scope?: ReadonlySet<string>,
|
|
76
105
|
): void {
|
|
77
|
-
recordServed(store, path,
|
|
106
|
+
recordServed(store, path, servedMapFromDiff(diff), scope);
|
|
78
107
|
}
|
|
79
108
|
|
|
80
109
|
export function clearServed(store: HashStore, path: string): void {
|
|
@@ -83,14 +112,14 @@ export function clearServed(store: HashStore, path: string): void {
|
|
|
83
112
|
|
|
84
113
|
export async function recordServedSafe(
|
|
85
114
|
path: string,
|
|
86
|
-
|
|
115
|
+
entries: Map<string, string>,
|
|
87
116
|
context: string,
|
|
88
117
|
scope?: ReadonlySet<string>,
|
|
89
118
|
): Promise<void> {
|
|
90
|
-
if (
|
|
119
|
+
if (entries.size === 0 && !scope) return;
|
|
91
120
|
try {
|
|
92
121
|
const store = await loadHashStore();
|
|
93
|
-
recordServed(store, path,
|
|
122
|
+
recordServed(store, path, entries, scope);
|
|
94
123
|
} catch (error) {
|
|
95
124
|
console.error(`Failed to record served state (${context}):`, error);
|
|
96
125
|
}
|
|
@@ -103,5 +132,5 @@ export async function recordServedDiffSafe(
|
|
|
103
132
|
scope?: ReadonlySet<string>,
|
|
104
133
|
): Promise<void> {
|
|
105
134
|
if (!diff) return;
|
|
106
|
-
await recordServedSafe(path,
|
|
135
|
+
await recordServedSafe(path, servedMapFromDiff(diff), context, scope);
|
|
107
136
|
}
|
package/src/write-hook.ts
CHANGED
|
@@ -8,19 +8,19 @@ import { abortIf, splitLines, isRec, normalizeFilePath } from "./utils";
|
|
|
8
8
|
|
|
9
9
|
const HASH_ECHO_RE = new RegExp(`^(${HASH_CLASS})${HASH_SEP}`);
|
|
10
10
|
|
|
11
|
-
function searchEcho(lines: string[], served: ReadonlySet<string>): { line: number; hash: string } | undefined {
|
|
11
|
+
function searchEcho(lines: string[], served: ReadonlyMap<string, string> | ReadonlySet<string>): { line: number; hash: string } | undefined {
|
|
12
12
|
for (let i = 0; i < lines.length; i++) {
|
|
13
13
|
const match = HASH_ECHO_RE.exec(lines[i]!);
|
|
14
|
-
if (match && served.has(match[1]!)) return { line: i + 1, hash: match[1]! };
|
|
14
|
+
if (match && served.has(match[1]! as never)) return { line: i + 1, hash: match[1]! };
|
|
15
15
|
}
|
|
16
16
|
return undefined;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export function findServedHashEcho(content: string, served: ReadonlySet<string>): { line: number; hash: string } | undefined {
|
|
19
|
+
export function findServedHashEcho(content: string, served: ReadonlyMap<string, string> | ReadonlySet<string>): { line: number; hash: string } | undefined {
|
|
20
20
|
return searchEcho(splitLines(content), served);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export function findEditHashEcho(lines: string[], served: ReadonlySet<string>): { line: number; hash: string } | undefined {
|
|
23
|
+
export function findEditHashEcho(lines: string[], served: ReadonlyMap<string, string> | ReadonlySet<string>): { line: number; hash: string } | undefined {
|
|
24
24
|
return searchEcho(lines, served);
|
|
25
25
|
}
|
|
26
26
|
|