Add new file UI (#14773)

Adds a notebook or markdown file to a book's top level or a section by right click on Book Tree View
This commit is contained in:
Barbara Valdez
2021-03-30 18:44:52 -07:00
committed by GitHub
parent b774f09b6c
commit 8aa222d1c8
13 changed files with 428 additions and 247 deletions

View File

@@ -25,7 +25,6 @@ export class BookModel {
private _contentFolderPath: string;
private _configPath: string;
private _bookVersion: BookVersion;
private _rootPath: string;
private _errorMessage: string;
private _activePromise: Deferred<void> | undefined = undefined;
private _queuedPromises: Deferred<void>[] = [];
@@ -104,11 +103,9 @@ export class BookModel {
}
this._bookVersion = BookVersion.v1;
this._contentFolderPath = path.posix.join(this.bookPath, content, '');
this._rootPath = path.dirname(path.dirname(this._tableOfContentsPath));
} else {
this._contentFolderPath = this.bookPath;
this._tableOfContentsPath = path.posix.join(this.bookPath, '_toc.yml');
this._rootPath = path.dirname(this._tableOfContentsPath);
this._bookVersion = BookVersion.v2;
}
}
@@ -190,7 +187,7 @@ export class BookModel {
version: this._bookVersion,
title: config.title,
contentPath: this._tableOfContentsPath,
root: this._rootPath,
root: this.bookPath,
tableOfContents: { sections: this.parseJupyterSections(this._bookVersion, tableOfContents) },
page: tableOfContents,
type: BookTreeItemType.Book,
@@ -334,7 +331,7 @@ export class BookModel {
return this._errorMessage;
}
public get version(): string {
public get version(): BookVersion {
return this._bookVersion;
}
}

View File

@@ -11,12 +11,15 @@ import { BookVersion, convertTo } from './bookVersionHandler';
import * as vscode from 'vscode';
import * as loc from '../common/localizedConstants';
import { BookModel } from './bookModel';
import { TocEntryPathHandler } from './tocEntryPathHandler';
import { FileExtension } from '../common/utils';
export interface IBookTocManager {
updateBook(element: BookTreeItem, book: BookTreeItem, targetSection?: JupyterBookSection): Promise<void>;
removeNotebook(element: BookTreeItem): Promise<void>;
createBook(bookContentPath: string, contentFolder: string): Promise<void>;
recovery(): Promise<void>
addNewFile(pathDetails: TocEntryPathHandler, bookItem: BookTreeItem): Promise<void>;
recovery(): Promise<void>;
}
export interface quickPickResults {
@@ -24,7 +27,7 @@ export interface quickPickResults {
book?: BookTreeItem
}
const allowedFileExtensions: string[] = ['.md', '.ipynb'];
const allowedFileExtensions: string[] = [FileExtension.Markdown, FileExtension.Notebook];
export function hasSections(node: JupyterBookSection): boolean {
return node.sections !== undefined && node.sections.length > 0;
@@ -38,12 +41,12 @@ export class BookTocManager implements IBookTocManager {
public tocFiles: Map<string, string> = new Map<string, string>();
private sourceBookContentPath: string;
private targetBookContentPath: string;
private _sourceBook: BookModel;
constructor(targetBook?: BookModel, sourceBook?: BookModel) {
this._sourceBook = sourceBook;
this.sourceBookContentPath = sourceBook?.bookItems[0].rootContentPath;
this.targetBookContentPath = targetBook?.bookItems[0].rootContentPath;
constructor(private _sourceBook?: BookModel, private _targetBook?: BookModel) {
this._targetBook?.unwatchTOC();
this._sourceBook?.unwatchTOC();
this.sourceBookContentPath = this._sourceBook?.bookItems[0].rootContentPath;
this.targetBookContentPath = this._targetBook?.bookItems[0].rootContentPath;
}
/**
@@ -180,19 +183,25 @@ export class BookTocManager implements IBookTocManager {
}
/**
* Reads and modifies the table of contents file of the target book.
* @param version the version of the target book
* Reads and modifies the table of contents file of book.
* @param version the version of book.
* @param tocPath Path to the table of contents
* @param findSection The section that will be modified.
* @param findSection The section that will be modified. If findSection is undefined then the added section is added at the end of the toc file.
* @param addSection The section that'll be added to the target section. If it's undefined then the target section (findSection) is removed from the table of contents.
*/
async updateTOC(version: BookVersion, tocPath: string, findSection: JupyterBookSection, addSection?: JupyterBookSection): Promise<void> {
async updateTOC(version: BookVersion, tocPath: string, findSection?: JupyterBookSection, addSection?: JupyterBookSection): Promise<void> {
const tocFile = await fs.readFile(tocPath, 'utf8');
this.tableofContents = yaml.safeLoad(tocFile);
if (!this.tocFiles.has(tocPath)) {
this.tocFiles.set(tocPath, tocFile);
}
const isModified = this.modifyToc(version, this.tableofContents, findSection, addSection);
let isModified = false;
if (findSection) {
isModified = this.modifyToc(version, this.tableofContents, findSection, addSection);
} else if (addSection) {
this.tableofContents.push(addSection);
isModified = true;
}
if (isModified) {
await fs.writeFile(tocPath, yaml.safeDump(this.tableofContents, { lineWidth: Infinity, noRefs: true, skipInvalid: true }));
} else {
@@ -307,7 +316,7 @@ export class BookTocManager implements IBookTocManager {
* @param section The section that's been moved.
* @param book The target book.
*/
async addSection(section: BookTreeItem, book: BookTreeItem): Promise<void> {
async moveSectionFiles(section: BookTreeItem, book: BookTreeItem): Promise<void> {
const uri = path.posix.join(path.posix.sep, path.relative(section.rootContentPath, section.book.contentPath));
let moveFile = path.join(path.parse(uri).dir, path.parse(uri).name);
let fileName = undefined;
@@ -347,20 +356,20 @@ export class BookTocManager implements IBookTocManager {
}
/**
* Moves a notebook to a book top level or a book's section. If there's a target section we add the the targetSection directory if it has one and append it to the
* notebook's path. The overwrite option is set to false to prevent any issues with duplicated file names.
* @param element Notebook, Markdown File, or section that will be added to the book.
* Moves a file to a book top level or a book's section. If there's a target section we add the the targetSection directory if it has one and append it to the
* files's path. The overwrite option is set to false to prevent any issues with duplicated file names.
* @param element Notebook, Markdown File, or book's notebook that will be added to the book.
* @param targetBook Book that will be modified.
*/
async addNotebook(notebook: BookTreeItem, book: BookTreeItem): Promise<void> {
async moveFile(file: BookTreeItem, book: BookTreeItem): Promise<void> {
const rootPath = book.rootContentPath;
const notebookPath = path.parse(notebook.book.contentPath);
const filePath = path.parse(file.book.contentPath);
let fileName = undefined;
try {
await fs.move(notebook.book.contentPath, path.join(rootPath, notebookPath.base), { overwrite: false });
await fs.move(file.book.contentPath, path.join(rootPath, filePath.base), { overwrite: false });
} catch (error) {
if (error.code === 'EEXIST') {
fileName = await this.renameFile(notebook.book.contentPath, path.join(rootPath, notebookPath.base));
fileName = await this.renameFile(file.book.contentPath, path.join(rootPath, filePath.base));
}
else {
throw (error);
@@ -368,14 +377,14 @@ export class BookTocManager implements IBookTocManager {
}
if (this._sourceBook) {
const sectionTOC = this._sourceBook.bookItems[0].findChildSection(notebook.uri);
const sectionTOC = this._sourceBook.bookItems[0].findChildSection(file.uri);
if (sectionTOC) {
this.newSection = sectionTOC;
}
}
fileName = fileName === undefined ? notebookPath.name : path.parse(fileName).name;
fileName = fileName === undefined ? filePath.name : path.parse(fileName).name;
this.newSection.file = path.posix.join(path.posix.sep, fileName);
this.newSection.title = notebook.book.title;
this.newSection.title = file.book.title;
if (book.version === BookVersion.v1) {
// here we only convert if is v1 because we are already using the v2 notation for every book that we read.
this.newSection = convertTo(book.version, this.newSection);
@@ -388,50 +397,76 @@ export class BookTocManager implements IBookTocManager {
* @param targetBook Book that will be modified.
* @param targetSection Book section that'll be modified.
*/
public async updateBook(element: BookTreeItem, targetBook: BookTreeItem, targetSection?: JupyterBookSection): Promise<void> {
if (element.contextValue === 'section') {
// modify the sourceBook toc and remove the section
const findSection: JupyterBookSection = { file: element.book.page.file, title: element.book.page.title };
await this.addSection(element, targetBook);
await this.updateTOC(element.book.version, element.tableOfContentsPath, findSection, undefined);
if (targetSection) {
// adding new section to the target book toc file
await this.updateTOC(targetBook.book.version, targetBook.tableOfContentsPath, targetSection, this.newSection);
public async updateBook(element: BookTreeItem, targetItem: BookTreeItem, targetSection?: JupyterBookSection): Promise<void> {
try {
if (element.contextValue === 'section') {
// modify the sourceBook toc and remove the section
const findSection: JupyterBookSection = { file: element.book.page.file, title: element.book.page.title };
await this.moveSectionFiles(element, targetItem);
// remove section from book
await this.updateTOC(element.book.version, element.tableOfContentsPath, findSection, undefined);
// add section to book
await this.updateTOC(targetItem.book.version, targetItem.tableOfContentsPath, targetSection, this.newSection);
}
else {
//since there's not a target section, we just append the section at the end of the file
if (this.targetBookContentPath !== this.sourceBookContentPath) {
this.tableofContents = targetBook.sections.map(section => convertTo(targetBook.version, section));
// the notebook is part of a book so we need to modify its toc as well
const findSection = { file: element.book.page.file, title: element.book.page.title };
await this.moveFile(element, targetItem);
if (element.contextValue === 'savedBookNotebook' || element.contextValue === 'Markdown') {
// remove notebook entry from book toc
await this.updateTOC(element.book.version, element.tableOfContentsPath, findSection, undefined);
} else {
// close the standalone notebook, so it doesn't throw an error when we move the notebook to new location.
await vscode.commands.executeCommand('notebook.command.closeNotebook', element);
}
await this.updateTOC(targetItem.book.version, targetItem.tableOfContentsPath, targetSection, this.newSection);
}
} catch (e) {
await this.recovery();
vscode.window.showErrorMessage(loc.editBookError(element.book.contentPath, e instanceof Error ? e.message : e));
} finally {
try {
await this._targetBook.reinitializeContents();
} finally {
if (this._sourceBook && this._sourceBook.bookPath !== this._targetBook.bookPath) {
// refresh source book model to pick up latest changes
await this._sourceBook.reinitializeContents();
}
this.tableofContents.push(this.newSection);
await fs.writeFile(targetBook.tableOfContentsPath, yaml.safeDump(this.tableofContents, { lineWidth: Infinity, noRefs: true, skipInvalid: true }));
}
}
else if (element.contextValue === 'savedNotebook' || element.contextValue === 'savedBookNotebook') {
// the notebook is part of a book so we need to modify its toc as well
const findSection = { file: element.book.page.file, title: element.book.page.title };
await this.addNotebook(element, targetBook);
if (element.tableOfContentsPath) {
await this.updateTOC(element.book.version, element.tableOfContentsPath, findSection, undefined);
} else {
// close the standalone notebook, so it doesn't throw an error when we move the notebook to new location.
await vscode.commands.executeCommand('notebook.command.closeNotebook', element);
}
if (!targetSection) {
if (this.targetBookContentPath !== this.sourceBookContentPath) {
this.tableofContents = targetBook.sections.map(section => convertTo(targetBook.version, section));
}
this.tableofContents.push(this.newSection);
await fs.writeFile(targetBook.tableOfContentsPath, yaml.safeDump(this.tableofContents, { lineWidth: Infinity, noRefs: true, skipInvalid: true }));
} else {
await this.updateTOC(targetBook.book.version, targetBook.tableOfContentsPath, targetSection, this.newSection);
}
}
public async addNewFile(pathDetails: TocEntryPathHandler, bookItem: BookTreeItem): Promise<void> {
let findSection: JupyterBookSection | undefined = undefined;
await fs.writeFile(pathDetails.filePath, '');
if (bookItem.contextValue === 'section') {
findSection = { file: bookItem.book.page.file, title: bookItem.book.page.title };
}
let fileEntryInToc: JupyterBookSection = {
title: pathDetails.titleInTocEntry,
file: pathDetails.fileInTocEntry
};
if (bookItem.book.version === BookVersion.v1) {
fileEntryInToc = convertTo(BookVersion.v1, fileEntryInToc);
}
// book is already opened in notebooks view, so modifying the toc will add the new file automatically
await this.updateTOC(bookItem.book.version, bookItem.tableOfContentsPath, findSection, fileEntryInToc);
await this._sourceBook.reinitializeContents();
await this.openResource(pathDetails);
}
public async openResource(pathDetails: TocEntryPathHandler): Promise<void> {
if (pathDetails.fileExtension === FileExtension.Notebook) {
await vscode.commands.executeCommand('bookTreeView.openNotebook', pathDetails.filePath);
} else {
await vscode.commands.executeCommand('bookTreeView.openMarkdown', pathDetails.filePath);
}
}
public async removeNotebook(element: BookTreeItem): Promise<void> {
const findSection = { file: element.book.page.file, title: element.book.page.title };
await this.updateTOC(element.book.version, element.tableOfContentsPath, findSection, undefined);
await this._sourceBook.reinitializeContents();
}
public get modifiedDir(): Set<string> {

View File

@@ -41,7 +41,7 @@ export class BookTreeItem extends vscode.TreeItem {
private _uri: string | undefined;
private _previousUri: string;
private _nextUri: string;
public readonly version: string;
public readonly version: BookVersion;
public command: vscode.Command;
public resourceUri: vscode.Uri;
private _rootContentPath: string;
@@ -71,7 +71,7 @@ export class BookTreeItem extends vscode.TreeItem {
this.contextValue = BookTreeItemType.ExternalLink;
} else {
this.contextValue = book.type === BookTreeItemType.Notebook ? (isBookItemPinned(book.contentPath) ? BookTreeItemType.pinnedNotebook : getNotebookType(book)) : BookTreeItemType.section;
this.contextValue = book.type === BookTreeItemType.Notebook ? (isBookItemPinned(book.contentPath) ? BookTreeItemType.pinnedNotebook : getNotebookType(book)) : BookTreeItemType.Markdown;
}
this.setPageVariables();
this.setCommand();
@@ -84,7 +84,7 @@ export class BookTreeItem extends vscode.TreeItem {
}
else {
// if it's a section, book or a notebook's book then we set the table of contents path.
if (this.book.type === BookTreeItemType.Book || this.contextValue === BookTreeItemType.section || (book.tableOfContents.sections && book.type === BookTreeItemType.Notebook)) {
if (this.book.type === BookTreeItemType.Book || this.contextValue === BookTreeItemType.section || this.contextValue === BookTreeItemType.savedBookNotebook || book.tableOfContents.sections && book.type === BookTreeItemType.Markdown) {
this._tableOfContentsPath = getTocPath(this.book.version, this.book.root);
}
this._rootContentPath = getContentPath(this.book.version, this.book.root, '');

View File

@@ -16,10 +16,11 @@ import { Deferred } from '../common/promise';
import { IBookTrustManager, BookTrustManager } from './bookTrustManager';
import * as loc from '../common/localizedConstants';
import * as glob from 'fast-glob';
import { getPinnedNotebooks, confirmReplace, getNotebookType } from '../common/utils';
import { getPinnedNotebooks, confirmMessageDialog, getNotebookType, FileExtension } from '../common/utils';
import { IBookPinManager, BookPinManager } from './bookPinManager';
import { BookTocManager, IBookTocManager, quickPickResults } from './bookTocManager';
import { CreateBookDialog } from '../dialog/createBookDialog';
import { AddFileDialog } from '../dialog/addFileDialog';
import { getContentPath } from './bookVersionHandler';
import { TelemetryReporter, BookTelemetryView, NbTelemetryActions } from '../telemetry';
@@ -197,37 +198,10 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
const pickedSection = selectionResults.quickPickSection;
const updateBook = selectionResults.book;
const targetSection = pickedSection.detail !== undefined ? updateBook.findChildSection(pickedSection.detail) : undefined;
if (movingElement.tableOfContents.sections) {
if (movingElement.contextValue === 'savedNotebook' || movingElement.contextValue === 'savedBookNotebook') {
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 source book.
if (sourceBook) {
sourceBook.unwatchTOC();
}
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 {
try {
await targetBook.reinitializeContents();
} finally {
if (sourceBook && sourceBook.bookPath !== targetBook.bookPath) {
// refresh source book model to pick up latest changes
await sourceBook.reinitializeContents();
}
if (sourceBook) {
sourceBook.watchTOC();
}
}
}
this.bookTocManager = new BookTocManager(sourceBook, targetBook);
await this.bookTocManager.updateBook(movingElement, updateBook, targetSection);
}
}
@@ -276,7 +250,23 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
}
}
async createMarkdownFile(bookItem: BookTreeItem): Promise<void> {
const book = this.books.find(b => b.bookPath === bookItem.root);
this.bookTocManager = new BookTocManager(book);
const dialog = new AddFileDialog(this.bookTocManager, bookItem, FileExtension.Markdown);
await dialog.createDialog();
}
async createNotebook(bookItem: BookTreeItem): Promise<void> {
const book = this.books.find(b => b.bookPath === bookItem.root);
this.bookTocManager = new BookTocManager(book);
const dialog = new AddFileDialog(this.bookTocManager, bookItem, FileExtension.Notebook);
await dialog.createDialog();
}
async removeNotebook(bookItem: BookTreeItem): Promise<void> {
const book = this.books.find(b => b.bookPath === bookItem.root);
this.bookTocManager = new BookTocManager(book);
return this.bookTocManager.removeNotebook(bookItem);
}
@@ -479,7 +469,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
let destinationUri: vscode.Uri = vscode.Uri.file(path.join(pickedFolder.fsPath, path.basename(this.currentBook.bookPath)));
if (destinationUri) {
if (await fs.pathExists(destinationUri.fsPath)) {
let doReplace = await confirmReplace(this.prompter);
let doReplace = await confirmMessageDialog(this.prompter, loc.confirmReplace);
if (!doReplace) {
return undefined;
}

View File

@@ -0,0 +1,20 @@
/*---------------------------------------------------------------------------------------------
* 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 { FileExtension } from '../common/utils';
export class TocEntryPathHandler {
public readonly fileInTocEntry: string;
public readonly titleInTocEntry: string;
public readonly fileExtension: FileExtension;
constructor(public readonly filePath: string, public readonly bookRoot: string, title?: string) {
const relativePath = path.relative(bookRoot, filePath);
const pathDetails = path.parse(relativePath);
this.fileInTocEntry = relativePath.replace(pathDetails.ext, '');
this.titleInTocEntry = title ?? pathDetails.name;
this.fileExtension = pathDetails.ext === FileExtension.Notebook ? FileExtension.Notebook : FileExtension.Markdown;
}
}