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 { 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<void> = new vscode.EventEmitter<void>(); // signals a UI refresh on .fire() events
private _weAreEditing: boolean = false
private _redDiffController: RedDiffHighlightController | undefined;
// used internally by vscode
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()
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)
}
}

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/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