mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 12:21:13 -05:00
Add text decorations based edit streaming
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
src/integrations/editor/DecorationController.ts
Normal file
81
src/integrations/editor/DecorationController.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 (it's already saved above)
|
||||||
// close the tab if it's open
|
|
||||||
const tabs = vscode.window.tabGroups.all
|
const tabs = vscode.window.tabGroups.all
|
||||||
.map((tg) => tg.tabs)
|
.map((tg) => tg.tabs)
|
||||||
.flat()
|
.flat()
|
||||||
@@ -104,172 +63,90 @@ 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) {
|
||||||
|
if (!tab.isDirty) {
|
||||||
await vscode.window.tabGroups.close(tab)
|
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> {
|
async update(accumulatedContent: string, isFinal: boolean) {
|
||||||
if (!this.relPath) {
|
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 diffViewEditor = vscode.window.activeTextEditor
|
||||||
|
if (!diffViewEditor) {
|
||||||
const fileExists = this.editType === "modify"
|
console.error("No active diff view editor")
|
||||||
const absolutePath = path.resolve(this.cwd, this.relPath)
|
return
|
||||||
|
}
|
||||||
const updatedDocument = vscode.workspace.textDocuments.find((doc) =>
|
for (let i = 0; i < diffLines.length; i++) {
|
||||||
arePathsEqual(doc.uri.fsPath, absolutePath)
|
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
|
||||||
// edit needs to happen after we close the original tab
|
|
||||||
const edit = new vscode.WorkspaceEdit()
|
const edit = new vscode.WorkspaceEdit()
|
||||||
if (!fileExists) {
|
const rangeToReplace = new vscode.Range(0, 0, currentLine + 1, 0)
|
||||||
// edit.insert(updatedDocument.uri, new vscode.Position(0, 0), newContent)
|
const contentToReplace = accumulatedLines.slice(0, currentLine + 1).join("\n") + "\n"
|
||||||
const fullRange = new vscode.Range(
|
edit.replace(document.uri, rangeToReplace, contentToReplace)
|
||||||
updatedDocument.positionAt(0),
|
await vscode.workspace.applyEdit(edit)
|
||||||
updatedDocument.positionAt(updatedDocument.getText().length)
|
// Update decorations
|
||||||
)
|
this.activeLineController.setActiveLine(currentLine)
|
||||||
edit.replace(updatedDocument.uri, fullRange, this.newContent)
|
this.fadedOverlayController.updateOverlayAfterLine(currentLine, document.lineCount)
|
||||||
} else {
|
// Scroll to the current line
|
||||||
const fullRange = new vscode.Range(
|
this.scrollEditorToLine(currentLine)
|
||||||
updatedDocument.positionAt(0),
|
}
|
||||||
updatedDocument.positionAt(updatedDocument.getText().length)
|
// Update the streamedLines with the new accumulated content
|
||||||
)
|
this.streamedLines = accumulatedLines
|
||||||
edit.replace(updatedDocument.uri, fullRange, this.newContent)
|
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(): Promise<{ newProblemsMessage: string | undefined; userEdits: string | undefined }> {
|
||||||
|
if (!this.relPath || !this.newContent || !this.activeDiffEditor) {
|
||||||
async saveChanges() {
|
return { newProblemsMessage: undefined, userEdits: undefined }
|
||||||
if (!this.relPath || !this.newContent) {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const absolutePath = path.resolve(this.cwd, this.relPath)
|
const absolutePath = path.resolve(this.cwd, this.relPath)
|
||||||
|
const updatedDocument = this.activeDiffEditor.document
|
||||||
const updatedDocument = vscode.workspace.textDocuments.find((doc) =>
|
|
||||||
arePathsEqual(doc.uri.fsPath, absolutePath)
|
|
||||||
)!
|
|
||||||
|
|
||||||
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()
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -289,44 +166,42 @@ export class DiffViewProvider {
|
|||||||
applying a fix, Claude won't be notified, which is generally fine since the
|
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.
|
initial fix is usually correct and it may just take time for linters to catch up.
|
||||||
*/
|
*/
|
||||||
// const postDiagnostics = vscode.languages.getDiagnostics()
|
const postDiagnostics = vscode.languages.getDiagnostics()
|
||||||
// const newProblems = diagnosticsToProblemsString(
|
const newProblems = diagnosticsToProblemsString(
|
||||||
// getNewDiagnostics(preDiagnostics, postDiagnostics),
|
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)
|
vscode.DiagnosticSeverity.Error, // only including errors since warnings can be distracting (if user wants to fix warnings they can use the @problems mention)
|
||||||
// ],
|
],
|
||||||
// cwd
|
this.cwd
|
||||||
// ) // will be empty string if no errors
|
) // will be empty string if no errors
|
||||||
// const newProblemsMessage =
|
const newProblemsMessage =
|
||||||
// newProblems.length > 0 ? `\n\nNew problems detected after saving the file:\n${newProblems}` : ""
|
newProblems.length > 0 ? `\n\nNew problems detected after saving the file:\n${newProblems}` : ""
|
||||||
// // await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), { preview: false })
|
|
||||||
|
|
||||||
// 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 normalizedEditedContent = editedContent.replace(/\r\n|\n/g, newContentEOL).trimEnd() + newContentEOL // trimEnd to fix issue where editor adds in extra new line automatically
|
||||||
const normalizedNewContent = this.newContent.replace(/\r\n|\n/g, newContentEOL) // just in case the new content has a mix of varying EOL characters
|
// 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) =>
|
const updatedDocument = this.activeDiffEditor.document
|
||||||
arePathsEqual(doc.uri.fsPath, absolutePath)
|
|
||||||
)!
|
|
||||||
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)
|
.flatMap((tg) => tg.tabs)
|
||||||
.flat()
|
|
||||||
.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.)
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user