mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-06 09:35:41 -05:00
Add Open Notebook Folder functionality to Books viewlet. (#9939)
This commit is contained in:
@@ -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": [
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 |
3
extensions/notebook/resources/light/open_folder.svg
Normal file
3
extensions/notebook/resources/light/open_folder.svg
Normal 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 |
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"); }
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user