From 2b2a275fb0ad626c3a9ab091ad3111a67900e62e Mon Sep 17 00:00:00 2001 From: Cory Rivera Date: Mon, 13 Apr 2020 23:42:02 -0700 Subject: [PATCH] Add Open Notebook Folder functionality to Books viewlet. (#9939) --- extensions/notebook/package.json | 28 +++- extensions/notebook/package.nls.json | 4 +- .../resources/dark/open_folder_inverse.svg | 3 + .../notebook/resources/light/open_folder.svg | 3 + extensions/notebook/src/book/bookModel.ts | 93 +++++++++---- extensions/notebook/src/book/bookTreeItem.ts | 23 ++-- extensions/notebook/src/book/bookTreeView.ts | 122 +++++++++++++----- .../notebook/src/common/localizedConstants.ts | 4 +- extensions/notebook/src/extension.ts | 4 +- .../notebook/src/test/book/book.test.ts | 7 +- .../src/test/book/bookTrustManager.test.ts | 5 + .../notebook/browser/notebook.component.ts | 2 +- 12 files changed, 224 insertions(+), 74 deletions(-) create mode 100644 extensions/notebook/resources/dark/open_folder_inverse.svg create mode 100644 extensions/notebook/resources/light/open_folder.svg diff --git a/extensions/notebook/package.json b/extensions/notebook/package.json index 453d3a4045..a88013d408 100644 --- a/extensions/notebook/package.json +++ b/extensions/notebook/package.json @@ -19,7 +19,7 @@ "properties": { "notebook.maxBookSearchDepth": { "type": "number", - "default": 5, + "default": 10, "description": "%notebook.maxBookSearchDepth.description%" }, "notebook.pythonPath": { @@ -203,10 +203,23 @@ "light": "resources/light/open_notebook.svg" } }, + { + "command": "notebook.command.openNotebookFolder", + "title": "%title.openNotebookFolder%", + "category": "%books-preview-category%", + "icon": { + "dark": "resources/dark/open_folder_inverse.svg", + "light": "resources/light/open_folder.svg" + } + }, { "command": "notebook.command.closeBook", "title": "%title.closeJupyterBook%" }, + { + "command": "notebook.command.closeNotebook", + "title": "%title.closeJupyterNotebook%" + }, { "command": "notebook.command.createBook", "title": "%title.createJupyterBook%", @@ -317,6 +330,10 @@ "command": "notebook.command.closeBook", "when": "false" }, + { + "command": "notebook.command.closeNotebook", + "when": "false" + }, { "command": "notebook.command.revealInBooksViewlet", "when": "false" @@ -370,6 +387,10 @@ { "command": "notebook.command.closeBook", "when": "view == bookTreeView && viewItem == savedBook" + }, + { + "command": "notebook.command.closeNotebook", + "when": "view == bookTreeView && viewItem == savedNotebook" } ], "view/title": [ @@ -381,6 +402,11 @@ { "command": "notebook.command.createBook", "when": "view == bookTreeView" + }, + { + "command": "notebook.command.openNotebookFolder", + "when": "view == bookTreeView", + "group": "navigation" } ], "notebook/toolbar": [ diff --git a/extensions/notebook/package.nls.json b/extensions/notebook/package.nls.json index e1a1dd2bc4..611130978a 100644 --- a/extensions/notebook/package.nls.json +++ b/extensions/notebook/package.nls.json @@ -38,6 +38,8 @@ "title.PreviewLocalizedBook": "Get localized SQL Server 2019 guide", "title.openJupyterBook": "Open Jupyter Book", "title.closeJupyterBook": "Close Jupyter Book", + "title.closeJupyterNotebook": "Close Jupyter Notebook", "title.revealInBooksViewlet": "Reveal in Books", - "title.createJupyterBook": "Create Book (Preview)" + "title.createJupyterBook": "Create Book (Preview)", + "title.openNotebookFolder": "Open Notebooks in Folder" } diff --git a/extensions/notebook/resources/dark/open_folder_inverse.svg b/extensions/notebook/resources/dark/open_folder_inverse.svg new file mode 100644 index 0000000000..0f5431ee7b --- /dev/null +++ b/extensions/notebook/resources/dark/open_folder_inverse.svg @@ -0,0 +1,3 @@ + + + diff --git a/extensions/notebook/resources/light/open_folder.svg b/extensions/notebook/resources/light/open_folder.svg new file mode 100644 index 0000000000..a93e51cc1b --- /dev/null +++ b/extensions/notebook/resources/light/open_folder.svg @@ -0,0 +1,3 @@ + + + diff --git a/extensions/notebook/src/book/bookModel.ts b/extensions/notebook/src/book/bookModel.ts index edd0009971..fe4c648b66 100644 --- a/extensions/notebook/src/book/bookModel.ts +++ b/extensions/notebook/src/book/bookModel.ts @@ -6,15 +6,12 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import * as yaml from 'js-yaml'; -import * as glob from 'fast-glob'; import { BookTreeItem, BookTreeItemType } from './bookTreeItem'; -import { maxBookSearchDepth, notebookConfigKey } from '../common/constants'; import * as path from 'path'; import * as fileServices from 'fs'; import * as fs from 'fs-extra'; import * as loc from '../common/localizedConstants'; import { IJupyterBookToc, IJupyterBookSection } from '../contracts/content'; -import { isNullOrUndefined } from 'util'; import { ApiWrapper } from '../common/apiWrapper'; @@ -23,24 +20,30 @@ const fsPromises = fileServices.promises; export class BookModel implements azdata.nb.NavigationProvider { private _bookItems: BookTreeItem[]; private _allNotebooks = new Map(); - private _tableOfContentPaths: string[] = []; + private _tableOfContentsPath: string; readonly providerId: string = 'BookNavigator'; private _errorMessage: string; private apiWrapper: ApiWrapper = new ApiWrapper(); - constructor(public bookPath: string, public openAsUntitled: boolean, private _extensionContext: vscode.ExtensionContext) { - this.bookPath = bookPath; - this.openAsUntitled = openAsUntitled; + constructor( + public readonly bookPath: string, + public readonly openAsUntitled: boolean, + public readonly isNotebook: boolean, + private _extensionContext: vscode.ExtensionContext) { this._bookItems = []; this._extensionContext.subscriptions.push(azdata.nb.registerNavigationProvider(this)); } public async initializeContents(): Promise { - this._tableOfContentPaths = []; this._bookItems = []; - await this.getTableOfContentFiles(this.bookPath); - await this.readBooks(); + this._allNotebooks = new Map(); + if (this.isNotebook) { + this.readNotebook(); + } else { + await this.loadTableOfContentFiles(this.bookPath); + await this.readBooks(); + } } public getAllNotebooks(): Map { @@ -51,20 +54,14 @@ export class BookModel implements azdata.nb.NavigationProvider { return this._allNotebooks.get(uri); } - public async getTableOfContentFiles(folderPath: string): Promise { - let notebookConfig = vscode.workspace.getConfiguration(notebookConfigKey); - let maxDepth = notebookConfig[maxBookSearchDepth]; - // Use default value if user enters an invalid value - if (isNullOrUndefined(maxDepth) || maxDepth < 0) { - maxDepth = 5; - } else if (maxDepth === 0) { // No limit of search depth if user enters 0 - maxDepth = undefined; + public async loadTableOfContentFiles(folderPath: string): Promise { + if (this.isNotebook) { + return; } - let p: string = path.posix.join(glob.escapePath(folderPath.replace(/\\/g, '/')), '**', '_data', 'toc.yml'); - let tableOfContentPaths: string[] = await glob(p, { deep: maxDepth }); - if (tableOfContentPaths.length > 0) { - this._tableOfContentPaths = this._tableOfContentPaths.concat(tableOfContentPaths); + let tableOfContentsPath: string = path.posix.join(folderPath, '_data', 'toc.yml'); + if (await fs.pathExists(tableOfContentsPath)) { + this._tableOfContentsPath = tableOfContentsPath; vscode.commands.executeCommand('setContext', 'bookOpened', true); } else { this._errorMessage = loc.missingTocError; @@ -72,16 +69,55 @@ export class BookModel implements azdata.nb.NavigationProvider { } } + public readNotebook(): BookTreeItem { + if (!this.isNotebook) { + return undefined; + } + + let pathDetails = path.parse(this.bookPath); + let notebookItem = new BookTreeItem({ + title: pathDetails.name, + contentPath: this.bookPath, + root: pathDetails.dir, + tableOfContents: { sections: undefined }, + page: { sections: undefined }, + type: BookTreeItemType.Notebook, + treeItemCollapsibleState: vscode.TreeItemCollapsibleState.Expanded, + isUntitled: this.openAsUntitled, + }, + { + light: this._extensionContext.asAbsolutePath('resources/light/notebook.svg'), + dark: this._extensionContext.asAbsolutePath('resources/dark/notebook_inverse.svg') + } + ); + this._bookItems.push(notebookItem); + if (this.openAsUntitled && !this._allNotebooks.get(pathDetails.base)) { + this._allNotebooks.set(pathDetails.base, notebookItem); + } else { + // convert to URI to avoid casing issue with drive letters when getting navigation links + let uriToNotebook: vscode.Uri = vscode.Uri.file(this.bookPath); + if (!this._allNotebooks.get(uriToNotebook.fsPath)) { + this._allNotebooks.set(uriToNotebook.fsPath, notebookItem); + } + } + return notebookItem; + } + public async readBooks(): Promise { - for (const contentPath of this._tableOfContentPaths) { - let root: string = path.dirname(path.dirname(contentPath)); + if (this.isNotebook) { + return undefined; + } + + if (this._tableOfContentsPath) { + let root: string = path.dirname(path.dirname(this._tableOfContentsPath)); try { let fileContents = await fsPromises.readFile(path.join(root, '_config.yml'), 'utf-8'); const config = yaml.safeLoad(fileContents.toString()); - fileContents = await fsPromises.readFile(contentPath, 'utf-8'); + fileContents = await fsPromises.readFile(this._tableOfContentsPath, 'utf-8'); const tableOfContents: any = yaml.safeLoad(fileContents.toString()); let book: BookTreeItem = new BookTreeItem({ title: config.title, + contentPath: this._tableOfContentsPath, root: root, tableOfContents: { sections: this.parseJupyterSections(tableOfContents) }, page: tableOfContents, @@ -114,6 +150,7 @@ export class BookModel implements azdata.nb.NavigationProvider { if (sections[i].external) { let externalLink: BookTreeItem = new BookTreeItem({ title: sections[i].title, + contentPath: undefined, root: root, tableOfContents: tableOfContents, page: sections[i], @@ -136,6 +173,7 @@ export class BookModel implements azdata.nb.NavigationProvider { if (await fs.pathExists(pathToNotebook)) { let notebook = new BookTreeItem({ title: sections[i].title, + contentPath: pathToNotebook, root: root, tableOfContents: tableOfContents, page: sections[i], @@ -165,6 +203,7 @@ export class BookModel implements azdata.nb.NavigationProvider { } else if (await fs.pathExists(pathToMarkdown)) { let markdown: BookTreeItem = new BookTreeItem({ title: sections[i].title, + contentPath: pathToMarkdown, root: root, tableOfContents: tableOfContents, page: sections[i], @@ -208,8 +247,8 @@ export class BookModel implements azdata.nb.NavigationProvider { } - public get tableOfContentPaths(): string[] { - return this._tableOfContentPaths; + public get tableOfContentsPath(): string { + return this._tableOfContentsPath; } getNavigation(uri: vscode.Uri): Thenable { diff --git a/extensions/notebook/src/book/bookTreeItem.ts b/extensions/notebook/src/book/bookTreeItem.ts index 25ecc49d0d..b52c0bac9d 100644 --- a/extensions/notebook/src/book/bookTreeItem.ts +++ b/extensions/notebook/src/book/bookTreeItem.ts @@ -18,6 +18,7 @@ export enum BookTreeItemType { export interface BookTreeItemFormat { title: string; + contentPath: string; root: string; tableOfContents: IJupyterBookToc; page: any; @@ -47,6 +48,12 @@ export class BookTreeItem extends vscode.TreeItem { } else { if (book.page && book.page.sections && book.page.sections.length > 0) { this.contextValue = 'section'; + } else if (book.type === BookTreeItemType.Notebook && !book.tableOfContents.sections) { + if (book.isUntitled) { + this.contextValue = 'unsavedNotebook'; + } else { + this.contextValue = 'savedNotebook'; + } } this.setPageVariables(); this.setCommand(); @@ -63,19 +70,19 @@ export class BookTreeItem extends vscode.TreeItem { this._sections = this.book.page.sections || this.book.page.subsections; this._uri = this.book.page.url; - let index = (this.book.tableOfContents.sections.indexOf(this.book.page)); - this.setPreviousUri(index); - this.setNextUri(index); + if (this.book.tableOfContents.sections) { + let index = (this.book.tableOfContents.sections.indexOf(this.book.page)); + this.setPreviousUri(index); + this.setNextUri(index); + } } private setCommand() { if (this.book.type === BookTreeItemType.Notebook) { // The Notebook editor expects a posix path for the resource (it will still resolve to the correct fsPath based on OS) - const pathToNotebook = path.posix.join(this.book.root, 'content', this._uri.concat('.ipynb')); - this.command = { command: this.book.isUntitled ? 'bookTreeView.openUntitledNotebook' : 'bookTreeView.openNotebook', title: loc.openNotebookCommand, arguments: [pathToNotebook], }; + this.command = { command: this.book.isUntitled ? 'bookTreeView.openUntitledNotebook' : 'bookTreeView.openNotebook', title: loc.openNotebookCommand, arguments: [this.book.contentPath], }; } else if (this.book.type === BookTreeItemType.Markdown) { - let pathToMarkdown = path.join(this.book.root, 'content', this._uri.concat('.md')); - this.command = { command: 'bookTreeView.openMarkdown', title: loc.openMarkdownCommand, arguments: [pathToMarkdown], }; + this.command = { command: 'bookTreeView.openMarkdown', title: loc.openMarkdownCommand, arguments: [this.book.contentPath], }; } else if (this.book.type === BookTreeItemType.ExternalLink) { this.command = { command: 'bookTreeView.openExternalLink', title: loc.openExternalLinkCommand, arguments: [this._uri], }; } @@ -146,7 +153,7 @@ export class BookTreeItem extends vscode.TreeItem { return `${this._uri}`; } else { - return undefined; + return this.book.type === BookTreeItemType.Book ? this.book.root : this.book.contentPath; } } diff --git a/extensions/notebook/src/book/bookTreeView.ts b/extensions/notebook/src/book/bookTreeView.ts index 0440c845b9..8d762e4426 100644 --- a/extensions/notebook/src/book/bookTreeView.ts +++ b/extensions/notebook/src/book/bookTreeView.ts @@ -11,15 +11,22 @@ import * as constants from '../common/constants'; import * as fsw from 'fs'; import { IPrompter, QuestionTypes, IQuestion } from '../prompts/question'; import CodeAdapter from '../prompts/adapter'; -import { BookTreeItem } from './bookTreeItem'; +import { BookTreeItem, BookTreeItemType } from './bookTreeItem'; import { BookModel } from './bookModel'; import { Deferred } from '../common/promise'; import { IBookTrustManager, BookTrustManager } from './bookTrustManager'; import * as loc from '../common/localizedConstants'; import { ApiWrapper } from '../common/apiWrapper'; +import * as glob from 'fast-glob'; +import { isNullOrUndefined } from 'util'; const Content = 'content'; +interface BookSearchResults { + notebookPaths: string[]; + bookPaths: string[]; +} + export class BookTreeViewProvider implements vscode.TreeDataProvider { private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; @@ -50,7 +57,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider { try { - await this.createAndAddBookModel(workspaceFolder.uri.fsPath); + await this.loadNotebooksInFolder(workspaceFolder.uri.fsPath); } catch { // no-op, not all workspace folders are going to be valid books } @@ -97,31 +104,38 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider { + async openBook(bookPath: string, urlToOpen?: string, showPreview?: boolean, isNotebook?: boolean): Promise { try { - let books: BookModel[] = this.books.filter(book => book.bookPath === bookPath) || []; + // Convert path to posix style for easier comparisons + bookPath = bookPath.replace(/\\/g, '/'); + // Check if the book is already open in viewlet. - if (books.length > 0 && books[0].bookItems.length > 0) { - this.currentBook = books[0]; - await this.showPreviewFile(urlToOpen); - } - else { - await this.createAndAddBookModel(bookPath); + let existingBook = this.books.find(book => book.bookPath === bookPath); + if (existingBook?.bookItems.length > 0) { + this.currentBook = existingBook; + } else { + await this.createAndAddBookModel(bookPath, isNotebook); let bookViewer = vscode.window.createTreeView(this.viewId, { showCollapseAll: true, treeDataProvider: this }); - this.currentBook = this.books.filter(book => book.bookPath === bookPath)[0]; + this.currentBook = this.books.find(book => book.bookPath === bookPath); bookViewer.reveal(this.currentBook.bookItems[0], { expand: vscode.TreeItemCollapsibleState.Expanded, focus: true, select: true }); + } + + if (showPreview) { await this.showPreviewFile(urlToOpen); } + // add file watcher on toc file. - fsw.watch(path.join(bookPath, '_data', 'toc.yml'), async (event, filename) => { - if (event === 'change') { - let index = this.books.findIndex(book => book.bookPath === bookPath); - await this.books[index].initializeContents().then(() => { - this._onDidChangeTreeData.fire(this.books[index].bookItems[0]); - }); - this._onDidChangeTreeData.fire(); - } - }); + if (!isNotebook) { + fsw.watch(path.join(this.currentBook.bookPath, '_data', 'toc.yml'), async (event, filename) => { + if (event === 'change') { + let changedBook = this.books.find(book => book.bookPath === bookPath); + await changedBook.initializeContents().then(() => { + this._onDidChangeTreeData.fire(changedBook.bookItems[0]); + }); + this._onDidChangeTreeData.fire(); + } + }); + } } catch (e) { vscode.window.showErrorMessage(loc.openFileError(bookPath, e instanceof Error ? e.message : e)); } @@ -131,7 +145,9 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider b.bookPath.replace(/\\/g, '/') === book.root)); + let targetPath = book.book.type === BookTreeItemType.Book ? book.root : book.book.contentPath; + let targetBook = this.books.find(b => b.bookPath === targetPath); + let index: number = this.books.indexOf(targetBook); if (index > -1) { deletedBook = this.books.splice(index, 1)[0]; if (this.currentBook === deletedBook) { @@ -143,7 +159,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider { - const book: BookModel = new BookModel(bookPath, this._openAsUntitled, this._extensionContext); + private async createAndAddBookModel(bookPath: string, isNotebook: boolean): Promise { + const book: BookModel = new BookModel(bookPath, this._openAsUntitled, isNotebook, this._extensionContext); await book.initializeContents(); this.books.push(book); if (!this.currentBook) { @@ -271,7 +287,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider 0) { let pickedFolder = uris[0]; @@ -328,10 +344,58 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider 0) { let bookPath = uris[0]; - await this.openBook(bookPath.fsPath); + await this.openBook(bookPath.fsPath, undefined, true); } } + public async openNotebookFolder(): Promise { + const allFilesFilter = loc.allFiles; + let filter: any = {}; + filter[allFilesFilter] = '*'; + let uris = await vscode.window.showOpenDialog({ + filters: filter, + canSelectFiles: false, + canSelectMany: false, + canSelectFolders: true, + openLabel: loc.labelSelectFolder + }); + if (uris && uris.length > 0) { + await this.loadNotebooksInFolder(uris[0]?.fsPath); + } + } + + private async loadNotebooksInFolder(folderPath: string) { + let bookCollection = await this.getNotebooksInTree(folderPath); + for (let i = 0; i < bookCollection.bookPaths.length; i++) { + await this.openBook(bookCollection.bookPaths[i], undefined, false); + } + for (let i = 0; i < bookCollection.notebookPaths.length; i++) { + await this.openBook(bookCollection.notebookPaths[i], undefined, false, true); + } + } + + private async getNotebooksInTree(folderPath: string): Promise { + let notebookConfig = vscode.workspace.getConfiguration(constants.notebookConfigKey); + let maxDepth = notebookConfig[constants.maxBookSearchDepth]; + // Use default value if user enters an invalid value + if (isNullOrUndefined(maxDepth) || maxDepth < 0) { + maxDepth = 10; + } else if (maxDepth === 0) { // No limit of search depth if user enters 0 + maxDepth = undefined; + } + + let escapedPath = glob.escapePath(folderPath.replace(/\\/g, '/')); + let bookFilter = path.posix.join(escapedPath, '**', '_data', 'toc.yml'); + let bookPaths = await glob(bookFilter, { deep: maxDepth }); + let tocTrimLength = '/_data/toc.yml'.length * -1; + bookPaths = bookPaths.map(path => path.slice(0, tocTrimLength)); + + let notebookFilter = path.posix.join(escapedPath, '**', '*.ipynb'); + let notebookPaths = await glob(notebookFilter, { ignore: bookPaths.map(path => glob.escapePath(path) + '/**/*.ipynb'), deep: maxDepth }); + + return { notebookPaths: notebookPaths, bookPaths: bookPaths }; + } + private runThrottledAction(resource: string, action: () => void) { const isResourceChange = resource !== this._resource; if (isResourceChange) { @@ -378,11 +442,11 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider { - booksitems = booksitems.concat(book.bookItems); + bookItems = bookItems.concat(book.bookItems); }); - return Promise.resolve(booksitems); + return Promise.resolve(bookItems); } } diff --git a/extensions/notebook/src/common/localizedConstants.ts b/extensions/notebook/src/common/localizedConstants.ts index e4e2fd3015..6e98403a58 100644 --- a/extensions/notebook/src/common/localizedConstants.ts +++ b/extensions/notebook/src/common/localizedConstants.ts @@ -15,7 +15,7 @@ export const msgSampleCodeDataFrame = localize('msgSampleCodeDataFrame', "This s // Book view-let constants export const allFiles = localize('allFiles', "All Files"); -export const labelPickFolder = localize('labelPickFolder', "Pick Folder"); +export const labelSelectFolder = localize('labelSelectFolder', "Select Folder"); export const labelBookFolder = localize('labelBookFolder', "Select Book"); export const confirmReplace = localize('confirmReplace', "Folder already exists. Are you sure you want to delete and replace this folder?"); export const openNotebookCommand = localize('openNotebookCommand', "Open Notebook"); @@ -25,7 +25,7 @@ export const msgBookTrusted = localize('msgBookTrusted', "Book is now trusted in export const msgBookAlreadyTrusted = localize('msgBookAlreadyTrusted', "Book is already trusted in this workspace."); export const msgBookUntrusted = localize('msgBookUntrusted', "Book is no longer trusted in this workspace"); export const msgBookAlreadyUntrusted = localize('msgBookAlreadyUntrusted', "Book is already untrusted in this workspace."); -export const missingTocError = localize('bookInitializeFailed', "Failed to find a toc.yml."); +export const missingTocError = localize('bookInitializeFailed', "Failed to find a Table of Contents file in the specified book."); export function missingFileError(title: string): string { return localize('missingFileError', "Missing file : {0}", title); } export function invalidTocFileError(): string { return localize('InvalidError.tocFile', "Invalid toc file"); } diff --git a/extensions/notebook/src/extension.ts b/extensions/notebook/src/extension.ts index 9ba7072a22..ba51586f75 100644 --- a/extensions/notebook/src/extension.ts +++ b/extensions/notebook/src/extension.ts @@ -30,7 +30,7 @@ type ChooseCellType = { label: string, id: CellType }; export async function activate(extensionContext: vscode.ExtensionContext): Promise { 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 ? untitledBookTreeViewProvider.openBook(bookPath, urlToOpen) : bookTreeViewProvider.openBook(bookPath, urlToOpen))); + extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openBook', (bookPath: string, openAsUntitled: boolean, urlToOpen?: string) => openAsUntitled ? untitledBookTreeViewProvider.openBook(bookPath, urlToOpen, true) : bookTreeViewProvider.openBook(bookPath, urlToOpen, true))); extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openNotebook', (resource) => bookTreeViewProvider.openNotebook(resource))); extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openUntitledNotebook', (resource) => untitledBookTreeViewProvider.openNotebookAsUntitled(resource))); extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openMarkdown', (resource) => bookTreeViewProvider.openMarkdown(resource))); @@ -41,6 +41,8 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.searchUntitledBook', () => untitledBookTreeViewProvider.searchJupyterBooks())); extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.openBook', () => bookTreeViewProvider.openNewBook())); extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.closeBook', (book: any) => bookTreeViewProvider.closeBook(book))); + extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.closeNotebook', (book: any) => bookTreeViewProvider.closeBook(book))); + extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.openNotebookFolder', () => bookTreeViewProvider.openNotebookFolder())); extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.createBook', async () => { let untitledFileName: vscode.Uri = vscode.Uri.parse(`untitled:${createBookPath}`); diff --git a/extensions/notebook/src/test/book/book.test.ts b/extensions/notebook/src/test/book/book.test.ts index ba0a70929f..38c1b938ef 100644 --- a/extensions/notebook/src/test/book/book.test.ts +++ b/extensions/notebook/src/test/book/book.test.ts @@ -235,10 +235,9 @@ describe('BookTreeViewProviderTests', function () { }); it('should ignore toc.yml files not in _data folder', async () => { - await bookTreeViewProvider.currentBook.getTableOfContentFiles(rootFolderPath); - for (let p of bookTreeViewProvider.currentBook.tableOfContentPaths) { - should(p.toLocaleLowerCase()).equal(tableOfContentsFile.replace(/\\/g, '/').toLocaleLowerCase()); - } + await bookTreeViewProvider.currentBook.loadTableOfContentFiles(rootFolderPath); + let path = bookTreeViewProvider.currentBook.tableOfContentsPath; + should(path.toLocaleLowerCase()).equal(tableOfContentsFile.replace(/\\/g, '/').toLocaleLowerCase()); }); this.afterAll(async function (): Promise { diff --git a/extensions/notebook/src/test/book/bookTrustManager.test.ts b/extensions/notebook/src/test/book/bookTrustManager.test.ts index 976125a38d..5dd695d95d 100644 --- a/extensions/notebook/src/test/book/bookTrustManager.test.ts +++ b/extensions/notebook/src/test/book/bookTrustManager.test.ts @@ -53,6 +53,7 @@ describe('BookTrustManagerTests', function () { // Mock Book Data let bookTreeItemFormat1: BookTreeItemFormat = { + contentPath: undefined, root: '/temp/SubFolder/', tableOfContents: { sections: [ @@ -72,6 +73,7 @@ describe('BookTrustManagerTests', function () { }; let bookTreeItemFormat2: BookTreeItemFormat = { + contentPath: undefined, root: '/temp/SubFolder2/', tableOfContents: { sections: [ @@ -88,6 +90,7 @@ describe('BookTrustManagerTests', function () { }; let bookTreeItemFormat3: BookTreeItemFormat = { + contentPath: undefined, root: '/temp2/SubFolder3/', tableOfContents: { sections: [ @@ -206,6 +209,7 @@ describe('BookTrustManagerTests', function () { apiWrapperMock.setup(api => api.getWorkspaceFolders()).returns(() => []); apiWrapperMock.setup(api => api.getConfiguration(TypeMoq.It.isValue(constants.notebookConfigKey))).returns(() => workspaceConfigurtionMock.object); let bookTreeItemFormat1: BookTreeItemFormat = { + contentPath: undefined, root: '/temp/SubFolder/', tableOfContents: { sections: [ @@ -225,6 +229,7 @@ describe('BookTrustManagerTests', function () { }; let bookTreeItemFormat2: BookTreeItemFormat = { + contentPath: undefined, root: '/temp/SubFolder2/', tableOfContents: { sections: [ diff --git a/src/sql/workbench/contrib/notebook/browser/notebook.component.ts b/src/sql/workbench/contrib/notebook/browser/notebook.component.ts index 2e0bbabcef..d9742845e3 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebook.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebook.component.ts @@ -538,7 +538,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe } isActive(): boolean { - return this.editorService.activeEditor.matches(this.notebookParams.input); + return this.editorService.activeEditor ? this.editorService.activeEditor.matches(this.notebookParams.input) : false; } isVisible(): boolean {