Skip to content

Commit 9d21508

Browse files
authored
feat: Create snapshots when running notebooks (#256)
1 parent a996dfb commit 9d21508

31 files changed

Lines changed: 3355 additions & 766 deletions

README.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ A powerful [Visual Studio Code](https://marketplace.visualstudio.com/items?itemN
1616

1717
![Deepnote Projects](./assets/deepnote-projects.png)
1818

19-
2019
Run Deepnote locally inside your IDE and unlock the next generation of data workflows:
2120

2221
- **Rich block types:** Combine Python, Markdown, data visualizations, tables, and more — all in one place
@@ -66,6 +65,7 @@ Open the Command Palette (`Cmd+Shift+P` or `Ctrl+Shift+P`) and type `Deepnote` t
6665
| `Deepnote: Import Notebook` | Import an existing notebook into your project |
6766
| `Notebook: Select Notebook Kernel` | Select or switch kernels within your notebook |
6867
| `Notebook: Change Cell Language` | Change the language of the cell currently in focus |
68+
| `Deepnote: Enable Snapshots` | Enable snapshot mode for the current workspace |
6969

7070
### Database integrations
7171

@@ -89,6 +89,24 @@ SELECT * FROM users WHERE created_at > '2024-01-01'
8989

9090
Results are displayed as interactive tables that you can explore and export.
9191

92+
### Snapshot mode
93+
94+
Snapshot mode gives you a historical, portable record of all notebook executions without polluting your main project files. This makes it easier to work with Git since outputs are stored separately from your source code.
95+
96+
**How it works:**
97+
98+
- Execution outputs are saved to a `snapshots/` folder alongside your project
99+
- Your main `.deepnote` file stays clean (no outputs), making diffs readable
100+
- Each "Run All" execution creates a timestamped snapshot for historical tracking
101+
- Running individual cells updates only the latest snapshot
102+
103+
**To enable:**
104+
105+
1. Open Command Palette (`Cmd+Shift+P` or `Ctrl+Shift+P`)
106+
2. Run `Deepnote: Enable Snapshots`
107+
108+
Once enabled, snapshots are automatically created when you execute notebooks. You can add the `snapshots/` folder to `.gitignore` to keep outputs local, or commit them to share execution history with your team.
109+
92110
## Need help?
93111

94112
- Join our [Community](https://github.com/deepnote/deepnote/discussions)!

cspell.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,14 @@
7878
"scikit",
7979
"scipy",
8080
"sklearn",
81+
"slugification",
82+
"slugified",
83+
"slugifies",
84+
"slugify",
8185
"sqlalchemy",
8286
"taskkill",
8387
"testdb",
88+
"testproject",
8489
"toolsai",
8590
"trino",
8691
"Trino",

package.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,16 @@
9393
"category": "Deepnote",
9494
"icon": "$(reveal)"
9595
},
96+
{
97+
"command": "deepnote.enableSnapshots",
98+
"title": "%deepnote.commands.enableSnapshots.title%",
99+
"category": "Deepnote"
100+
},
101+
{
102+
"command": "deepnote.disableSnapshots",
103+
"title": "%deepnote.commands.disableSnapshots.title%",
104+
"category": "Deepnote"
105+
},
96106
{
97107
"command": "deepnote.environments.create",
98108
"title": "%deepnote.commands.environments.create.title%",
@@ -1640,6 +1650,12 @@
16401650
"description": "Disable SSL certificate verification (for development only)",
16411651
"scope": "application"
16421652
},
1653+
"deepnote.snapshots.enabled": {
1654+
"type": "boolean",
1655+
"default": false,
1656+
"description": "When enabled, outputs are saved to separate snapshot files in a 'snapshots' folder instead of the main .deepnote file.",
1657+
"scope": "resource"
1658+
},
16431659
"deepnote.experiments.enabled": {
16441660
"type": "boolean",
16451661
"default": true,

package.nls.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,8 @@
250250
"deepnote.commands.openNotebook.title": "Open Notebook",
251251
"deepnote.commands.openFile.title": "Open File",
252252
"deepnote.commands.revealInExplorer.title": "Reveal in Explorer",
253+
"deepnote.commands.enableSnapshots.title": "Enable Snapshots",
254+
"deepnote.commands.disableSnapshots.title": "Disable Snapshots",
253255
"deepnote.commands.manageIntegrations.title": "Manage Integrations",
254256
"deepnote.commands.newProject.title": "New Project",
255257
"deepnote.commands.importNotebook.title": "Import Notebook",

src/kernels/execution/cellExecutionQueue.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import { CodeExecution } from './codeExecution';
1414
import { once } from '../../platform/common/utils/events';
1515
import { getCellMetadata } from '../../platform/common/utils';
1616
import { NotebookCellExecutionState, notebookCellExecutions } from '../../platform/notebooks/cellExecutionStateService';
17-
import { ISnapshotMetadataService } from '../../platform/notebooks/deepnote/types';
17+
// eslint-disable-next-line import/no-restricted-paths
18+
import { ISnapshotMetadataService } from '../../notebooks/deepnote/snapshots/snapshotService';
1819

1920
/**
2021
* A queue responsible for execution of cells.
@@ -324,5 +325,10 @@ export class CellExecutionQueue implements Disposable {
324325
break;
325326
}
326327
}
328+
329+
// Notify listeners that execution queue is complete
330+
if (this.notebook) {
331+
notebookCellExecutions.notifyQueueComplete(this.notebook.uri.toString());
332+
}
327333
}
328334
}

src/kernels/kernelExecution.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ import {
4545
import { CodeExecution } from './execution/codeExecution';
4646
import type { ICodeExecution } from './execution/types';
4747
import { NotebookCellExecutionState, notebookCellExecutions } from '../platform/notebooks/cellExecutionStateService';
48-
import { ISnapshotMetadataService } from '../platform/notebooks/deepnote/types';
48+
import { ISnapshotMetadataService } from '../notebooks/deepnote/snapshots/snapshotService';
4949

5050
/**
5151
* Everything in this classes gets disposed via the `onWillCancel` hook.

src/kernels/kernelProvider.node.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ import { IReplNotebookTrackerService } from '../platform/notebooks/replNotebookT
3232
import { logger } from '../platform/logging';
3333
import { getDisplayPath } from '../platform/common/platform/fs-paths.node';
3434
import { IRawNotebookSupportedService } from './raw/types';
35-
import { ISnapshotMetadataService } from '../platform/notebooks/deepnote/types';
35+
// eslint-disable-next-line import/no-restricted-paths
36+
import { ISnapshotMetadataService } from '../notebooks/deepnote/snapshots/snapshotService';
3637

3738
/**
3839
* Node version of a kernel provider. Needed in order to create the node version of a kernel.

src/notebooks/deepnote/deepnoteActivationService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { DeepnoteNotebookSerializer } from './deepnoteSerializer';
99
import { DeepnoteExplorerView } from './deepnoteExplorerView';
1010
import { IIntegrationManager } from './integrations/types';
1111
import { DeepnoteInputBlockEditProtection } from './deepnoteInputBlockEditProtection';
12-
import { ISnapshotMetadataService, ISnapshotMetadataServiceFull } from './snapshotMetadataService';
12+
import { SnapshotService } from './snapshots/snapshotService';
1313

1414
/**
1515
* Service responsible for activating and configuring Deepnote notebook support in VS Code.
@@ -30,7 +30,7 @@ export class DeepnoteActivationService implements IExtensionSyncActivationServic
3030
@inject(IDeepnoteNotebookManager) private readonly notebookManager: IDeepnoteNotebookManager,
3131
@inject(IIntegrationManager) integrationManager: IIntegrationManager,
3232
@inject(ILogger) private readonly logger: ILogger,
33-
@inject(ISnapshotMetadataService) @optional() private readonly snapshotService?: ISnapshotMetadataServiceFull
33+
@inject(SnapshotService) @optional() private readonly snapshotService?: SnapshotService
3434
) {
3535
this.integrationManager = integrationManager;
3636
}

src/notebooks/deepnote/deepnoteDataConverter.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ export class DeepnoteDataConverter {
9595
cell.metadata = {
9696
...block.metadata,
9797
id: block.id,
98+
// Store a backup of the ID under a different key in case VS Code modifies 'id'
99+
__deepnoteBlockId: block.id,
98100
type: block.type,
99101
sortingKey: block.sortingKey,
100102
...(blockWithOptionalFields.blockGroup && { blockGroup: blockWithOptionalFields.blockGroup }),

src/notebooks/deepnote/deepnoteNotebookCommandListener.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { injectable, inject } from 'inversify';
22
import {
33
commands,
4+
ConfigurationTarget,
45
window,
56
NotebookCellData,
67
NotebookCellKind,
@@ -16,7 +17,7 @@ import z from 'zod';
1617

1718
import { logger } from '../../platform/logging';
1819
import { IExtensionSyncActivationService } from '../../platform/activation/types';
19-
import { IDisposableRegistry } from '../../platform/common/types';
20+
import { IConfigurationService, IDisposableRegistry } from '../../platform/common/types';
2021
import { Commands } from '../../platform/common/constants';
2122
import { notebookUpdaterUtils } from '../../kernels/execution/notebookUpdater';
2223
import { WrappedError } from '../../platform/errors/types';
@@ -149,7 +150,10 @@ export function getNextDeepnoteVariableName(cells: NotebookCell[], prefix: 'df'
149150
*/
150151
@injectable()
151152
export class DeepnoteNotebookCommandListener implements IExtensionSyncActivationService {
152-
constructor(@inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry) {}
153+
constructor(
154+
@inject(IConfigurationService) private readonly configurationService: IConfigurationService,
155+
@inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry
156+
) {}
153157

154158
/**
155159
* Activates the service by registering Deepnote-specific commands.
@@ -217,6 +221,10 @@ export class DeepnoteNotebookCommandListener implements IExtensionSyncActivation
217221
this.addTextBlockCommandHandler({ textBlockType: 'text-cell-p' })
218222
)
219223
);
224+
this.disposableRegistry.push(commands.registerCommand(Commands.EnableSnapshots, () => this.enableSnapshots()));
225+
this.disposableRegistry.push(
226+
commands.registerCommand(Commands.DisableSnapshots, () => this.disableSnapshots())
227+
);
220228
}
221229

222230
public async addSqlBlock(): Promise<void> {
@@ -537,4 +545,34 @@ export class DeepnoteNotebookCommandListener implements IExtensionSyncActivation
537545
// Enter edit mode on the new cell
538546
await commands.executeCommand('notebook.cell.edit');
539547
}
548+
549+
private async disableSnapshots(): Promise<void> {
550+
try {
551+
await this.configurationService.updateSetting(
552+
'snapshots.enabled',
553+
false,
554+
undefined,
555+
ConfigurationTarget.Workspace
556+
);
557+
void window.showInformationMessage(l10n.t('Snapshots disabled for this workspace.'));
558+
} catch (error) {
559+
logger.error('Failed to disable snapshots', error);
560+
void window.showErrorMessage(l10n.t('Failed to disable snapshots.'));
561+
}
562+
}
563+
564+
private async enableSnapshots(): Promise<void> {
565+
try {
566+
await this.configurationService.updateSetting(
567+
'snapshots.enabled',
568+
true,
569+
undefined,
570+
ConfigurationTarget.Workspace
571+
);
572+
void window.showInformationMessage(l10n.t('Snapshots enabled for this workspace.'));
573+
} catch (error) {
574+
logger.error('Failed to enable snapshots', error);
575+
void window.showErrorMessage(l10n.t('Failed to enable snapshots.'));
576+
}
577+
}
540578
}

0 commit comments

Comments
 (0)