Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ A powerful [Visual Studio Code](https://marketplace.visualstudio.com/items?itemN

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


Run Deepnote locally inside your IDE and unlock the next generation of data workflows:

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

### Database integrations

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

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

### Snapshot mode

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.

**How it works:**

- Execution outputs are saved to a `snapshots/` folder alongside your project
- Your main `.deepnote` file stays clean (no outputs), making diffs readable
- Each "Run All" execution creates a timestamped snapshot for historical tracking
- Running individual cells updates only the latest snapshot

**To enable:**

1. Open Command Palette (`Cmd+Shift+P` or `Ctrl+Shift+P`)
2. Run `Deepnote: Enable Snapshots`

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.

## Need help?

- Join our [Community](https://github.com/deepnote/deepnote/discussions)!
Expand Down
5 changes: 5 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,14 @@
"scikit",
"scipy",
"sklearn",
"slugification",
"slugified",
"slugifies",
"slugify",
"sqlalchemy",
"taskkill",
"testdb",
"testproject",
"toolsai",
"trino",
"Trino",
Expand Down
16 changes: 16 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@
"category": "Deepnote",
"icon": "$(reveal)"
},
{
"command": "deepnote.enableSnapshots",
"title": "%deepnote.commands.enableSnapshots.title%",
"category": "Deepnote"
},
{
"command": "deepnote.disableSnapshots",
"title": "%deepnote.commands.disableSnapshots.title%",
"category": "Deepnote"
},
{
"command": "deepnote.environments.create",
"title": "%deepnote.commands.environments.create.title%",
Expand Down Expand Up @@ -1640,6 +1650,12 @@
"description": "Disable SSL certificate verification (for development only)",
"scope": "application"
},
"deepnote.snapshots.enabled": {
"type": "boolean",
"default": false,
"description": "When enabled, outputs are saved to separate snapshot files in a 'snapshots' folder instead of the main .deepnote file.",
"scope": "resource"
},
"deepnote.experiments.enabled": {
"type": "boolean",
"default": true,
Expand Down
2 changes: 2 additions & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,8 @@
"deepnote.commands.openNotebook.title": "Open Notebook",
"deepnote.commands.openFile.title": "Open File",
"deepnote.commands.revealInExplorer.title": "Reveal in Explorer",
"deepnote.commands.enableSnapshots.title": "Enable Snapshots",
"deepnote.commands.disableSnapshots.title": "Disable Snapshots",
"deepnote.commands.manageIntegrations.title": "Manage Integrations",
"deepnote.commands.newProject.title": "New Project",
"deepnote.commands.importNotebook.title": "Import Notebook",
Expand Down
5 changes: 5 additions & 0 deletions src/kernels/execution/cellExecutionQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,5 +324,10 @@ export class CellExecutionQueue implements Disposable {
break;
}
}

// Notify listeners that execution queue is complete
if (this.notebook) {
notebookCellExecutions.notifyQueueComplete(this.notebook.uri.toString());
}
}
}
4 changes: 2 additions & 2 deletions src/notebooks/deepnote/deepnoteActivationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { DeepnoteNotebookSerializer } from './deepnoteSerializer';
import { DeepnoteExplorerView } from './deepnoteExplorerView';
import { IIntegrationManager } from './integrations/types';
import { DeepnoteInputBlockEditProtection } from './deepnoteInputBlockEditProtection';
import { ISnapshotMetadataService, ISnapshotMetadataServiceFull } from './snapshotMetadataService';
import { ISnapshotService } from './snapshotService';

/**
* Service responsible for activating and configuring Deepnote notebook support in VS Code.
Expand All @@ -30,7 +30,7 @@ export class DeepnoteActivationService implements IExtensionSyncActivationServic
@inject(IDeepnoteNotebookManager) private readonly notebookManager: IDeepnoteNotebookManager,
@inject(IIntegrationManager) integrationManager: IIntegrationManager,
@inject(ILogger) private readonly logger: ILogger,
@inject(ISnapshotMetadataService) @optional() private readonly snapshotService?: ISnapshotMetadataServiceFull
@inject(ISnapshotService) @optional() private readonly snapshotService?: ISnapshotService
) {
this.integrationManager = integrationManager;
}
Expand Down
42 changes: 40 additions & 2 deletions src/notebooks/deepnote/deepnoteNotebookCommandListener.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { injectable, inject } from 'inversify';
import {
commands,
ConfigurationTarget,
window,
NotebookCellData,
NotebookCellKind,
Expand All @@ -16,7 +17,7 @@ import z from 'zod';

import { logger } from '../../platform/logging';
import { IExtensionSyncActivationService } from '../../platform/activation/types';
import { IDisposableRegistry } from '../../platform/common/types';
import { IConfigurationService, IDisposableRegistry } from '../../platform/common/types';
import { Commands } from '../../platform/common/constants';
import { notebookUpdaterUtils } from '../../kernels/execution/notebookUpdater';
import { WrappedError } from '../../platform/errors/types';
Expand Down Expand Up @@ -149,7 +150,10 @@ export function getNextDeepnoteVariableName(cells: NotebookCell[], prefix: 'df'
*/
@injectable()
export class DeepnoteNotebookCommandListener implements IExtensionSyncActivationService {
constructor(@inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry) {}
constructor(
@inject(IConfigurationService) private readonly configurationService: IConfigurationService,
@inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry
) {}

/**
* Activates the service by registering Deepnote-specific commands.
Expand Down Expand Up @@ -217,6 +221,10 @@ export class DeepnoteNotebookCommandListener implements IExtensionSyncActivation
this.addTextBlockCommandHandler({ textBlockType: 'text-cell-p' })
)
);
this.disposableRegistry.push(commands.registerCommand(Commands.EnableSnapshots, () => this.enableSnapshots()));
this.disposableRegistry.push(
commands.registerCommand(Commands.DisableSnapshots, () => this.disableSnapshots())
);
}

public async addSqlBlock(): Promise<void> {
Expand Down Expand Up @@ -537,4 +545,34 @@ export class DeepnoteNotebookCommandListener implements IExtensionSyncActivation
// Enter edit mode on the new cell
await commands.executeCommand('notebook.cell.edit');
}

private async disableSnapshots(): Promise<void> {
try {
await this.configurationService.updateSetting(
'snapshots.enabled',
false,
undefined,
ConfigurationTarget.Workspace
);
void window.showInformationMessage(l10n.t('Snapshots disabled for this workspace.'));
} catch (error) {
logger.error('Failed to disable snapshots', error);
void window.showErrorMessage(l10n.t('Failed to disable snapshots.'));
}
}

private async enableSnapshots(): Promise<void> {
try {
await this.configurationService.updateSetting(
'snapshots.enabled',
true,
undefined,
ConfigurationTarget.Workspace
);
void window.showInformationMessage(l10n.t('Snapshots enabled for this workspace.'));
} catch (error) {
logger.error('Failed to enable snapshots', error);
void window.showErrorMessage(l10n.t('Failed to enable snapshots.'));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
InputBlockType
} from './deepnoteNotebookCommandListener';
import { formatInputBlockCellContent, getInputBlockLanguage } from './inputBlockContentFormatter';
import { IDisposable } from '../../platform/common/types';
import { IConfigurationService, IDisposable } from '../../platform/common/types';
import * as notebookUpdater from '../../kernels/execution/notebookUpdater';
import { createMockedNotebookDocument } from '../../test/datascience/editor-integration/helpers';
import { WrappedError } from '../../platform/errors/types';
Expand All @@ -29,11 +29,21 @@ suite('DeepnoteNotebookCommandListener', () => {
let commandListener: DeepnoteNotebookCommandListener;
let disposables: IDisposable[];
let sandbox: sinon.SinonSandbox;
let mockConfigService: IConfigurationService;

function createMockConfigService(): IConfigurationService {
return {
getSettings: sinon.stub().returns({}),
updateSetting: sinon.stub().resolves(),
updateSectionSetting: sinon.stub().resolves()
} as unknown as IConfigurationService;
}

setup(() => {
sandbox = sinon.createSandbox();
disposables = [];
commandListener = new DeepnoteNotebookCommandListener(disposables);
mockConfigService = createMockConfigService();
commandListener = new DeepnoteNotebookCommandListener(mockConfigService, disposables);
});

teardown(() => {
Expand Down Expand Up @@ -78,7 +88,7 @@ suite('DeepnoteNotebookCommandListener', () => {

// Create new instance and activate again
const disposables2: IDisposable[] = [];
const commandListener2 = new DeepnoteNotebookCommandListener(disposables2);
const commandListener2 = new DeepnoteNotebookCommandListener(createMockConfigService(), disposables2);
commandListener2.activate();

// Both should register the same number of commands
Expand Down
2 changes: 1 addition & 1 deletion src/notebooks/deepnote/deepnoteNotebookManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import type { DeepnoteProject } from '../../platform/deepnote/deepnoteTypes';
export class DeepnoteNotebookManager implements IDeepnoteNotebookManager {
private readonly currentNotebookId = new Map<string, string>();
private readonly originalProjects = new Map<string, DeepnoteProject>();
private readonly selectedNotebookByProject = new Map<string, string>();
private readonly projectsWithInitNotebookRun = new Set<string>();
private readonly selectedNotebookByProject = new Map<string, string>();

/**
* Gets the currently selected notebook ID for a project.
Expand Down
41 changes: 37 additions & 4 deletions src/notebooks/deepnote/deepnoteSerializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { logger } from '../../platform/logging';
import { IDeepnoteNotebookManager } from '../types';
import { DeepnoteDataConverter } from './deepnoteDataConverter';
import type { DeepnoteNotebook } from '../../platform/deepnote/deepnoteTypes';
import { ISnapshotMetadataService, ISnapshotMetadataServiceFull } from './snapshotMetadataService';
import { ISnapshotService } from './snapshotService';
import { computeHash } from '../../platform/common/crypto';

export type { DeepnoteBlock, DeepnoteFile } from '@deepnote/blocks';
Expand Down Expand Up @@ -56,7 +56,7 @@ export class DeepnoteNotebookSerializer implements NotebookSerializer {

constructor(
@inject(IDeepnoteNotebookManager) private readonly notebookManager: IDeepnoteNotebookManager,
@inject(ISnapshotMetadataService) @optional() private readonly snapshotService?: ISnapshotMetadataServiceFull
@inject(ISnapshotService) @optional() private readonly snapshotService?: ISnapshotService
) {}

/**
Expand Down Expand Up @@ -114,10 +114,33 @@ export class DeepnoteNotebookSerializer implements NotebookSerializer {
throw new Error(l10n.t('No notebook selected or found'));
}

const cells = this.converter.convertBlocksToCells(selectedNotebook.blocks ?? []);
let cells = this.converter.convertBlocksToCells(selectedNotebook.blocks ?? []);

logger.debug(`DeepnoteSerializer: Converted ${cells.length} cells from notebook blocks`);

// Merge outputs from snapshot if snapshots are enabled
if (this.snapshotService?.isSnapshotsEnabled()) {
try {
const snapshotOutputs = await this.snapshotService.readSnapshot(projectId);

if (snapshotOutputs) {
logger.debug(`DeepnoteSerializer: Merging ${snapshotOutputs.size} outputs from snapshot`);
const blocksWithOutputs = this.snapshotService.mergeOutputsIntoBlocks(
selectedNotebook.blocks ?? [],
snapshotOutputs
);

cells = this.converter.convertBlocksToCells(blocksWithOutputs);
}
} catch (error) {
logger.warn(
`DeepnoteSerializer: Failed to merge snapshot outputs for project ${projectId}, using baseline cells`,
error
);
// Fall back to baseline cells (already set above)
}
}

this.notebookManager.storeOriginalProject(deepnoteFile.project.id, deepnoteFile, selectedNotebook.id);
logger.debug(`DeepnoteSerializer: Stored project ${projectId} in notebook manager`);

Expand Down Expand Up @@ -199,7 +222,17 @@ export class DeepnoteNotebookSerializer implements NotebookSerializer {
// Add snapshot metadata to blocks (contentHash and execution timing)
await this.addSnapshotMetadataToBlocks(blocks, data);

notebook.blocks = cloneWithoutCircularRefs<DeepnoteBlock[]>(blocks);
// Handle snapshot mode: strip outputs from main file (snapshots are created on execution, not save)
if (this.snapshotService?.isSnapshotsEnabled()) {
// Strip outputs from main file blocks - snapshots are created during cell execution
// Also clone to remove circular references that may cause yaml.dump to fail
const strippedBlocks = this.snapshotService.stripOutputsFromBlocks(blocks);
notebook.blocks = cloneWithoutCircularRefs<DeepnoteBlock[]>(strippedBlocks);
logger.debug('SerializeNotebook: Stripped outputs from main file (snapshot mode)');
} else {
// Default behavior: outputs in main file
notebook.blocks = cloneWithoutCircularRefs<DeepnoteBlock[]>(blocks);
}

logger.debug('SerializeNotebook: Cloned blocks, updating modifiedAt');

Expand Down
24 changes: 22 additions & 2 deletions src/notebooks/deepnote/deepnoteTreeDataProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ import type { DeepnoteProject, DeepnoteNotebook } from '../../platform/deepnote/
import { readDeepnoteProjectFile } from './deepnoteProjectUtils';
import { ILogger } from '../../platform/logging/types';

/** File suffix for snapshot files, used to filter them from the explorer */
const SNAPSHOT_FILE_SUFFIX = '.snapshot.deepnote';

/**
* Checks if a URI represents a snapshot file
*/
function isSnapshotFile(uri: Uri): boolean {
return uri.path.endsWith(SNAPSHOT_FILE_SUFFIX);
}

/**
* Comparator function for sorting tree items alphabetically by label (case-insensitive)
*/
Expand Down Expand Up @@ -203,8 +213,9 @@ export class DeepnoteTreeDataProvider implements TreeDataProvider<DeepnoteTreeIt
for (const workspaceFolder of workspace.workspaceFolders || []) {
const pattern = new RelativePattern(workspaceFolder, '**/*.deepnote');
const files = await workspace.findFiles(pattern);
const projectFiles = files.filter((file) => !file.path.endsWith(SNAPSHOT_FILE_SUFFIX));

for (const file of files) {
for (const file of projectFiles) {
try {
const project = await this.loadDeepnoteProject(file);
if (!project) {
Expand Down Expand Up @@ -316,16 +327,25 @@ export class DeepnoteTreeDataProvider implements TreeDataProvider<DeepnoteTreeIt
}

this.fileWatcher.onDidChange((uri) => {
if (isSnapshotFile(uri)) {
return;
}
// Use granular refresh for file changes
void this.refreshProject(uri.path);
});

this.fileWatcher.onDidCreate(() => {
this.fileWatcher.onDidCreate((uri) => {
if (isSnapshotFile(uri)) {
return;
}
// New file created, do full refresh
this._onDidChangeTreeData.fire();
});

this.fileWatcher.onDidDelete((uri) => {
if (isSnapshotFile(uri)) {
return;
}
// File deleted, clear both caches and do full refresh
this.cachedProjects.delete(uri.path);
this.treeItemCache.delete(`project:${uri.path}`);
Expand Down
Loading
Loading