Add text decorations based edit streaming

This commit is contained in:
Saoud Rizwan
2024-09-30 15:54:35 -04:00
parent 20c1984fb0
commit c2a2e1b54c
4 changed files with 292 additions and 254 deletions

View File

@@ -893,16 +893,9 @@ export class ClaudeDev {
await this.diffViewProvider.open(relPath)
}
// editor is open, stream content in
await this.diffViewProvider.update(newContent)
await this.diffViewProvider.update(newContent, false)
break
} 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) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("write_to_file", "path"))
@@ -917,6 +910,16 @@ export class ClaudeDev {
}
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({
...sharedMessageProps,
content: fileExists ? undefined : newContent,
@@ -933,7 +936,7 @@ export class ClaudeDev {
await this.diffViewProvider.revertChanges()
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
if (userEdits) {
await this.say(
@@ -945,10 +948,12 @@ export class ClaudeDev {
} satisfies ClaudeSayTool)
)
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 {
pushToolResult(`The content was successfully saved to ${relPath.toPosix()}.`)
pushToolResult(
`The content was successfully saved to ${relPath.toPosix()}.${newProblemsMessage}`
)
}
await this.diffViewProvider.reset()
break

View File

@@ -89,13 +89,13 @@ Remember:
- 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.
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:
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.
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.*

View File

@@ -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)
}
}

View File

@@ -4,6 +4,9 @@ import * as fs from "fs/promises"
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"
export class DiffViewProvider {
editType?: "create" | "modify"
@@ -13,6 +16,11 @@ export class DiffViewProvider {
private documentWasOpen = false
private relPath?: 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) {}
@@ -20,9 +28,7 @@ export class DiffViewProvider {
this.relPath = relPath
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 (fileExists) {
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
// const preDiagnostics = vscode.languages.getDiagnostics()
this.preDiagnostics = vscode.languages.getDiagnostics()
if (fileExists) {
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 {
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
// Keep track of newly created directories
this.createdDirs = await 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.
// 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)
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
.map((tg) => tg.tabs)
.flat()
@@ -104,172 +63,90 @@ export class DiffViewProvider {
(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
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
}
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 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()
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 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 updatedDocument = vscode.workspace.textDocuments.find((doc) =>
arePathsEqual(doc.uri.fsPath, absolutePath)
)!
const updatedDocument = this.activeDiffEditor.document
const editedContent = updatedDocument.getText()
if (updatedDocument.isDirty) {
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 this.closeAllDiffViews()
/*
@@ -289,44 +166,42 @@ export class DiffViewProvider {
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 })
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.
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) {
// user made changes before approving edit
return formatResponse.createPrettyPatch(
const userEdits = formatResponse.createPrettyPatch(
this.relPath.toPosix(),
normalizedNewContent,
normalizedEditedContent
)
return { newProblemsMessage, userEdits }
} else {
// no changes to claude's edits
return undefined
return { newProblemsMessage, userEdits: undefined }
}
}
async revertChanges(): Promise<void> {
if (!this.relPath) {
if (!this.relPath || !this.activeDiffEditor) {
return
}
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)
if (!fileExists) {
if (updatedDocument.isDirty) {
@@ -366,13 +241,11 @@ export class DiffViewProvider {
private async closeAllDiffViews() {
const tabs = vscode.window.tabGroups.all
.map((tg) => tg.tabs)
.flat()
.flatMap((tg) => tg.tabs)
.filter(
(tab) =>
tab.input instanceof vscode.TabInputTextDiff && tab.input?.original?.scheme === "claude-dev-diff"
)
for (const tab of tabs) {
// trying to close dirty views results in save popup
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?
async reset() {
this.editType = undefined
@@ -388,7 +337,10 @@ export class DiffViewProvider {
this.originalContent = undefined
this.createdDirs = []
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.)
}