Pinning Notebooks on Notebooks view (#11963)

* initial commit

* added tests

* code cleanup and more tests

* add missed util test

* changes to address comments

* remove pin from resources
This commit is contained in:
Maddy
2020-08-31 08:53:11 -07:00
committed by GitHub
parent b4a3325a21
commit ae830d9e64
15 changed files with 506 additions and 26 deletions

View File

@@ -59,6 +59,14 @@
"type": "number",
"default": 60000,
"description": "%notebook.remoteBookDownloadTimeout.description%"
},
"notebook.pinnedNotebooks": {
"type": "array",
"default": [],
"description": "%notebook.pinnedNotebooks.description%",
"items": {
"type": "string"
}
}
}
},
@@ -247,6 +255,21 @@
"dark": "resources/dark/open_notebook_inverse.svg",
"light": "resources/light/open_notebook.svg"
}
},
{
"command": "notebook.command.pinNotebook",
"title": "%title.pinNotebook%",
"category": "%books-preview-category%",
"icon": "$(pinned)"
},
{
"command": "notebook.command.unpinNotebook",
"title": "%title.unpinNotebook%",
"category": "%books-preview-category%",
"icon": {
"dark": "resources/dark/unpin_inverse.svg",
"light": "resources/light/unpin.svg"
}
}
],
"languages": [
@@ -364,6 +387,14 @@
{
"command": "notebook.command.revealInBooksViewlet",
"when": "false"
},
{
"command": "notebook.command.pinNotebook",
"when": "false"
},
{
"command": "notebook.command.unpinNotebook",
"when": "false"
}
],
"notebooks/title": [
@@ -424,6 +455,16 @@
{
"command": "notebook.command.closeNotebook",
"when": "view == bookTreeView && viewItem == savedNotebook"
},
{
"command": "notebook.command.pinNotebook",
"when": "view == bookTreeView && viewItem == savedNotebook",
"group": "inline"
},
{
"command": "notebook.command.unpinNotebook",
"when": "view == pinnedBooksView || view == bookTreeView && viewItem == pinnedNotebook",
"group": "inline"
}
],
"view/title": [
@@ -454,6 +495,11 @@
},
"views": {
"notebooks": [
{
"id": "pinnedBooksView",
"name": "%title.PinnedBooks%",
"when": "showPinnedbooks"
},
{
"id": "bookTreeView",
"name": "%title.SavedBooks%"

View File

@@ -10,6 +10,7 @@
"notebook.maxBookSearchDepth.description": "Maximum depth of subdirectories to search for Books (Enter 0 for infinite)",
"notebook.collapseBookItems.description": "Collapse Book items at root level in the Notebooks Viewlet",
"notebook.remoteBookDownloadTimeout.description": "Download timeout in milliseconds for GitHub books",
"notebook.pinnedNotebooks.description": "Notebooks that are pinned by the user for the current workspace",
"notebook.command.new": "New Notebook",
"notebook.command.open": "Open Notebook",
"notebook.analyzeJupyterNotebook": "Analyze in Notebook",
@@ -37,6 +38,7 @@
"title.searchJupyterBook": "Search Book",
"title.SavedBooks": "Notebooks",
"title.ProvidedBooks": "Provided Books",
"title.PinnedBooks": "Pinned notebooks",
"title.PreviewLocalizedBook": "Get localized SQL Server 2019 guide",
"title.openJupyterBook": "Open Jupyter Book",
"title.closeJupyterBook": "Close Jupyter Book",
@@ -44,5 +46,7 @@
"title.revealInBooksViewlet": "Reveal in Books",
"title.createJupyterBook": "Create Book (Preview)",
"title.openNotebookFolder": "Open Notebooks in Folder",
"title.openRemoteJupyterBook": "Add Remote Jupyter Book"
"title.openRemoteJupyterBook": "Add Remote Jupyter Book",
"title.pinNotebook": "Pin Notebook",
"title.unpinNotebook": "Unpin Notebook"
}

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="M11.5 9C11.9844 9 12.4375 9.09115 12.8594 9.27344C13.2812 9.45573 13.6536 9.70573 13.9766 10.0234C14.2995 10.3411 14.5495 10.7109 14.7266 11.1328C14.9036 11.5547 14.9948 12.0104 15 12.5C15 12.9844 14.9089 13.4375 14.7266 13.8594C14.5443 14.2812 14.2943 14.6536 13.9766 14.9766C13.6589 15.2995 13.2891 15.5495 12.8672 15.7266C12.4453 15.9036 11.9896 15.9948 11.5 16C11.0156 16 10.5625 15.9089 10.1406 15.7266C9.71875 15.5443 9.34635 15.2943 9.02344 14.9766C8.70052 14.6589 8.45052 14.2891 8.27344 13.8672C8.09635 13.4453 8.00521 12.9896 8 12.5C8 12.0156 8.09115 11.5625 8.27344 11.1406C8.45573 10.7188 8.70573 10.3464 9.02344 10.0234C9.34115 9.70052 9.71094 9.45052 10.1328 9.27344C10.5547 9.09635 11.0104 9.00521 11.5 9ZM9 12.5C9 12.8438 9.0651 13.1667 9.19531 13.4688C9.32552 13.7708 9.5026 14.0365 9.72656 14.2656C9.95052 14.4948 10.2161 14.6745 10.5234 14.8047C10.8307 14.9349 11.1562 15 11.5 15C11.7448 15 11.9844 14.9661 12.2188 14.8984C12.4531 14.8307 12.6719 14.7266 12.875 14.5859L9.41406 11.125C9.27865 11.3281 9.17708 11.5469 9.10938 11.7812C9.04167 12.0156 9.00521 12.2552 9 12.5ZM13.5859 13.875C13.7214 13.6719 13.8229 13.4531 13.8906 13.2188C13.9583 12.9844 13.9948 12.7448 14 12.5C14 12.1562 13.9349 11.8333 13.8047 11.5312C13.6745 11.2292 13.4948 10.9661 13.2656 10.7422C13.0365 10.5182 12.7708 10.3385 12.4688 10.2031C12.1667 10.0677 11.8438 10 11.5 10C11.2552 10 11.0156 10.0339 10.7812 10.1016C10.5469 10.1693 10.3281 10.2734 10.125 10.4141L13.5859 13.875ZM15.5469 5.84375C15.375 6.01562 15.2083 6.17188 15.0469 6.3125C14.8854 6.45312 14.7161 6.57552 14.5391 6.67969C14.362 6.78385 14.1719 6.85938 13.9688 6.90625C13.7656 6.95312 13.5312 6.98177 13.2656 6.99219C13.0885 6.99219 12.9193 6.97656 12.7578 6.94531L11.7109 8H10.2891L12.4609 5.82812L12.5391 5.85156C12.6589 5.88802 12.7786 5.91927 12.8984 5.94531C13.0182 5.97135 13.1406 5.98698 13.2656 5.99219C13.5521 5.99219 13.8229 5.92448 14.0781 5.78906L10.2109 1.92188C10.0755 2.17708 10.0078 2.44792 10.0078 2.73438C10.0078 2.85938 10.0208 2.98177 10.0469 3.10156C10.0729 3.22135 10.1068 3.34115 10.1484 3.46094L10.1719 3.53906L6.32812 7.375C6.20312 7.32292 6.08333 7.27344 5.96875 7.22656C5.85417 7.17969 5.73958 7.14062 5.625 7.10938C5.51042 7.07812 5.39323 7.05469 5.27344 7.03906C5.15365 7.02344 5.02083 7.01302 4.875 7.00781C4.57812 7.00781 4.28906 7.04948 4.00781 7.13281C3.72656 7.21615 3.46615 7.34375 3.22656 7.51562L7 11.2891V12.7109L5.5 11.2031L1.0625 15.6484L0 16L0.351562 14.9375L4.79688 10.5L1.78125 7.48438L2.13281 7.13281C2.4974 6.76823 2.91927 6.48958 3.39844 6.29688C3.8776 6.10417 4.375 6.00781 4.89062 6.00781C5.09375 6.00781 5.29427 6.02344 5.49219 6.05469C5.6901 6.08594 5.89062 6.13542 6.09375 6.20312L9.05469 3.24219C9.02344 3.08073 9.00781 2.91146 9.00781 2.73438C9.00781 2.47917 9.03385 2.25 9.08594 2.04688C9.13802 1.84375 9.21615 1.65104 9.32031 1.46875C9.42448 1.28646 9.54427 1.11719 9.67969 0.960938C9.8151 0.804688 9.97396 0.635417 10.1562 0.453125L15.5469 5.84375Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 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="M11.5 9C11.9844 9 12.4375 9.09115 12.8594 9.27344C13.2812 9.45573 13.6536 9.70573 13.9766 10.0234C14.2995 10.3411 14.5495 10.7109 14.7266 11.1328C14.9036 11.5547 14.9948 12.0104 15 12.5C15 12.9844 14.9089 13.4375 14.7266 13.8594C14.5443 14.2812 14.2943 14.6536 13.9766 14.9766C13.6589 15.2995 13.2891 15.5495 12.8672 15.7266C12.4453 15.9036 11.9896 15.9948 11.5 16C11.0156 16 10.5625 15.9089 10.1406 15.7266C9.71875 15.5443 9.34635 15.2943 9.02344 14.9766C8.70052 14.6589 8.45052 14.2891 8.27344 13.8672C8.09635 13.4453 8.00521 12.9896 8 12.5C8 12.0156 8.09115 11.5625 8.27344 11.1406C8.45573 10.7188 8.70573 10.3464 9.02344 10.0234C9.34115 9.70052 9.71094 9.45052 10.1328 9.27344C10.5547 9.09635 11.0104 9.00521 11.5 9ZM9 12.5C9 12.8438 9.0651 13.1667 9.19531 13.4688C9.32552 13.7708 9.5026 14.0365 9.72656 14.2656C9.95052 14.4948 10.2161 14.6745 10.5234 14.8047C10.8307 14.9349 11.1562 15 11.5 15C11.7448 15 11.9844 14.9661 12.2188 14.8984C12.4531 14.8307 12.6719 14.7266 12.875 14.5859L9.41406 11.125C9.27865 11.3281 9.17708 11.5469 9.10938 11.7812C9.04167 12.0156 9.00521 12.2552 9 12.5ZM13.5859 13.875C13.7214 13.6719 13.8229 13.4531 13.8906 13.2188C13.9583 12.9844 13.9948 12.7448 14 12.5C14 12.1562 13.9349 11.8333 13.8047 11.5312C13.6745 11.2292 13.4948 10.9661 13.2656 10.7422C13.0365 10.5182 12.7708 10.3385 12.4688 10.2031C12.1667 10.0677 11.8438 10 11.5 10C11.2552 10 11.0156 10.0339 10.7812 10.1016C10.5469 10.1693 10.3281 10.2734 10.125 10.4141L13.5859 13.875ZM15.5469 5.84375C15.375 6.01562 15.2083 6.17188 15.0469 6.3125C14.8854 6.45312 14.7161 6.57552 14.5391 6.67969C14.362 6.78385 14.1719 6.85938 13.9688 6.90625C13.7656 6.95312 13.5312 6.98177 13.2656 6.99219C13.0885 6.99219 12.9193 6.97656 12.7578 6.94531L11.7109 8H10.2891L12.4609 5.82812L12.5391 5.85156C12.6589 5.88802 12.7786 5.91927 12.8984 5.94531C13.0182 5.97135 13.1406 5.98698 13.2656 5.99219C13.5521 5.99219 13.8229 5.92448 14.0781 5.78906L10.2109 1.92188C10.0755 2.17708 10.0078 2.44792 10.0078 2.73438C10.0078 2.85938 10.0208 2.98177 10.0469 3.10156C10.0729 3.22135 10.1068 3.34115 10.1484 3.46094L10.1719 3.53906L6.32812 7.375C6.20312 7.32292 6.08333 7.27344 5.96875 7.22656C5.85417 7.17969 5.73958 7.14062 5.625 7.10938C5.51042 7.07812 5.39323 7.05469 5.27344 7.03906C5.15365 7.02344 5.02083 7.01302 4.875 7.00781C4.57812 7.00781 4.28906 7.04948 4.00781 7.13281C3.72656 7.21615 3.46615 7.34375 3.22656 7.51562L7 11.2891V12.7109L5.5 11.2031L1.0625 15.6484L0 16L0.351562 14.9375L4.79688 10.5L1.78125 7.48438L2.13281 7.13281C2.4974 6.76823 2.91927 6.48958 3.39844 6.29688C3.8776 6.10417 4.375 6.00781 4.89062 6.00781C5.09375 6.00781 5.29427 6.02344 5.49219 6.05469C5.6901 6.08594 5.89062 6.13542 6.09375 6.20312L9.05469 3.24219C9.02344 3.08073 9.00781 2.91146 9.00781 2.73438C9.00781 2.47917 9.03385 2.25 9.08594 2.04688C9.13802 1.84375 9.21615 1.65104 9.32031 1.46875C9.42448 1.28646 9.54427 1.11719 9.67969 0.960938C9.8151 0.804688 9.97396 0.635417 10.1562 0.453125L15.5469 5.84375Z" fill="#323130"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,89 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import * as vscode from 'vscode';
import * as constants from './../common/constants';
import { BookTreeItem } from './bookTreeItem';
import { getPinnedNotebooks } from '../common/utils';
export interface IBookPinManager {
pinNotebook(notebook: BookTreeItem): boolean;
unpinNotebook(notebook: BookTreeItem): boolean;
}
enum PinBookOperation {
Pin,
Unpin
}
export class BookPinManager implements IBookPinManager {
constructor() {
this.setPinnedSectionContext();
}
setPinnedSectionContext(): void {
if (getPinnedNotebooks().length > 0) {
vscode.commands.executeCommand(constants.BuiltInCommands.SetContext, constants.showPinnedBooksContextKey, true);
} else {
vscode.commands.executeCommand(constants.BuiltInCommands.SetContext, constants.showPinnedBooksContextKey, false);
}
}
isNotebookPinned(notebookPath: string): boolean {
if (getPinnedNotebooks().findIndex(x => x === notebookPath) > -1) {
return true;
}
return false;
}
pinNotebook(notebook: BookTreeItem): boolean {
return this.isNotebookPinned(notebook.book.contentPath) ? false : this.updatePinnedBooks(notebook, PinBookOperation.Pin);
}
unpinNotebook(notebook: BookTreeItem): boolean {
return this.updatePinnedBooks(notebook, PinBookOperation.Unpin);
}
getPinnedBookPathsInConfig(): string[] {
let config: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration(constants.notebookConfigKey);
let pinnedBookDirectories: string[] = config.get(constants.pinnedBooksConfigKey);
return pinnedBookDirectories;
}
setPinnedBookPathsInConfig(bookPaths: string[]) {
let config: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration(constants.notebookConfigKey);
let storeInWorspace: boolean = this.hasWorkspaceFolders();
config.update(constants.pinnedBooksConfigKey, bookPaths, storeInWorspace ? false : vscode.ConfigurationTarget.Global);
}
hasWorkspaceFolders(): boolean {
let workspaceFolders = vscode.workspace.workspaceFolders;
return workspaceFolders && workspaceFolders.length > 0;
}
updatePinnedBooks(notebook: BookTreeItem, operation: PinBookOperation) {
let modifiedPinnedBooks = false;
let bookPathToChange: string = notebook.book.contentPath;
let pinnedBooks: string[] = this.getPinnedBookPathsInConfig();
let existingBookIndex = pinnedBooks.map(pinnedBookPath => path.normalize(pinnedBookPath)).indexOf(bookPathToChange);
if (existingBookIndex !== -1 && operation === PinBookOperation.Unpin) {
pinnedBooks.splice(existingBookIndex, 1);
modifiedPinnedBooks = true;
} else if (existingBookIndex === -1 && operation === PinBookOperation.Pin) {
pinnedBooks.push(bookPathToChange);
modifiedPinnedBooks = true;
}
this.setPinnedBookPathsInConfig(pinnedBooks);
this.setPinnedSectionContext();
return modifiedPinnedBooks;
}
}

View File

@@ -8,6 +8,7 @@ import * as path from 'path';
import * as fs from 'fs';
import { IJupyterBookSection, IJupyterBookToc } from '../contracts/content';
import * as loc from '../common/localizedConstants';
import { isBookItemPinned } from '../common/utils';
export enum BookTreeItemType {
Book = 'Book',
@@ -54,6 +55,8 @@ export class BookTreeItem extends vscode.TreeItem {
} else {
this.contextValue = 'savedNotebook';
}
} else {
this.contextValue = book.type === BookTreeItemType.Notebook ? (isBookItemPinned(book.contentPath) ? 'pinnedNotebook' : 'savedNotebook') : 'section';
}
this.setPageVariables();
this.setCommand();

View File

@@ -17,7 +17,8 @@ import { IBookTrustManager, BookTrustManager } from './bookTrustManager';
import * as loc from '../common/localizedConstants';
import * as glob from 'fast-glob';
import { isNullOrUndefined } from 'util';
import { debounce } from '../common/utils';
import { debounce, getPinnedNotebooks } from '../common/utils';
import { IBookPinManager, BookPinManager } from './bookPinManager';
const Content = 'content';
@@ -34,6 +35,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
private _initializeDeferred: Deferred<void> = new Deferred<void>();
private _openAsUntitled: boolean;
private _bookTrustManager: IBookTrustManager;
public bookPinManager: IBookPinManager;
private _bookViewer: vscode.TreeView<BookTreeItem>;
public viewId: string;
@@ -44,8 +46,9 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
this._openAsUntitled = openAsUntitled;
this._extensionContext = extensionContext;
this.books = [];
this.initialize(workspaceFolders).catch(e => console.error(e));
this.bookPinManager = new BookPinManager();
this.viewId = view;
this.initialize(workspaceFolders).catch(e => console.error(e));
this.prompter = new CodeAdapter();
this._bookTrustManager = new BookTrustManager(this.books);
@@ -53,13 +56,24 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
}
private async initialize(workspaceFolders: vscode.WorkspaceFolder[]): Promise<void> {
await Promise.all(workspaceFolders.map(async (workspaceFolder) => {
try {
await this.loadNotebooksInFolder(workspaceFolder.uri.fsPath);
} catch {
// no-op, not all workspace folders are going to be valid books
}
}));
if (this.viewId === constants.PINNED_BOOKS_VIEWID) {
await Promise.all(getPinnedNotebooks().map(async (notebookPath) => {
try {
await this.createAndAddBookModel(notebookPath, true);
} catch {
// no-op, not all workspace folders are going to be valid books
}
}));
} else {
await Promise.all(workspaceFolders.map(async (workspaceFolder) => {
try {
await this.loadNotebooksInFolder(workspaceFolder.uri.fsPath);
} catch {
// no-op, not all workspace folders are going to be valid books
}
}));
}
this._initializeDeferred.resolve();
}
@@ -97,6 +111,26 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
}
}
async pinNotebook(bookTreeItem: BookTreeItem): Promise<void> {
let bookPathToUpdate = bookTreeItem.book?.contentPath;
if (bookPathToUpdate) {
let pinStatusChanged = this.bookPinManager.pinNotebook(bookTreeItem);
if (pinStatusChanged) {
bookTreeItem.contextValue = 'pinnedNotebook';
}
}
}
async unpinNotebook(bookTreeItem: BookTreeItem): Promise<void> {
let bookPathToUpdate = bookTreeItem.book?.contentPath;
if (bookPathToUpdate) {
let pinStatusChanged = this.bookPinManager.unpinNotebook(bookTreeItem);
if (pinStatusChanged) {
bookTreeItem.contextValue = 'savedNotebook';
}
}
}
async openBook(bookPath: string, urlToOpen?: string, showPreview?: boolean, isNotebook?: boolean): Promise<void> {
try {
// Convert path to posix style for easier comparisons
@@ -132,6 +166,20 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
}
}
async addNotebookToPinnedView(bookItem: BookTreeItem): Promise<void> {
let notebookPath: string = bookItem.book.contentPath;
if (notebookPath) {
await this.createAndAddBookModel(notebookPath, true);
}
}
async removeNotebookFromPinnedView(bookItem: BookTreeItem): Promise<void> {
let notebookPath: string = bookItem.book.contentPath;
if (notebookPath) {
await this.closeBook(bookItem);
}
}
@debounce(1500)
async fireBookRefresh(book: BookModel): Promise<void> {
await book.initializeContents().then(() => {
@@ -169,21 +217,23 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
* @param bookPath The path to the book folder to create the model for
*/
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) {
this.currentBook = book;
}
this._bookViewer = vscode.window.createTreeView(this.viewId, { showCollapseAll: true, treeDataProvider: this });
this._bookViewer.onDidChangeVisibility(e => {
let openDocument = azdata.nb.activeNotebookEditor;
let notebookPath = openDocument?.document.uri;
// call reveal only once on the correct view
if (e.visible && ((!this._openAsUntitled && notebookPath?.scheme !== 'untitled') || (this._openAsUntitled && notebookPath?.scheme === 'untitled'))) {
this.revealActiveDocumentInViewlet();
if (!this.books.find(x => x.bookPath === bookPath)) {
const book: BookModel = new BookModel(bookPath, this._openAsUntitled, isNotebook, this._extensionContext);
await book.initializeContents();
this.books.push(book);
if (!this.currentBook) {
this.currentBook = book;
}
});
this._bookViewer = vscode.window.createTreeView(this.viewId, { showCollapseAll: true, treeDataProvider: this });
this._bookViewer.onDidChangeVisibility(e => {
let openDocument = azdata.nb.activeNotebookEditor;
let notebookPath = openDocument?.document.uri;
// call reveal only once on the correct view
if (e.visible && ((!this._openAsUntitled && notebookPath?.scheme !== 'untitled') || (this._openAsUntitled && notebookPath?.scheme === 'untitled'))) {
this.revealActiveDocumentInViewlet();
}
});
}
}
async showPreviewFile(urlToOpen?: string): Promise<void> {

View File

@@ -6,7 +6,7 @@
import * as vscode from 'vscode';
import { NotebookUtils } from './notebookUtils';
import { BookTreeViewProvider } from '../book/bookTreeView';
import { NavigationProviders, BOOKS_VIEWID, PROVIDED_BOOKS_VIEWID, extensionOutputChannelName } from './constants';
import { NavigationProviders, BOOKS_VIEWID, PROVIDED_BOOKS_VIEWID, PINNED_BOOKS_VIEWID, extensionOutputChannelName } from './constants';
/**
* Global context for the application
@@ -16,6 +16,7 @@ export class AppContext {
public readonly notebookUtils: NotebookUtils;
public readonly bookTreeViewProvider: BookTreeViewProvider;
public readonly providedBookTreeViewProvider: BookTreeViewProvider;
public readonly pinnedBookTreeViewProvider: BookTreeViewProvider;
public readonly outputChannel: vscode.OutputChannel;
constructor(public readonly extensionContext: vscode.ExtensionContext) {
@@ -24,6 +25,7 @@ export class AppContext {
let workspaceFolders = vscode.workspace.workspaceFolders?.slice() ?? [];
this.bookTreeViewProvider = new BookTreeViewProvider(workspaceFolders, extensionContext, false, BOOKS_VIEWID, NavigationProviders.NotebooksNavigator);
this.providedBookTreeViewProvider = new BookTreeViewProvider([], extensionContext, true, PROVIDED_BOOKS_VIEWID, NavigationProviders.ProvidedBooksNavigator);
this.pinnedBookTreeViewProvider = new BookTreeViewProvider([], extensionContext, false, PINNED_BOOKS_VIEWID, NavigationProviders.NotebooksNavigator);
this.outputChannel = vscode.window.createOutputChannel(extensionOutputChannelName);
}
}

View File

@@ -19,6 +19,7 @@ export const pythonPathConfigKey = 'pythonPath';
export const existingPythonConfigKey = 'useExistingPython';
export const notebookConfigKey = 'notebook';
export const trustedBooksConfigKey = 'trustedBooks';
export const pinnedBooksConfigKey = 'pinnedNotebooks';
export const maxBookSearchDepth = 'maxBookSearchDepth';
export const remoteBookDownloadTimeout = 'remoteBookDownloadTimeout';
export const collapseBookItems = 'collapseBookItems';
@@ -45,10 +46,13 @@ export const sparkScalaDisplayName = 'Spark | Scala';
export const sparkRDisplayName = 'Spark | R';
export const powershellDisplayName = 'PowerShell';
export const allKernelsName = 'All Kernels';
export const BOOKS_VIEWID = 'bookTreeView';
export const PROVIDED_BOOKS_VIEWID = 'providedBooksView';
export const PINNED_BOOKS_VIEWID = 'pinnedBooksView';
export const visitedNotebooksMementoKey = 'notebooks.visited';
export const pinnedNotebooksMementoKey = 'notebooks.pinned';
export enum BuiltInCommands {
SetContext = 'setContext'
@@ -69,6 +73,7 @@ export enum NavigationProviders {
}
export const unsavedBooksContextKey = 'unsavedBooks';
export const showPinnedBooksContextKey = 'showPinnedbooks';
export const pythonWindowsInstallUrl = 'https://go.microsoft.com/fwlink/?linkid=2110625';
export const pythonMacInstallUrl = 'https://go.microsoft.com/fwlink/?linkid=2128152';

View File

@@ -25,6 +25,8 @@ 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 function msgBookPinned(book: string): string { return localize('msgBookPinned', "Book {0} is now pinned in the workspace.", book); }
export function msgBookUnpinned(book: string): string { return localize('msgBookUnpinned', "Book {0} is no longer pinned in this workspace", book); }
export const missingTocError = localize('bookInitializeFailed', "Failed to find a Table of Contents file in the specified book.");
export const noBooksSelectedError = localize('noBooksSelected', "No books are currently selected in the viewlet.");

View File

@@ -9,7 +9,7 @@ import * as nls from 'vscode-nls';
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as crypto from 'crypto';
import { notebookLanguages } from './constants';
import { notebookLanguages, notebookConfigKey, pinnedBooksConfigKey } from './constants';
const localize = nls.loadMessageBundle();
@@ -323,3 +323,18 @@ export async function getRandomToken(size: number = 24): Promise<string> {
});
});
}
export function isBookItemPinned(notebookPath: string): boolean {
let pinnedNotebooks: string[] = getPinnedNotebooks();
if (pinnedNotebooks?.indexOf(notebookPath) > -1) {
return true;
}
return false;
}
export function getPinnedNotebooks(): string[] {
let config: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration(notebookConfigKey);
let pinnedNotebooks: string[] = config.get(pinnedBooksConfigKey) ?? [];
return pinnedNotebooks;
}

View File

@@ -39,6 +39,15 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi
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', (folderPath?: string, urlToOpen?: string, showPreview?: boolean,) => bookTreeViewProvider.openNotebookFolder(folderPath, urlToOpen, showPreview)));
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.pinNotebook', async (book: any) => {
await bookTreeViewProvider.pinNotebook(book);
await pinnedBookTreeViewProvider.addNotebookToPinnedView(book);
}));
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.unpinNotebook', async (book: any) => {
await bookTreeViewProvider.unpinNotebook(book);
await pinnedBookTreeViewProvider.removeNotebookFromPinnedView(book);
}));
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.createBook', async () => {
let untitledFileName: vscode.Uri = vscode.Uri.parse(`untitled:${createBookPath}`);
await vscode.workspace.openTextDocument(createBookPath).then((document) => {
@@ -128,6 +137,8 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi
await bookTreeViewProvider.initialized;
const providedBookTreeViewProvider = appContext.providedBookTreeViewProvider;
await providedBookTreeViewProvider.initialized;
const pinnedBookTreeViewProvider = appContext.pinnedBookTreeViewProvider;
await pinnedBookTreeViewProvider.initialized;
azdata.nb.onDidChangeActiveNotebookEditor(e => {
if (e.document.uri.scheme === 'untitled') {

View File

@@ -124,6 +124,7 @@ describe('BooksTreeViewTests', function () {
should(appContext).not.be.undefined();
should(appContext.bookTreeViewProvider).not.be.undefined();
should(appContext.providedBookTreeViewProvider).not.be.undefined();
should(appContext.pinnedBookTreeViewProvider).not.be.undefined();
});
it('should initialize correctly with empty workspace array', async () => {
@@ -346,6 +347,67 @@ describe('BooksTreeViewTests', function () {
});
describe('pinnedBookTreeViewProvider', function (): void {
let pinnedTreeViewProvider: BookTreeViewProvider;
let bookTreeViewProvider: BookTreeViewProvider;
let bookItem: BookTreeItem;
this.beforeAll(async () => {
pinnedTreeViewProvider = appContext.pinnedBookTreeViewProvider;
bookTreeViewProvider = appContext.bookTreeViewProvider;
let errorCase = new Promise((resolve, reject) => setTimeout(() => resolve(), 5000));
await Promise.race([bookTreeViewProvider.initialized, errorCase.then(() => { throw new Error('BookTreeViewProvider did not initialize in time'); })]);
await Promise.race([pinnedTreeViewProvider.initialized, errorCase.then(() => { throw new Error('PinnedTreeViewProvider did not initialize in time'); })]);
await bookTreeViewProvider.openBook(bookFolderPath, undefined, false, false);
bookItem = bookTreeViewProvider.books[0].bookItems[0];
});
afterEach(function (): void {
sinon.restore();
});
it('pinnedBookTreeViewProvider should not have any books when there are no pinned notebooks', async function (): Promise<void> {
const notebooks = pinnedTreeViewProvider.books;
should(notebooks.length).equal(0, 'Pinned Notebooks view should not have any notebooks');
});
it('pinNotebook should add notebook to pinnedBookTreeViewProvider', async function (): Promise<void> {
await vscode.commands.executeCommand('notebook.command.pinNotebook', bookItem);
const notebooks = pinnedTreeViewProvider.books;
should(notebooks.length).equal(1, 'Pinned Notebooks view should have a notebook');
});
it('unpinNotebook should remove notebook from pinnedBookTreeViewProvider', async function (): Promise<void> {
await vscode.commands.executeCommand('notebook.command.unpinNotebook', pinnedTreeViewProvider.books[0].bookItems[0]);
const notebooks = pinnedTreeViewProvider.books;
should(notebooks.length).equal(0, 'Pinned Notebooks view should not have any notebooks');
});
it('pinNotebook should invoke bookPinManagers pinNotebook method', async function (): Promise<void> {
let pinBookSpy = sinon.spy(bookTreeViewProvider.bookPinManager, 'pinNotebook');
await bookTreeViewProvider.pinNotebook(bookItem);
should(pinBookSpy.calledOnce).be.true('Should invoke bookPinManagers pinNotebook to update pinnedNotebooks config');
});
it('unpinNotebook should invoke bookPinManagers unpinNotebook method', async function (): Promise<void> {
let unpinNotebookSpy = sinon.spy(bookTreeViewProvider.bookPinManager, 'unpinNotebook');
await bookTreeViewProvider.unpinNotebook(bookItem);
should(unpinNotebookSpy.calledOnce).be.true('Should invoke bookPinManagers unpinNotebook to update pinnedNotebooks config');
});
it('addNotebookToPinnedView should add notebook to the TreeViewProvider', async function (): Promise<void> {
let notebooks = pinnedTreeViewProvider.books.length;
await pinnedTreeViewProvider.addNotebookToPinnedView(bookItem);
should(pinnedTreeViewProvider.books.length).equal(notebooks + 1, 'Should add the notebook as new item to the TreeViewProvider');
});
it('removeNotebookFromPinnedView should remove notebook from the TreeViewProvider', async function (): Promise<void> {
let notebooks = pinnedTreeViewProvider.books.length;
await pinnedTreeViewProvider.removeNotebookFromPinnedView(pinnedTreeViewProvider.books[0].bookItems[0]);
should(pinnedTreeViewProvider.books.length).equal(notebooks - 1, 'Should remove the notebook from the TreeViewProvider');
});
});
this.afterAll(async function (): Promise<void> {
console.log('Removing temporary files...');
if (await exists(rootFolderPath)) {

View File

@@ -0,0 +1,167 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as should from 'should';
import * as path from 'path';
import * as TypeMoq from 'typemoq';
import * as constants from '../../common/constants';
import { IBookPinManager, BookPinManager } from '../../book/bookPinManager';
import { BookTreeItem, BookTreeItemFormat, BookTreeItemType } from '../../book/bookTreeItem';
import * as vscode from 'vscode';
import { BookModel } from '../../book/bookModel';
import * as sinon from 'sinon';
import { isBookItemPinned } from '../../common/utils';
describe('BookPinManagerTests', function () {
describe('PinningNotebooks', () => {
let bookPinManager: IBookPinManager;
let pinnedNotebooks: string[];
let books: BookModel[];
afterEach(function (): void {
sinon.restore();
});
beforeEach(() => {
pinnedNotebooks = ['/temp/SubFolder/content/sample/notebook1.ipynb', '/temp/SubFolder/content/sample/notebook2.ipynb'];
// Mock Workspace Configuration
let workspaceConfigurtionMock: TypeMoq.IMock<vscode.WorkspaceConfiguration> = TypeMoq.Mock.ofType<vscode.WorkspaceConfiguration>();
workspaceConfigurtionMock.setup(config => config.get(TypeMoq.It.isValue(constants.pinnedBooksConfigKey))).returns(() => [].concat(pinnedNotebooks));
workspaceConfigurtionMock.setup(config => config.update(TypeMoq.It.isValue(constants.pinnedBooksConfigKey), TypeMoq.It.isAny(), TypeMoq.It.isValue(false))).returns((key: string, newValues: string[]) => {
pinnedNotebooks.splice(0, pinnedNotebooks.length, ...newValues);
return Promise.resolve();
});
sinon.replaceGetter(vscode.workspace, 'workspaceFolders', () => {
return <vscode.WorkspaceFolder[]>[{
uri: {
fsPath: '/temp/'
},
},
{
uri: {
fsPath: '/temp2/'
}
},
];
});
sinon.stub(vscode.workspace, 'getConfiguration').returns(workspaceConfigurtionMock.object);
// Mock Book Data
let bookTreeItemFormat1: BookTreeItemFormat = {
contentPath: '/temp/SubFolder/content/sample/notebook1.ipynb',
root: '/temp/SubFolder/',
tableOfContents: {
sections: [
{
url: path.join(path.sep, 'sample', 'notebook1')
},
{
url: path.join(path.sep, 'sample', 'notebook2')
}
]
},
isUntitled: undefined,
page: undefined,
title: undefined,
treeItemCollapsibleState: undefined,
type: BookTreeItemType.Book
};
let bookTreeItemFormat2: BookTreeItemFormat = {
contentPath: '/temp/SubFolder2/content/sample/notebook.ipynb',
root: '/temp/SubFolder2/',
tableOfContents: {
sections: [
{
url: path.join(path.sep, 'sample', 'notebook')
}
]
},
isUntitled: undefined,
page: undefined,
title: undefined,
treeItemCollapsibleState: undefined,
type: BookTreeItemType.Book
};
let bookTreeItemFormat3: BookTreeItemFormat = {
contentPath: '/temp2/SubFolder3/content/sample/notebook.ipynb',
root: '/temp2/SubFolder3/',
tableOfContents: {
sections: [
{
url: path.join(path.sep, 'sample', 'notebook')
}
]
},
isUntitled: undefined,
page: undefined,
title: undefined,
treeItemCollapsibleState: undefined,
type: BookTreeItemType.Book
};
let bookModel1Mock: TypeMoq.IMock<BookModel> = TypeMoq.Mock.ofType<BookModel>();
bookModel1Mock.setup(model => model.bookItems).returns(() => [new BookTreeItem(bookTreeItemFormat1, undefined), new BookTreeItem(bookTreeItemFormat2, undefined)]);
bookModel1Mock.setup(model => model.getNotebook(TypeMoq.It.isValue(path.join(path.sep, 'temp', 'SubFolder', 'content', 'sample', 'notebook.ipynb')))).returns((uri: string) => TypeMoq.Mock.ofType<BookTreeItem>().object);
bookModel1Mock.setup(model => model.getNotebook(TypeMoq.It.isValue(path.join(path.sep, 'temp', 'SubFolder', 'content', 'sample', 'notebook2.ipynb')))).returns((uri: string) => TypeMoq.Mock.ofType<BookTreeItem>().object);
bookModel1Mock.setup(model => model.getNotebook(TypeMoq.It.isValue(path.join(path.sep, 'temp', 'SubFolder2', 'content', 'sample', 'notebook.ipynb')))).returns((uri: string) => TypeMoq.Mock.ofType<BookTreeItem>().object);
bookModel1Mock.setup(model => model.getNotebook(TypeMoq.It.isAnyString())).returns((uri: string) => undefined);
let bookModel2Mock: TypeMoq.IMock<BookModel> = TypeMoq.Mock.ofType<BookModel>();
bookModel2Mock.setup(model => model.bookItems).returns(() => [new BookTreeItem(bookTreeItemFormat3, undefined)]);
bookModel2Mock.setup(model => model.getNotebook(TypeMoq.It.isValue(path.join(path.sep, 'temp2', 'SubFolder', 'content', 'sample', 'notebook.ipynb')))).returns((uri: string) => TypeMoq.Mock.ofType<BookTreeItem>().object);
bookModel2Mock.setup(model => model.getNotebook(TypeMoq.It.isAnyString())).returns((uri: string) => undefined);
books = [bookModel1Mock.object, bookModel2Mock.object];
bookPinManager = new BookPinManager();
});
it('should have notebooks in the pinnedBooksConfigKey when pinned within a workspace', async () => {
let notebookUri1 = books[0].bookItems[0].book.contentPath;
let isNotebook1Pinned = isBookItemPinned(notebookUri1);
should(isNotebook1Pinned).be.true('Notebook 1 should be pinned');
});
it('should NOT pin a notebook that is not pinned within a workspace', async () => {
let notebookUri = path.join(path.sep, 'temp', 'SubFolder2', 'content', 'sample', 'notebook.ipynb');
let isNotebookPinned = isBookItemPinned(notebookUri);
should(isNotebookPinned).be.false('Notebook should not be pinned');
});
it('should pin notebook after book has been pinned from viewlet within a workspace', async () => {
let notebookUri = books[0].bookItems[1].book.contentPath;
let isNotebookPinnedBeforeChange = isBookItemPinned(notebookUri);
should(isNotebookPinnedBeforeChange).be.false('Notebook should NOT be pinned');
// mock pin book item from viewlet
bookPinManager.pinNotebook(books[0].bookItems[1]);
let isNotebookPinnedAfterChange = isBookItemPinned(notebookUri);
should(isNotebookPinnedAfterChange).be.true('Notebook should be pinned');
});
it('should NOT pin a notebook when unpinned from viewlet within a workspace', async () => {
let notebookUri = books[0].bookItems[0].book.contentPath;
let isNotebookPinned = isBookItemPinned(notebookUri);
should(isNotebookPinned).be.true('Notebook should be pinned');
bookPinManager.unpinNotebook(books[0].bookItems[0]);
let isNotebookPinnedAfterChange = isBookItemPinned(notebookUri);
should(isNotebookPinnedAfterChange).be.false('Notebook should not be pinned after notebook is unpinned');
});
});
});

View File

@@ -327,4 +327,22 @@ describe('Utils Tests', function () {
}
});
});
describe('isBookItemPinned', function (): void {
it('Should NOT pin an unknown book within a workspace', async function (): Promise<void> {
let notebookUri = path.join(path.sep, 'randomfolder', 'randomsubfolder', 'content', 'randomnotebook.ipynb');
let isNotebookPinned = utils.isBookItemPinned(notebookUri);
should(isNotebookPinned).be.false('Random notebooks should not be pinned');
});
});
describe('getPinnedNotebooks', function (): void {
it('Should NOT have any pinned notebooks', async function (): Promise<void> {
let pinnedNotebooks: string[] = utils.getPinnedNotebooks();
should(pinnedNotebooks.length).equal(0, 'Should not have any pinned notebooks');
});
});
});