Fix #5238 Notebooks should support relative links (#6289)

* Fix #5238 Notebooks should support relative links
- Added detection of relative #links inside notebooks
- Added handling of these, at least for current notebook

Not handled: open other notebook & scroll to position.
This commit is contained in:
Kevin Cunnane
2019-07-09 14:30:57 -07:00
committed by GitHub
parent 930731423d
commit aef74c6d5a
10 changed files with 101 additions and 7 deletions

View File

@@ -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);
}
}
}

View File

@@ -5,7 +5,7 @@
*--------------------------------------------------------------------------------------------*/
-->
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
<div #outputarea link-handler [isTrusted]="isTrusted" class="notebook-output" style="flex: 0 0 auto;">
<div #outputarea link-handler [isTrusted]="isTrusted" [notebookUri]="notebookUri" class="notebook-output" style="flex: 0 0 auto;">
<output-component *ngFor="let output of cellModel.outputs" [cellOutput]="output" [trustedMode] = "cellModel.trustedMode" [cellModel]="cellModel" [activeCellId]="activeCellId">
</output-component>
</div>

View File

@@ -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();

View File

@@ -10,7 +10,7 @@
</code-component>
</div>
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: row">
<div #preview link-handler [isTrusted]="isTrusted" class="notebook-preview" style="flex: 1 1 auto" (dblclick)="toggleEditMode()">
<div #preview link-handler [isTrusted]="isTrusted" [notebookUri]="notebookUri" class="notebook-preview" style="flex: 1 1 auto" (dblclick)="toggleEditMode()">
</div>
<div #moreactions class="moreActions" style="flex: 0 0 auto; display: flex; flex-flow:column;width: 20px; min-height: 20px; max-height: 20px; padding-top: 0px; orientation: portrait">
</div>

View File

@@ -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'

View File

@@ -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;
(<HTMLElement>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;
}
}

View File

@@ -1,5 +1,5 @@
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: row">
<div class="icon in-progress" *ngIf="loading === true"></div>
<div #output link-handler [isTrusted]="isTrusted" class="notebook-preview" style="flex: 1 1 auto">
<div #output link-handler [isTrusted]="isTrusted" [notebookUri]="notebookUri" class="notebook-preview" style="flex: 1 1 auto">
</div>
</div>

View File

@@ -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) {

View File

@@ -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<boolean>;
clearOutput(cell: ICellModel): Promise<boolean>;
clearAllOutputs(): Promise<boolean>;
getSections(): INotebookSection[];
navigateToSection(sectionId: string): void;
}

View File

@@ -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);
}
}
}