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.
Files changed (157) hide show
  1. package/node_modules/@gsd/native/dist/ast/index.d.ts +4 -0
  2. package/node_modules/@gsd/native/dist/ast/index.js +7 -0
  3. package/node_modules/@gsd/native/dist/ast/types.d.ts +69 -0
  4. package/node_modules/@gsd/native/dist/ast/types.js +1 -0
  5. package/node_modules/@gsd/native/{src/clipboard/index.ts → dist/clipboard/index.d.ts} +3 -15
  6. package/node_modules/@gsd/native/dist/clipboard/index.js +33 -0
  7. package/node_modules/@gsd/native/dist/clipboard/types.d.ts +7 -0
  8. package/node_modules/@gsd/native/dist/clipboard/types.js +1 -0
  9. package/node_modules/@gsd/native/dist/diff/index.d.ts +33 -0
  10. package/node_modules/@gsd/native/dist/diff/index.js +38 -0
  11. package/node_modules/@gsd/native/dist/diff/types.d.ts +23 -0
  12. package/node_modules/@gsd/native/dist/diff/types.js +1 -0
  13. package/node_modules/@gsd/native/{src/fd/index.ts → dist/fd/index.d.ts} +2 -12
  14. package/node_modules/@gsd/native/dist/fd/index.js +26 -0
  15. package/node_modules/@gsd/native/dist/fd/types.d.ts +29 -0
  16. package/node_modules/@gsd/native/dist/fd/types.js +1 -0
  17. package/node_modules/@gsd/native/{src/glob/index.ts → dist/glob/index.d.ts} +3 -19
  18. package/node_modules/@gsd/native/dist/glob/index.js +31 -0
  19. package/node_modules/@gsd/native/dist/glob/types.d.ts +50 -0
  20. package/node_modules/@gsd/native/dist/glob/types.js +1 -0
  21. package/node_modules/@gsd/native/dist/grep/index.d.ts +20 -0
  22. package/node_modules/@gsd/native/dist/grep/index.js +23 -0
  23. package/node_modules/@gsd/native/dist/grep/types.d.ts +99 -0
  24. package/node_modules/@gsd/native/dist/grep/types.js +1 -0
  25. package/node_modules/@gsd/native/dist/gsd-parser/index.d.ts +45 -0
  26. package/node_modules/@gsd/native/dist/gsd-parser/index.js +54 -0
  27. package/node_modules/@gsd/native/dist/gsd-parser/types.d.ts +55 -0
  28. package/node_modules/@gsd/native/dist/gsd-parser/types.js +7 -0
  29. package/node_modules/@gsd/native/{src/highlight/index.ts → dist/highlight/index.d.ts} +3 -19
  30. package/node_modules/@gsd/native/dist/highlight/index.js +33 -0
  31. package/node_modules/@gsd/native/dist/highlight/types.d.ts +25 -0
  32. package/node_modules/@gsd/native/dist/highlight/types.js +1 -0
  33. package/node_modules/@gsd/native/{src/html/index.ts → dist/html/index.d.ts} +1 -10
  34. package/node_modules/@gsd/native/dist/html/index.js +16 -0
  35. package/node_modules/@gsd/native/dist/html/types.d.ts +7 -0
  36. package/node_modules/@gsd/native/dist/html/types.js +1 -0
  37. package/node_modules/@gsd/native/{src/image/index.ts → dist/image/index.d.ts} +1 -14
  38. package/node_modules/@gsd/native/dist/image/index.js +18 -0
  39. package/node_modules/@gsd/native/dist/image/types.d.ts +35 -0
  40. package/node_modules/@gsd/native/dist/image/types.js +26 -0
  41. package/node_modules/@gsd/native/{src/index.ts → dist/index.d.ts} +12 -60
  42. package/node_modules/@gsd/native/dist/index.js +28 -0
  43. package/node_modules/@gsd/native/dist/native.d.ts +44 -0
  44. package/node_modules/@gsd/native/dist/native.js +34 -0
  45. package/node_modules/@gsd/native/dist/ps/index.d.ts +38 -0
  46. package/node_modules/@gsd/native/{src/ps/index.ts → dist/ps/index.js} +8 -13
  47. package/node_modules/@gsd/native/{src/ps/types.ts → dist/ps/types.d.ts} +2 -2
  48. package/node_modules/@gsd/native/dist/ps/types.js +1 -0
  49. package/node_modules/@gsd/native/{src/text/index.ts → dist/text/index.d.ts} +6 -76
  50. package/node_modules/@gsd/native/dist/text/index.js +66 -0
  51. package/node_modules/@gsd/native/dist/text/types.d.ts +27 -0
  52. package/node_modules/@gsd/native/dist/text/types.js +10 -0
  53. package/node_modules/@gsd/native/{src/ttsr/index.ts → dist/ttsr/index.d.ts} +3 -15
  54. package/node_modules/@gsd/native/dist/ttsr/index.js +32 -0
  55. package/node_modules/@gsd/native/{src/ttsr/types.ts → dist/ttsr/types.d.ts} +4 -5
  56. package/node_modules/@gsd/native/dist/ttsr/types.js +1 -0
  57. package/node_modules/@gsd/native/package.json +24 -23
  58. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/edit-diff.d.ts +11 -5
  59. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/edit-diff.d.ts.map +1 -1
  60. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/edit-diff.js +19 -142
  61. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/edit-diff.js.map +1 -1
  62. package/node_modules/@gsd/pi-coding-agent/src/core/tools/edit-diff.ts +23 -157
  63. package/package.json +4 -2
  64. package/packages/native/dist/ast/index.d.ts +4 -0
  65. package/packages/native/dist/ast/index.js +7 -0
  66. package/packages/native/dist/ast/types.d.ts +69 -0
  67. package/packages/native/dist/ast/types.js +1 -0
  68. package/packages/native/dist/clipboard/index.d.ts +28 -0
  69. package/packages/native/dist/clipboard/index.js +33 -0
  70. package/packages/native/dist/clipboard/types.d.ts +7 -0
  71. package/packages/native/dist/clipboard/types.js +1 -0
  72. package/packages/native/dist/diff/index.d.ts +33 -0
  73. package/packages/native/dist/diff/index.js +38 -0
  74. package/packages/native/dist/diff/types.d.ts +23 -0
  75. package/packages/native/dist/diff/types.js +1 -0
  76. package/packages/native/dist/fd/index.d.ts +25 -0
  77. package/packages/native/dist/fd/index.js +26 -0
  78. package/packages/native/dist/fd/types.d.ts +29 -0
  79. package/packages/native/dist/fd/types.js +1 -0
  80. package/packages/native/dist/glob/index.d.ts +28 -0
  81. package/packages/native/dist/glob/index.js +31 -0
  82. package/packages/native/dist/glob/types.d.ts +50 -0
  83. package/packages/native/dist/glob/types.js +1 -0
  84. package/packages/native/dist/grep/index.d.ts +20 -0
  85. package/packages/native/dist/grep/index.js +23 -0
  86. package/packages/native/dist/grep/types.d.ts +99 -0
  87. package/packages/native/dist/grep/types.js +1 -0
  88. package/packages/native/dist/gsd-parser/index.d.ts +45 -0
  89. package/packages/native/dist/gsd-parser/index.js +54 -0
  90. package/packages/native/dist/gsd-parser/types.d.ts +55 -0
  91. package/packages/native/dist/gsd-parser/types.js +7 -0
  92. package/packages/native/dist/highlight/index.d.ts +28 -0
  93. package/packages/native/dist/highlight/index.js +33 -0
  94. package/packages/native/dist/highlight/types.d.ts +25 -0
  95. package/packages/native/dist/highlight/types.js +1 -0
  96. package/packages/native/dist/html/index.d.ts +15 -0
  97. package/packages/native/dist/html/index.js +16 -0
  98. package/packages/native/dist/html/types.d.ts +7 -0
  99. package/packages/native/dist/html/types.js +1 -0
  100. package/packages/native/dist/image/index.d.ts +15 -0
  101. package/packages/native/dist/image/index.js +18 -0
  102. package/packages/native/dist/image/types.d.ts +35 -0
  103. package/packages/native/dist/image/types.js +26 -0
  104. package/packages/native/dist/index.d.ts +40 -0
  105. package/packages/native/dist/index.js +28 -0
  106. package/packages/native/dist/native.d.ts +44 -0
  107. package/packages/native/dist/native.js +34 -0
  108. package/packages/native/dist/ps/index.d.ts +38 -0
  109. package/packages/native/dist/ps/index.js +47 -0
  110. package/packages/native/dist/ps/types.d.ts +5 -0
  111. package/packages/native/dist/ps/types.js +1 -0
  112. package/packages/native/dist/text/index.d.ts +55 -0
  113. package/packages/native/dist/text/index.js +66 -0
  114. package/packages/native/dist/text/types.d.ts +27 -0
  115. package/packages/native/dist/text/types.js +10 -0
  116. package/packages/native/dist/ttsr/index.d.ts +27 -0
  117. package/packages/native/dist/ttsr/index.js +32 -0
  118. package/packages/native/dist/ttsr/types.d.ts +9 -0
  119. package/packages/native/dist/ttsr/types.js +1 -0
  120. package/packages/native/package.json +24 -23
  121. package/packages/native/src/__tests__/diff.test.mjs +189 -0
  122. package/packages/native/src/__tests__/ttsr.test.mjs +135 -0
  123. package/packages/native/src/diff/index.ts +61 -0
  124. package/packages/native/src/diff/types.ts +24 -0
  125. package/packages/native/src/gsd-parser/index.ts +98 -0
  126. package/packages/native/src/gsd-parser/types.ts +62 -0
  127. package/packages/native/src/index.ts +23 -0
  128. package/packages/native/src/native.ts +8 -0
  129. package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts +11 -5
  130. package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts.map +1 -1
  131. package/packages/pi-coding-agent/dist/core/tools/edit-diff.js +19 -142
  132. package/packages/pi-coding-agent/dist/core/tools/edit-diff.js.map +1 -1
  133. package/packages/pi-coding-agent/src/core/tools/edit-diff.ts +23 -157
  134. package/src/resources/extensions/gsd/files.ts +9 -0
  135. package/src/resources/extensions/gsd/native-parser-bridge.ts +135 -0
  136. package/src/resources/extensions/ttsr/ttsr-manager.ts +86 -0
  137. package/node_modules/@gsd/native/src/__tests__/clipboard.test.mjs +0 -79
  138. package/node_modules/@gsd/native/src/__tests__/fd.test.mjs +0 -164
  139. package/node_modules/@gsd/native/src/__tests__/glob.test.mjs +0 -237
  140. package/node_modules/@gsd/native/src/__tests__/grep.test.mjs +0 -162
  141. package/node_modules/@gsd/native/src/__tests__/highlight.test.mjs +0 -156
  142. package/node_modules/@gsd/native/src/__tests__/html.test.mjs +0 -98
  143. package/node_modules/@gsd/native/src/__tests__/image.test.mjs +0 -137
  144. package/node_modules/@gsd/native/src/__tests__/ps.test.mjs +0 -109
  145. package/node_modules/@gsd/native/src/__tests__/text.test.mjs +0 -262
  146. package/node_modules/@gsd/native/src/ast/index.ts +0 -12
  147. package/node_modules/@gsd/native/src/ast/types.ts +0 -75
  148. package/node_modules/@gsd/native/src/clipboard/types.ts +0 -7
  149. package/node_modules/@gsd/native/src/fd/types.ts +0 -31
  150. package/node_modules/@gsd/native/src/glob/types.ts +0 -53
  151. package/node_modules/@gsd/native/src/grep/index.ts +0 -48
  152. package/node_modules/@gsd/native/src/grep/types.ts +0 -105
  153. package/node_modules/@gsd/native/src/highlight/types.ts +0 -25
  154. package/node_modules/@gsd/native/src/html/types.ts +0 -7
  155. package/node_modules/@gsd/native/src/image/types.ts +0 -41
  156. package/node_modules/@gsd/native/src/native.ts +0 -94
  157. 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 * as Diff from "diff";
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. Applies progressive transformations:
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 (trailing whitespace stripped,
54
- * Unicode quotes/dashes normalized to ASCII).
42
+ * fuzzy-normalized version of the content.
55
43
  */
56
44
  export function fuzzyFindText(content, oldText) {
57
- // Try exact match first
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 parts = Diff.diffLines(oldContent, newContent);
102
- const output = [];
103
- const oldLines = oldContent.split("\n");
104
- const newLines = newContent.split("\n");
105
- const maxLineNum = Math.max(oldLines.length, newLines.length);
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":"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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 * 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 * as Diff from "diff";
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. Applies progressive transformations:
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 (trailing whitespace stripped,
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
- // Try exact match first
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 parts = Diff.diffLines(oldContent, newContent);
133
- const output: string[] = [];
134
-
135
- const oldLines = oldContent.split("\n");
136
- const newLines = newContent.split("\n");
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.0",
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:pi": "npm run build:pi-tui && npm run build:pi-ai && npm run build:pi-agent-core && npm run build:pi-coding-agent",
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,7 @@
1
+ import { native } from "../native.js";
2
+ export function astGrep(options) {
3
+ return native.astGrep(options);
4
+ }
5
+ export function astEdit(options) {
6
+ return native.astEdit(options);
7
+ }
@@ -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
+ }