mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-10 18:22:34 -05:00
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:
89
extensions/notebook/src/book/bookPinManager.ts
Normal file
89
extensions/notebook/src/book/bookPinManager.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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.");
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
167
extensions/notebook/src/test/book/bookPinManager.test.ts
Normal file
167
extensions/notebook/src/test/book/bookPinManager.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user