diff --git a/src/diff/base-unified-diff.ts b/src/diff/base-unified-diff.ts index cd899a6..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(); } @@ -143,7 +143,7 @@ export abstract class BaseUnifiedDiffManager { /** * Reject all changes */ - protected rejectAll(): void { + public rejectAll(): void { const sharedModel = this.getSharedModel(); sharedModel.setSource(this._originalSource); this.deactivate(); @@ -184,9 +184,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 b98cc2a..b43d3ac 100644 --- a/src/diff/unified-cell.ts +++ b/src/diff/unified-cell.ts @@ -33,12 +33,20 @@ export class UnifiedCellDiffManager extends BaseUnifiedDiffManager { super(options); this._cell = options.cell; this._cellFooterTracker = options.cellFooterTracker; + this._originalSource = options.originalSource ?? ''; this.activate(); } private static _activeDiffCount = 0; private _toolbarObserver?: MutationObserver; + /** + * Check if this cell still has pending changes + */ + public hasPendingChanges(): boolean { + return this._originalSource !== this._cell.model.sharedModel.getSource(); + } + /** * Get the shared model for source manipulation */ @@ -116,7 +124,12 @@ export class UnifiedCellDiffManager extends BaseUnifiedDiffManager { return; } - const cellId = this._cell.id; + if (!this.hasPendingChanges()) { + this.removeToolbarButtons(); + return; + } + + const cellId = this._cell.model.id; const footer = this._cellFooterTracker.getFooter(cellId); if (!footer) { return; @@ -124,19 +137,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() }); @@ -159,7 +170,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; diff --git a/src/plugin.ts b/src/plugin.ts index cc731c8..1d5df52 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -63,6 +63,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,7 +306,76 @@ const unifiedCellDiffPlugin: JupyterFrontEndPlugin = { trans }); cellDiffManagers.set(cell.id, manager); + + registerCellManager(currentNotebook.id, manager); + } + }); + notebookTracker.widgetAdded.connect((sender, notebookPanel) => { + const notebookId = notebookPanel.id; + + let floatingPanel: HTMLElement | null = null; + + 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 (floatingPanel && floatingPanel.parentElement) { + floatingPanel.parentElement.removeChild(floatingPanel); + } + floatingPanel = null; + return; + } + + if (!floatingPanel) { + floatingPanel = createFloatingPanel(); + notebookPanel.node.appendChild(floatingPanel); + } } + + notebookPanel.disposed.connect(() => { + clearNotebookManagers(notebookId); + if (floatingPanel && floatingPanel.parentElement) { + floatingPanel.parentElement.removeChild(floatingPanel); + } + floatingPanel = null; + }); + + notebookPanel.node.addEventListener('diff-updated', updateFloatingPanel); + + 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..9ba95f1 100644 --- a/style/base.css +++ b/style/base.css @@ -77,3 +77,47 @@ background-color: var(--jp-layout-color3); border-color: var(--jp-border-color1); } + +/* 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); + padding: 8px 12px; + box-shadow: 0 2px 8px #00000040; + 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: 2px; + 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: var(--jp-success-color1); +} + +.jp-unified-diff-floating-panel .jp-merge-accept-button:hover { + background-color: var(--jp-success-color0); +} + +.jp-unified-diff-floating-panel .jp-merge-reject-button { + background-color: var(--jp-error-color1); +} + +.jp-unified-diff-floating-panel .jp-merge-reject-button:hover { + background-color: var(--jp-error-color0); +}