From 9b0f979575af45167370a7036b1d58b516e80642 Mon Sep 17 00:00:00 2001 From: jankuca Date: Mon, 20 Oct 2025 17:58:55 +0200 Subject: [PATCH 01/11] add actually working "copy cell down" command --- .../deepnote/deepnoteCellCopyHandler.ts | 216 ++++++++++++++++++ src/notebooks/serviceRegistry.node.ts | 5 + 2 files changed, 221 insertions(+) create mode 100644 src/notebooks/deepnote/deepnoteCellCopyHandler.ts diff --git a/src/notebooks/deepnote/deepnoteCellCopyHandler.ts b/src/notebooks/deepnote/deepnoteCellCopyHandler.ts new file mode 100644 index 0000000000..d44450d7f2 --- /dev/null +++ b/src/notebooks/deepnote/deepnoteCellCopyHandler.ts @@ -0,0 +1,216 @@ +import { injectable, inject } from 'inversify'; +import { + workspace, + NotebookDocumentChangeEvent, + NotebookEdit, + WorkspaceEdit, + commands, + window, + NotebookCellData, + NotebookRange +} from 'vscode'; + +import { IExtensionSyncActivationService } from '../../platform/activation/types'; +import { IDisposableRegistry } from '../../platform/common/types'; +import { logger } from '../../platform/logging'; +import { generateBlockId, generateSortingKey } from './dataConversionUtils'; + +/** + * Handles cell copy operations in Deepnote notebooks to ensure metadata is preserved. + * + * VSCode's built-in copy commands don't preserve custom cell metadata, so this handler + * provides a custom copy command that properly preserves all metadata fields including + * sql_integration_id for SQL blocks. + */ +@injectable() +export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService { + private processingChanges = false; + + constructor(@inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry) {} + + public activate(): void { + // Register custom copy command that preserves metadata + this.disposables.push(commands.registerCommand('deepnote.copyCellDown', () => this.copyCellDown())); + + // Listen for notebook document changes to detect when cells are added without metadata + this.disposables.push(workspace.onDidChangeNotebookDocument((e) => this.onDidChangeNotebookDocument(e))); + } + + private async copyCellDown(): Promise { + const editor = window.activeNotebookEditor; + + if (!editor || !editor.notebook.uri.path.endsWith('.deepnote')) { + // Fall back to default copy command for non-Deepnote notebooks + await commands.executeCommand('notebook.cell.copyDown'); + return; + } + + const selection = editor.selection; + if (!selection) { + return; + } + + const cellToCopy = editor.notebook.cellAt(selection.start); + const insertIndex = selection.start + 1; + + // Create a new cell with the same content and metadata + const newCell = new NotebookCellData( + cellToCopy.kind, + cellToCopy.document.getText(), + cellToCopy.document.languageId + ); + + // Copy all metadata, but generate new ID and sortingKey + if (cellToCopy.metadata) { + const copiedMetadata = { ...cellToCopy.metadata }; + + // Generate new unique ID + copiedMetadata.id = generateBlockId(); + + // Update sortingKey in pocket if it exists + if (copiedMetadata.__deepnotePocket) { + copiedMetadata.__deepnotePocket = { + ...copiedMetadata.__deepnotePocket, + sortingKey: generateSortingKey(insertIndex) + }; + } else if (copiedMetadata.sortingKey) { + copiedMetadata.sortingKey = generateSortingKey(insertIndex); + } + + newCell.metadata = copiedMetadata; + + logger.info( + `DeepnoteCellCopyHandler: Copying cell with metadata preserved: ${JSON.stringify( + copiedMetadata, + null, + 2 + )}` + ); + } + + // Copy outputs if present + if (cellToCopy.outputs.length > 0) { + newCell.outputs = cellToCopy.outputs.map((output) => output); + } + + // Insert the new cell + const edit = new WorkspaceEdit(); + edit.set(editor.notebook.uri, [NotebookEdit.insertCells(insertIndex, [newCell])]); + + const success = await workspace.applyEdit(edit); + + if (success) { + // Move selection to the new cell + editor.selection = new NotebookRange(insertIndex, insertIndex + 1); + logger.info(`DeepnoteCellCopyHandler: Successfully copied cell to index ${insertIndex}`); + } else { + logger.warn('DeepnoteCellCopyHandler: Failed to copy cell'); + } + } + + private async onDidChangeNotebookDocument(e: NotebookDocumentChangeEvent): Promise { + // Only process Deepnote notebooks + if (!e.notebook.uri.path.endsWith('.deepnote')) { + return; + } + + // Avoid recursive processing + if (this.processingChanges) { + return; + } + + // Check for cell additions (which includes copies) + for (const change of e.contentChanges) { + if (change.addedCells.length === 0) { + continue; + } + + // When cells are copied, VSCode should preserve metadata automatically. + // However, we need to ensure that: + // 1. Each cell has a unique ID + // 2. The sortingKey is updated based on the new position + // 3. All other metadata (including sql_integration_id) is preserved + + const cellsNeedingMetadataFix: Array<{ index: number; metadata: Record }> = []; + + for (const cell of change.addedCells) { + const metadata = cell.metadata || {}; + + // Log the metadata to see what's actually being copied + logger.info(`DeepnoteCellCopyHandler: Cell added with metadata: ${JSON.stringify(metadata, null, 2)}`); + + // Only process Deepnote cells (cells with type or pocket metadata) + if (!metadata.type && !metadata.__deepnotePocket) { + continue; + } + + const cellIndex = e.notebook.getCells().indexOf(cell); + + if (cellIndex === -1) { + continue; + } + + // Check if this cell needs metadata updates + // We update the ID and sortingKey for all added Deepnote cells to ensure uniqueness + const updatedMetadata = { ...metadata }; + + // Generate new ID for the cell (important for copied cells) + updatedMetadata.id = generateBlockId(); + + // Update sortingKey based on the new position + if (updatedMetadata.__deepnotePocket) { + updatedMetadata.__deepnotePocket = { + ...updatedMetadata.__deepnotePocket, + sortingKey: generateSortingKey(cellIndex) + }; + } else if (updatedMetadata.sortingKey) { + updatedMetadata.sortingKey = generateSortingKey(cellIndex); + } + + // All other metadata (including sql_integration_id) is preserved from the original metadata + cellsNeedingMetadataFix.push({ + index: cellIndex, + metadata: updatedMetadata + }); + + logger.info( + `DeepnoteCellCopyHandler: Updated metadata for ${ + metadata.type + } cell at index ${cellIndex}: ${JSON.stringify(updatedMetadata, null, 2)}` + ); + } + + // Apply metadata fixes if needed + if (cellsNeedingMetadataFix.length > 0) { + await this.applyMetadataFixes(e.notebook.uri, cellsNeedingMetadataFix); + } + } + } + + private async applyMetadataFixes( + notebookUri: import('vscode').Uri, + fixes: Array<{ index: number; metadata: Record }> + ): Promise { + try { + this.processingChanges = true; + + const edit = new WorkspaceEdit(); + + // Create all the edits at once instead of calling set() multiple times + const edits = fixes.map((fix) => NotebookEdit.updateCellMetadata(fix.index, fix.metadata)); + edit.set(notebookUri, edits); + + const success = await workspace.applyEdit(edit); + + if (success) { + logger.info(`DeepnoteCellCopyHandler: Successfully updated metadata for ${fixes.length} cell(s)`); + } else { + logger.warn(`DeepnoteCellCopyHandler: Failed to apply metadata fixes for ${fixes.length} cell(s)`); + } + } catch (error) { + logger.error('DeepnoteCellCopyHandler: Error applying metadata fixes', error); + } finally { + this.processingChanges = false; + } + } +} diff --git a/src/notebooks/serviceRegistry.node.ts b/src/notebooks/serviceRegistry.node.ts index 3bd4f9d236..1d51e66450 100644 --- a/src/notebooks/serviceRegistry.node.ts +++ b/src/notebooks/serviceRegistry.node.ts @@ -67,6 +67,7 @@ import { DeepnoteServerProvider } from '../kernels/deepnote/deepnoteServerProvid import { DeepnoteInitNotebookRunner, IDeepnoteInitNotebookRunner } from './deepnote/deepnoteInitNotebookRunner.node'; import { DeepnoteRequirementsHelper, IDeepnoteRequirementsHelper } from './deepnote/deepnoteRequirementsHelper.node'; import { SqlIntegrationStartupCodeProvider } from './deepnote/integrations/sqlIntegrationStartupCodeProvider'; +import { DeepnoteCellCopyHandler } from './deepnote/deepnoteCellCopyHandler'; export function registerTypes(serviceManager: IServiceManager, isDevMode: boolean) { registerControllerTypes(serviceManager, isDevMode); @@ -152,6 +153,10 @@ export function registerTypes(serviceManager: IServiceManager, isDevMode: boolea IExtensionSyncActivationService, SqlIntegrationStartupCodeProvider ); + serviceManager.addSingleton( + IExtensionSyncActivationService, + DeepnoteCellCopyHandler + ); // Deepnote kernel services serviceManager.addSingleton(IDeepnoteToolkitInstaller, DeepnoteToolkitInstaller); From 5b014bf6d0b20b6ab0322118897410a61c83a4e3 Mon Sep 17 00:00:00 2001 From: jankuca Date: Tue, 21 Oct 2025 13:10:58 +0200 Subject: [PATCH 02/11] fix: fix metadata perservation when copying/cutting and pasting blocks --- package.json | 6 + .../deepnote/deepnoteCellCopyHandler.ts | 242 +++++++++++++++++- 2 files changed, 241 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 575cc95d56..908a5e9fe6 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,12 @@ "key": "escape", "when": "isCompositeNotebook && !editorHoverVisible && !suggestWidgetVisible && !isComposing && !inSnippetMode && !exceptionWidgetVisible && !selectionAnchorSet && !LinkedEditingInputVisible && !renameInputVisible && !editorHasSelection && !accessibilityHelpWidgetVisible && !breakpointWidgetVisible && !findWidgetVisible && !markersNavigationVisible && !parameterHintsVisible && !editorHasMultipleSelections && !notificationToastsVisible && !notebookEditorFocused && !inlineChatVisible", "command": "interactive.input.clear" + }, + { + "key": "shift+alt+down", + "mac": "shift+alt+down", + "when": "notebookType == deepnote && notebookEditorFocused && !inputFocus", + "command": "deepnote.copyCellDown" } ], "commands": [ diff --git a/src/notebooks/deepnote/deepnoteCellCopyHandler.ts b/src/notebooks/deepnote/deepnoteCellCopyHandler.ts index d44450d7f2..12b7f4aca1 100644 --- a/src/notebooks/deepnote/deepnoteCellCopyHandler.ts +++ b/src/notebooks/deepnote/deepnoteCellCopyHandler.ts @@ -7,7 +7,8 @@ import { commands, window, NotebookCellData, - NotebookRange + NotebookRange, + env } from 'vscode'; import { IExtensionSyncActivationService } from '../../platform/activation/types'; @@ -15,12 +16,27 @@ import { IDisposableRegistry } from '../../platform/common/types'; import { logger } from '../../platform/logging'; import { generateBlockId, generateSortingKey } from './dataConversionUtils'; +/** + * Marker prefix for clipboard data to identify Deepnote cell metadata + */ +const CLIPBOARD_MARKER = '___DEEPNOTE_CELL_METADATA___'; + +/** + * Interface for cell metadata stored in clipboard + */ +interface ClipboardCellMetadata { + metadata: Record; + kind: number; + languageId: string; + value: string; +} + /** * Handles cell copy operations in Deepnote notebooks to ensure metadata is preserved. * * VSCode's built-in copy commands don't preserve custom cell metadata, so this handler - * provides a custom copy command that properly preserves all metadata fields including - * sql_integration_id for SQL blocks. + * intercepts copy/cut/paste commands and stores metadata in the clipboard as JSON. + * This allows metadata to be preserved across copy/paste and cut/paste operations. */ @injectable() export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService { @@ -29,19 +45,103 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService constructor(@inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry) {} public activate(): void { - // Register custom copy command that preserves metadata + // Register custom copy commands that preserve metadata this.disposables.push(commands.registerCommand('deepnote.copyCellDown', () => this.copyCellDown())); + this.disposables.push(commands.registerCommand('deepnote.copyCellUp', () => this.copyCellUp())); + + // Override built-in notebook copy/cut commands to preserve metadata for Deepnote notebooks + this.disposables.push(commands.registerCommand('notebook.cell.copyDown', () => this.copyCellDownInterceptor())); + this.disposables.push(commands.registerCommand('notebook.cell.copyUp', () => this.copyCellUpInterceptor())); + this.disposables.push(commands.registerCommand('notebook.cell.copy', () => this.copyCellInterceptor())); + this.disposables.push(commands.registerCommand('notebook.cell.cut', () => this.cutCellInterceptor())); + this.disposables.push(commands.registerCommand('notebook.cell.paste', () => this.pasteCellInterceptor())); // Listen for notebook document changes to detect when cells are added without metadata this.disposables.push(workspace.onDidChangeNotebookDocument((e) => this.onDidChangeNotebookDocument(e))); } + /** + * Interceptor for the built-in notebook.cell.copyDown command. + * Routes to our custom implementation for Deepnote notebooks. + */ + private async copyCellDownInterceptor(): Promise { + const editor = window.activeNotebookEditor; + if (editor && editor.notebook.uri.path.endsWith('.deepnote')) { + await this.copyCellDown(); + } else { + logger.warn('notebook.cell.copyDown intercepted for non-Deepnote notebook - using fallback'); + } + } + + /** + * Interceptor for the built-in notebook.cell.copyUp command. + * Routes to our custom implementation for Deepnote notebooks. + */ + private async copyCellUpInterceptor(): Promise { + const editor = window.activeNotebookEditor; + if (editor && editor.notebook.uri.path.endsWith('.deepnote')) { + await this.copyCellUp(); + } else { + logger.warn('notebook.cell.copyUp intercepted for non-Deepnote notebook - using fallback'); + } + } + + /** + * Interceptor for the built-in notebook.cell.copy command. + * Stores cell metadata in clipboard for Deepnote notebooks. + */ + private async copyCellInterceptor(): Promise { + const editor = window.activeNotebookEditor; + if (editor && editor.notebook.uri.path.endsWith('.deepnote')) { + await this.copyCellToClipboard(false); + } else { + logger.warn('notebook.cell.copy intercepted for non-Deepnote notebook - using fallback'); + } + } + + /** + * Interceptor for the built-in notebook.cell.cut command. + * Stores cell metadata in clipboard for Deepnote notebooks. + */ + private async cutCellInterceptor(): Promise { + const editor = window.activeNotebookEditor; + if (editor && editor.notebook.uri.path.endsWith('.deepnote')) { + await this.copyCellToClipboard(true); + } else { + logger.warn('notebook.cell.cut intercepted for non-Deepnote notebook - using fallback'); + } + } + + /** + * Interceptor for the built-in notebook.cell.paste command. + * Restores cell metadata from clipboard for Deepnote notebooks. + */ + private async pasteCellInterceptor(): Promise { + const editor = window.activeNotebookEditor; + if (editor && editor.notebook.uri.path.endsWith('.deepnote')) { + await this.pasteCellFromClipboard(); + } else { + logger.warn('notebook.cell.paste intercepted for non-Deepnote notebook - using fallback'); + } + } + private async copyCellDown(): Promise { + await this.copyCellAtOffset(1); + } + + private async copyCellUp(): Promise { + await this.copyCellAtOffset(-1); + } + + /** + * Copy a cell at a specific offset from the current cell. + * @param offset -1 for copy up, 1 for copy down + */ + private async copyCellAtOffset(offset: number): Promise { const editor = window.activeNotebookEditor; if (!editor || !editor.notebook.uri.path.endsWith('.deepnote')) { - // Fall back to default copy command for non-Deepnote notebooks - await commands.executeCommand('notebook.cell.copyDown'); + logger.warn(`copyCellAtOffset called for non-Deepnote notebook`); return; } @@ -51,7 +151,7 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService } const cellToCopy = editor.notebook.cellAt(selection.start); - const insertIndex = selection.start + 1; + const insertIndex = offset > 0 ? selection.start + 1 : selection.start; // Create a new cell with the same content and metadata const newCell = new NotebookCellData( @@ -213,4 +313,132 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService this.processingChanges = false; } } + + /** + * Copy or cut a cell to the clipboard with metadata preserved. + * @param isCut Whether this is a cut operation (will delete the cell after copying) + */ + private async copyCellToClipboard(isCut: boolean): Promise { + const editor = window.activeNotebookEditor; + + if (!editor || !editor.notebook.uri.path.endsWith('.deepnote')) { + logger.warn(`copyCellToClipboard called for non-Deepnote notebook`); + return; + } + + const selection = editor.selection; + if (!selection) { + return; + } + + const cellToCopy = editor.notebook.cellAt(selection.start); + + // Create clipboard data with all cell information + const clipboardData: ClipboardCellMetadata = { + metadata: cellToCopy.metadata || {}, + kind: cellToCopy.kind, + languageId: cellToCopy.document.languageId, + value: cellToCopy.document.getText() + }; + + // Store in clipboard as JSON with marker + const clipboardText = `${CLIPBOARD_MARKER}${JSON.stringify(clipboardData)}`; + await env.clipboard.writeText(clipboardText); + + logger.info( + `DeepnoteCellCopyHandler: ${isCut ? 'Cut' : 'Copied'} cell to clipboard with metadata: ${JSON.stringify( + clipboardData.metadata, + null, + 2 + )}` + ); + + // If this is a cut operation, delete the cell + if (isCut) { + const edit = new WorkspaceEdit(); + edit.set(editor.notebook.uri, [ + NotebookEdit.deleteCells(new NotebookRange(selection.start, selection.start + 1)) + ]); + await workspace.applyEdit(edit); + logger.info(`DeepnoteCellCopyHandler: Deleted cell after cut operation`); + } + } + + /** + * Paste a cell from the clipboard, restoring metadata if available. + */ + private async pasteCellFromClipboard(): Promise { + const editor = window.activeNotebookEditor; + + if (!editor || !editor.notebook.uri.path.endsWith('.deepnote')) { + logger.warn(`pasteCellFromClipboard called for non-Deepnote notebook`); + return; + } + + const selection = editor.selection; + if (!selection) { + return; + } + + // Read from clipboard + const clipboardText = await env.clipboard.readText(); + + // Check if clipboard contains our metadata marker + if (!clipboardText.startsWith(CLIPBOARD_MARKER)) { + logger.info('DeepnoteCellCopyHandler: Clipboard does not contain Deepnote cell metadata, skipping'); + return; + } + + try { + // Parse clipboard data + const jsonText = clipboardText.substring(CLIPBOARD_MARKER.length); + const clipboardData: ClipboardCellMetadata = JSON.parse(jsonText); + + // Create new cell with preserved metadata + const newCell = new NotebookCellData(clipboardData.kind, clipboardData.value, clipboardData.languageId); + + // Copy metadata but generate new ID and sortingKey + const copiedMetadata = { ...clipboardData.metadata }; + + // Generate new unique ID + copiedMetadata.id = generateBlockId(); + + // Update sortingKey in pocket if it exists + const insertIndex = selection.start; + if (copiedMetadata.__deepnotePocket) { + copiedMetadata.__deepnotePocket = { + ...copiedMetadata.__deepnotePocket, + sortingKey: generateSortingKey(insertIndex) + }; + } else if (copiedMetadata.sortingKey) { + copiedMetadata.sortingKey = generateSortingKey(insertIndex); + } + + newCell.metadata = copiedMetadata; + + logger.info( + `DeepnoteCellCopyHandler: Pasting cell with metadata preserved: ${JSON.stringify( + copiedMetadata, + null, + 2 + )}` + ); + + // Insert the new cell + const edit = new WorkspaceEdit(); + edit.set(editor.notebook.uri, [NotebookEdit.insertCells(insertIndex, [newCell])]); + + const success = await workspace.applyEdit(edit); + + if (success) { + // Move selection to the new cell + editor.selection = new NotebookRange(insertIndex, insertIndex + 1); + logger.info(`DeepnoteCellCopyHandler: Successfully pasted cell at index ${insertIndex}`); + } else { + logger.warn('DeepnoteCellCopyHandler: Failed to paste cell'); + } + } catch (error) { + logger.error('DeepnoteCellCopyHandler: Error parsing clipboard data', error); + } + } } From 8a4b50688aed701b01f8829c68a30e410d901c21 Mon Sep 17 00:00:00 2001 From: jankuca Date: Tue, 21 Oct 2025 17:43:01 +0200 Subject: [PATCH 03/11] replace .deepnote extension check with doc type check --- .../deepnote/deepnoteCellCopyHandler.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteCellCopyHandler.ts b/src/notebooks/deepnote/deepnoteCellCopyHandler.ts index 12b7f4aca1..c10e65ea24 100644 --- a/src/notebooks/deepnote/deepnoteCellCopyHandler.ts +++ b/src/notebooks/deepnote/deepnoteCellCopyHandler.ts @@ -66,7 +66,7 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService */ private async copyCellDownInterceptor(): Promise { const editor = window.activeNotebookEditor; - if (editor && editor.notebook.uri.path.endsWith('.deepnote')) { + if (editor && editor.notebook && editor.notebook.notebookType === 'deepnote') { await this.copyCellDown(); } else { logger.warn('notebook.cell.copyDown intercepted for non-Deepnote notebook - using fallback'); @@ -79,7 +79,7 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService */ private async copyCellUpInterceptor(): Promise { const editor = window.activeNotebookEditor; - if (editor && editor.notebook.uri.path.endsWith('.deepnote')) { + if (editor && editor.notebook && editor.notebook.notebookType === 'deepnote') { await this.copyCellUp(); } else { logger.warn('notebook.cell.copyUp intercepted for non-Deepnote notebook - using fallback'); @@ -92,7 +92,7 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService */ private async copyCellInterceptor(): Promise { const editor = window.activeNotebookEditor; - if (editor && editor.notebook.uri.path.endsWith('.deepnote')) { + if (editor && editor.notebook && editor.notebook.notebookType === 'deepnote') { await this.copyCellToClipboard(false); } else { logger.warn('notebook.cell.copy intercepted for non-Deepnote notebook - using fallback'); @@ -105,7 +105,7 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService */ private async cutCellInterceptor(): Promise { const editor = window.activeNotebookEditor; - if (editor && editor.notebook.uri.path.endsWith('.deepnote')) { + if (editor && editor.notebook && editor.notebook.notebookType === 'deepnote') { await this.copyCellToClipboard(true); } else { logger.warn('notebook.cell.cut intercepted for non-Deepnote notebook - using fallback'); @@ -118,7 +118,7 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService */ private async pasteCellInterceptor(): Promise { const editor = window.activeNotebookEditor; - if (editor && editor.notebook.uri.path.endsWith('.deepnote')) { + if (editor && editor.notebook && editor.notebook.notebookType === 'deepnote') { await this.pasteCellFromClipboard(); } else { logger.warn('notebook.cell.paste intercepted for non-Deepnote notebook - using fallback'); @@ -140,7 +140,7 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService private async copyCellAtOffset(offset: number): Promise { const editor = window.activeNotebookEditor; - if (!editor || !editor.notebook.uri.path.endsWith('.deepnote')) { + if (!editor || !editor.notebook || editor.notebook.notebookType !== 'deepnote') { logger.warn(`copyCellAtOffset called for non-Deepnote notebook`); return; } @@ -210,7 +210,7 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService private async onDidChangeNotebookDocument(e: NotebookDocumentChangeEvent): Promise { // Only process Deepnote notebooks - if (!e.notebook.uri.path.endsWith('.deepnote')) { + if (e.notebook.notebookType !== 'deepnote') { return; } @@ -321,7 +321,7 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService private async copyCellToClipboard(isCut: boolean): Promise { const editor = window.activeNotebookEditor; - if (!editor || !editor.notebook.uri.path.endsWith('.deepnote')) { + if (!editor || !editor.notebook || editor.notebook.notebookType !== 'deepnote') { logger.warn(`copyCellToClipboard called for non-Deepnote notebook`); return; } @@ -370,7 +370,7 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService private async pasteCellFromClipboard(): Promise { const editor = window.activeNotebookEditor; - if (!editor || !editor.notebook.uri.path.endsWith('.deepnote')) { + if (!editor || !editor.notebook || editor.notebook.notebookType !== 'deepnote') { logger.warn(`pasteCellFromClipboard called for non-Deepnote notebook`); return; } From e1c12c687810b19a5879050c4a8c5d150ebe89bd Mon Sep 17 00:00:00 2001 From: jankuca Date: Tue, 21 Oct 2025 17:48:41 +0200 Subject: [PATCH 04/11] redact logs --- .../deepnote/deepnoteCellCopyHandler.ts | 84 ++++--------------- 1 file changed, 18 insertions(+), 66 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteCellCopyHandler.ts b/src/notebooks/deepnote/deepnoteCellCopyHandler.ts index c10e65ea24..1f12de5ee3 100644 --- a/src/notebooks/deepnote/deepnoteCellCopyHandler.ts +++ b/src/notebooks/deepnote/deepnoteCellCopyHandler.ts @@ -93,7 +93,7 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService private async copyCellInterceptor(): Promise { const editor = window.activeNotebookEditor; if (editor && editor.notebook && editor.notebook.notebookType === 'deepnote') { - await this.copyCellToClipboard(false); + await this.copyCellToClipboard({ isCut: false }); } else { logger.warn('notebook.cell.copy intercepted for non-Deepnote notebook - using fallback'); } @@ -106,7 +106,7 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService private async cutCellInterceptor(): Promise { const editor = window.activeNotebookEditor; if (editor && editor.notebook && editor.notebook.notebookType === 'deepnote') { - await this.copyCellToClipboard(true); + await this.copyCellToClipboard({ isCut: true }); } else { logger.warn('notebook.cell.cut intercepted for non-Deepnote notebook - using fallback'); } @@ -160,32 +160,11 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService cellToCopy.document.languageId ); - // Copy all metadata, but generate new ID and sortingKey + // Copy all metadata (ID and sortingKey will be generated by onDidChangeNotebookDocument) if (cellToCopy.metadata) { const copiedMetadata = { ...cellToCopy.metadata }; - - // Generate new unique ID - copiedMetadata.id = generateBlockId(); - - // Update sortingKey in pocket if it exists - if (copiedMetadata.__deepnotePocket) { - copiedMetadata.__deepnotePocket = { - ...copiedMetadata.__deepnotePocket, - sortingKey: generateSortingKey(insertIndex) - }; - } else if (copiedMetadata.sortingKey) { - copiedMetadata.sortingKey = generateSortingKey(insertIndex); - } - newCell.metadata = copiedMetadata; - - logger.info( - `DeepnoteCellCopyHandler: Copying cell with metadata preserved: ${JSON.stringify( - copiedMetadata, - null, - 2 - )}` - ); + logger.debug('DeepnoteCellCopyHandler: Copying cell with metadata preserved'); } // Copy outputs if present @@ -202,7 +181,7 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService if (success) { // Move selection to the new cell editor.selection = new NotebookRange(insertIndex, insertIndex + 1); - logger.info(`DeepnoteCellCopyHandler: Successfully copied cell to index ${insertIndex}`); + logger.debug(`DeepnoteCellCopyHandler: Successfully copied cell to index ${insertIndex}`); } else { logger.warn('DeepnoteCellCopyHandler: Failed to copy cell'); } @@ -237,7 +216,7 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService const metadata = cell.metadata || {}; // Log the metadata to see what's actually being copied - logger.info(`DeepnoteCellCopyHandler: Cell added with metadata: ${JSON.stringify(metadata, null, 2)}`); + logger.debug('DeepnoteCellCopyHandler: Cell added with metadata'); // Only process Deepnote cells (cells with type or pocket metadata) if (!metadata.type && !metadata.__deepnotePocket) { @@ -273,10 +252,8 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService metadata: updatedMetadata }); - logger.info( - `DeepnoteCellCopyHandler: Updated metadata for ${ - metadata.type - } cell at index ${cellIndex}: ${JSON.stringify(updatedMetadata, null, 2)}` + logger.debug( + `DeepnoteCellCopyHandler: Updated metadata for ${metadata.type} cell at index ${cellIndex}` ); } @@ -303,7 +280,7 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService const success = await workspace.applyEdit(edit); if (success) { - logger.info(`DeepnoteCellCopyHandler: Successfully updated metadata for ${fixes.length} cell(s)`); + logger.debug(`DeepnoteCellCopyHandler: Successfully updated metadata for ${fixes.length} cell(s)`); } else { logger.warn(`DeepnoteCellCopyHandler: Failed to apply metadata fixes for ${fixes.length} cell(s)`); } @@ -318,7 +295,7 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService * Copy or cut a cell to the clipboard with metadata preserved. * @param isCut Whether this is a cut operation (will delete the cell after copying) */ - private async copyCellToClipboard(isCut: boolean): Promise { + private async copyCellToClipboard(params: { isCut: boolean }): Promise { const editor = window.activeNotebookEditor; if (!editor || !editor.notebook || editor.notebook.notebookType !== 'deepnote') { @@ -345,22 +322,16 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService const clipboardText = `${CLIPBOARD_MARKER}${JSON.stringify(clipboardData)}`; await env.clipboard.writeText(clipboardText); - logger.info( - `DeepnoteCellCopyHandler: ${isCut ? 'Cut' : 'Copied'} cell to clipboard with metadata: ${JSON.stringify( - clipboardData.metadata, - null, - 2 - )}` - ); + logger.debug(`DeepnoteCellCopyHandler: ${params.isCut ? 'Cut' : 'Copied'} cell to clipboard with metadata`); // If this is a cut operation, delete the cell - if (isCut) { + if (params.isCut) { const edit = new WorkspaceEdit(); edit.set(editor.notebook.uri, [ NotebookEdit.deleteCells(new NotebookRange(selection.start, selection.start + 1)) ]); await workspace.applyEdit(edit); - logger.info(`DeepnoteCellCopyHandler: Deleted cell after cut operation`); + logger.debug(`DeepnoteCellCopyHandler: Deleted cell after cut operation`); } } @@ -385,7 +356,7 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService // Check if clipboard contains our metadata marker if (!clipboardText.startsWith(CLIPBOARD_MARKER)) { - logger.info('DeepnoteCellCopyHandler: Clipboard does not contain Deepnote cell metadata, skipping'); + logger.debug('DeepnoteCellCopyHandler: Clipboard does not contain Deepnote cell metadata, skipping'); return; } @@ -397,32 +368,13 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService // Create new cell with preserved metadata const newCell = new NotebookCellData(clipboardData.kind, clipboardData.value, clipboardData.languageId); - // Copy metadata but generate new ID and sortingKey - const copiedMetadata = { ...clipboardData.metadata }; - - // Generate new unique ID - copiedMetadata.id = generateBlockId(); - - // Update sortingKey in pocket if it exists const insertIndex = selection.start; - if (copiedMetadata.__deepnotePocket) { - copiedMetadata.__deepnotePocket = { - ...copiedMetadata.__deepnotePocket, - sortingKey: generateSortingKey(insertIndex) - }; - } else if (copiedMetadata.sortingKey) { - copiedMetadata.sortingKey = generateSortingKey(insertIndex); - } + // Copy metadata (ID and sortingKey will be generated by onDidChangeNotebookDocument) + const copiedMetadata = { ...clipboardData.metadata }; newCell.metadata = copiedMetadata; - logger.info( - `DeepnoteCellCopyHandler: Pasting cell with metadata preserved: ${JSON.stringify( - copiedMetadata, - null, - 2 - )}` - ); + logger.debug('DeepnoteCellCopyHandler: Copying cell with metadata preserved'); // Insert the new cell const edit = new WorkspaceEdit(); @@ -433,7 +385,7 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService if (success) { // Move selection to the new cell editor.selection = new NotebookRange(insertIndex, insertIndex + 1); - logger.info(`DeepnoteCellCopyHandler: Successfully pasted cell at index ${insertIndex}`); + logger.debug(`DeepnoteCellCopyHandler: Successfully pasted cell at index ${insertIndex}`); } else { logger.warn('DeepnoteCellCopyHandler: Failed to paste cell'); } From 05b7caa276a9eee93ee63411bf9434a0cd02dfd8 Mon Sep 17 00:00:00 2001 From: jankuca Date: Tue, 21 Oct 2025 17:49:48 +0200 Subject: [PATCH 05/11] fix: deep clone outputs --- src/notebooks/deepnote/deepnoteCellCopyHandler.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteCellCopyHandler.ts b/src/notebooks/deepnote/deepnoteCellCopyHandler.ts index 1f12de5ee3..f66333dc1d 100644 --- a/src/notebooks/deepnote/deepnoteCellCopyHandler.ts +++ b/src/notebooks/deepnote/deepnoteCellCopyHandler.ts @@ -8,7 +8,9 @@ import { window, NotebookCellData, NotebookRange, - env + env, + NotebookCellOutputItem, + NotebookCellOutput } from 'vscode'; import { IExtensionSyncActivationService } from '../../platform/activation/types'; @@ -169,7 +171,9 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService // Copy outputs if present if (cellToCopy.outputs.length > 0) { - newCell.outputs = cellToCopy.outputs.map((output) => output); + newCell.outputs = cellToCopy.outputs.map( + (o) => new NotebookCellOutput(o.items.map((i) => new NotebookCellOutputItem(i.data, i.mime))) + ); } // Insert the new cell From 483dd850ab6c1c7e40fba6c0c4f56a86cfd49559 Mon Sep 17 00:00:00 2001 From: jankuca Date: Tue, 21 Oct 2025 17:53:51 +0200 Subject: [PATCH 06/11] remove deepnote.copy* commands since we are overriding native ones --- package.json | 6 ------ src/notebooks/deepnote/deepnoteCellCopyHandler.ts | 4 ---- 2 files changed, 10 deletions(-) diff --git a/package.json b/package.json index 908a5e9fe6..575cc95d56 100644 --- a/package.json +++ b/package.json @@ -70,12 +70,6 @@ "key": "escape", "when": "isCompositeNotebook && !editorHoverVisible && !suggestWidgetVisible && !isComposing && !inSnippetMode && !exceptionWidgetVisible && !selectionAnchorSet && !LinkedEditingInputVisible && !renameInputVisible && !editorHasSelection && !accessibilityHelpWidgetVisible && !breakpointWidgetVisible && !findWidgetVisible && !markersNavigationVisible && !parameterHintsVisible && !editorHasMultipleSelections && !notificationToastsVisible && !notebookEditorFocused && !inlineChatVisible", "command": "interactive.input.clear" - }, - { - "key": "shift+alt+down", - "mac": "shift+alt+down", - "when": "notebookType == deepnote && notebookEditorFocused && !inputFocus", - "command": "deepnote.copyCellDown" } ], "commands": [ diff --git a/src/notebooks/deepnote/deepnoteCellCopyHandler.ts b/src/notebooks/deepnote/deepnoteCellCopyHandler.ts index f66333dc1d..292a3fa5c1 100644 --- a/src/notebooks/deepnote/deepnoteCellCopyHandler.ts +++ b/src/notebooks/deepnote/deepnoteCellCopyHandler.ts @@ -47,10 +47,6 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService constructor(@inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry) {} public activate(): void { - // Register custom copy commands that preserve metadata - this.disposables.push(commands.registerCommand('deepnote.copyCellDown', () => this.copyCellDown())); - this.disposables.push(commands.registerCommand('deepnote.copyCellUp', () => this.copyCellUp())); - // Override built-in notebook copy/cut commands to preserve metadata for Deepnote notebooks this.disposables.push(commands.registerCommand('notebook.cell.copyDown', () => this.copyCellDownInterceptor())); this.disposables.push(commands.registerCommand('notebook.cell.copyUp', () => this.copyCellUpInterceptor())); From 0c661e08424b6e509931701fbd04c7074256ec23 Mon Sep 17 00:00:00 2001 From: jankuca Date: Tue, 21 Oct 2025 17:54:28 +0200 Subject: [PATCH 07/11] fix fallback to native commands in non-deepnote notebooks --- src/notebooks/deepnote/deepnoteCellCopyHandler.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteCellCopyHandler.ts b/src/notebooks/deepnote/deepnoteCellCopyHandler.ts index 292a3fa5c1..dc34b6de4d 100644 --- a/src/notebooks/deepnote/deepnoteCellCopyHandler.ts +++ b/src/notebooks/deepnote/deepnoteCellCopyHandler.ts @@ -67,7 +67,7 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService if (editor && editor.notebook && editor.notebook.notebookType === 'deepnote') { await this.copyCellDown(); } else { - logger.warn('notebook.cell.copyDown intercepted for non-Deepnote notebook - using fallback'); + await commands.executeCommand('default:notebook.cell.copyDown'); } } @@ -80,7 +80,7 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService if (editor && editor.notebook && editor.notebook.notebookType === 'deepnote') { await this.copyCellUp(); } else { - logger.warn('notebook.cell.copyUp intercepted for non-Deepnote notebook - using fallback'); + await commands.executeCommand('default:notebook.cell.copyUp'); } } @@ -93,7 +93,7 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService if (editor && editor.notebook && editor.notebook.notebookType === 'deepnote') { await this.copyCellToClipboard({ isCut: false }); } else { - logger.warn('notebook.cell.copy intercepted for non-Deepnote notebook - using fallback'); + await commands.executeCommand('default:notebook.cell.copy'); } } @@ -106,7 +106,7 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService if (editor && editor.notebook && editor.notebook.notebookType === 'deepnote') { await this.copyCellToClipboard({ isCut: true }); } else { - logger.warn('notebook.cell.cut intercepted for non-Deepnote notebook - using fallback'); + await commands.executeCommand('default:notebook.cell.cut'); } } @@ -119,7 +119,7 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService if (editor && editor.notebook && editor.notebook.notebookType === 'deepnote') { await this.pasteCellFromClipboard(); } else { - logger.warn('notebook.cell.paste intercepted for non-Deepnote notebook - using fallback'); + await commands.executeCommand('default:notebook.cell.paste'); } } From 5b2cf58769fb5face0d2cb1cda7b3e52449437ae Mon Sep 17 00:00:00 2001 From: jankuca Date: Wed, 22 Oct 2025 15:09:00 +0200 Subject: [PATCH 08/11] fix: preserve output metadata --- src/notebooks/deepnote/deepnoteCellCopyHandler.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/notebooks/deepnote/deepnoteCellCopyHandler.ts b/src/notebooks/deepnote/deepnoteCellCopyHandler.ts index dc34b6de4d..fa65ed7ba7 100644 --- a/src/notebooks/deepnote/deepnoteCellCopyHandler.ts +++ b/src/notebooks/deepnote/deepnoteCellCopyHandler.ts @@ -168,7 +168,11 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService // Copy outputs if present if (cellToCopy.outputs.length > 0) { newCell.outputs = cellToCopy.outputs.map( - (o) => new NotebookCellOutput(o.items.map((i) => new NotebookCellOutputItem(i.data, i.mime))) + (o) => + new NotebookCellOutput( + o.items.map((i) => new NotebookCellOutputItem(i.data, i.mime)), + o.metadata + ) ); } From 9efd426f8edbc0c8f7b15a5e04fb24b53775ca28 Mon Sep 17 00:00:00 2001 From: jankuca Date: Wed, 22 Oct 2025 15:25:39 +0200 Subject: [PATCH 09/11] lower log level --- src/notebooks/deepnote/deepnoteCellCopyHandler.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteCellCopyHandler.ts b/src/notebooks/deepnote/deepnoteCellCopyHandler.ts index fa65ed7ba7..85c8612104 100644 --- a/src/notebooks/deepnote/deepnoteCellCopyHandler.ts +++ b/src/notebooks/deepnote/deepnoteCellCopyHandler.ts @@ -139,7 +139,7 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService const editor = window.activeNotebookEditor; if (!editor || !editor.notebook || editor.notebook.notebookType !== 'deepnote') { - logger.warn(`copyCellAtOffset called for non-Deepnote notebook`); + logger.debug(`copyCellAtOffset called for non-Deepnote notebook`); return; } @@ -303,7 +303,7 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService const editor = window.activeNotebookEditor; if (!editor || !editor.notebook || editor.notebook.notebookType !== 'deepnote') { - logger.warn(`copyCellToClipboard called for non-Deepnote notebook`); + logger.debug(`copyCellToClipboard called for non-Deepnote notebook`); return; } @@ -346,7 +346,7 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService const editor = window.activeNotebookEditor; if (!editor || !editor.notebook || editor.notebook.notebookType !== 'deepnote') { - logger.warn(`pasteCellFromClipboard called for non-Deepnote notebook`); + logger.debug(`pasteCellFromClipboard called for non-Deepnote notebook`); return; } From e58d4406af4eb44d5707816fee4138a277645491 Mon Sep 17 00:00:00 2001 From: jankuca Date: Wed, 22 Oct 2025 15:28:08 +0200 Subject: [PATCH 10/11] fix: add fallback to default command on error on non-deepnote copy&paste --- src/notebooks/deepnote/deepnoteCellCopyHandler.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteCellCopyHandler.ts b/src/notebooks/deepnote/deepnoteCellCopyHandler.ts index 85c8612104..6ccf97ccf3 100644 --- a/src/notebooks/deepnote/deepnoteCellCopyHandler.ts +++ b/src/notebooks/deepnote/deepnoteCellCopyHandler.ts @@ -171,7 +171,7 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService (o) => new NotebookCellOutput( o.items.map((i) => new NotebookCellOutputItem(i.data, i.mime)), - o.metadata + o.metadata ? { ...o.metadata } : undefined ) ); } @@ -360,7 +360,7 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService // Check if clipboard contains our metadata marker if (!clipboardText.startsWith(CLIPBOARD_MARKER)) { - logger.debug('DeepnoteCellCopyHandler: Clipboard does not contain Deepnote cell metadata, skipping'); + await commands.executeCommand('default:notebook.cell.paste'); return; } @@ -395,6 +395,7 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService } } catch (error) { logger.error('DeepnoteCellCopyHandler: Error parsing clipboard data', error); + await commands.executeCommand('default:notebook.cell.paste'); } } } From e7d9e0700feac2130f30a0230d7cd77a09e28154 Mon Sep 17 00:00:00 2001 From: jankuca Date: Wed, 22 Oct 2025 15:44:11 +0200 Subject: [PATCH 11/11] use structured clonning --- src/notebooks/deepnote/deepnoteCellCopyHandler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteCellCopyHandler.ts b/src/notebooks/deepnote/deepnoteCellCopyHandler.ts index 6ccf97ccf3..e5889dfa55 100644 --- a/src/notebooks/deepnote/deepnoteCellCopyHandler.ts +++ b/src/notebooks/deepnote/deepnoteCellCopyHandler.ts @@ -160,7 +160,7 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService // Copy all metadata (ID and sortingKey will be generated by onDidChangeNotebookDocument) if (cellToCopy.metadata) { - const copiedMetadata = { ...cellToCopy.metadata }; + const copiedMetadata = structuredClone(cellToCopy.metadata); newCell.metadata = copiedMetadata; logger.debug('DeepnoteCellCopyHandler: Copying cell with metadata preserved'); } @@ -171,7 +171,7 @@ export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService (o) => new NotebookCellOutput( o.items.map((i) => new NotebookCellOutputItem(i.data, i.mime)), - o.metadata ? { ...o.metadata } : undefined + o.metadata ? structuredClone(o.metadata) : undefined ) ); }