From 9b1e3c69d99df0a97794cf34d705fe1da9a15f37 Mon Sep 17 00:00:00 2001 From: Nakul Date: Fri, 24 Oct 2025 15:34:10 +0530 Subject: [PATCH 1/7] Adding Accept and Reject All buttons for bulk changes. --- src/diff/base-unified-diff.ts | 11 +++-- src/diff/unified-cell.ts | 23 +++++++--- src/plugin.ts | 85 +++++++++++++++++++++++++++++++++++ style/base.css | 31 +++++++++++++ 4 files changed, 140 insertions(+), 10 deletions(-) diff --git a/src/diff/base-unified-diff.ts b/src/diff/base-unified-diff.ts index aaff741..bc5efd9 100644 --- a/src/diff/base-unified-diff.ts +++ b/src/diff/base-unified-diff.ts @@ -123,17 +123,20 @@ export abstract class BaseUnifiedDiffManager { /** * Accept all changes */ - protected acceptAll(): void { - // simply accept the current state + public acceptAll(): void { + const sharedModel = this.getSharedModel(); + this._originalSource = sharedModel.getSource(); + this._newSource = this._originalSource; this._deactivate(); } /** * Reject all changes */ - protected rejectAll(): void { + public rejectAll(): void { const sharedModel = this.getSharedModel(); sharedModel.setSource(this._originalSource); + this._newSource = this._originalSource; this._deactivate(); } @@ -172,9 +175,9 @@ export abstract class BaseUnifiedDiffManager { protected showActionButtons: boolean; protected acceptAllButton: ToolbarButton | null = null; protected rejectAllButton: ToolbarButton | null = null; - private _originalSource: string; private _newSource: string; private _isInitialized: boolean; private _isDisposed: boolean; private _diffCompartment: Compartment; + public _originalSource: string; } diff --git a/src/diff/unified-cell.ts b/src/diff/unified-cell.ts index b30453e..919d8c3 100644 --- a/src/diff/unified-cell.ts +++ b/src/diff/unified-cell.ts @@ -33,9 +33,17 @@ export class UnifiedCellDiffManager extends BaseUnifiedDiffManager { super(options); this._cell = options.cell; this._cellFooterTracker = options.cellFooterTracker; + this._originalSource = options.originalSource ?? ''; this.activate(); } + /** + * Check if this cell still has pending changes + */ + public hasPendingChanges(): boolean { + return this._originalSource !== this._cell.sharedModel.getSource(); + } + /** * Get the shared model for source manipulation */ @@ -51,6 +59,11 @@ export class UnifiedCellDiffManager extends BaseUnifiedDiffManager { return; } + if (!this.hasPendingChanges()) { + this.removeToolbarButtons(); + return; + } + const cellId = this._cell.id; const footer = this._cellFooterTracker.getFooter(cellId); if (!footer) { @@ -59,19 +72,17 @@ export class UnifiedCellDiffManager extends BaseUnifiedDiffManager { this.acceptAllButton = new ToolbarButton({ icon: checkIcon, - label: this.trans.__('Accept All'), - tooltip: this.trans.__('Accept all chunks'), + label: this.trans.__('Accept'), + tooltip: this.trans.__('Accept changes in this cell'), enabled: true, - className: 'jp-UnifiedDiff-acceptAll', onClick: () => this.acceptAll() }); this.rejectAllButton = new ToolbarButton({ icon: undoIcon, - label: this.trans.__('Reject All'), - tooltip: this.trans.__('Reject all chunks'), + label: this.trans.__('Reject'), + tooltip: this.trans.__('Reject changes in this cell'), enabled: true, - className: 'jp-UnifiedDiff-rejectAll', onClick: () => this.rejectAll() }); diff --git a/src/plugin.ts b/src/plugin.ts index e063030..f198e00 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -19,6 +19,7 @@ import { UnifiedFileDiffManager } from './diff/unified-file'; import { CodeMirrorEditor } from '@jupyterlab/codemirror'; +import { ToolbarButton } from '@jupyterlab/ui-components'; /** * The translation namespace for the plugin. @@ -63,6 +64,29 @@ export function findCell( return cell ?? null; } +/** + * Registry for notebook-level diff managers + */ +const notebookDiffRegistry = new Map(); + +let registerCellManager = ( + notebookId: string, + manager: UnifiedCellDiffManager +): void => { + if (!notebookDiffRegistry.has(notebookId)) { + notebookDiffRegistry.set(notebookId, []); + } + notebookDiffRegistry.get(notebookId)!.push(manager); +}; + +function getNotebookManagers(notebookId: string) { + return notebookDiffRegistry.get(notebookId) || []; +} + +function clearNotebookManagers(notebookId: string) { + notebookDiffRegistry.delete(notebookId); +} + /** * Split cell diff plugin - shows side-by-side comparison */ @@ -283,8 +307,69 @@ const unifiedCellDiffPlugin: JupyterFrontEndPlugin = { trans }); cellDiffManagers.set(cell.id, manager); + + registerCellManager(currentNotebook.id, manager); } }); + notebookTracker.widgetAdded.connect((sender, notebookPanel) => { + const notebookId = notebookPanel.id; + + let acceptAllButton: ToolbarButton | null = null; + let rejectAllButton: ToolbarButton | null = null; + + function updateToolbar() { + const managers = getNotebookManagers(notebookId); + + const anyPending = managers.some(m => m.hasPendingChanges()); + if (!anyPending) { + if (acceptAllButton) { + acceptAllButton.dispose(); + } + if (rejectAllButton) { + rejectAllButton.dispose(); + } + acceptAllButton = null; + rejectAllButton = null; + return; + } + + if (!acceptAllButton) { + acceptAllButton = new ToolbarButton({ + label: trans.__('Accept All'), + className: 'accept-all-changes', + tooltip: trans.__('Accept all changes in this notebook'), + onClick: () => { + getNotebookManagers(notebookId).forEach(m => m.acceptAll()); + updateToolbar(); + } + }); + notebookPanel.toolbar.addItem('accept-all-changes', acceptAllButton); + } + + if (!rejectAllButton) { + rejectAllButton = new ToolbarButton({ + label: trans.__('Reject All'), + className: 'reject-all-changes', + tooltip: trans.__('Reject all changes in this notebook'), + onClick: () => { + getNotebookManagers(notebookId).forEach(m => m.rejectAll()); + updateToolbar(); + } + }); + notebookPanel.toolbar.addItem('reject-all-changes', rejectAllButton); + } + } + + notebookPanel.disposed.connect(() => clearNotebookManagers(notebookId)); + notebookPanel.node.addEventListener('diff-updated', updateToolbar); + + const originalRegister = registerCellManager; + registerCellManager = (nid: string, manager: UnifiedCellDiffManager) => { + originalRegister(nid, manager); + const event = new Event('diff-updated'); + notebookPanel.node.dispatchEvent(event); + }; + }); } }; diff --git a/style/base.css b/style/base.css index 82fd63e..8165b88 100644 --- a/style/base.css +++ b/style/base.css @@ -77,3 +77,34 @@ background-color: var(--jp-layout-color3); border-color: var(--jp-border-color1); } + +.jp-ToolbarButtonComponent.accept-all-changes { + background-color: #28a745; + cursor: pointer; + padding: 2px 8px; + border-radius: 4px; + font-size: 12px; + transition: + background-color 0.2s, + border-color 0.2s; +} + +.jp-ToolbarButtonComponent.accept-all-changes:hover { + background-color: #058522; +} + +.jp-ToolbarButtonComponent.reject-all-changes { + background-color: #dc3545; + cursor: pointer; + padding: 2px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: bold; + transition: + background-color 0.2s, + border-color 0.2s; +} + +.jp-ToolbarButtonComponent.reject-all-changes:hover { + background-color: #c82333; +} From b6e3fad8e951148e0dc2ed98157084f7b0b3f14f Mon Sep 17 00:00:00 2001 From: Nakul Date: Sat, 25 Oct 2025 22:21:32 +0530 Subject: [PATCH 2/7] Moving buttons notebook toolbar to floating state. --- src/plugin.ts | 81 +++++++++++++++++++++++++++----------------------- style/base.css | 58 ++++++++++++++++++++++-------------- 2 files changed, 80 insertions(+), 59 deletions(-) diff --git a/src/plugin.ts b/src/plugin.ts index f198e00..dd89272 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -19,7 +19,6 @@ import { UnifiedFileDiffManager } from './diff/unified-file'; import { CodeMirrorEditor } from '@jupyterlab/codemirror'; -import { ToolbarButton } from '@jupyterlab/ui-components'; /** * The translation namespace for the plugin. @@ -314,54 +313,62 @@ const unifiedCellDiffPlugin: JupyterFrontEndPlugin = { notebookTracker.widgetAdded.connect((sender, notebookPanel) => { const notebookId = notebookPanel.id; - let acceptAllButton: ToolbarButton | null = null; - let rejectAllButton: ToolbarButton | null = null; + let floatingPanel: HTMLElement | null = null; - function updateToolbar() { - const managers = getNotebookManagers(notebookId); + function createFloatingPanel(): HTMLElement { + const panel = document.createElement('div'); + panel.classList.add('jp-unified-diff-floating-panel'); + + const acceptButton = document.createElement('button'); + acceptButton.classList.add('jp-merge-accept-button'); + acceptButton.textContent = 'Accept All'; + acceptButton.title = trans.__('Accept all changes in this notebook'); + acceptButton.onclick = () => { + getNotebookManagers(notebookId).forEach(m => m.acceptAll()); + updateFloatingPanel(); + }; + + const rejectButton = document.createElement('button'); + rejectButton.classList.add('jp-merge-reject-button'); + rejectButton.textContent = 'Reject All'; + rejectButton.title = trans.__('Reject all changes in this notebook'); + rejectButton.onclick = () => { + getNotebookManagers(notebookId).forEach(m => m.rejectAll()); + updateFloatingPanel(); + }; + + panel.appendChild(acceptButton); + panel.appendChild(rejectButton); + return panel; + } + function updateFloatingPanel(): void { + const managers = getNotebookManagers(notebookId); const anyPending = managers.some(m => m.hasPendingChanges()); + if (!anyPending) { - if (acceptAllButton) { - acceptAllButton.dispose(); - } - if (rejectAllButton) { - rejectAllButton.dispose(); + if (floatingPanel && floatingPanel.parentElement) { + floatingPanel.parentElement.removeChild(floatingPanel); } - acceptAllButton = null; - rejectAllButton = null; + floatingPanel = null; return; } - if (!acceptAllButton) { - acceptAllButton = new ToolbarButton({ - label: trans.__('Accept All'), - className: 'accept-all-changes', - tooltip: trans.__('Accept all changes in this notebook'), - onClick: () => { - getNotebookManagers(notebookId).forEach(m => m.acceptAll()); - updateToolbar(); - } - }); - notebookPanel.toolbar.addItem('accept-all-changes', acceptAllButton); + if (!floatingPanel) { + floatingPanel = createFloatingPanel(); + notebookPanel.node.appendChild(floatingPanel); } + } - if (!rejectAllButton) { - rejectAllButton = new ToolbarButton({ - label: trans.__('Reject All'), - className: 'reject-all-changes', - tooltip: trans.__('Reject all changes in this notebook'), - onClick: () => { - getNotebookManagers(notebookId).forEach(m => m.rejectAll()); - updateToolbar(); - } - }); - notebookPanel.toolbar.addItem('reject-all-changes', rejectAllButton); + notebookPanel.disposed.connect(() => { + clearNotebookManagers(notebookId); + if (floatingPanel && floatingPanel.parentElement) { + floatingPanel.parentElement.removeChild(floatingPanel); } - } + floatingPanel = null; + }); - notebookPanel.disposed.connect(() => clearNotebookManagers(notebookId)); - notebookPanel.node.addEventListener('diff-updated', updateToolbar); + notebookPanel.node.addEventListener('diff-updated', updateFloatingPanel); const originalRegister = registerCellManager; registerCellManager = (nid: string, manager: UnifiedCellDiffManager) => { diff --git a/style/base.css b/style/base.css index 8165b88..99b1688 100644 --- a/style/base.css +++ b/style/base.css @@ -78,33 +78,47 @@ border-color: var(--jp-border-color1); } -.jp-ToolbarButtonComponent.accept-all-changes { - background-color: #28a745; - cursor: pointer; - padding: 2px 8px; +/* Floating unified-cell-diff control panel */ +.jp-unified-diff-floating-panel { + position: absolute; + bottom: 16px; + right: 16px; + display: flex; + gap: 12px; + background: var(--jp-layout-color1); + border: 1px solid var(--jp-border-color2); + border-radius: 8px; + padding: 8px 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25); + z-index: 200; + opacity: 0.95; + transition: opacity 0.2s ease-in-out; +} + +.jp-unified-diff-floating-panel .jp-merge-accept-button, +.jp-unified-diff-floating-panel .jp-merge-reject-button { + padding: 12px 10px; + font-size: 13px; border-radius: 4px; - font-size: 12px; - transition: - background-color 0.2s, - border-color 0.2s; + cursor: pointer; + color: white; + font-weight: inherit; + border: none; + transition: background-color 0.2s ease-in-out; } -.jp-ToolbarButtonComponent.accept-all-changes:hover { - background-color: #058522; +.jp-unified-diff-floating-panel .jp-merge-accept-button { + background-color: #089e03; } -.jp-ToolbarButtonComponent.reject-all-changes { - background-color: #dc3545; - cursor: pointer; - padding: 2px 8px; - border-radius: 4px; - font-size: 12px; - font-weight: bold; - transition: - background-color 0.2s, - border-color 0.2s; +.jp-unified-diff-floating-panel .jp-merge-accept-button:hover { + background-color: #027e1d; +} + +.jp-unified-diff-floating-panel .jp-merge-reject-button { + background-color: #b80606; } -.jp-ToolbarButtonComponent.reject-all-changes:hover { - background-color: #c82333; +.jp-unified-diff-floating-panel .jp-merge-reject-button:hover { + background-color: #8f0505; } From c34a1113bc16fda421d234fb560ad6183f39014a Mon Sep 17 00:00:00 2001 From: Nakul Date: Sat, 25 Oct 2025 22:29:08 +0530 Subject: [PATCH 3/7] lint fix --- style/base.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/style/base.css b/style/base.css index 99b1688..e8a9fc2 100644 --- a/style/base.css +++ b/style/base.css @@ -89,7 +89,7 @@ border: 1px solid var(--jp-border-color2); border-radius: 8px; padding: 8px 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25); + box-shadow: 0 2px 8px #00000040; z-index: 200; opacity: 0.95; transition: opacity 0.2s ease-in-out; From 904b7706139c6bfc22203107ea25c77579c5f6a1 Mon Sep 17 00:00:00 2001 From: Nakul Date: Sun, 26 Oct 2025 14:31:13 +0530 Subject: [PATCH 4/7] using jupyterlab variables for colors to get consistent behavior with other UI. --- style/base.css | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/style/base.css b/style/base.css index e8a9fc2..65a6e3b 100644 --- a/style/base.css +++ b/style/base.css @@ -87,7 +87,7 @@ gap: 12px; background: var(--jp-layout-color1); border: 1px solid var(--jp-border-color2); - border-radius: 8px; + border-radius: 4px; padding: 8px 12px; box-shadow: 0 2px 8px #00000040; z-index: 200; @@ -101,24 +101,25 @@ font-size: 13px; border-radius: 4px; cursor: pointer; - color: white; font-weight: inherit; border: none; transition: background-color 0.2s ease-in-out; } .jp-unified-diff-floating-panel .jp-merge-accept-button { - background-color: #089e03; + background-color: var(--jp-success-color1); + color: var(--jp-ui-inverse-font-color0); } .jp-unified-diff-floating-panel .jp-merge-accept-button:hover { - background-color: #027e1d; + background-color: var(--jp-success-color0); } .jp-unified-diff-floating-panel .jp-merge-reject-button { - background-color: #b80606; + background-color: var(--jp-error-color1); + color: var(--jp-ui-inverse-font-color0); } .jp-unified-diff-floating-panel .jp-merge-reject-button:hover { - background-color: #8f0505; + background-color: var(--jp-error-color0); } From 098fdcfc4b470020b44777b8eb3a4f6ab71533d7 Mon Sep 17 00:00:00 2001 From: Nakul Date: Sun, 26 Oct 2025 19:51:23 +0530 Subject: [PATCH 5/7] buttons text color and edge radius changed --- style/base.css | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/style/base.css b/style/base.css index 65a6e3b..9ba95f1 100644 --- a/style/base.css +++ b/style/base.css @@ -87,7 +87,6 @@ gap: 12px; background: var(--jp-layout-color1); border: 1px solid var(--jp-border-color2); - border-radius: 4px; padding: 8px 12px; box-shadow: 0 2px 8px #00000040; z-index: 200; @@ -99,8 +98,9 @@ .jp-unified-diff-floating-panel .jp-merge-reject-button { padding: 12px 10px; font-size: 13px; - border-radius: 4px; + border-radius: 2px; cursor: pointer; + color: white; font-weight: inherit; border: none; transition: background-color 0.2s ease-in-out; @@ -108,7 +108,6 @@ .jp-unified-diff-floating-panel .jp-merge-accept-button { background-color: var(--jp-success-color1); - color: var(--jp-ui-inverse-font-color0); } .jp-unified-diff-floating-panel .jp-merge-accept-button:hover { @@ -117,7 +116,6 @@ .jp-unified-diff-floating-panel .jp-merge-reject-button { background-color: var(--jp-error-color1); - color: var(--jp-ui-inverse-font-color0); } .jp-unified-diff-floating-panel .jp-merge-reject-button:hover { From 63eee78e2cbdf7d313ebe562e1e965f627dc4bbc Mon Sep 17 00:00:00 2001 From: Nakul Date: Mon, 27 Oct 2025 15:54:31 +0530 Subject: [PATCH 6/7] build fix --- src/diff/unified-cell.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/diff/unified-cell.ts b/src/diff/unified-cell.ts index 3a92c8d..1015e8c 100644 --- a/src/diff/unified-cell.ts +++ b/src/diff/unified-cell.ts @@ -43,7 +43,7 @@ export class UnifiedCellDiffManager extends BaseUnifiedDiffManager { * Check if this cell still has pending changes */ public hasPendingChanges(): boolean { - return this._originalSource !== this._cell.sharedModel.getSource(); + return this._originalSource !== this._cell.model.sharedModel.getSource(); } /** @@ -125,7 +125,7 @@ export class UnifiedCellDiffManager extends BaseUnifiedDiffManager { return; } - const cellId = this._cell.id; + const cellId = this._cell.model.id; const footer = this._cellFooterTracker.getFooter(cellId); if (!footer) { return; @@ -166,7 +166,7 @@ export class UnifiedCellDiffManager extends BaseUnifiedDiffManager { return; } - const cellId = this._cell.id; + const cellId = this._cell.model.id; const footer = this._cellFooterTracker.getFooter(cellId); if (!footer) { return; From 14f0d3a61591d643b1f92fc03f333b99151b13f2 Mon Sep 17 00:00:00 2001 From: Nakul Date: Wed, 29 Oct 2025 08:05:06 +0530 Subject: [PATCH 7/7] making acceptall() public --- src/diff/base-unified-diff.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/diff/base-unified-diff.ts b/src/diff/base-unified-diff.ts index afa9fd5..6bbfe1c 100644 --- a/src/diff/base-unified-diff.ts +++ b/src/diff/base-unified-diff.ts @@ -135,7 +135,7 @@ export abstract class BaseUnifiedDiffManager { /** * Accept all changes */ - protected acceptAll(): void { + public acceptAll(): void { // simply accept the current state this.deactivate(); }