mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
Add abillity to open to specific item within a Jupyter book (#7155)
* Add abillity to open to specific item within a Jupyter book * Move helper method into BookTreeItem class * Fix default URL path * Add typing to Jupyter book code * Update comment and typings * Fix compile error and cleanup
This commit is contained in:
@@ -124,8 +124,8 @@ class AzdataExtensionBookContributionProvider extends Disposable implements Book
|
|||||||
this.contributions.map(book => {
|
this.contributions.map(book => {
|
||||||
let bookName: string = path.basename(book.path);
|
let bookName: string = path.basename(book.path);
|
||||||
vscode.commands.executeCommand('setContext', bookName, true);
|
vscode.commands.executeCommand('setContext', bookName, true);
|
||||||
vscode.commands.registerCommand('books.' + bookName, async (context) => {
|
vscode.commands.registerCommand('books.' + bookName, async (urlToOpen?: string) => {
|
||||||
vscode.commands.executeCommand('bookTreeView.openBook', book.path, true);
|
vscode.commands.executeCommand('bookTreeView.openBook', book.path, true, urlToOpen);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import * as vscode from 'vscode';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as nls from 'vscode-nls';
|
import * as nls from 'vscode-nls';
|
||||||
|
import { IJupyterBookSection, IJupyterBookToc } from '../contracts/content';
|
||||||
const localize = nls.loadMessageBundle();
|
const localize = nls.loadMessageBundle();
|
||||||
|
|
||||||
export enum BookTreeItemType {
|
export enum BookTreeItemType {
|
||||||
@@ -19,14 +20,14 @@ export enum BookTreeItemType {
|
|||||||
export interface BookTreeItemFormat {
|
export interface BookTreeItemFormat {
|
||||||
title: string;
|
title: string;
|
||||||
root: string;
|
root: string;
|
||||||
tableOfContents: any[];
|
tableOfContents: IJupyterBookToc;
|
||||||
page: any;
|
page: any;
|
||||||
type: BookTreeItemType;
|
type: BookTreeItemType;
|
||||||
treeItemCollapsibleState: number;
|
treeItemCollapsibleState: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BookTreeItem extends vscode.TreeItem {
|
export class BookTreeItem extends vscode.TreeItem {
|
||||||
private _sections: any[];
|
private _sections: IJupyterBookSection[];
|
||||||
private _uri: string;
|
private _uri: string;
|
||||||
private _previousUri: string;
|
private _previousUri: string;
|
||||||
private _nextUri: string;
|
private _nextUri: string;
|
||||||
@@ -54,7 +55,7 @@ export class BookTreeItem extends vscode.TreeItem {
|
|||||||
this._sections = this.book.page.sections || this.book.page.subsections;
|
this._sections = this.book.page.sections || this.book.page.subsections;
|
||||||
this._uri = this.book.page.url;
|
this._uri = this.book.page.url;
|
||||||
|
|
||||||
let index = (this.book.tableOfContents.indexOf(this.book.page));
|
let index = (this.book.tableOfContents.sections.indexOf(this.book.page));
|
||||||
this.setPreviousUri(index);
|
this.setPreviousUri(index);
|
||||||
this.setNextUri(index);
|
this.setNextUri(index);
|
||||||
}
|
}
|
||||||
@@ -74,9 +75,9 @@ export class BookTreeItem extends vscode.TreeItem {
|
|||||||
private setPreviousUri(index: number): void {
|
private setPreviousUri(index: number): void {
|
||||||
let i = --index;
|
let i = --index;
|
||||||
while (i > -1) {
|
while (i > -1) {
|
||||||
if (this.book.tableOfContents[i].url) {
|
if (this.book.tableOfContents.sections[i].url) {
|
||||||
// TODO: Currently only navigating to notebooks. Need to add logic for markdown.
|
// TODO: Currently only navigating to notebooks. Need to add logic for markdown.
|
||||||
let pathToNotebook = path.join(this.book.root, 'content', this.book.tableOfContents[i].url.concat('.ipynb'));
|
let pathToNotebook = path.join(this.book.root, 'content', this.book.tableOfContents.sections[i].url.concat('.ipynb'));
|
||||||
if (fs.existsSync(pathToNotebook)) {
|
if (fs.existsSync(pathToNotebook)) {
|
||||||
this._previousUri = pathToNotebook;
|
this._previousUri = pathToNotebook;
|
||||||
return;
|
return;
|
||||||
@@ -88,10 +89,10 @@ export class BookTreeItem extends vscode.TreeItem {
|
|||||||
|
|
||||||
private setNextUri(index: number): void {
|
private setNextUri(index: number): void {
|
||||||
let i = ++index;
|
let i = ++index;
|
||||||
while (i < this.book.tableOfContents.length) {
|
while (i < this.book.tableOfContents.sections.length) {
|
||||||
if (this.book.tableOfContents[i].url) {
|
if (this.book.tableOfContents.sections[i].url) {
|
||||||
// TODO: Currently only navigating to notebooks. Need to add logic for markdown.
|
// TODO: Currently only navigating to notebooks. Need to add logic for markdown.
|
||||||
let pathToNotebook = path.join(this.book.root, 'content', this.book.tableOfContents[i].url.concat('.ipynb'));
|
let pathToNotebook = path.join(this.book.root, 'content', this.book.tableOfContents.sections[i].url.concat('.ipynb'));
|
||||||
if (fs.existsSync(pathToNotebook)) {
|
if (fs.existsSync(pathToNotebook)) {
|
||||||
this._nextUri = pathToNotebook;
|
this._nextUri = pathToNotebook;
|
||||||
return;
|
return;
|
||||||
@@ -113,7 +114,7 @@ export class BookTreeItem extends vscode.TreeItem {
|
|||||||
return this.book.root;
|
return this.book.root;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get tableOfContents(): any[] {
|
public get tableOfContents(): IJupyterBookToc {
|
||||||
return this.book.tableOfContents;
|
return this.book.tableOfContents;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,4 +138,29 @@ export class BookTreeItem extends vscode.TreeItem {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to find a child section with a specified URL
|
||||||
|
* @param url The url of the section we're searching for
|
||||||
|
*/
|
||||||
|
public findChildSection(url?: string): IJupyterBookSection | undefined {
|
||||||
|
if (!url) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return this.findChildSectionRecur(this, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
private findChildSectionRecur(section: IJupyterBookSection, url: string): IJupyterBookSection | undefined {
|
||||||
|
if (section.url && section.url === url) {
|
||||||
|
return section;
|
||||||
|
} else if (section.sections) {
|
||||||
|
for (const childSection of section.sections) {
|
||||||
|
const foundSection = this.findChildSectionRecur(childSection, url);
|
||||||
|
if (foundSection) {
|
||||||
|
return foundSection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { maxBookSearchDepth, notebookConfigKey } from '../common/constants';
|
|||||||
import { isEditorTitleFree } from '../common/utils';
|
import { isEditorTitleFree } from '../common/utils';
|
||||||
import * as nls from 'vscode-nls';
|
import * as nls from 'vscode-nls';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
|
import { IJupyterBookToc, IJupyterBookSection } from '../contracts/content';
|
||||||
|
|
||||||
const localize = nls.loadMessageBundle();
|
const localize = nls.loadMessageBundle();
|
||||||
const existsAsync = promisify(fs.exists);
|
const existsAsync = promisify(fs.exists);
|
||||||
@@ -72,7 +73,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
|
|||||||
this._onReadAllTOCFiles.fire();
|
this._onReadAllTOCFiles.fire();
|
||||||
}
|
}
|
||||||
|
|
||||||
async openBook(bookPath: string, context: vscode.ExtensionContext, openAsUntitled: boolean): Promise<void> {
|
async openBook(bookPath: string, openAsUntitled: boolean, urlToOpen?: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Check if the book is already open in viewlet.
|
// Check if the book is already open in viewlet.
|
||||||
if (this._tableOfContentPaths.indexOf(path.join(bookPath, '_data', 'toc.yml').replace(/\\/g, '/')) > -1 && this._allNotebooks.size > 0) {
|
if (this._tableOfContentPaths.indexOf(path.join(bookPath, '_data', 'toc.yml').replace(/\\/g, '/')) > -1 && this._allNotebooks.size > 0) {
|
||||||
@@ -80,21 +81,24 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
await this.getTableOfContentFiles([bookPath]);
|
await this.getTableOfContentFiles([bookPath]);
|
||||||
let bookViewer = vscode.window.createTreeView('bookTreeView', { showCollapseAll: true, treeDataProvider: this });
|
const bookViewer = vscode.window.createTreeView('bookTreeView', { showCollapseAll: true, treeDataProvider: this });
|
||||||
await vscode.commands.executeCommand('workbench.books.action.focusBooksExplorer');
|
await vscode.commands.executeCommand('workbench.books.action.focusBooksExplorer');
|
||||||
this._openAsUntitled = openAsUntitled;
|
this._openAsUntitled = openAsUntitled;
|
||||||
let books = this.getBooks();
|
let books = this.getBooks();
|
||||||
if (books && books.length > 0) {
|
if (books && books.length > 0) {
|
||||||
bookViewer.reveal(books[0], { expand: vscode.TreeItemCollapsibleState.Expanded, focus: true, select: true });
|
const rootTreeItem = books[0];
|
||||||
const readmeMarkdown: string = path.join(bookPath, 'content', books[0].tableOfContents[0].url.concat('.md'));
|
const sectionToOpen = rootTreeItem.findChildSection(urlToOpen);
|
||||||
const readmeNotebook: string = path.join(bookPath, 'content', books[0].tableOfContents[0].url.concat('.ipynb'));
|
bookViewer.reveal(rootTreeItem, { expand: vscode.TreeItemCollapsibleState.Expanded, focus: true, select: true });
|
||||||
const markdownExists = await existsAsync(readmeMarkdown);
|
const urlPath = sectionToOpen ? sectionToOpen.url : rootTreeItem.tableOfContents.sections[0].url;
|
||||||
const notebookExists = await existsAsync(readmeNotebook);
|
const sectionToOpenMarkdown: string = path.join(bookPath, 'content', urlPath.concat('.md'));
|
||||||
|
const sectionToOpenNotebook: string = path.join(bookPath, 'content', urlPath.concat('.ipynb'));
|
||||||
|
const markdownExists = await existsAsync(sectionToOpenMarkdown);
|
||||||
|
const notebookExists = await existsAsync(sectionToOpenNotebook);
|
||||||
if (markdownExists) {
|
if (markdownExists) {
|
||||||
vscode.commands.executeCommand('markdown.showPreview', vscode.Uri.file(readmeMarkdown));
|
vscode.commands.executeCommand('markdown.showPreview', vscode.Uri.file(sectionToOpenMarkdown));
|
||||||
}
|
}
|
||||||
else if (notebookExists) {
|
else if (notebookExists) {
|
||||||
vscode.workspace.openTextDocument(readmeNotebook);
|
this.openNotebook(sectionToOpenNotebook);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -196,12 +200,12 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private flattenArray(array: any[], title: string): any[] {
|
/**
|
||||||
try {
|
* Recursively parses out a section of a Jupyter Book.
|
||||||
return array.reduce((acc, val) => Array.isArray(val.sections) ? acc.concat(val).concat(this.flattenArray(val.sections, title)) : acc.concat(val), []);
|
* @param array The input data to parse
|
||||||
} catch (e) {
|
*/
|
||||||
throw localize('Invalid toc.yml', 'Error: {0} has an incorrect toc.yml file', title);
|
private parseJupyterSection(array: any[]): IJupyterBookSection[] {
|
||||||
}
|
return array.reduce((acc, val) => Array.isArray(val.sections) ? acc.concat(val).concat(this.parseJupyterSection(val.sections)) : acc.concat(val), []);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getBooks(): BookTreeItem[] {
|
public getBooks(): BookTreeItem[] {
|
||||||
@@ -211,20 +215,24 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
|
|||||||
try {
|
try {
|
||||||
const config = yaml.safeLoad(fs.readFileSync(path.join(root, '_config.yml'), 'utf-8'));
|
const config = yaml.safeLoad(fs.readFileSync(path.join(root, '_config.yml'), 'utf-8'));
|
||||||
const tableOfContents = yaml.safeLoad(fs.readFileSync(this._tableOfContentPaths[i], 'utf-8'));
|
const tableOfContents = yaml.safeLoad(fs.readFileSync(this._tableOfContentPaths[i], 'utf-8'));
|
||||||
let book = new BookTreeItem({
|
try {
|
||||||
title: config.title,
|
let book = new BookTreeItem({
|
||||||
root: root,
|
title: config.title,
|
||||||
tableOfContents: this.flattenArray(tableOfContents, config.title),
|
root: root,
|
||||||
page: tableOfContents,
|
tableOfContents: { sections: this.parseJupyterSection(tableOfContents) },
|
||||||
type: BookTreeItemType.Book,
|
page: tableOfContents,
|
||||||
treeItemCollapsibleState: vscode.TreeItemCollapsibleState.Expanded,
|
type: BookTreeItemType.Book,
|
||||||
},
|
treeItemCollapsibleState: vscode.TreeItemCollapsibleState.Expanded,
|
||||||
{
|
},
|
||||||
light: this._extensionContext.asAbsolutePath('resources/light/book.svg'),
|
{
|
||||||
dark: this._extensionContext.asAbsolutePath('resources/dark/book_inverse.svg')
|
light: this._extensionContext.asAbsolutePath('resources/light/book.svg'),
|
||||||
}
|
dark: this._extensionContext.asAbsolutePath('resources/dark/book_inverse.svg')
|
||||||
);
|
}
|
||||||
books.push(book);
|
);
|
||||||
|
books.push(book);
|
||||||
|
} catch (e) {
|
||||||
|
throw Error(localize('invalidTocError', "Error: {0} has an incorrect toc.yml file. {1}", config.title, e instanceof Error ? e.message : e));
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let error = e instanceof Error ? e.message : e;
|
let error = e instanceof Error ? e.message : e;
|
||||||
this._errorMessage = error;
|
this._errorMessage = error;
|
||||||
@@ -234,7 +242,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
|
|||||||
return books;
|
return books;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSections(tableOfContents: any[], sections: any[], root: string): BookTreeItem[] {
|
public getSections(tableOfContents: IJupyterBookToc, sections: IJupyterBookSection[], root: string): BookTreeItem[] {
|
||||||
let notebooks: BookTreeItem[] = [];
|
let notebooks: BookTreeItem[] = [];
|
||||||
for (let i = 0; i < sections.length; i++) {
|
for (let i = 0; i < sections.length; i++) {
|
||||||
if (sections[i].url) {
|
if (sections[i].url) {
|
||||||
|
|||||||
@@ -64,3 +64,55 @@ export type OutputType =
|
|||||||
| 'stream'
|
| 'stream'
|
||||||
| 'error'
|
| 'error'
|
||||||
| 'update_display_data';
|
| 'update_display_data';
|
||||||
|
|
||||||
|
export interface IJupyterBookToc {
|
||||||
|
sections: IJupyterBookSection[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A section of a Jupyter book.
|
||||||
|
*
|
||||||
|
* This is taken from https://github.com/jupyter/jupyter-book/blob/master/jupyter_book/book_template/_data/toc.yml but is not
|
||||||
|
* enforced so invalid JSON may result in expected values being undefined.
|
||||||
|
*/
|
||||||
|
export interface IJupyterBookSection {
|
||||||
|
/**
|
||||||
|
* Title of chapter or section
|
||||||
|
*/
|
||||||
|
title?: string;
|
||||||
|
/**
|
||||||
|
* URL of section relative to the /content/ folder.
|
||||||
|
*/
|
||||||
|
url?: string;
|
||||||
|
/**
|
||||||
|
* Contains a list of more entries that make up the chapter's/section's sub-sections
|
||||||
|
*/
|
||||||
|
sections?: IJupyterBookSection[];
|
||||||
|
/**
|
||||||
|
* If the section shouldn't have a number in the sidebar
|
||||||
|
*/
|
||||||
|
not_numbered?: string;
|
||||||
|
/**
|
||||||
|
* If you'd like the sections of this chapter to always be expanded in the sidebar.
|
||||||
|
*/
|
||||||
|
expand_sections?: boolean;
|
||||||
|
/**
|
||||||
|
* Whether the URL is an external link or points to content in the book
|
||||||
|
*/
|
||||||
|
external?: boolean;
|
||||||
|
|
||||||
|
// Below are some special values that trigger specific behavior:
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will provide a link to a search page
|
||||||
|
*/
|
||||||
|
search?: boolean;
|
||||||
|
/**
|
||||||
|
* Will insert a divider in the sidebar
|
||||||
|
*/
|
||||||
|
divider?: boolean;
|
||||||
|
/**
|
||||||
|
* Will insert a header with no link in the sidebar
|
||||||
|
*/
|
||||||
|
header?: boolean;
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,11 +30,11 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi
|
|||||||
const bookTreeViewProvider = new BookTreeViewProvider(vscode.workspace.workspaceFolders || [], extensionContext);
|
const bookTreeViewProvider = new BookTreeViewProvider(vscode.workspace.workspaceFolders || [], extensionContext);
|
||||||
extensionContext.subscriptions.push(vscode.window.registerTreeDataProvider('bookTreeView', bookTreeViewProvider));
|
extensionContext.subscriptions.push(vscode.window.registerTreeDataProvider('bookTreeView', bookTreeViewProvider));
|
||||||
extensionContext.subscriptions.push(azdata.nb.registerNavigationProvider(bookTreeViewProvider));
|
extensionContext.subscriptions.push(azdata.nb.registerNavigationProvider(bookTreeViewProvider));
|
||||||
extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openBook', (resource, openAsReadonly) => bookTreeViewProvider.openBook(resource, extensionContext, openAsReadonly)));
|
extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openBook', (bookPath: string, openAsUntitled: boolean, urlToOpen?: string) => bookTreeViewProvider.openBook(bookPath, openAsUntitled, urlToOpen)));
|
||||||
extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openNotebookAsUntitled', (resource) => bookTreeViewProvider.openNotebookAsUntitled(resource)));
|
extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openNotebookAsUntitled', (resource: string) => bookTreeViewProvider.openNotebookAsUntitled(resource)));
|
||||||
extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openNotebook', (resource) => bookTreeViewProvider.openNotebook(resource)));
|
extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openNotebook', (resource: string) => bookTreeViewProvider.openNotebook(resource)));
|
||||||
extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openMarkdown', (resource) => bookTreeViewProvider.openMarkdown(resource)));
|
extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openMarkdown', (resource: string) => bookTreeViewProvider.openMarkdown(resource)));
|
||||||
extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openExternalLink', (resource) => bookTreeViewProvider.openExternalLink(resource)));
|
extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openExternalLink', (resource: string) => bookTreeViewProvider.openExternalLink(resource)));
|
||||||
|
|
||||||
extensionContext.subscriptions.push(vscode.commands.registerCommand('_notebook.command.new', (context?: azdata.ConnectedContext) => {
|
extensionContext.subscriptions.push(vscode.commands.registerCommand('_notebook.command.new', (context?: azdata.ConnectedContext) => {
|
||||||
let connectionProfile: azdata.IConnectionProfile = undefined;
|
let connectionProfile: azdata.IConnectionProfile = undefined;
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ describe.skip('BookTreeViewProviderTests', function() {
|
|||||||
|
|
||||||
it('should show error if notebook or markdown file is missing', function(): void {
|
it('should show error if notebook or markdown file is missing', function(): void {
|
||||||
let books = bookTreeViewProvider.getBooks();
|
let books = bookTreeViewProvider.getBooks();
|
||||||
let children = bookTreeViewProvider.getSections([], books[0].sections, rootFolderPath);
|
let children = bookTreeViewProvider.getSections({ sections: [] }, books[0].sections, rootFolderPath);
|
||||||
should(bookTreeViewProvider.errorMessage).equal('Missing file : Notebook1');
|
should(bookTreeViewProvider.errorMessage).equal('Missing file : Notebook1');
|
||||||
// Rest of book should be detected correctly even with a missing file
|
// Rest of book should be detected correctly even with a missing file
|
||||||
equalBookItems(children[0], expectedNotebook2);
|
equalBookItems(children[0], expectedNotebook2);
|
||||||
|
|||||||
Reference in New Issue
Block a user