gsd-pi 2.10.1 → 2.10.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/loader.js +0 -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/dist/index.d.ts +6 -0
- package/node_modules/@gsd/native/dist/index.js +3 -0
- package/node_modules/@gsd/native/dist/native.d.ts +15 -1
- package/node_modules/@gsd/native/dist/native.js +39 -9
- package/node_modules/@gsd/native/dist/ttsr/index.d.ts +27 -0
- package/node_modules/@gsd/native/dist/ttsr/index.js +32 -0
- package/node_modules/@gsd/native/dist/ttsr/types.d.ts +9 -0
- package/node_modules/@gsd/native/dist/ttsr/types.js +1 -0
- 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 +8 -2
- 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/index.d.ts +6 -0
- package/packages/native/dist/index.js +3 -0
- package/packages/native/dist/native.d.ts +15 -1
- package/packages/native/dist/native.js +39 -9
- 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/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 +26 -0
- package/packages/native/src/native.ts +50 -9
- package/packages/native/src/ttsr/index.ts +39 -0
- package/packages/native/src/ttsr/types.ts +10 -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/dist/modes/interactive/theme/dark.json +0 -85
- package/dist/modes/interactive/theme/light.json +0 -84
- package/dist/modes/interactive/theme/theme-schema.json +0 -335
- package/dist/modes/interactive/theme/theme.d.ts +0 -78
- package/dist/modes/interactive/theme/theme.d.ts.map +0 -1
- package/dist/modes/interactive/theme/theme.js +0 -949
- package/dist/modes/interactive/theme/theme.js.map +0 -1
package/README.md
CHANGED
|
@@ -417,6 +417,8 @@ Anthropic, OpenAI, Google (Gemini), OpenRouter, GitHub Copilot, Amazon Bedrock,
|
|
|
417
417
|
|
|
418
418
|
If you have a **Claude Max**, **Codex**, or **GitHub Copilot** subscription, you can use those directly — Pi handles the OAuth flow. No API key needed.
|
|
419
419
|
|
|
420
|
+
> **Note:** Using OAuth tokens from subscription plans (e.g. Claude Max) outside their native applications may not be explicitly permitted by the provider's Terms of Service. GSD supports API key authentication for all providers as an alternative. Use at your own discretion.
|
|
421
|
+
|
|
420
422
|
### OpenRouter
|
|
421
423
|
|
|
422
424
|
[OpenRouter](https://openrouter.ai) gives you access to hundreds of models through a single API key. Use it to run GSD with Llama, DeepSeek, Qwen, or anything else OpenRouter supports.
|
package/dist/loader.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native fuzzy text matching and diff generation for the edit tool.
|
|
3
|
+
*
|
|
4
|
+
* Uses the `similar` Rust crate (Myers' algorithm) for O(n+d) diffing,
|
|
5
|
+
* and single-pass Unicode normalization for fuzzy matching.
|
|
6
|
+
*/
|
|
7
|
+
import type { DiffResult, FuzzyMatchResult } from "./types.js";
|
|
8
|
+
export type { DiffResult, FuzzyMatchResult };
|
|
9
|
+
/**
|
|
10
|
+
* Normalize text for fuzzy matching:
|
|
11
|
+
* - Strip trailing whitespace from each line
|
|
12
|
+
* - Smart quotes to ASCII equivalents
|
|
13
|
+
* - Unicode dashes/hyphens to ASCII hyphen
|
|
14
|
+
* - Special Unicode spaces to regular space
|
|
15
|
+
*/
|
|
16
|
+
export declare function normalizeForFuzzyMatch(text: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Find `oldText` in `content`, trying exact match first, then fuzzy match.
|
|
19
|
+
*
|
|
20
|
+
* When fuzzy matching is used, `contentForReplacement` is the normalized
|
|
21
|
+
* version of `content`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function fuzzyFindText(content: string, oldText: string): FuzzyMatchResult;
|
|
24
|
+
/**
|
|
25
|
+
* Generate a unified diff string with line numbers and context.
|
|
26
|
+
*
|
|
27
|
+
* Uses Myers' diff algorithm via the `similar` Rust crate.
|
|
28
|
+
*
|
|
29
|
+
* @param oldContent Original text
|
|
30
|
+
* @param newContent Modified text
|
|
31
|
+
* @param contextLines Number of context lines around changes (default: 4)
|
|
32
|
+
*/
|
|
33
|
+
export declare function generateDiff(oldContent: string, newContent: string, contextLines?: number): DiffResult;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native fuzzy text matching and diff generation for the edit tool.
|
|
3
|
+
*
|
|
4
|
+
* Uses the `similar` Rust crate (Myers' algorithm) for O(n+d) diffing,
|
|
5
|
+
* and single-pass Unicode normalization for fuzzy matching.
|
|
6
|
+
*/
|
|
7
|
+
import { native } from "../native.js";
|
|
8
|
+
/**
|
|
9
|
+
* Normalize text for fuzzy matching:
|
|
10
|
+
* - Strip trailing whitespace from each line
|
|
11
|
+
* - Smart quotes to ASCII equivalents
|
|
12
|
+
* - Unicode dashes/hyphens to ASCII hyphen
|
|
13
|
+
* - Special Unicode spaces to regular space
|
|
14
|
+
*/
|
|
15
|
+
export function normalizeForFuzzyMatch(text) {
|
|
16
|
+
return native.normalizeForFuzzyMatch(text);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Find `oldText` in `content`, trying exact match first, then fuzzy match.
|
|
20
|
+
*
|
|
21
|
+
* When fuzzy matching is used, `contentForReplacement` is the normalized
|
|
22
|
+
* version of `content`.
|
|
23
|
+
*/
|
|
24
|
+
export function fuzzyFindText(content, oldText) {
|
|
25
|
+
return native.fuzzyFindText(content, oldText);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Generate a unified diff string with line numbers and context.
|
|
29
|
+
*
|
|
30
|
+
* Uses Myers' diff algorithm via the `similar` Rust crate.
|
|
31
|
+
*
|
|
32
|
+
* @param oldContent Original text
|
|
33
|
+
* @param newContent Modified text
|
|
34
|
+
* @param contextLines Number of context lines around changes (default: 4)
|
|
35
|
+
*/
|
|
36
|
+
export function generateDiff(oldContent, newContent, contextLines) {
|
|
37
|
+
return native.generateDiff(oldContent, newContent, contextLines);
|
|
38
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Result of fuzzy text matching (exact match tried first, then normalized). */
|
|
2
|
+
export interface FuzzyMatchResult {
|
|
3
|
+
/** Whether a match was found. */
|
|
4
|
+
found: boolean;
|
|
5
|
+
/** UTF-16 code unit index where the match starts (-1 if not found). */
|
|
6
|
+
index: number;
|
|
7
|
+
/** Length of the matched text in UTF-16 code units (0 if not found). */
|
|
8
|
+
matchLength: number;
|
|
9
|
+
/** Whether fuzzy (normalized) matching was used instead of exact. */
|
|
10
|
+
usedFuzzyMatch: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Content to use for replacement operations.
|
|
13
|
+
* Original content when exact match; normalized content when fuzzy match.
|
|
14
|
+
*/
|
|
15
|
+
contentForReplacement: string;
|
|
16
|
+
}
|
|
17
|
+
/** Result of unified diff generation. */
|
|
18
|
+
export interface DiffResult {
|
|
19
|
+
/** The unified diff string with line numbers. */
|
|
20
|
+
diff: string;
|
|
21
|
+
/** Line number of the first change in the new file (undefined if no changes). */
|
|
22
|
+
firstChangedLine: number | undefined;
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -28,7 +28,13 @@ export { htmlToMarkdown } from "./html/index.js";
|
|
|
28
28
|
export type { HtmlToMarkdownOptions } from "./html/index.js";
|
|
29
29
|
export { wrapTextWithAnsi, truncateToWidth, sliceWithWidth, extractSegments, sanitizeText, visibleWidth, EllipsisKind, } from "./text/index.js";
|
|
30
30
|
export type { SliceResult, ExtractSegmentsResult } from "./text/index.js";
|
|
31
|
+
export { normalizeForFuzzyMatch, fuzzyFindText, generateDiff, } from "./diff/index.js";
|
|
32
|
+
export type { FuzzyMatchResult, DiffResult } from "./diff/index.js";
|
|
31
33
|
export { fuzzyFind } from "./fd/index.js";
|
|
32
34
|
export type { FuzzyFindMatch, FuzzyFindOptions, FuzzyFindResult, } from "./fd/index.js";
|
|
33
35
|
export { parseImage, ImageFormat, SamplingFilter } from "./image/index.js";
|
|
34
36
|
export type { NativeImageHandle } from "./image/index.js";
|
|
37
|
+
export { ttsrCompileRules, ttsrCheckBuffer, ttsrFreeRules } from "./ttsr/index.js";
|
|
38
|
+
export type { TtsrHandle, TtsrRuleInput } from "./ttsr/index.js";
|
|
39
|
+
export { parseFrontmatter, extractSection as nativeExtractSection, extractAllSections, batchParseGsdFiles, parseRoadmapFile, } from "./gsd-parser/index.js";
|
|
40
|
+
export type { BatchParseResult, FrontmatterResult, NativeBoundaryMapEntry, NativeRoadmap, NativeRoadmapSlice, ParsedGsdFile, SectionResult, } from "./gsd-parser/index.js";
|
|
@@ -21,5 +21,8 @@ export { glob, invalidateFsScanCache } from "./glob/index.js";
|
|
|
21
21
|
export { astGrep, astEdit } from "./ast/index.js";
|
|
22
22
|
export { htmlToMarkdown } from "./html/index.js";
|
|
23
23
|
export { wrapTextWithAnsi, truncateToWidth, sliceWithWidth, extractSegments, sanitizeText, visibleWidth, EllipsisKind, } from "./text/index.js";
|
|
24
|
+
export { normalizeForFuzzyMatch, fuzzyFindText, generateDiff, } from "./diff/index.js";
|
|
24
25
|
export { fuzzyFind } from "./fd/index.js";
|
|
25
26
|
export { parseImage, ImageFormat, SamplingFilter } from "./image/index.js";
|
|
27
|
+
export { ttsrCompileRules, ttsrCheckBuffer, ttsrFreeRules } from "./ttsr/index.js";
|
|
28
|
+
export { parseFrontmatter, extractSection as nativeExtractSection, extractAllSections, batchParseGsdFiles, parseRoadmapFile, } from "./gsd-parser/index.js";
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
* Native addon loader.
|
|
3
3
|
*
|
|
4
4
|
* Locates and loads the compiled Rust N-API addon (`.node` file).
|
|
5
|
-
*
|
|
5
|
+
* Resolution order:
|
|
6
|
+
* 1. @gsd-build/engine-{platform} npm optional dependency (production install)
|
|
7
|
+
* 2. native/addon/gsd_engine.{platform}.node (local release build)
|
|
8
|
+
* 3. native/addon/gsd_engine.dev.node (local debug build)
|
|
6
9
|
*/
|
|
7
10
|
export declare const native: {
|
|
8
11
|
search: (content: Buffer | Uint8Array, options: unknown) => unknown;
|
|
@@ -29,5 +32,16 @@ export declare const native: {
|
|
|
29
32
|
sanitizeText: (text: string) => string;
|
|
30
33
|
visibleWidth: (text: string, tabWidth?: number) => number;
|
|
31
34
|
fuzzyFind: (options: unknown) => unknown;
|
|
35
|
+
normalizeForFuzzyMatch: (text: string) => string;
|
|
36
|
+
fuzzyFindText: (content: string, oldText: string) => unknown;
|
|
37
|
+
generateDiff: (oldContent: string, newContent: string, contextLines?: number) => unknown;
|
|
32
38
|
NativeImage: unknown;
|
|
39
|
+
ttsrCompileRules: (rules: unknown[]) => number;
|
|
40
|
+
ttsrCheckBuffer: (handle: number, buffer: string) => string[];
|
|
41
|
+
ttsrFreeRules: (handle: number) => void;
|
|
42
|
+
parseFrontmatter: (content: string) => unknown;
|
|
43
|
+
extractSection: (content: string, heading: string, level?: number) => unknown;
|
|
44
|
+
extractAllSections: (content: string, level?: number) => string;
|
|
45
|
+
batchParseGsdFiles: (directory: string) => unknown;
|
|
46
|
+
parseRoadmapFile: (content: string) => unknown;
|
|
33
47
|
};
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
* Native addon loader.
|
|
3
3
|
*
|
|
4
4
|
* Locates and loads the compiled Rust N-API addon (`.node` file).
|
|
5
|
-
*
|
|
5
|
+
* Resolution order:
|
|
6
|
+
* 1. @gsd-build/engine-{platform} npm optional dependency (production install)
|
|
7
|
+
* 2. native/addon/gsd_engine.{platform}.node (local release build)
|
|
8
|
+
* 3. native/addon/gsd_engine.dev.node (local debug build)
|
|
6
9
|
*/
|
|
7
10
|
import { createRequire } from "node:module";
|
|
8
11
|
import * as path from "node:path";
|
|
@@ -11,24 +14,51 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
11
14
|
const require = createRequire(import.meta.url);
|
|
12
15
|
const addonDir = path.resolve(__dirname, "..", "..", "..", "native", "addon");
|
|
13
16
|
const platformTag = `${process.platform}-${process.arch}`;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
/** Map Node.js platform/arch to the npm package suffix */
|
|
18
|
+
const platformPackageMap = {
|
|
19
|
+
"darwin-arm64": "darwin-arm64",
|
|
20
|
+
"darwin-x64": "darwin-x64",
|
|
21
|
+
"linux-x64": "linux-x64-gnu",
|
|
22
|
+
"linux-arm64": "linux-arm64-gnu",
|
|
23
|
+
"win32-x64": "win32-x64-msvc",
|
|
24
|
+
};
|
|
18
25
|
function loadNative() {
|
|
19
26
|
const errors = [];
|
|
20
|
-
|
|
27
|
+
// 1. Try the platform-specific npm optional dependency
|
|
28
|
+
const packageSuffix = platformPackageMap[platformTag];
|
|
29
|
+
if (packageSuffix) {
|
|
21
30
|
try {
|
|
22
|
-
return require(
|
|
31
|
+
return require(`@gsd-build/engine-${packageSuffix}`);
|
|
23
32
|
}
|
|
24
33
|
catch (err) {
|
|
25
34
|
const message = err instanceof Error ? err.message : String(err);
|
|
26
|
-
errors.push(
|
|
35
|
+
errors.push(`@gsd-build/engine-${packageSuffix}: ${message}`);
|
|
27
36
|
}
|
|
28
37
|
}
|
|
38
|
+
// 2. Try local release build (native/addon/gsd_engine.{platform}.node)
|
|
39
|
+
const releasePath = path.join(addonDir, `gsd_engine.${platformTag}.node`);
|
|
40
|
+
try {
|
|
41
|
+
return require(releasePath);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
45
|
+
errors.push(`${releasePath}: ${message}`);
|
|
46
|
+
}
|
|
47
|
+
// 3. Try local dev build (native/addon/gsd_engine.dev.node)
|
|
48
|
+
const devPath = path.join(addonDir, "gsd_engine.dev.node");
|
|
49
|
+
try {
|
|
50
|
+
return require(devPath);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
54
|
+
errors.push(`${devPath}: ${message}`);
|
|
55
|
+
}
|
|
29
56
|
const details = errors.map((e) => ` - ${e}`).join("\n");
|
|
57
|
+
const supportedPlatforms = Object.keys(platformPackageMap);
|
|
30
58
|
throw new Error(`Failed to load gsd_engine native addon for ${platformTag}.\n\n` +
|
|
31
59
|
`Tried:\n${details}\n\n` +
|
|
32
|
-
`
|
|
60
|
+
`Supported platforms: ${supportedPlatforms.join(", ")}\n` +
|
|
61
|
+
`If your platform is listed, try reinstalling: npm i -g gsd-pi\n` +
|
|
62
|
+
`Otherwise, please open an issue: https://github.com/gsd-build/gsd-2/issues`);
|
|
33
63
|
}
|
|
34
64
|
export const native = loadNative();
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native TTSR regex engine.
|
|
3
|
+
*
|
|
4
|
+
* Pre-compiles all rule condition patterns into a single Rust RegexSet for
|
|
5
|
+
* O(1)-style matching per buffer check, replacing per-rule JS regex iteration.
|
|
6
|
+
*/
|
|
7
|
+
import type { TtsrHandle, TtsrRuleInput } from "./types.js";
|
|
8
|
+
export type { TtsrHandle, TtsrRuleInput };
|
|
9
|
+
/**
|
|
10
|
+
* Compile TTSR rules into an optimized native regex engine.
|
|
11
|
+
*
|
|
12
|
+
* Returns an opaque handle for use with `ttsrCheckBuffer` and `ttsrFreeRules`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function ttsrCompileRules(rules: TtsrRuleInput[]): TtsrHandle;
|
|
15
|
+
/**
|
|
16
|
+
* Check a buffer against compiled TTSR rules.
|
|
17
|
+
*
|
|
18
|
+
* Returns an array of unique rule names whose conditions matched.
|
|
19
|
+
* All patterns are tested in a single pass via Rust's RegexSet.
|
|
20
|
+
*/
|
|
21
|
+
export declare function ttsrCheckBuffer(handle: TtsrHandle, buffer: string): string[];
|
|
22
|
+
/**
|
|
23
|
+
* Free a compiled TTSR rule set, releasing native memory.
|
|
24
|
+
*
|
|
25
|
+
* Call when rules are no longer needed (e.g., session end).
|
|
26
|
+
*/
|
|
27
|
+
export declare function ttsrFreeRules(handle: TtsrHandle): void;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native TTSR regex engine.
|
|
3
|
+
*
|
|
4
|
+
* Pre-compiles all rule condition patterns into a single Rust RegexSet for
|
|
5
|
+
* O(1)-style matching per buffer check, replacing per-rule JS regex iteration.
|
|
6
|
+
*/
|
|
7
|
+
import { native } from "../native.js";
|
|
8
|
+
/**
|
|
9
|
+
* Compile TTSR rules into an optimized native regex engine.
|
|
10
|
+
*
|
|
11
|
+
* Returns an opaque handle for use with `ttsrCheckBuffer` and `ttsrFreeRules`.
|
|
12
|
+
*/
|
|
13
|
+
export function ttsrCompileRules(rules) {
|
|
14
|
+
return native.ttsrCompileRules(rules);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Check a buffer against compiled TTSR rules.
|
|
18
|
+
*
|
|
19
|
+
* Returns an array of unique rule names whose conditions matched.
|
|
20
|
+
* All patterns are tested in a single pass via Rust's RegexSet.
|
|
21
|
+
*/
|
|
22
|
+
export function ttsrCheckBuffer(handle, buffer) {
|
|
23
|
+
return native.ttsrCheckBuffer(handle, buffer);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Free a compiled TTSR rule set, releasing native memory.
|
|
27
|
+
*
|
|
28
|
+
* Call when rules are no longer needed (e.g., session end).
|
|
29
|
+
*/
|
|
30
|
+
export function ttsrFreeRules(handle) {
|
|
31
|
+
native.ttsrFreeRules(handle);
|
|
32
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Input rule for TTSR regex compilation. */
|
|
2
|
+
export interface TtsrRuleInput {
|
|
3
|
+
/** Unique rule name. */
|
|
4
|
+
name: string;
|
|
5
|
+
/** Regex condition patterns (any match triggers the rule). */
|
|
6
|
+
conditions: string[];
|
|
7
|
+
}
|
|
8
|
+
/** Opaque handle to a compiled TTSR rule set. */
|
|
9
|
+
export type TtsrHandle = number;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,12 +1,15 @@
|
|
|
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
|
export declare function detectLineEnding(content: string): "\r\n" | "\n";
|
|
6
9
|
export declare function normalizeToLF(text: string): string;
|
|
7
10
|
export declare function restoreLineEndings(text: string, ending: "\r\n" | "\n"): string;
|
|
8
11
|
/**
|
|
9
|
-
* Normalize text for fuzzy matching
|
|
12
|
+
* Normalize text for fuzzy matching (native Rust implementation).
|
|
10
13
|
* - Strip trailing whitespace from each line
|
|
11
14
|
* - Normalize smart quotes to ASCII equivalents
|
|
12
15
|
* - Normalize Unicode dashes/hyphens to ASCII hyphen
|
|
@@ -29,10 +32,11 @@ export interface FuzzyMatchResult {
|
|
|
29
32
|
contentForReplacement: string;
|
|
30
33
|
}
|
|
31
34
|
/**
|
|
32
|
-
* Find oldText in content, trying exact match first, then fuzzy match
|
|
35
|
+
* Find oldText in content, trying exact match first, then fuzzy match
|
|
36
|
+
* (native Rust implementation).
|
|
37
|
+
*
|
|
33
38
|
* When fuzzy matching is used, the returned contentForReplacement is the
|
|
34
|
-
* fuzzy-normalized version of the content
|
|
35
|
-
* Unicode quotes/dashes normalized to ASCII).
|
|
39
|
+
* fuzzy-normalized version of the content.
|
|
36
40
|
*/
|
|
37
41
|
export declare function fuzzyFindText(content: string, oldText: string): FuzzyMatchResult;
|
|
38
42
|
/** Strip UTF-8 BOM if present, return both the BOM (if any) and the text without it */
|
|
@@ -41,7 +45,9 @@ export declare function stripBom(content: string): {
|
|
|
41
45
|
text: string;
|
|
42
46
|
};
|
|
43
47
|
/**
|
|
44
|
-
* Generate a unified diff string with line numbers and context
|
|
48
|
+
* Generate a unified diff string with line numbers and context
|
|
49
|
+
* (native Rust implementation using Myers' algorithm via the `similar` crate).
|
|
50
|
+
*
|
|
45
51
|
* Returns both the diff string and the first changed line number (in the new file).
|
|
46
52
|
*/
|
|
47
53
|
export declare function generateDiffString(oldContent: string, newContent: string, contextLines?: number): {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"edit-diff.d.ts","sourceRoot":"","sources":["../../../src/core/tools/edit-diff.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"edit-diff.d.ts","sourceRoot":"","sources":["../../../src/core/tools/edit-diff.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAWH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM/D;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAE9E;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED,MAAM,WAAW,gBAAgB;IAChC,gCAAgC;IAChC,KAAK,EAAE,OAAO,CAAC;IACf,4FAA4F;IAC5F,KAAK,EAAE,MAAM,CAAC;IACd,iCAAiC;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,4DAA4D;IAC5D,cAAc,EAAE,OAAO,CAAC;IACxB;;;OAGG;IACH,qBAAqB,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,gBAAgB,CAEhF;AAED,uFAAuF;AACvF,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAEvE;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CACjC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,YAAY,SAAI,GACd;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAMxD;AAED,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;AAED,MAAM,WAAW,aAAa;IAC7B,KAAK,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACpC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,GACT,OAAO,CAAC,cAAc,GAAG,aAAa,CAAC,CA6DzC"}
|
|
@@ -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":"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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"]}
|