diff --git a/src/sql/workbench/parts/notebook/cellViews/linkHandler.directive.ts b/src/sql/workbench/parts/notebook/cellViews/linkHandler.directive.ts index 5d624358db..7245ed653d 100644 --- a/src/sql/workbench/parts/notebook/cellViews/linkHandler.directive.ts +++ b/src/sql/workbench/parts/notebook/cellViews/linkHandler.directive.ts @@ -5,19 +5,27 @@ import { Directive, Inject, HostListener, Input } from '@angular/core'; +import * as types from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { onUnexpectedError } from 'vs/base/common/errors'; import product from 'vs/platform/product/node/product'; +import { INotebookService } from 'sql/workbench/services/notebook/common/notebookService'; const knownSchemes = new Set(['http', 'https', 'file', 'mailto', 'data', `${product.urlProtocol}`, 'azuredatastudio', 'azuredatastudio-insiders', 'vscode', 'vscode-insiders', 'vscode-resource']); @Directive({ selector: '[link-handler]', }) export class LinkHandlerDirective { - + private workbenchFilePath: URI; @Input() isTrusted: boolean; - constructor(@Inject(IOpenerService) private readonly openerService: IOpenerService) { + @Input() notebookUri: URI; + + constructor( + @Inject(IOpenerService) private readonly openerService: IOpenerService, + @Inject(INotebookService) private readonly notebookService: INotebookService + ) { + this.workbenchFilePath = URI.parse(require.toUrl('vs/code/electron-browser/workbench/workbench.html')); } @HostListener('click', ['$event']) @@ -52,7 +60,11 @@ export class LinkHandlerDirective { // ignore } if (uri && this.openerService && this.isSupportedLink(uri)) { - this.openerService.open(uri).catch(onUnexpectedError); + if (uri.fragment && uri.fragment.length > 0 && uri.path === this.workbenchFilePath.path) { + this.notebookService.navigateTo(this.notebookUri, uri.fragment); + } else { + this.openerService.open(uri).catch(onUnexpectedError); + } } } diff --git a/src/sql/workbench/parts/notebook/cellViews/outputArea.component.html b/src/sql/workbench/parts/notebook/cellViews/outputArea.component.html index 02e8a85963..abb1c02977 100644 --- a/src/sql/workbench/parts/notebook/cellViews/outputArea.component.html +++ b/src/sql/workbench/parts/notebook/cellViews/outputArea.component.html @@ -5,7 +5,7 @@ *--------------------------------------------------------------------------------------------*/ -->
-
+
diff --git a/src/sql/workbench/parts/notebook/cellViews/outputArea.component.ts b/src/sql/workbench/parts/notebook/cellViews/outputArea.component.ts index cc91b63ce6..c39b858103 100644 --- a/src/sql/workbench/parts/notebook/cellViews/outputArea.component.ts +++ b/src/sql/workbench/parts/notebook/cellViews/outputArea.component.ts @@ -9,6 +9,7 @@ import { AngularDisposable } from 'sql/base/node/lifecycle'; import { ICellModel } from 'sql/workbench/parts/notebook/models/modelInterfaces'; import * as themeColors from 'vs/workbench/common/theme'; import { IWorkbenchThemeService, IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import { URI } from 'vs/base/common/uri'; export const OUTPUT_AREA_SELECTOR: string = 'output-area-component'; @@ -48,6 +49,10 @@ export class OutputAreaComponent extends AngularDisposable implements OnInit { return this.cellModel.trustedMode; } + public get notebookUri(): URI { + return this.cellModel.notebookModel.notebookUri; + } + private setFocusAndScroll(node: HTMLElement): void { if (node) { node.focus(); diff --git a/src/sql/workbench/parts/notebook/cellViews/textCell.component.html b/src/sql/workbench/parts/notebook/cellViews/textCell.component.html index 49eb055c52..72604439b1 100644 --- a/src/sql/workbench/parts/notebook/cellViews/textCell.component.html +++ b/src/sql/workbench/parts/notebook/cellViews/textCell.component.html @@ -10,7 +10,7 @@
-
+
diff --git a/src/sql/workbench/parts/notebook/cellViews/textCell.component.ts b/src/sql/workbench/parts/notebook/cellViews/textCell.component.ts index 969ceb5600..fe88133091 100644 --- a/src/sql/workbench/parts/notebook/cellViews/textCell.component.ts +++ b/src/sql/workbench/parts/notebook/cellViews/textCell.component.ts @@ -154,6 +154,10 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges { return this.model.trustedMode; } + public get notebookUri(): URI { + return this.model.notebookUri; + } + /** * Updates the preview of markdown component with latest changes * If content is empty and in non-edit mode, default it to 'Double-click to edit' diff --git a/src/sql/workbench/parts/notebook/notebook.component.ts b/src/sql/workbench/parts/notebook/notebook.component.ts index 92775a5432..bb1897a1af 100644 --- a/src/sql/workbench/parts/notebook/notebook.component.ts +++ b/src/sql/workbench/parts/notebook/notebook.component.ts @@ -24,7 +24,7 @@ import { AngularDisposable } from 'sql/base/node/lifecycle'; import { CellTypes, CellType } from 'sql/workbench/parts/notebook/models/contracts'; import { ICellModel, IModelFactory, INotebookModel, NotebookContentChange } from 'sql/workbench/parts/notebook/models/modelInterfaces'; import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement'; -import { INotebookService, INotebookParams, INotebookManager, INotebookEditor, DEFAULT_NOTEBOOK_PROVIDER, SQL_NOTEBOOK_PROVIDER } from 'sql/workbench/services/notebook/common/notebookService'; +import { INotebookService, INotebookParams, INotebookManager, INotebookEditor, INotebookSection, DEFAULT_NOTEBOOK_PROVIDER, SQL_NOTEBOOK_PROVIDER } from 'sql/workbench/services/notebook/common/notebookService'; import { IBootstrapParams } from 'sql/platform/bootstrap/node/bootstrapService'; import { NotebookModel } from 'sql/workbench/parts/notebook/models/notebookModel'; import { ModelFactory } from 'sql/workbench/parts/notebook/models/modelFactory'; @@ -582,4 +582,48 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe } } + getSections(): INotebookSection[] { + return this.getSectionElements(); + } + + private getSectionElements(): NotebookSection[] { + let headers: NotebookSection[] = []; + let el: HTMLElement = this.container.nativeElement; + let headerElements = el.querySelectorAll('h1, h2, h3, h4, h5, h6'); + for (let i = 0; i < headerElements.length; i++) { + let headerEl = headerElements[i] as HTMLElement; + if (headerEl['id']) { + headers.push(new NotebookSection(headerEl)); + } + } + return headers; + } + + navigateToSection(id: string): void { + id = id.toLowerCase(); + let section = this.getSectionElements().find(s => s.relativeUri && s.relativeUri.toLowerCase() === id); + if (section) { + // Scroll this section to the top of the header instead of just bringing header into view. + let scrollTop = jQuery(section.headerEl).offset().top; + (this.container.nativeElement).scrollTo({ + top: scrollTop, + behavior: 'smooth' + }); + section.headerEl.focus(); + } + } } + +class NotebookSection implements INotebookSection { + + constructor(public headerEl: HTMLElement) { + } + + get relativeUri(): string { + return this.headerEl['id']; + } + + get header(): string { + return this.headerEl.textContent; + } +} \ No newline at end of file diff --git a/src/sql/workbench/parts/notebook/outputs/markdownOutput.component.html b/src/sql/workbench/parts/notebook/outputs/markdownOutput.component.html index 8d9fd68187..7e1dc70754 100644 --- a/src/sql/workbench/parts/notebook/outputs/markdownOutput.component.html +++ b/src/sql/workbench/parts/notebook/outputs/markdownOutput.component.html @@ -1,5 +1,5 @@
-
+
diff --git a/src/sql/workbench/parts/notebook/outputs/markdownOutput.component.ts b/src/sql/workbench/parts/notebook/outputs/markdownOutput.component.ts index 9c09cac652..70852f71af 100644 --- a/src/sql/workbench/parts/notebook/outputs/markdownOutput.component.ts +++ b/src/sql/workbench/parts/notebook/outputs/markdownOutput.component.ts @@ -19,6 +19,7 @@ import { convertVscodeResourceToFileInSubDirectories, useInProcMarkdown } from ' import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { NotebookMarkdownRenderer } from 'sql/workbench/parts/notebook/outputs/notebookMarkdown'; +import { URI } from 'vs/base/common/uri'; @Component({ selector: MarkdownOutputComponent.SELECTOR, @@ -75,6 +76,10 @@ export class MarkdownOutputComponent extends AngularDisposable implements IMimeC return this._bundleOptions && this._bundleOptions.trusted; } + public get notebookUri(): URI { + return this.cellModel.notebookModel.notebookUri; + } + //Gets sanitizer from ISanitizer interface private get sanitizer(): ISanitizer { if (this._sanitizer) { diff --git a/src/sql/workbench/services/notebook/common/notebookService.ts b/src/sql/workbench/services/notebook/common/notebookService.ts index 7b8663d499..7dcef36598 100644 --- a/src/sql/workbench/services/notebook/common/notebookService.ts +++ b/src/sql/workbench/services/notebook/common/notebookService.ts @@ -90,6 +90,12 @@ export interface INotebookService { */ serializeNotebookStateChange(notebookUri: URI, changeType: NotebookChangeType): void; + /** + * + * @param notebookUri URI of the notebook to navigate to + * @param sectionId ID of the section to navigate to + */ + navigateTo(notebookUri: URI, sectionId: string): void; } export interface INotebookProvider { @@ -117,6 +123,15 @@ export interface INotebookParams extends IBootstrapParams { modelFactory?: ModelFactory; } +/** + * Defines a section in a notebook as the header text for that section, + * the relative URI that can be used to link to it inside Notebook documents + */ +export interface INotebookSection { + header: string; + relativeUri: string; +} + export interface INotebookEditor { readonly notebookParams: INotebookParams; readonly id: string; @@ -131,4 +146,6 @@ export interface INotebookEditor { runAllCells(startCell?: ICellModel, endCell?: ICellModel): Promise; clearOutput(cell: ICellModel): Promise; clearAllOutputs(): Promise; + getSections(): INotebookSection[]; + navigateToSection(sectionId: string): void; } diff --git a/src/sql/workbench/services/notebook/common/notebookServiceImpl.ts b/src/sql/workbench/services/notebook/common/notebookServiceImpl.ts index afe7130d48..4f413558da 100644 --- a/src/sql/workbench/services/notebook/common/notebookServiceImpl.ts +++ b/src/sql/workbench/services/notebook/common/notebookServiceImpl.ts @@ -619,4 +619,11 @@ export class NotebookService extends Disposable implements INotebookService { } } } + + navigateTo(notebookUri: URI, sectionId: string): void { + let editor = this._editors.get(notebookUri.toString()); + if (editor) { + editor.navigateToSection(sectionId); + } + } }