Browse Source

Add text decorations based edit streaming

Saoud Rizwan 1 year ago
parent
commit
c2a2e1b54c

+ 16 - 11
src/core/ClaudeDev.ts

@@ -893,16 +893,9 @@ export class ClaudeDev {
 									await this.diffViewProvider.open(relPath)
 									await this.diffViewProvider.open(relPath)
 								}
 								}
 								// editor is open, stream content in
 								// editor is open, stream content in
-								await this.diffViewProvider.update(newContent)
+								await this.diffViewProvider.update(newContent, false)
 								break
 								break
 							} else {
 							} else {
-								// if isEditingFile false, that means we have the full contents of the file already.
-								// it's important to note how this function works, you can't make the assumption that the block.partial conditional will always be called since it may immediately get complete, non-partial data. So this part of the logic will always be called.
-								// in other words, you must always repeat the block.partial logic here
-								if (!this.diffViewProvider.isEditing) {
-									await this.diffViewProvider.open(relPath)
-								}
-								await this.diffViewProvider.update(newContent)
 								if (!relPath) {
 								if (!relPath) {
 									this.consecutiveMistakeCount++
 									this.consecutiveMistakeCount++
 									pushToolResult(await this.sayAndCreateMissingParamError("write_to_file", "path"))
 									pushToolResult(await this.sayAndCreateMissingParamError("write_to_file", "path"))
@@ -917,6 +910,16 @@ export class ClaudeDev {
 								}
 								}
 								this.consecutiveMistakeCount = 0
 								this.consecutiveMistakeCount = 0
 
 
+								// if isEditingFile false, that means we have the full contents of the file already.
+								// it's important to note how this function works, you can't make the assumption that the block.partial conditional will always be called since it may immediately get complete, non-partial data. So this part of the logic will always be called.
+								// in other words, you must always repeat the block.partial logic here
+								if (!this.diffViewProvider.isEditing) {
+									await this.diffViewProvider.open(relPath)
+								}
+								await this.diffViewProvider.update(newContent, true)
+								await delay(300) // wait for diff view to update
+								this.diffViewProvider.scrollToFirstDiff()
+
 								const completeMessage = JSON.stringify({
 								const completeMessage = JSON.stringify({
 									...sharedMessageProps,
 									...sharedMessageProps,
 									content: fileExists ? undefined : newContent,
 									content: fileExists ? undefined : newContent,
@@ -933,7 +936,7 @@ export class ClaudeDev {
 									await this.diffViewProvider.revertChanges()
 									await this.diffViewProvider.revertChanges()
 									break
 									break
 								}
 								}
-								const userEdits = await this.diffViewProvider.saveChanges()
+								const { newProblemsMessage, userEdits } = await this.diffViewProvider.saveChanges()
 								this.didEditFile = true // used to determine if we should wait for busy terminal to update before sending api request
 								this.didEditFile = true // used to determine if we should wait for busy terminal to update before sending api request
 								if (userEdits) {
 								if (userEdits) {
 									await this.say(
 									await this.say(
@@ -945,10 +948,12 @@ export class ClaudeDev {
 										} satisfies ClaudeSayTool)
 										} satisfies ClaudeSayTool)
 									)
 									)
 									pushToolResult(
 									pushToolResult(
-										`The user made the following updates to your content:\n\n${userEdits}\n\nThe updated content, which includes both your original modifications and the user's additional edits, has been successfully saved to ${relPath.toPosix()}. (Note this does not mean you need to re-write the file with the user's changes, as they have already been applied to the file.)`
+										`The user made the following updates to your content:\n\n${userEdits}\n\nThe updated content, which includes both your original modifications and the user's additional edits, has been successfully saved to ${relPath.toPosix()}. (Note this does not mean you need to re-write the file with the user's changes, as they have already been applied to the file.)${newProblemsMessage}`
 									)
 									)
 								} else {
 								} else {
-									pushToolResult(`The content was successfully saved to ${relPath.toPosix()}.`)
+									pushToolResult(
+										`The content was successfully saved to ${relPath.toPosix()}.${newProblemsMessage}`
+									)
 								}
 								}
 								await this.diffViewProvider.reset()
 								await this.diffViewProvider.reset()
 								break
 								break

+ 2 - 2
src/core/prompts/system.ts

@@ -89,13 +89,13 @@ Remember:
 - Formulate your tool use using the XML format specified for each tool.
 - Formulate your tool use using the XML format specified for each tool.
 - After using a tool, you will receive the tool use result in the user's next message. This result will provide you with the necessary information to continue your task or make further decisions.
 - After using a tool, you will receive the tool use result in the user's next message. This result will provide you with the necessary information to continue your task or make further decisions.
 
 
-CRITICAL RULE: You must use only one tool at a time. Multiple tool uses in a single message are strictly prohibited.
+CRITICAL RULE: You are only allowed to use one tool per message. Multiple tool uses in a single message is STRICTLY FORBIDDEN. Even if it may seem efficient to use multiple tools at once, the system will crash and burn if you do so.
 
 
 To ensure compliance:
 To ensure compliance:
 
 
 1. After each tool use, wait for the result before proceeding.
 1. After each tool use, wait for the result before proceeding.
 2. Analyze the result of each tool use before deciding on the next step.
 2. Analyze the result of each tool use before deciding on the next step.
-3. If multiple actions are needed, break them into separate, sequential steps, each using a single tool.
+3. If multiple actions are needed, break them into separate, sequential messages, each using a single tool.
 
 
 Remember: *One tool use per message. No exceptions.*
 Remember: *One tool use per message. No exceptions.*
 
 

+ 81 - 0
src/integrations/editor/DecorationController.ts

@@ -0,0 +1,81 @@
+import * as vscode from "vscode"
+
+const fadedOverlayDecorationType = vscode.window.createTextEditorDecorationType({
+	backgroundColor: "rgba(255, 255, 0, 0.1)",
+	opacity: "0.4",
+	isWholeLine: true,
+})
+
+const activeLineDecorationType = vscode.window.createTextEditorDecorationType({
+	backgroundColor: "rgba(255, 255, 0, 0.3)",
+	opacity: "1",
+	isWholeLine: true,
+	border: "1px solid rgba(255, 255, 0, 0.5)",
+})
+
+type DecorationType = "fadedOverlay" | "activeLine"
+
+export class DecorationController {
+	private decorationType: DecorationType
+	private editor: vscode.TextEditor
+	private ranges: vscode.Range[] = []
+
+	constructor(decorationType: DecorationType, editor: vscode.TextEditor) {
+		this.decorationType = decorationType
+		this.editor = editor
+	}
+
+	getDecoration() {
+		switch (this.decorationType) {
+			case "fadedOverlay":
+				return fadedOverlayDecorationType
+			case "activeLine":
+				return activeLineDecorationType
+		}
+	}
+
+	addLines(startIndex: number, numLines: number) {
+		// Guard against invalid inputs
+		if (startIndex < 0 || numLines <= 0) {
+			return
+		}
+
+		const lastRange = this.ranges[this.ranges.length - 1]
+		if (lastRange && lastRange.end.line === startIndex - 1) {
+			this.ranges[this.ranges.length - 1] = lastRange.with(undefined, lastRange.end.translate(numLines))
+		} else {
+			const endLine = startIndex + numLines - 1
+			this.ranges.push(new vscode.Range(startIndex, 0, endLine, Number.MAX_SAFE_INTEGER))
+		}
+
+		this.editor.setDecorations(this.getDecoration(), this.ranges)
+	}
+
+	clear() {
+		this.ranges = []
+		this.editor.setDecorations(this.getDecoration(), this.ranges)
+	}
+
+	updateOverlayAfterLine(line: number, totalLines: number) {
+		// Remove any existing ranges that start at or after the current line
+		this.ranges = this.ranges.filter((range) => range.end.line < line)
+
+		// Add a new range for all lines after the current line
+		if (line < totalLines - 1) {
+			this.ranges.push(
+				new vscode.Range(
+					new vscode.Position(line + 1, 0),
+					new vscode.Position(totalLines - 1, Number.MAX_SAFE_INTEGER)
+				)
+			)
+		}
+
+		// Apply the updated decorations
+		this.editor.setDecorations(this.getDecoration(), this.ranges)
+	}
+
+	setActiveLine(line: number) {
+		this.ranges = [new vscode.Range(line, 0, line, Number.MAX_SAFE_INTEGER)]
+		this.editor.setDecorations(this.getDecoration(), this.ranges)
+	}
+}

+ 193 - 241
src/integrations/editor/DiffViewProvider.ts

@@ -4,6 +4,9 @@ import * as fs from "fs/promises"
 import { createDirectoriesForFile } from "../../utils/fs"
 import { createDirectoriesForFile } from "../../utils/fs"
 import { arePathsEqual } from "../../utils/path"
 import { arePathsEqual } from "../../utils/path"
 import { formatResponse } from "../../core/prompts/responses"
 import { formatResponse } from "../../core/prompts/responses"
+import { DecorationController } from "./DecorationController"
+import * as diff from "diff"
+import { diagnosticsToProblemsString, getNewDiagnostics } from "../diagnostics"
 
 
 export class DiffViewProvider {
 export class DiffViewProvider {
 	editType?: "create" | "modify"
 	editType?: "create" | "modify"
@@ -13,6 +16,11 @@ export class DiffViewProvider {
 	private documentWasOpen = false
 	private documentWasOpen = false
 	private relPath?: string
 	private relPath?: string
 	private newContent?: string
 	private newContent?: string
+	private activeDiffEditor?: vscode.TextEditor
+	private fadedOverlayController?: DecorationController
+	private activeLineController?: DecorationController
+	private streamedLines: string[] = []
+	private preDiagnostics: [vscode.Uri, vscode.Diagnostic[]][] = []
 
 
 	constructor(private cwd: string) {}
 	constructor(private cwd: string) {}
 
 
@@ -20,9 +28,7 @@ export class DiffViewProvider {
 		this.relPath = relPath
 		this.relPath = relPath
 		const fileExists = this.editType === "modify"
 		const fileExists = this.editType === "modify"
 		const absolutePath = path.resolve(this.cwd, relPath)
 		const absolutePath = path.resolve(this.cwd, relPath)
-
 		this.isEditing = true
 		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) {
 		if (fileExists) {
 			const existingDocument = vscode.workspace.textDocuments.find((doc) =>
 			const existingDocument = vscode.workspace.textDocuments.find((doc) =>
@@ -34,69 +40,22 @@ export class DiffViewProvider {
 		}
 		}
 
 
 		// get diagnostics before editing the file, we'll compare to diagnostics after editing to see if claude needs to fix anything
 		// get diagnostics before editing the file, we'll compare to diagnostics after editing to see if claude needs to fix anything
-		// const preDiagnostics = vscode.languages.getDiagnostics()
+		this.preDiagnostics = vscode.languages.getDiagnostics()
 
 
 		if (fileExists) {
 		if (fileExists) {
 			this.originalContent = await fs.readFile(absolutePath, "utf-8")
 			this.originalContent = await fs.readFile(absolutePath, "utf-8")
-			// fix issue where claude always removes newline from the file
-			// const eol = this.originalContent.includes("\r\n") ? "\r\n" : "\n"
-			// if (this.originalContent.endsWith(eol) && !this.newContent.endsWith(eol)) {
-			// 	this.newContent += eol
-			// }
 		} else {
 		} else {
 			this.originalContent = ""
 			this.originalContent = ""
 		}
 		}
-
-		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
 		// 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
 		this.createdDirs = await createDirectoriesForFile(absolutePath)
 		this.createdDirs = await createDirectoriesForFile(absolutePath)
-		// console.log(`Created directories: ${createdDirs.join(", ")}`)
 		// make sure the file exists before we open it
 		// make sure the file exists before we open it
 		if (!fileExists) {
 		if (!fileExists) {
 			await fs.writeFile(absolutePath, "")
 			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.
-		// 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
-		// a consistent editing experience across all platforms. This also has the added benefit of not
-		// polluting the user's workspace with temporary files.
-
-		// 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 inMemoryDocument = await vscode.workspace.openTextDocument(inMemoryDocumentUri)
-		// const edit = new vscode.WorkspaceEdit()
-		// edit.insert(inMemoryDocumentUri, new vscode.Position(0, 0), newContent)
-		// await vscode.workspace.applyEdit(edit)
-
-		// Show diff
-		await vscode.commands.executeCommand(
-			"vscode.diff",
-			vscode.Uri.parse(`claude-dev-diff:${fileName}`).with({
-				query: Buffer.from(this.originalContent).toString("base64"),
-			}),
-			updatedDocument.uri,
-			`${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)
 		// 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
 		this.documentWasOpen = false
-
-		// close the tab if it's open
+		// close the tab if it's open (it's already saved above)
 		const tabs = vscode.window.tabGroups.all
 		const tabs = vscode.window.tabGroups.all
 			.map((tg) => tg.tabs)
 			.map((tg) => tg.tabs)
 			.flat()
 			.flat()
@@ -104,229 +63,145 @@ export class DiffViewProvider {
 				(tab) => tab.input instanceof vscode.TabInputText && arePathsEqual(tab.input.uri.fsPath, absolutePath)
 				(tab) => tab.input instanceof vscode.TabInputText && arePathsEqual(tab.input.uri.fsPath, absolutePath)
 			)
 			)
 		for (const tab of tabs) {
 		for (const tab of tabs) {
-			await vscode.window.tabGroups.close(tab)
+			if (!tab.isDirty) {
+				await vscode.window.tabGroups.close(tab)
+			}
 			this.documentWasOpen = true
 			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
+		this.fadedOverlayController.addLines(0, this.activeDiffEditor.document.lineCount)
+		this.scrollEditorToLine(0) // will this crash for new files?
+		this.streamedLines = []
 	}
 	}
 
 
-	async update(newContent: string): Promise<void> {
-		if (!this.relPath) {
+	async update(accumulatedContent: string, isFinal: boolean) {
+		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
+		}
+		const diffLines = accumulatedLines.slice(this.streamedLines.length)
+		const document = vscode.window.activeTextEditor?.document
+		if (!document) {
+			console.error("No active text editor")
 			return
 			return
 		}
 		}
-		this.newContent = newContent
-
-		const fileExists = this.editType === "modify"
-		const absolutePath = path.resolve(this.cwd, this.relPath)
-
-		const updatedDocument = vscode.workspace.textDocuments.find((doc) =>
-			arePathsEqual(doc.uri.fsPath, absolutePath)
-		)!
-
-		// edit needs to happen after we close the original tab
-		const edit = new vscode.WorkspaceEdit()
-		if (!fileExists) {
-			// edit.insert(updatedDocument.uri, new vscode.Position(0, 0), newContent)
-			const fullRange = new vscode.Range(
-				updatedDocument.positionAt(0),
-				updatedDocument.positionAt(updatedDocument.getText().length)
-			)
-			edit.replace(updatedDocument.uri, fullRange, this.newContent)
-		} else {
-			const fullRange = new vscode.Range(
-				updatedDocument.positionAt(0),
-				updatedDocument.positionAt(updatedDocument.getText().length)
-			)
-			edit.replace(updatedDocument.uri, fullRange, this.newContent)
+		const diffViewEditor = vscode.window.activeTextEditor
+		if (!diffViewEditor) {
+			console.error("No active diff view editor")
+			return
+		}
+		for (let i = 0; i < diffLines.length; i++) {
+			const currentLine = this.streamedLines.length + i
+			// Replace all content up to the current line with accumulated lines
+			// This is necessary (as compared to inserting one line at a time) to handle cases where html tags on previous lines are auto closed for example
+			const edit = new vscode.WorkspaceEdit()
+			const rangeToReplace = new vscode.Range(0, 0, currentLine + 1, 0)
+			const contentToReplace = accumulatedLines.slice(0, currentLine + 1).join("\n") + "\n"
+			edit.replace(document.uri, rangeToReplace, contentToReplace)
+			await vscode.workspace.applyEdit(edit)
+			// Update decorations
+			this.activeLineController.setActiveLine(currentLine)
+			this.fadedOverlayController.updateOverlayAfterLine(currentLine, document.lineCount)
+			// Scroll to the current line
+			this.scrollEditorToLine(currentLine)
+		}
+		// 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
+			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)
+			}
+			// Add empty last line if original content had one
+			const hasEmptyLastLine = this.originalContent?.endsWith("\n")
+			if (hasEmptyLastLine) {
+				const accumulatedLines = accumulatedContent.split("\n")
+				if (accumulatedLines[accumulatedLines.length - 1] !== "") {
+					accumulatedContent += "\n"
+				}
+			}
+			// Clear all decorations at the end (before applying final edit)
+			this.fadedOverlayController.clear()
+			this.activeLineController.clear()
 		}
 		}
-		// 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
-
-		// Find the first range where the content differs and scroll to it
-		// if (fileExists) {
-		// 	const diffResult = diff.diffLines(originalContent, newContent)
-		// 	for (let i = 0, lineCount = 0; i < diffResult.length; i++) {
-		// 		const part = diffResult[i]
-		// 		if (part.added || part.removed) {
-		// 			const startLine = lineCount + 1
-		// 			const endLine = lineCount + (part.count || 0)
-		// 			const activeEditor = vscode.window.activeTextEditor
-		// 			if (activeEditor) {
-		// 				try {
-		// 					activeEditor.revealRange(
-		// 						// + 3 to move the editor up slightly as this looks better
-		// 						new vscode.Range(
-		// 							new vscode.Position(startLine, 0),
-		// 							new vscode.Position(Math.min(endLine + 3, activeEditor.document.lineCount - 1), 0)
-		// 						),
-		// 						vscode.TextEditorRevealType.InCenter
-		// 					)
-		// 				} catch (error) {
-		// 					console.error(`Error revealing range for ${absolutePath}: ${error}`)
-		// 				}
-		// 			}
-		// 			break
-		// 		}
-		// 		lineCount += part.count || 0
-		// 	}
-		// }
-
-		// remove cursor from the document
-		// await vscode.commands.executeCommand("workbench.action.focusSideBar")
-
-		// 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)
-		// 	// try {
-		// 	// 	await vscode.window.showTextDocument(inMemoryDocument, {
-		// 	// 		preview: false, // ensures it opens in non-preview tab (preview tabs are easily replaced)
-		// 	// 		preserveFocus: false,
-		// 	// 	})
-		// 	// 	// await vscode.window.showTextDocument(inMemoryDocument.uri, { preview: true, preserveFocus: false })
-		// 	// } catch (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()
-		// }
 	}
 	}
 
 
-	// async applyEdit(relPath: string, newContent: string): Promise<void> {}
-
-	async saveChanges() {
-		if (!this.relPath || !this.newContent) {
-			return
+	async saveChanges(): Promise<{ newProblemsMessage: string | undefined; userEdits: string | undefined }> {
+		if (!this.relPath || !this.newContent || !this.activeDiffEditor) {
+			return { newProblemsMessage: undefined, userEdits: undefined }
 		}
 		}
-
 		const absolutePath = path.resolve(this.cwd, this.relPath)
 		const absolutePath = path.resolve(this.cwd, this.relPath)
-
-		const updatedDocument = vscode.workspace.textDocuments.find((doc) =>
-			arePathsEqual(doc.uri.fsPath, absolutePath)
-		)!
-
+		const updatedDocument = this.activeDiffEditor.document
 		const editedContent = updatedDocument.getText()
 		const editedContent = updatedDocument.getText()
 		if (updatedDocument.isDirty) {
 		if (updatedDocument.isDirty) {
 			await updatedDocument.save()
 			await updatedDocument.save()
 		}
 		}
 
 
-		// 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)
-
-		// 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
-		// try {
-		// 	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()
-		// } catch (saveError) {
-		// 	console.log(`Could not open editor for ${absolutePath}: ${saveError}`)
-		// 	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)
-		// 	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 })
-
 		await this.closeAllDiffViews()
 		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 Claude's edit, we know they're
-			directly related to the work he's doing. This eliminates the risk of Claude
-			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 Claude 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, Claude 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, Claude 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 = diagnosticsToProblemsString(
-		// 	getNewDiagnostics(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)
-		// 	],
-		// 	cwd
-		// ) // will be empty string if no errors
-		// const newProblemsMessage =
-		// 	newProblems.length > 0 ? `\n\nNew problems detected after saving the file:\n${newProblems}` : ""
-		// // await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), { preview: false })
+		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 Claude's edit, we know they're
+		directly related to the work he's doing. This eliminates the risk of Claude
+		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 Claude 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, Claude 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, Claude 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 = 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
+		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 newContentEOL = this.newContent.includes("\r\n") ? "\r\n" : "\n"
-		const normalizedEditedContent = editedContent.replace(/\r\n|\n/g, newContentEOL)
-		const normalizedNewContent = this.newContent.replace(/\r\n|\n/g, newContentEOL) // just in case the new content has a mix of varying EOL characters
-
+		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
+		const normalizedNewContent = this.newContent.replace(/\r\n|\n/g, newContentEOL).trimEnd() + newContentEOL
 		if (normalizedEditedContent !== normalizedNewContent) {
 		if (normalizedEditedContent !== normalizedNewContent) {
 			// user made changes before approving edit
 			// user made changes before approving edit
-			return formatResponse.createPrettyPatch(
+			const userEdits = formatResponse.createPrettyPatch(
 				this.relPath.toPosix(),
 				this.relPath.toPosix(),
 				normalizedNewContent,
 				normalizedNewContent,
 				normalizedEditedContent
 				normalizedEditedContent
 			)
 			)
+			return { newProblemsMessage, userEdits }
 		} else {
 		} else {
 			// no changes to claude's edits
 			// no changes to claude's edits
-			return undefined
+			return { newProblemsMessage, userEdits: undefined }
 		}
 		}
 	}
 	}
 
 
 	async revertChanges(): Promise<void> {
 	async revertChanges(): Promise<void> {
-		if (!this.relPath) {
+		if (!this.relPath || !this.activeDiffEditor) {
 			return
 			return
 		}
 		}
 		const fileExists = this.editType === "modify"
 		const fileExists = this.editType === "modify"
-		const updatedDocument = vscode.workspace.textDocuments.find((doc) =>
-			arePathsEqual(doc.uri.fsPath, absolutePath)
-		)!
+		const updatedDocument = this.activeDiffEditor.document
 		const absolutePath = path.resolve(this.cwd, this.relPath)
 		const absolutePath = path.resolve(this.cwd, this.relPath)
 		if (!fileExists) {
 		if (!fileExists) {
 			if (updatedDocument.isDirty) {
 			if (updatedDocument.isDirty) {
@@ -366,13 +241,11 @@ export class DiffViewProvider {
 
 
 	private async closeAllDiffViews() {
 	private async closeAllDiffViews() {
 		const tabs = vscode.window.tabGroups.all
 		const tabs = vscode.window.tabGroups.all
-			.map((tg) => tg.tabs)
-			.flat()
+			.flatMap((tg) => tg.tabs)
 			.filter(
 			.filter(
 				(tab) =>
 				(tab) =>
 					tab.input instanceof vscode.TabInputTextDiff && tab.input?.original?.scheme === "claude-dev-diff"
 					tab.input instanceof vscode.TabInputTextDiff && tab.input?.original?.scheme === "claude-dev-diff"
 			)
 			)
-
 		for (const tab of tabs) {
 		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) {
 			if (!tab.isDirty) {
@@ -381,6 +254,82 @@ export class DiffViewProvider {
 		}
 		}
 	}
 	}
 
 
+	private async openDiffEditor(): Promise<vscode.TextEditor> {
+		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
+		const diffTab = vscode.window.tabGroups.all
+			.flatMap((group) => group.tabs)
+			.find(
+				(tab) =>
+					tab.input instanceof vscode.TabInputTextDiff &&
+					tab.input?.original?.scheme === "claude-dev-diff" &&
+					arePathsEqual(tab.input.modified.fsPath, uri.fsPath)
+			)
+		if (diffTab && diffTab.input instanceof vscode.TabInputTextDiff) {
+			const editor = await vscode.window.showTextDocument(diffTab.input.modified)
+			return 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(`claude-dev-diff:${fileName}`).with({
+					query: Buffer.from(this.originalContent ?? "").toString("base64"),
+				}),
+				uri,
+				`${fileName}: ${fileExists ? "Original ↔ Claude's Changes" : "New File"} (Editable)`
+			)
+			// This should never happen but if it does it's worth investigating
+			setTimeout(() => {
+				disposable.dispose()
+				reject(new Error("Failed to open diff editor"))
+			}, 5_000)
+		})
+	}
+
+	private scrollEditorToLine(line: number) {
+		if (this.activeDiffEditor) {
+			const scrollLine = line + 4
+			this.activeDiffEditor.revealRange(
+				new vscode.Range(scrollLine, 0, scrollLine, 0),
+				vscode.TextEditorRevealType.InCenter
+			)
+		}
+	}
+
+	scrollToFirstDiff() {
+		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
+				this.activeDiffEditor.revealRange(
+					new vscode.Range(lineCount, 0, lineCount, 0),
+					vscode.TextEditorRevealType.InCenter
+				)
+				return
+			}
+			if (!part.removed) {
+				lineCount += part.count || 0
+			}
+		}
+	}
+
 	// close editor if open?
 	// close editor if open?
 	async reset() {
 	async reset() {
 		this.editType = undefined
 		this.editType = undefined
@@ -388,7 +337,10 @@ export class DiffViewProvider {
 		this.originalContent = undefined
 		this.originalContent = undefined
 		this.createdDirs = []
 		this.createdDirs = []
 		this.documentWasOpen = false
 		this.documentWasOpen = false
+		this.activeDiffEditor = undefined
+		this.fadedOverlayController = undefined
+		this.activeLineController = undefined
+		this.streamedLines = []
+		this.preDiagnostics = []
 	}
 	}
-
-	// ... (other helper methods like showDiffView, closeExistingTab, deleteNewFile, revertExistingFile, etc.)
 }
 }