mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 20:31:37 -05:00
Use original file when using write_to_file for better editing experience and avoid bugs around revertAndClose command
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
"name": "claude-dev",
|
"name": "claude-dev",
|
||||||
"displayName": "Claude Dev",
|
"displayName": "Claude Dev",
|
||||||
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, executing commands, and more with your permission every step of the way.",
|
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, executing commands, and more with your permission every step of the way.",
|
||||||
"version": "1.5.29",
|
"version": "1.5.30",
|
||||||
"icon": "icon.png",
|
"icon": "icon.png",
|
||||||
"engines": {
|
"engines": {
|
||||||
"vscode": "^1.84.0"
|
"vscode": "^1.84.0"
|
||||||
|
|||||||
268
src/ClaudeDev.ts
268
src/ClaudeDev.ts
@@ -807,6 +807,14 @@ export class ClaudeDev {
|
|||||||
.then(() => true)
|
.then(() => true)
|
||||||
.catch(() => false)
|
.catch(() => false)
|
||||||
|
|
||||||
|
// if the file is already open, ensure it's not dirty before getting its contents
|
||||||
|
if (fileExists) {
|
||||||
|
const existingDocument = vscode.workspace.textDocuments.find((doc) => doc.uri.fsPath === absolutePath)
|
||||||
|
if (existingDocument && existingDocument.isDirty) {
|
||||||
|
await existingDocument.save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let originalContent: string
|
let originalContent: string
|
||||||
if (fileExists) {
|
if (fileExists) {
|
||||||
originalContent = await fs.readFile(absolutePath, "utf-8")
|
originalContent = await fs.readFile(absolutePath, "utf-8")
|
||||||
@@ -821,6 +829,28 @@ export class ClaudeDev {
|
|||||||
|
|
||||||
const fileName = path.basename(absolutePath)
|
const fileName = path.basename(absolutePath)
|
||||||
|
|
||||||
|
// for new files, create any necessary directories and keep track of new directories to delete if the user denies the operation
|
||||||
|
|
||||||
|
// Keep track of newly created directories
|
||||||
|
const createdDirs: string[] = await this.createDirectoriesForFile(absolutePath)
|
||||||
|
console.log(`Created directories: ${createdDirs.join(", ")}`)
|
||||||
|
// make sure the file exists before we open it
|
||||||
|
if (!fileExists) {
|
||||||
|
await fs.writeFile(absolutePath, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the existing file with the new contents
|
||||||
|
const updatedDocument = await vscode.workspace.openTextDocument(vscode.Uri.file(absolutePath))
|
||||||
|
|
||||||
|
// await updatedDocument.save()
|
||||||
|
// const edit = new vscode.WorkspaceEdit()
|
||||||
|
// const fullRange = new vscode.Range(
|
||||||
|
// updatedDocument.positionAt(0),
|
||||||
|
// updatedDocument.positionAt(updatedDocument.getText().length)
|
||||||
|
// )
|
||||||
|
// edit.replace(updatedDocument.uri, fullRange, newContent)
|
||||||
|
// await vscode.workspace.applyEdit(edit)
|
||||||
|
|
||||||
// Windows file locking issues can prevent temporary files from being saved or closed properly.
|
// Windows file locking issues can prevent temporary files from being saved or closed properly.
|
||||||
// To avoid these problems, we use in-memory TextDocument objects with the `untitled` scheme.
|
// To avoid these problems, we use in-memory TextDocument objects with the `untitled` scheme.
|
||||||
// This method keeps the document entirely in memory, bypassing the filesystem and ensuring
|
// This method keeps the document entirely in memory, bypassing the filesystem and ensuring
|
||||||
@@ -828,11 +858,11 @@ export class ClaudeDev {
|
|||||||
// polluting the user's workspace with temporary files.
|
// polluting the user's workspace with temporary files.
|
||||||
|
|
||||||
// Create an in-memory document for the new content
|
// Create an in-memory document for the new content
|
||||||
const inMemoryDocumentUri = vscode.Uri.parse(`untitled:${fileName}`) // untitled scheme is necessary to open a file without it being saved to disk
|
// const inMemoryDocumentUri = vscode.Uri.parse(`untitled:${fileName}`) // untitled scheme is necessary to open a file without it being saved to disk
|
||||||
const inMemoryDocument = await vscode.workspace.openTextDocument(inMemoryDocumentUri)
|
// const inMemoryDocument = await vscode.workspace.openTextDocument(inMemoryDocumentUri)
|
||||||
const edit = new vscode.WorkspaceEdit()
|
// const edit = new vscode.WorkspaceEdit()
|
||||||
edit.insert(inMemoryDocumentUri, new vscode.Position(0, 0), newContent)
|
// edit.insert(inMemoryDocumentUri, new vscode.Position(0, 0), newContent)
|
||||||
await vscode.workspace.applyEdit(edit)
|
// await vscode.workspace.applyEdit(edit)
|
||||||
|
|
||||||
// Show diff
|
// Show diff
|
||||||
await vscode.commands.executeCommand(
|
await vscode.commands.executeCommand(
|
||||||
@@ -840,9 +870,37 @@ export class ClaudeDev {
|
|||||||
vscode.Uri.parse(`claude-dev-diff:${fileName}`).with({
|
vscode.Uri.parse(`claude-dev-diff:${fileName}`).with({
|
||||||
query: Buffer.from(originalContent).toString("base64"),
|
query: Buffer.from(originalContent).toString("base64"),
|
||||||
}),
|
}),
|
||||||
inMemoryDocument.uri,
|
updatedDocument.uri,
|
||||||
`${fileName}: ${fileExists ? "Original ↔ Claude's Changes" : "New File"} (Editable)`
|
`${fileName}: ${fileExists ? "Original ↔ Claude's Changes" : "New File"} (Editable)`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// if the file was already open, close it (must happen after showing the diff view since if it's the only tab the column will close)
|
||||||
|
let documentWasOpen = false
|
||||||
|
|
||||||
|
// close the tab if it's open
|
||||||
|
const tabs = vscode.window.tabGroups.all
|
||||||
|
.map((tg) => tg.tabs)
|
||||||
|
.flat()
|
||||||
|
.filter((tab) => tab.input instanceof vscode.TabInputText && tab.input.uri.fsPath === absolutePath)
|
||||||
|
for (const tab of tabs) {
|
||||||
|
await vscode.window.tabGroups.close(tab)
|
||||||
|
console.log(`Closed tab for ${absolutePath}`)
|
||||||
|
documentWasOpen = true
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Document was open: ${documentWasOpen}`)
|
||||||
|
|
||||||
|
// edit needs to happen after we close the original tab
|
||||||
|
const edit = new vscode.WorkspaceEdit()
|
||||||
|
const fullRange = new vscode.Range(
|
||||||
|
updatedDocument.positionAt(0),
|
||||||
|
updatedDocument.positionAt(updatedDocument.getText().length)
|
||||||
|
)
|
||||||
|
edit.replace(updatedDocument.uri, fullRange, newContent)
|
||||||
|
// Apply the edit, but without saving so this doesnt trigger a local save in timeline history
|
||||||
|
await vscode.workspace.applyEdit(edit) // has the added benefit of maintaing the file's original EOLs
|
||||||
|
|
||||||
|
// remove cursor from the document
|
||||||
await vscode.commands.executeCommand("workbench.action.focusSideBar")
|
await vscode.commands.executeCommand("workbench.action.focusSideBar")
|
||||||
|
|
||||||
let userResponse: {
|
let userResponse: {
|
||||||
@@ -871,35 +929,64 @@ export class ClaudeDev {
|
|||||||
}
|
}
|
||||||
const { response, text, images } = userResponse
|
const { response, text, images } = userResponse
|
||||||
|
|
||||||
const closeInMemoryDocAndDiffViews = async () => {
|
// const closeInMemoryDocAndDiffViews = async () => {
|
||||||
// ensure that the in-memory doc is active editor (this seems to fail on windows machines if its already active, so ignoring if there's an error as it's likely it's already active anyways)
|
// // ensure that the in-memory doc is active editor (this seems to fail on windows machines if its already active, so ignoring if there's an error as it's likely it's already active anyways)
|
||||||
try {
|
// // try {
|
||||||
await vscode.window.showTextDocument(inMemoryDocument, {
|
// // await vscode.window.showTextDocument(inMemoryDocument, {
|
||||||
preview: false, // ensures it opens in non-preview tab (preview tabs are easily replaced)
|
// // preview: false, // ensures it opens in non-preview tab (preview tabs are easily replaced)
|
||||||
preserveFocus: false,
|
// // preserveFocus: false,
|
||||||
})
|
// // })
|
||||||
// await vscode.window.showTextDocument(inMemoryDocument.uri, { preview: true, preserveFocus: false })
|
// // // await vscode.window.showTextDocument(inMemoryDocument.uri, { preview: true, preserveFocus: false })
|
||||||
} catch (error) {
|
// // } catch (error) {
|
||||||
console.log(`Could not open editor for ${absolutePath}: ${error}`)
|
// // console.log(`Could not open editor for ${absolutePath}: ${error}`)
|
||||||
|
// // }
|
||||||
|
// // await delay(50)
|
||||||
|
// // // Wait for the in-memory document to become the active editor (sometimes vscode timing issues happen and this would accidentally close claude dev!)
|
||||||
|
// // await pWaitFor(
|
||||||
|
// // () => {
|
||||||
|
// // return vscode.window.activeTextEditor?.document === inMemoryDocument
|
||||||
|
// // },
|
||||||
|
// // { timeout: 5000, interval: 50 }
|
||||||
|
// // )
|
||||||
|
|
||||||
|
// // if (vscode.window.activeTextEditor?.document === inMemoryDocument) {
|
||||||
|
// // await vscode.commands.executeCommand("workbench.action.revertAndCloseActiveEditor") // allows us to close the untitled doc without being prompted to save it
|
||||||
|
// // }
|
||||||
|
|
||||||
|
// await this.closeDiffViews()
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (response !== "yesButtonTapped") {
|
||||||
|
if (!fileExists) {
|
||||||
|
if (updatedDocument.isDirty) {
|
||||||
|
await updatedDocument.save()
|
||||||
}
|
}
|
||||||
await delay(50)
|
await this.closeDiffViews()
|
||||||
// Wait for the in-memory document to become the active editor (sometimes vscode timing issues happen and this would accidentally close claude dev!)
|
await fs.unlink(absolutePath)
|
||||||
await pWaitFor(
|
// Remove only the directories we created, in reverse order
|
||||||
() => {
|
for (let i = createdDirs.length - 1; i >= 0; i--) {
|
||||||
return vscode.window.activeTextEditor?.document === inMemoryDocument
|
await fs.rmdir(createdDirs[i])
|
||||||
},
|
console.log(`Directory ${createdDirs[i]} has been deleted.`)
|
||||||
{ timeout: 5000, interval: 50 }
|
}
|
||||||
|
console.log(`File ${absolutePath} has been deleted.`)
|
||||||
|
} else {
|
||||||
|
// revert document
|
||||||
|
const edit = new vscode.WorkspaceEdit()
|
||||||
|
const fullRange = new vscode.Range(
|
||||||
|
updatedDocument.positionAt(0),
|
||||||
|
updatedDocument.positionAt(updatedDocument.getText().length)
|
||||||
)
|
)
|
||||||
|
edit.replace(updatedDocument.uri, fullRange, originalContent)
|
||||||
if (vscode.window.activeTextEditor?.document === inMemoryDocument) {
|
// Apply the edit and save, since contents shouldnt have changed this wont show in local history unless of course the user made changes and saved during the edit
|
||||||
await vscode.commands.executeCommand("workbench.action.revertAndCloseActiveEditor") // allows us to close the untitled doc without being prompted to save it
|
await vscode.workspace.applyEdit(edit)
|
||||||
|
await updatedDocument.save()
|
||||||
|
console.log(`File ${absolutePath} has been reverted to its original content.`)
|
||||||
|
if (documentWasOpen) {
|
||||||
|
await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), { preview: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.closeDiffViews()
|
await this.closeDiffViews()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response !== "yesButtonTapped") {
|
|
||||||
await closeInMemoryDocAndDiffViews()
|
|
||||||
if (response === "messageResponse") {
|
if (response === "messageResponse") {
|
||||||
await this.say("user_feedback", text, images)
|
await this.say("user_feedback", text, images)
|
||||||
return this.formatIntoToolResponse(await this.formatGenericToolFeedback(text), images)
|
return this.formatIntoToolResponse(await this.formatGenericToolFeedback(text), images)
|
||||||
@@ -907,39 +994,63 @@ export class ClaudeDev {
|
|||||||
return "The user denied this operation."
|
return "The user denied this operation."
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the potentially edited content from the in-memory document
|
const editedContent = updatedDocument.getText()
|
||||||
const editedContent = inMemoryDocument.getText()
|
if (updatedDocument.isDirty) {
|
||||||
if (!fileExists) {
|
await updatedDocument.save()
|
||||||
await fs.mkdir(path.dirname(absolutePath), { recursive: true })
|
|
||||||
await fs.writeFile(absolutePath, "")
|
|
||||||
}
|
}
|
||||||
await closeInMemoryDocAndDiffViews()
|
|
||||||
|
// Read the potentially edited content from the document
|
||||||
|
|
||||||
|
// trigger an entry in the local history for the file
|
||||||
|
// if (fileExists) {
|
||||||
|
// await fs.writeFile(absolutePath, originalContent)
|
||||||
|
// const editor = await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), { preview: false })
|
||||||
|
// const edit = new vscode.WorkspaceEdit()
|
||||||
|
// const fullRange = new vscode.Range(
|
||||||
|
// editor.document.positionAt(0),
|
||||||
|
// editor.document.positionAt(editor.document.getText().length)
|
||||||
|
// )
|
||||||
|
// edit.replace(editor.document.uri, fullRange, editedContent)
|
||||||
|
// // Apply the edit, this will trigger a local save and timeline history
|
||||||
|
// await vscode.workspace.applyEdit(edit) // has the added benefit of maintaing the file's original EOLs
|
||||||
|
// await editor.document.save()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!fileExists) {
|
||||||
|
// await fs.mkdir(path.dirname(absolutePath), { recursive: true })
|
||||||
|
// await fs.writeFile(absolutePath, "")
|
||||||
|
// }
|
||||||
|
// await closeInMemoryDocAndDiffViews()
|
||||||
|
|
||||||
// await fs.writeFile(absolutePath, editedContent)
|
// await fs.writeFile(absolutePath, editedContent)
|
||||||
|
|
||||||
// open file and add text to it, if it fails fallback to using writeFile
|
// open file and add text to it, if it fails fallback to using writeFile
|
||||||
// we try doing it this way since it adds to local history for users to see what's changed in the file's timeline
|
// we try doing it this way since it adds to local history for users to see what's changed in the file's timeline
|
||||||
try {
|
// try {
|
||||||
const editor = await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), { preview: false })
|
// const editor = await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), { preview: false })
|
||||||
const edit = new vscode.WorkspaceEdit()
|
// const edit = new vscode.WorkspaceEdit()
|
||||||
const fullRange = new vscode.Range(
|
// const fullRange = new vscode.Range(
|
||||||
editor.document.positionAt(0),
|
// editor.document.positionAt(0),
|
||||||
editor.document.positionAt(editor.document.getText().length)
|
// editor.document.positionAt(editor.document.getText().length)
|
||||||
)
|
// )
|
||||||
edit.replace(editor.document.uri, fullRange, editedContent)
|
// edit.replace(editor.document.uri, fullRange, editedContent)
|
||||||
// Apply the edit, this will trigger a local save and timeline history
|
// // Apply the edit, this will trigger a local save and timeline history
|
||||||
await vscode.workspace.applyEdit(edit) // has the added benefit of maintaing the file's original EOLs
|
// await vscode.workspace.applyEdit(edit) // has the added benefit of maintaing the file's original EOLs
|
||||||
await editor.document.save()
|
// await editor.document.save()
|
||||||
} catch (saveError) {
|
// } catch (saveError) {
|
||||||
console.log(`Could not open editor for ${absolutePath}: ${saveError}`)
|
// console.log(`Could not open editor for ${absolutePath}: ${saveError}`)
|
||||||
await fs.writeFile(absolutePath, editedContent)
|
// await fs.writeFile(absolutePath, editedContent)
|
||||||
// calling showTextDocument would sometimes fail even though changes were applied, so we'll ignore these one-off errors (likely due to vscode locking issues)
|
// // calling showTextDocument would sometimes fail even though changes were applied, so we'll ignore these one-off errors (likely due to vscode locking issues)
|
||||||
try {
|
// try {
|
||||||
|
// await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), { preview: false })
|
||||||
|
// } catch (openFileError) {
|
||||||
|
// console.log(`Could not open editor for ${absolutePath}: ${openFileError}`)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), { preview: false })
|
await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), { preview: false })
|
||||||
} catch (openFileError) {
|
|
||||||
console.log(`Could not open editor for ${absolutePath}: ${openFileError}`)
|
await this.closeDiffViews()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), { preview: false })
|
// await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), { preview: false })
|
||||||
|
|
||||||
@@ -957,7 +1068,7 @@ export class ClaudeDev {
|
|||||||
diff: this.createPrettyPatch(relPath, normalizedNewContent, normalizedEditedContent),
|
diff: this.createPrettyPatch(relPath, normalizedNewContent, normalizedEditedContent),
|
||||||
} as ClaudeSayTool)
|
} as ClaudeSayTool)
|
||||||
)
|
)
|
||||||
return `The user made the following updates to your content:\n\n${userDiff}\n\nThe updated content was successfully saved to ${relPath}.`
|
return `The user made the following updates to your content:\n\n${userDiff}\n\nThe updated content, which includes both your original modifications and the user's additional edits, has been successfully saved to ${relPath}.`
|
||||||
} else {
|
} else {
|
||||||
return `The content was successfully saved to ${relPath}.`
|
return `The content was successfully saved to ${relPath}.`
|
||||||
}
|
}
|
||||||
@@ -971,6 +1082,51 @@ export class ClaudeDev {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously creates all non-existing subdirectories for a given file path
|
||||||
|
* and collects them in an array for later deletion.
|
||||||
|
*
|
||||||
|
* @param filePath - The full path to a file.
|
||||||
|
* @returns A promise that resolves to an array of newly created directories.
|
||||||
|
*/
|
||||||
|
async createDirectoriesForFile(filePath: string): Promise<string[]> {
|
||||||
|
const newDirectories: string[] = []
|
||||||
|
const normalizedFilePath = path.normalize(filePath) // Normalize path for cross-platform compatibility
|
||||||
|
const directoryPath = path.dirname(normalizedFilePath)
|
||||||
|
|
||||||
|
let currentPath = directoryPath
|
||||||
|
const dirsToCreate: string[] = []
|
||||||
|
|
||||||
|
// Traverse up the directory tree and collect missing directories
|
||||||
|
while (!(await this.exists(currentPath))) {
|
||||||
|
dirsToCreate.push(currentPath)
|
||||||
|
currentPath = path.dirname(currentPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create directories from the topmost missing one down to the target directory
|
||||||
|
for (let i = dirsToCreate.length - 1; i >= 0; i--) {
|
||||||
|
await fs.mkdir(dirsToCreate[i])
|
||||||
|
newDirectories.push(dirsToCreate[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return newDirectories
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to check if a path exists.
|
||||||
|
*
|
||||||
|
* @param path - The path to check.
|
||||||
|
* @returns A promise that resolves to true if the path exists, false otherwise.
|
||||||
|
*/
|
||||||
|
async exists(filePath: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await fs.access(filePath)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
createPrettyPatch(filename = "file", oldStr: string, newStr: string) {
|
createPrettyPatch(filename = "file", oldStr: string, newStr: string) {
|
||||||
const patch = diff.createPatch(filename, oldStr, newStr)
|
const patch = diff.createPatch(filename, oldStr, newStr)
|
||||||
const lines = patch.split("\n")
|
const lines = patch.split("\n")
|
||||||
|
|||||||
Reference in New Issue
Block a user