diff --git a/extensions/notebook/package.json b/extensions/notebook/package.json index e0f25badd4..59c2121be5 100644 --- a/extensions/notebook/package.json +++ b/extensions/notebook/package.json @@ -186,6 +186,10 @@ "light": "resources/light/open_notebook.svg" } }, + { + "command": "notebook.command.closeBook", + "title": "%title.closeJupyterBook%" + }, { "command": "notebook.command.createBook", "title": "%title.createJupyterBook%", @@ -288,6 +292,10 @@ "command": "notebook.command.searchUntitledBook", "when": "false" }, + { + "command": "notebook.command.closeBook", + "when": "false" + }, { "command": "notebook.command.revealInBooksViewlet", "when": "false" @@ -332,6 +340,10 @@ "command": "notebook.command.saveBook", "when": "view == unsavedBookTreeView && viewItem == unsavedBook && unsavedBooks", "group": "inline" + }, + { + "command": "notebook.command.closeBook", + "when": "view == bookTreeView && viewItem == savedBook" } ], "view/title": [ diff --git a/extensions/notebook/package.nls.json b/extensions/notebook/package.nls.json index f2c74625d2..8effe01d3c 100644 --- a/extensions/notebook/package.nls.json +++ b/extensions/notebook/package.nls.json @@ -35,6 +35,7 @@ "title.UnsavedBooks": "Unsaved Books", "title.PreviewLocalizedBook": "Get localized SQL Server 2019 guide", "title.openJupyterBook": "Open Jupyter Book", + "title.closeJupyterBook": "Close Jupyter Book", "title.revealInBooksViewlet": "Reveal in Books", "title.createJupyterBook": "Create Book" } diff --git a/extensions/notebook/src/book/bookModel.ts b/extensions/notebook/src/book/bookModel.ts index 4b9b448419..55051766de 100644 --- a/extensions/notebook/src/book/bookModel.ts +++ b/extensions/notebook/src/book/bookModel.ts @@ -37,6 +37,8 @@ export class BookModel implements azdata.nb.NavigationProvider { } public async initializeContents(): Promise { + this._tableOfContentPaths = []; + this._bookItems = []; await this.getTableOfContentFiles(this.bookPath); await this.readBooks(); } @@ -157,8 +159,8 @@ export class BookModel implements azdata.nb.NavigationProvider { let uriToNotebook: vscode.Uri = vscode.Uri.file(pathToNotebook); if (!this._allNotebooks.get(uriToNotebook.fsPath)) { this._allNotebooks.set(uriToNotebook.fsPath, notebook); - notebooks.push(notebook); } + notebooks.push(notebook); } } else if (await fs.pathExists(pathToMarkdown)) { let markdown: BookTreeItem = new BookTreeItem({ diff --git a/extensions/notebook/src/book/bookTreeView.ts b/extensions/notebook/src/book/bookTreeView.ts index 394076f713..81b5b9c3d3 100644 --- a/extensions/notebook/src/book/bookTreeView.ts +++ b/extensions/notebook/src/book/bookTreeView.ts @@ -7,6 +7,7 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import * as path from 'path'; import * as fs from 'fs-extra'; +import * as fsw from 'fs'; import { IPrompter, QuestionTypes, IQuestion } from '../prompts/question'; import CodeAdapter from '../prompts/adapter'; import { BookTreeItem } from './bookTreeItem'; @@ -74,11 +75,43 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider { + 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(); + } + }); } catch (e) { vscode.window.showErrorMessage(loc.openFileError(bookPath, e instanceof Error ? e.message : e)); } } + async closeBook(book: BookTreeItem): Promise { + // remove book from the saved books + let deletedBook: BookModel; + try { + let index: number = this.books.indexOf(this.books.find(b => b.bookPath.replace(/\\/g, '/') === book.root)); + if (index > -1) { + deletedBook = this.books.splice(index, 1)[0]; + if (this.currentBook === deletedBook) { + this.currentBook = this.books.length > 0 ? this.books[this.books.length - 1] : undefined; + } + this._onDidChangeTreeData.fire(); + } + } catch (e) { + vscode.window.showErrorMessage(loc.closeBookError(book.root, e instanceof Error ? e.message : e)); + } finally { + // remove watch on toc file. + if (deletedBook) { + fsw.unwatchFile(path.join(deletedBook.bookPath, '_data', 'toc.yml')); + } + } + } + /** * Creates a model for the specified folder path and adds it to the known list of books if we * were able to successfully parse it. diff --git a/extensions/notebook/src/common/localizedConstants.ts b/extensions/notebook/src/common/localizedConstants.ts index 2ceb50f38d..ce1a3d2940 100644 --- a/extensions/notebook/src/common/localizedConstants.ts +++ b/extensions/notebook/src/common/localizedConstants.ts @@ -33,3 +33,4 @@ export function openNotebookError(resource: string, error: string): string { ret export function openMarkdownError(resource: string, error: string): string { return localize('openMarkdownError', "Open markdown {0} failed: {1}", resource, error); } export function openUntitledNotebookError(resource: string, error: string): string { return localize('openUntitledNotebookError', "Open untitled notebook {0} as untitled failed: {1}", resource, error); } export function openExternalLinkError(resource: string, error: string): string { return localize('openExternalLinkError', "Open link {0} failed: {1}", resource, error); } +export function closeBookError(resource: string, error: string): string { return localize('closeBookError', "Close book {0} failed: {1}", resource, error); } diff --git a/extensions/notebook/src/extension.ts b/extensions/notebook/src/extension.ts index 4e4af8c725..284ba9f315 100644 --- a/extensions/notebook/src/extension.ts +++ b/extensions/notebook/src/extension.ts @@ -39,6 +39,7 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.searchBook', (item) => bookTreeViewProvider.searchJupyterBooks(item))); 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.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 c5aa2b1181..5178aefa59 100644 --- a/extensions/notebook/src/test/book/book.test.ts +++ b/extensions/notebook/src/test/book/book.test.ts @@ -114,6 +114,7 @@ describe('BookTreeViewProviderTests', function () { await fs.writeFile(notebook2File, ''); await fs.writeFile(notebook3File, ''); await fs.writeFile(markdownFile, ''); + appContext = new AppContext(undefined, new ApiWrapper()); }); it('should initialize correctly with empty workspace array', async () => { @@ -230,6 +231,7 @@ describe('BookTreeViewProviderTests', function () { bookTreeViewProvider = new BookTreeViewProvider(appContext.apiWrapper, [folder], mockExtensionContext, false, 'bookTreeView'); let errorCase = new Promise((resolve, reject) => setTimeout(() => resolve(), 5000)); await Promise.race([bookTreeViewProvider.initialized, errorCase.then(() => { throw new Error('BookTreeViewProvider did not initialize in time'); })]); + appContext = new AppContext(undefined, new ApiWrapper()); }); it('should ignore toc.yml files not in _data folder', async () => { @@ -273,6 +275,7 @@ describe('BookTreeViewProviderTests', function () { bookTreeViewProvider = new BookTreeViewProvider(appContext.apiWrapper, [folder], mockExtensionContext, false, 'bookTreeView'); let errorCase = new Promise((resolve, reject) => setTimeout(() => resolve(), 5000)); await Promise.race([bookTreeViewProvider.initialized, errorCase.then(() => { throw new Error('BookTreeViewProvider did not initialize in time'); })]); + appContext = new AppContext(undefined, new ApiWrapper()); }); it('should show error message if config.yml file not found', async () => { @@ -332,6 +335,7 @@ describe('BookTreeViewProviderTests', function () { bookTreeViewProvider = new BookTreeViewProvider(appContext.apiWrapper, [folder], mockExtensionContext, false, 'bookTreeView'); let errorCase = new Promise((resolve, reject) => setTimeout(() => resolve(), 5000)); await Promise.race([bookTreeViewProvider.initialized, errorCase.then(() => { throw new Error('BookTreeViewProvider did not initialize in time'); })]); + appContext = new AppContext(undefined, new ApiWrapper()); }); it('should show error if notebook or markdown file is missing', async function (): Promise {