Books/navigation (#6280)

* added previous and next buttons

* previous and next notebook API

* links for prev/next notebooks

* fixed first and last pages

* made code more readable

* addressed Kevin's comments

* moved logic over to BookTreeItem

* show buttons in dev mode only

* added BookTreeItemFormat interface

* added interface and enum

* removed localize call
This commit is contained in:
Lucy Zhang
2019-07-12 10:18:46 -07:00
committed by GitHub
parent 6606be998b
commit a706ff4d5b
13 changed files with 347 additions and 37 deletions

View File

@@ -4,20 +4,118 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
export enum BookTreeItemType {
Book = 'Book',
Notebook = 'Notebook',
Markdown = 'Markdown',
ExternalLink = 'ExternalLink'
}
export interface BookTreeItemFormat {
title: string;
root: string;
tableOfContents: any[];
page: any;
type: BookTreeItemType;
}
export class BookTreeItem extends vscode.TreeItem { export class BookTreeItem extends vscode.TreeItem {
private _sections: any[];
private _uri: string;
private _previousUri: string;
private _nextUri: string;
public command: vscode.Command;
constructor( constructor(public book: BookTreeItemFormat) {
public readonly title: string, super(book.title, vscode.TreeItemCollapsibleState.Collapsed);
public readonly root: string,
public readonly tableOfContents: any[], if (book.type === BookTreeItemType.Book) {
public readonly collapsibleState: vscode.TreeItemCollapsibleState, this.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed;
public uri?: string, this._sections = book.page;
public command?: vscode.Command } else {
) { this.setPageVariables();
super(title, collapsibleState); this.setCommand();
}
} }
contextValue = 'book'; private setPageVariables() {
this.collapsibleState = (this.book.page.sections || this.book.page.subsections) && this.book.page.expand_sections ?
vscode.TreeItemCollapsibleState.Expanded :
this.book.page.sections || this.book.page.subsections ?
vscode.TreeItemCollapsibleState.Collapsed :
vscode.TreeItemCollapsibleState.None;
this._sections = this.book.page.sections || this.book.page.subsections;
this._uri = this.book.page.url;
let index = (this.book.tableOfContents.indexOf(this.book.page));
this.setPreviousUri(index);
this.setNextUri(index);
}
private setCommand() {
if (this.book.type === BookTreeItemType.Notebook) {
let pathToNotebook = path.join(this.book.root, 'content', this._uri.concat('.ipynb'));
this.command = { command: 'bookTreeView.openNotebook', title: localize('openNotebookCommand', 'Open Notebook'), arguments: [pathToNotebook], };
} else if (this.book.type === BookTreeItemType.Markdown) {
let pathToMarkdown = path.join(this.book.root, 'content', this._uri.concat('.md'));
this.command = { command: 'bookTreeView.openMarkdown', title: localize('openMarkdownCommand', 'Open Markdown'), arguments: [pathToMarkdown], };
} else if (this.book.type === BookTreeItemType.ExternalLink) {
this.command = { command: 'bookTreeView.openExternalLink', title: localize('openExternalLinkCommand', 'Open External Link'), arguments: [this._uri], };
}
}
private setPreviousUri(index: number): void {
let i = --index;
while (i > -1) {
if (this.book.tableOfContents[i].url) {
// 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'));
if (fs.existsSync(pathToNotebook)) {
this._previousUri = pathToNotebook;
return;
}
}
i--;
}
}
private setNextUri(index: number): void {
let i = ++index;
while (i < this.book.tableOfContents.length) {
if (this.book.tableOfContents[i].url) {
// 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'));
if (fs.existsSync(pathToNotebook)) {
this._nextUri = pathToNotebook;
return;
}
}
i++;
}
}
public get root(): string {
return this.book.root;
}
public get tableOfContents(): any[] {
return this.book.tableOfContents;
}
public get sections(): any[] {
return this._sections;
}
public get previousUri(): string {
return this._previousUri;
}
public get nextUri(): string {
return this._nextUri;
}
} }

View File

@@ -3,20 +3,23 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode'; 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 yaml from 'js-yaml'; import * as yaml from 'js-yaml';
import { BookTreeItem } from './bookTreeItem'; import { BookTreeItem, BookTreeItemType } from './bookTreeItem';
import * as nls from 'vscode-nls'; import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle(); const localize = nls.loadMessageBundle();
export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeItem> { export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeItem>, azdata.nb.NavigationProvider {
readonly providerId: string = 'BookNavigator';
private _onDidChangeTreeData: vscode.EventEmitter<BookTreeItem | undefined> = new vscode.EventEmitter<BookTreeItem | undefined>(); private _onDidChangeTreeData: vscode.EventEmitter<BookTreeItem | undefined> = new vscode.EventEmitter<BookTreeItem | undefined>();
readonly onDidChangeTreeData: vscode.Event<BookTreeItem | undefined> = this._onDidChangeTreeData.event; readonly onDidChangeTreeData: vscode.Event<BookTreeItem | undefined> = this._onDidChangeTreeData.event;
private _tableOfContentsPath: string[]; private _tableOfContentsPath: string[];
private _allNotebooks = new Map<string, BookTreeItem>();
constructor(private workspaceRoot: string) { constructor(private workspaceRoot: string) {
if (workspaceRoot !== '') { if (workspaceRoot !== '') {
@@ -71,15 +74,14 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
} }
} }
getTreeItem(element: BookTreeItem): vscode.TreeItem { getTreeItem(element: BookTreeItem): vscode.TreeItem {
return element; return element;
} }
getChildren(element?: BookTreeItem): Thenable<BookTreeItem[]> { getChildren(element?: BookTreeItem): Thenable<BookTreeItem[]> {
if (element) { if (element) {
if (element.tableOfContents) { if (element.sections) {
return Promise.resolve(this.getSections(element.tableOfContents, element.root)); return Promise.resolve(this.getSections(element.tableOfContents, element.sections, element.root));
} else { } else {
return Promise.resolve([]); return Promise.resolve([]);
} }
@@ -88,6 +90,10 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
} }
} }
private flattenArray(array: any[]): any[] {
return array.reduce((acc, val) => Array.isArray(val.sections) ? acc.concat(val).concat(this.flattenArray(val.sections)) : acc.concat(val), []);
}
private getBooks(): BookTreeItem[] { private getBooks(): BookTreeItem[] {
let books: BookTreeItem[] = []; let books: BookTreeItem[] = [];
for (let i in this._tableOfContentsPath) { for (let i in this._tableOfContentsPath) {
@@ -95,7 +101,13 @@ 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._tableOfContentsPath[i], 'utf-8')); const tableOfContents = yaml.safeLoad(fs.readFileSync(this._tableOfContentsPath[i], 'utf-8'));
let book = new BookTreeItem(config.title, root, tableOfContents, vscode.TreeItemCollapsibleState.Collapsed); let book = new BookTreeItem({
title: config.title,
root: root,
tableOfContents: this.flattenArray(tableOfContents),
page: tableOfContents,
type: BookTreeItemType.Book
});
books.push(book); books.push(book);
} catch (e) { } catch (e) {
vscode.window.showErrorMessage(localize('openConfigFileError', 'Open file {0} failed: {1}', vscode.window.showErrorMessage(localize('openConfigFileError', 'Open file {0} failed: {1}',
@@ -106,30 +118,45 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
return books; return books;
} }
private getSections(sec: any[], root: string): BookTreeItem[] { private getSections(tableOfContents: any[], sections: any[], root: string): BookTreeItem[] {
let notebooks: BookTreeItem[] = []; let notebooks: BookTreeItem[] = [];
for (let i = 0; i < sec.length; i++) { for (let i = 0; i < sections.length; i++) {
if (sec[i].url) { if (sections[i].url) {
if (sec[i].external) { if (sections[i].external) {
let externalLink = new BookTreeItem(sec[i].title, root, sec[i].sections, sec[i].sections ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.None, sec[i].url, { command: 'bookTreeView.openExternalLink', title: localize('openExternalLinkCommand', 'Open External Link'), arguments: [sec[i].url], }); let externalLink = new BookTreeItem({
title: sections[i].title,
root: root,
tableOfContents: tableOfContents,
page: sections[i],
type: BookTreeItemType.ExternalLink
});
notebooks.push(externalLink); notebooks.push(externalLink);
} else { } else {
let pathToNotebook = path.join(root, 'content', sec[i].url.concat('.ipynb')); let pathToNotebook = path.join(root, 'content', sections[i].url.concat('.ipynb'));
let pathToMarkdown = path.join(root, 'content', sec[i].url.concat('.md')); let pathToMarkdown = path.join(root, 'content', sections[i].url.concat('.md'));
// Note: Currently, if there is an ipynb and a md file with the same name, Jupyter Books only shows the notebook. // Note: Currently, if there is an ipynb and a md file with the same name, Jupyter Books only shows the notebook.
// Following Jupyter Books behavior for now // Following Jupyter Books behavior for now
if (fs.existsSync(pathToNotebook)) { if (fs.existsSync(pathToNotebook)) {
let notebook = new BookTreeItem(sec[i].title, root, sec[i].sections || sec[i].subsections, let notebook = new BookTreeItem({
(sec[i].sections || sec[i].subsections) && sec[i].expand_sections ? vscode.TreeItemCollapsibleState.Expanded : sec[i].sections || sec[i].subsections ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None, title: sections[i].title,
sec[i].url, { command: 'bookTreeView.openNotebook', title: localize('openNotebookCommand', 'Open Notebook'), arguments: [pathToNotebook], }); root: root,
tableOfContents: tableOfContents,
page: sections[i],
type: BookTreeItemType.Notebook
});
notebooks.push(notebook); notebooks.push(notebook);
this._allNotebooks.set(pathToNotebook, notebook);
} else if (fs.existsSync(pathToMarkdown)) { } else if (fs.existsSync(pathToMarkdown)) {
let markdown = new BookTreeItem(sec[i].title, root, sec[i].sections || sec[i].subsections, let markdown = new BookTreeItem({
(sec[i].sections || sec[i].subsections) && sec[i].expand_sections ? vscode.TreeItemCollapsibleState.Expanded : sec[i].sections || sec[i].subsections ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None, title: sections[i].title,
sec[i].url, { command: 'bookTreeView.openMarkdown', title: localize('openMarkdownCommand', 'Open Markdown'), arguments: [pathToMarkdown], }); root: root,
tableOfContents: tableOfContents,
page: sections[i],
type: BookTreeItemType.Markdown
});
notebooks.push(markdown); notebooks.push(markdown);
} else { } else {
vscode.window.showErrorMessage(localize('missingFileError', 'Missing file : {0}', sec[i].title)); vscode.window.showErrorMessage(localize('missingFileError', 'Missing file : {0}', sections[i].title));
} }
} }
} else { } else {
@@ -138,4 +165,24 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
} }
return notebooks; return notebooks;
} }
getNavigation(uri: vscode.Uri): Thenable<azdata.nb.NavigationResult> {
let notebook = this._allNotebooks.get(uri.fsPath);
let result: azdata.nb.NavigationResult;
if (notebook) {
result = {
hasNavigation: true,
previous: notebook.previousUri ? vscode.Uri.file(notebook.previousUri) : undefined,
next: notebook.nextUri ? vscode.Uri.file(notebook.nextUri) : undefined
};
} else {
result = {
hasNavigation: false,
previous: undefined,
next: undefined
};
}
return Promise.resolve(result);
}
} }

View File

@@ -29,10 +29,11 @@ type ChooseCellType = { label: string, id: CellType };
export async function activate(extensionContext: vscode.ExtensionContext): Promise<IExtensionApi> { export async function activate(extensionContext: vscode.ExtensionContext): Promise<IExtensionApi> {
const bookTreeViewProvider = new BookTreeViewProvider(vscode.workspace.rootPath || ''); const bookTreeViewProvider = new BookTreeViewProvider(vscode.workspace.rootPath || '');
vscode.window.registerTreeDataProvider('bookTreeView', bookTreeViewProvider); extensionContext.subscriptions.push(vscode.window.registerTreeDataProvider('bookTreeView', bookTreeViewProvider));
vscode.commands.registerCommand('bookTreeView.openNotebook', (resource) => bookTreeViewProvider.openNotebook(resource)); extensionContext.subscriptions.push(azdata.nb.registerNavigationProvider(bookTreeViewProvider));
vscode.commands.registerCommand('bookTreeView.openMarkdown', (resource) => bookTreeViewProvider.openMarkdown(resource)); extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openNotebook', (resource) => bookTreeViewProvider.openNotebook(resource)));
vscode.commands.registerCommand('bookTreeView.openExternalLink', (resource) => bookTreeViewProvider.openExternalLink(resource)); extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openMarkdown', (resource) => bookTreeViewProvider.openMarkdown(resource)));
extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openExternalLink', (resource) => 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;

View File

@@ -5255,6 +5255,19 @@ declare module 'azdata' {
value: string; value: string;
} }
export function registerNavigationProvider(provider: NavigationProvider): vscode.Disposable;
export interface NavigationProvider {
readonly providerId: string;
getNavigation(notebookUri: vscode.Uri): Thenable<NavigationResult>;
}
export interface NavigationResult {
hasNavigation: boolean;
previous?: vscode.Uri;
next?: vscode.Uri;
}
//#endregion //#endregion
} }

View File

@@ -13,6 +13,7 @@ import { Disposable } from 'vs/workbench/api/common/extHostTypes';
import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters';
import { IMainContext } from 'vs/workbench/api/common/extHost.protocol'; import { IMainContext } from 'vs/workbench/api/common/extHost.protocol';
import { ok } from 'vs/base/common/assert'; import { ok } from 'vs/base/common/assert';
import { localize } from 'vs/nls';
import { import {
SqlMainContext, INotebookDocumentsAndEditorsDelta, ExtHostNotebookDocumentsAndEditorsShape, SqlMainContext, INotebookDocumentsAndEditorsDelta, ExtHostNotebookDocumentsAndEditorsShape,
@@ -21,10 +22,13 @@ import {
import { ExtHostNotebookDocumentData } from 'sql/workbench/api/node/extHostNotebookDocumentData'; import { ExtHostNotebookDocumentData } from 'sql/workbench/api/node/extHostNotebookDocumentData';
import { ExtHostNotebookEditor } from 'sql/workbench/api/node/extHostNotebookEditor'; import { ExtHostNotebookEditor } from 'sql/workbench/api/node/extHostNotebookEditor';
type Adapter = azdata.nb.NavigationProvider;
export class ExtHostNotebookDocumentsAndEditors implements ExtHostNotebookDocumentsAndEditorsShape { export class ExtHostNotebookDocumentsAndEditors implements ExtHostNotebookDocumentsAndEditorsShape {
private static _handlePool: number = 0;
private _disposables: Disposable[] = []; private _disposables: Disposable[] = [];
private _adapters = new Map<number, Adapter>();
private _activeEditorId: string; private _activeEditorId: string;
private _proxy: MainThreadNotebookDocumentsAndEditorsShape; private _proxy: MainThreadNotebookDocumentsAndEditorsShape;
@@ -152,6 +156,33 @@ export class ExtHostNotebookDocumentsAndEditors implements ExtHostNotebookDocume
} }
} }
private _nextHandle(): number {
return ExtHostNotebookDocumentsAndEditors._handlePool++;
}
private _addNewAdapter(adapter: Adapter): number {
const handle = this._nextHandle();
this._adapters.set(handle, adapter);
return handle;
}
private _getAdapter<T>(id: number): T {
let adapter = <T><any>this._adapters.get(id);
if (adapter === undefined) {
throw new Error('No adapter found');
}
return adapter;
}
$getNavigation(handle: number, notebookUri: UriComponents): Thenable<azdata.nb.NavigationResult> {
let navProvider = this._getAdapter<azdata.nb.NavigationProvider>(handle);
if (navProvider) {
let uri = URI.revive(notebookUri);
return navProvider.getNavigation(uri);
}
throw new Error('No navigation provider found for handle ${handle}');
}
//#endregion //#endregion
//#region Extension accessible methods //#region Extension accessible methods
@@ -212,5 +243,17 @@ export class ExtHostNotebookDocumentsAndEditors implements ExtHostNotebookDocume
this._editors.forEach(data => result.push(data)); this._editors.forEach(data => result.push(data));
return result; return result;
} }
registerNavigationProvider(provider: azdata.nb.NavigationProvider): vscode.Disposable {
if (!provider || !provider.providerId) {
throw new Error(localize('providerRequired', 'A NotebookProvider with valid providerId must be passed to this method'));
}
const handle = this._addNewAdapter(provider);
this._proxy.$registerNavigationProvider(provider.providerId, handle);
return new Disposable(() => {
this._adapters.delete(handle);
});
}
//#endregion //#endregion
} }

View File

@@ -327,7 +327,8 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements
@IInstantiationService private _instantiationService: IInstantiationService, @IInstantiationService private _instantiationService: IInstantiationService,
@IEditorService private _editorService: IEditorService, @IEditorService private _editorService: IEditorService,
@IEditorGroupsService private _editorGroupService: IEditorGroupsService, @IEditorGroupsService private _editorGroupService: IEditorGroupsService,
@ICapabilitiesService private _capabilitiesService: ICapabilitiesService @ICapabilitiesService private _capabilitiesService: ICapabilitiesService,
@INotebookService private readonly _notebookService: INotebookService
) { ) {
super(); super();
if (extHostContext) { if (extHostContext) {
@@ -685,4 +686,22 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements
this._modelToDisposeMap.set(editor.id, listeners); this._modelToDisposeMap.set(editor.id, listeners);
}); });
} }
$registerNavigationProvider(providerId: string, handle: number): void {
this._notebookService.registerNavigationProvider({
providerId: providerId,
onNext: async (uri) => {
let result = await this._proxy.$getNavigation(handle, uri);
if (result) {
this.doOpenEditor(result.next, {});
}
},
onPrevious: async (uri) => {
let result = await this._proxy.$getNavigation(handle, uri);
if (result) {
this.doOpenEditor(result.previous, {});
}
}
});
}
} }

View File

@@ -501,6 +501,9 @@ export function createApiFactory(
registerNotebookProvider(provider: azdata.nb.NotebookProvider): vscode.Disposable { registerNotebookProvider(provider: azdata.nb.NotebookProvider): vscode.Disposable {
return extHostNotebook.registerNotebookProvider(provider); return extHostNotebook.registerNotebookProvider(provider);
}, },
registerNavigationProvider(provider: azdata.nb.NavigationProvider): vscode.Disposable {
return extHostNotebookDocumentsAndEditors.registerNavigationProvider(provider);
},
CellRange: sqlExtHostTypes.CellRange, CellRange: sqlExtHostTypes.CellRange,
NotebookChangeKind: sqlExtHostTypes.NotebookChangeKind NotebookChangeKind: sqlExtHostTypes.NotebookChangeKind
}; };

View File

@@ -913,6 +913,7 @@ export interface INotebookShowOptions {
export interface ExtHostNotebookDocumentsAndEditorsShape { export interface ExtHostNotebookDocumentsAndEditorsShape {
$acceptDocumentsAndEditorsDelta(delta: INotebookDocumentsAndEditorsDelta): void; $acceptDocumentsAndEditorsDelta(delta: INotebookDocumentsAndEditorsDelta): void;
$acceptModelChanged(strURL: UriComponents, e: INotebookModelChangedData); $acceptModelChanged(strURL: UriComponents, e: INotebookModelChangedData);
$getNavigation(handle: number, uri: vscode.Uri): Thenable<azdata.nb.NavigationResult>;
} }
export interface MainThreadNotebookDocumentsAndEditorsShape extends IDisposable { export interface MainThreadNotebookDocumentsAndEditorsShape extends IDisposable {
@@ -924,6 +925,7 @@ export interface MainThreadNotebookDocumentsAndEditorsShape extends IDisposable
$clearOutput(id: string, cellUri: UriComponents): Promise<boolean>; $clearOutput(id: string, cellUri: UriComponents): Promise<boolean>;
$clearAllOutputs(id: string): Promise<boolean>; $clearAllOutputs(id: string): Promise<boolean>;
$changeKernel(id: string, kernel: azdata.nb.IKernelInfo): Promise<boolean>; $changeKernel(id: string, kernel: azdata.nb.IKernelInfo): Promise<boolean>;
$registerNavigationProvider(providerId: string, handle: number);
} }
export interface ExtHostExtensionManagementShape { export interface ExtHostExtensionManagementShape {

View File

@@ -19,5 +19,7 @@
<placeholder-cell-component [cellModel]="cell" [model]="model"> <placeholder-cell-component [cellModel]="cell" [model]="model">
</placeholder-cell-component> </placeholder-cell-component>
</div> </div>
<div class="book-nav" #bookNav>
</div>
</div> </div>
</div> </div>

View File

@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { nb } from 'azdata'; import { nb } from 'azdata';
import * as vscode from 'vscode';
import { OnInit, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, OnDestroy } from '@angular/core'; import { OnInit, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, OnDestroy } from '@angular/core';
import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
@@ -19,6 +20,7 @@ import { IAction, Action, IActionViewItem } from 'vs/base/common/actions';
import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import * as DOM from 'vs/base/browser/dom';
import { AngularDisposable } from 'sql/base/node/lifecycle'; import { AngularDisposable } from 'sql/base/node/lifecycle';
import { CellTypes, CellType } from 'sql/workbench/parts/notebook/models/contracts'; import { CellTypes, CellType } from 'sql/workbench/parts/notebook/models/contracts';
@@ -50,6 +52,8 @@ import { ILogService } from 'vs/platform/log/common/log';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { LabeledMenuItemActionItem, fillInActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { LabeledMenuItemActionItem, fillInActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { Button } from 'sql/base/browser/ui/button/button';
import { attachButtonStyler } from 'sql/platform/theme/common/styler';
import { isUndefinedOrNull } from 'vs/base/common/types'; import { isUndefinedOrNull } from 'vs/base/common/types';
@@ -63,6 +67,8 @@ export const NOTEBOOK_SELECTOR: string = 'notebook-component';
export class NotebookComponent extends AngularDisposable implements OnInit, OnDestroy, INotebookEditor { export class NotebookComponent extends AngularDisposable implements OnInit, OnDestroy, INotebookEditor {
@ViewChild('toolbar', { read: ElementRef }) private toolbar: ElementRef; @ViewChild('toolbar', { read: ElementRef }) private toolbar: ElementRef;
@ViewChild('container', { read: ElementRef }) private container: ElementRef; @ViewChild('container', { read: ElementRef }) private container: ElementRef;
@ViewChild('bookNav', { read: ElementRef }) private bookNav: ElementRef;
private _model: NotebookModel; private _model: NotebookModel;
private _isInErrorState: boolean = false; private _isInErrorState: boolean = false;
private _errorMessage: string; private _errorMessage: string;
@@ -127,6 +133,10 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this)); this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
this.updateTheme(this.themeService.getColorTheme()); this.updateTheme(this.themeService.getColorTheme());
this.initActionBar(); this.initActionBar();
if (this.contextKeyService.getContextKeyValue('isDevelopment') &&
this.contextKeyService.getContextKeyValue('bookOpened')) {
this.initNavSection();
}
this.setScrollPosition(); this.setScrollPosition();
this.doLoad(); this.doLoad();
} }
@@ -392,7 +402,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
this.notificationService.error(error); this.notificationService.error(error);
} }
protected initActionBar() { protected initActionBar(): void {
let kernelContainer = document.createElement('div'); let kernelContainer = document.createElement('div');
let kernelDropdown = new KernelsDropdown(kernelContainer, this.contextViewService, this.modelReady); let kernelDropdown = new KernelsDropdown(kernelContainer, this.contextViewService, this.modelReady);
kernelDropdown.render(kernelContainer); kernelDropdown.render(kernelContainer);
@@ -428,6 +438,24 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
{ action: this._runAllCellsAction }, { action: this._runAllCellsAction },
{ action: clearResultsButton } { action: clearResultsButton }
]); ]);
}
protected initNavSection(): void {
this.addButton(localize('previousButtonLabel', "Previous"),
() => this.previousPage());
this.addButton(localize('nextButtonLabel', "Next"),
() => this.nextPage());
}
private addButton(label: string, onDidClick?: () => void): void {
const container = DOM.append(this.bookNav.nativeElement, DOM.$('.dialog-message-button'));
let button = new Button(container);
button.icon = '';
button.label = label;
if (onDidClick) {
this._register(button.onDidClick(onDidClick));
}
this._register(attachButtonStyler(button, this.themeService));
} }
@@ -582,6 +610,28 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
} }
} }
public async nextPage(): Promise<void> {
try {
let navProvider = this.notebookService.getNavigationProvider(this.model.notebookUri);
if (navProvider) {
navProvider.onNext(this.model.notebookUri);
}
} catch (error) {
this.notificationService.error(notebookUtils.getErrorMessage(error));
}
}
public previousPage() {
try {
let navProvider = this.notebookService.getNavigationProvider(this.model.notebookUri);
if (navProvider) {
navProvider.onPrevious(this.model.notebookUri);
}
} catch (error) {
this.notificationService.error(notebookUtils.getErrorMessage(error));
}
}
getSections(): INotebookSection[] { getSections(): INotebookSection[] {
return this.getSectionElements(); return this.getSectionElements();
} }

View File

@@ -125,3 +125,15 @@
.hc-black .monaco-workbench .notebook-action.new-notebook { .hc-black .monaco-workbench .notebook-action.new-notebook {
background: url('./media/dark/new_notebook_inverse.svg') center center no-repeat; background: url('./media/dark/new_notebook_inverse.svg') center center no-repeat;
} }
.notebookEditor .book-nav {
display: flex;
align-items: center;
justify-content: center;
margin: 5px;
}
.notebookEditor .book-nav .dialog-message-button {
min-width: 60px;
margin-right: 10px;
}

View File

@@ -45,6 +45,10 @@ export interface INotebookService {
*/ */
unregisterProvider(providerId: string): void; unregisterProvider(providerId: string): void;
registerNavigationProvider(provider: INavigationProvider): void;
getNavigationProvider(notebookUri: URI): INavigationProvider;
getSupportedFileExtensions(): string[]; getSupportedFileExtensions(): string[];
getProvidersForFileType(fileType: string): string[]; getProvidersForFileType(fileType: string): string[];
@@ -149,3 +153,9 @@ export interface INotebookEditor {
getSections(): INotebookSection[]; getSections(): INotebookSection[];
navigateToSection(sectionId: string): void; navigateToSection(sectionId: string): void;
} }
export interface INavigationProvider {
providerId: string;
onNext(uri: URI): void;
onPrevious(uri: URI): void;
}

View File

@@ -10,7 +10,7 @@ import { Registry } from 'vs/platform/registry/common/platform';
import { import {
INotebookService, INotebookManager, INotebookProvider, INotebookService, INotebookManager, INotebookProvider,
DEFAULT_NOTEBOOK_FILETYPE, INotebookEditor, SQL_NOTEBOOK_PROVIDER, OVERRIDE_EDITOR_THEMING_SETTING DEFAULT_NOTEBOOK_FILETYPE, INotebookEditor, SQL_NOTEBOOK_PROVIDER, OVERRIDE_EDITOR_THEMING_SETTING, INavigationProvider
} from 'sql/workbench/services/notebook/common/notebookService'; } from 'sql/workbench/services/notebook/common/notebookService';
import { RenderMimeRegistry } from 'sql/workbench/parts/notebook/outputs/registry'; import { RenderMimeRegistry } from 'sql/workbench/parts/notebook/outputs/registry';
import { standardRendererFactories } from 'sql/workbench/parts/notebook/outputs/factories'; import { standardRendererFactories } from 'sql/workbench/parts/notebook/outputs/factories';
@@ -97,6 +97,7 @@ export class NotebookService extends Disposable implements INotebookService {
private _trustedNotebooksMemento: Memento; private _trustedNotebooksMemento: Memento;
private _mimeRegistry: RenderMimeRegistry; private _mimeRegistry: RenderMimeRegistry;
private _providers: Map<string, ProviderDescriptor> = new Map(); private _providers: Map<string, ProviderDescriptor> = new Map();
private _navigationProviders: Map<string, INavigationProvider> = new Map();
private _managersMap: Map<string, INotebookManager[]> = new Map(); private _managersMap: Map<string, INotebookManager[]> = new Map();
private _onNotebookEditorAdd = new Emitter<INotebookEditor>(); private _onNotebookEditorAdd = new Emitter<INotebookEditor>();
private _onNotebookEditorRemove = new Emitter<INotebookEditor>(); private _onNotebookEditorRemove = new Emitter<INotebookEditor>();
@@ -269,6 +270,15 @@ export class NotebookService extends Disposable implements INotebookService {
this._providers.delete(providerId); this._providers.delete(providerId);
} }
registerNavigationProvider(provider: INavigationProvider): void {
this._navigationProviders.set(provider.providerId, provider);
}
getNavigationProvider(): INavigationProvider {
let provider = this._navigationProviders.size > 0 ? this._navigationProviders.values().next().value : undefined;
return provider;
}
get isRegistrationComplete(): boolean { get isRegistrationComplete(): boolean {
return this._isRegistrationComplete; return this._isRegistrationComplete;
} }