From dd32435eb13c4abdcfe4aa09fe38fa0796d0a62c Mon Sep 17 00:00:00 2001 From: honeycomb-sh Date: Tue, 24 Sep 2024 02:05:32 +0000 Subject: [PATCH] Implement red diff highlighting in VS Code extension - Create RedDiffHighlightController in VS Code core - Update editor.all.ts to include new controller - Modify ApprovalCodeLensProvider to handle red highlights - Integrate red and green diff display in extension UI --- .../void/src/ApprovalCodeLensProvider.ts | 206 ++++++++++-------- .../redDiffHighlightController.ts | 39 ++++ src/vs/editor/editor.all.ts | 2 + 3 files changed, 156 insertions(+), 91 deletions(-) create mode 100644 src/vs/editor/contrib/redDiffHighlight/redDiffHighlightController.ts diff --git a/extensions/void/src/ApprovalCodeLensProvider.ts b/extensions/void/src/ApprovalCodeLensProvider.ts index 80386fa1..27e2dba8 100644 --- a/extensions/void/src/ApprovalCodeLensProvider.ts +++ b/extensions/void/src/ApprovalCodeLensProvider.ts @@ -1,11 +1,13 @@ import * as vscode from 'vscode'; import { SuggestedEdit } from './getDiffedLines'; +import { RedDiffHighlightController } from 'vs/editor/contrib/redDiffHighlight/redDiffHighlightController'; // each diff on the user's screen right now type DiffType = { diffid: number, lenses: vscode.CodeLens[], greenRange: vscode.Range, + redRange: vscode.Range, originalCode: string, // If a revert happens, we replace the greenRange with this content. } @@ -24,6 +26,7 @@ export class ApprovalCodeLensProvider implements vscode.CodeLensProvider { private _onDidChangeCodeLenses: vscode.EventEmitter = new vscode.EventEmitter(); // signals a UI refresh on .fire() events private _weAreEditing: boolean = false + private _redDiffController: RedDiffHighlightController | undefined; // used internally by vscode public readonly onDidChangeCodeLenses: vscode.Event = this._onDidChangeCodeLenses.event; @@ -37,6 +40,12 @@ export class ApprovalCodeLensProvider implements vscode.CodeLensProvider { // declared by us, registered with vscode.languages.registerCodeLensProvider() constructor() { + // Initialize RedDiffHighlightController + const editor = vscode.window.activeTextEditor; + if (editor) { + this._redDiffController = editor.getContribution(RedDiffHighlightController.ID) as RedDiffHighlightController; + } + // this acts as a useEffect. Every time text changes, clear the diffs in this editor vscode.workspace.onDidChangeTextDocument((e) => { const editor = vscode.window.activeTextEditor @@ -47,18 +56,22 @@ export class ApprovalCodeLensProvider implements vscode.CodeLensProvider { const docUri = editor.document.uri const docUriStr = docUri.toString() this._diffsOfDocument[docUriStr].splice(0) // clear diffs - editor.setDecorations(greenDecoration, []) // clear decorations + editor.setDecorations(greenDecoration, []) // clear green decorations + this._redDiffController?.removeRedHighlight() // clear red decorations this._computedLensesOfDocument[docUriStr] = this._diffsOfDocument[docUriStr].flatMap(diff => diff.lenses) // recompute this._onDidChangeCodeLenses.fire() // refresh }) } - // used by us only - private refreshLenses = (editor: vscode.TextEditor, docUriStr: string) => { - editor.setDecorations(greenDecoration, this._diffsOfDocument[docUriStr].map(diff => diff.greenRange)) // refresh highlighting - this._computedLensesOfDocument[docUriStr] = this._diffsOfDocument[docUriStr].flatMap(diff => diff.lenses) // recompute _computedLensesOfDocument (can optimize this later) - this._onDidChangeCodeLenses.fire() // fire event for vscode to refresh lenses - } +// used by us only +private refreshLenses = (editor: vscode.TextEditor, docUriStr: string) => { + editor.setDecorations(greenDecoration, this._diffsOfDocument[docUriStr].map(diff => diff.greenRange)) // refresh green highlighting + this._diffsOfDocument[docUriStr].forEach(diff => { + this._redDiffController?.addRedHighlight(diff.redRange); // refresh red highlighting + }); + this._computedLensesOfDocument[docUriStr] = this._diffsOfDocument[docUriStr].flatMap(diff => diff.lenses) // recompute _computedLensesOfDocument (can optimize this later) + this._onDidChangeCodeLenses.fire() // fire event for vscode to refresh lenses +} // used by us only public async addNewApprovals(editor: vscode.TextEditor, suggestedEdits: SuggestedEdit[]) { @@ -76,40 +89,45 @@ export class ApprovalCodeLensProvider implements vscode.CodeLensProvider { // must do this before adding codelenses or highlighting so that codelens and highlights will apply to the fresh code and not the old code // apply changes in reverse order so additions don't push down the line numbers of the next edit let workspaceEdit = new vscode.WorkspaceEdit(); - for (let i = suggestedEdits.length - 1; i > -1; i -= 1) { - let suggestedEdit = suggestedEdits[i] +for (let i = suggestedEdits.length - 1; i > -1; i -= 1) { + let suggestedEdit = suggestedEdits[i] - let greenRange: vscode.Range + let greenRange: vscode.Range + let redRange: vscode.Range - // INSERTIONS (e.g. {originalStartLine: 0, originalEndLine: -1}) - if (suggestedEdit.originalStartLine > suggestedEdit.originalEndLine) { - const originalPosition = new vscode.Position(suggestedEdit.originalStartLine, 0) - workspaceEdit.insert(docUri, originalPosition, suggestedEdit.newContent + '\n') // add back in the line we deleted when we made the startline->endline range go negative - greenRange = new vscode.Range(suggestedEdit.startLine, 0, suggestedEdit.endLine + 1, 0) - } - // DELETIONS - else if (suggestedEdit.startLine > suggestedEdit.endLine) { - const deleteRange = new vscode.Range(suggestedEdit.originalStartLine, 0, suggestedEdit.originalEndLine + 1, 0) - workspaceEdit.delete(docUri, deleteRange) - greenRange = new vscode.Range(suggestedEdit.startLine, 0, suggestedEdit.startLine, 0) - suggestedEdit.originalContent += '\n' // add back in the line we deleted when we made the startline->endline range go negative - } - // REPLACEMENTS - else { - const originalRange = new vscode.Range(suggestedEdit.originalStartLine, 0, suggestedEdit.originalEndLine, Number.MAX_SAFE_INTEGER) - workspaceEdit.replace(docUri, originalRange, suggestedEdit.newContent) - greenRange = new vscode.Range(suggestedEdit.startLine, 0, suggestedEdit.endLine, Number.MAX_SAFE_INTEGER) - } + // INSERTIONS (e.g. {originalStartLine: 0, originalEndLine: -1}) + if (suggestedEdit.originalStartLine > suggestedEdit.originalEndLine) { + const originalPosition = new vscode.Position(suggestedEdit.originalStartLine, 0) + workspaceEdit.insert(docUri, originalPosition, suggestedEdit.newContent + '\n') // add back in the line we deleted when we made the startline->endline range go negative + greenRange = new vscode.Range(suggestedEdit.startLine, 0, suggestedEdit.endLine + 1, 0) + redRange = new vscode.Range(suggestedEdit.originalStartLine, 0, suggestedEdit.originalStartLine, 0) + } + // DELETIONS + else if (suggestedEdit.startLine > suggestedEdit.endLine) { + const deleteRange = new vscode.Range(suggestedEdit.originalStartLine, 0, suggestedEdit.originalEndLine + 1, 0) + workspaceEdit.delete(docUri, deleteRange) + greenRange = new vscode.Range(suggestedEdit.startLine, 0, suggestedEdit.startLine, 0) + redRange = deleteRange + suggestedEdit.originalContent += '\n' // add back in the line we deleted when we made the startline->endline range go negative + } + // REPLACEMENTS + else { + const originalRange = new vscode.Range(suggestedEdit.originalStartLine, 0, suggestedEdit.originalEndLine, Number.MAX_SAFE_INTEGER) + workspaceEdit.replace(docUri, originalRange, suggestedEdit.newContent) + greenRange = new vscode.Range(suggestedEdit.startLine, 0, suggestedEdit.endLine, Number.MAX_SAFE_INTEGER) + redRange = originalRange + } - this._diffsOfDocument[docUriStr].push({ - diffid: this._diffidPool, - greenRange: greenRange, - originalCode: suggestedEdit.originalContent, - lenses: [ - new vscode.CodeLens(greenRange, { title: 'Accept', command: 'void.approveDiff', arguments: [{ diffid: this._diffidPool }] }), - new vscode.CodeLens(greenRange, { title: 'Reject', command: 'void.discardDiff', arguments: [{ diffid: this._diffidPool }] }) - ] - }); +this._diffsOfDocument[docUriStr].push({ + diffid: this._diffidPool, + greenRange: greenRange, + redRange: redRange, + originalCode: suggestedEdit.originalContent, + lenses: [ + new vscode.CodeLens(greenRange, { title: 'Accept', command: 'void.approveDiff', arguments: [{ diffid: this._diffidPool }] }), + new vscode.CodeLens(greenRange, { title: 'Reject', command: 'void.discardDiff', arguments: [{ diffid: this._diffidPool }] }) + ] +}); this._diffidPool += 1 } @@ -124,63 +142,69 @@ export class ApprovalCodeLensProvider implements vscode.CodeLensProvider { console.log('diffs after added:', this._diffsOfDocument[docUriStr]) } - // called on void.approveDiff - public async approveDiff({ diffid }: { diffid: number }) { - const editor = vscode.window.activeTextEditor - if (!editor) - return +// called on void.approveDiff +public async approveDiff({ diffid }: { diffid: number }) { + const editor = vscode.window.activeTextEditor + if (!editor) + return - const docUri = editor.document.uri - const docUriStr = docUri.toString() + const docUri = editor.document.uri + const docUriStr = docUri.toString() - // get index of this diff in diffsOfDocument - const index = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid); - if (index === -1) { - console.error('Error: DiffID could not be found: ', diffid, this._diffsOfDocument[docUriStr]) - return - } - - // remove this diff from the diffsOfDocument[docStr] (can change this behavior in future if add something like history) - this._diffsOfDocument[docUriStr].splice(index, 1) - - // refresh - this.refreshLenses(editor, docUriStr) + // get index of this diff in diffsOfDocument + const index = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid); + if (index === -1) { + console.error('Error: DiffID could not be found: ', diffid, this._diffsOfDocument[docUriStr]) + return } + // remove red highlight for this diff + this._redDiffController?.removeRedHighlight(this._diffsOfDocument[docUriStr][index].redRange); - // called on void.discardDiff - public async discardDiff({ diffid }: { diffid: number }) { - const editor = vscode.window.activeTextEditor - if (!editor) - return + // remove this diff from the diffsOfDocument[docStr] (can change this behavior in future if add something like history) + this._diffsOfDocument[docUriStr].splice(index, 1) - const docUri = editor.document.uri - const docUriStr = docUri.toString() - - // get index of this diff in diffsOfDocument - const index = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid); - if (index === -1) { - console.error('Void error: DiffID could not be found: ', diffid, this._diffsOfDocument[docUriStr]) - return - } - - const { greenRange: range, lenses, originalCode } = this._diffsOfDocument[docUriStr][index] // do this before we splice and mess up index - - // remove this diff from the diffsOfDocument[docStr] (can change this behavior in future if add something like history) - this._diffsOfDocument[docUriStr].splice(index, 1) - - // clear the decoration in this diffs range - editor.setDecorations(greenDecoration, this._diffsOfDocument[docUriStr].map(diff => diff.greenRange)) - - // REVERT THE CHANGE (this is the only part that's different from approveDiff) - let workspaceEdit = new vscode.WorkspaceEdit(); - workspaceEdit.replace(docUri, range, originalCode); - this._weAreEditing = true - await vscode.workspace.applyEdit(workspaceEdit) - await vscode.workspace.save(docUri) - this._weAreEditing = false - - // refresh - this.refreshLenses(editor, docUriStr) - } + // refresh + this.refreshLenses(editor, docUriStr) +} + + +// called on void.discardDiff +public async discardDiff({ diffid }: { diffid: number }) { + const editor = vscode.window.activeTextEditor + if (!editor) + return + + const docUri = editor.document.uri + const docUriStr = docUri.toString() + + // get index of this diff in diffsOfDocument + const index = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid); + if (index === -1) { + console.error('Void error: DiffID could not be found: ', diffid, this._diffsOfDocument[docUriStr]) + return + } + + const { greenRange, redRange, originalCode } = this._diffsOfDocument[docUriStr][index] // do this before we splice and mess up index + + // remove this diff from the diffsOfDocument[docStr] (can change this behavior in future if add something like history) + this._diffsOfDocument[docUriStr].splice(index, 1) + + // clear the green decoration in this diff's range + editor.setDecorations(greenDecoration, this._diffsOfDocument[docUriStr].map(diff => diff.greenRange)) + + // clear the red decoration in this diff's range + this._redDiffController?.removeRedHighlight(redRange); + + // REVERT THE CHANGE + let workspaceEdit = new vscode.WorkspaceEdit(); + workspaceEdit.replace(docUri, greenRange, originalCode); + this._weAreEditing = true + await vscode.workspace.applyEdit(workspaceEdit) + await vscode.workspace.save(docUri) + this._weAreEditing = false + + // refresh + this.refreshLenses(editor, docUriStr) +} } diff --git a/src/vs/editor/contrib/redDiffHighlight/redDiffHighlightController.ts b/src/vs/editor/contrib/redDiffHighlight/redDiffHighlightController.ts new file mode 100644 index 00000000..a19a6f17 --- /dev/null +++ b/src/vs/editor/contrib/redDiffHighlight/redDiffHighlightController.ts @@ -0,0 +1,39 @@ +import * as vscode from 'vscode'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorAction, ServicesAccessor, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { IEditorContribution } from 'vs/editor/common/editorCommon'; + +export class RedDiffHighlightController implements IEditorContribution { + public static readonly ID = 'editor.contrib.redDiffHighlight'; + + private readonly _editor: ICodeEditor; + private readonly _redDecoration: vscode.TextEditorDecorationType; + + constructor(editor: ICodeEditor) { + this._editor = editor; + this._redDecoration = vscode.window.createTextEditorDecorationType({ + backgroundColor: 'rgba(255, 0, 0, 0.2)', + isWholeLine: false, + }); + } + + public dispose(): void { + this._redDecoration.dispose(); + } + + public addRedHighlight(range: vscode.Range): void { + const editor = vscode.window.activeTextEditor; + if (editor) { + editor.setDecorations(this._redDecoration, [range]); + } + } + + public removeRedHighlight(): void { + const editor = vscode.window.activeTextEditor; + if (editor) { + editor.setDecorations(this._redDecoration, []); + } + } +} + +registerEditorContribution(RedDiffHighlightController.ID, RedDiffHighlightController); \ No newline at end of file diff --git a/src/vs/editor/editor.all.ts b/src/vs/editor/editor.all.ts index e40c7056..8ef0a556 100644 --- a/src/vs/editor/editor.all.ts +++ b/src/vs/editor/editor.all.ts @@ -63,6 +63,8 @@ import 'vs/editor/contrib/wordOperations/browser/wordOperations'; import 'vs/editor/contrib/wordPartOperations/browser/wordPartOperations'; import 'vs/editor/contrib/readOnlyMessage/browser/contribution'; import 'vs/editor/contrib/diffEditorBreadcrumbs/browser/contribution'; +import 'vs/editor/contrib/redDiffHighlight/redDiffHighlightController'; + // Load up these strings even in VSCode, even if they are not used // in order to get them translated