diff --git a/extensions/mssql/src/dashboard/bookExtensions.ts b/extensions/mssql/src/dashboard/bookExtensions.ts index 05c0bfa677..50cf641a10 100644 --- a/extensions/mssql/src/dashboard/bookExtensions.ts +++ b/extensions/mssql/src/dashboard/bookExtensions.ts @@ -124,8 +124,8 @@ class AzdataExtensionBookContributionProvider extends Disposable implements Book this.contributions.map(book => { let bookName: string = path.basename(book.path); vscode.commands.executeCommand('setContext', bookName, true); - vscode.commands.registerCommand('books.' + bookName, async (context) => { - vscode.commands.executeCommand('bookTreeView.openBook', book.path, true); + vscode.commands.registerCommand('books.' + bookName, async (urlToOpen?: string) => { + vscode.commands.executeCommand('bookTreeView.openBook', book.path, true, urlToOpen); }); }); } diff --git a/extensions/notebook/src/book/bookTreeItem.ts b/extensions/notebook/src/book/bookTreeItem.ts index 6895e10ae0..fc0600239d 100644 --- a/extensions/notebook/src/book/bookTreeItem.ts +++ b/extensions/notebook/src/book/bookTreeItem.ts @@ -7,6 +7,7 @@ import * as vscode from 'vscode'; import * as path from 'path'; import * as fs from 'fs'; import * as nls from 'vscode-nls'; +import { IJupyterBookSection, IJupyterBookToc } from '../contracts/content'; const localize = nls.loadMessageBundle(); export enum BookTreeItemType { @@ -19,14 +20,14 @@ export enum BookTreeItemType { export interface BookTreeItemFormat { title: string; root: string; - tableOfContents: any[]; + tableOfContents: IJupyterBookToc; page: any; type: BookTreeItemType; treeItemCollapsibleState: number; } export class BookTreeItem extends vscode.TreeItem { - private _sections: any[]; + private _sections: IJupyterBookSection[]; private _uri: string; private _previousUri: string; private _nextUri: string; @@ -54,7 +55,7 @@ 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.indexOf(this.book.page)); + let index = (this.book.tableOfContents.sections.indexOf(this.book.page)); this.setPreviousUri(index); this.setNextUri(index); } @@ -74,9 +75,9 @@ export class BookTreeItem extends vscode.TreeItem { private setPreviousUri(index: number): void { let i = --index; while (i > -1) { - if (this.book.tableOfContents[i].url) { + if (this.book.tableOfContents.sections[i].url) { // TODO: Currently only navigating to notebooks. Need to add logic for markdown. - let pathToNotebook = path.join(this.book.root, 'content', this.book.tableOfContents[i].url.concat('.ipynb')); + let pathToNotebook = path.join(this.book.root, 'content', this.book.tableOfContents.sections[i].url.concat('.ipynb')); if (fs.existsSync(pathToNotebook)) { this._previousUri = pathToNotebook; return; @@ -88,10 +89,10 @@ export class BookTreeItem extends vscode.TreeItem { private setNextUri(index: number): void { let i = ++index; - while (i < this.book.tableOfContents.length) { - if (this.book.tableOfContents[i].url) { + while (i < this.book.tableOfContents.sections.length) { + if (this.book.tableOfContents.sections[i].url) { // TODO: Currently only navigating to notebooks. Need to add logic for markdown. - let pathToNotebook = path.join(this.book.root, 'content', this.book.tableOfContents[i].url.concat('.ipynb')); + let pathToNotebook = path.join(this.book.root, 'content', this.book.tableOfContents.sections[i].url.concat('.ipynb')); if (fs.existsSync(pathToNotebook)) { this._nextUri = pathToNotebook; return; @@ -113,7 +114,7 @@ export class BookTreeItem extends vscode.TreeItem { return this.book.root; } - public get tableOfContents(): any[] { + public get tableOfContents(): IJupyterBookToc { return this.book.tableOfContents; } @@ -137,4 +138,29 @@ export class BookTreeItem extends vscode.TreeItem { return undefined; } } + + /** + * Helper method to find a child section with a specified URL + * @param url The url of the section we're searching for + */ + public findChildSection(url?: string): IJupyterBookSection | undefined { + if (!url) { + return undefined; + } + return this.findChildSectionRecur(this, url); + } + + private findChildSectionRecur(section: IJupyterBookSection, url: string): IJupyterBookSection | undefined { + if (section.url && section.url === url) { + return section; + } else if (section.sections) { + for (const childSection of section.sections) { + const foundSection = this.findChildSectionRecur(childSection, url); + if (foundSection) { + return foundSection; + } + } + } + return undefined; + } } diff --git a/extensions/notebook/src/book/bookTreeView.ts b/extensions/notebook/src/book/bookTreeView.ts index 4cf6e3ce4b..a1de4a70ad 100644 --- a/extensions/notebook/src/book/bookTreeView.ts +++ b/extensions/notebook/src/book/bookTreeView.ts @@ -14,6 +14,7 @@ import { maxBookSearchDepth, notebookConfigKey } from '../common/constants'; import { isEditorTitleFree } from '../common/utils'; import * as nls from 'vscode-nls'; import { promisify } from 'util'; +import { IJupyterBookToc, IJupyterBookSection } from '../contracts/content'; const localize = nls.loadMessageBundle(); const existsAsync = promisify(fs.exists); @@ -72,7 +73,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider { + async openBook(bookPath: string, openAsUntitled: boolean, urlToOpen?: string): Promise { try { // Check if the book is already open in viewlet. if (this._tableOfContentPaths.indexOf(path.join(bookPath, '_data', 'toc.yml').replace(/\\/g, '/')) > -1 && this._allNotebooks.size > 0) { @@ -80,21 +81,24 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider 0) { - bookViewer.reveal(books[0], { expand: vscode.TreeItemCollapsibleState.Expanded, focus: true, select: true }); - const readmeMarkdown: string = path.join(bookPath, 'content', books[0].tableOfContents[0].url.concat('.md')); - const readmeNotebook: string = path.join(bookPath, 'content', books[0].tableOfContents[0].url.concat('.ipynb')); - const markdownExists = await existsAsync(readmeMarkdown); - const notebookExists = await existsAsync(readmeNotebook); + const rootTreeItem = books[0]; + const sectionToOpen = rootTreeItem.findChildSection(urlToOpen); + bookViewer.reveal(rootTreeItem, { expand: vscode.TreeItemCollapsibleState.Expanded, focus: true, select: true }); + const urlPath = sectionToOpen ? sectionToOpen.url : rootTreeItem.tableOfContents.sections[0].url; + const sectionToOpenMarkdown: string = path.join(bookPath, 'content', urlPath.concat('.md')); + const sectionToOpenNotebook: string = path.join(bookPath, 'content', urlPath.concat('.ipynb')); + const markdownExists = await existsAsync(sectionToOpenMarkdown); + const notebookExists = await existsAsync(sectionToOpenNotebook); if (markdownExists) { - vscode.commands.executeCommand('markdown.showPreview', vscode.Uri.file(readmeMarkdown)); + vscode.commands.executeCommand('markdown.showPreview', vscode.Uri.file(sectionToOpenMarkdown)); } else if (notebookExists) { - vscode.workspace.openTextDocument(readmeNotebook); + this.openNotebook(sectionToOpenNotebook); } } } @@ -196,12 +200,12 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider Array.isArray(val.sections) ? acc.concat(val).concat(this.flattenArray(val.sections, title)) : acc.concat(val), []); - } catch (e) { - throw localize('Invalid toc.yml', 'Error: {0} has an incorrect toc.yml file', title); - } + /** + * Recursively parses out a section of a Jupyter Book. + * @param array The input data to parse + */ + private parseJupyterSection(array: any[]): IJupyterBookSection[] { + return array.reduce((acc, val) => Array.isArray(val.sections) ? acc.concat(val).concat(this.parseJupyterSection(val.sections)) : acc.concat(val), []); } public getBooks(): BookTreeItem[] { @@ -211,20 +215,24 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider bookTreeViewProvider.openBook(resource, extensionContext, openAsReadonly))); - extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openNotebookAsUntitled', (resource) => bookTreeViewProvider.openNotebookAsUntitled(resource))); - extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openNotebook', (resource) => bookTreeViewProvider.openNotebook(resource))); - extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openMarkdown', (resource) => bookTreeViewProvider.openMarkdown(resource))); - extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openExternalLink', (resource) => bookTreeViewProvider.openExternalLink(resource))); + extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openBook', (bookPath: string, openAsUntitled: boolean, urlToOpen?: string) => bookTreeViewProvider.openBook(bookPath, openAsUntitled, urlToOpen))); + extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openNotebookAsUntitled', (resource: string) => bookTreeViewProvider.openNotebookAsUntitled(resource))); + extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openNotebook', (resource: string) => bookTreeViewProvider.openNotebook(resource))); + extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openMarkdown', (resource: string) => bookTreeViewProvider.openMarkdown(resource))); + extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openExternalLink', (resource: string) => bookTreeViewProvider.openExternalLink(resource))); extensionContext.subscriptions.push(vscode.commands.registerCommand('_notebook.command.new', (context?: azdata.ConnectedContext) => { let connectionProfile: azdata.IConnectionProfile = undefined; diff --git a/extensions/notebook/src/test/book/book.test.ts b/extensions/notebook/src/test/book/book.test.ts index 191f906aff..92910def71 100644 --- a/extensions/notebook/src/test/book/book.test.ts +++ b/extensions/notebook/src/test/book/book.test.ts @@ -285,7 +285,7 @@ describe.skip('BookTreeViewProviderTests', function() { it('should show error if notebook or markdown file is missing', function(): void { let books = bookTreeViewProvider.getBooks(); - let children = bookTreeViewProvider.getSections([], books[0].sections, rootFolderPath); + let children = bookTreeViewProvider.getSections({ sections: [] }, books[0].sections, rootFolderPath); should(bookTreeViewProvider.errorMessage).equal('Missing file : Notebook1'); // Rest of book should be detected correctly even with a missing file equalBookItems(children[0], expectedNotebook2);