|
|
@@ -1,16 +1,19 @@
|
|
|
import * as vscode from "vscode"
|
|
|
import * as path from "path"
|
|
|
import * as fs from "fs/promises"
|
|
|
+import * as diff from "diff"
|
|
|
+import stripBom from "strip-bom"
|
|
|
+
|
|
|
import { createDirectoriesForFile } from "../../utils/fs"
|
|
|
import { arePathsEqual } from "../../utils/path"
|
|
|
import { formatResponse } from "../../core/prompts/responses"
|
|
|
-import { DecorationController } from "./DecorationController"
|
|
|
-import * as diff from "diff"
|
|
|
import { diagnosticsToProblemsString, getNewDiagnostics } from "../diagnostics"
|
|
|
-import stripBom from "strip-bom"
|
|
|
+
|
|
|
+import { DecorationController } from "./DecorationController"
|
|
|
|
|
|
export const DIFF_VIEW_URI_SCHEME = "cline-diff"
|
|
|
|
|
|
+// TODO: https://github.com/cline/cline/pull/3354
|
|
|
export class DiffViewProvider {
|
|
|
editType?: "create" | "modify"
|
|
|
isEditing = false
|
|
|
@@ -32,17 +35,21 @@ export class DiffViewProvider {
|
|
|
const fileExists = this.editType === "modify"
|
|
|
const absolutePath = path.resolve(this.cwd, relPath)
|
|
|
this.isEditing = true
|
|
|
- // if the file is already open, ensure it's not dirty before getting its contents
|
|
|
+
|
|
|
+ // If the file is already open, ensure it's not dirty before getting its
|
|
|
+ // contents.
|
|
|
if (fileExists) {
|
|
|
const existingDocument = vscode.workspace.textDocuments.find((doc) =>
|
|
|
arePathsEqual(doc.uri.fsPath, absolutePath),
|
|
|
)
|
|
|
+
|
|
|
if (existingDocument && existingDocument.isDirty) {
|
|
|
await existingDocument.save()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // get diagnostics before editing the file, we'll compare to diagnostics after editing to see if cline needs to fix anything
|
|
|
+ // Get diagnostics before editing the file, we'll compare to diagnostics
|
|
|
+ // after editing to see if cline needs to fix anything.
|
|
|
this.preDiagnostics = vscode.languages.getDiagnostics()
|
|
|
|
|
|
if (fileExists) {
|
|
|
@@ -50,33 +57,41 @@ export class DiffViewProvider {
|
|
|
} else {
|
|
|
this.originalContent = ""
|
|
|
}
|
|
|
- // for new files, create any necessary directories and keep track of new directories to delete if the user denies the operation
|
|
|
+
|
|
|
+ // For new files, create any necessary directories and keep track of new
|
|
|
+ // directories to delete if the user denies the operation.
|
|
|
this.createdDirs = await createDirectoriesForFile(absolutePath)
|
|
|
- // make sure the file exists before we open it
|
|
|
+
|
|
|
+ // Make sure the file exists before we open it.
|
|
|
if (!fileExists) {
|
|
|
await fs.writeFile(absolutePath, "")
|
|
|
}
|
|
|
- // 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)
|
|
|
+
|
|
|
+ // 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).
|
|
|
this.documentWasOpen = false
|
|
|
- // close the tab if it's open (it's already saved above)
|
|
|
+
|
|
|
+ // Close the tab if it's open (it's already saved above).
|
|
|
const tabs = vscode.window.tabGroups.all
|
|
|
.map((tg) => tg.tabs)
|
|
|
.flat()
|
|
|
.filter(
|
|
|
(tab) => tab.input instanceof vscode.TabInputText && arePathsEqual(tab.input.uri.fsPath, absolutePath),
|
|
|
)
|
|
|
+
|
|
|
for (const tab of tabs) {
|
|
|
if (!tab.isDirty) {
|
|
|
await vscode.window.tabGroups.close(tab)
|
|
|
}
|
|
|
this.documentWasOpen = true
|
|
|
}
|
|
|
+
|
|
|
this.activeDiffEditor = await this.openDiffEditor()
|
|
|
this.fadedOverlayController = new DecorationController("fadedOverlay", this.activeDiffEditor)
|
|
|
this.activeLineController = new DecorationController("activeLine", this.activeDiffEditor)
|
|
|
- // Apply faded overlay to all lines initially
|
|
|
+ // Apply faded overlay to all lines initially.
|
|
|
this.fadedOverlayController.addLines(0, this.activeDiffEditor.document.lineCount)
|
|
|
- this.scrollEditorToLine(0) // will this crash for new files?
|
|
|
+ this.scrollEditorToLine(0) // Will this crash for new files?
|
|
|
this.streamedLines = []
|
|
|
}
|
|
|
|
|
|
@@ -84,58 +99,70 @@ export class DiffViewProvider {
|
|
|
if (!this.relPath || !this.activeLineController || !this.fadedOverlayController) {
|
|
|
throw new Error("Required values not set")
|
|
|
}
|
|
|
+
|
|
|
this.newContent = accumulatedContent
|
|
|
const accumulatedLines = accumulatedContent.split("\n")
|
|
|
+
|
|
|
if (!isFinal) {
|
|
|
- accumulatedLines.pop() // remove the last partial line only if it's not the final update
|
|
|
+ accumulatedLines.pop() // Remove the last partial line only if it's not the final update.
|
|
|
}
|
|
|
|
|
|
const diffEditor = this.activeDiffEditor
|
|
|
const document = diffEditor?.document
|
|
|
+
|
|
|
if (!diffEditor || !document) {
|
|
|
throw new Error("User closed text editor, unable to edit file...")
|
|
|
}
|
|
|
|
|
|
- // Place cursor at the beginning of the diff editor to keep it out of the way of the stream animation
|
|
|
+ // Place cursor at the beginning of the diff editor to keep it out of
|
|
|
+ // the way of the stream animation.
|
|
|
const beginningOfDocument = new vscode.Position(0, 0)
|
|
|
diffEditor.selection = new vscode.Selection(beginningOfDocument, beginningOfDocument)
|
|
|
|
|
|
const endLine = accumulatedLines.length
|
|
|
- // Replace all content up to the current line with accumulated lines
|
|
|
+ // Replace all content up to the current line with accumulated lines.
|
|
|
const edit = new vscode.WorkspaceEdit()
|
|
|
const rangeToReplace = new vscode.Range(0, 0, endLine + 1, 0)
|
|
|
const contentToReplace = accumulatedLines.slice(0, endLine + 1).join("\n") + "\n"
|
|
|
edit.replace(document.uri, rangeToReplace, this.stripAllBOMs(contentToReplace))
|
|
|
await vscode.workspace.applyEdit(edit)
|
|
|
- // Update decorations
|
|
|
+ // Update decorations.
|
|
|
this.activeLineController.setActiveLine(endLine)
|
|
|
this.fadedOverlayController.updateOverlayAfterLine(endLine, document.lineCount)
|
|
|
- // Scroll to the current line
|
|
|
+ // Scroll to the current line.
|
|
|
this.scrollEditorToLine(endLine)
|
|
|
|
|
|
- // Update the streamedLines with the new accumulated content
|
|
|
+ // Update the streamedLines with the new accumulated content.
|
|
|
this.streamedLines = accumulatedLines
|
|
|
+
|
|
|
if (isFinal) {
|
|
|
- // Handle any remaining lines if the new content is shorter than the original
|
|
|
+ // Handle any remaining lines if the new content is shorter than the
|
|
|
+ // original.
|
|
|
if (this.streamedLines.length < document.lineCount) {
|
|
|
const edit = new vscode.WorkspaceEdit()
|
|
|
edit.delete(document.uri, new vscode.Range(this.streamedLines.length, 0, document.lineCount, 0))
|
|
|
await vscode.workspace.applyEdit(edit)
|
|
|
}
|
|
|
- // Preserve empty last line if original content had one
|
|
|
+
|
|
|
+ // Preserve empty last line if original content had one.
|
|
|
const hasEmptyLastLine = this.originalContent?.endsWith("\n")
|
|
|
+
|
|
|
if (hasEmptyLastLine && !accumulatedContent.endsWith("\n")) {
|
|
|
accumulatedContent += "\n"
|
|
|
}
|
|
|
- // Apply the final content
|
|
|
+
|
|
|
+ // Apply the final content.
|
|
|
const finalEdit = new vscode.WorkspaceEdit()
|
|
|
+
|
|
|
finalEdit.replace(
|
|
|
document.uri,
|
|
|
new vscode.Range(0, 0, document.lineCount, 0),
|
|
|
this.stripAllBOMs(accumulatedContent),
|
|
|
)
|
|
|
+
|
|
|
await vscode.workspace.applyEdit(finalEdit)
|
|
|
- // Clear all decorations at the end (after applying final edit)
|
|
|
+
|
|
|
+ // Clear all decorations at the end (after applying final edit).
|
|
|
this.fadedOverlayController.clear()
|
|
|
this.activeLineController.clear()
|
|
|
}
|
|
|
@@ -149,59 +176,68 @@ export class DiffViewProvider {
|
|
|
if (!this.relPath || !this.newContent || !this.activeDiffEditor) {
|
|
|
return { newProblemsMessage: undefined, userEdits: undefined, finalContent: undefined }
|
|
|
}
|
|
|
+
|
|
|
const absolutePath = path.resolve(this.cwd, this.relPath)
|
|
|
const updatedDocument = this.activeDiffEditor.document
|
|
|
const editedContent = updatedDocument.getText()
|
|
|
+
|
|
|
if (updatedDocument.isDirty) {
|
|
|
await updatedDocument.save()
|
|
|
}
|
|
|
|
|
|
- await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), { preview: false })
|
|
|
+ await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), { preview: false, preserveFocus: true })
|
|
|
await this.closeAllDiffViews()
|
|
|
|
|
|
- /*
|
|
|
- Getting diagnostics before and after the file edit is a better approach than
|
|
|
- automatically tracking problems in real-time. This method ensures we only
|
|
|
- report new problems that are a direct result of this specific edit.
|
|
|
- Since these are new problems resulting from Roo's edit, we know they're
|
|
|
- directly related to the work he's doing. This eliminates the risk of Roo
|
|
|
- going off-task or getting distracted by unrelated issues, which was a problem
|
|
|
- with the previous auto-debug approach. Some users' machines may be slow to
|
|
|
- update diagnostics, so this approach provides a good balance between automation
|
|
|
- and avoiding potential issues where Roo might get stuck in loops due to
|
|
|
- outdated problem information. If no new problems show up by the time the user
|
|
|
- accepts the changes, they can always debug later using the '@problems' mention.
|
|
|
- This way, Roo only becomes aware of new problems resulting from his edits
|
|
|
- and can address them accordingly. If problems don't change immediately after
|
|
|
- applying a fix, won't be notified, which is generally fine since the
|
|
|
- initial fix is usually correct and it may just take time for linters to catch up.
|
|
|
- */
|
|
|
+ // Getting diagnostics before and after the file edit is a better approach than
|
|
|
+ // automatically tracking problems in real-time. This method ensures we only
|
|
|
+ // report new problems that are a direct result of this specific edit.
|
|
|
+ // Since these are new problems resulting from Roo's edit, we know they're
|
|
|
+ // directly related to the work he's doing. This eliminates the risk of Roo
|
|
|
+ // going off-task or getting distracted by unrelated issues, which was a problem
|
|
|
+ // with the previous auto-debug approach. Some users' machines may be slow to
|
|
|
+ // update diagnostics, so this approach provides a good balance between automation
|
|
|
+ // and avoiding potential issues where Roo might get stuck in loops due to
|
|
|
+ // outdated problem information. If no new problems show up by the time the user
|
|
|
+ // accepts the changes, they can always debug later using the '@problems' mention.
|
|
|
+ // This way, Roo only becomes aware of new problems resulting from his edits
|
|
|
+ // and can address them accordingly. If problems don't change immediately after
|
|
|
+ // applying a fix, won't be notified, which is generally fine since the
|
|
|
+ // initial fix is usually correct and it may just take time for linters to catch up.
|
|
|
const postDiagnostics = vscode.languages.getDiagnostics()
|
|
|
+
|
|
|
const newProblems = await diagnosticsToProblemsString(
|
|
|
getNewDiagnostics(this.preDiagnostics, postDiagnostics),
|
|
|
[
|
|
|
vscode.DiagnosticSeverity.Error, // only including errors since warnings can be distracting (if user wants to fix warnings they can use the @problems mention)
|
|
|
],
|
|
|
this.cwd,
|
|
|
- ) // will be empty string if no errors
|
|
|
+ ) // Will be empty string if no errors.
|
|
|
+
|
|
|
const newProblemsMessage =
|
|
|
newProblems.length > 0 ? `\n\nNew problems detected after saving the file:\n${newProblems}` : ""
|
|
|
|
|
|
- // If the edited content has different EOL characters, we don't want to show a diff with all the EOL differences.
|
|
|
+ // If the edited content has different EOL characters, we don't want to
|
|
|
+ // show a diff with all the EOL differences.
|
|
|
const newContentEOL = this.newContent.includes("\r\n") ? "\r\n" : "\n"
|
|
|
- const normalizedEditedContent = editedContent.replace(/\r\n|\n/g, newContentEOL).trimEnd() + newContentEOL // trimEnd to fix issue where editor adds in extra new line automatically
|
|
|
- // just in case the new content has a mix of varying EOL characters
|
|
|
+
|
|
|
+ // `trimEnd` to fix issue where editor adds in extra new line
|
|
|
+ // automatically.
|
|
|
+ const normalizedEditedContent = editedContent.replace(/\r\n|\n/g, newContentEOL).trimEnd() + newContentEOL
|
|
|
+
|
|
|
+ // Just in case the new content has a mix of varying EOL characters.
|
|
|
const normalizedNewContent = this.newContent.replace(/\r\n|\n/g, newContentEOL).trimEnd() + newContentEOL
|
|
|
+
|
|
|
if (normalizedEditedContent !== normalizedNewContent) {
|
|
|
- // user made changes before approving edit
|
|
|
+ // User made changes before approving edit.
|
|
|
const userEdits = formatResponse.createPrettyPatch(
|
|
|
this.relPath.toPosix(),
|
|
|
normalizedNewContent,
|
|
|
normalizedEditedContent,
|
|
|
)
|
|
|
+
|
|
|
return { newProblemsMessage, userEdits, finalContent: normalizedEditedContent }
|
|
|
} else {
|
|
|
- // no changes to cline's edits
|
|
|
+ // No changes to Roo's edits.
|
|
|
return { newProblemsMessage, userEdits: undefined, finalContent: normalizedEditedContent }
|
|
|
}
|
|
|
}
|
|
|
@@ -210,42 +246,55 @@ export class DiffViewProvider {
|
|
|
if (!this.relPath || !this.activeDiffEditor) {
|
|
|
return
|
|
|
}
|
|
|
+
|
|
|
const fileExists = this.editType === "modify"
|
|
|
const updatedDocument = this.activeDiffEditor.document
|
|
|
const absolutePath = path.resolve(this.cwd, this.relPath)
|
|
|
+
|
|
|
if (!fileExists) {
|
|
|
if (updatedDocument.isDirty) {
|
|
|
await updatedDocument.save()
|
|
|
}
|
|
|
+
|
|
|
await this.closeAllDiffViews()
|
|
|
await fs.unlink(absolutePath)
|
|
|
- // Remove only the directories we created, in reverse order
|
|
|
+
|
|
|
+ // Remove only the directories we created, in reverse order.
|
|
|
for (let i = this.createdDirs.length - 1; i >= 0; i--) {
|
|
|
await fs.rmdir(this.createdDirs[i])
|
|
|
console.log(`Directory ${this.createdDirs[i]} has been deleted.`)
|
|
|
}
|
|
|
+
|
|
|
console.log(`File ${absolutePath} has been deleted.`)
|
|
|
} else {
|
|
|
- // revert document
|
|
|
+ // 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, this.originalContent ?? "")
|
|
|
- // 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
|
|
|
+
|
|
|
+ // Apply the edit and save, since contents shouldnt have changed
|
|
|
+ // this won't show in local history unless of course the user made
|
|
|
+ // changes and saved during the edit.
|
|
|
await vscode.workspace.applyEdit(edit)
|
|
|
await updatedDocument.save()
|
|
|
console.log(`File ${absolutePath} has been reverted to its original content.`)
|
|
|
+
|
|
|
if (this.documentWasOpen) {
|
|
|
await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), {
|
|
|
preview: false,
|
|
|
+ preserveFocus: true,
|
|
|
})
|
|
|
}
|
|
|
+
|
|
|
await this.closeAllDiffViews()
|
|
|
}
|
|
|
|
|
|
- // edit is done
|
|
|
+ // Edit is done.
|
|
|
await this.reset()
|
|
|
}
|
|
|
|
|
|
@@ -257,8 +306,9 @@ export class DiffViewProvider {
|
|
|
tab.input instanceof vscode.TabInputTextDiff &&
|
|
|
tab.input?.original?.scheme === DIFF_VIEW_URI_SCHEME,
|
|
|
)
|
|
|
+
|
|
|
for (const tab of tabs) {
|
|
|
- // trying to close dirty views results in save popup
|
|
|
+ // Trying to close dirty views results in save popup.
|
|
|
if (!tab.isDirty) {
|
|
|
await vscode.window.tabGroups.close(tab)
|
|
|
}
|
|
|
@@ -269,8 +319,12 @@ export class DiffViewProvider {
|
|
|
if (!this.relPath) {
|
|
|
throw new Error("No file path set")
|
|
|
}
|
|
|
+
|
|
|
const uri = vscode.Uri.file(path.resolve(this.cwd, this.relPath))
|
|
|
- // If this diff editor is already open (ie if a previous write file was interrupted) then we should activate that instead of opening a new diff
|
|
|
+
|
|
|
+ // If this diff editor is already open (ie if a previous write file was
|
|
|
+ // interrupted) then we should activate that instead of opening a new
|
|
|
+ // diff.
|
|
|
const diffTab = vscode.window.tabGroups.all
|
|
|
.flatMap((group) => group.tabs)
|
|
|
.find(
|
|
|
@@ -279,20 +333,24 @@ export class DiffViewProvider {
|
|
|
tab.input?.original?.scheme === DIFF_VIEW_URI_SCHEME &&
|
|
|
arePathsEqual(tab.input.modified.fsPath, uri.fsPath),
|
|
|
)
|
|
|
+
|
|
|
if (diffTab && diffTab.input instanceof vscode.TabInputTextDiff) {
|
|
|
- const editor = await vscode.window.showTextDocument(diffTab.input.modified)
|
|
|
+ const editor = await vscode.window.showTextDocument(diffTab.input.modified, { preserveFocus: true })
|
|
|
return editor
|
|
|
}
|
|
|
- // Open new diff editor
|
|
|
+
|
|
|
+ // Open new diff editor.
|
|
|
return new Promise<vscode.TextEditor>((resolve, reject) => {
|
|
|
const fileName = path.basename(uri.fsPath)
|
|
|
const fileExists = this.editType === "modify"
|
|
|
+
|
|
|
const disposable = vscode.window.onDidChangeActiveTextEditor((editor) => {
|
|
|
if (editor && arePathsEqual(editor.document.uri.fsPath, uri.fsPath)) {
|
|
|
disposable.dispose()
|
|
|
resolve(editor)
|
|
|
}
|
|
|
})
|
|
|
+
|
|
|
vscode.commands.executeCommand(
|
|
|
"vscode.diff",
|
|
|
vscode.Uri.parse(`${DIFF_VIEW_URI_SCHEME}:${fileName}`).with({
|
|
|
@@ -300,8 +358,10 @@ export class DiffViewProvider {
|
|
|
}),
|
|
|
uri,
|
|
|
`${fileName}: ${fileExists ? "Original ↔ Roo's Changes" : "New File"} (Editable)`,
|
|
|
+ { preserveFocus: true },
|
|
|
)
|
|
|
- // This may happen on very slow machines ie project idx
|
|
|
+
|
|
|
+ // This may happen on very slow machines i.e. project idx.
|
|
|
setTimeout(() => {
|
|
|
disposable.dispose()
|
|
|
reject(new Error("Failed to open diff editor, please try again..."))
|
|
|
@@ -312,6 +372,7 @@ export class DiffViewProvider {
|
|
|
private scrollEditorToLine(line: number) {
|
|
|
if (this.activeDiffEditor) {
|
|
|
const scrollLine = line + 4
|
|
|
+
|
|
|
this.activeDiffEditor.revealRange(
|
|
|
new vscode.Range(scrollLine, 0, scrollLine, 0),
|
|
|
vscode.TextEditorRevealType.InCenter,
|
|
|
@@ -323,18 +384,23 @@ export class DiffViewProvider {
|
|
|
if (!this.activeDiffEditor) {
|
|
|
return
|
|
|
}
|
|
|
+
|
|
|
const currentContent = this.activeDiffEditor.document.getText()
|
|
|
const diffs = diff.diffLines(this.originalContent || "", currentContent)
|
|
|
+
|
|
|
let lineCount = 0
|
|
|
+
|
|
|
for (const part of diffs) {
|
|
|
if (part.added || part.removed) {
|
|
|
- // Found the first diff, scroll to it
|
|
|
+ // Found the first diff, scroll to it.
|
|
|
this.activeDiffEditor.revealRange(
|
|
|
new vscode.Range(lineCount, 0, lineCount, 0),
|
|
|
vscode.TextEditorRevealType.InCenter,
|
|
|
)
|
|
|
+
|
|
|
return
|
|
|
}
|
|
|
+
|
|
|
if (!part.removed) {
|
|
|
lineCount += part.count || 0
|
|
|
}
|
|
|
@@ -344,17 +410,24 @@ export class DiffViewProvider {
|
|
|
private stripAllBOMs(input: string): string {
|
|
|
let result = input
|
|
|
let previous
|
|
|
+
|
|
|
do {
|
|
|
previous = result
|
|
|
result = stripBom(result)
|
|
|
} while (result !== previous)
|
|
|
+
|
|
|
return result
|
|
|
}
|
|
|
|
|
|
- // close editor if open?
|
|
|
async reset() {
|
|
|
- // Ensure any diff views opened by this provider are closed to release memory
|
|
|
- await this.closeAllDiffViews()
|
|
|
+ // Ensure any diff views opened by this provider are closed to release
|
|
|
+ // memory.
|
|
|
+ try {
|
|
|
+ await this.closeAllDiffViews()
|
|
|
+ } catch (error) {
|
|
|
+ console.error("Error closing diff views", error)
|
|
|
+ }
|
|
|
+
|
|
|
this.editType = undefined
|
|
|
this.isEditing = false
|
|
|
this.originalContent = undefined
|