Skip to content

Commit 9b0f979

Browse files
committed
add actually working "copy cell down" command
1 parent 55d8454 commit 9b0f979

File tree

2 files changed

+221
-0
lines changed

2 files changed

+221
-0
lines changed
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import { injectable, inject } from 'inversify';
2+
import {
3+
workspace,
4+
NotebookDocumentChangeEvent,
5+
NotebookEdit,
6+
WorkspaceEdit,
7+
commands,
8+
window,
9+
NotebookCellData,
10+
NotebookRange
11+
} from 'vscode';
12+
13+
import { IExtensionSyncActivationService } from '../../platform/activation/types';
14+
import { IDisposableRegistry } from '../../platform/common/types';
15+
import { logger } from '../../platform/logging';
16+
import { generateBlockId, generateSortingKey } from './dataConversionUtils';
17+
18+
/**
19+
* Handles cell copy operations in Deepnote notebooks to ensure metadata is preserved.
20+
*
21+
* VSCode's built-in copy commands don't preserve custom cell metadata, so this handler
22+
* provides a custom copy command that properly preserves all metadata fields including
23+
* sql_integration_id for SQL blocks.
24+
*/
25+
@injectable()
26+
export class DeepnoteCellCopyHandler implements IExtensionSyncActivationService {
27+
private processingChanges = false;
28+
29+
constructor(@inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry) {}
30+
31+
public activate(): void {
32+
// Register custom copy command that preserves metadata
33+
this.disposables.push(commands.registerCommand('deepnote.copyCellDown', () => this.copyCellDown()));
34+
35+
// Listen for notebook document changes to detect when cells are added without metadata
36+
this.disposables.push(workspace.onDidChangeNotebookDocument((e) => this.onDidChangeNotebookDocument(e)));
37+
}
38+
39+
private async copyCellDown(): Promise<void> {
40+
const editor = window.activeNotebookEditor;
41+
42+
if (!editor || !editor.notebook.uri.path.endsWith('.deepnote')) {
43+
// Fall back to default copy command for non-Deepnote notebooks
44+
await commands.executeCommand('notebook.cell.copyDown');
45+
return;
46+
}
47+
48+
const selection = editor.selection;
49+
if (!selection) {
50+
return;
51+
}
52+
53+
const cellToCopy = editor.notebook.cellAt(selection.start);
54+
const insertIndex = selection.start + 1;
55+
56+
// Create a new cell with the same content and metadata
57+
const newCell = new NotebookCellData(
58+
cellToCopy.kind,
59+
cellToCopy.document.getText(),
60+
cellToCopy.document.languageId
61+
);
62+
63+
// Copy all metadata, but generate new ID and sortingKey
64+
if (cellToCopy.metadata) {
65+
const copiedMetadata = { ...cellToCopy.metadata };
66+
67+
// Generate new unique ID
68+
copiedMetadata.id = generateBlockId();
69+
70+
// Update sortingKey in pocket if it exists
71+
if (copiedMetadata.__deepnotePocket) {
72+
copiedMetadata.__deepnotePocket = {
73+
...copiedMetadata.__deepnotePocket,
74+
sortingKey: generateSortingKey(insertIndex)
75+
};
76+
} else if (copiedMetadata.sortingKey) {
77+
copiedMetadata.sortingKey = generateSortingKey(insertIndex);
78+
}
79+
80+
newCell.metadata = copiedMetadata;
81+
82+
logger.info(
83+
`DeepnoteCellCopyHandler: Copying cell with metadata preserved: ${JSON.stringify(
84+
copiedMetadata,
85+
null,
86+
2
87+
)}`
88+
);
89+
}
90+
91+
// Copy outputs if present
92+
if (cellToCopy.outputs.length > 0) {
93+
newCell.outputs = cellToCopy.outputs.map((output) => output);
94+
}
95+
96+
// Insert the new cell
97+
const edit = new WorkspaceEdit();
98+
edit.set(editor.notebook.uri, [NotebookEdit.insertCells(insertIndex, [newCell])]);
99+
100+
const success = await workspace.applyEdit(edit);
101+
102+
if (success) {
103+
// Move selection to the new cell
104+
editor.selection = new NotebookRange(insertIndex, insertIndex + 1);
105+
logger.info(`DeepnoteCellCopyHandler: Successfully copied cell to index ${insertIndex}`);
106+
} else {
107+
logger.warn('DeepnoteCellCopyHandler: Failed to copy cell');
108+
}
109+
}
110+
111+
private async onDidChangeNotebookDocument(e: NotebookDocumentChangeEvent): Promise<void> {
112+
// Only process Deepnote notebooks
113+
if (!e.notebook.uri.path.endsWith('.deepnote')) {
114+
return;
115+
}
116+
117+
// Avoid recursive processing
118+
if (this.processingChanges) {
119+
return;
120+
}
121+
122+
// Check for cell additions (which includes copies)
123+
for (const change of e.contentChanges) {
124+
if (change.addedCells.length === 0) {
125+
continue;
126+
}
127+
128+
// When cells are copied, VSCode should preserve metadata automatically.
129+
// However, we need to ensure that:
130+
// 1. Each cell has a unique ID
131+
// 2. The sortingKey is updated based on the new position
132+
// 3. All other metadata (including sql_integration_id) is preserved
133+
134+
const cellsNeedingMetadataFix: Array<{ index: number; metadata: Record<string, unknown> }> = [];
135+
136+
for (const cell of change.addedCells) {
137+
const metadata = cell.metadata || {};
138+
139+
// Log the metadata to see what's actually being copied
140+
logger.info(`DeepnoteCellCopyHandler: Cell added with metadata: ${JSON.stringify(metadata, null, 2)}`);
141+
142+
// Only process Deepnote cells (cells with type or pocket metadata)
143+
if (!metadata.type && !metadata.__deepnotePocket) {
144+
continue;
145+
}
146+
147+
const cellIndex = e.notebook.getCells().indexOf(cell);
148+
149+
if (cellIndex === -1) {
150+
continue;
151+
}
152+
153+
// Check if this cell needs metadata updates
154+
// We update the ID and sortingKey for all added Deepnote cells to ensure uniqueness
155+
const updatedMetadata = { ...metadata };
156+
157+
// Generate new ID for the cell (important for copied cells)
158+
updatedMetadata.id = generateBlockId();
159+
160+
// Update sortingKey based on the new position
161+
if (updatedMetadata.__deepnotePocket) {
162+
updatedMetadata.__deepnotePocket = {
163+
...updatedMetadata.__deepnotePocket,
164+
sortingKey: generateSortingKey(cellIndex)
165+
};
166+
} else if (updatedMetadata.sortingKey) {
167+
updatedMetadata.sortingKey = generateSortingKey(cellIndex);
168+
}
169+
170+
// All other metadata (including sql_integration_id) is preserved from the original metadata
171+
cellsNeedingMetadataFix.push({
172+
index: cellIndex,
173+
metadata: updatedMetadata
174+
});
175+
176+
logger.info(
177+
`DeepnoteCellCopyHandler: Updated metadata for ${
178+
metadata.type
179+
} cell at index ${cellIndex}: ${JSON.stringify(updatedMetadata, null, 2)}`
180+
);
181+
}
182+
183+
// Apply metadata fixes if needed
184+
if (cellsNeedingMetadataFix.length > 0) {
185+
await this.applyMetadataFixes(e.notebook.uri, cellsNeedingMetadataFix);
186+
}
187+
}
188+
}
189+
190+
private async applyMetadataFixes(
191+
notebookUri: import('vscode').Uri,
192+
fixes: Array<{ index: number; metadata: Record<string, unknown> }>
193+
): Promise<void> {
194+
try {
195+
this.processingChanges = true;
196+
197+
const edit = new WorkspaceEdit();
198+
199+
// Create all the edits at once instead of calling set() multiple times
200+
const edits = fixes.map((fix) => NotebookEdit.updateCellMetadata(fix.index, fix.metadata));
201+
edit.set(notebookUri, edits);
202+
203+
const success = await workspace.applyEdit(edit);
204+
205+
if (success) {
206+
logger.info(`DeepnoteCellCopyHandler: Successfully updated metadata for ${fixes.length} cell(s)`);
207+
} else {
208+
logger.warn(`DeepnoteCellCopyHandler: Failed to apply metadata fixes for ${fixes.length} cell(s)`);
209+
}
210+
} catch (error) {
211+
logger.error('DeepnoteCellCopyHandler: Error applying metadata fixes', error);
212+
} finally {
213+
this.processingChanges = false;
214+
}
215+
}
216+
}

src/notebooks/serviceRegistry.node.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import { DeepnoteServerProvider } from '../kernels/deepnote/deepnoteServerProvid
6767
import { DeepnoteInitNotebookRunner, IDeepnoteInitNotebookRunner } from './deepnote/deepnoteInitNotebookRunner.node';
6868
import { DeepnoteRequirementsHelper, IDeepnoteRequirementsHelper } from './deepnote/deepnoteRequirementsHelper.node';
6969
import { SqlIntegrationStartupCodeProvider } from './deepnote/integrations/sqlIntegrationStartupCodeProvider';
70+
import { DeepnoteCellCopyHandler } from './deepnote/deepnoteCellCopyHandler';
7071

7172
export function registerTypes(serviceManager: IServiceManager, isDevMode: boolean) {
7273
registerControllerTypes(serviceManager, isDevMode);
@@ -152,6 +153,10 @@ export function registerTypes(serviceManager: IServiceManager, isDevMode: boolea
152153
IExtensionSyncActivationService,
153154
SqlIntegrationStartupCodeProvider
154155
);
156+
serviceManager.addSingleton<IExtensionSyncActivationService>(
157+
IExtensionSyncActivationService,
158+
DeepnoteCellCopyHandler
159+
);
155160

156161
// Deepnote kernel services
157162
serviceManager.addSingleton<IDeepnoteToolkitInstaller>(IDeepnoteToolkitInstaller, DeepnoteToolkitInstaller);

0 commit comments

Comments
 (0)