Skip to content

Commit 7aa38d1

Browse files
committed
feat: Add a 'Select Notebook' command.
1 parent af8d5cb commit 7aa38d1

File tree

4 files changed

+160
-5
lines changed

4 files changed

+160
-5
lines changed

package.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,12 @@
316316
"title": "%jupyter.command.jupyter.viewOutput.title%",
317317
"category": "Jupyter"
318318
},
319+
{
320+
"command": "jupyter.selectdeepnotenotebook",
321+
"title": "Select Notebook",
322+
"category": "Deepnote",
323+
"enablement": "notebookType == deepnote"
324+
},
319325
{
320326
"command": "jupyter.notebookeditor.export",
321327
"title": "%DataScience.notebookExportAs%",
@@ -884,6 +890,11 @@
884890
"group": "navigation@2",
885891
"when": "notebookType == 'jupyter-notebook' && config.jupyter.showOutlineButtonInNotebookToolbar"
886892
},
893+
{
894+
"command": "jupyter.selectdeepnotenotebook",
895+
"group": "navigation@2",
896+
"when": "notebookType == deepnote"
897+
},
887898
{
888899
"command": "jupyter.continueEditSessionInCodespace",
889900
"group": "navigation@3",

src/notebooks/deepnote/deepnoteActivationService.ts

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
// Licensed under the MIT License.
33

44
import { injectable } from 'inversify';
5-
import { workspace } from 'vscode';
5+
import { workspace, commands, window, QuickPickItem, WorkspaceEdit, NotebookEdit, NotebookRange } from 'vscode';
66
import { IExtensionSyncActivationService } from '../../platform/activation/types';
77
import { IExtensionContext } from '../../platform/common/types';
88
import { inject } from 'inversify';
9-
import { DeepnoteNotebookSerializer } from './deepnoteSerializer';
9+
import { DeepnoteNotebookSerializer, DeepnoteProject, DeepnoteNotebook } from './deepnoteSerializer';
10+
import { Commands } from '../../platform/common/constants';
1011

1112
// Responsible for registering the Deepnote notebook serializer
1213
@injectable()
@@ -16,8 +17,96 @@ export class DeepnoteActivationService implements IExtensionSyncActivationServic
1617
) {}
1718

1819
public activate() {
20+
const serializer = new DeepnoteNotebookSerializer();
1921
this.extensionContext.subscriptions.push(
20-
workspace.registerNotebookSerializer('deepnote', new DeepnoteNotebookSerializer())
22+
workspace.registerNotebookSerializer('deepnote', serializer)
2123
);
24+
25+
// Register command to switch between notebooks
26+
this.extensionContext.subscriptions.push(
27+
commands.registerCommand(Commands.SelectDeepnoteNotebook, () => this.selectNotebook())
28+
);
29+
}
30+
31+
private async selectNotebook() {
32+
const activeEditor = window.activeNotebookEditor;
33+
if (!activeEditor || activeEditor.notebook.notebookType !== 'deepnote') {
34+
window.showErrorMessage('Please open a Deepnote file first');
35+
return;
36+
}
37+
38+
const notebookUri = activeEditor.notebook.uri;
39+
const rawContent = await workspace.fs.readFile(notebookUri);
40+
const contentString = Buffer.from(rawContent).toString('utf8');
41+
42+
try {
43+
const yaml = await import('js-yaml');
44+
const deepnoteProject = yaml.load(contentString) as DeepnoteProject;
45+
46+
if (!deepnoteProject.project?.notebooks) {
47+
window.showErrorMessage('Invalid Deepnote file: no notebooks found');
48+
return;
49+
}
50+
51+
if (deepnoteProject.project.notebooks.length === 1) {
52+
window.showInformationMessage('This Deepnote file contains only one notebook');
53+
return;
54+
}
55+
56+
const currentNotebookId = activeEditor.notebook.metadata?.deepnoteNotebookId;
57+
58+
interface NotebookQuickPickItem extends QuickPickItem {
59+
notebook: DeepnoteNotebook;
60+
}
61+
62+
const items: NotebookQuickPickItem[] = deepnoteProject.project.notebooks.map(notebook => ({
63+
label: notebook.name,
64+
description: `${notebook.blocks.length} cells${notebook.id === currentNotebookId ? ' (current)' : ''}`,
65+
detail: `ID: ${notebook.id}${notebook.workingDirectory ? ` | Working Directory: ${notebook.workingDirectory}` : ''}`,
66+
notebook
67+
}));
68+
69+
const selectedItem = await window.showQuickPick(items, {
70+
placeHolder: 'Select a notebook to switch to',
71+
title: 'Switch Notebook'
72+
});
73+
74+
if (selectedItem && selectedItem.notebook.id !== currentNotebookId) {
75+
// Create new cells from the selected notebook
76+
const serializer = new DeepnoteNotebookSerializer();
77+
const cells = serializer.convertBlocksToCells(selectedItem.notebook.blocks);
78+
79+
// Create a workspace edit to replace all cells
80+
const edit = new WorkspaceEdit();
81+
const notebookEdit = NotebookEdit.replaceCells(
82+
new NotebookRange(0, activeEditor.notebook.cellCount),
83+
cells
84+
);
85+
86+
// Also update metadata to reflect the new notebook
87+
const metadataEdit = NotebookEdit.updateNotebookMetadata({
88+
...activeEditor.notebook.metadata,
89+
deepnoteNotebookId: selectedItem.notebook.id,
90+
deepnoteNotebookName: selectedItem.notebook.name
91+
});
92+
93+
edit.set(notebookUri, [notebookEdit, metadataEdit]);
94+
95+
// Apply the edit
96+
const success = await workspace.applyEdit(edit);
97+
98+
if (success) {
99+
// Store the selected notebook ID for future reference
100+
const fileId = deepnoteProject.project.id;
101+
DeepnoteNotebookSerializer.setSelectedNotebookForUri(fileId, selectedItem.notebook.id);
102+
103+
window.showInformationMessage(`Switched to notebook: ${selectedItem.notebook.name}`);
104+
} else {
105+
window.showErrorMessage('Failed to switch notebook');
106+
}
107+
}
108+
} catch (error) {
109+
window.showErrorMessage(`Error switching notebook: ${error instanceof Error ? error.message : 'Unknown error'}`);
110+
}
22111
}
23112
}

src/notebooks/deepnote/deepnoteSerializer.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
import { NotebookCellData, NotebookCellKind, NotebookData, NotebookSerializer, CancellationToken, window, QuickPickItem } from 'vscode';
4+
import { NotebookCellData, NotebookCellKind, NotebookData, NotebookSerializer, CancellationToken, window, QuickPickItem, Uri, workspace, commands } from 'vscode';
55
import * as yaml from 'js-yaml';
66

77
export interface DeepnoteProject {
@@ -38,6 +38,26 @@ export interface DeepnoteBlock {
3838
}
3939

4040
export class DeepnoteNotebookSerializer implements NotebookSerializer {
41+
private static selectedNotebookByUri = new Map<string, string>();
42+
private static skipPromptForUri = new Set<string>();
43+
44+
static setSelectedNotebookForUri(uri: string, notebookId: string) {
45+
this.selectedNotebookByUri.set(uri, notebookId);
46+
this.skipPromptForUri.add(uri);
47+
}
48+
49+
static getSelectedNotebookForUri(uri: string): string | undefined {
50+
return this.selectedNotebookByUri.get(uri);
51+
}
52+
53+
static shouldSkipPrompt(uri: string): boolean {
54+
if (this.skipPromptForUri.has(uri)) {
55+
this.skipPromptForUri.delete(uri);
56+
return true;
57+
}
58+
return false;
59+
}
60+
4161
async deserializeNotebook(content: Uint8Array, _token: CancellationToken): Promise<NotebookData> {
4262
try {
4363
const contentString = Buffer.from(content).toString('utf8');
@@ -50,15 +70,49 @@ export class DeepnoteNotebookSerializer implements NotebookSerializer {
5070
// If there are multiple notebooks, we need to let the user select which one
5171
let selectedNotebook: DeepnoteNotebook;
5272

73+
// Create a unique identifier for this file based on project ID
74+
const fileId = deepnoteProject.project.id;
75+
76+
// Check if we should skip the prompt (command was used)
77+
const skipPrompt = DeepnoteNotebookSerializer.shouldSkipPrompt(fileId);
78+
79+
// Check if a notebook was pre-selected via the command
80+
const storedNotebookId = DeepnoteNotebookSerializer.getSelectedNotebookForUri(fileId);
81+
5382
if (deepnoteProject.project.notebooks.length === 1) {
5483
selectedNotebook = deepnoteProject.project.notebooks[0];
84+
} else if (skipPrompt && storedNotebookId) {
85+
// Use the stored selection when triggered by command
86+
const preSelected = deepnoteProject.project.notebooks.find(nb => nb.id === storedNotebookId);
87+
if (preSelected) {
88+
selectedNotebook = preSelected;
89+
} else {
90+
// Stored notebook not found, use first one
91+
selectedNotebook = deepnoteProject.project.notebooks[0];
92+
}
93+
} else if (storedNotebookId && !skipPrompt) {
94+
// Normal file open - check if we have a previously selected notebook
95+
const preSelected = deepnoteProject.project.notebooks.find(nb => nb.id === storedNotebookId);
96+
if (preSelected) {
97+
selectedNotebook = preSelected;
98+
} else {
99+
// Previously selected notebook not found, prompt for selection
100+
const selected = await this.selectNotebook(deepnoteProject.project.notebooks);
101+
selectedNotebook = selected || deepnoteProject.project.notebooks[0];
102+
if (selected) {
103+
DeepnoteNotebookSerializer.setSelectedNotebookForUri(fileId, selected.id);
104+
}
105+
}
55106
} else {
107+
// No stored selection, prompt user
56108
const selected = await this.selectNotebook(deepnoteProject.project.notebooks);
57109
if (!selected) {
58110
// If user cancelled selection, default to the first notebook
59111
selectedNotebook = deepnoteProject.project.notebooks[0];
60112
} else {
61113
selectedNotebook = selected;
114+
// Store the selection for future use
115+
DeepnoteNotebookSerializer.setSelectedNotebookForUri(fileId, selected.id);
62116
}
63117
}
64118

@@ -107,7 +161,7 @@ export class DeepnoteNotebookSerializer implements NotebookSerializer {
107161
return selectedItem?.notebook;
108162
}
109163

110-
private convertBlocksToCells(blocks: DeepnoteBlock[]): NotebookCellData[] {
164+
public convertBlocksToCells(blocks: DeepnoteBlock[]): NotebookCellData[] {
111165
return blocks
112166
.sort((a, b) => a.sortingKey.localeCompare(b.sortingKey))
113167
.map(block => this.convertBlockToCell(block));

src/platform/common/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ export namespace Commands {
218218
export const ScrollToCell = 'jupyter.scrolltocell';
219219
export const CreateNewNotebook = 'jupyter.createnewnotebook';
220220
export const ViewJupyterOutput = 'jupyter.viewOutput';
221+
export const SelectDeepnoteNotebook = 'jupyter.selectdeepnotenotebook';
221222
export const ExportAsPythonScript = 'jupyter.exportAsPythonScript';
222223
export const ExportToHTML = 'jupyter.exportToHTML';
223224
export const ExportToPDF = 'jupyter.exportToPDF';

0 commit comments

Comments
 (0)