Add Open Notebook Folder functionality to Books viewlet. (#9939)

This commit is contained in:
Cory Rivera
2020-04-13 23:42:02 -07:00
committed by GitHub
parent a8cf029633
commit 2b2a275fb0
12 changed files with 224 additions and 74 deletions

View File

@@ -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": [

View File

@@ -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"
}

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.5161 6.50115C13.5903 6.50115 13.6642 6.50822 13.7367 6.52218L13.8443 6.54826L13.9492 6.58452C14.516 6.81113 14.8071 7.43239 14.6328 8.00586L14.5994 8.10092L12.8831 12.3939C12.6695 12.9281 12.1711 13.2898 11.604 13.3328L11.4877 13.3373H3.4285C3.4053 13.3373 3.38213 13.3368 3.35899 13.3357L2.83354 13.3343C2.04002 13.3336 1.39066 12.7171 1.3375 11.9367L1.33398 11.834V4.17254C1.33398 3.3796 1.94931 2.73018 2.72882 2.67617L2.83139 2.67254L5.47001 2.66797C5.7779 2.66744 6.07737 2.76163 6.3285 2.93612L6.43304 3.01577L8.01465 4.33401L11.1673 4.33448C11.9251 4.33448 12.5517 4.89646 12.653 5.6264L12.6639 5.73178L12.6673 5.83448V6.50068L13.5161 6.50115ZM4.82389 7.50115C4.63701 7.50115 4.46788 7.60506 4.38213 7.76691L4.35401 7.83023L2.95862 11.6664C2.86422 11.9259 2.99807 12.2128 3.25758 12.3072C3.2941 12.3205 3.33198 12.3294 3.37045 12.3339L11.4901 12.3371C11.5355 12.3371 11.58 12.3309 11.6224 12.3193L11.6826 12.2978C11.7793 12.2568 11.8619 12.1858 11.9168 12.094L11.9526 12.0213L13.6515 7.72915C13.6854 7.64357 13.6435 7.54672 13.5579 7.51284L13.5277 7.5041L13.4965 7.50115H4.82389ZM5.47174 3.66797L2.83312 3.67254C2.5803 3.67298 2.37157 3.861 2.33854 4.10477L2.33398 4.17254L2.33332 10.4586L3.41425 7.48839C3.61637 6.93273 4.12413 6.55124 4.70649 6.50572L4.82389 6.50115L11.6667 6.50064L11.6673 5.83448C11.6673 5.58135 11.4792 5.37215 11.2352 5.33904L11.1673 5.33448H7.65292L5.79275 3.78391C5.72068 3.72383 5.63354 3.68555 5.54152 3.67274L5.47174 3.66797Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.5161 6.50115C13.5903 6.50115 13.6642 6.50822 13.7367 6.52218L13.8443 6.54826L13.9492 6.58452C14.516 6.81113 14.8071 7.43239 14.6328 8.00586L14.5994 8.10092L12.8831 12.3939C12.6695 12.9281 12.1711 13.2898 11.604 13.3328L11.4877 13.3373H3.4285C3.4053 13.3373 3.38213 13.3368 3.35899 13.3357L2.83354 13.3343C2.04002 13.3336 1.39066 12.7171 1.3375 11.9367L1.33398 11.834V4.17254C1.33398 3.3796 1.94931 2.73018 2.72882 2.67617L2.83139 2.67254L5.47001 2.66797C5.7779 2.66744 6.07737 2.76163 6.3285 2.93612L6.43304 3.01577L8.01465 4.33401L11.1673 4.33448C11.9251 4.33448 12.5517 4.89646 12.653 5.6264L12.6639 5.73178L12.6673 5.83448V6.50068L13.5161 6.50115ZM4.82389 7.50115C4.63701 7.50115 4.46788 7.60506 4.38213 7.76691L4.35401 7.83023L2.95862 11.6664C2.86422 11.9259 2.99807 12.2128 3.25758 12.3072C3.2941 12.3205 3.33198 12.3294 3.37045 12.3339L11.4901 12.3371C11.5355 12.3371 11.58 12.3309 11.6224 12.3193L11.6826 12.2978C11.7793 12.2568 11.8619 12.1858 11.9168 12.094L11.9526 12.0213L13.6515 7.72915C13.6854 7.64357 13.6435 7.54672 13.5579 7.51284L13.5277 7.5041L13.4965 7.50115H4.82389ZM5.47174 3.66797L2.83312 3.67254C2.5803 3.67298 2.37157 3.861 2.33854 4.10477L2.33398 4.17254L2.33332 10.4586L3.41425 7.48839C3.61637 6.93273 4.12413 6.55124 4.70649 6.50572L4.82389 6.50115L11.6667 6.50064L11.6673 5.83448C11.6673 5.58135 11.4792 5.37215 11.2352 5.33904L11.1673 5.33448H7.65292L5.79275 3.78391C5.72068 3.72383 5.63354 3.68555 5.54152 3.67274L5.47174 3.66797Z" fill="#212121"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -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<string, BookTreeItem>();
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<void> {
this._tableOfContentPaths = [];
this._bookItems = [];
await this.getTableOfContentFiles(this.bookPath);
await this.readBooks();
this._allNotebooks = new Map<string, BookTreeItem>();
if (this.isNotebook) {
this.readNotebook();
} else {
await this.loadTableOfContentFiles(this.bookPath);
await this.readBooks();
}
}
public getAllNotebooks(): Map<string, BookTreeItem> {
@@ -51,20 +54,14 @@ export class BookModel implements azdata.nb.NavigationProvider {
return this._allNotebooks.get(uri);
}
public async getTableOfContentFiles(folderPath: string): Promise<void> {
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<void> {
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<BookTreeItem[]> {
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<azdata.nb.NavigationResult> {

View File

@@ -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;
}
}

View File

@@ -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<BookTreeItem> {
private _onDidChangeTreeData: vscode.EventEmitter<BookTreeItem | undefined> = new vscode.EventEmitter<BookTreeItem | undefined>();
readonly onDidChangeTreeData: vscode.Event<BookTreeItem | undefined> = this._onDidChangeTreeData.event;
@@ -50,7 +57,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
await vscode.commands.executeCommand('setContext', 'unsavedBooks', this._openAsUntitled);
await Promise.all(workspaceFolders.map(async (workspaceFolder) => {
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<BookTreeIte
}
}
async openBook(bookPath: string, urlToOpen?: string): Promise<void> {
async openBook(bookPath: string, urlToOpen?: string, showPreview?: boolean, isNotebook?: boolean): Promise<void> {
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<BookTreeIte
// 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));
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<BookTreeIte
vscode.window.showErrorMessage(loc.closeBookError(book.root, e instanceof Error ? e.message : e));
} finally {
// remove watch on toc file.
if (deletedBook) {
if (deletedBook && !deletedBook.isNotebook) {
fsw.unwatchFile(path.join(deletedBook.bookPath, '_data', 'toc.yml'));
}
}
@@ -154,8 +170,8 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
* were able to successfully parse it.
* @param bookPath The path to the book folder to create the model for
*/
private async createAndAddBookModel(bookPath: string): Promise<void> {
const book: BookModel = new BookModel(bookPath, this._openAsUntitled, this._extensionContext);
private async createAndAddBookModel(bookPath: string, isNotebook: boolean): Promise<void> {
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<BookTreeIte
canSelectFiles: false,
canSelectMany: false,
canSelectFolders: true,
openLabel: loc.labelPickFolder
openLabel: loc.labelSelectFolder
});
if (uris && uris.length > 0) {
let pickedFolder = uris[0];
@@ -328,10 +344,58 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
});
if (uris && uris.length > 0) {
let bookPath = uris[0];
await this.openBook(bookPath.fsPath);
await this.openBook(bookPath.fsPath, undefined, true);
}
}
public async openNotebookFolder(): Promise<void> {
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<BookSearchResults> {
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<BookTreeIte
return Promise.resolve([]);
}
} else {
let booksitems: BookTreeItem[] = [];
let bookItems: BookTreeItem[] = [];
this.books.map(book => {
booksitems = booksitems.concat(book.bookItems);
bookItems = bookItems.concat(book.bookItems);
});
return Promise.resolve(booksitems);
return Promise.resolve(bookItems);
}
}

View File

@@ -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"); }

View File

@@ -30,7 +30,7 @@ type ChooseCellType = { label: string, id: CellType };
export async function activate(extensionContext: vscode.ExtensionContext): Promise<IExtensionApi> {
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}`);

View File

@@ -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<void> {

View File

@@ -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: [

View File

@@ -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 {