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": { "properties": {
"notebook.maxBookSearchDepth": { "notebook.maxBookSearchDepth": {
"type": "number", "type": "number",
"default": 5, "default": 10,
"description": "%notebook.maxBookSearchDepth.description%" "description": "%notebook.maxBookSearchDepth.description%"
}, },
"notebook.pythonPath": { "notebook.pythonPath": {
@@ -203,10 +203,23 @@
"light": "resources/light/open_notebook.svg" "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", "command": "notebook.command.closeBook",
"title": "%title.closeJupyterBook%" "title": "%title.closeJupyterBook%"
}, },
{
"command": "notebook.command.closeNotebook",
"title": "%title.closeJupyterNotebook%"
},
{ {
"command": "notebook.command.createBook", "command": "notebook.command.createBook",
"title": "%title.createJupyterBook%", "title": "%title.createJupyterBook%",
@@ -317,6 +330,10 @@
"command": "notebook.command.closeBook", "command": "notebook.command.closeBook",
"when": "false" "when": "false"
}, },
{
"command": "notebook.command.closeNotebook",
"when": "false"
},
{ {
"command": "notebook.command.revealInBooksViewlet", "command": "notebook.command.revealInBooksViewlet",
"when": "false" "when": "false"
@@ -370,6 +387,10 @@
{ {
"command": "notebook.command.closeBook", "command": "notebook.command.closeBook",
"when": "view == bookTreeView && viewItem == savedBook" "when": "view == bookTreeView && viewItem == savedBook"
},
{
"command": "notebook.command.closeNotebook",
"when": "view == bookTreeView && viewItem == savedNotebook"
} }
], ],
"view/title": [ "view/title": [
@@ -381,6 +402,11 @@
{ {
"command": "notebook.command.createBook", "command": "notebook.command.createBook",
"when": "view == bookTreeView" "when": "view == bookTreeView"
},
{
"command": "notebook.command.openNotebookFolder",
"when": "view == bookTreeView",
"group": "navigation"
} }
], ],
"notebook/toolbar": [ "notebook/toolbar": [

View File

@@ -38,6 +38,8 @@
"title.PreviewLocalizedBook": "Get localized SQL Server 2019 guide", "title.PreviewLocalizedBook": "Get localized SQL Server 2019 guide",
"title.openJupyterBook": "Open Jupyter Book", "title.openJupyterBook": "Open Jupyter Book",
"title.closeJupyterBook": "Close Jupyter Book", "title.closeJupyterBook": "Close Jupyter Book",
"title.closeJupyterNotebook": "Close Jupyter Notebook",
"title.revealInBooksViewlet": "Reveal in Books", "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 azdata from 'azdata';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as yaml from 'js-yaml'; import * as yaml from 'js-yaml';
import * as glob from 'fast-glob';
import { BookTreeItem, BookTreeItemType } from './bookTreeItem'; import { BookTreeItem, BookTreeItemType } from './bookTreeItem';
import { maxBookSearchDepth, notebookConfigKey } from '../common/constants';
import * as path from 'path'; import * as path from 'path';
import * as fileServices from 'fs'; import * as fileServices from 'fs';
import * as fs from 'fs-extra'; import * as fs from 'fs-extra';
import * as loc from '../common/localizedConstants'; import * as loc from '../common/localizedConstants';
import { IJupyterBookToc, IJupyterBookSection } from '../contracts/content'; import { IJupyterBookToc, IJupyterBookSection } from '../contracts/content';
import { isNullOrUndefined } from 'util';
import { ApiWrapper } from '../common/apiWrapper'; import { ApiWrapper } from '../common/apiWrapper';
@@ -23,25 +20,31 @@ const fsPromises = fileServices.promises;
export class BookModel implements azdata.nb.NavigationProvider { export class BookModel implements azdata.nb.NavigationProvider {
private _bookItems: BookTreeItem[]; private _bookItems: BookTreeItem[];
private _allNotebooks = new Map<string, BookTreeItem>(); private _allNotebooks = new Map<string, BookTreeItem>();
private _tableOfContentPaths: string[] = []; private _tableOfContentsPath: string;
readonly providerId: string = 'BookNavigator'; readonly providerId: string = 'BookNavigator';
private _errorMessage: string; private _errorMessage: string;
private apiWrapper: ApiWrapper = new ApiWrapper(); private apiWrapper: ApiWrapper = new ApiWrapper();
constructor(public bookPath: string, public openAsUntitled: boolean, private _extensionContext: vscode.ExtensionContext) { constructor(
this.bookPath = bookPath; public readonly bookPath: string,
this.openAsUntitled = openAsUntitled; public readonly openAsUntitled: boolean,
public readonly isNotebook: boolean,
private _extensionContext: vscode.ExtensionContext) {
this._bookItems = []; this._bookItems = [];
this._extensionContext.subscriptions.push(azdata.nb.registerNavigationProvider(this)); this._extensionContext.subscriptions.push(azdata.nb.registerNavigationProvider(this));
} }
public async initializeContents(): Promise<void> { public async initializeContents(): Promise<void> {
this._tableOfContentPaths = [];
this._bookItems = []; this._bookItems = [];
await this.getTableOfContentFiles(this.bookPath); this._allNotebooks = new Map<string, BookTreeItem>();
if (this.isNotebook) {
this.readNotebook();
} else {
await this.loadTableOfContentFiles(this.bookPath);
await this.readBooks(); await this.readBooks();
} }
}
public getAllNotebooks(): Map<string, BookTreeItem> { public getAllNotebooks(): Map<string, BookTreeItem> {
return this._allNotebooks; return this._allNotebooks;
@@ -51,20 +54,14 @@ export class BookModel implements azdata.nb.NavigationProvider {
return this._allNotebooks.get(uri); return this._allNotebooks.get(uri);
} }
public async getTableOfContentFiles(folderPath: string): Promise<void> { public async loadTableOfContentFiles(folderPath: string): Promise<void> {
let notebookConfig = vscode.workspace.getConfiguration(notebookConfigKey); if (this.isNotebook) {
let maxDepth = notebookConfig[maxBookSearchDepth]; return;
// 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: string = path.posix.join(glob.escapePath(folderPath.replace(/\\/g, '/')), '**', '_data', 'toc.yml'); let tableOfContentsPath: string = path.posix.join(folderPath, '_data', 'toc.yml');
let tableOfContentPaths: string[] = await glob(p, { deep: maxDepth }); if (await fs.pathExists(tableOfContentsPath)) {
if (tableOfContentPaths.length > 0) { this._tableOfContentsPath = tableOfContentsPath;
this._tableOfContentPaths = this._tableOfContentPaths.concat(tableOfContentPaths);
vscode.commands.executeCommand('setContext', 'bookOpened', true); vscode.commands.executeCommand('setContext', 'bookOpened', true);
} else { } else {
this._errorMessage = loc.missingTocError; 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[]> { public async readBooks(): Promise<BookTreeItem[]> {
for (const contentPath of this._tableOfContentPaths) { if (this.isNotebook) {
let root: string = path.dirname(path.dirname(contentPath)); return undefined;
}
if (this._tableOfContentsPath) {
let root: string = path.dirname(path.dirname(this._tableOfContentsPath));
try { try {
let fileContents = await fsPromises.readFile(path.join(root, '_config.yml'), 'utf-8'); let fileContents = await fsPromises.readFile(path.join(root, '_config.yml'), 'utf-8');
const config = yaml.safeLoad(fileContents.toString()); 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()); const tableOfContents: any = yaml.safeLoad(fileContents.toString());
let book: BookTreeItem = new BookTreeItem({ let book: BookTreeItem = new BookTreeItem({
title: config.title, title: config.title,
contentPath: this._tableOfContentsPath,
root: root, root: root,
tableOfContents: { sections: this.parseJupyterSections(tableOfContents) }, tableOfContents: { sections: this.parseJupyterSections(tableOfContents) },
page: tableOfContents, page: tableOfContents,
@@ -114,6 +150,7 @@ export class BookModel implements azdata.nb.NavigationProvider {
if (sections[i].external) { if (sections[i].external) {
let externalLink: BookTreeItem = new BookTreeItem({ let externalLink: BookTreeItem = new BookTreeItem({
title: sections[i].title, title: sections[i].title,
contentPath: undefined,
root: root, root: root,
tableOfContents: tableOfContents, tableOfContents: tableOfContents,
page: sections[i], page: sections[i],
@@ -136,6 +173,7 @@ export class BookModel implements azdata.nb.NavigationProvider {
if (await fs.pathExists(pathToNotebook)) { if (await fs.pathExists(pathToNotebook)) {
let notebook = new BookTreeItem({ let notebook = new BookTreeItem({
title: sections[i].title, title: sections[i].title,
contentPath: pathToNotebook,
root: root, root: root,
tableOfContents: tableOfContents, tableOfContents: tableOfContents,
page: sections[i], page: sections[i],
@@ -165,6 +203,7 @@ export class BookModel implements azdata.nb.NavigationProvider {
} else if (await fs.pathExists(pathToMarkdown)) { } else if (await fs.pathExists(pathToMarkdown)) {
let markdown: BookTreeItem = new BookTreeItem({ let markdown: BookTreeItem = new BookTreeItem({
title: sections[i].title, title: sections[i].title,
contentPath: pathToMarkdown,
root: root, root: root,
tableOfContents: tableOfContents, tableOfContents: tableOfContents,
page: sections[i], page: sections[i],
@@ -208,8 +247,8 @@ export class BookModel implements azdata.nb.NavigationProvider {
} }
public get tableOfContentPaths(): string[] { public get tableOfContentsPath(): string {
return this._tableOfContentPaths; return this._tableOfContentsPath;
} }
getNavigation(uri: vscode.Uri): Thenable<azdata.nb.NavigationResult> { getNavigation(uri: vscode.Uri): Thenable<azdata.nb.NavigationResult> {

View File

@@ -18,6 +18,7 @@ export enum BookTreeItemType {
export interface BookTreeItemFormat { export interface BookTreeItemFormat {
title: string; title: string;
contentPath: string;
root: string; root: string;
tableOfContents: IJupyterBookToc; tableOfContents: IJupyterBookToc;
page: any; page: any;
@@ -47,6 +48,12 @@ export class BookTreeItem extends vscode.TreeItem {
} else { } else {
if (book.page && book.page.sections && book.page.sections.length > 0) { if (book.page && book.page.sections && book.page.sections.length > 0) {
this.contextValue = 'section'; 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.setPageVariables();
this.setCommand(); this.setCommand();
@@ -63,19 +70,19 @@ export class BookTreeItem extends vscode.TreeItem {
this._sections = this.book.page.sections || this.book.page.subsections; this._sections = this.book.page.sections || this.book.page.subsections;
this._uri = this.book.page.url; this._uri = this.book.page.url;
if (this.book.tableOfContents.sections) {
let index = (this.book.tableOfContents.sections.indexOf(this.book.page)); let index = (this.book.tableOfContents.sections.indexOf(this.book.page));
this.setPreviousUri(index); this.setPreviousUri(index);
this.setNextUri(index); this.setNextUri(index);
} }
}
private setCommand() { private setCommand() {
if (this.book.type === BookTreeItemType.Notebook) { 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) // 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: [this.book.contentPath], };
this.command = { command: this.book.isUntitled ? 'bookTreeView.openUntitledNotebook' : 'bookTreeView.openNotebook', title: loc.openNotebookCommand, arguments: [pathToNotebook], };
} else if (this.book.type === BookTreeItemType.Markdown) { } 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: [this.book.contentPath], };
this.command = { command: 'bookTreeView.openMarkdown', title: loc.openMarkdownCommand, arguments: [pathToMarkdown], };
} else if (this.book.type === BookTreeItemType.ExternalLink) { } else if (this.book.type === BookTreeItemType.ExternalLink) {
this.command = { command: 'bookTreeView.openExternalLink', title: loc.openExternalLinkCommand, arguments: [this._uri], }; this.command = { command: 'bookTreeView.openExternalLink', title: loc.openExternalLinkCommand, arguments: [this._uri], };
} }
@@ -146,7 +153,7 @@ export class BookTreeItem extends vscode.TreeItem {
return `${this._uri}`; return `${this._uri}`;
} }
else { 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 * as fsw from 'fs';
import { IPrompter, QuestionTypes, IQuestion } from '../prompts/question'; import { IPrompter, QuestionTypes, IQuestion } from '../prompts/question';
import CodeAdapter from '../prompts/adapter'; import CodeAdapter from '../prompts/adapter';
import { BookTreeItem } from './bookTreeItem'; import { BookTreeItem, BookTreeItemType } from './bookTreeItem';
import { BookModel } from './bookModel'; import { BookModel } from './bookModel';
import { Deferred } from '../common/promise'; import { Deferred } from '../common/promise';
import { IBookTrustManager, BookTrustManager } from './bookTrustManager'; import { IBookTrustManager, BookTrustManager } from './bookTrustManager';
import * as loc from '../common/localizedConstants'; import * as loc from '../common/localizedConstants';
import { ApiWrapper } from '../common/apiWrapper'; import { ApiWrapper } from '../common/apiWrapper';
import * as glob from 'fast-glob';
import { isNullOrUndefined } from 'util';
const Content = 'content'; const Content = 'content';
interface BookSearchResults {
notebookPaths: string[];
bookPaths: string[];
}
export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeItem> { export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeItem> {
private _onDidChangeTreeData: vscode.EventEmitter<BookTreeItem | undefined> = new vscode.EventEmitter<BookTreeItem | undefined>(); private _onDidChangeTreeData: vscode.EventEmitter<BookTreeItem | undefined> = new vscode.EventEmitter<BookTreeItem | undefined>();
readonly onDidChangeTreeData: vscode.Event<BookTreeItem | undefined> = this._onDidChangeTreeData.event; 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 vscode.commands.executeCommand('setContext', 'unsavedBooks', this._openAsUntitled);
await Promise.all(workspaceFolders.map(async (workspaceFolder) => { await Promise.all(workspaceFolders.map(async (workspaceFolder) => {
try { try {
await this.createAndAddBookModel(workspaceFolder.uri.fsPath); await this.loadNotebooksInFolder(workspaceFolder.uri.fsPath);
} catch { } catch {
// no-op, not all workspace folders are going to be valid books // 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 { 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. // Check if the book is already open in viewlet.
if (books.length > 0 && books[0].bookItems.length > 0) { let existingBook = this.books.find(book => book.bookPath === bookPath);
this.currentBook = books[0]; if (existingBook?.bookItems.length > 0) {
await this.showPreviewFile(urlToOpen); this.currentBook = existingBook;
} } else {
else { await this.createAndAddBookModel(bookPath, isNotebook);
await this.createAndAddBookModel(bookPath);
let bookViewer = vscode.window.createTreeView(this.viewId, { showCollapseAll: true, treeDataProvider: this }); 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 }); bookViewer.reveal(this.currentBook.bookItems[0], { expand: vscode.TreeItemCollapsibleState.Expanded, focus: true, select: true });
}
if (showPreview) {
await this.showPreviewFile(urlToOpen); await this.showPreviewFile(urlToOpen);
} }
// add file watcher on toc file. // add file watcher on toc file.
fsw.watch(path.join(bookPath, '_data', 'toc.yml'), async (event, filename) => { if (!isNotebook) {
fsw.watch(path.join(this.currentBook.bookPath, '_data', 'toc.yml'), async (event, filename) => {
if (event === 'change') { if (event === 'change') {
let index = this.books.findIndex(book => book.bookPath === bookPath); let changedBook = this.books.find(book => book.bookPath === bookPath);
await this.books[index].initializeContents().then(() => { await changedBook.initializeContents().then(() => {
this._onDidChangeTreeData.fire(this.books[index].bookItems[0]); this._onDidChangeTreeData.fire(changedBook.bookItems[0]);
}); });
this._onDidChangeTreeData.fire(); this._onDidChangeTreeData.fire();
} }
}); });
}
} catch (e) { } catch (e) {
vscode.window.showErrorMessage(loc.openFileError(bookPath, e instanceof Error ? e.message : 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 // remove book from the saved books
let deletedBook: BookModel; let deletedBook: BookModel;
try { 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) { if (index > -1) {
deletedBook = this.books.splice(index, 1)[0]; deletedBook = this.books.splice(index, 1)[0];
if (this.currentBook === deletedBook) { 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)); vscode.window.showErrorMessage(loc.closeBookError(book.root, e instanceof Error ? e.message : e));
} finally { } finally {
// remove watch on toc file. // remove watch on toc file.
if (deletedBook) { if (deletedBook && !deletedBook.isNotebook) {
fsw.unwatchFile(path.join(deletedBook.bookPath, '_data', 'toc.yml')); 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. * were able to successfully parse it.
* @param bookPath The path to the book folder to create the model for * @param bookPath The path to the book folder to create the model for
*/ */
private async createAndAddBookModel(bookPath: string): Promise<void> { private async createAndAddBookModel(bookPath: string, isNotebook: boolean): Promise<void> {
const book: BookModel = new BookModel(bookPath, this._openAsUntitled, this._extensionContext); const book: BookModel = new BookModel(bookPath, this._openAsUntitled, isNotebook, this._extensionContext);
await book.initializeContents(); await book.initializeContents();
this.books.push(book); this.books.push(book);
if (!this.currentBook) { if (!this.currentBook) {
@@ -271,7 +287,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
canSelectFiles: false, canSelectFiles: false,
canSelectMany: false, canSelectMany: false,
canSelectFolders: true, canSelectFolders: true,
openLabel: loc.labelPickFolder openLabel: loc.labelSelectFolder
}); });
if (uris && uris.length > 0) { if (uris && uris.length > 0) {
let pickedFolder = uris[0]; let pickedFolder = uris[0];
@@ -328,10 +344,58 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
}); });
if (uris && uris.length > 0) { if (uris && uris.length > 0) {
let bookPath = uris[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) { private runThrottledAction(resource: string, action: () => void) {
const isResourceChange = resource !== this._resource; const isResourceChange = resource !== this._resource;
if (isResourceChange) { if (isResourceChange) {
@@ -378,11 +442,11 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
return Promise.resolve([]); return Promise.resolve([]);
} }
} else { } else {
let booksitems: BookTreeItem[] = []; let bookItems: BookTreeItem[] = [];
this.books.map(book => { 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 // Book view-let constants
export const allFiles = localize('allFiles', "All Files"); 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 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 confirmReplace = localize('confirmReplace', "Folder already exists. Are you sure you want to delete and replace this folder?");
export const openNotebookCommand = localize('openNotebookCommand', "Open Notebook"); 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 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 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 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 missingFileError(title: string): string { return localize('missingFileError', "Missing file : {0}", title); }
export function invalidTocFileError(): string { return localize('InvalidError.tocFile', "Invalid toc file"); } 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> { export async function activate(extensionContext: vscode.ExtensionContext): Promise<IExtensionApi> {
const createBookPath: string = path.posix.join(extensionContext.extensionPath, 'resources', 'notebooks', 'JupyterBooksCreate.ipynb'); 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.openNotebook', (resource) => bookTreeViewProvider.openNotebook(resource)));
extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openUntitledNotebook', (resource) => untitledBookTreeViewProvider.openNotebookAsUntitled(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.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.searchUntitledBook', () => untitledBookTreeViewProvider.searchJupyterBooks()));
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.openBook', () => bookTreeViewProvider.openNewBook())); 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.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 () => { extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.createBook', async () => {
let untitledFileName: vscode.Uri = vscode.Uri.parse(`untitled:${createBookPath}`); 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 () => { it('should ignore toc.yml files not in _data folder', async () => {
await bookTreeViewProvider.currentBook.getTableOfContentFiles(rootFolderPath); await bookTreeViewProvider.currentBook.loadTableOfContentFiles(rootFolderPath);
for (let p of bookTreeViewProvider.currentBook.tableOfContentPaths) { let path = bookTreeViewProvider.currentBook.tableOfContentsPath;
should(p.toLocaleLowerCase()).equal(tableOfContentsFile.replace(/\\/g, '/').toLocaleLowerCase()); should(path.toLocaleLowerCase()).equal(tableOfContentsFile.replace(/\\/g, '/').toLocaleLowerCase());
}
}); });
this.afterAll(async function (): Promise<void> { this.afterAll(async function (): Promise<void> {

View File

@@ -53,6 +53,7 @@ describe('BookTrustManagerTests', function () {
// Mock Book Data // Mock Book Data
let bookTreeItemFormat1: BookTreeItemFormat = { let bookTreeItemFormat1: BookTreeItemFormat = {
contentPath: undefined,
root: '/temp/SubFolder/', root: '/temp/SubFolder/',
tableOfContents: { tableOfContents: {
sections: [ sections: [
@@ -72,6 +73,7 @@ describe('BookTrustManagerTests', function () {
}; };
let bookTreeItemFormat2: BookTreeItemFormat = { let bookTreeItemFormat2: BookTreeItemFormat = {
contentPath: undefined,
root: '/temp/SubFolder2/', root: '/temp/SubFolder2/',
tableOfContents: { tableOfContents: {
sections: [ sections: [
@@ -88,6 +90,7 @@ describe('BookTrustManagerTests', function () {
}; };
let bookTreeItemFormat3: BookTreeItemFormat = { let bookTreeItemFormat3: BookTreeItemFormat = {
contentPath: undefined,
root: '/temp2/SubFolder3/', root: '/temp2/SubFolder3/',
tableOfContents: { tableOfContents: {
sections: [ sections: [
@@ -206,6 +209,7 @@ describe('BookTrustManagerTests', function () {
apiWrapperMock.setup(api => api.getWorkspaceFolders()).returns(() => []); apiWrapperMock.setup(api => api.getWorkspaceFolders()).returns(() => []);
apiWrapperMock.setup(api => api.getConfiguration(TypeMoq.It.isValue(constants.notebookConfigKey))).returns(() => workspaceConfigurtionMock.object); apiWrapperMock.setup(api => api.getConfiguration(TypeMoq.It.isValue(constants.notebookConfigKey))).returns(() => workspaceConfigurtionMock.object);
let bookTreeItemFormat1: BookTreeItemFormat = { let bookTreeItemFormat1: BookTreeItemFormat = {
contentPath: undefined,
root: '/temp/SubFolder/', root: '/temp/SubFolder/',
tableOfContents: { tableOfContents: {
sections: [ sections: [
@@ -225,6 +229,7 @@ describe('BookTrustManagerTests', function () {
}; };
let bookTreeItemFormat2: BookTreeItemFormat = { let bookTreeItemFormat2: BookTreeItemFormat = {
contentPath: undefined,
root: '/temp/SubFolder2/', root: '/temp/SubFolder2/',
tableOfContents: { tableOfContents: {
sections: [ sections: [

View File

@@ -538,7 +538,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
} }
isActive(): boolean { isActive(): boolean {
return this.editorService.activeEditor.matches(this.notebookParams.input); return this.editorService.activeEditor ? this.editorService.activeEditor.matches(this.notebookParams.input) : false;
} }
isVisible(): boolean { isVisible(): boolean {