From 1c9b755deeeb35f6922eda96b960317432b3aca4 Mon Sep 17 00:00:00 2001 From: Nakul Date: Fri, 7 Nov 2025 21:52:03 +0530 Subject: [PATCH 1/7] Adding Arrow button to revert a single block changes in cell --- src/diff/cell.ts | 101 ++++++++++++++++++++++++++++++++++++++++++----- style/base.css | 9 +++++ 2 files changed, 101 insertions(+), 9 deletions(-) diff --git a/src/diff/cell.ts b/src/diff/cell.ts index fdbf1eb..eb506cc 100644 --- a/src/diff/cell.ts +++ b/src/diff/cell.ts @@ -1,6 +1,12 @@ import { python } from '@codemirror/lang-python'; -import { MergeView } from '@codemirror/merge'; -import { EditorView } from '@codemirror/view'; +import { MergeView, getChunks } from '@codemirror/merge'; +import { + EditorView, + Decoration, + WidgetType, + DecorationSet +} from '@codemirror/view'; +import { StateEffect, StateField, RangeSetBuilder } from '@codemirror/state'; import { jupyterTheme } from '@jupyterlab/codemirror'; import { Message } from '@lumino/messaging'; import { Widget } from '@lumino/widgets'; @@ -52,7 +58,8 @@ class CodeMirrorSplitDiffWidget extends BaseDiffWidget { basicSetup, python(), EditorView.editable.of(false), - jupyterTheme + jupyterTheme, + splitDiffDecorationField ] }, b: { @@ -60,29 +67,105 @@ class CodeMirrorSplitDiffWidget extends BaseDiffWidget { extensions: [ basicSetup, python(), - EditorView.editable.of(false), - jupyterTheme + EditorView.editable.of(true), + jupyterTheme, + splitDiffDecorationField ] }, - parent: this.node + parent: this.node, + gutter: true, + highlightChanges: true }); + + this._renderMergeButtons(); } /** - * Destroy the split view and clean up resources. + * Render "merge change" buttons in the diff on left editor. */ + private _renderMergeButtons(): void { + const editorA = this._splitView.a; + const editorB = this._splitView.b; + + const result = getChunks(editorA.state); + const chunks = result?.chunks; + + if (!chunks || chunks.length === 0) { + return; + } + + const builder = new RangeSetBuilder(); + + chunks.forEach((chunk: any) => { + const { fromA, toA, fromB, toB } = chunk; + + const arrowWidget = Decoration.widget({ + widget: new (class extends WidgetType { + toDOM() { + const btn = document.createElement('button'); + btn.textContent = '🡪'; + btn.className = 'jp-DiffMergeArrow'; + btn.onclick = () => { + const origText = editorA.state.doc.sliceString(fromA, toA); + + editorB.dispatch({ + changes: { from: fromB, to: toB, insert: origText } + }); + editorA.dispatch({ + effects: addSplitDiffDecorations.of( + editorA.state.field(splitDiffDecorationField).update({ + filter: (from, to, value) => from !== fromA + }) + ) + }); + }; + return btn; + } + })(), + side: 1 + }); + + builder.add(fromA, fromA, arrowWidget); + }); + + editorA.dispatch({ + effects: addSplitDiffDecorations.of(builder.finish()) + }); + } + private _destroySplitView(): void { if (this._splitView) { this._splitView.destroy(); - this._splitView = null; + this._splitView = null!; } } private _originalCode: string; private _modifiedCode: string; - private _splitView: MergeView | null = null; + + private _splitView!: MergeView & { + a: EditorView; + b: EditorView; + }; } +const addSplitDiffDecorations = StateEffect.define(); + +const splitDiffDecorationField = StateField.define({ + create() { + return Decoration.none; + }, + update(deco, tr) { + for (const ef of tr.effects) { + if (ef.is(addSplitDiffDecorations)) { + return ef.value; + } + } + return deco.map(tr.changes); + }, + provide: f => EditorView.decorations.from(f) +}); + export async function createCodeMirrorSplitDiffWidget( options: IDiffWidgetOptions ): Promise { diff --git a/style/base.css b/style/base.css index 82fd63e..bf62a14 100644 --- a/style/base.css +++ b/style/base.css @@ -77,3 +77,12 @@ background-color: var(--jp-layout-color3); border-color: var(--jp-border-color1); } + +.jp-DiffMergeArrow { + padding: 2px 6px; + border: none; + background: none; + font-size: 15px; + cursor: pointer; + margin-left: 4px; +} From c78528df6baf853736d34e88cacc370000a4513b Mon Sep 17 00:00:00 2001 From: Nakul Date: Fri, 7 Nov 2025 22:07:56 +0530 Subject: [PATCH 2/7] adding duplicate code to make right pane editable --- src/diff/cell.ts | 10 +++++++++- src/widget.ts | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/diff/cell.ts b/src/diff/cell.ts index eb506cc..6bb0f0b 100644 --- a/src/diff/cell.ts +++ b/src/diff/cell.ts @@ -69,7 +69,15 @@ class CodeMirrorSplitDiffWidget extends BaseDiffWidget { python(), EditorView.editable.of(true), jupyterTheme, - splitDiffDecorationField + splitDiffDecorationField, + EditorView.updateListener.of(update => { + if (update.docChanged) { + const newText = update.state.doc.toString(); + + this._modifiedCode = newText; + this._newSource = newText; + } + }) ] }, parent: this.node, diff --git a/src/widget.ts b/src/widget.ts index e37ef67..a744f16 100644 --- a/src/widget.ts +++ b/src/widget.ts @@ -100,7 +100,7 @@ export abstract class BaseDiffWidget extends Widget { */ public onAcceptClick(): void { if (this._cell) { - this._cell.sharedModel.setSource(this._newSource); + this._cell.sharedModel.setSource(this._newSource || this._originalSource); this._closeDiffView(); } } @@ -191,9 +191,9 @@ export abstract class BaseDiffWidget extends Widget { private _cell: ICellModel; private _cellFooterTracker: ICellFooterTracker; private _originalSource: string; - private _newSource: string; private _showActionButtons: boolean; private _openDiff: boolean; private _toggleButton: ToolbarButton | null = null; private _trans: TranslationBundle; + public _newSource: string; } From 099948b13416c6774730b403971dce65df1df76d Mon Sep 17 00:00:00 2001 From: Nakul Date: Sat, 8 Nov 2025 21:28:58 +0530 Subject: [PATCH 3/7] hide button is both pane are same and after reverting changes --- src/diff/cell.ts | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/src/diff/cell.ts b/src/diff/cell.ts index 6bb0f0b..4fc10c0 100644 --- a/src/diff/cell.ts +++ b/src/diff/cell.ts @@ -76,6 +76,8 @@ class CodeMirrorSplitDiffWidget extends BaseDiffWidget { this._modifiedCode = newText; this._newSource = newText; + + this._renderMergeButtons(); } }) ] @@ -95,17 +97,32 @@ class CodeMirrorSplitDiffWidget extends BaseDiffWidget { const editorA = this._splitView.a; const editorB = this._splitView.b; - const result = getChunks(editorA.state); - const chunks = result?.chunks; + const result = getChunks(editorB.state); + const chunks = result?.chunks ?? []; - if (!chunks || chunks.length === 0) { - return; - } + const updatedSet = new Set(); + chunks.forEach((chunk: any) => { + const id = `${chunk.fromA}-${chunk.toA}`; + updatedSet.add(id); + + if (!this._activeChunks.has(id)) { + this._activeChunks.add(id); + } + }); + for (const id of this._activeChunks) { + if (!updatedSet.has(id)) { + this._activeChunks.delete(id); + } + } const builder = new RangeSetBuilder(); chunks.forEach((chunk: any) => { const { fromA, toA, fromB, toB } = chunk; + const id = `${fromA}-${toA}`; + + // eslint-disable-next-line @typescript-eslint/no-this-alias + const diffWidget = this; const arrowWidget = Decoration.widget({ widget: new (class extends WidgetType { @@ -119,13 +136,9 @@ class CodeMirrorSplitDiffWidget extends BaseDiffWidget { editorB.dispatch({ changes: { from: fromB, to: toB, insert: origText } }); - editorA.dispatch({ - effects: addSplitDiffDecorations.of( - editorA.state.field(splitDiffDecorationField).update({ - filter: (from, to, value) => from !== fromA - }) - ) - }); + + diffWidget._activeChunks.delete(id); + diffWidget._renderMergeButtons(); }; return btn; } @@ -150,6 +163,7 @@ class CodeMirrorSplitDiffWidget extends BaseDiffWidget { private _originalCode: string; private _modifiedCode: string; + private _activeChunks = new Set(); private _splitView!: MergeView & { a: EditorView; From e534b4c22fa245a89f1aad21228b34973de55182 Mon Sep 17 00:00:00 2001 From: Nakul Date: Sat, 8 Nov 2025 21:39:13 +0530 Subject: [PATCH 4/7] updading names --- src/diff/cell.ts | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/diff/cell.ts b/src/diff/cell.ts index 4fc10c0..373980a 100644 --- a/src/diff/cell.ts +++ b/src/diff/cell.ts @@ -59,7 +59,7 @@ class CodeMirrorSplitDiffWidget extends BaseDiffWidget { python(), EditorView.editable.of(false), jupyterTheme, - splitDiffDecorationField + splitDiffDecoField ] }, b: { @@ -69,7 +69,7 @@ class CodeMirrorSplitDiffWidget extends BaseDiffWidget { python(), EditorView.editable.of(true), jupyterTheme, - splitDiffDecorationField, + splitDiffDecoField, EditorView.updateListener.of(update => { if (update.docChanged) { const newText = update.state.doc.toString(); @@ -77,7 +77,7 @@ class CodeMirrorSplitDiffWidget extends BaseDiffWidget { this._modifiedCode = newText; this._newSource = newText; - this._renderMergeButtons(); + this._renderArrowButtons(); } }) ] @@ -87,17 +87,17 @@ class CodeMirrorSplitDiffWidget extends BaseDiffWidget { highlightChanges: true }); - this._renderMergeButtons(); + this._renderArrowButtons(); } /** * Render "merge change" buttons in the diff on left editor. */ - private _renderMergeButtons(): void { - const editorA = this._splitView.a; - const editorB = this._splitView.b; + private _renderArrowButtons(): void { + const paneA = this._splitView.a; + const paneB = this._splitView.b; - const result = getChunks(editorB.state); + const result = getChunks(paneB.state); const chunks = result?.chunks ?? []; const updatedSet = new Set(); @@ -127,20 +127,20 @@ class CodeMirrorSplitDiffWidget extends BaseDiffWidget { const arrowWidget = Decoration.widget({ widget: new (class extends WidgetType { toDOM() { - const btn = document.createElement('button'); - btn.textContent = '🡪'; - btn.className = 'jp-DiffMergeArrow'; - btn.onclick = () => { - const origText = editorA.state.doc.sliceString(fromA, toA); + const arrowBtn = document.createElement('button'); + arrowBtn.textContent = '🡪'; + arrowBtn.className = 'jp-DiffMergeArrow'; + arrowBtn.onclick = () => { + const origText = paneA.state.doc.sliceString(fromA, toA); - editorB.dispatch({ + paneB.dispatch({ changes: { from: fromB, to: toB, insert: origText } }); diffWidget._activeChunks.delete(id); - diffWidget._renderMergeButtons(); + diffWidget._renderArrowButtons(); }; - return btn; + return arrowBtn; } })(), side: 1 @@ -149,8 +149,8 @@ class CodeMirrorSplitDiffWidget extends BaseDiffWidget { builder.add(fromA, fromA, arrowWidget); }); - editorA.dispatch({ - effects: addSplitDiffDecorations.of(builder.finish()) + paneA.dispatch({ + effects: addSplitDiffDeco.of(builder.finish()) }); } @@ -171,15 +171,15 @@ class CodeMirrorSplitDiffWidget extends BaseDiffWidget { }; } -const addSplitDiffDecorations = StateEffect.define(); +const addSplitDiffDeco = StateEffect.define(); -const splitDiffDecorationField = StateField.define({ +const splitDiffDecoField = StateField.define({ create() { return Decoration.none; }, update(deco, tr) { for (const ef of tr.effects) { - if (ef.is(addSplitDiffDecorations)) { + if (ef.is(addSplitDiffDeco)) { return ef.value; } } From 17c06c1287459d39afb7559b99701c6f0a84839c Mon Sep 17 00:00:00 2001 From: Nakul Date: Mon, 10 Nov 2025 18:16:22 +0530 Subject: [PATCH 5/7] updating arrow button handling and visibility --- src/diff/cell.ts | 133 +++++++++++++++++++---------------------------- style/base.css | 38 +++++++++++--- 2 files changed, 85 insertions(+), 86 deletions(-) diff --git a/src/diff/cell.ts b/src/diff/cell.ts index 373980a..6cc2b66 100644 --- a/src/diff/cell.ts +++ b/src/diff/cell.ts @@ -1,12 +1,6 @@ import { python } from '@codemirror/lang-python'; import { MergeView, getChunks } from '@codemirror/merge'; -import { - EditorView, - Decoration, - WidgetType, - DecorationSet -} from '@codemirror/view'; -import { StateEffect, StateField, RangeSetBuilder } from '@codemirror/state'; +import { EditorView } from '@codemirror/view'; import { jupyterTheme } from '@jupyterlab/codemirror'; import { Message } from '@lumino/messaging'; import { Widget } from '@lumino/widgets'; @@ -58,8 +52,7 @@ class CodeMirrorSplitDiffWidget extends BaseDiffWidget { basicSetup, python(), EditorView.editable.of(false), - jupyterTheme, - splitDiffDecoField + jupyterTheme ] }, b: { @@ -69,7 +62,6 @@ class CodeMirrorSplitDiffWidget extends BaseDiffWidget { python(), EditorView.editable.of(true), jupyterTheme, - splitDiffDecoField, EditorView.updateListener.of(update => { if (update.docChanged) { const newText = update.state.doc.toString(); @@ -87,71 +79,68 @@ class CodeMirrorSplitDiffWidget extends BaseDiffWidget { highlightChanges: true }); - this._renderArrowButtons(); + const container = this._splitView.dom; + const overlay = document.createElement('div'); + overlay.className = 'jp-DiffArrowOverlay'; + container.appendChild(overlay); + this._arrowOverlay = overlay; + + this._addScrollSync(); + setTimeout(() => this._renderArrowButtons(), 50); } /** * Render "merge change" buttons in the diff on left editor. */ private _renderArrowButtons(): void { + if (!this._splitView) { + return; + } + const paneA = this._splitView.a; const paneB = this._splitView.b; - const result = getChunks(paneB.state); const chunks = result?.chunks ?? []; - const updatedSet = new Set(); - chunks.forEach((chunk: any) => { - const id = `${chunk.fromA}-${chunk.toA}`; - updatedSet.add(id); + this._arrowOverlay.innerHTML = ''; - if (!this._activeChunks.has(id)) { - this._activeChunks.add(id); - } - }); - - for (const id of this._activeChunks) { - if (!updatedSet.has(id)) { - this._activeChunks.delete(id); - } - } - const builder = new RangeSetBuilder(); - - chunks.forEach((chunk: any) => { + chunks.forEach(chunk => { const { fromA, toA, fromB, toB } = chunk; - const id = `${fromA}-${toA}`; - - // eslint-disable-next-line @typescript-eslint/no-this-alias - const diffWidget = this; - - const arrowWidget = Decoration.widget({ - widget: new (class extends WidgetType { - toDOM() { - const arrowBtn = document.createElement('button'); - arrowBtn.textContent = '🡪'; - arrowBtn.className = 'jp-DiffMergeArrow'; - arrowBtn.onclick = () => { - const origText = paneA.state.doc.sliceString(fromA, toA); - - paneB.dispatch({ - changes: { from: fromB, to: toB, insert: origText } - }); - - diffWidget._activeChunks.delete(id); - diffWidget._renderArrowButtons(); - }; - return arrowBtn; - } - })(), - side: 1 - }); - - builder.add(fromA, fromA, arrowWidget); + const lineBlockA = paneA.lineBlockAt(fromA); + const lineBlockB = paneB.lineBlockAt(fromB); + const midTop = (lineBlockA.top + lineBlockB.top) / 2; + + const connector = document.createElement('div'); + connector.className = 'jp-DiffConnectorLine'; + connector.style.top = `${midTop}px`; + + const arrowBtn = document.createElement('button'); + arrowBtn.textContent = '🡪'; + arrowBtn.className = 'jp-DiffArrow'; + arrowBtn.title = 'Revert Block'; + + arrowBtn.onclick = () => { + const origText = paneA.state.doc.sliceString(fromA, toA); + paneB.dispatch({ + changes: { from: fromB, to: toB, insert: origText } + }); + this._renderArrowButtons(); + }; + + connector.appendChild(arrowBtn); + this._arrowOverlay.appendChild(connector); }); + } - paneA.dispatch({ - effects: addSplitDiffDeco.of(builder.finish()) - }); + /** + * Keep arrow overlay in sync with editor scroll. + */ + private _addScrollSync(): void { + const paneA = this._splitView.a; + const paneB = this._splitView.b; + const sync = () => this._renderArrowButtons(); + paneA.scrollDOM.addEventListener('scroll', sync); + paneB.scrollDOM.addEventListener('scroll', sync); } private _destroySplitView(): void { @@ -159,35 +148,19 @@ class CodeMirrorSplitDiffWidget extends BaseDiffWidget { this._splitView.destroy(); this._splitView = null!; } + if (this._arrowOverlay) { + this._arrowOverlay.remove(); + } } private _originalCode: string; private _modifiedCode: string; - private _activeChunks = new Set(); - + private _arrowOverlay!: HTMLDivElement; private _splitView!: MergeView & { a: EditorView; b: EditorView; }; } - -const addSplitDiffDeco = StateEffect.define(); - -const splitDiffDecoField = StateField.define({ - create() { - return Decoration.none; - }, - update(deco, tr) { - for (const ef of tr.effects) { - if (ef.is(addSplitDiffDeco)) { - return ef.value; - } - } - return deco.map(tr.changes); - }, - provide: f => EditorView.decorations.from(f) -}); - export async function createCodeMirrorSplitDiffWidget( options: IDiffWidgetOptions ): Promise { diff --git a/style/base.css b/style/base.css index bf62a14..e3cf213 100644 --- a/style/base.css +++ b/style/base.css @@ -78,11 +78,37 @@ border-color: var(--jp-border-color1); } -.jp-DiffMergeArrow { - padding: 2px 6px; - border: none; - background: none; - font-size: 15px; +.jp-DiffArrowOverlay { + position: absolute; + top: 0; + left: 0; + right: 22px; + bottom: 0; + pointer-events: none; + z-index: 3; +} + +.jp-DiffConnectorLine { + position: absolute; + width: 100%; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + pointer-events: none; + z-index: 4; +} + +.jp-DiffArrow { + pointer-events: all; + background: var(--jp-layout-color1); + padding: 0px 6px; cursor: pointer; - margin-left: 4px; + border: none; + font-size: 12px; + transition: background-color 0.15s; +} + +.jp-DiffArrow:hover { + background-color: var(--jp-layout-color3); } From 91d350618bbedaf7b786b42d26ed71b1692ce3bc Mon Sep 17 00:00:00 2001 From: Nakul Date: Mon, 10 Nov 2025 18:22:32 +0530 Subject: [PATCH 6/7] css lint fix --- style/base.css | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/style/base.css b/style/base.css index e3cf213..80b1de8 100644 --- a/style/base.css +++ b/style/base.css @@ -80,10 +80,7 @@ .jp-DiffArrowOverlay { position: absolute; - top: 0; - left: 0; - right: 22px; - bottom: 0; + inset: 0 22px 0 0; pointer-events: none; z-index: 3; } @@ -102,7 +99,7 @@ .jp-DiffArrow { pointer-events: all; background: var(--jp-layout-color1); - padding: 0px 6px; + padding: 0 6px; cursor: pointer; border: none; font-size: 12px; From 90dd67ae258dbcdadaa7d6285d6656685ab3b2d3 Mon Sep 17 00:00:00 2001 From: Nakul Date: Sat, 15 Nov 2025 19:06:28 +0530 Subject: [PATCH 7/7] Clamping values b/w min and max --- src/diff/cell.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/diff/cell.ts b/src/diff/cell.ts index 6cc2b66..f0b84af 100644 --- a/src/diff/cell.ts +++ b/src/diff/cell.ts @@ -120,9 +120,15 @@ class CodeMirrorSplitDiffWidget extends BaseDiffWidget { arrowBtn.title = 'Revert Block'; arrowBtn.onclick = () => { + const docB = paneB.state.doc; + const docLength = docB.length; + + const safeFromB = Math.min(Math.max(0, fromB), docLength); + const safeToB = Math.min(Math.max(safeFromB, toB), docLength); + const origText = paneA.state.doc.sliceString(fromA, toA); paneB.dispatch({ - changes: { from: fromB, to: toB, insert: origText } + changes: { from: safeFromB, to: safeToB, insert: origText } }); this._renderArrowButtons(); };