Skip to content
Merged
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
8 changes: 7 additions & 1 deletion src/kernels/execution/cellExecutionQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import { CodeExecution } from './codeExecution';
import { once } from '../../platform/common/utils/events';
import { getCellMetadata } from '../../platform/common/utils';
import { NotebookCellExecutionState, notebookCellExecutions } from '../../platform/notebooks/cellExecutionStateService';
import { ISnapshotMetadataService } from '../../platform/notebooks/deepnote/types';
// eslint-disable-next-line import/no-restricted-paths
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a question: why is this a restricted path?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These rules are from the original repo and I'm not sure why its restricted.

import { ISnapshotMetadataService } from '../../notebooks/deepnote/snapshots/snapshotService';

/**
* A queue responsible for execution of cells.
Expand Down Expand Up @@ -324,5 +325,10 @@ export class CellExecutionQueue implements Disposable {
break;
}
}

// Notify listeners that execution queue is complete
if (this.notebook) {
notebookCellExecutions.notifyQueueComplete(this.notebook.uri.toString());
}
}
}
2 changes: 1 addition & 1 deletion src/kernels/kernelExecution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import {
import { CodeExecution } from './execution/codeExecution';
import type { ICodeExecution } from './execution/types';
import { NotebookCellExecutionState, notebookCellExecutions } from '../platform/notebooks/cellExecutionStateService';
import { ISnapshotMetadataService } from '../platform/notebooks/deepnote/types';
import { ISnapshotMetadataService } from '../notebooks/deepnote/snapshots/snapshotService';

/**
* Everything in this classes gets disposed via the `onWillCancel` hook.
Expand Down
3 changes: 2 additions & 1 deletion src/kernels/kernelProvider.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ import { IReplNotebookTrackerService } from '../platform/notebooks/replNotebookT
import { logger } from '../platform/logging';
import { getDisplayPath } from '../platform/common/platform/fs-paths.node';
import { IRawNotebookSupportedService } from './raw/types';
import { ISnapshotMetadataService } from '../platform/notebooks/deepnote/types';
// eslint-disable-next-line import/no-restricted-paths
import { ISnapshotMetadataService } from '../notebooks/deepnote/snapshots/snapshotService';

/**
* Node version of a kernel provider. Needed in order to create the node version of a kernel.
Expand Down
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 { SnapshotService } from './snapshots/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(SnapshotService) @optional() private readonly snapshotService?: SnapshotService
) {
this.integrationManager = integrationManager;
}
Expand Down
2 changes: 2 additions & 0 deletions src/notebooks/deepnote/deepnoteDataConverter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ export class DeepnoteDataConverter {
cell.metadata = {
...block.metadata,
id: block.id,
// Store a backup of the ID under a different key in case VS Code modifies 'id'
__deepnoteBlockId: block.id,
type: block.type,
sortingKey: block.sortingKey,
...(blockWithOptionalFields.blockGroup && { blockGroup: blockWithOptionalFields.blockGroup }),
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
12 changes: 9 additions & 3 deletions 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 Expand Up @@ -61,7 +61,13 @@ export class DeepnoteNotebookManager implements IDeepnoteNotebookManager {
* @param notebookId Initial notebook ID to set as current
*/
storeOriginalProject(projectId: string, project: DeepnoteProject, notebookId: string): void {
this.originalProjects.set(projectId, project);
// Deep clone to prevent mutations from affecting stored state
// This is critical for multi-notebook projects where multiple notebooks
// share the same stored project reference
// Using structuredClone to handle circular references (e.g., in output metadata)
const clonedProject = structuredClone(project);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this change unrelated to snapshots? Or is it to fix issues that arose while introducing them?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was an existing bug that I found while testing the snapshots.


this.originalProjects.set(projectId, clonedProject);
this.currentNotebookId.set(projectId, notebookId);
}

Expand Down Expand Up @@ -90,7 +96,7 @@ export class DeepnoteNotebookManager implements IDeepnoteNotebookManager {
return false;
}

const updatedProject = JSON.parse(JSON.stringify(project)) as DeepnoteProject;
const updatedProject = structuredClone(project);
updatedProject.project.integrations = integrations;

const currentNotebookId = this.currentNotebookId.get(projectId);
Expand Down
Loading
Loading