diff --git a/extensions/notebook/src/book/bookModel.ts b/extensions/notebook/src/book/bookModel.ts index 65c6d91d1f..ba6b083dff 100644 --- a/extensions/notebook/src/book/bookModel.ts +++ b/extensions/notebook/src/book/bookModel.ts @@ -15,6 +15,7 @@ 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'; const fsPromises = fileServices.promises; @@ -25,6 +26,9 @@ export class BookModel implements azdata.nb.NavigationProvider { private _tableOfContentPaths: 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; @@ -51,25 +55,26 @@ export class BookModel implements azdata.nb.NavigationProvider { maxDepth = undefined; } - let p = path.join(folderPath, '**', '_data', 'toc.yml').replace(/\\/g, '/'); - let tableOfContentPaths = await glob(p, { deep: maxDepth }); + let p: string = path.join(folderPath, '**', '_data', 'toc.yml').replace(/\\/g, '/'); + let tableOfContentPaths: string[] = await glob(p, { deep: maxDepth }); if (tableOfContentPaths.length > 0) { this._tableOfContentPaths = this._tableOfContentPaths.concat(tableOfContentPaths); vscode.commands.executeCommand('setContext', 'bookOpened', true); } else { + this._errorMessage = loc.missingTocError; throw new Error(loc.missingTocError); } } public async readBooks(): Promise { for (const contentPath of this._tableOfContentPaths) { - let root = path.dirname(path.dirname(contentPath)); + let root: string = 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({ + const tableOfContents: any = yaml.safeLoad(fileContents.toString()); + let book: BookTreeItem = new BookTreeItem({ title: config.title, root: root, tableOfContents: { sections: this.parseJupyterSections(tableOfContents) }, @@ -85,8 +90,8 @@ export class BookModel implements azdata.nb.NavigationProvider { ); this._bookItems.push(book); } catch (e) { - let error = e instanceof Error ? e.message : e; - vscode.window.showErrorMessage(error); + this._errorMessage = loc.readBookError(this.bookPath, e instanceof Error ? e.message : e); + this.apiWrapper.showErrorMessage(this._errorMessage); } } return this._bookItems; @@ -101,7 +106,7 @@ export class BookModel implements azdata.nb.NavigationProvider { for (let i = 0; i < sections.length; i++) { if (sections[i].url) { if (sections[i].external) { - let externalLink = new BookTreeItem({ + let externalLink: BookTreeItem = new BookTreeItem({ title: sections[i].title, root: root, tableOfContents: tableOfContents, @@ -143,15 +148,14 @@ export class BookModel implements azdata.nb.NavigationProvider { this._allNotebooks.set(path.basename(pathToNotebook), notebook); notebooks.push(notebook); } - } - else { + } else { if (!this._allNotebooks.get(pathToNotebook)) { this._allNotebooks.set(pathToNotebook, notebook); notebooks.push(notebook); } } } else if (await fs.pathExists(pathToMarkdown)) { - let markdown = new BookTreeItem({ + let markdown: BookTreeItem = new BookTreeItem({ title: sections[i].title, root: root, tableOfContents: tableOfContents, @@ -167,8 +171,8 @@ export class BookModel implements azdata.nb.NavigationProvider { ); notebooks.push(markdown); } else { - let error = loc.missingFileError(sections[i].title); - vscode.window.showErrorMessage(error); + this._errorMessage = loc.missingFileError(sections[i].title); + this.apiWrapper.showErrorMessage(this._errorMessage); } } } else { @@ -184,29 +188,31 @@ export class BookModel implements azdata.nb.NavigationProvider { */ 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 = loc.invalidTocFileError(error); + return section.reduce((acc, val) => Array.isArray(val.sections) ? + acc.concat(val).concat(this.parseJupyterSections(val.sections)) : acc.concat(val), []); + } catch (e) { + this._errorMessage = loc.invalidTocFileError(); if (section.length > 0) { - err = loc.invalidTocError(section[0].title); + this._errorMessage = loc.invalidTocError(section[0].title); } - vscode.window.showErrorMessage(err); - throw err; + throw this._errorMessage; } } - public get tableOfContentPaths() { + public get tableOfContentPaths(): string[] { 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 notebook: BookTreeItem = + !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.getUntitledUri(notebook.previousUri) : vscode.Uri.file(notebook.previousUri) : undefined, + previous: notebook.previousUri ? + this.openAsUntitled ? this.getUntitledUri(notebook.previousUri) : vscode.Uri.file(notebook.previousUri) : undefined, next: notebook.nextUri ? this.openAsUntitled ? this.getUntitledUri(notebook.nextUri) : vscode.Uri.file(notebook.nextUri) : undefined }; } else { @@ -222,4 +228,9 @@ export class BookModel implements azdata.nb.NavigationProvider { getUntitledUri(resource: string): vscode.Uri { return vscode.Uri.parse(`untitled:${resource}`); } + + public get errorMessage(): string { + return this._errorMessage; + } + } diff --git a/extensions/notebook/src/book/bookTreeView.ts b/extensions/notebook/src/book/bookTreeView.ts index ff317986de..ae6e972914 100644 --- a/extensions/notebook/src/book/bookTreeView.ts +++ b/extensions/notebook/src/book/bookTreeView.ts @@ -24,8 +24,6 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider = new Deferred(); - // For testing - private _errorMessage: string; private _openAsUntitled: boolean; public viewId: string; public books: BookModel[]; @@ -292,10 +290,6 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider { @@ -44,27 +46,28 @@ describe('BookTreeViewProviderTests', function() { let nonBookFolderPath: string; let bookFolderPath: string; let rootFolderPath: string; - let expectedNotebook1: ExpectedBookItem; - let expectedNotebook2: ExpectedBookItem; - let expectedNotebook3: ExpectedBookItem; - let expectedMarkdown: ExpectedBookItem; - let expectedExternalLink: ExpectedBookItem; - let expectedBook: ExpectedBookItem; + let expectedNotebook1: IExpectedBookItem; + let expectedNotebook2: IExpectedBookItem; + let expectedNotebook3: IExpectedBookItem; + let expectedMarkdown: IExpectedBookItem; + let expectedExternalLink: IExpectedBookItem; + let expectedBook: IExpectedBookItem; this.beforeAll(async () => { mockExtensionContext = new MockExtensionContext(); rootFolderPath = path.join(os.tmpdir(), `BookTestData_${uuid.v4()}`); nonBookFolderPath = path.join(rootFolderPath, `NonBook`); bookFolderPath = path.join(rootFolderPath, `Book`); - let dataFolderPath = path.join(bookFolderPath, '_data'); - let contentFolderPath = path.join(bookFolderPath, 'content'); - let configFile = path.join(bookFolderPath, '_config.yml'); - let tableOfContentsFile = path.join(dataFolderPath, 'toc.yml'); - let notebook1File = path.join(contentFolderPath, 'notebook1.ipynb'); - let notebook2File = path.join(contentFolderPath, 'notebook2.ipynb'); - let notebook3File = path.join(contentFolderPath, 'notebook3.ipynb'); - let markdownFile = path.join(contentFolderPath, 'markdown.md'); + let dataFolderPath: string = path.join(bookFolderPath, '_data'); + let contentFolderPath: string = path.join(bookFolderPath, 'content'); + let configFile: string = path.join(bookFolderPath, '_config.yml'); + let tableOfContentsFile: string = path.join(dataFolderPath, 'toc.yml'); + let notebook1File: string = path.join(contentFolderPath, 'notebook1.ipynb'); + let notebook2File: string = path.join(contentFolderPath, 'notebook2.ipynb'); + let notebook3File: string = path.join(contentFolderPath, 'notebook3.ipynb'); + let markdownFile: string = path.join(contentFolderPath, 'markdown.md'); expectedNotebook1 = { + // tslint:disable-next-line: quotemark title: 'Notebook1', url: '/notebook1', previousUri: undefined, @@ -185,7 +188,7 @@ describe('BookTreeViewProviderTests', function() { equalBookItems(notebook3, expectedNotebook3); }); - this.afterAll(async function () { + this.afterAll(async function (): Promise { console.log('Removing temporary files...'); if (await exists(rootFolderPath)) { await promisify(rimraf)(rootFolderPath); @@ -204,9 +207,9 @@ describe('BookTreeViewProviderTests', function() { this.beforeAll(async () => { rootFolderPath = path.join(os.tmpdir(), `BookTestData_${uuid.v4()}`); - let dataFolderPath = path.join(rootFolderPath, '_data'); + let dataFolderPath: string = path.join(rootFolderPath, '_data'); tableOfContentsFile = path.join(dataFolderPath, 'toc.yml'); - let tableOfContentsFileIgnore = path.join(rootFolderPath, 'toc.yml'); + let tableOfContentsFileIgnore: string = path.join(rootFolderPath, 'toc.yml'); await fs.mkdir(rootFolderPath); await fs.mkdir(dataFolderPath); await fs.writeFile(tableOfContentsFile, '- title: Notebook1\n url: /notebook1\n sections:\n - title: Notebook2\n url: /notebook2\n - title: Notebook3\n url: /notebook3\n- title: Markdown\n url: /markdown\n- title: GitHub\n url: https://github.com/\n external: true'); @@ -229,7 +232,7 @@ describe('BookTreeViewProviderTests', function() { } }); - this.afterAll(async function () { + this.afterAll(async function (): Promise { if (await exists(rootFolderPath)) { await promisify(rimraf)(rootFolderPath); } @@ -237,7 +240,7 @@ describe('BookTreeViewProviderTests', function() { }); - describe('BookTreeViewProvider.getBooks @UNSTABLE@', function (): void { + describe('BookTreeViewProvider.getBooks', function (): void { let rootFolderPath: string; let configFile: string; let folder: vscode.WorkspaceFolder; @@ -246,7 +249,7 @@ describe('BookTreeViewProviderTests', function() { this.beforeAll(async () => { rootFolderPath = path.join(os.tmpdir(), `BookTestData_${uuid.v4()}`); - let dataFolderPath = path.join(rootFolderPath, '_data'); + let dataFolderPath: string = path.join(rootFolderPath, '_data'); configFile = path.join(rootFolderPath, '_config.yml'); tocFile = path.join(dataFolderPath, 'toc.yml'); await fs.mkdir(rootFolderPath); @@ -265,16 +268,16 @@ describe('BookTreeViewProviderTests', function() { it('should show error message if config.yml file not found', async () => { await bookTreeViewProvider.currentBook.readBooks(); - should(bookTreeViewProvider.errorMessage.toLocaleLowerCase()).equal(('ENOENT: no such file or directory, open \'' + configFile + '\'').toLocaleLowerCase()); + should(bookTreeViewProvider.currentBook.errorMessage.toLocaleLowerCase()).equal(('Failed to read book '+ bookTreeViewProvider.currentBook.bookPath +': ENOENT: no such file or directory, open \'' + configFile + '\'').toLocaleLowerCase()); }); - it('should show error if toc.yml file format is invalid', async function(): Promise { + it('should show error if toc.yml file format is invalid', async function (): Promise { await fs.writeFile(configFile, 'title: Test Book'); await bookTreeViewProvider.currentBook.readBooks(); - should(bookTreeViewProvider.errorMessage).equal('Error: Test Book has an incorrect toc.yml file'); + should(bookTreeViewProvider.currentBook.errorMessage.toLocaleLowerCase()).equal(('Failed to read book '+ bookTreeViewProvider.currentBook.bookPath +': Invalid toc file').toLocaleLowerCase()); }); - this.afterAll(async function () { + this.afterAll(async function (): Promise { if (await exists(rootFolderPath)) { await promisify(rimraf)(rootFolderPath); } @@ -282,12 +285,12 @@ describe('BookTreeViewProviderTests', function() { }); - describe('BookTreeViewProvider.getSections @UNSTABLE@', function (): void { + describe('BookTreeViewProvider.getSections', function (): void { let rootFolderPath: string; let tableOfContentsFile: string; let bookTreeViewProvider: BookTreeViewProvider; let folder: vscode.WorkspaceFolder; - let expectedNotebook2: ExpectedBookItem; + let expectedNotebook2: IExpectedBookItem; this.beforeAll(async () => { rootFolderPath = path.join(os.tmpdir(), `BookTestData_${uuid.v4()}`); @@ -320,15 +323,15 @@ describe('BookTreeViewProviderTests', function() { await Promise.race([bookTreeViewProvider.initialized, errorCase.then(() => { throw new Error('BookTreeViewProvider did not initialize in time'); })]); }); - it('should show error if notebook or markdown file is missing', async function(): Promise { - let books = bookTreeViewProvider.currentBook.bookItems; + it('should show error if notebook or markdown file is missing', async function (): Promise { + let books: BookTreeItem[] = 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 + should(bookTreeViewProvider.currentBook.errorMessage).equal('Missing file : Notebook1'); + // rest of book should be detected correctly even with a missing file equalBookItems(children[0], expectedNotebook2); }); - this.afterAll(async function () { + this.afterAll(async function (): Promise { if (await exists(rootFolderPath)) { await promisify(rimraf)(rootFolderPath); }