diff --git a/extensions/mssql/src/dashboard/bookWidget.ts b/extensions/mssql/src/dashboard/bookWidget.ts
index 72dd35a144..2bd5270ca3 100644
--- a/extensions/mssql/src/dashboard/bookWidget.ts
+++ b/extensions/mssql/src/dashboard/bookWidget.ts
@@ -70,5 +70,5 @@ export function registerBooksWidget(bookContributionProvider: BookContributionPr
}
function openBookViewlet(folderUri: vscode.Uri): void {
- vscode.commands.executeCommand('bookTreeView.openBook', folderUri.fsPath, true);
+ vscode.commands.executeCommand('bookTreeView.openBook', folderUri.fsPath, true, undefined);
}
diff --git a/extensions/notebook/package.json b/extensions/notebook/package.json
index 1282b788d1..7f9150bbc9 100644
--- a/extensions/notebook/package.json
+++ b/extensions/notebook/package.json
@@ -139,6 +139,14 @@
"command": "books.sqlserver2019",
"title": "%title.SQL19PreviewBook%",
"category": "%books-preview-category%"
+ },
+ {
+ "command": "notebook.command.saveBook",
+ "title": "%title.saveJupyterBook%",
+ "icon": {
+ "dark": "resources/dark/save_inverse.svg",
+ "light": "resources/light/save.svg"
+ }
}
],
"languages": [
@@ -244,6 +252,13 @@
"group": "1notebook@1"
}
],
+ "view/item/context": [
+ {
+ "command": "notebook.command.saveBook",
+ "when": "view == untitledBookTreeView && viewItem == untitledBook && untitledBooks && notebookQuality != stable",
+ "group": "inline"
+ }
+ ],
"notebook/toolbar": [
{
"command": "jupyter.cmd.managePackages",
@@ -354,8 +369,11 @@
"books-explorer": [
{
"id": "bookTreeView",
- "name": "Books",
- "when": "notebookQuality != stable"
+ "name": "%title.SavedBooks%"
+ },
+ {
+ "id": "untitledBookTreeView",
+ "name": "%title.UntitledBooks%"
}
]
}
diff --git a/extensions/notebook/package.nls.json b/extensions/notebook/package.nls.json
index 10b64cb3f1..38a2fbdd3f 100644
--- a/extensions/notebook/package.nls.json
+++ b/extensions/notebook/package.nls.json
@@ -28,5 +28,8 @@
"title.configurePython": "Configure Python for Notebooks",
"title.managePackages": "Manage Packages",
"title.SQL19PreviewBook": "SQL Server 2019 Guide",
- "books-preview-category": "Jupyter Books"
+ "books-preview-category": "Jupyter Books",
+ "title.saveJupyterBook": "Save Book",
+ "title.SavedBooks": "Saved Books",
+ "title.UntitledBooks": "Untitled Books"
}
diff --git a/extensions/notebook/resources/dark/save_inverse.svg b/extensions/notebook/resources/dark/save_inverse.svg
new file mode 100644
index 0000000000..9bb41fe118
--- /dev/null
+++ b/extensions/notebook/resources/dark/save_inverse.svg
@@ -0,0 +1,5 @@
+
diff --git a/extensions/notebook/resources/light/save.svg b/extensions/notebook/resources/light/save.svg
new file mode 100644
index 0000000000..e3392d6b96
--- /dev/null
+++ b/extensions/notebook/resources/light/save.svg
@@ -0,0 +1,5 @@
+
diff --git a/extensions/notebook/src/book/bookModel.ts b/extensions/notebook/src/book/bookModel.ts
new file mode 100644
index 0000000000..bf9fecb3f9
--- /dev/null
+++ b/extensions/notebook/src/book/bookModel.ts
@@ -0,0 +1,231 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+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 nls from 'vscode-nls';
+import { IJupyterBookToc, IJupyterBookSection } from '../contracts/content';
+import { isNullOrUndefined } from 'util';
+
+const localize = nls.loadMessageBundle();
+const fsPromises = fileServices.promises;
+
+export class BookModel implements azdata.nb.NavigationProvider {
+ private _bookItems: BookTreeItem[];
+ private _allNotebooks = new Map();
+ private _tableOfContentPaths: string[] = [];
+ readonly providerId: string = 'BookNavigator';
+
+ constructor(public bookPath: string, public openAsUntitled: boolean, private _extensionContext: vscode.ExtensionContext) {
+ this.bookPath = bookPath;
+ this.openAsUntitled = openAsUntitled;
+ this._bookItems = [];
+ this._extensionContext.subscriptions.push(azdata.nb.registerNavigationProvider(this));
+ }
+
+ public async initializeContents(): Promise {
+ await this.getTableOfContentFiles(this.bookPath);
+ await this.readBooks();
+ }
+
+ public getAllBooks(): Map {
+ return this._allNotebooks;
+ }
+
+ public async getTableOfContentFiles(workspacePath: string): Promise {
+ try {
+ 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;
+ }
+
+ let p = path.join(workspacePath, '**', '_data', 'toc.yml').replace(/\\/g, '/');
+ let tableOfContentPaths = await glob(p, { deep: maxDepth });
+ this._tableOfContentPaths = this._tableOfContentPaths.concat(tableOfContentPaths);
+ let bookOpened: boolean = this._tableOfContentPaths.length > 0;
+ vscode.commands.executeCommand('setContext', 'bookOpened', bookOpened);
+ } catch (error) {
+ console.log(error);
+ }
+ }
+
+ public async readBooks(): Promise {
+ for (const contentPath of this._tableOfContentPaths) {
+ let root = path.dirname(path.dirname(contentPath));
+ 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');
+ const tableOfContents = yaml.safeLoad(fileContents.toString());
+ let book = new BookTreeItem({
+ title: config.title,
+ root: root,
+ tableOfContents: { sections: this.parseJupyterSections(tableOfContents) },
+ page: tableOfContents,
+ type: BookTreeItemType.Book,
+ treeItemCollapsibleState: vscode.TreeItemCollapsibleState.Expanded,
+ isUntitled: this.openAsUntitled,
+ },
+ {
+ light: this._extensionContext.asAbsolutePath('resources/light/book.svg'),
+ dark: this._extensionContext.asAbsolutePath('resources/dark/book_inverse.svg')
+ }
+ );
+ this._bookItems.push(book);
+ } catch (e) {
+ let error = e instanceof Error ? e.message : e;
+ vscode.window.showErrorMessage(error);
+ }
+ }
+ return this._bookItems;
+ }
+
+ public get bookItems(): BookTreeItem[] {
+ return this._bookItems;
+ }
+
+ public async getSections(tableOfContents: IJupyterBookToc, sections: IJupyterBookSection[], root: string): Promise {
+ let notebooks: BookTreeItem[] = [];
+ for (let i = 0; i < sections.length; i++) {
+ if (sections[i].url) {
+ if (sections[i].external) {
+ let externalLink = new BookTreeItem({
+ title: sections[i].title,
+ root: root,
+ tableOfContents: tableOfContents,
+ page: sections[i],
+ type: BookTreeItemType.ExternalLink,
+ treeItemCollapsibleState: vscode.TreeItemCollapsibleState.Collapsed,
+ isUntitled: this.openAsUntitled
+ },
+ {
+ light: this._extensionContext.asAbsolutePath('resources/light/link.svg'),
+ dark: this._extensionContext.asAbsolutePath('resources/dark/link_inverse.svg')
+ }
+ );
+
+ notebooks.push(externalLink);
+ } else {
+ let pathToNotebook = path.join(root, 'content', sections[i].url.concat('.ipynb'));
+ let pathToMarkdown = path.join(root, 'content', sections[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 (await fs.pathExists(pathToNotebook)) {
+ let notebook = new BookTreeItem({
+ title: sections[i].title,
+ root: root,
+ tableOfContents: tableOfContents,
+ page: sections[i],
+ type: BookTreeItemType.Notebook,
+ treeItemCollapsibleState: vscode.TreeItemCollapsibleState.Collapsed,
+ isUntitled: this.openAsUntitled
+ },
+ {
+ light: this._extensionContext.asAbsolutePath('resources/light/notebook.svg'),
+ dark: this._extensionContext.asAbsolutePath('resources/dark/notebook_inverse.svg')
+ }
+ );
+
+ if (this.openAsUntitled) {
+ if (!this._allNotebooks.get(path.basename(pathToNotebook))) {
+ this._allNotebooks.set(path.basename(pathToNotebook), notebook);
+ notebooks.push(notebook);
+ }
+ }
+ else {
+ if (!this._allNotebooks.get(pathToNotebook)) {
+ this._allNotebooks.set(pathToNotebook, notebook);
+ notebooks.push(notebook);
+ }
+ }
+ } else if (await fs.pathExists(pathToMarkdown)) {
+ let markdown = new BookTreeItem({
+ title: sections[i].title,
+ root: root,
+ tableOfContents: tableOfContents,
+ page: sections[i],
+ type: BookTreeItemType.Markdown,
+ treeItemCollapsibleState: vscode.TreeItemCollapsibleState.Collapsed,
+ isUntitled: this.openAsUntitled
+ },
+ {
+ light: this._extensionContext.asAbsolutePath('resources/light/markdown.svg'),
+ dark: this._extensionContext.asAbsolutePath('resources/dark/markdown_inverse.svg')
+ }
+ );
+ notebooks.push(markdown);
+ } else {
+ let error = localize('missingFileError', "Missing file : {0}", sections[i].title);
+ vscode.window.showErrorMessage(error);
+ }
+ }
+ } else {
+ // TODO: search functionality (#6160)
+ }
+ }
+ return notebooks;
+ }
+
+ /**
+ * Recursively parses out a section of a Jupyter Book.
+ * @param section The input data to parse
+ */
+ private parseJupyterSections(section: any[]): IJupyterBookSection[] {
+ try {
+ return section.reduce((acc, val) => Array.isArray(val.sections) ? acc.concat(val).concat(this.parseJupyterSections(val.sections)) : acc.concat(val), []);
+ } catch (error) {
+ let err: string = localize('InvalidError.tocFile', "{0}", error);
+ if (section.length > 0) {
+ err = localize('Invalid toc.yml', "Error: {0} has an incorrect toc.yml file", section[0].title); //need to find a way to get title.
+ }
+ vscode.window.showErrorMessage(err);
+ throw err;
+ }
+
+ }
+
+ public get tableOfContentPaths() {
+ return this._tableOfContentPaths;
+ }
+
+ getNavigation(uri: vscode.Uri): Thenable {
+ let notebook = !this.openAsUntitled ? this._allNotebooks.get(uri.fsPath) : this._allNotebooks.get(path.basename(uri.fsPath));
+ let result: azdata.nb.NavigationResult;
+ if (notebook) {
+ result = {
+ hasNavigation: true,
+ previous: notebook.previousUri ? this.openAsUntitled ? this.getPlatformSpecificUri(notebook.previousUri) : vscode.Uri.file(notebook.previousUri) : undefined,
+ next: notebook.nextUri ? this.openAsUntitled ? this.getPlatformSpecificUri(notebook.nextUri) : vscode.Uri.file(notebook.nextUri) : undefined
+ };
+ } else {
+ result = {
+ hasNavigation: false,
+ previous: undefined,
+ next: undefined
+ };
+ }
+ return Promise.resolve(result);
+ }
+
+ getPlatformSpecificUri(resource: string): vscode.Uri {
+ if (process.platform === 'win32') {
+ return vscode.Uri.parse(`untitled:${resource}`);
+ }
+ else {
+ return vscode.Uri.parse(resource).with({ scheme: 'untitled' });
+ }
+ }
+}
diff --git a/extensions/notebook/src/book/bookTreeItem.ts b/extensions/notebook/src/book/bookTreeItem.ts
index 426807bbd1..8ca075e00a 100644
--- a/extensions/notebook/src/book/bookTreeItem.ts
+++ b/extensions/notebook/src/book/bookTreeItem.ts
@@ -24,6 +24,7 @@ export interface BookTreeItemFormat {
page: any;
type: BookTreeItemType;
treeItemCollapsibleState: number;
+ isUntitled: boolean;
}
export class BookTreeItem extends vscode.TreeItem {
@@ -39,6 +40,9 @@ export class BookTreeItem extends vscode.TreeItem {
if (book.type === BookTreeItemType.Book) {
this.collapsibleState = book.treeItemCollapsibleState;
this._sections = book.page;
+ if (book.isUntitled) {
+ this.contextValue = 'untitledBook';
+ }
} else {
this.setPageVariables();
this.setCommand();
@@ -63,12 +67,12 @@ export class BookTreeItem extends vscode.TreeItem {
private setCommand() {
if (this.book.type === BookTreeItemType.Notebook) {
let pathToNotebook = path.join(this.book.root, 'content', this._uri.concat('.ipynb'));
- this.command = { command: 'bookTreeView.openNotebook', title: localize('openNotebookCommand', 'Open Notebook'), arguments: [pathToNotebook], };
+ this.command = { command: this.book.isUntitled ? 'bookTreeView.openUntitledNotebook' : 'bookTreeView.openNotebook', title: localize('openNotebookCommand', "Open Notebook"), arguments: [pathToNotebook], };
} 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: localize('openMarkdownCommand', 'Open Markdown'), arguments: [pathToMarkdown], };
+ this.command = { command: 'bookTreeView.openMarkdown', title: localize('openMarkdownCommand', "Open Markdown"), arguments: [pathToMarkdown], };
} else if (this.book.type === BookTreeItemType.ExternalLink) {
- this.command = { command: 'bookTreeView.openExternalLink', title: localize('openExternalLinkCommand', 'Open External Link'), arguments: [this._uri], };
+ this.command = { command: 'bookTreeView.openExternalLink', title: localize('openExternalLinkCommand', "Open External Link"), arguments: [this._uri], };
}
}
diff --git a/extensions/notebook/src/book/bookTreeView.ts b/extensions/notebook/src/book/bookTreeView.ts
index 5217f03e9f..3df4ec98fa 100644
--- a/extensions/notebook/src/book/bookTreeView.ts
+++ b/extensions/notebook/src/book/bookTreeView.ts
@@ -6,99 +6,75 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as path from 'path';
-import { promises as fs } from 'fs';
-import * as yaml from 'js-yaml';
-import * as glob from 'fast-glob';
-import { BookTreeItem, BookTreeItemType } from './bookTreeItem';
-import { maxBookSearchDepth, notebookConfigKey } from '../common/constants';
-import { isEditorTitleFree, exists } from '../common/utils';
+import * as fs from 'fs-extra';
+import { IPrompter, QuestionTypes, IQuestion } from '../prompts/question';
+import CodeAdapter from '../prompts/adapter';
+import { BookTreeItem } from './bookTreeItem';
import * as nls from 'vscode-nls';
-import { IJupyterBookToc, IJupyterBookSection } from '../contracts/content';
+import { isEditorTitleFree } from '../common/utils';
+import { BookModel } from './bookModel';
const localize = nls.loadMessageBundle();
-export class BookTreeViewProvider implements vscode.TreeDataProvider, azdata.nb.NavigationProvider {
- readonly providerId: string = 'BookNavigator';
+export class BookTreeViewProvider implements vscode.TreeDataProvider {
private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter();
readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event;
- private _tableOfContentPaths: string[] = [];
- private _allNotebooks = new Map();
- private _extensionContext: vscode.ExtensionContext;
private _throttleTimer: any;
private _resource: string;
+ private _extensionContext: vscode.ExtensionContext;
+ private prompter: IPrompter;
+
// For testing
private _errorMessage: string;
private _onReadAllTOCFiles: vscode.EventEmitter = new vscode.EventEmitter();
private _openAsUntitled: boolean;
+ public viewId: string;
+ public books: BookModel[];
+ public currentBook: BookModel;
+
+ constructor(workspaceFolders: vscode.WorkspaceFolder[], extensionContext: vscode.ExtensionContext, openAsUntitled: boolean, view: string) {
+ this._openAsUntitled = openAsUntitled;
+ this._extensionContext = extensionContext;
+ this.books = [];
+ this.initialize(workspaceFolders.map(a => a.uri.fsPath));
+ vscode.commands.executeCommand('setContext', 'untitledBooks', openAsUntitled);
+ this.viewId = view;
+ this.prompter = new CodeAdapter();
- constructor(workspaceFolders: vscode.WorkspaceFolder[], extensionContext: vscode.ExtensionContext) {
- this.initialize(workspaceFolders, null, extensionContext);
}
- private initialize(workspaceFolders: vscode.WorkspaceFolder[], bookPath: string, context: vscode.ExtensionContext): void {
- let workspacePaths: string[] = [];
- if (bookPath) {
- workspacePaths.push(bookPath);
- }
- else if (workspaceFolders) {
- workspacePaths = workspaceFolders.map(a => a.uri.fsPath);
- }
- this.getTableOfContentFiles(workspacePaths).then(() => undefined, (err) => { console.log(err); });
- this._extensionContext = context;
+ private async initialize(bookPaths: string[]): Promise {
+ return Promise.all(bookPaths.map(async (bookPath) => {
+ let book: BookModel = new BookModel(bookPath, this._openAsUntitled, this._extensionContext);
+ await book.initializeContents();
+ this.books.push(book);
+ if (!this.currentBook) {
+ this.currentBook = book;
+ }
+ }));
}
public get onReadAllTOCFiles(): vscode.Event {
return this._onReadAllTOCFiles.event;
}
- async getTableOfContentFiles(workspacePaths: string[]): Promise {
- let notebookConfig = vscode.workspace.getConfiguration(notebookConfigKey);
- let maxDepth = notebookConfig[maxBookSearchDepth];
- // Use default value if user enters an invalid value
- if (maxDepth === undefined || maxDepth < 0) {
- maxDepth = 5;
- } else if (maxDepth === 0) { // No limit of search depth if user enters 0
- maxDepth = undefined;
- }
- for (let workspacePath of workspacePaths) {
- let p = path.join(workspacePath, '**', '_data', 'toc.yml').replace(/\\/g, '/');
- let tableOfContentPaths = await glob(p, { deep: maxDepth });
- this._tableOfContentPaths = this._tableOfContentPaths.concat(tableOfContentPaths);
- }
- let bookOpened: boolean = this._tableOfContentPaths.length > 0;
- vscode.commands.executeCommand('setContext', 'bookOpened', bookOpened);
- this._onReadAllTOCFiles.fire();
- }
- async openBook(bookPath: string, openAsUntitled: boolean, urlToOpen?: string): Promise {
+
+ async openBook(bookPath: string, urlToOpen?: string): Promise {
try {
+ let books: BookModel[] = this.books.filter(book => book.bookPath === bookPath) || [];
// 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) {
- vscode.commands.executeCommand('workbench.books.action.focusBooksExplorer');
+ if (books.length > 0 && books[0].bookItems) {
+ this.currentBook = books[0];
+ this.showPreviewFile(urlToOpen);
}
else {
- await this.getTableOfContentFiles([bookPath]);
- const bookViewer = vscode.window.createTreeView('bookTreeView', { showCollapseAll: true, treeDataProvider: this });
- await vscode.commands.executeCommand('workbench.books.action.focusBooksExplorer');
- this._openAsUntitled = openAsUntitled;
- let books = await this.getBooks();
- if (books && books.length > 0) {
- 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 exists(sectionToOpenMarkdown);
- const notebookExists = await exists(sectionToOpenNotebook);
- if (markdownExists) {
- vscode.commands.executeCommand('markdown.showPreview', vscode.Uri.file(sectionToOpenMarkdown));
- }
- else if (notebookExists) {
- this.openNotebook(sectionToOpenNotebook);
- }
- }
+ await this.initialize([bookPath]);
+ let bookViewer = vscode.window.createTreeView(this.viewId, { showCollapseAll: true, treeDataProvider: this });
+ this.currentBook = this.books.filter(book => book.bookPath === bookPath)[0];
+ bookViewer.reveal(this.currentBook.bookItems[0], { expand: vscode.TreeItemCollapsibleState.Expanded, focus: true, select: true });
+ this.showPreviewFile(urlToOpen);
}
} catch (e) {
vscode.window.showErrorMessage(localize('openBookError', "Open book {0} failed: {1}",
@@ -107,6 +83,22 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider {
+ if (this.currentBook) {
+ const bookRoot = this.currentBook.bookItems[0];
+ const sectionToOpen = bookRoot.findChildSection(urlToOpen);
+ const urlPath = sectionToOpen ? sectionToOpen.url : bookRoot.tableOfContents.sections[0].url;
+ const sectionToOpenMarkdown: string = path.join(this.currentBook.bookPath, 'content', urlPath.concat('.md'));
+ const sectionToOpenNotebook: string = path.join(this.currentBook.bookPath, 'content', urlPath.concat('.ipynb'));
+ if (await fs.pathExists(sectionToOpenMarkdown)) {
+ this.openMarkdown(sectionToOpenMarkdown);
+ }
+ else if (await fs.pathExists(sectionToOpenNotebook)) {
+ this.openNotebook(sectionToOpenNotebook);
+ }
+ }
+ }
+
async openNotebook(resource: string): Promise {
try {
if (this._openAsUntitled) {
@@ -117,7 +109,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider {
+ if (this.currentBook.bookPath) {
+ const allFilesFilter = localize('allFiles', "All Files");
+ let filter: any = {};
+ filter[allFilesFilter] = '*';
+ let uris = await vscode.window.showOpenDialog({
+ filters: filter,
+ canSelectFiles: false,
+ canSelectMany: false,
+ canSelectFolders: true,
+ openLabel: localize('labelPickFolder', "Pick Folder")
+ });
+ if (uris && uris.length > 0) {
+ let pickedFolder = uris[0];
+ let destinationUri: vscode.Uri = vscode.Uri.file(path.join(pickedFolder.fsPath, path.basename(this.currentBook.bookPath)));
+ if (destinationUri) {
+ if (await fs.pathExists(destinationUri.fsPath)) {
+ let doReplace = await this.confirmReplace();
+ if (!doReplace) {
+ return undefined;
+ }
+ else {
+ //remove folder if exists
+ await fs.remove(destinationUri.fsPath);
+ }
+ }
+ //make directory for each contribution book.
+ await fs.mkdir(destinationUri.fsPath);
+ await fs.copy(this.currentBook.bookPath, destinationUri.fsPath);
+
+ //remove book from the untitled books and open it from Saved books
+ let untitledBookIndex: number = this.books.indexOf(this.currentBook);
+ if (untitledBookIndex > -1) {
+ this.books.splice(untitledBookIndex, 1);
+ this.currentBook = undefined;
+ this._onDidChangeTreeData.fire();
+ vscode.commands.executeCommand('bookTreeView.openBook', destinationUri.fsPath, false, undefined);
+ }
+ }
+ }
+ }
+
+ }
+
private runThrottledAction(resource: string, action: () => void) {
const isResourceChange = resource !== this._resource;
if (isResourceChange) {
@@ -189,168 +225,58 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider {
if (element) {
if (element.sections) {
- return Promise.resolve(this.getSections(element.tableOfContents, element.sections, element.root));
+ return Promise.resolve(this.currentBook.getSections(element.tableOfContents, element.sections, element.root).then(sections => { return sections; }));
} else {
return Promise.resolve([]);
}
} else {
- return Promise.resolve(this.getBooks());
+ let booksitems: BookTreeItem[] = [];
+ this.books.map(book => {
+ booksitems = booksitems.concat(book.bookItems);
+ });
+ return Promise.resolve(booksitems);
}
}
- /**
- * 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 async getBooks(): Promise {
- let books: BookTreeItem[] = [];
- for (const contentPath of this._tableOfContentPaths) {
- let root = path.dirname(path.dirname(contentPath));
- try {
- const config = yaml.safeLoad((await fs.readFile(path.join(root, '_config.yml'), 'utf-8')).toString());
- const tableOfContents = yaml.safeLoad((await fs.readFile(contentPath, 'utf-8')).toString());
- try {
- let book = new BookTreeItem({
- title: config.title,
- root: root,
- tableOfContents: { sections: this.parseJupyterSection(tableOfContents) },
- page: tableOfContents,
- type: BookTreeItemType.Book,
- treeItemCollapsibleState: vscode.TreeItemCollapsibleState.Expanded,
- },
- {
- light: this._extensionContext.asAbsolutePath('resources/light/book.svg'),
- dark: this._extensionContext.asAbsolutePath('resources/dark/book_inverse.svg')
- }
- );
- books.push(book);
- } catch (e) {
- throw Error(localize('invalidTocError', "Error: {0} has an incorrect toc.yml file. {1}", config.title, e instanceof Error ? e.message : e));
+ getParent(element?: BookTreeItem): vscode.ProviderResult {
+ if (element) {
+ let parentPath;
+ if (element.root.endsWith('.md')) {
+ parentPath = path.join(this.currentBook.bookPath, 'content', 'readme.md');
+ if (parentPath === element.root) {
+ return undefined;
}
- } catch (e) {
- let error = e instanceof Error ? e.message : e;
- this._errorMessage = error;
- vscode.window.showErrorMessage(error);
}
- }
- return books;
- }
-
- public async getSections(tableOfContents: IJupyterBookToc, sections: IJupyterBookSection[], root: string): Promise {
- let notebooks: BookTreeItem[] = [];
- for (let i = 0; i < sections.length; i++) {
- if (sections[i].url) {
- if (sections[i].external) {
- let externalLink = new BookTreeItem({
- title: sections[i].title,
- root: root,
- tableOfContents: tableOfContents,
- page: sections[i],
- type: BookTreeItemType.ExternalLink,
- treeItemCollapsibleState: vscode.TreeItemCollapsibleState.Collapsed
- },
- {
- light: this._extensionContext.asAbsolutePath('resources/light/link.svg'),
- dark: this._extensionContext.asAbsolutePath('resources/dark/link_inverse.svg')
- }
- );
-
- notebooks.push(externalLink);
- } else {
- let pathToNotebook = path.join(root, 'content', sections[i].url.concat('.ipynb'));
- let pathToMarkdown = path.join(root, 'content', sections[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 (await exists(pathToNotebook)) {
- let notebook = new BookTreeItem({
- title: sections[i].title,
- root: root,
- tableOfContents: tableOfContents,
- page: sections[i],
- type: BookTreeItemType.Notebook,
- treeItemCollapsibleState: vscode.TreeItemCollapsibleState.Collapsed
- },
- {
- light: this._extensionContext.asAbsolutePath('resources/light/notebook.svg'),
- dark: this._extensionContext.asAbsolutePath('resources/dark/notebook_inverse.svg')
- }
- );
- notebooks.push(notebook);
- this._allNotebooks.set(pathToNotebook, notebook);
- if (this._openAsUntitled) {
- this._allNotebooks.set(path.basename(pathToNotebook), notebook);
- }
- } else if (await exists(pathToMarkdown)) {
- let markdown = new BookTreeItem({
- title: sections[i].title,
- root: root,
- tableOfContents: tableOfContents,
- page: sections[i],
- type: BookTreeItemType.Markdown,
- treeItemCollapsibleState: vscode.TreeItemCollapsibleState.Collapsed
- },
- {
- light: this._extensionContext.asAbsolutePath('resources/light/markdown.svg'),
- dark: this._extensionContext.asAbsolutePath('resources/dark/markdown_inverse.svg')
- }
- );
- notebooks.push(markdown);
- } else {
- let error = localize('missingFileError', 'Missing file : {0}', sections[i].title);
- this._errorMessage = error;
- vscode.window.showErrorMessage(error);
- }
- }
- } else {
- // TODO: search functionality (#6160)
+ else if (element.root.endsWith('.ipynb')) {
+ let baseName: string = path.basename(element.root);
+ parentPath = element.root.replace(baseName, 'readme.md');
}
- }
- return notebooks;
- }
-
- getNavigation(uri: vscode.Uri): Thenable {
- let notebook = uri.scheme !== 'untitled' ? this._allNotebooks.get(uri.fsPath) : this._allNotebooks.get(path.basename(uri.fsPath));
- let result: azdata.nb.NavigationResult;
- if (notebook) {
- result = {
- hasNavigation: true,
- previous: notebook.previousUri ? this._openAsUntitled ? vscode.Uri.parse(notebook.previousUri).with({ scheme: 'untitled' }) : vscode.Uri.file(notebook.previousUri) : undefined,
- next: notebook.nextUri ? this._openAsUntitled ? vscode.Uri.parse(notebook.nextUri).with({ scheme: 'untitled' }) : vscode.Uri.file(notebook.nextUri) : undefined
- };
+ else {
+ return undefined;
+ }
+ return this.currentBook.getAllBooks().get(parentPath);
} else {
- result = {
- hasNavigation: false,
- previous: undefined,
- next: undefined
- };
+ return undefined;
}
- return Promise.resolve(result);
}
public get errorMessage() {
return this._errorMessage;
}
- public get tableOfContentPaths() {
- return this._tableOfContentPaths;
- }
-
getUntitledNotebookUri(resource: string): vscode.Uri {
let untitledFileName: vscode.Uri;
- if (process.platform.indexOf('win') > -1) {
+ if (process.platform === 'win32') {
let title = path.join(path.dirname(resource), this.findNextUntitledFileName(resource));
untitledFileName = vscode.Uri.parse(`untitled:${title}`);
}
else {
untitledFileName = vscode.Uri.parse(resource).with({ scheme: 'untitled' });
}
- if (!this._allNotebooks.get(untitledFileName.fsPath) && !this._allNotebooks.get(path.basename(untitledFileName.fsPath))) {
- let notebook = this._allNotebooks.get(resource);
- this._allNotebooks.set(path.basename(untitledFileName.fsPath), notebook);
+ if (!this.currentBook.getAllBooks().get(untitledFileName.fsPath) && !this.currentBook.getAllBooks().get(path.basename(untitledFileName.fsPath))) {
+ let notebook = this.currentBook.getAllBooks().get(resource);
+ this.currentBook.getAllBooks().set(path.basename(untitledFileName.fsPath), notebook);
}
return untitledFileName;
}
@@ -368,4 +294,14 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider {
+ return await this.prompter.promptSingle({
+ type: QuestionTypes.confirm,
+ message: localize('confirmReplace', "Folder already exists. Are you sure you want to delete and replace this folder?"),
+ default: false
+ });
+ }
+
+
}
diff --git a/extensions/notebook/src/extension.ts b/extensions/notebook/src/extension.ts
index 15b0a40ef9..c8f32d7e12 100644
--- a/extensions/notebook/src/extension.ts
+++ b/extensions/notebook/src/extension.ts
@@ -22,19 +22,22 @@ 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");
-
+const BOOKS_VIEWID = 'bookTreeView';
+const READONLY_BOOKS_VIEWID = 'untitledBookTreeView';
let controller: JupyterController;
type ChooseCellType = { label: string, id: CellType };
export async function activate(extensionContext: vscode.ExtensionContext): Promise {
- const bookTreeViewProvider = new BookTreeViewProvider(vscode.workspace.workspaceFolders || [], extensionContext);
- extensionContext.subscriptions.push(vscode.window.registerTreeDataProvider('bookTreeView', bookTreeViewProvider));
- extensionContext.subscriptions.push(azdata.nb.registerNavigationProvider(bookTreeViewProvider));
- 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)));
+ const bookTreeViewProvider = new BookTreeViewProvider(vscode.workspace.workspaceFolders || [], extensionContext, false, BOOKS_VIEWID);
+ const untitledBookTreeViewProvider = new BookTreeViewProvider([], extensionContext, true, READONLY_BOOKS_VIEWID);
+ extensionContext.subscriptions.push(vscode.window.registerTreeDataProvider(BOOKS_VIEWID, bookTreeViewProvider));
+ extensionContext.subscriptions.push(vscode.window.registerTreeDataProvider(READONLY_BOOKS_VIEWID, untitledBookTreeViewProvider));
+ 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.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)));
+ extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openExternalLink', (resource) => bookTreeViewProvider.openExternalLink(resource)));
+ extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.saveBook', () => untitledBookTreeViewProvider.saveJupyterBooks()));
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 e4993e24d7..b2c41725bf 100644
--- a/extensions/notebook/src/test/book/book.test.ts
+++ b/extensions/notebook/src/test/book/book.test.ts
@@ -111,7 +111,7 @@ describe.skip('BookTreeViewProviderTests', function() {
index: 0
};
console.log('Creating BookTreeViewProvider...');
- bookTreeViewProvider = new BookTreeViewProvider([folder], mockExtensionContext.object);
+ bookTreeViewProvider = new BookTreeViewProvider([folder], mockExtensionContext.object, false, 'bookTreeView');
let tocRead = new Promise((resolve, reject) => bookTreeViewProvider.onReadAllTOCFiles(() => resolve()));
let errorCase = new Promise((resolve, reject) => setTimeout(() => resolve(), 5000));
await Promise.race([tocRead, errorCase.then(() => { throw new Error('Table of Contents were not ready in time'); })]);
@@ -178,15 +178,15 @@ describe.skip('BookTreeViewProviderTests', function() {
name: '',
index: 0
};
- bookTreeViewProvider = new BookTreeViewProvider([folder], mockExtensionContext.object);
+ bookTreeViewProvider = new BookTreeViewProvider([folder], mockExtensionContext.object, false, 'bookTreeView');
let tocRead = new Promise((resolve, reject) => bookTreeViewProvider.onReadAllTOCFiles(() => resolve()));
let errorCase = new Promise((resolve, reject) => setTimeout(() => resolve(), 5000));
await Promise.race([tocRead, errorCase.then(() => { throw new Error('Table of Contents were not ready in time'); })]);
});
it('should ignore toc.yml files not in _data folder', function(): void {
- bookTreeViewProvider.getTableOfContentFiles([folder.uri.toString()]);
- for (let p of bookTreeViewProvider.tableOfContentPaths) {
+ bookTreeViewProvider.currentBook.getTableOfContentFiles(folder.uri.toString());
+ for (let p of bookTreeViewProvider.currentBook.tableOfContentPaths) {
should(p.toLocaleLowerCase()).equal(tableOfContentsFile.replace(/\\/g, '/').toLocaleLowerCase());
}
});
@@ -220,19 +220,19 @@ describe.skip('BookTreeViewProviderTests', function() {
name: '',
index: 0
};
- bookTreeViewProvider = new BookTreeViewProvider([folder], mockExtensionContext.object);
+ bookTreeViewProvider = new BookTreeViewProvider([folder], mockExtensionContext.object, false, 'bookTreeView');
let tocRead = new Promise((resolve, reject) => bookTreeViewProvider.onReadAllTOCFiles(() => resolve()));
let errorCase = new Promise((resolve, reject) => setTimeout(() => resolve(), 5000));
await Promise.race([tocRead, errorCase.then(() => { throw new Error('Table of Contents were not ready in time'); })]);
});
it('should show error message if config.yml file not found', function(): void {
- bookTreeViewProvider.getBooks();
+ bookTreeViewProvider.currentBook.readBooks();
should(bookTreeViewProvider.errorMessage.toLocaleLowerCase()).equal(('ENOENT: no such file or directory, open \'' + configFile + '\'').toLocaleLowerCase());
});
it('should show error if toc.yml file format is invalid', async function(): Promise {
await fs.writeFile(configFile, 'title: Test Book');
- bookTreeViewProvider.getBooks();
+ bookTreeViewProvider.currentBook.readBooks();
should(bookTreeViewProvider.errorMessage).equal('Error: Test Book has an incorrect toc.yml file');
});
@@ -277,15 +277,15 @@ describe.skip('BookTreeViewProviderTests', function() {
name: '',
index: 0
};
- bookTreeViewProvider = new BookTreeViewProvider([folder], mockExtensionContext.object);
+ bookTreeViewProvider = new BookTreeViewProvider([folder], mockExtensionContext.object, false, 'bookTreeView');
let tocRead = new Promise((resolve, reject) => bookTreeViewProvider.onReadAllTOCFiles(() => resolve()));
let errorCase = new Promise((resolve, reject) => setTimeout(() => resolve(), 5000));
await Promise.race([tocRead, errorCase.then(() => { throw new Error('Table of Contents were not ready in time'); })]);
});
it('should show error if notebook or markdown file is missing', async function(): Promise {
- let books = bookTreeViewProvider.getBooks();
- let children = await bookTreeViewProvider.getSections({ sections: [] }, (await books)[0].sections, rootFolderPath);
+ let books = bookTreeViewProvider.currentBook.bookItems;
+ let children = await bookTreeViewProvider.currentBook.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);
diff --git a/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts b/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts
index 062698f354..ef20d65753 100644
--- a/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts
+++ b/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts
@@ -708,8 +708,8 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements
let result = await this._proxy.$getNavigation(handle, uri);
if (result) {
if (result.next.scheme === Schemas.untitled) {
- let untitledNbName: URI = URI.parse(`untitled:${path.basename(result.next.path)}`);
- let content = await this._fileService.readFile(URI.parse(result.next.path));
+ let untitledNbName: URI = URI.parse(`untitled:${result.next.path}`);
+ let content = await this._fileService.readFile(URI.file(result.next.path));
await this.doOpenEditor(untitledNbName, { initialContent: content.value.toString(), initialDirtyState: false });
}
else {
@@ -721,8 +721,8 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements
let result = await this._proxy.$getNavigation(handle, uri);
if (result) {
if (result.previous.scheme === Schemas.untitled) {
- let untitledNbName: URI = URI.parse(`untitled:${path.basename(result.previous.path)}`);
- let content = await this._fileService.readFile(URI.parse(result.previous.path));
+ let untitledNbName: URI = URI.parse(`untitled:${result.previous.path}`);
+ let content = await this._fileService.readFile(URI.file(result.previous.path));
await this.doOpenEditor(untitledNbName, { initialContent: content.value.toString(), initialDirtyState: false });
}
else {
diff --git a/src/sql/workbench/parts/notebook/browser/notebook.common.contribution.ts b/src/sql/workbench/parts/notebook/browser/notebook.common.contribution.ts
index cd833c9e67..4b53573c5d 100644
--- a/src/sql/workbench/parts/notebook/browser/notebook.common.contribution.ts
+++ b/src/sql/workbench/parts/notebook/browser/notebook.common.contribution.ts
@@ -158,14 +158,6 @@ configurationRegistry.registerConfiguration({
}
});
-/**
-* Explorer viewlet id.
-*/
-export const VIEWLET_ID = 'bookTreeView';
-/**
-* Explorer viewlet container.
-*/
-export const VIEW_CONTAINER: ViewContainer = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer(VIEWLET_ID);
registerAction({
id: 'workbench.books.action.focusBooksExplorer',
handler: async (accessor) => {