Turbo edits v2 (#1653)
Fixes #1222 #1646 TODOs - [x] description? - [x] collect errors across all files for turbo edits - [x] be forgiving around whitespaces - [x] write e2e tests - [x] do more manual testing across different models <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds Turbo Edits v2 search-replace flow with settings/UI selector, parser/renderer, dry-run validation + fallback, proposal integration, and comprehensive tests; updates licensing. > > - **Engine/Processing**: > - Add `dyad-search-replace` end-to-end: parsing (`getDyadSearchReplaceTags`), markdown rendering (`DyadSearchReplace`), and application (`applySearchReplace`) with dry-run validation and fallback to `dyad-write`. > - Inject Turbo Edits v2 system prompt; toggle via `isTurboEditsV2Enabled`; disable classic lazy edits when v2 is on. > - Include search-replace edits in proposals and full-response processing. > - **Settings/UI**: > - Introduce `proLazyEditsMode` (`off`|`v1`|`v2`) and helper selectors; update `ProModeSelector` with Turbo Edits and Smart Context selectors (`data-testid`s). > - **LLM/token flow**: > - Construct system prompt conditionally; update token counting and chat stream to validate and repair search-replace responses. > - **Tests**: > - Add unit tests for search-replace processor; e2e tests for Turbo Edits v2 and options; fixtures and snapshots. > - **Licensing/Docs**: > - Add `src/pro/LICENSE` (FSL 1.1 ALv2 future), update root `LICENSE` and README license section. > - **Tooling**: > - Update `.prettierignore`; enhance test helpers (selectors, path normalization, snapshot filtering). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 7aefa02bfae2fe22a25c7d87f3c4c326f820f1e6. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
This commit is contained in:
105
src/pro/LICENSE
Normal file
105
src/pro/LICENSE
Normal file
@@ -0,0 +1,105 @@
|
||||
# Functional Source License, Version 1.1, ALv2 Future License
|
||||
|
||||
## Abbreviation
|
||||
|
||||
FSL-1.1-ALv2
|
||||
|
||||
## Notice
|
||||
|
||||
Copyright 2025 Dyad Tech, Inc.
|
||||
|
||||
## Terms and Conditions
|
||||
|
||||
### Licensor ("We")
|
||||
|
||||
The party offering the Software under these Terms and Conditions.
|
||||
|
||||
### The Software
|
||||
|
||||
The "Software" is each version of the software that we make available under
|
||||
these Terms and Conditions, as indicated by our inclusion of these Terms and
|
||||
Conditions with the Software.
|
||||
|
||||
### License Grant
|
||||
|
||||
Subject to your compliance with this License Grant and the Patents,
|
||||
Redistribution and Trademark clauses below, we hereby grant you the right to
|
||||
use, copy, modify, create derivative works, publicly perform, publicly display
|
||||
and redistribute the Software for any Permitted Purpose identified below.
|
||||
|
||||
### Permitted Purpose
|
||||
|
||||
A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
|
||||
means making the Software available to others in a commercial product or
|
||||
service that:
|
||||
|
||||
1. substitutes for the Software;
|
||||
|
||||
2. substitutes for any other product or service we offer using the Software
|
||||
that exists as of the date we make the Software available; or
|
||||
|
||||
3. offers the same or substantially similar functionality as the Software.
|
||||
|
||||
Permitted Purposes specifically include using the Software:
|
||||
|
||||
1. for your internal use and access;
|
||||
|
||||
2. for non-commercial education;
|
||||
|
||||
3. for non-commercial research; and
|
||||
|
||||
4. in connection with professional services that you provide to a licensee
|
||||
using the Software in accordance with these Terms and Conditions.
|
||||
|
||||
### Patents
|
||||
|
||||
To the extent your use for a Permitted Purpose would necessarily infringe our
|
||||
patents, the license grant above includes a license under our patents. If you
|
||||
make a claim against any party that the Software infringes or contributes to
|
||||
the infringement of any patent, then your patent license to the Software ends
|
||||
immediately.
|
||||
|
||||
### Redistribution
|
||||
|
||||
The Terms and Conditions apply to all copies, modifications and derivatives of
|
||||
the Software.
|
||||
|
||||
If you redistribute any copies, modifications or derivatives of the Software,
|
||||
you must include a copy of or a link to these Terms and Conditions and not
|
||||
remove any copyright notices provided in or with the Software.
|
||||
|
||||
### Disclaimer
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
|
||||
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
|
||||
|
||||
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
|
||||
SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
|
||||
EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
|
||||
|
||||
### Trademarks
|
||||
|
||||
Except for displaying the License Details and identifying us as the origin of
|
||||
the Software, you have no right under these Terms and Conditions to use our
|
||||
trademarks, trade names, service marks or product names.
|
||||
|
||||
## Grant of Future License
|
||||
|
||||
We hereby irrevocably grant you an additional license to use the Software under
|
||||
the Apache License, Version 2.0 that is effective on the second anniversary of
|
||||
the date we make the Software available. On or after that date, you may use the
|
||||
Software under the Apache License, Version 2.0, in which case the following
|
||||
will apply:
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
||||
this file except in compliance with the License.
|
||||
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed
|
||||
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations under the License.
|
||||
301
src/pro/main/ipc/processors/search_replace_processor.spec.ts
Normal file
301
src/pro/main/ipc/processors/search_replace_processor.spec.ts
Normal file
@@ -0,0 +1,301 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { applySearchReplace } from "@/pro/main/ipc/processors/search_replace_processor";
|
||||
import { parseSearchReplaceBlocks } from "@/pro/shared/search_replace_parser";
|
||||
|
||||
describe("search_replace_processor - parseSearchReplaceBlocks", () => {
|
||||
it("parses multiple blocks with start_line in ascending order", () => {
|
||||
const diff = `
|
||||
<<<<<<< SEARCH
|
||||
line one
|
||||
=======
|
||||
LINE ONE
|
||||
>>>>>>> REPLACE
|
||||
|
||||
<<<<<<< SEARCH
|
||||
line four
|
||||
=======
|
||||
LINE FOUR
|
||||
>>>>>>> REPLACE
|
||||
`;
|
||||
const blocks = parseSearchReplaceBlocks(diff);
|
||||
expect(blocks.length).toBe(2);
|
||||
expect(blocks[0].searchContent.trim()).toBe("line one");
|
||||
expect(blocks[0].replaceContent.trim()).toBe("LINE ONE");
|
||||
});
|
||||
});
|
||||
|
||||
describe("search_replace_processor - applySearchReplace", () => {
|
||||
it("applies single block with exact start_line match", () => {
|
||||
const original = [
|
||||
"def calculate_total(items):",
|
||||
" total = 0",
|
||||
" for item in items:",
|
||||
" total += item",
|
||||
" return total",
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
const diff = `
|
||||
<<<<<<< SEARCH
|
||||
def calculate_total(items):
|
||||
total = 0
|
||||
=======
|
||||
def calculate_sum(items):
|
||||
total = 0
|
||||
>>>>>>> REPLACE
|
||||
`;
|
||||
|
||||
const { success, content } = applySearchReplace(original, diff);
|
||||
expect(success).toBe(true);
|
||||
expect(content).toContain("def calculate_sum(items):");
|
||||
expect(content).not.toContain("def calculate_total(items):");
|
||||
});
|
||||
|
||||
it("falls back to global exact search when start_line missing", () => {
|
||||
const original = ["alpha", "beta", "gamma"].join("\n");
|
||||
const diff = `
|
||||
<<<<<<< SEARCH
|
||||
beta
|
||||
=======
|
||||
BETA
|
||||
>>>>>>> REPLACE
|
||||
`;
|
||||
const { success, content } = applySearchReplace(original, diff);
|
||||
expect(success).toBe(true);
|
||||
expect(content).toBe(["alpha", "BETA", "gamma"].join("\n"));
|
||||
});
|
||||
|
||||
it("applies multiple blocks in order and accounts for line deltas", () => {
|
||||
const original = ["1", "2", "3", "4", "5"].join("\n");
|
||||
const diff = `
|
||||
<<<<<<< SEARCH
|
||||
1
|
||||
=======
|
||||
ONE\nONE-EXTRA
|
||||
>>>>>>> REPLACE
|
||||
|
||||
<<<<<<< SEARCH
|
||||
4
|
||||
=======
|
||||
FOUR
|
||||
>>>>>>> REPLACE
|
||||
`;
|
||||
const { success, content } = applySearchReplace(original, diff);
|
||||
expect(success).toBe(true);
|
||||
expect(content).toBe(
|
||||
["ONE", "ONE-EXTRA", "2", "3", "FOUR", "5"].join("\n"),
|
||||
);
|
||||
});
|
||||
|
||||
it("detects and strips line-numbered content, inferring start line when omitted", () => {
|
||||
const original = ["a", "b", "c", "d"].join("\n");
|
||||
const diff = `
|
||||
<<<<<<< SEARCH
|
||||
a\nb
|
||||
=======
|
||||
A\nB
|
||||
>>>>>>> REPLACE
|
||||
`;
|
||||
const { success, content } = applySearchReplace(original, diff);
|
||||
expect(success).toBe(true);
|
||||
expect(content).toBe(["A", "B", "c", "d"].join("\n"));
|
||||
});
|
||||
|
||||
it("preserves indentation relative to matched block", () => {
|
||||
const original = [
|
||||
"function test() {",
|
||||
" if (x) {",
|
||||
" doThing();",
|
||||
" }",
|
||||
"}",
|
||||
].join("\n");
|
||||
const diff = `
|
||||
<<<<<<< SEARCH
|
||||
if (x) {
|
||||
doThing();
|
||||
=======
|
||||
if (x) {
|
||||
doOther();
|
||||
doAnother();
|
||||
>>>>>>> REPLACE
|
||||
`;
|
||||
const { success, content } = applySearchReplace(original, diff);
|
||||
expect(success).toBe(true);
|
||||
// The replacement lines should keep the base indent of two spaces (from matched block)
|
||||
expect(content).toContain(" if (x) {");
|
||||
expect(content).toContain(" doOther();");
|
||||
expect(content).toContain(" doAnother();");
|
||||
});
|
||||
|
||||
it("supports deletions when replace content is empty", () => {
|
||||
const original = ["x", "y", "z"].join("\n");
|
||||
const diff = `
|
||||
<<<<<<< SEARCH
|
||||
y
|
||||
=======
|
||||
|
||||
>>>>>>> REPLACE
|
||||
`;
|
||||
const { success, content } = applySearchReplace(original, diff);
|
||||
expect(success).toBe(true);
|
||||
expect(content).toBe(["x", "z"].join("\n"));
|
||||
});
|
||||
|
||||
it("preserves CRLF line endings", () => {
|
||||
const original = ["a", "b", "c"].join("\r\n");
|
||||
const diff = `
|
||||
<<<<<<< SEARCH
|
||||
b
|
||||
=======
|
||||
B
|
||||
>>>>>>> REPLACE
|
||||
`;
|
||||
const { success, content } = applySearchReplace(original, diff);
|
||||
expect(success).toBe(true);
|
||||
expect(content).toBe(["a", "B", "c"].join("\r\n"));
|
||||
});
|
||||
|
||||
it("unescapes markers inside content and matches literally", () => {
|
||||
const original = ["begin", ">>>>>>> REPLACE", "end"].join("\n");
|
||||
const diff = `
|
||||
<<<<<<< SEARCH
|
||||
\\>>>>>>> REPLACE
|
||||
=======
|
||||
LITERAL MARKER
|
||||
>>>>>>> REPLACE
|
||||
`;
|
||||
const { success, content } = applySearchReplace(original, diff);
|
||||
expect(success).toBe(true);
|
||||
expect(content).toBe(["begin", "LITERAL MARKER", "end"].join("\n"));
|
||||
});
|
||||
|
||||
it("errors when SEARCH block does not match any content", () => {
|
||||
const original = "foo\nbar\nbaz";
|
||||
const diff = `
|
||||
<<<<<<< SEARCH
|
||||
NOT IN FILE
|
||||
=======
|
||||
STILL NOT
|
||||
>>>>>>> REPLACE
|
||||
`;
|
||||
const { success, error } = applySearchReplace(original, diff);
|
||||
expect(success).toBe(false);
|
||||
expect(error).toMatch(/Search block did not match any content/i);
|
||||
});
|
||||
|
||||
it("matches despite differing indentation and trailing whitespace", () => {
|
||||
const original = [
|
||||
"\tfunction example() {",
|
||||
"\t doThing(); ", // extra trailing spaces
|
||||
"\t}",
|
||||
].join("\n");
|
||||
|
||||
const diff = `
|
||||
<<<<<<< SEARCH
|
||||
function example() {
|
||||
doThing();
|
||||
}
|
||||
=======
|
||||
function example() {
|
||||
doOther();
|
||||
}
|
||||
>>>>>>> REPLACE
|
||||
`;
|
||||
|
||||
const { success, content } = applySearchReplace(original, diff);
|
||||
expect(success).toBe(true);
|
||||
expect(content).toContain("doOther();");
|
||||
expect(content).not.toContain("doThing();");
|
||||
});
|
||||
|
||||
it("matches when search uses spaces and target uses tabs (and vice versa)", () => {
|
||||
const original = ["\tif (ready) {", "\t\tstart();", "\t}"].join("\n");
|
||||
|
||||
const diff = `
|
||||
<<<<<<< SEARCH
|
||||
if (ready) {
|
||||
start();
|
||||
}
|
||||
=======
|
||||
if (ready) {
|
||||
launch();
|
||||
}
|
||||
>>>>>>> REPLACE
|
||||
`;
|
||||
|
||||
const { success, content } = applySearchReplace(original, diff);
|
||||
expect(success).toBe(true);
|
||||
expect(content).toContain("launch();");
|
||||
expect(content).not.toContain("start();");
|
||||
});
|
||||
|
||||
it("errors when SEARCH and REPLACE blocks are identical", () => {
|
||||
const original = ["x", "y", "z"].join("\n");
|
||||
const diff = `
|
||||
<<<<<<< SEARCH
|
||||
middle
|
||||
=======
|
||||
middle
|
||||
>>>>>>> REPLACE
|
||||
`;
|
||||
const { success, error } = applySearchReplace(original, diff);
|
||||
expect(success).toBe(false);
|
||||
expect(error).toMatch(/Search and replace blocks are identical/i);
|
||||
});
|
||||
|
||||
it("errors when SEARCH block matches multiple locations (ambiguous)", () => {
|
||||
const original = ["foo", "bar", "baz", "bar", "qux"].join("\n");
|
||||
|
||||
const diff = `
|
||||
<<<<<<< SEARCH
|
||||
bar
|
||||
=======
|
||||
BAR
|
||||
>>>>>>> REPLACE
|
||||
`;
|
||||
|
||||
const { success, error } = applySearchReplace(original, diff);
|
||||
expect(success).toBe(false);
|
||||
expect(error).toMatch(/(ambiguous|multiple)/i);
|
||||
});
|
||||
|
||||
it("errors when SEARCH block fuzzy matches multiple locations (ambiguous)", () => {
|
||||
const original = [
|
||||
"\tif (ready) {",
|
||||
"\t\tstart(); ",
|
||||
"\t}",
|
||||
" if (ready) {",
|
||||
" start(); ",
|
||||
" }",
|
||||
].join("\n");
|
||||
|
||||
const diff = `
|
||||
<<<<<<< SEARCH
|
||||
if (ready) {
|
||||
start();
|
||||
}
|
||||
=======
|
||||
if (ready) {
|
||||
launch();
|
||||
}
|
||||
>>>>>>> REPLACE
|
||||
`;
|
||||
|
||||
const { success, error } = applySearchReplace(original, diff);
|
||||
expect(success).toBe(false);
|
||||
expect(error).toMatch(/fuzzy matched/i);
|
||||
});
|
||||
|
||||
it("errors when SEARCH block is empty", () => {
|
||||
const original = ["a", "b"].join("\n");
|
||||
const diff = `
|
||||
<<<<<<< SEARCH
|
||||
=======
|
||||
REPLACEMENT
|
||||
>>>>>>> REPLACE
|
||||
`;
|
||||
const { success, error } = applySearchReplace(original, diff);
|
||||
expect(success).toBe(false);
|
||||
expect(error).toMatch(/empty SEARCH block is not allowed/i);
|
||||
});
|
||||
});
|
||||
175
src/pro/main/ipc/processors/search_replace_processor.ts
Normal file
175
src/pro/main/ipc/processors/search_replace_processor.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
/* eslint-disable no-irregular-whitespace */
|
||||
|
||||
import { parseSearchReplaceBlocks } from "@/pro/shared/search_replace_parser";
|
||||
|
||||
function unescapeMarkers(content: string): string {
|
||||
return content
|
||||
.replace(/^\\<<<<<<</gm, "<<<<<<<")
|
||||
.replace(/^\\=======/gm, "=======")
|
||||
.replace(/^\\>>>>>>>/gm, ">>>>>>>");
|
||||
}
|
||||
|
||||
export function applySearchReplace(
|
||||
originalContent: string,
|
||||
diffContent: string,
|
||||
): {
|
||||
success: boolean;
|
||||
content?: string;
|
||||
error?: string;
|
||||
} {
|
||||
const blocks = parseSearchReplaceBlocks(diffContent);
|
||||
if (blocks.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
"Invalid diff format - missing required sections. Expected <<<<<<< SEARCH / ======= / >>>>>>> REPLACE",
|
||||
};
|
||||
}
|
||||
|
||||
const lineEnding = originalContent.includes("\r\n") ? "\r\n" : "\n";
|
||||
let resultLines = originalContent.split(/\r?\n/);
|
||||
let appliedCount = 0;
|
||||
|
||||
for (const block of blocks) {
|
||||
let { searchContent, replaceContent } = block;
|
||||
|
||||
// Normalize markers and strip line numbers if present on all lines
|
||||
searchContent = unescapeMarkers(searchContent);
|
||||
replaceContent = unescapeMarkers(replaceContent);
|
||||
|
||||
let searchLines = searchContent === "" ? [] : searchContent.split(/\r?\n/);
|
||||
let replaceLines =
|
||||
replaceContent === "" ? [] : replaceContent.split(/\r?\n/);
|
||||
|
||||
if (searchLines.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Invalid diff format - empty SEARCH block is not allowed",
|
||||
};
|
||||
}
|
||||
|
||||
// If search and replace are identical, it's a no-op and should be treated as an error
|
||||
if (searchLines.join("\n") === replaceLines.join("\n")) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Search and replace blocks are identical",
|
||||
};
|
||||
}
|
||||
|
||||
let matchIndex = -1;
|
||||
|
||||
const target = searchLines.join("\n");
|
||||
const hay = resultLines.join("\n");
|
||||
|
||||
// Try exact string matching first and detect ambiguity
|
||||
const exactPositions: number[] = [];
|
||||
let fromIndex = 0;
|
||||
while (true) {
|
||||
const found = hay.indexOf(target, fromIndex);
|
||||
if (found === -1) break;
|
||||
exactPositions.push(found);
|
||||
fromIndex = found + 1;
|
||||
}
|
||||
|
||||
if (exactPositions.length > 1) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
"Search block matched multiple locations in the target file (ambiguous)",
|
||||
};
|
||||
}
|
||||
if (exactPositions.length === 1) {
|
||||
const pos = exactPositions[0];
|
||||
matchIndex = hay.substring(0, pos).split("\n").length - 1;
|
||||
}
|
||||
|
||||
if (matchIndex === -1) {
|
||||
// Lenient fallback: ignore leading indentation and trailing whitespace
|
||||
const normalizeForMatch = (line: string) =>
|
||||
line.replace(/^[\t ]*/, "").replace(/[\t ]+$/, "");
|
||||
|
||||
const normalizedSearch = searchLines.map(normalizeForMatch);
|
||||
|
||||
const candidates: number[] = [];
|
||||
for (let i = 0; i <= resultLines.length - searchLines.length; i++) {
|
||||
let allMatch = true;
|
||||
for (let j = 0; j < searchLines.length; j++) {
|
||||
if (normalizeForMatch(resultLines[i + j]) !== normalizedSearch[j]) {
|
||||
allMatch = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (allMatch) {
|
||||
candidates.push(i);
|
||||
if (candidates.length > 1) break; // we only care if >1 for ambiguity
|
||||
}
|
||||
}
|
||||
|
||||
if (candidates.length > 1) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
"Search block fuzzy matched multiple locations in the target file (ambiguous)",
|
||||
};
|
||||
}
|
||||
|
||||
if (candidates.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Search block did not match any content in the target file",
|
||||
};
|
||||
}
|
||||
|
||||
matchIndex = candidates[0];
|
||||
}
|
||||
|
||||
const matchedLines = resultLines.slice(
|
||||
matchIndex,
|
||||
matchIndex + searchLines.length,
|
||||
);
|
||||
|
||||
// Preserve indentation relative to first matched line
|
||||
const originalIndents = matchedLines.map((line) => {
|
||||
const m = line.match(/^[\t ]*/);
|
||||
return m ? m[0] : "";
|
||||
});
|
||||
const searchIndents = searchLines.map((line) => {
|
||||
const m = line.match(/^[\t ]*/);
|
||||
return m ? m[0] : "";
|
||||
});
|
||||
|
||||
const indentedReplaceLines = replaceLines.map((line) => {
|
||||
const matchedIndent = originalIndents[0] || "";
|
||||
const currentIndentMatch = line.match(/^[\t ]*/);
|
||||
const currentIndent = currentIndentMatch ? currentIndentMatch[0] : "";
|
||||
const searchBaseIndent = searchIndents[0] || "";
|
||||
|
||||
const searchBaseLevel = searchBaseIndent.length;
|
||||
const currentLevel = currentIndent.length;
|
||||
const relativeLevel = currentLevel - searchBaseLevel;
|
||||
|
||||
const finalIndent =
|
||||
relativeLevel < 0
|
||||
? matchedIndent.slice(
|
||||
0,
|
||||
Math.max(0, matchedIndent.length + relativeLevel),
|
||||
)
|
||||
: matchedIndent + currentIndent.slice(searchBaseLevel);
|
||||
|
||||
return finalIndent + line.trim();
|
||||
});
|
||||
|
||||
const beforeMatch = resultLines.slice(0, matchIndex);
|
||||
const afterMatch = resultLines.slice(matchIndex + searchLines.length);
|
||||
resultLines = [...beforeMatch, ...indentedReplaceLines, ...afterMatch];
|
||||
appliedCount++;
|
||||
}
|
||||
|
||||
if (appliedCount === 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: "No search/replace blocks could be applied",
|
||||
};
|
||||
}
|
||||
return { success: true, content: resultLines.join(lineEnding) };
|
||||
}
|
||||
94
src/pro/main/prompts/turbo_edits_v2_prompt.ts
Normal file
94
src/pro/main/prompts/turbo_edits_v2_prompt.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
// This approach is inspired by Roo Code
|
||||
// https://github.com/RooCodeInc/Roo-Code/blob/fceb4130478b20de2bc854c8dd0aad743f844b53/src/core/diff/strategies/multi-search-replace.ts#L4
|
||||
// but we've modified it to be simpler and not rely on line numbers.
|
||||
//
|
||||
// Also, credit to https://aider.chat/ for popularizing this approach
|
||||
|
||||
export const TURBO_EDITS_V2_SYSTEM_PROMPT = `
|
||||
|
||||
# Search-replace file edits
|
||||
|
||||
- Request to apply PRECISE, TARGETED modifications to an existing file by searching for specific sections of content and replacing them. This tool is for SURGICAL EDITS ONLY - specific changes to existing code.
|
||||
- You can perform multiple distinct search and replace operations within a single \`dyad-search-replace\` call by providing multiple SEARCH/REPLACE blocks. This is the preferred way to make several targeted changes efficiently.
|
||||
- The SEARCH section must match exactly ONE existing content section - it must be unique within the file, including whitespace and indentation.
|
||||
- When applying the diffs, be extra careful to remember to change any closing brackets or other syntax that may be affected by the diff farther down in the file.
|
||||
- ALWAYS make as many changes in a single 'dyad-search-replace' call as possible using multiple SEARCH/REPLACE blocks.
|
||||
- Do not use both \`dyad-write\` and \`dyad-search-replace\` on the same file within a single response.
|
||||
- Include a brief description of the changes you are making in the \`description\` parameter.
|
||||
|
||||
Diff format:
|
||||
\`\`\`
|
||||
<<<<<<< SEARCH
|
||||
[exact content to find including whitespace]
|
||||
=======
|
||||
[new content to replace with]
|
||||
>>>>>>> REPLACE
|
||||
\`\`\`
|
||||
|
||||
Example:
|
||||
|
||||
Original file:
|
||||
\`\`\`
|
||||
def calculate_total(items):
|
||||
total = 0
|
||||
for item in items:
|
||||
total += item
|
||||
return total
|
||||
\`\`\`
|
||||
|
||||
Search/Replace content:
|
||||
\`\`\`
|
||||
<<<<<<< SEARCH
|
||||
def calculate_total(items):
|
||||
total = 0
|
||||
for item in items:
|
||||
total += item
|
||||
return total
|
||||
=======
|
||||
def calculate_total(items):
|
||||
"""Calculate total with 10% markup"""
|
||||
return sum(item * 1.1 for item in items)
|
||||
>>>>>>> REPLACE
|
||||
|
||||
\`\`\`
|
||||
|
||||
Search/Replace content with multiple edits:
|
||||
\`\`\`
|
||||
<<<<<<< SEARCH
|
||||
def calculate_total(items):
|
||||
sum = 0
|
||||
=======
|
||||
def calculate_sum(items):
|
||||
sum = 0
|
||||
>>>>>>> REPLACE
|
||||
|
||||
<<<<<<< SEARCH
|
||||
total += item
|
||||
return total
|
||||
=======
|
||||
sum += item
|
||||
return sum
|
||||
>>>>>>> REPLACE
|
||||
\`\`\`
|
||||
|
||||
|
||||
Usage:
|
||||
<dyad-search-replace path="path/to/file.js" description="Brief description of the changes you are making">
|
||||
<<<<<<< SEARCH
|
||||
def calculate_total(items):
|
||||
sum = 0
|
||||
=======
|
||||
def calculate_sum(items):
|
||||
sum = 0
|
||||
>>>>>>> REPLACE
|
||||
|
||||
<<<<<<< SEARCH
|
||||
total += item
|
||||
return total
|
||||
=======
|
||||
sum += item
|
||||
return sum
|
||||
>>>>>>> REPLACE
|
||||
</dyad-search-replace>
|
||||
|
||||
`;
|
||||
17
src/pro/shared/search_replace_parser.ts
Normal file
17
src/pro/shared/search_replace_parser.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export type SearchReplaceBlock = {
|
||||
searchContent: string;
|
||||
replaceContent: string;
|
||||
};
|
||||
|
||||
const BLOCK_REGEX =
|
||||
/(?:^|\n)<<<<<<<\s+SEARCH>?\s*\n([\s\S]*?)(?:\n)?(?:(?<=\n)(?<!\\)=======\s*\n)([\s\S]*?)(?:\n)?(?:(?<=\n)(?<!\\)>>>>>>>\s+REPLACE)(?=\n|$)/g;
|
||||
|
||||
export function parseSearchReplaceBlocks(
|
||||
diffContent: string,
|
||||
): SearchReplaceBlock[] {
|
||||
const matches = [...diffContent.matchAll(BLOCK_REGEX)];
|
||||
return matches.map((m) => ({
|
||||
searchContent: m[1] ?? "",
|
||||
replaceContent: m[2] ?? "",
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user