gsd-pi 2.10.0 → 2.10.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/node_modules/@gsd/native/dist/ast/index.d.ts +4 -0
- package/node_modules/@gsd/native/dist/ast/index.js +7 -0
- package/node_modules/@gsd/native/dist/ast/types.d.ts +69 -0
- package/node_modules/@gsd/native/dist/ast/types.js +1 -0
- package/node_modules/@gsd/native/{src/clipboard/index.ts → dist/clipboard/index.d.ts} +3 -15
- package/node_modules/@gsd/native/dist/clipboard/index.js +33 -0
- package/node_modules/@gsd/native/dist/clipboard/types.d.ts +7 -0
- package/node_modules/@gsd/native/dist/clipboard/types.js +1 -0
- package/node_modules/@gsd/native/dist/diff/index.d.ts +33 -0
- package/node_modules/@gsd/native/dist/diff/index.js +38 -0
- package/node_modules/@gsd/native/dist/diff/types.d.ts +23 -0
- package/node_modules/@gsd/native/dist/diff/types.js +1 -0
- package/node_modules/@gsd/native/{src/fd/index.ts → dist/fd/index.d.ts} +2 -12
- package/node_modules/@gsd/native/dist/fd/index.js +26 -0
- package/node_modules/@gsd/native/dist/fd/types.d.ts +29 -0
- package/node_modules/@gsd/native/dist/fd/types.js +1 -0
- package/node_modules/@gsd/native/{src/glob/index.ts → dist/glob/index.d.ts} +3 -19
- package/node_modules/@gsd/native/dist/glob/index.js +31 -0
- package/node_modules/@gsd/native/dist/glob/types.d.ts +50 -0
- package/node_modules/@gsd/native/dist/glob/types.js +1 -0
- package/node_modules/@gsd/native/dist/grep/index.d.ts +20 -0
- package/node_modules/@gsd/native/dist/grep/index.js +23 -0
- package/node_modules/@gsd/native/dist/grep/types.d.ts +99 -0
- package/node_modules/@gsd/native/dist/grep/types.js +1 -0
- package/node_modules/@gsd/native/dist/gsd-parser/index.d.ts +45 -0
- package/node_modules/@gsd/native/dist/gsd-parser/index.js +54 -0
- package/node_modules/@gsd/native/dist/gsd-parser/types.d.ts +55 -0
- package/node_modules/@gsd/native/dist/gsd-parser/types.js +7 -0
- package/node_modules/@gsd/native/{src/highlight/index.ts → dist/highlight/index.d.ts} +3 -19
- package/node_modules/@gsd/native/dist/highlight/index.js +33 -0
- package/node_modules/@gsd/native/dist/highlight/types.d.ts +25 -0
- package/node_modules/@gsd/native/dist/highlight/types.js +1 -0
- package/node_modules/@gsd/native/{src/html/index.ts → dist/html/index.d.ts} +1 -10
- package/node_modules/@gsd/native/dist/html/index.js +16 -0
- package/node_modules/@gsd/native/dist/html/types.d.ts +7 -0
- package/node_modules/@gsd/native/dist/html/types.js +1 -0
- package/node_modules/@gsd/native/{src/image/index.ts → dist/image/index.d.ts} +1 -14
- package/node_modules/@gsd/native/dist/image/index.js +18 -0
- package/node_modules/@gsd/native/dist/image/types.d.ts +35 -0
- package/node_modules/@gsd/native/dist/image/types.js +26 -0
- package/node_modules/@gsd/native/{src/index.ts → dist/index.d.ts} +12 -60
- package/node_modules/@gsd/native/dist/index.js +28 -0
- package/node_modules/@gsd/native/dist/native.d.ts +44 -0
- package/node_modules/@gsd/native/dist/native.js +34 -0
- package/node_modules/@gsd/native/dist/ps/index.d.ts +38 -0
- package/node_modules/@gsd/native/{src/ps/index.ts → dist/ps/index.js} +8 -13
- package/node_modules/@gsd/native/{src/ps/types.ts → dist/ps/types.d.ts} +2 -2
- package/node_modules/@gsd/native/dist/ps/types.js +1 -0
- package/node_modules/@gsd/native/{src/text/index.ts → dist/text/index.d.ts} +6 -76
- package/node_modules/@gsd/native/dist/text/index.js +66 -0
- package/node_modules/@gsd/native/dist/text/types.d.ts +27 -0
- package/node_modules/@gsd/native/dist/text/types.js +10 -0
- package/node_modules/@gsd/native/{src/ttsr/index.ts → dist/ttsr/index.d.ts} +3 -15
- package/node_modules/@gsd/native/dist/ttsr/index.js +32 -0
- package/node_modules/@gsd/native/{src/ttsr/types.ts → dist/ttsr/types.d.ts} +4 -5
- package/node_modules/@gsd/native/dist/ttsr/types.js +1 -0
- package/node_modules/@gsd/native/package.json +24 -23
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/edit-diff.d.ts +11 -5
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/edit-diff.js +19 -142
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/edit-diff.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/src/core/tools/edit-diff.ts +23 -157
- package/package.json +4 -2
- package/packages/native/dist/ast/index.d.ts +4 -0
- package/packages/native/dist/ast/index.js +7 -0
- package/packages/native/dist/ast/types.d.ts +69 -0
- package/packages/native/dist/ast/types.js +1 -0
- package/packages/native/dist/clipboard/index.d.ts +28 -0
- package/packages/native/dist/clipboard/index.js +33 -0
- package/packages/native/dist/clipboard/types.d.ts +7 -0
- package/packages/native/dist/clipboard/types.js +1 -0
- package/packages/native/dist/diff/index.d.ts +33 -0
- package/packages/native/dist/diff/index.js +38 -0
- package/packages/native/dist/diff/types.d.ts +23 -0
- package/packages/native/dist/diff/types.js +1 -0
- package/packages/native/dist/fd/index.d.ts +25 -0
- package/packages/native/dist/fd/index.js +26 -0
- package/packages/native/dist/fd/types.d.ts +29 -0
- package/packages/native/dist/fd/types.js +1 -0
- package/packages/native/dist/glob/index.d.ts +28 -0
- package/packages/native/dist/glob/index.js +31 -0
- package/packages/native/dist/glob/types.d.ts +50 -0
- package/packages/native/dist/glob/types.js +1 -0
- package/packages/native/dist/grep/index.d.ts +20 -0
- package/packages/native/dist/grep/index.js +23 -0
- package/packages/native/dist/grep/types.d.ts +99 -0
- package/packages/native/dist/grep/types.js +1 -0
- package/packages/native/dist/gsd-parser/index.d.ts +45 -0
- package/packages/native/dist/gsd-parser/index.js +54 -0
- package/packages/native/dist/gsd-parser/types.d.ts +55 -0
- package/packages/native/dist/gsd-parser/types.js +7 -0
- package/packages/native/dist/highlight/index.d.ts +28 -0
- package/packages/native/dist/highlight/index.js +33 -0
- package/packages/native/dist/highlight/types.d.ts +25 -0
- package/packages/native/dist/highlight/types.js +1 -0
- package/packages/native/dist/html/index.d.ts +15 -0
- package/packages/native/dist/html/index.js +16 -0
- package/packages/native/dist/html/types.d.ts +7 -0
- package/packages/native/dist/html/types.js +1 -0
- package/packages/native/dist/image/index.d.ts +15 -0
- package/packages/native/dist/image/index.js +18 -0
- package/packages/native/dist/image/types.d.ts +35 -0
- package/packages/native/dist/image/types.js +26 -0
- package/packages/native/dist/index.d.ts +40 -0
- package/packages/native/dist/index.js +28 -0
- package/packages/native/dist/native.d.ts +44 -0
- package/packages/native/dist/native.js +34 -0
- package/packages/native/dist/ps/index.d.ts +38 -0
- package/packages/native/dist/ps/index.js +47 -0
- package/packages/native/dist/ps/types.d.ts +5 -0
- package/packages/native/dist/ps/types.js +1 -0
- package/packages/native/dist/text/index.d.ts +55 -0
- package/packages/native/dist/text/index.js +66 -0
- package/packages/native/dist/text/types.d.ts +27 -0
- package/packages/native/dist/text/types.js +10 -0
- package/packages/native/dist/ttsr/index.d.ts +27 -0
- package/packages/native/dist/ttsr/index.js +32 -0
- package/packages/native/dist/ttsr/types.d.ts +9 -0
- package/packages/native/dist/ttsr/types.js +1 -0
- package/packages/native/package.json +24 -23
- package/packages/native/src/__tests__/diff.test.mjs +189 -0
- package/packages/native/src/__tests__/ttsr.test.mjs +135 -0
- package/packages/native/src/diff/index.ts +61 -0
- package/packages/native/src/diff/types.ts +24 -0
- package/packages/native/src/gsd-parser/index.ts +98 -0
- package/packages/native/src/gsd-parser/types.ts +62 -0
- package/packages/native/src/index.ts +23 -0
- package/packages/native/src/native.ts +8 -0
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts +11 -5
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.js +19 -142
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.js.map +1 -1
- package/packages/pi-coding-agent/src/core/tools/edit-diff.ts +23 -157
- package/src/resources/extensions/gsd/files.ts +9 -0
- package/src/resources/extensions/gsd/native-parser-bridge.ts +135 -0
- package/src/resources/extensions/ttsr/ttsr-manager.ts +86 -0
- package/node_modules/@gsd/native/src/__tests__/clipboard.test.mjs +0 -79
- package/node_modules/@gsd/native/src/__tests__/fd.test.mjs +0 -164
- package/node_modules/@gsd/native/src/__tests__/glob.test.mjs +0 -237
- package/node_modules/@gsd/native/src/__tests__/grep.test.mjs +0 -162
- package/node_modules/@gsd/native/src/__tests__/highlight.test.mjs +0 -156
- package/node_modules/@gsd/native/src/__tests__/html.test.mjs +0 -98
- package/node_modules/@gsd/native/src/__tests__/image.test.mjs +0 -137
- package/node_modules/@gsd/native/src/__tests__/ps.test.mjs +0 -109
- package/node_modules/@gsd/native/src/__tests__/text.test.mjs +0 -262
- package/node_modules/@gsd/native/src/ast/index.ts +0 -12
- package/node_modules/@gsd/native/src/ast/types.ts +0 -75
- package/node_modules/@gsd/native/src/clipboard/types.ts +0 -7
- package/node_modules/@gsd/native/src/fd/types.ts +0 -31
- package/node_modules/@gsd/native/src/glob/types.ts +0 -53
- package/node_modules/@gsd/native/src/grep/index.ts +0 -48
- package/node_modules/@gsd/native/src/grep/types.ts +0 -105
- package/node_modules/@gsd/native/src/highlight/types.ts +0 -25
- package/node_modules/@gsd/native/src/html/types.ts +0 -7
- package/node_modules/@gsd/native/src/image/types.ts +0 -41
- package/node_modules/@gsd/native/src/native.ts +0 -94
- package/node_modules/@gsd/native/src/text/types.ts +0 -29
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared diff computation utilities for the edit tool.
|
|
3
3
|
* Used by both edit.ts (for execution) and tool-execution.ts (for preview rendering).
|
|
4
|
+
*
|
|
5
|
+
* Hot-path functions (fuzzyFindText, normalizeForFuzzyMatch, generateDiffString)
|
|
6
|
+
* delegate to the native Rust engine for performance on large files.
|
|
4
7
|
*/
|
|
5
|
-
import
|
|
8
|
+
import { fuzzyFindText as nativeFuzzyFindText, generateDiff as nativeGenerateDiff, normalizeForFuzzyMatch as nativeNormalizeForFuzzyMatch, } from "@gsd/native";
|
|
6
9
|
import { constants } from "fs";
|
|
7
10
|
import { access, readFile } from "fs/promises";
|
|
8
11
|
import { resolveToCwd } from "./path-utils.js";
|
|
@@ -22,167 +25,41 @@ export function restoreLineEndings(text, ending) {
|
|
|
22
25
|
return ending === "\r\n" ? text.replace(/\n/g, "\r\n") : text;
|
|
23
26
|
}
|
|
24
27
|
/**
|
|
25
|
-
* Normalize text for fuzzy matching
|
|
28
|
+
* Normalize text for fuzzy matching (native Rust implementation).
|
|
26
29
|
* - Strip trailing whitespace from each line
|
|
27
30
|
* - Normalize smart quotes to ASCII equivalents
|
|
28
31
|
* - Normalize Unicode dashes/hyphens to ASCII hyphen
|
|
29
32
|
* - Normalize special Unicode spaces to regular space
|
|
30
33
|
*/
|
|
31
34
|
export function normalizeForFuzzyMatch(text) {
|
|
32
|
-
return (text
|
|
33
|
-
// Strip trailing whitespace per line
|
|
34
|
-
.split("\n")
|
|
35
|
-
.map((line) => line.trimEnd())
|
|
36
|
-
.join("\n")
|
|
37
|
-
// Smart single quotes → '
|
|
38
|
-
.replace(/[\u2018\u2019\u201A\u201B]/g, "'")
|
|
39
|
-
// Smart double quotes → "
|
|
40
|
-
.replace(/[\u201C\u201D\u201E\u201F]/g, '"')
|
|
41
|
-
// Various dashes/hyphens → -
|
|
42
|
-
// U+2010 hyphen, U+2011 non-breaking hyphen, U+2012 figure dash,
|
|
43
|
-
// U+2013 en-dash, U+2014 em-dash, U+2015 horizontal bar, U+2212 minus
|
|
44
|
-
.replace(/[\u2010\u2011\u2012\u2013\u2014\u2015\u2212]/g, "-")
|
|
45
|
-
// Special spaces → regular space
|
|
46
|
-
// U+00A0 NBSP, U+2002-U+200A various spaces, U+202F narrow NBSP,
|
|
47
|
-
// U+205F medium math space, U+3000 ideographic space
|
|
48
|
-
.replace(/[\u00A0\u2002-\u200A\u202F\u205F\u3000]/g, " "));
|
|
35
|
+
return nativeNormalizeForFuzzyMatch(text);
|
|
49
36
|
}
|
|
50
37
|
/**
|
|
51
|
-
* Find oldText in content, trying exact match first, then fuzzy match
|
|
38
|
+
* Find oldText in content, trying exact match first, then fuzzy match
|
|
39
|
+
* (native Rust implementation).
|
|
40
|
+
*
|
|
52
41
|
* When fuzzy matching is used, the returned contentForReplacement is the
|
|
53
|
-
* fuzzy-normalized version of the content
|
|
54
|
-
* Unicode quotes/dashes normalized to ASCII).
|
|
42
|
+
* fuzzy-normalized version of the content.
|
|
55
43
|
*/
|
|
56
44
|
export function fuzzyFindText(content, oldText) {
|
|
57
|
-
|
|
58
|
-
const exactIndex = content.indexOf(oldText);
|
|
59
|
-
if (exactIndex !== -1) {
|
|
60
|
-
return {
|
|
61
|
-
found: true,
|
|
62
|
-
index: exactIndex,
|
|
63
|
-
matchLength: oldText.length,
|
|
64
|
-
usedFuzzyMatch: false,
|
|
65
|
-
contentForReplacement: content,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
// Try fuzzy match - work entirely in normalized space
|
|
69
|
-
const fuzzyContent = normalizeForFuzzyMatch(content);
|
|
70
|
-
const fuzzyOldText = normalizeForFuzzyMatch(oldText);
|
|
71
|
-
const fuzzyIndex = fuzzyContent.indexOf(fuzzyOldText);
|
|
72
|
-
if (fuzzyIndex === -1) {
|
|
73
|
-
return {
|
|
74
|
-
found: false,
|
|
75
|
-
index: -1,
|
|
76
|
-
matchLength: 0,
|
|
77
|
-
usedFuzzyMatch: false,
|
|
78
|
-
contentForReplacement: content,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
// When fuzzy matching, we work in the normalized space for replacement.
|
|
82
|
-
// This means the output will have normalized whitespace/quotes/dashes,
|
|
83
|
-
// which is acceptable since we're fixing minor formatting differences anyway.
|
|
84
|
-
return {
|
|
85
|
-
found: true,
|
|
86
|
-
index: fuzzyIndex,
|
|
87
|
-
matchLength: fuzzyOldText.length,
|
|
88
|
-
usedFuzzyMatch: true,
|
|
89
|
-
contentForReplacement: fuzzyContent,
|
|
90
|
-
};
|
|
45
|
+
return nativeFuzzyFindText(content, oldText);
|
|
91
46
|
}
|
|
92
47
|
/** Strip UTF-8 BOM if present, return both the BOM (if any) and the text without it */
|
|
93
48
|
export function stripBom(content) {
|
|
94
49
|
return content.startsWith("\uFEFF") ? { bom: "\uFEFF", text: content.slice(1) } : { bom: "", text: content };
|
|
95
50
|
}
|
|
96
51
|
/**
|
|
97
|
-
* Generate a unified diff string with line numbers and context
|
|
52
|
+
* Generate a unified diff string with line numbers and context
|
|
53
|
+
* (native Rust implementation using Myers' algorithm via the `similar` crate).
|
|
54
|
+
*
|
|
98
55
|
* Returns both the diff string and the first changed line number (in the new file).
|
|
99
56
|
*/
|
|
100
57
|
export function generateDiffString(oldContent, newContent, contextLines = 4) {
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const lineNumWidth = String(maxLineNum).length;
|
|
107
|
-
let oldLineNum = 1;
|
|
108
|
-
let newLineNum = 1;
|
|
109
|
-
let lastWasChange = false;
|
|
110
|
-
let firstChangedLine;
|
|
111
|
-
for (let i = 0; i < parts.length; i++) {
|
|
112
|
-
const part = parts[i];
|
|
113
|
-
const raw = part.value.split("\n");
|
|
114
|
-
if (raw[raw.length - 1] === "") {
|
|
115
|
-
raw.pop();
|
|
116
|
-
}
|
|
117
|
-
if (part.added || part.removed) {
|
|
118
|
-
// Capture the first changed line (in the new file)
|
|
119
|
-
if (firstChangedLine === undefined) {
|
|
120
|
-
firstChangedLine = newLineNum;
|
|
121
|
-
}
|
|
122
|
-
// Show the change
|
|
123
|
-
for (const line of raw) {
|
|
124
|
-
if (part.added) {
|
|
125
|
-
const lineNum = String(newLineNum).padStart(lineNumWidth, " ");
|
|
126
|
-
output.push(`+${lineNum} ${line}`);
|
|
127
|
-
newLineNum++;
|
|
128
|
-
}
|
|
129
|
-
else {
|
|
130
|
-
// removed
|
|
131
|
-
const lineNum = String(oldLineNum).padStart(lineNumWidth, " ");
|
|
132
|
-
output.push(`-${lineNum} ${line}`);
|
|
133
|
-
oldLineNum++;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
lastWasChange = true;
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
// Context lines - only show a few before/after changes
|
|
140
|
-
const nextPartIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed);
|
|
141
|
-
if (lastWasChange || nextPartIsChange) {
|
|
142
|
-
// Show context
|
|
143
|
-
let linesToShow = raw;
|
|
144
|
-
let skipStart = 0;
|
|
145
|
-
let skipEnd = 0;
|
|
146
|
-
if (!lastWasChange) {
|
|
147
|
-
// Show only last N lines as leading context
|
|
148
|
-
skipStart = Math.max(0, raw.length - contextLines);
|
|
149
|
-
linesToShow = raw.slice(skipStart);
|
|
150
|
-
}
|
|
151
|
-
if (!nextPartIsChange && linesToShow.length > contextLines) {
|
|
152
|
-
// Show only first N lines as trailing context
|
|
153
|
-
skipEnd = linesToShow.length - contextLines;
|
|
154
|
-
linesToShow = linesToShow.slice(0, contextLines);
|
|
155
|
-
}
|
|
156
|
-
// Add ellipsis if we skipped lines at start
|
|
157
|
-
if (skipStart > 0) {
|
|
158
|
-
output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
|
|
159
|
-
// Update line numbers for the skipped leading context
|
|
160
|
-
oldLineNum += skipStart;
|
|
161
|
-
newLineNum += skipStart;
|
|
162
|
-
}
|
|
163
|
-
for (const line of linesToShow) {
|
|
164
|
-
const lineNum = String(oldLineNum).padStart(lineNumWidth, " ");
|
|
165
|
-
output.push(` ${lineNum} ${line}`);
|
|
166
|
-
oldLineNum++;
|
|
167
|
-
newLineNum++;
|
|
168
|
-
}
|
|
169
|
-
// Add ellipsis if we skipped lines at end
|
|
170
|
-
if (skipEnd > 0) {
|
|
171
|
-
output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
|
|
172
|
-
// Update line numbers for the skipped trailing context
|
|
173
|
-
oldLineNum += skipEnd;
|
|
174
|
-
newLineNum += skipEnd;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
else {
|
|
178
|
-
// Skip these context lines entirely
|
|
179
|
-
oldLineNum += raw.length;
|
|
180
|
-
newLineNum += raw.length;
|
|
181
|
-
}
|
|
182
|
-
lastWasChange = false;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
return { diff: output.join("\n"), firstChangedLine };
|
|
58
|
+
const result = nativeGenerateDiff(oldContent, newContent, contextLines);
|
|
59
|
+
return {
|
|
60
|
+
diff: result.diff,
|
|
61
|
+
firstChangedLine: result.firstChangedLine ?? undefined,
|
|
62
|
+
};
|
|
186
63
|
}
|
|
187
64
|
/**
|
|
188
65
|
* Compute the diff for an edit operation without applying it.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"edit-diff.js","sourceRoot":"","sources":["../../../src/core/tools/edit-diff.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared diff computation utilities for the edit tool.\n * Used by both edit.ts (for execution) and tool-execution.ts (for preview rendering).\n */\n\nimport * as Diff from \"diff\";\nimport { constants } from \"fs\";\nimport { access, readFile } from \"fs/promises\";\nimport { resolveToCwd } from \"./path-utils.js\";\n\nexport function detectLineEnding(content: string): \"\\r\\n\" | \"\\n\" {\n\tconst crlfIdx = content.indexOf(\"\\r\\n\");\n\tconst lfIdx = content.indexOf(\"\\n\");\n\tif (lfIdx === -1) return \"\\n\";\n\tif (crlfIdx === -1) return \"\\n\";\n\treturn crlfIdx < lfIdx ? \"\\r\\n\" : \"\\n\";\n}\n\nexport function normalizeToLF(text: string): string {\n\treturn text.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n}\n\nexport function restoreLineEndings(text: string, ending: \"\\r\\n\" | \"\\n\"): string {\n\treturn ending === \"\\r\\n\" ? text.replace(/\\n/g, \"\\r\\n\") : text;\n}\n\n/**\n * Normalize text for fuzzy matching. Applies progressive transformations:\n * - Strip trailing whitespace from each line\n * - Normalize smart quotes to ASCII equivalents\n * - Normalize Unicode dashes/hyphens to ASCII hyphen\n * - Normalize special Unicode spaces to regular space\n */\nexport function normalizeForFuzzyMatch(text: string): string {\n\treturn (\n\t\ttext\n\t\t\t// Strip trailing whitespace per line\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => line.trimEnd())\n\t\t\t.join(\"\\n\")\n\t\t\t// Smart single quotes → '\n\t\t\t.replace(/[\\u2018\\u2019\\u201A\\u201B]/g, \"'\")\n\t\t\t// Smart double quotes → \"\n\t\t\t.replace(/[\\u201C\\u201D\\u201E\\u201F]/g, '\"')\n\t\t\t// Various dashes/hyphens → -\n\t\t\t// U+2010 hyphen, U+2011 non-breaking hyphen, U+2012 figure dash,\n\t\t\t// U+2013 en-dash, U+2014 em-dash, U+2015 horizontal bar, U+2212 minus\n\t\t\t.replace(/[\\u2010\\u2011\\u2012\\u2013\\u2014\\u2015\\u2212]/g, \"-\")\n\t\t\t// Special spaces → regular space\n\t\t\t// U+00A0 NBSP, U+2002-U+200A various spaces, U+202F narrow NBSP,\n\t\t\t// U+205F medium math space, U+3000 ideographic space\n\t\t\t.replace(/[\\u00A0\\u2002-\\u200A\\u202F\\u205F\\u3000]/g, \" \")\n\t);\n}\n\nexport interface FuzzyMatchResult {\n\t/** Whether a match was found */\n\tfound: boolean;\n\t/** The index where the match starts (in the content that should be used for replacement) */\n\tindex: number;\n\t/** Length of the matched text */\n\tmatchLength: number;\n\t/** Whether fuzzy matching was used (false = exact match) */\n\tusedFuzzyMatch: boolean;\n\t/**\n\t * The content to use for replacement operations.\n\t * When exact match: original content. When fuzzy match: normalized content.\n\t */\n\tcontentForReplacement: string;\n}\n\n/**\n * Find oldText in content, trying exact match first, then fuzzy match.\n * When fuzzy matching is used, the returned contentForReplacement is the\n * fuzzy-normalized version of the content (trailing whitespace stripped,\n * Unicode quotes/dashes normalized to ASCII).\n */\nexport function fuzzyFindText(content: string, oldText: string): FuzzyMatchResult {\n\t// Try exact match first\n\tconst exactIndex = content.indexOf(oldText);\n\tif (exactIndex !== -1) {\n\t\treturn {\n\t\t\tfound: true,\n\t\t\tindex: exactIndex,\n\t\t\tmatchLength: oldText.length,\n\t\t\tusedFuzzyMatch: false,\n\t\t\tcontentForReplacement: content,\n\t\t};\n\t}\n\n\t// Try fuzzy match - work entirely in normalized space\n\tconst fuzzyContent = normalizeForFuzzyMatch(content);\n\tconst fuzzyOldText = normalizeForFuzzyMatch(oldText);\n\tconst fuzzyIndex = fuzzyContent.indexOf(fuzzyOldText);\n\n\tif (fuzzyIndex === -1) {\n\t\treturn {\n\t\t\tfound: false,\n\t\t\tindex: -1,\n\t\t\tmatchLength: 0,\n\t\t\tusedFuzzyMatch: false,\n\t\t\tcontentForReplacement: content,\n\t\t};\n\t}\n\n\t// When fuzzy matching, we work in the normalized space for replacement.\n\t// This means the output will have normalized whitespace/quotes/dashes,\n\t// which is acceptable since we're fixing minor formatting differences anyway.\n\treturn {\n\t\tfound: true,\n\t\tindex: fuzzyIndex,\n\t\tmatchLength: fuzzyOldText.length,\n\t\tusedFuzzyMatch: true,\n\t\tcontentForReplacement: fuzzyContent,\n\t};\n}\n\n/** Strip UTF-8 BOM if present, return both the BOM (if any) and the text without it */\nexport function stripBom(content: string): { bom: string; text: string } {\n\treturn content.startsWith(\"\\uFEFF\") ? { bom: \"\\uFEFF\", text: content.slice(1) } : { bom: \"\", text: content };\n}\n\n/**\n * Generate a unified diff string with line numbers and context.\n * Returns both the diff string and the first changed line number (in the new file).\n */\nexport function generateDiffString(\n\toldContent: string,\n\tnewContent: string,\n\tcontextLines = 4,\n): { diff: string; firstChangedLine: number | undefined } {\n\tconst parts = Diff.diffLines(oldContent, newContent);\n\tconst output: string[] = [];\n\n\tconst oldLines = oldContent.split(\"\\n\");\n\tconst newLines = newContent.split(\"\\n\");\n\tconst maxLineNum = Math.max(oldLines.length, newLines.length);\n\tconst lineNumWidth = String(maxLineNum).length;\n\n\tlet oldLineNum = 1;\n\tlet newLineNum = 1;\n\tlet lastWasChange = false;\n\tlet firstChangedLine: number | undefined;\n\n\tfor (let i = 0; i < parts.length; i++) {\n\t\tconst part = parts[i];\n\t\tconst raw = part.value.split(\"\\n\");\n\t\tif (raw[raw.length - 1] === \"\") {\n\t\t\traw.pop();\n\t\t}\n\n\t\tif (part.added || part.removed) {\n\t\t\t// Capture the first changed line (in the new file)\n\t\t\tif (firstChangedLine === undefined) {\n\t\t\t\tfirstChangedLine = newLineNum;\n\t\t\t}\n\n\t\t\t// Show the change\n\t\t\tfor (const line of raw) {\n\t\t\t\tif (part.added) {\n\t\t\t\t\tconst lineNum = String(newLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\toutput.push(`+${lineNum} ${line}`);\n\t\t\t\t\tnewLineNum++;\n\t\t\t\t} else {\n\t\t\t\t\t// removed\n\t\t\t\t\tconst lineNum = String(oldLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\toutput.push(`-${lineNum} ${line}`);\n\t\t\t\t\toldLineNum++;\n\t\t\t\t}\n\t\t\t}\n\t\t\tlastWasChange = true;\n\t\t} else {\n\t\t\t// Context lines - only show a few before/after changes\n\t\t\tconst nextPartIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed);\n\n\t\t\tif (lastWasChange || nextPartIsChange) {\n\t\t\t\t// Show context\n\t\t\t\tlet linesToShow = raw;\n\t\t\t\tlet skipStart = 0;\n\t\t\t\tlet skipEnd = 0;\n\n\t\t\t\tif (!lastWasChange) {\n\t\t\t\t\t// Show only last N lines as leading context\n\t\t\t\t\tskipStart = Math.max(0, raw.length - contextLines);\n\t\t\t\t\tlinesToShow = raw.slice(skipStart);\n\t\t\t\t}\n\n\t\t\t\tif (!nextPartIsChange && linesToShow.length > contextLines) {\n\t\t\t\t\t// Show only first N lines as trailing context\n\t\t\t\t\tskipEnd = linesToShow.length - contextLines;\n\t\t\t\t\tlinesToShow = linesToShow.slice(0, contextLines);\n\t\t\t\t}\n\n\t\t\t\t// Add ellipsis if we skipped lines at start\n\t\t\t\tif (skipStart > 0) {\n\t\t\t\t\toutput.push(` ${\"\".padStart(lineNumWidth, \" \")} ...`);\n\t\t\t\t\t// Update line numbers for the skipped leading context\n\t\t\t\t\toldLineNum += skipStart;\n\t\t\t\t\tnewLineNum += skipStart;\n\t\t\t\t}\n\n\t\t\t\tfor (const line of linesToShow) {\n\t\t\t\t\tconst lineNum = String(oldLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\toutput.push(` ${lineNum} ${line}`);\n\t\t\t\t\toldLineNum++;\n\t\t\t\t\tnewLineNum++;\n\t\t\t\t}\n\n\t\t\t\t// Add ellipsis if we skipped lines at end\n\t\t\t\tif (skipEnd > 0) {\n\t\t\t\t\toutput.push(` ${\"\".padStart(lineNumWidth, \" \")} ...`);\n\t\t\t\t\t// Update line numbers for the skipped trailing context\n\t\t\t\t\toldLineNum += skipEnd;\n\t\t\t\t\tnewLineNum += skipEnd;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Skip these context lines entirely\n\t\t\t\toldLineNum += raw.length;\n\t\t\t\tnewLineNum += raw.length;\n\t\t\t}\n\n\t\t\tlastWasChange = false;\n\t\t}\n\t}\n\n\treturn { diff: output.join(\"\\n\"), firstChangedLine };\n}\n\nexport interface EditDiffResult {\n\tdiff: string;\n\tfirstChangedLine: number | undefined;\n}\n\nexport interface EditDiffError {\n\terror: string;\n}\n\n/**\n * Compute the diff for an edit operation without applying it.\n * Used for preview rendering in the TUI before the tool executes.\n */\nexport async function computeEditDiff(\n\tpath: string,\n\toldText: string,\n\tnewText: string,\n\tcwd: string,\n): Promise<EditDiffResult | EditDiffError> {\n\tconst absolutePath = resolveToCwd(path, cwd);\n\n\ttry {\n\t\t// Check if file exists and is readable\n\t\ttry {\n\t\t\tawait access(absolutePath, constants.R_OK);\n\t\t} catch {\n\t\t\treturn { error: `File not found: ${path}` };\n\t\t}\n\n\t\t// Read the file\n\t\tconst rawContent = await readFile(absolutePath, \"utf-8\");\n\n\t\t// Strip BOM before matching (LLM won't include invisible BOM in oldText)\n\t\tconst { text: content } = stripBom(rawContent);\n\n\t\tconst normalizedContent = normalizeToLF(content);\n\t\tconst normalizedOldText = normalizeToLF(oldText);\n\t\tconst normalizedNewText = normalizeToLF(newText);\n\n\t\t// Find the old text using fuzzy matching (tries exact match first, then fuzzy)\n\t\tconst matchResult = fuzzyFindText(normalizedContent, normalizedOldText);\n\n\t\tif (!matchResult.found) {\n\t\t\treturn {\n\t\t\t\terror: `Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`,\n\t\t\t};\n\t\t}\n\n\t\t// Count occurrences using fuzzy-normalized content for consistency\n\t\tconst fuzzyContent = normalizeForFuzzyMatch(normalizedContent);\n\t\tconst fuzzyOldText = normalizeForFuzzyMatch(normalizedOldText);\n\t\tconst occurrences = fuzzyContent.split(fuzzyOldText).length - 1;\n\n\t\tif (occurrences > 1) {\n\t\t\treturn {\n\t\t\t\terror: `Found ${occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique.`,\n\t\t\t};\n\t\t}\n\n\t\t// Compute the new content using the matched position\n\t\t// When fuzzy matching was used, contentForReplacement is the normalized version\n\t\tconst baseContent = matchResult.contentForReplacement;\n\t\tconst newContent =\n\t\t\tbaseContent.substring(0, matchResult.index) +\n\t\t\tnormalizedNewText +\n\t\t\tbaseContent.substring(matchResult.index + matchResult.matchLength);\n\n\t\t// Check if it would actually change anything\n\t\tif (baseContent === newContent) {\n\t\t\treturn {\n\t\t\t\terror: `No changes would be made to ${path}. The replacement produces identical content.`,\n\t\t\t};\n\t\t}\n\n\t\t// Generate the diff\n\t\treturn generateDiffString(baseContent, newContent);\n\t} catch (err) {\n\t\treturn { error: err instanceof Error ? err.message : String(err) };\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"edit-diff.js","sourceRoot":"","sources":["../../../src/core/tools/edit-diff.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EACN,aAAa,IAAI,mBAAmB,EACpC,YAAY,IAAI,kBAAkB,EAClC,sBAAsB,IAAI,4BAA4B,GACtD,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAC/B,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE/C,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC/C,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACxC,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,IAAI,KAAK,KAAK,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAC9B,IAAI,OAAO,KAAK,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAChC,OAAO,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;AACxC,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,IAAY;IACzC,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;AACzD,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,IAAY,EAAE,MAAqB;IACrE,OAAO,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAC/D,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CAAC,IAAY;IAClD,OAAO,4BAA4B,CAAC,IAAI,CAAC,CAAC;AAC3C,CAAC;AAkBD;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,OAAe,EAAE,OAAe;IAC7D,OAAO,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;AAC9C,CAAC;AAED,uFAAuF;AACvF,MAAM,UAAU,QAAQ,CAAC,OAAe;IACvC,OAAO,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AAC9G,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CACjC,UAAkB,EAClB,UAAkB,EAClB,YAAY,GAAG,CAAC;IAEhB,MAAM,MAAM,GAAG,kBAAkB,CAAC,UAAU,EAAE,UAAU,EAAE,YAAY,CAAC,CAAC;IACxE,OAAO;QACN,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,gBAAgB,EAAE,MAAM,CAAC,gBAAgB,IAAI,SAAS;KACtD,CAAC;AACH,CAAC;AAWD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACpC,IAAY,EACZ,OAAe,EACf,OAAe,EACf,GAAW;IAEX,MAAM,YAAY,GAAG,YAAY,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAE7C,IAAI,CAAC;QACJ,uCAAuC;QACvC,IAAI,CAAC;YACJ,MAAM,MAAM,CAAC,YAAY,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;QAC5C,CAAC;QAAC,MAAM,CAAC;YACR,OAAO,EAAE,KAAK,EAAE,mBAAmB,IAAI,EAAE,EAAE,CAAC;QAC7C,CAAC;QAED,gBAAgB;QAChB,MAAM,UAAU,GAAG,MAAM,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;QAEzD,yEAAyE;QACzE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC;QAE/C,MAAM,iBAAiB,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;QACjD,MAAM,iBAAiB,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;QACjD,MAAM,iBAAiB,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;QAEjD,+EAA+E;QAC/E,MAAM,WAAW,GAAG,aAAa,CAAC,iBAAiB,EAAE,iBAAiB,CAAC,CAAC;QAExE,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC;YACxB,OAAO;gBACN,KAAK,EAAE,oCAAoC,IAAI,0EAA0E;aACzH,CAAC;QACH,CAAC;QAED,mEAAmE;QACnE,MAAM,YAAY,GAAG,sBAAsB,CAAC,iBAAiB,CAAC,CAAC;QAC/D,MAAM,YAAY,GAAG,sBAAsB,CAAC,iBAAiB,CAAC,CAAC;QAC/D,MAAM,WAAW,GAAG,YAAY,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;QAEhE,IAAI,WAAW,GAAG,CAAC,EAAE,CAAC;YACrB,OAAO;gBACN,KAAK,EAAE,SAAS,WAAW,+BAA+B,IAAI,2EAA2E;aACzI,CAAC;QACH,CAAC;QAED,qDAAqD;QACrD,gFAAgF;QAChF,MAAM,WAAW,GAAG,WAAW,CAAC,qBAAqB,CAAC;QACtD,MAAM,UAAU,GACf,WAAW,CAAC,SAAS,CAAC,CAAC,EAAE,WAAW,CAAC,KAAK,CAAC;YAC3C,iBAAiB;YACjB,WAAW,CAAC,SAAS,CAAC,WAAW,CAAC,KAAK,GAAG,WAAW,CAAC,WAAW,CAAC,CAAC;QAEpE,6CAA6C;QAC7C,IAAI,WAAW,KAAK,UAAU,EAAE,CAAC;YAChC,OAAO;gBACN,KAAK,EAAE,+BAA+B,IAAI,+CAA+C;aACzF,CAAC;QACH,CAAC;QAED,oBAAoB;QACpB,OAAO,kBAAkB,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;IACpD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,OAAO,EAAE,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;IACpE,CAAC;AACF,CAAC","sourcesContent":["/**\n * Shared diff computation utilities for the edit tool.\n * Used by both edit.ts (for execution) and tool-execution.ts (for preview rendering).\n *\n * Hot-path functions (fuzzyFindText, normalizeForFuzzyMatch, generateDiffString)\n * delegate to the native Rust engine for performance on large files.\n */\n\nimport {\n\tfuzzyFindText as nativeFuzzyFindText,\n\tgenerateDiff as nativeGenerateDiff,\n\tnormalizeForFuzzyMatch as nativeNormalizeForFuzzyMatch,\n} from \"@gsd/native\";\nimport { constants } from \"fs\";\nimport { access, readFile } from \"fs/promises\";\nimport { resolveToCwd } from \"./path-utils.js\";\n\nexport function detectLineEnding(content: string): \"\\r\\n\" | \"\\n\" {\n\tconst crlfIdx = content.indexOf(\"\\r\\n\");\n\tconst lfIdx = content.indexOf(\"\\n\");\n\tif (lfIdx === -1) return \"\\n\";\n\tif (crlfIdx === -1) return \"\\n\";\n\treturn crlfIdx < lfIdx ? \"\\r\\n\" : \"\\n\";\n}\n\nexport function normalizeToLF(text: string): string {\n\treturn text.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n}\n\nexport function restoreLineEndings(text: string, ending: \"\\r\\n\" | \"\\n\"): string {\n\treturn ending === \"\\r\\n\" ? text.replace(/\\n/g, \"\\r\\n\") : text;\n}\n\n/**\n * Normalize text for fuzzy matching (native Rust implementation).\n * - Strip trailing whitespace from each line\n * - Normalize smart quotes to ASCII equivalents\n * - Normalize Unicode dashes/hyphens to ASCII hyphen\n * - Normalize special Unicode spaces to regular space\n */\nexport function normalizeForFuzzyMatch(text: string): string {\n\treturn nativeNormalizeForFuzzyMatch(text);\n}\n\nexport interface FuzzyMatchResult {\n\t/** Whether a match was found */\n\tfound: boolean;\n\t/** The index where the match starts (in the content that should be used for replacement) */\n\tindex: number;\n\t/** Length of the matched text */\n\tmatchLength: number;\n\t/** Whether fuzzy matching was used (false = exact match) */\n\tusedFuzzyMatch: boolean;\n\t/**\n\t * The content to use for replacement operations.\n\t * When exact match: original content. When fuzzy match: normalized content.\n\t */\n\tcontentForReplacement: string;\n}\n\n/**\n * Find oldText in content, trying exact match first, then fuzzy match\n * (native Rust implementation).\n *\n * When fuzzy matching is used, the returned contentForReplacement is the\n * fuzzy-normalized version of the content.\n */\nexport function fuzzyFindText(content: string, oldText: string): FuzzyMatchResult {\n\treturn nativeFuzzyFindText(content, oldText);\n}\n\n/** Strip UTF-8 BOM if present, return both the BOM (if any) and the text without it */\nexport function stripBom(content: string): { bom: string; text: string } {\n\treturn content.startsWith(\"\\uFEFF\") ? { bom: \"\\uFEFF\", text: content.slice(1) } : { bom: \"\", text: content };\n}\n\n/**\n * Generate a unified diff string with line numbers and context\n * (native Rust implementation using Myers' algorithm via the `similar` crate).\n *\n * Returns both the diff string and the first changed line number (in the new file).\n */\nexport function generateDiffString(\n\toldContent: string,\n\tnewContent: string,\n\tcontextLines = 4,\n): { diff: string; firstChangedLine: number | undefined } {\n\tconst result = nativeGenerateDiff(oldContent, newContent, contextLines);\n\treturn {\n\t\tdiff: result.diff,\n\t\tfirstChangedLine: result.firstChangedLine ?? undefined,\n\t};\n}\n\nexport interface EditDiffResult {\n\tdiff: string;\n\tfirstChangedLine: number | undefined;\n}\n\nexport interface EditDiffError {\n\terror: string;\n}\n\n/**\n * Compute the diff for an edit operation without applying it.\n * Used for preview rendering in the TUI before the tool executes.\n */\nexport async function computeEditDiff(\n\tpath: string,\n\toldText: string,\n\tnewText: string,\n\tcwd: string,\n): Promise<EditDiffResult | EditDiffError> {\n\tconst absolutePath = resolveToCwd(path, cwd);\n\n\ttry {\n\t\t// Check if file exists and is readable\n\t\ttry {\n\t\t\tawait access(absolutePath, constants.R_OK);\n\t\t} catch {\n\t\t\treturn { error: `File not found: ${path}` };\n\t\t}\n\n\t\t// Read the file\n\t\tconst rawContent = await readFile(absolutePath, \"utf-8\");\n\n\t\t// Strip BOM before matching (LLM won't include invisible BOM in oldText)\n\t\tconst { text: content } = stripBom(rawContent);\n\n\t\tconst normalizedContent = normalizeToLF(content);\n\t\tconst normalizedOldText = normalizeToLF(oldText);\n\t\tconst normalizedNewText = normalizeToLF(newText);\n\n\t\t// Find the old text using fuzzy matching (tries exact match first, then fuzzy)\n\t\tconst matchResult = fuzzyFindText(normalizedContent, normalizedOldText);\n\n\t\tif (!matchResult.found) {\n\t\t\treturn {\n\t\t\t\terror: `Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`,\n\t\t\t};\n\t\t}\n\n\t\t// Count occurrences using fuzzy-normalized content for consistency\n\t\tconst fuzzyContent = normalizeForFuzzyMatch(normalizedContent);\n\t\tconst fuzzyOldText = normalizeForFuzzyMatch(normalizedOldText);\n\t\tconst occurrences = fuzzyContent.split(fuzzyOldText).length - 1;\n\n\t\tif (occurrences > 1) {\n\t\t\treturn {\n\t\t\t\terror: `Found ${occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique.`,\n\t\t\t};\n\t\t}\n\n\t\t// Compute the new content using the matched position\n\t\t// When fuzzy matching was used, contentForReplacement is the normalized version\n\t\tconst baseContent = matchResult.contentForReplacement;\n\t\tconst newContent =\n\t\t\tbaseContent.substring(0, matchResult.index) +\n\t\t\tnormalizedNewText +\n\t\t\tbaseContent.substring(matchResult.index + matchResult.matchLength);\n\n\t\t// Check if it would actually change anything\n\t\tif (baseContent === newContent) {\n\t\t\treturn {\n\t\t\t\terror: `No changes would be made to ${path}. The replacement produces identical content.`,\n\t\t\t};\n\t\t}\n\n\t\t// Generate the diff\n\t\treturn generateDiffString(baseContent, newContent);\n\t} catch (err) {\n\t\treturn { error: err instanceof Error ? err.message : String(err) };\n\t}\n}\n"]}
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared diff computation utilities for the edit tool.
|
|
3
3
|
* Used by both edit.ts (for execution) and tool-execution.ts (for preview rendering).
|
|
4
|
+
*
|
|
5
|
+
* Hot-path functions (fuzzyFindText, normalizeForFuzzyMatch, generateDiffString)
|
|
6
|
+
* delegate to the native Rust engine for performance on large files.
|
|
4
7
|
*/
|
|
5
8
|
|
|
6
|
-
import
|
|
9
|
+
import {
|
|
10
|
+
fuzzyFindText as nativeFuzzyFindText,
|
|
11
|
+
generateDiff as nativeGenerateDiff,
|
|
12
|
+
normalizeForFuzzyMatch as nativeNormalizeForFuzzyMatch,
|
|
13
|
+
} from "@gsd/native";
|
|
7
14
|
import { constants } from "fs";
|
|
8
15
|
import { access, readFile } from "fs/promises";
|
|
9
16
|
import { resolveToCwd } from "./path-utils.js";
|
|
@@ -25,32 +32,14 @@ export function restoreLineEndings(text: string, ending: "\r\n" | "\n"): string
|
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
/**
|
|
28
|
-
* Normalize text for fuzzy matching
|
|
35
|
+
* Normalize text for fuzzy matching (native Rust implementation).
|
|
29
36
|
* - Strip trailing whitespace from each line
|
|
30
37
|
* - Normalize smart quotes to ASCII equivalents
|
|
31
38
|
* - Normalize Unicode dashes/hyphens to ASCII hyphen
|
|
32
39
|
* - Normalize special Unicode spaces to regular space
|
|
33
40
|
*/
|
|
34
41
|
export function normalizeForFuzzyMatch(text: string): string {
|
|
35
|
-
return (
|
|
36
|
-
text
|
|
37
|
-
// Strip trailing whitespace per line
|
|
38
|
-
.split("\n")
|
|
39
|
-
.map((line) => line.trimEnd())
|
|
40
|
-
.join("\n")
|
|
41
|
-
// Smart single quotes → '
|
|
42
|
-
.replace(/[\u2018\u2019\u201A\u201B]/g, "'")
|
|
43
|
-
// Smart double quotes → "
|
|
44
|
-
.replace(/[\u201C\u201D\u201E\u201F]/g, '"')
|
|
45
|
-
// Various dashes/hyphens → -
|
|
46
|
-
// U+2010 hyphen, U+2011 non-breaking hyphen, U+2012 figure dash,
|
|
47
|
-
// U+2013 en-dash, U+2014 em-dash, U+2015 horizontal bar, U+2212 minus
|
|
48
|
-
.replace(/[\u2010\u2011\u2012\u2013\u2014\u2015\u2212]/g, "-")
|
|
49
|
-
// Special spaces → regular space
|
|
50
|
-
// U+00A0 NBSP, U+2002-U+200A various spaces, U+202F narrow NBSP,
|
|
51
|
-
// U+205F medium math space, U+3000 ideographic space
|
|
52
|
-
.replace(/[\u00A0\u2002-\u200A\u202F\u205F\u3000]/g, " ")
|
|
53
|
-
);
|
|
42
|
+
return nativeNormalizeForFuzzyMatch(text);
|
|
54
43
|
}
|
|
55
44
|
|
|
56
45
|
export interface FuzzyMatchResult {
|
|
@@ -70,49 +59,14 @@ export interface FuzzyMatchResult {
|
|
|
70
59
|
}
|
|
71
60
|
|
|
72
61
|
/**
|
|
73
|
-
* Find oldText in content, trying exact match first, then fuzzy match
|
|
62
|
+
* Find oldText in content, trying exact match first, then fuzzy match
|
|
63
|
+
* (native Rust implementation).
|
|
64
|
+
*
|
|
74
65
|
* When fuzzy matching is used, the returned contentForReplacement is the
|
|
75
|
-
* fuzzy-normalized version of the content
|
|
76
|
-
* Unicode quotes/dashes normalized to ASCII).
|
|
66
|
+
* fuzzy-normalized version of the content.
|
|
77
67
|
*/
|
|
78
68
|
export function fuzzyFindText(content: string, oldText: string): FuzzyMatchResult {
|
|
79
|
-
|
|
80
|
-
const exactIndex = content.indexOf(oldText);
|
|
81
|
-
if (exactIndex !== -1) {
|
|
82
|
-
return {
|
|
83
|
-
found: true,
|
|
84
|
-
index: exactIndex,
|
|
85
|
-
matchLength: oldText.length,
|
|
86
|
-
usedFuzzyMatch: false,
|
|
87
|
-
contentForReplacement: content,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Try fuzzy match - work entirely in normalized space
|
|
92
|
-
const fuzzyContent = normalizeForFuzzyMatch(content);
|
|
93
|
-
const fuzzyOldText = normalizeForFuzzyMatch(oldText);
|
|
94
|
-
const fuzzyIndex = fuzzyContent.indexOf(fuzzyOldText);
|
|
95
|
-
|
|
96
|
-
if (fuzzyIndex === -1) {
|
|
97
|
-
return {
|
|
98
|
-
found: false,
|
|
99
|
-
index: -1,
|
|
100
|
-
matchLength: 0,
|
|
101
|
-
usedFuzzyMatch: false,
|
|
102
|
-
contentForReplacement: content,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// When fuzzy matching, we work in the normalized space for replacement.
|
|
107
|
-
// This means the output will have normalized whitespace/quotes/dashes,
|
|
108
|
-
// which is acceptable since we're fixing minor formatting differences anyway.
|
|
109
|
-
return {
|
|
110
|
-
found: true,
|
|
111
|
-
index: fuzzyIndex,
|
|
112
|
-
matchLength: fuzzyOldText.length,
|
|
113
|
-
usedFuzzyMatch: true,
|
|
114
|
-
contentForReplacement: fuzzyContent,
|
|
115
|
-
};
|
|
69
|
+
return nativeFuzzyFindText(content, oldText);
|
|
116
70
|
}
|
|
117
71
|
|
|
118
72
|
/** Strip UTF-8 BOM if present, return both the BOM (if any) and the text without it */
|
|
@@ -121,7 +75,9 @@ export function stripBom(content: string): { bom: string; text: string } {
|
|
|
121
75
|
}
|
|
122
76
|
|
|
123
77
|
/**
|
|
124
|
-
* Generate a unified diff string with line numbers and context
|
|
78
|
+
* Generate a unified diff string with line numbers and context
|
|
79
|
+
* (native Rust implementation using Myers' algorithm via the `similar` crate).
|
|
80
|
+
*
|
|
125
81
|
* Returns both the diff string and the first changed line number (in the new file).
|
|
126
82
|
*/
|
|
127
83
|
export function generateDiffString(
|
|
@@ -129,101 +85,11 @@ export function generateDiffString(
|
|
|
129
85
|
newContent: string,
|
|
130
86
|
contextLines = 4,
|
|
131
87
|
): { diff: string; firstChangedLine: number | undefined } {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const maxLineNum = Math.max(oldLines.length, newLines.length);
|
|
138
|
-
const lineNumWidth = String(maxLineNum).length;
|
|
139
|
-
|
|
140
|
-
let oldLineNum = 1;
|
|
141
|
-
let newLineNum = 1;
|
|
142
|
-
let lastWasChange = false;
|
|
143
|
-
let firstChangedLine: number | undefined;
|
|
144
|
-
|
|
145
|
-
for (let i = 0; i < parts.length; i++) {
|
|
146
|
-
const part = parts[i];
|
|
147
|
-
const raw = part.value.split("\n");
|
|
148
|
-
if (raw[raw.length - 1] === "") {
|
|
149
|
-
raw.pop();
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (part.added || part.removed) {
|
|
153
|
-
// Capture the first changed line (in the new file)
|
|
154
|
-
if (firstChangedLine === undefined) {
|
|
155
|
-
firstChangedLine = newLineNum;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Show the change
|
|
159
|
-
for (const line of raw) {
|
|
160
|
-
if (part.added) {
|
|
161
|
-
const lineNum = String(newLineNum).padStart(lineNumWidth, " ");
|
|
162
|
-
output.push(`+${lineNum} ${line}`);
|
|
163
|
-
newLineNum++;
|
|
164
|
-
} else {
|
|
165
|
-
// removed
|
|
166
|
-
const lineNum = String(oldLineNum).padStart(lineNumWidth, " ");
|
|
167
|
-
output.push(`-${lineNum} ${line}`);
|
|
168
|
-
oldLineNum++;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
lastWasChange = true;
|
|
172
|
-
} else {
|
|
173
|
-
// Context lines - only show a few before/after changes
|
|
174
|
-
const nextPartIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed);
|
|
175
|
-
|
|
176
|
-
if (lastWasChange || nextPartIsChange) {
|
|
177
|
-
// Show context
|
|
178
|
-
let linesToShow = raw;
|
|
179
|
-
let skipStart = 0;
|
|
180
|
-
let skipEnd = 0;
|
|
181
|
-
|
|
182
|
-
if (!lastWasChange) {
|
|
183
|
-
// Show only last N lines as leading context
|
|
184
|
-
skipStart = Math.max(0, raw.length - contextLines);
|
|
185
|
-
linesToShow = raw.slice(skipStart);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (!nextPartIsChange && linesToShow.length > contextLines) {
|
|
189
|
-
// Show only first N lines as trailing context
|
|
190
|
-
skipEnd = linesToShow.length - contextLines;
|
|
191
|
-
linesToShow = linesToShow.slice(0, contextLines);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Add ellipsis if we skipped lines at start
|
|
195
|
-
if (skipStart > 0) {
|
|
196
|
-
output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
|
|
197
|
-
// Update line numbers for the skipped leading context
|
|
198
|
-
oldLineNum += skipStart;
|
|
199
|
-
newLineNum += skipStart;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
for (const line of linesToShow) {
|
|
203
|
-
const lineNum = String(oldLineNum).padStart(lineNumWidth, " ");
|
|
204
|
-
output.push(` ${lineNum} ${line}`);
|
|
205
|
-
oldLineNum++;
|
|
206
|
-
newLineNum++;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Add ellipsis if we skipped lines at end
|
|
210
|
-
if (skipEnd > 0) {
|
|
211
|
-
output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
|
|
212
|
-
// Update line numbers for the skipped trailing context
|
|
213
|
-
oldLineNum += skipEnd;
|
|
214
|
-
newLineNum += skipEnd;
|
|
215
|
-
}
|
|
216
|
-
} else {
|
|
217
|
-
// Skip these context lines entirely
|
|
218
|
-
oldLineNum += raw.length;
|
|
219
|
-
newLineNum += raw.length;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
lastWasChange = false;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return { diff: output.join("\n"), firstChangedLine };
|
|
88
|
+
const result = nativeGenerateDiff(oldContent, newContent, contextLines);
|
|
89
|
+
return {
|
|
90
|
+
diff: result.diff,
|
|
91
|
+
firstChangedLine: result.firstChangedLine ?? undefined,
|
|
92
|
+
};
|
|
227
93
|
}
|
|
228
94
|
|
|
229
95
|
export interface EditDiffResult {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gsd-pi",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.2",
|
|
4
4
|
"description": "GSD — Get Shit Done coding agent",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
"build:pi-ai": "npm run build -w @gsd/pi-ai",
|
|
41
41
|
"build:pi-agent-core": "npm run build -w @gsd/pi-agent-core",
|
|
42
42
|
"build:pi-coding-agent": "npm run build -w @gsd/pi-coding-agent",
|
|
43
|
-
"build:
|
|
43
|
+
"build:native-pkg": "npm run build -w @gsd/native",
|
|
44
|
+
"build:pi": "npm run build:native-pkg && npm run build:pi-tui && npm run build:pi-ai && npm run build:pi-agent-core && npm run build:pi-coding-agent",
|
|
44
45
|
"build": "npm run build:pi && tsc && npm run copy-themes",
|
|
45
46
|
"copy-themes": "node -e \"const{mkdirSync,cpSync}=require('fs');const{resolve}=require('path');const src=resolve(__dirname,'packages/pi-coding-agent/dist/modes/interactive/theme');mkdirSync('pkg/dist/modes/interactive/theme',{recursive:true});cpSync(src,'pkg/dist/modes/interactive/theme',{recursive:true})\"",
|
|
46
47
|
"test": "node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/*.test.ts src/resources/extensions/gsd/tests/*.test.mjs src/tests/*.test.ts",
|
|
@@ -67,6 +68,7 @@
|
|
|
67
68
|
"sharp": "^0.34.5"
|
|
68
69
|
},
|
|
69
70
|
"bundleDependencies": [
|
|
71
|
+
"@gsd/native",
|
|
70
72
|
"@gsd/pi-agent-core",
|
|
71
73
|
"@gsd/pi-ai",
|
|
72
74
|
"@gsd/pi-coding-agent",
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AstFindOptions, AstFindResult, AstReplaceOptions, AstReplaceResult, AstFindMatch, AstReplaceChange, AstReplaceFileChange } from "./types.js";
|
|
2
|
+
export type { AstFindMatch, AstFindOptions, AstFindResult, AstReplaceChange, AstReplaceFileChange, AstReplaceOptions, AstReplaceResult };
|
|
3
|
+
export declare function astGrep(options: AstFindOptions): AstFindResult;
|
|
4
|
+
export declare function astEdit(options: AstReplaceOptions): AstReplaceResult;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export interface AstFindOptions {
|
|
2
|
+
patterns: string[];
|
|
3
|
+
lang?: string;
|
|
4
|
+
path?: string;
|
|
5
|
+
glob?: string;
|
|
6
|
+
selector?: string;
|
|
7
|
+
strictness?: string;
|
|
8
|
+
limit?: number;
|
|
9
|
+
offset?: number;
|
|
10
|
+
includeMeta?: boolean;
|
|
11
|
+
context?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface AstFindMatch {
|
|
14
|
+
path: string;
|
|
15
|
+
text: string;
|
|
16
|
+
byteStart: number;
|
|
17
|
+
byteEnd: number;
|
|
18
|
+
startLine: number;
|
|
19
|
+
startColumn: number;
|
|
20
|
+
endLine: number;
|
|
21
|
+
endColumn: number;
|
|
22
|
+
metaVariables?: Record<string, string>;
|
|
23
|
+
}
|
|
24
|
+
export interface AstFindResult {
|
|
25
|
+
matches: AstFindMatch[];
|
|
26
|
+
totalMatches: number;
|
|
27
|
+
filesWithMatches: number;
|
|
28
|
+
filesSearched: number;
|
|
29
|
+
limitReached: boolean;
|
|
30
|
+
parseErrors?: string[];
|
|
31
|
+
}
|
|
32
|
+
export interface AstReplaceOptions {
|
|
33
|
+
rewrites: Record<string, string>;
|
|
34
|
+
lang?: string;
|
|
35
|
+
path?: string;
|
|
36
|
+
glob?: string;
|
|
37
|
+
selector?: string;
|
|
38
|
+
strictness?: string;
|
|
39
|
+
dryRun?: boolean;
|
|
40
|
+
maxReplacements?: number;
|
|
41
|
+
maxFiles?: number;
|
|
42
|
+
failOnParseError?: boolean;
|
|
43
|
+
}
|
|
44
|
+
export interface AstReplaceChange {
|
|
45
|
+
path: string;
|
|
46
|
+
before: string;
|
|
47
|
+
after: string;
|
|
48
|
+
byteStart: number;
|
|
49
|
+
byteEnd: number;
|
|
50
|
+
deletedLength: number;
|
|
51
|
+
startLine: number;
|
|
52
|
+
startColumn: number;
|
|
53
|
+
endLine: number;
|
|
54
|
+
endColumn: number;
|
|
55
|
+
}
|
|
56
|
+
export interface AstReplaceFileChange {
|
|
57
|
+
path: string;
|
|
58
|
+
count: number;
|
|
59
|
+
}
|
|
60
|
+
export interface AstReplaceResult {
|
|
61
|
+
changes: AstReplaceChange[];
|
|
62
|
+
fileChanges: AstReplaceFileChange[];
|
|
63
|
+
totalReplacements: number;
|
|
64
|
+
filesTouched: number;
|
|
65
|
+
filesSearched: number;
|
|
66
|
+
applied: boolean;
|
|
67
|
+
limitReached: boolean;
|
|
68
|
+
parseErrors?: string[];
|
|
69
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native clipboard access using N-API.
|
|
3
|
+
*
|
|
4
|
+
* Cross-platform clipboard read/write backed by the `arboard` Rust crate.
|
|
5
|
+
* No external tools (pbcopy, xclip, etc.) required.
|
|
6
|
+
*/
|
|
7
|
+
import type { ClipboardImage } from "./types.js";
|
|
8
|
+
export type { ClipboardImage };
|
|
9
|
+
/**
|
|
10
|
+
* Copy plain text to the system clipboard.
|
|
11
|
+
*
|
|
12
|
+
* Runs synchronously to avoid macOS AppKit pasteboard warnings
|
|
13
|
+
* when writing from worker threads.
|
|
14
|
+
*/
|
|
15
|
+
export declare function copyToClipboard(text: string): void;
|
|
16
|
+
/**
|
|
17
|
+
* Read plain text from the system clipboard.
|
|
18
|
+
*
|
|
19
|
+
* Returns `null` when no text data is available.
|
|
20
|
+
*/
|
|
21
|
+
export declare function readTextFromClipboard(): string | null;
|
|
22
|
+
/**
|
|
23
|
+
* Read an image from the system clipboard.
|
|
24
|
+
*
|
|
25
|
+
* Returns a Promise that resolves to a `ClipboardImage` (PNG-encoded bytes)
|
|
26
|
+
* or `null` when no image data is available.
|
|
27
|
+
*/
|
|
28
|
+
export declare function readImageFromClipboard(): Promise<ClipboardImage | null>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native clipboard access using N-API.
|
|
3
|
+
*
|
|
4
|
+
* Cross-platform clipboard read/write backed by the `arboard` Rust crate.
|
|
5
|
+
* No external tools (pbcopy, xclip, etc.) required.
|
|
6
|
+
*/
|
|
7
|
+
import { native } from "../native.js";
|
|
8
|
+
/**
|
|
9
|
+
* Copy plain text to the system clipboard.
|
|
10
|
+
*
|
|
11
|
+
* Runs synchronously to avoid macOS AppKit pasteboard warnings
|
|
12
|
+
* when writing from worker threads.
|
|
13
|
+
*/
|
|
14
|
+
export function copyToClipboard(text) {
|
|
15
|
+
native.copyToClipboard(text);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Read plain text from the system clipboard.
|
|
19
|
+
*
|
|
20
|
+
* Returns `null` when no text data is available.
|
|
21
|
+
*/
|
|
22
|
+
export function readTextFromClipboard() {
|
|
23
|
+
return native.readTextFromClipboard();
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Read an image from the system clipboard.
|
|
27
|
+
*
|
|
28
|
+
* Returns a Promise that resolves to a `ClipboardImage` (PNG-encoded bytes)
|
|
29
|
+
* or `null` when no image data is available.
|
|
30
|
+
*/
|
|
31
|
+
export function readImageFromClipboard() {
|
|
32
|
+
return native.readImageFromClipboard();
|
|
33
|
+
}
|