Skip to content

Commit

Permalink
Fix open notebook bug (microsoft#10930)
Browse files Browse the repository at this point in the history
* Fix open notebook bug

* cleanup

* clean up spaces
  • Loading branch information
Charles-Gagnon authored Jun 16, 2020
1 parent 381a747 commit 94bc0d9
Show file tree
Hide file tree
Showing 9 changed files with 212 additions and 168 deletions.
4 changes: 4 additions & 0 deletions extensions/notebook/src/common/appContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@

import * as vscode from 'vscode';
import { ApiWrapper } from './apiWrapper';
import { NotebookUtils } from './notebookUtils';

/**
* Global context for the application
*/
export class AppContext {

private serviceMap: Map<string, any> = new Map();
public readonly notebookUtils: NotebookUtils;

constructor(public readonly extensionContext: vscode.ExtensionContext, public readonly apiWrapper: ApiWrapper) {
this.apiWrapper = apiWrapper || new ApiWrapper();
this.notebookUtils = new NotebookUtils(apiWrapper);
}

public getService<T>(serviceName: string): T {
Expand Down
236 changes: 112 additions & 124 deletions extensions/notebook/src/common/notebookUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,164 +4,152 @@
*--------------------------------------------------------------------------------------------*/

import * as azdata from 'azdata';
import * as crypto from 'crypto';
import * as os from 'os';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { getErrorMessage, isEditorTitleFree } from '../common/utils';
import { ApiWrapper } from './apiWrapper';

const localize = nls.loadMessageBundle();

const JUPYTER_NOTEBOOK_PROVIDER = 'jupyter';
const msgSampleCodeDataFrame = localize('msgSampleCodeDataFrame', "This sample code loads the file into a data frame and shows the first 10 results.");
const noNotebookVisible = localize('noNotebookVisible', "No notebook editor is active");

/**
* Creates a random token per https://nodejs.org/api/crypto.html#crypto_crypto_randombytes_size_callback.
* Defaults to 24 bytes, which creates a 48-char hex string
*/
export function getRandomToken(size: number = 24): Promise<string> {
return new Promise((resolve, reject) => {
crypto.randomBytes(size, (err, buffer) => {
if (err) {
reject(err);
}
let token = buffer.toString('hex');
resolve(token);
});
});
}
export class NotebookUtils {

export async function newNotebook(connectionProfile?: azdata.IConnectionProfile): Promise<azdata.nb.NotebookEditor> {
const title = findNextUntitledEditorName();
const untitledUri = vscode.Uri.parse(`untitled:${title}`);
const options: azdata.nb.NotebookShowOptions = connectionProfile ? {
viewColumn: null,
preserveFocus: true,
preview: null,
providerId: null,
connectionProfile: connectionProfile,
defaultKernel: null
} : null;
return azdata.nb.showNotebookDocument(untitledUri, options);
}
constructor(private _apiWrapper: ApiWrapper) { }

function findNextUntitledEditorName(): string {
let nextVal = 0;
// Note: this will go forever if it's coded wrong, or you have infinite Untitled notebooks!
while (true) {
let title = `Notebook-${nextVal}`;
if (isEditorTitleFree(title)) {
return title;
public async newNotebook(connectionProfile?: azdata.IConnectionProfile): Promise<azdata.nb.NotebookEditor> {
const title = this.findNextUntitledEditorName();
const untitledUri = vscode.Uri.parse(`untitled:${title}`);
const options: azdata.nb.NotebookShowOptions = connectionProfile ? {
viewColumn: null,
preserveFocus: true,
preview: null,
providerId: null,
connectionProfile: connectionProfile,
defaultKernel: null
} : null;
return azdata.nb.showNotebookDocument(untitledUri, options);
}

private findNextUntitledEditorName(): string {
let nextVal = 0;
// Note: this will go forever if it's coded wrong, or you have infinite Untitled notebooks!
while (true) {
let title = `Notebook-${nextVal}`;
if (isEditorTitleFree(title)) {
return title;
}
nextVal++;
}
nextVal++;
}
}

export async function openNotebook(): Promise<void> {
try {
let filter: { [key: string]: Array<string> } = {};
// TODO support querying valid notebook file types
filter[localize('notebookFiles', "Notebooks")] = ['ipynb'];
let file = await vscode.window.showOpenDialog({
filters: filter
});
if (file) {
let doc = await vscode.workspace.openTextDocument(file[0]);
vscode.window.showTextDocument(doc);
public async openNotebook(): Promise<void> {
try {
let filter: { [key: string]: Array<string> } = {};
// TODO support querying valid notebook file types
filter[localize('notebookFiles', "Notebooks")] = ['ipynb'];
let file = await this._apiWrapper.showOpenDialog({
filters: filter
});
if (file && file.length > 0) {
await azdata.nb.showNotebookDocument(file[0]);
}
} catch (err) {
this._apiWrapper.showErrorMessage(getErrorMessage(err));
}
} catch (err) {
vscode.window.showErrorMessage(getErrorMessage(err));
}
}

export async function runActiveCell(): Promise<void> {
try {
let notebook = azdata.nb.activeNotebookEditor;
if (notebook) {
await notebook.runCell();
} else {
throw new Error(noNotebookVisible);
public async runActiveCell(): Promise<void> {
try {
let notebook = azdata.nb.activeNotebookEditor;
if (notebook) {
await notebook.runCell();
} else {
throw new Error(noNotebookVisible);
}
} catch (err) {
vscode.window.showErrorMessage(getErrorMessage(err));
}
} catch (err) {
vscode.window.showErrorMessage(getErrorMessage(err));
}
}

export async function clearActiveCellOutput(): Promise<void> {
try {
let notebook = azdata.nb.activeNotebookEditor;
if (notebook) {
await notebook.clearOutput();
} else {
throw new Error(noNotebookVisible);
public async clearActiveCellOutput(): Promise<void> {
try {
let notebook = azdata.nb.activeNotebookEditor;
if (notebook) {
await notebook.clearOutput();
} else {
throw new Error(noNotebookVisible);
}
} catch (err) {
vscode.window.showErrorMessage(getErrorMessage(err));
}
} catch (err) {
vscode.window.showErrorMessage(getErrorMessage(err));
}
}

export async function runAllCells(startCell?: azdata.nb.NotebookCell, endCell?: azdata.nb.NotebookCell): Promise<void> {
try {
let notebook = azdata.nb.activeNotebookEditor;
if (notebook) {
await notebook.runAllCells(startCell, endCell);
} else {
throw new Error(noNotebookVisible);
public async runAllCells(startCell?: azdata.nb.NotebookCell, endCell?: azdata.nb.NotebookCell): Promise<void> {
try {
let notebook = azdata.nb.activeNotebookEditor;
if (notebook) {
await notebook.runAllCells(startCell, endCell);
} else {
throw new Error(noNotebookVisible);
}
} catch (err) {
vscode.window.showErrorMessage(getErrorMessage(err));
}
} catch (err) {
vscode.window.showErrorMessage(getErrorMessage(err));
}
}

export async function addCell(cellType: azdata.nb.CellType): Promise<void> {
try {
let notebook = azdata.nb.activeNotebookEditor;
if (notebook) {
await notebook.edit((editBuilder: azdata.nb.NotebookEditorEdit) => {
// TODO should prompt and handle cell placement
editBuilder.insertCell({
cell_type: cellType,
source: ''
public async addCell(cellType: azdata.nb.CellType): Promise<void> {
try {
let notebook = azdata.nb.activeNotebookEditor;
if (notebook) {
await notebook.edit((editBuilder: azdata.nb.NotebookEditorEdit) => {
// TODO should prompt and handle cell placement
editBuilder.insertCell({
cell_type: cellType,
source: ''
});
});
});
} else {
throw new Error(noNotebookVisible);
} else {
throw new Error(noNotebookVisible);
}
} catch (err) {
vscode.window.showErrorMessage(getErrorMessage(err));
}
} catch (err) {
vscode.window.showErrorMessage(getErrorMessage(err));
}
}

export async function analyzeNotebook(oeContext?: azdata.ObjectExplorerContext): Promise<void> {
// Ensure we get a unique ID for the notebook. For now we're using a different prefix to the built-in untitled files
// to handle this. We should look into improving this in the future
let title = findNextUntitledEditorName();
let untitledUri = vscode.Uri.parse(`untitled:${title}`);
public async analyzeNotebook(oeContext?: azdata.ObjectExplorerContext): Promise<void> {
// Ensure we get a unique ID for the notebook. For now we're using a different prefix to the built-in untitled files
// to handle this. We should look into improving this in the future
let title = this.findNextUntitledEditorName();
let untitledUri = vscode.Uri.parse(`untitled:${title}`);

let editor = await azdata.nb.showNotebookDocument(untitledUri, {
connectionProfile: oeContext ? oeContext.connectionProfile : undefined,
providerId: JUPYTER_NOTEBOOK_PROVIDER,
preview: false,
defaultKernel: {
name: 'pysparkkernel',
display_name: 'PySpark',
language: 'python'
}
});
if (oeContext && oeContext.nodeInfo && oeContext.nodeInfo.nodePath) {
// Get the file path after '/HDFS'
let hdfsPath: string = oeContext.nodeInfo.nodePath.substring(oeContext.nodeInfo.nodePath.indexOf('/HDFS') + '/HDFS'.length);
if (hdfsPath.length > 0) {
let analyzeCommand = '#' + msgSampleCodeDataFrame + os.EOL + 'df = (spark.read.option("inferSchema", "true")'
+ os.EOL + '.option("header", "true")' + os.EOL + '.csv("{0}"))' + os.EOL + 'df.show(10)';
let editor = await azdata.nb.showNotebookDocument(untitledUri, {
connectionProfile: oeContext ? oeContext.connectionProfile : undefined,
providerId: JUPYTER_NOTEBOOK_PROVIDER,
preview: false,
defaultKernel: {
name: 'pysparkkernel',
display_name: 'PySpark',
language: 'python'
}
});
if (oeContext && oeContext.nodeInfo && oeContext.nodeInfo.nodePath) {
// Get the file path after '/HDFS'
let hdfsPath: string = oeContext.nodeInfo.nodePath.substring(oeContext.nodeInfo.nodePath.indexOf('/HDFS') + '/HDFS'.length);
if (hdfsPath.length > 0) {
let analyzeCommand = '#' + msgSampleCodeDataFrame + os.EOL + 'df = (spark.read.option("inferSchema", "true")'
+ os.EOL + '.option("header", "true")' + os.EOL + '.csv("{0}"))' + os.EOL + 'df.show(10)';

editor.edit(editBuilder => {
editBuilder.insertCell({
cell_type: 'code',
source: analyzeCommand.replace('{0}', hdfsPath)
}, 0);
});
editor.edit(editBuilder => {
editBuilder.insertCell({
cell_type: 'code',
source: analyzeCommand.replace('{0}', hdfsPath)
}, 0);
});
}
}
}
}
17 changes: 17 additions & 0 deletions extensions/notebook/src/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as fs from 'fs-extra';
import * as nls from 'vscode-nls';
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as crypto from 'crypto';
import { notebookLanguages } from './constants';

const localize = nls.loadMessageBundle();
Expand Down Expand Up @@ -300,3 +301,19 @@ function decorate(decorator: (fn: Function, key: string) => Function): Function
export function getDropdownValue(dropdown: azdata.DropDownComponent): string {
return (typeof dropdown.value === 'string') ? dropdown.value : dropdown.value.name;
}

/**
* Creates a random token per https://nodejs.org/api/crypto.html#crypto_crypto_randombytes_size_callback.
* Defaults to 24 bytes, which creates a 48-char hex string
*/
export async function getRandomToken(size: number = 24): Promise<string> {
return new Promise((resolve, reject) => {
crypto.randomBytes(size, (err, buffer) => {
if (err) {
reject(err);
}
let token = buffer.toString('hex');
resolve(token);
});
});
}
21 changes: 10 additions & 11 deletions extensions/notebook/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import { CellType } from './contracts/content';
import { NotebookUriHandler } from './protocol/notebookUriHandler';
import { BookTreeViewProvider } from './book/bookTreeView';
import { NavigationProviders } from './common/constants';
import { newNotebook, openNotebook, runActiveCell, runAllCells, clearActiveCellOutput, addCell, analyzeNotebook } from './common/notebookUtils';

const localize = nls.loadMessageBundle();

Expand All @@ -26,6 +25,7 @@ let controller: JupyterController;
type ChooseCellType = { label: string, id: CellType };

export async function activate(extensionContext: vscode.ExtensionContext): Promise<IExtensionApi> {
const appContext = new AppContext(extensionContext, new ApiWrapper());
const createBookPath: string = path.posix.join(extensionContext.extensionPath, 'resources', 'notebooks', 'JupyterBooksCreate.ipynb');
extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openBook', (bookPath: string, openAsUntitled: boolean, urlToOpen?: string) => openAsUntitled ? providedBookTreeViewProvider.openBook(bookPath, urlToOpen, true) : bookTreeViewProvider.openBook(bookPath, urlToOpen, true)));
extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openNotebook', (resource) => bookTreeViewProvider.openNotebook(resource)));
Expand Down Expand Up @@ -56,19 +56,19 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi
if (context && context.connectionProfile) {
connectionProfile = context.connectionProfile;
}
return newNotebook(connectionProfile);
return appContext.notebookUtils.newNotebook(connectionProfile);
}));
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.open', async () => {
await openNotebook();
await appContext.notebookUtils.openNotebook();
}));
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.runactivecell', async () => {
await runActiveCell();
await appContext.notebookUtils.runActiveCell();
}));
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.runallcells', async () => {
await runAllCells();
await appContext.notebookUtils.runAllCells();
}));
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.clearactivecellresult', async () => {
await clearActiveCellOutput();
await appContext.notebookUtils.clearActiveCellOutput();
}));
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.addcell', async () => {
let cellType: CellType;
Expand All @@ -91,17 +91,17 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi
return;
}
if (cellType) {
await addCell(cellType);
await appContext.notebookUtils.addCell(cellType);
}
}));
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.addcode', async () => {
await addCell('code');
await appContext.notebookUtils.addCell('code');
}));
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.addtext', async () => {
await addCell('markdown');
await appContext.notebookUtils.addCell('markdown');
}));
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.analyzeNotebook', async (explorerContext: azdata.ObjectExplorerContext) => {
await analyzeNotebook(explorerContext);
await appContext.notebookUtils.analyzeNotebook(explorerContext);
}));
extensionContext.subscriptions.push(vscode.window.registerUriHandler(new NotebookUriHandler()));

Expand All @@ -110,7 +110,6 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi
await vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(urlToOpen));
}));

let appContext = new AppContext(extensionContext, new ApiWrapper());
controller = new JupyterController(appContext);
let result = await controller.activate();
if (!result) {
Expand Down
Loading

0 comments on commit 94bc0d9

Please sign in to comment.