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
This commit is contained in:
honeycomb-sh 2024-09-24 02:05:32 +00:00
parent 4d76fd80b9
commit dd32435eb1
3 changed files with 156 additions and 91 deletions

View file

@ -1,11 +1,13 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { SuggestedEdit } from './getDiffedLines'; import { SuggestedEdit } from './getDiffedLines';
import { RedDiffHighlightController } from 'vs/editor/contrib/redDiffHighlight/redDiffHighlightController';
// each diff on the user's screen right now // each diff on the user's screen right now
type DiffType = { type DiffType = {
diffid: number, diffid: number,
lenses: vscode.CodeLens[], lenses: vscode.CodeLens[],
greenRange: vscode.Range, greenRange: vscode.Range,
redRange: vscode.Range,
originalCode: string, // If a revert happens, we replace the greenRange with this content. 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<void> = new vscode.EventEmitter<void>(); // signals a UI refresh on .fire() events private _onDidChangeCodeLenses: vscode.EventEmitter<void> = new vscode.EventEmitter<void>(); // signals a UI refresh on .fire() events
private _weAreEditing: boolean = false private _weAreEditing: boolean = false
private _redDiffController: RedDiffHighlightController | undefined;
// used internally by vscode // used internally by vscode
public readonly onDidChangeCodeLenses: vscode.Event<void> = this._onDidChangeCodeLenses.event; public readonly onDidChangeCodeLenses: vscode.Event<void> = this._onDidChangeCodeLenses.event;
@ -37,6 +40,12 @@ export class ApprovalCodeLensProvider implements vscode.CodeLensProvider {
// declared by us, registered with vscode.languages.registerCodeLensProvider() // declared by us, registered with vscode.languages.registerCodeLensProvider()
constructor() { 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 // this acts as a useEffect. Every time text changes, clear the diffs in this editor
vscode.workspace.onDidChangeTextDocument((e) => { vscode.workspace.onDidChangeTextDocument((e) => {
const editor = vscode.window.activeTextEditor const editor = vscode.window.activeTextEditor
@ -47,18 +56,22 @@ export class ApprovalCodeLensProvider implements vscode.CodeLensProvider {
const docUri = editor.document.uri const docUri = editor.document.uri
const docUriStr = docUri.toString() const docUriStr = docUri.toString()
this._diffsOfDocument[docUriStr].splice(0) // clear diffs 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._computedLensesOfDocument[docUriStr] = this._diffsOfDocument[docUriStr].flatMap(diff => diff.lenses) // recompute
this._onDidChangeCodeLenses.fire() // refresh this._onDidChangeCodeLenses.fire() // refresh
}) })
} }
// used by us only // used by us only
private refreshLenses = (editor: vscode.TextEditor, docUriStr: string) => { private refreshLenses = (editor: vscode.TextEditor, docUriStr: string) => {
editor.setDecorations(greenDecoration, this._diffsOfDocument[docUriStr].map(diff => diff.greenRange)) // refresh highlighting editor.setDecorations(greenDecoration, this._diffsOfDocument[docUriStr].map(diff => diff.greenRange)) // refresh green highlighting
this._computedLensesOfDocument[docUriStr] = this._diffsOfDocument[docUriStr].flatMap(diff => diff.lenses) // recompute _computedLensesOfDocument (can optimize this later) this._diffsOfDocument[docUriStr].forEach(diff => {
this._onDidChangeCodeLenses.fire() // fire event for vscode to refresh lenses 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 // used by us only
public async addNewApprovals(editor: vscode.TextEditor, suggestedEdits: SuggestedEdit[]) { 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 // 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 // apply changes in reverse order so additions don't push down the line numbers of the next edit
let workspaceEdit = new vscode.WorkspaceEdit(); let workspaceEdit = new vscode.WorkspaceEdit();
for (let i = suggestedEdits.length - 1; i > -1; i -= 1) { for (let i = suggestedEdits.length - 1; i > -1; i -= 1) {
let suggestedEdit = suggestedEdits[i] let suggestedEdit = suggestedEdits[i]
let greenRange: vscode.Range let greenRange: vscode.Range
let redRange: vscode.Range
// INSERTIONS (e.g. {originalStartLine: 0, originalEndLine: -1}) // INSERTIONS (e.g. {originalStartLine: 0, originalEndLine: -1})
if (suggestedEdit.originalStartLine > suggestedEdit.originalEndLine) { if (suggestedEdit.originalStartLine > suggestedEdit.originalEndLine) {
const originalPosition = new vscode.Position(suggestedEdit.originalStartLine, 0) 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 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) 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) { // DELETIONS
const deleteRange = new vscode.Range(suggestedEdit.originalStartLine, 0, suggestedEdit.originalEndLine + 1, 0) else if (suggestedEdit.startLine > suggestedEdit.endLine) {
workspaceEdit.delete(docUri, deleteRange) const deleteRange = new vscode.Range(suggestedEdit.originalStartLine, 0, suggestedEdit.originalEndLine + 1, 0)
greenRange = new vscode.Range(suggestedEdit.startLine, 0, suggestedEdit.startLine, 0) workspaceEdit.delete(docUri, deleteRange)
suggestedEdit.originalContent += '\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.startLine, 0)
} redRange = deleteRange
// REPLACEMENTS suggestedEdit.originalContent += '\n' // add back in the line we deleted when we made the startline->endline range go negative
else { }
const originalRange = new vscode.Range(suggestedEdit.originalStartLine, 0, suggestedEdit.originalEndLine, Number.MAX_SAFE_INTEGER) // REPLACEMENTS
workspaceEdit.replace(docUri, originalRange, suggestedEdit.newContent) else {
greenRange = new vscode.Range(suggestedEdit.startLine, 0, suggestedEdit.endLine, Number.MAX_SAFE_INTEGER) 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({ this._diffsOfDocument[docUriStr].push({
diffid: this._diffidPool, diffid: this._diffidPool,
greenRange: greenRange, greenRange: greenRange,
originalCode: suggestedEdit.originalContent, redRange: redRange,
lenses: [ originalCode: suggestedEdit.originalContent,
new vscode.CodeLens(greenRange, { title: 'Accept', command: 'void.approveDiff', arguments: [{ diffid: this._diffidPool }] }), lenses: [
new vscode.CodeLens(greenRange, { title: 'Reject', command: 'void.discardDiff', arguments: [{ diffid: this._diffidPool }] }) 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 this._diffidPool += 1
} }
@ -124,63 +142,69 @@ export class ApprovalCodeLensProvider implements vscode.CodeLensProvider {
console.log('diffs after added:', this._diffsOfDocument[docUriStr]) console.log('diffs after added:', this._diffsOfDocument[docUriStr])
} }
// called on void.approveDiff // called on void.approveDiff
public async approveDiff({ diffid }: { diffid: number }) { public async approveDiff({ diffid }: { diffid: number }) {
const editor = vscode.window.activeTextEditor const editor = vscode.window.activeTextEditor
if (!editor) if (!editor)
return return
const docUri = editor.document.uri const docUri = editor.document.uri
const docUriStr = docUri.toString() const docUriStr = docUri.toString()
// get index of this diff in diffsOfDocument // get index of this diff in diffsOfDocument
const index = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid); const index = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid);
if (index === -1) { if (index === -1) {
console.error('Error: DiffID could not be found: ', diffid, this._diffsOfDocument[docUriStr]) console.error('Error: DiffID could not be found: ', diffid, this._diffsOfDocument[docUriStr])
return 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)
} }
// remove red highlight for this diff
this._redDiffController?.removeRedHighlight(this._diffsOfDocument[docUriStr][index].redRange);
// called on void.discardDiff // remove this diff from the diffsOfDocument[docStr] (can change this behavior in future if add something like history)
public async discardDiff({ diffid }: { diffid: number }) { this._diffsOfDocument[docUriStr].splice(index, 1)
const editor = vscode.window.activeTextEditor
if (!editor)
return
const docUri = editor.document.uri // refresh
const docUriStr = docUri.toString() this.refreshLenses(editor, docUriStr)
}
// get index of this diff in diffsOfDocument
const index = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid);
if (index === -1) { // called on void.discardDiff
console.error('Void error: DiffID could not be found: ', diffid, this._diffsOfDocument[docUriStr]) public async discardDiff({ diffid }: { diffid: number }) {
return const editor = vscode.window.activeTextEditor
} if (!editor)
return
const { greenRange: range, lenses, originalCode } = this._diffsOfDocument[docUriStr][index] // do this before we splice and mess up index
const docUri = editor.document.uri
// remove this diff from the diffsOfDocument[docStr] (can change this behavior in future if add something like history) const docUriStr = docUri.toString()
this._diffsOfDocument[docUriStr].splice(index, 1)
// get index of this diff in diffsOfDocument
// clear the decoration in this diffs range const index = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid);
editor.setDecorations(greenDecoration, this._diffsOfDocument[docUriStr].map(diff => diff.greenRange)) if (index === -1) {
console.error('Void error: DiffID could not be found: ', diffid, this._diffsOfDocument[docUriStr])
// REVERT THE CHANGE (this is the only part that's different from approveDiff) return
let workspaceEdit = new vscode.WorkspaceEdit(); }
workspaceEdit.replace(docUri, range, originalCode);
this._weAreEditing = true const { greenRange, redRange, originalCode } = this._diffsOfDocument[docUriStr][index] // do this before we splice and mess up index
await vscode.workspace.applyEdit(workspaceEdit)
await vscode.workspace.save(docUri) // remove this diff from the diffsOfDocument[docStr] (can change this behavior in future if add something like history)
this._weAreEditing = false this._diffsOfDocument[docUriStr].splice(index, 1)
// refresh // clear the green decoration in this diff's range
this.refreshLenses(editor, docUriStr) 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)
}
} }

View file

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

View file

@ -63,6 +63,8 @@ import 'vs/editor/contrib/wordOperations/browser/wordOperations';
import 'vs/editor/contrib/wordPartOperations/browser/wordPartOperations'; import 'vs/editor/contrib/wordPartOperations/browser/wordPartOperations';
import 'vs/editor/contrib/readOnlyMessage/browser/contribution'; import 'vs/editor/contrib/readOnlyMessage/browser/contribution';
import 'vs/editor/contrib/diffEditorBreadcrumbs/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 // Load up these strings even in VSCode, even if they are not used
// in order to get them translated // in order to get them translated