Editing Books (#13535)

* start work on ui

* Move notebooks complete

* Simplify version handling

* fix issues with add section method

* fix issues with the edit experience and add the quick pick for editing

* add version handler for re-writing tocs

* fix book toc manager tests

* add notebook test

* fix localize constant

* normalize paths on windows

* check that a section has sections before attempting to mve files

* Implement method for renaming duplicated files

* moving last notebook from section converts the section into notebook

* Add recovery method restore original state of file system

* Add clean up, for files that are copied instead of renamed

* remove dir complexity

* divide edit book in methods for easier testing and remove promise chain

* Keep structure of toc

* normalize paths on windows

* fix external link

* Add other fields for versions

* fix paths in uri of findSection

* add section to section test

* Add error messages

* address pr comments and add tests

* check that the new path of a notebook is different from the original path before deleting
This commit is contained in:
Barbara Valdez
2021-02-02 20:39:11 -08:00
committed by GitHub
parent 41915bda8d
commit 9ac180d772
13 changed files with 1296 additions and 595 deletions

View File

@@ -11,19 +11,17 @@ import * as constants from '../common/constants';
import { IPrompter, IQuestion, QuestionTypes } from '../prompts/question';
import CodeAdapter from '../prompts/adapter';
import { BookTreeItem, BookTreeItemType } from './bookTreeItem';
import { BookModel, BookVersion } from './bookModel';
import { BookModel } from './bookModel';
import { Deferred } from '../common/promise';
import { IBookTrustManager, BookTrustManager } from './bookTrustManager';
import * as loc from '../common/localizedConstants';
import * as glob from 'fast-glob';
import { IJupyterBookSectionV2, IJupyterBookSectionV1 } from '../contracts/content';
import { debounce, getPinnedNotebooks } from '../common/utils';
import { IBookPinManager, BookPinManager } from './bookPinManager';
import { BookTocManager, IBookTocManager } from './bookTocManager';
import { BookTocManager, IBookTocManager, quickPickResults } from './bookTocManager';
import { getContentPath } from './bookVersionHandler';
import { TelemetryReporter, BookTelemetryView, NbTelemetryActions } from '../telemetry';
const content = 'content';
interface BookSearchResults {
notebookPaths: string[];
bookPaths: string[];
@@ -50,7 +48,6 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
this._extensionContext = extensionContext;
this.books = [];
this.bookPinManager = new BookPinManager();
this.bookTocManager = new BookTocManager();
this.viewId = view;
this.initialize(workspaceFolders).catch(e => console.error(e));
this.prompter = new CodeAdapter();
@@ -93,6 +90,14 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
this._extensionContext.globalState.update(constants.visitedNotebooksMementoKey, value);
}
setFileWatcher(book: BookModel): void {
fs.watchFile(book.tableOfContentsPath, async (curr, prev) => {
if (curr.mtime > prev.mtime) {
this.fireBookRefresh(book);
}
});
}
trustBook(bookTreeItem?: BookTreeItem): void {
let bookPathToTrust: string = bookTreeItem ? bookTreeItem.root : this.currentBook?.bookPath;
if (bookPathToTrust) {
@@ -144,8 +149,92 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
TelemetryReporter.createActionEvent(BookTelemetryView, NbTelemetryActions.CreateBook).send();
}
async editBook(book: BookTreeItem, section: BookTreeItem): Promise<void> {
await this.bookTocManager.updateBook(section, book);
async getSelectionQuickPick(movingElement: BookTreeItem): Promise<quickPickResults> {
let bookOptions: vscode.QuickPickItem[] = [];
let pickedSection: vscode.QuickPickItem;
this.books.forEach(book => {
if (!book.isNotebook) {
bookOptions.push({ label: book.bookItems[0].title, detail: book.bookPath });
}
});
let pickedBook = await vscode.window.showQuickPick(bookOptions, {
canPickMany: false,
placeHolder: loc.labelBookFolder
});
if (pickedBook && movingElement) {
const updateBook = this.books.find(book => book.bookPath === pickedBook.detail).bookItems[0];
if (updateBook) {
let bookSections = updateBook.sections;
while (bookSections?.length > 0) {
bookOptions = [{ label: loc.labelAddToLevel, detail: pickedSection ? pickedSection.detail : '' }];
bookSections.forEach(section => {
if (section.sections) {
bookOptions.push({ label: section.title ? section.title : section.file, detail: section.file });
}
});
bookSections = [];
if (bookOptions.length > 1) {
pickedSection = await vscode.window.showQuickPick(bookOptions, {
canPickMany: false,
placeHolder: loc.labelBookSection
});
if (pickedSection && pickedSection.label === loc.labelAddToLevel) {
break;
}
else if (pickedSection && pickedSection.detail) {
if (updateBook.root === movingElement.root && pickedSection.detail === movingElement.uri) {
pickedSection = undefined;
} else {
bookSections = updateBook.findChildSection(pickedSection.detail).sections;
}
}
}
}
}
return { quickPickSection: pickedSection, book: updateBook };
}
return undefined;
}
async editBook(movingElement: BookTreeItem): Promise<void> {
const selectionResults = await this.getSelectionQuickPick(movingElement);
const pickedSection = selectionResults.quickPickSection;
const updateBook = selectionResults.book;
if (pickedSection && updateBook) {
const targetSection = pickedSection.detail !== undefined ? updateBook.findChildSection(pickedSection.detail) : undefined;
if (movingElement.tableOfContents.sections) {
if (movingElement.contextValue === 'savedNotebook') {
let sourceBook = this.books.find(book => book.getNotebook(path.normalize(movingElement.book.contentPath)));
movingElement.tableOfContents.sections = sourceBook?.bookItems[0].sections;
}
}
const sourceBook = this.books.find(book => book.bookPath === movingElement.book.root);
const targetBook = this.books.find(book => book.bookPath === updateBook.book.root);
this.bookTocManager = new BookTocManager(targetBook, sourceBook);
// remove watch on toc file from both books.
if (sourceBook) {
fs.unwatchFile(movingElement.tableOfContentsPath);
}
try {
await this.bookTocManager.updateBook(movingElement, updateBook, targetSection);
} catch (e) {
await this.bookTocManager.recovery();
vscode.window.showErrorMessage(loc.editBookError(updateBook.book.contentPath, e instanceof Error ? e.message : e));
} finally {
this.fireBookRefresh(targetBook);
if (sourceBook) {
// refresh source book model to pick up latest changes
this.fireBookRefresh(sourceBook);
}
// even if it fails, we still need to watch the toc file again.
if (sourceBook) {
this.setFileWatcher(sourceBook);
}
this.setFileWatcher(targetBook);
}
}
}
async openBook(bookPath: string, urlToOpen?: string, showPreview?: boolean, isNotebook?: boolean): Promise<void> {
@@ -180,6 +269,11 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
});
}
} catch (e) {
// if there is an error remove book from context
const index = this.books.findIndex(book => book.bookPath === bookPath);
if (index !== -1) {
this.books.splice(index, 1);
}
vscode.window.showErrorMessage(loc.openFileError(bookPath, e instanceof Error ? e.message : e));
}
}
@@ -267,9 +361,9 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
if (urlToOpen) {
const bookRoot = this.currentBook.bookItems[0];
const sectionToOpen = bookRoot.findChildSection(urlToOpen);
urlPath = sectionToOpen?.url;
urlPath = sectionToOpen?.file;
} else {
urlPath = this.currentBook.version === BookVersion.v1 ? (this.currentBook.bookItems[0].tableOfContents.sections[0] as IJupyterBookSectionV1).url : (this.currentBook.bookItems[0].tableOfContents.sections[0] as IJupyterBookSectionV2).file;
urlPath = this.currentBook.bookItems[0].tableOfContents.sections[0].file;
}
}
if (urlPath) {
@@ -434,20 +528,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
public async searchJupyterBooks(treeItem?: BookTreeItem): Promise<void> {
let folderToSearch: string;
if (treeItem && treeItem.sections !== undefined) {
if (treeItem.book.version === BookVersion.v1) {
if (treeItem.uri) {
folderToSearch = path.join(treeItem.book.root, content, path.dirname(treeItem.uri));
} else {
folderToSearch = path.join(treeItem.root, content);
}
} else if (treeItem.book.version === BookVersion.v2) {
if (treeItem.uri) {
folderToSearch = path.join(treeItem.book.root, path.dirname(treeItem.uri));
} else {
folderToSearch = path.join(treeItem.root);
}
}
folderToSearch = treeItem.uri ? getContentPath(treeItem.version, treeItem.book.root, path.dirname(treeItem.uri)) : getContentPath(treeItem.version, treeItem.book.root, '');
} else if (this.currentBook && !this.currentBook.isNotebook) {
folderToSearch = path.join(this.currentBook.contentFolderPath);
} else {
@@ -573,19 +654,14 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
}
}
/**
* Optional method on the vscode interface.
* Implementing getParent, due to reveal method in extHostTreeView.ts
* throwing error if it is not implemented.
*/
getParent(element?: BookTreeItem): vscode.ProviderResult<BookTreeItem> {
if (element?.uri) {
let parentPath: string;
let contentFolder = element.book.version === BookVersion.v1 ? path.join(element.book.root, content) : element.book.root;
parentPath = path.join(contentFolder, element.uri.substring(0, element.uri.lastIndexOf(path.posix.sep)));
if (parentPath === element.root) {
return undefined;
}
let parentPaths = Array.from(this.currentBook.getAllNotebooks()?.keys()).filter(x => x.indexOf(parentPath) > -1);
return parentPaths.length > 0 ? this.currentBook.getAllNotebooks().get(parentPaths[0]) : undefined;
} else {
return undefined;
}
// Remove it for perf issues.
return undefined;
}
getUntitledNotebookUri(resource: string): vscode.Uri {