From 98c6af628b35caa8d263379643dd48e6a1577cc3 Mon Sep 17 00:00:00 2001 From: Lucy Zhang Date: Thu, 27 Jun 2019 10:10:30 -0700 Subject: [PATCH] New feature: Jupyter Books (#6095) * Initial commit * Fixed broken branch * Show notebook titles in tree view * Added README * sections showing in tree view * Multiple books in treeview * removed book extension, added to notebook * removed book from extensions.ts * addressed Chris' comments * Addressed Charles' comments * fixed spelling in readme * added comment about same filenames * adding vsix * addressed Karl's comments --- build/lib/extensions.ts | 2 +- extensions/notebook/README.md | 6 + extensions/notebook/package.json | 12 +- extensions/notebook/src/book/bookTreeItem.ts | 24 ++++ extensions/notebook/src/book/bookTreeView.ts | 111 +++++++++++++++++++ extensions/notebook/src/extension.ts | 6 + extensions/notebook/yarn.lock | 5 + 7 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 extensions/notebook/src/book/bookTreeItem.ts create mode 100644 extensions/notebook/src/book/bookTreeView.ts diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index 2524c917b9..38ec0ba413 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -376,7 +376,7 @@ export function packageExtensionsStream(optsIn?: IPackageExtensionsOptions): Nod ]; const localExtensionDependencies = () => gulp.src(extensionDepsSrc, { base: '.', dot: true }) - .pipe(filter(['**', '!**/package-lock.json'])) + .pipe(filter(['**', '!**/package-lock.json'])); // Original code commented out here // const localExtensionDependencies = () => gulp.src('extensions/node_modules/**', { base: '.' }); diff --git a/extensions/notebook/README.md b/extensions/notebook/README.md index 67d15dad7d..f881bc6dd9 100644 --- a/extensions/notebook/README.md +++ b/extensions/notebook/README.md @@ -2,6 +2,12 @@ Welcome to the Notebook extension for Azure Data Studio! This extension supports core notebook functionality including configuration settings, actions such as New / Open Notebook, and more. +## Books in Azure Data Studio + +Jupyter Book allows opening a single "Book" of related notebooks and markdown files. This feature will work if you open any Book folder in Azure Data Studio. + +Download a [sample book](https://github.com/jupyter/jupyter-book) and open folder in Azure Data Studio to get started. You can learn more about Books on the [Jupyter Book homepage](https://jupyter.org/jupyter-book/intro.html). + ## Code of Conduct This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. diff --git a/extensions/notebook/package.json b/extensions/notebook/package.json index c7de136b9f..fd45f51de4 100644 --- a/extensions/notebook/package.json +++ b/extensions/notebook/package.json @@ -333,10 +333,20 @@ "connectionProviderIds": [] } ] + }, + "views": { + "explorer": [ + { + "id": "bookTreeView", + "name": "Books", + "when": "bookOpened && isDevelopment" + } + ] } }, "dependencies": { "@jupyterlab/services": "^3.2.1", + "@types/js-yaml": "^3.12.1", "decompress": "^4.2.0", "error-ex": "^1.3.1", "figures": "^2.0.0", @@ -364,4 +374,4 @@ "vscode": "1.1.5" }, "enableProposedApi": true -} +} \ No newline at end of file diff --git a/extensions/notebook/src/book/bookTreeItem.ts b/extensions/notebook/src/book/bookTreeItem.ts new file mode 100644 index 0000000000..1009621ede --- /dev/null +++ b/extensions/notebook/src/book/bookTreeItem.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; + +export class BookTreeItem extends vscode.TreeItem { + + constructor( + public readonly title: string, + public readonly root: string, + public readonly tableOfContents: any[], + public readonly collapsibleState: vscode.TreeItemCollapsibleState, + public uri?: string, + public readonly type?: vscode.FileType, + public command?: vscode.Command + ) { + super(title, collapsibleState); + } + + contextValue = 'book'; + +} \ No newline at end of file diff --git a/extensions/notebook/src/book/bookTreeView.ts b/extensions/notebook/src/book/bookTreeView.ts new file mode 100644 index 0000000000..dd47b10930 --- /dev/null +++ b/extensions/notebook/src/book/bookTreeView.ts @@ -0,0 +1,111 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as fs from 'fs'; +import * as yaml from 'js-yaml'; +import { BookTreeItem } from './bookTreeItem'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + + +export class BookTreeViewProvider implements vscode.TreeDataProvider { + + private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; + private _tableOfContentsPath: string[]; + + constructor(private workspaceRoot: string) { + if (workspaceRoot !== '') { + this._tableOfContentsPath = this.getTocFiles(this.workspaceRoot); + let bookOpened: boolean = this._tableOfContentsPath && this._tableOfContentsPath.length > 0; + vscode.commands.executeCommand('setContext', 'bookOpened', bookOpened); + } + } + + private getTocFiles(dir: string): string[] { + let allFiles: string[] = []; + let files = fs.readdirSync(dir); + for (let i in files) { + let name = path.join(dir, files[i]); + if (fs.statSync(name).isDirectory()) { + allFiles = allFiles.concat(this.getTocFiles(name)); + } else if (files[i] === 'toc.yml') { + allFiles.push(name); + } + } + return allFiles; + } + + async openNotebook(resource: vscode.Uri): Promise { + try { + let doc = await vscode.workspace.openTextDocument(resource); + vscode.window.showTextDocument(doc); + } catch (e) { + vscode.window.showErrorMessage(localize('openNotebookError', 'Open file {0} failed: {1}', + resource.fsPath, + e instanceof Error ? e.message : e)); + } + } + + getTreeItem(element: BookTreeItem): vscode.TreeItem { + return element; + } + + getChildren(element?: BookTreeItem): Thenable { + if (element) { + if (element.tableOfContents) { + return Promise.resolve(this.getSections(element.tableOfContents, element.root)); + } else { + return Promise.resolve([]); + } + } else { + return Promise.resolve(this.getBooks()); + } + } + + private getBooks(): BookTreeItem[] { + let books: BookTreeItem[] = []; + for (let i in this._tableOfContentsPath) { + let root = path.dirname(path.dirname(this._tableOfContentsPath[i])); + try { + const config = yaml.safeLoad(fs.readFileSync(path.join(root, '_config.yml'), 'utf-8')); + const tableOfContents = yaml.safeLoad(fs.readFileSync(this._tableOfContentsPath[i], 'utf-8')); + let book = new BookTreeItem(config.title, root, tableOfContents, vscode.TreeItemCollapsibleState.Collapsed); + books.push(book); + } catch (e) { + vscode.window.showErrorMessage(localize('openConfigFileError', 'Open file {0} failed: {1}', + path.join(root, '_config.yml'), + e instanceof Error ? e.message : e)); + } + } + return books; + } + + private getSections(sec: any[], root: string): BookTreeItem[] { + let notebooks: BookTreeItem[] = []; + for (let i = 0; i < sec.length; i++) { + if (sec[i].url) { + let pathToNotebook = path.join(root, 'content', sec[i].url.concat('.ipynb')); + let pathToMarkdown = path.join(root, 'content', sec[i].url.concat('.md')); + // Note: Currently, if there is an ipynb and a md file with the same name, Jupyter Books only shows the notebook. + // Following Jupyter Books behavior for now + if (fs.existsSync(pathToNotebook)) { + let notebook = new BookTreeItem(sec[i].title, root, sec[i].sections, sec[i].sections ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.None, sec[i].url, vscode.FileType.File, { command: 'bookTreeView.openNotebook', title: 'Open Notebook', arguments: [pathToNotebook], }); + notebooks.push(notebook); + } else if (fs.existsSync(pathToMarkdown)) { + let markdown = new BookTreeItem(sec[i].title, root, sec[i].sections, sec[i].sections ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.None, sec[i].url, vscode.FileType.File, { command: 'bookTreeView.openNotebook', title: 'Open Notebook', arguments: [pathToMarkdown], }); + notebooks.push(markdown); + } else { + vscode.window.showErrorMessage(localize('missingFileError', 'Missing file : {0}', sec[i].title)); + } + } else { + // TODO: search functionality (#6160) + } + } + return notebooks; + } +} diff --git a/extensions/notebook/src/extension.ts b/extensions/notebook/src/extension.ts index 7b23299103..1e621baa7c 100644 --- a/extensions/notebook/src/extension.ts +++ b/extensions/notebook/src/extension.ts @@ -15,6 +15,7 @@ import { IExtensionApi } from './types'; import { CellType } from './contracts/content'; import { getErrorMessage, isEditorTitleFree } from './common/utils'; import { NotebookUriHandler } from './protocol/notebookUriHandler'; +import { BookTreeViewProvider } from './book/bookTreeView'; const localize = nls.loadMessageBundle(); @@ -26,6 +27,11 @@ let controller: JupyterController; type ChooseCellType = { label: string, id: CellType }; export async function activate(extensionContext: vscode.ExtensionContext): Promise { + + const bookTreeViewProvider = new BookTreeViewProvider(vscode.workspace.rootPath || ''); + vscode.window.registerTreeDataProvider('bookTreeView', bookTreeViewProvider); + vscode.commands.registerCommand('bookTreeView.openNotebook', (resource) => bookTreeViewProvider.openNotebook(resource)); + extensionContext.subscriptions.push(vscode.commands.registerCommand('_notebook.command.new', (context?: azdata.ConnectedContext) => { let connectionProfile: azdata.IConnectionProfile = undefined; if (context && context.connectionProfile) { diff --git a/extensions/notebook/yarn.lock b/extensions/notebook/yarn.lock index d6990ecd96..5ae5f6e772 100644 --- a/extensions/notebook/yarn.lock +++ b/extensions/notebook/yarn.lock @@ -120,6 +120,11 @@ "@types/minimatch" "*" "@types/node" "*" +"@types/js-yaml@^3.12.1": + version "3.12.1" + resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.1.tgz#5c6f4a1eabca84792fbd916f0cb40847f123c656" + integrity sha512-SGGAhXLHDx+PK4YLNcNGa6goPf9XRWQNAUUbffkwVGGXIxmDKWyGGL4inzq2sPmExu431Ekb9aEMn9BkPqEYFA== + "@types/minimatch@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"