Add markdown cell to Notebook (#3014)

* add markdown cell

* add markdown preview for Notebook

* formatting

* address comment
This commit is contained in:
Abbie Petchtes
2018-10-26 16:20:06 -07:00
committed by GitHub
parent 2859bee4c0
commit 533f2734f1
15 changed files with 162 additions and 19 deletions

View File

@@ -23,7 +23,8 @@
"onCommand:markdown.showLockedPreviewToSide", "onCommand:markdown.showLockedPreviewToSide",
"onCommand:markdown.showSource", "onCommand:markdown.showSource",
"onCommand:markdown.showPreviewSecuritySelector", "onCommand:markdown.showPreviewSecuritySelector",
"onWebviewPanel:markdown.preview" "onWebviewPanel:markdown.preview",
"onCommand:notebook.showPreview"
], ],
"contributes": { "contributes": {
"commands": [ "commands": [
@@ -77,6 +78,11 @@
"command": "markdown.preview.toggleLock", "command": "markdown.preview.toggleLock",
"title": "%markdown.preview.toggleLock.title%", "title": "%markdown.preview.toggleLock.title%",
"category": "Markdown" "category": "Markdown"
},
{
"command": "notebook.showPreview",
"title": "notebook.showPreview",
"category": "Notebook"
} }
], ],
"menus": { "menus": {
@@ -154,6 +160,10 @@
{ {
"command": "markdown.preview.toggleLock", "command": "markdown.preview.toggleLock",
"when": "markdownPreviewFocus" "when": "markdownPreviewFocus"
},
{
"command": "notebook.showPreview",
"when": "false"
} }
] ]
}, },

View File

@@ -8,7 +8,8 @@ import * as vscode from 'vscode';
export interface Command { export interface Command {
readonly id: string; readonly id: string;
execute(...args: any[]): void; // {{SQL CARBON EDIT}}
execute(...args: any[]): any;
} }
export class CommandManager { export class CommandManager {
@@ -26,7 +27,8 @@ export class CommandManager {
return command; return command;
} }
private registerCommand(id: string, impl: (...args: any[]) => void, thisArg?: any) { // {{SQL CARBON EDIT}}
private registerCommand(id: string, impl: (...args: any[]) => any, thisArg?: any) {
if (this.commands.has(id)) { if (this.commands.has(id)) {
return; return;
} }

View File

@@ -11,3 +11,5 @@ export { RefreshPreviewCommand } from './refreshPreview';
export { ShowPreviewSecuritySelectorCommand } from './showPreviewSecuritySelector'; export { ShowPreviewSecuritySelectorCommand } from './showPreviewSecuritySelector';
export { MoveCursorToPositionCommand } from './moveCursorToPosition'; export { MoveCursorToPositionCommand } from './moveCursorToPosition';
export { ToggleLockCommand } from './toggleLock'; export { ToggleLockCommand } from './toggleLock';
// {{SQL CARBON EDIT}}
export { ShowNotebookPreview } from './showNotebookPreview';

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 vscode from 'vscode';
import { Command } from '../commandManager';
import { MarkdownEngine } from '../markdownEngine';
export class ShowNotebookPreview implements Command {
public readonly id = 'notebook.showPreview';
public constructor(
private readonly engine: MarkdownEngine
) { }
public async execute(document: vscode.Uri, textContent: string): Promise<string> {
return this.engine.renderText(document, textContent);
}
}

View File

@@ -59,6 +59,8 @@ export function activate(context: vscode.ExtensionContext) {
commandManager.register(new commands.OnPreviewStyleLoadErrorCommand()); commandManager.register(new commands.OnPreviewStyleLoadErrorCommand());
commandManager.register(new commands.OpenDocumentLinkCommand(engine)); commandManager.register(new commands.OpenDocumentLinkCommand(engine));
commandManager.register(new commands.ToggleLockCommand(previewManager)); commandManager.register(new commands.ToggleLockCommand(previewManager));
// {{SQL CARBON EDIT}}
commandManager.register(new commands.ShowNotebookPreview(engine));
context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(() => { context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(() => {
logger.updateConfiguration(); logger.updateConfiguration();

View File

@@ -86,6 +86,12 @@ export class MarkdownEngine {
return { text, offset }; return { text, offset };
} }
// {{SQL CARBON EDIT}}
public async renderText(document: vscode.Uri, text: string): Promise<string> {
const engine = await this.getEngine(document);
return engine.render(text);
}
public async render(document: vscode.Uri, stripFrontmatter: boolean, text: string): Promise<string> { public async render(document: vscode.Uri, stripFrontmatter: boolean, text: string): Promise<string> {
let offset = 0; let offset = 0;
if (stripFrontmatter) { if (stripFrontmatter) {

View File

@@ -4,13 +4,14 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import 'vs/css!./code'; import 'vs/css!./code';
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild } from '@angular/core'; import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, Output, EventEmitter } from '@angular/core';
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service'; import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
import { AngularDisposable } from 'sql/base/common/lifecycle'; import { AngularDisposable } from 'sql/base/common/lifecycle';
import { ComponentBase } from 'sql/parts/modelComponents/componentBase'; import { ComponentBase } from 'sql/parts/modelComponents/componentBase';
import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/parts/modelComponents/interfaces'; import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/parts/modelComponents/interfaces';
import { QueryTextEditor } from 'sql/parts/modelComponents/queryTextEditor'; import { QueryTextEditor } from 'sql/parts/modelComponents/queryTextEditor';
import { ICellModel } from 'sql/parts/notebook/cellViews/interfaces';
import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import * as themeColors from 'vs/workbench/common/theme'; import * as themeColors from 'vs/workbench/common/theme';
@@ -26,7 +27,6 @@ import * as DOM from 'vs/base/browser/dom';
import { IModeService } from 'vs/editor/common/services/modeService'; import { IModeService } from 'vs/editor/common/services/modeService';
import { IModelService } from 'vs/editor/common/services/modelService'; import { IModelService } from 'vs/editor/common/services/modelService';
export const CODE_SELECTOR: string = 'code-component'; export const CODE_SELECTOR: string = 'code-component';
@Component({ @Component({
@@ -36,9 +36,8 @@ export const CODE_SELECTOR: string = 'code-component';
export class CodeComponent extends AngularDisposable implements OnInit { export class CodeComponent extends AngularDisposable implements OnInit {
@ViewChild('toolbar', { read: ElementRef }) private toolbarElement: ElementRef; @ViewChild('toolbar', { read: ElementRef }) private toolbarElement: ElementRef;
@ViewChild('editor', { read: ElementRef }) private codeElement: ElementRef; @ViewChild('editor', { read: ElementRef }) private codeElement: ElementRef;
@Input() id: string; @Input() cellModel: ICellModel;
@Input() content: string; @Output() public onContentChanged = new EventEmitter<void>();
@Input() language: string;
private readonly _minimumHeight = 30; private readonly _minimumHeight = 30;
private _editor: QueryTextEditor; private _editor: QueryTextEditor;
@@ -81,18 +80,19 @@ export class CodeComponent extends AngularDisposable implements OnInit {
this._editor.setVisible(true); this._editor.setVisible(true);
this._editor.setMinimumHeight(this._minimumHeight); this._editor.setMinimumHeight(this._minimumHeight);
let uri = this.createUri(); let uri = this.createUri();
this._editorInput = instantiationService.createInstance(UntitledEditorInput, uri, false, this.language, '', ''); this._editorInput = instantiationService.createInstance(UntitledEditorInput, uri, false, this.cellModel.language, '', '');
this._editor.setInput(this._editorInput, undefined); this._editor.setInput(this._editorInput, undefined);
this._editorInput.resolve().then(model => { this._editorInput.resolve().then(model => {
this._editorModel = model.textEditorModel; this._editorModel = model.textEditorModel;
this._modelService.updateModel(this._editorModel, this.content); this._modelService.updateModel(this._editorModel, this.cellModel.source);
}); });
this._register(this._editor); this._register(this._editor);
this._register(this._editorInput); this._register(this._editorInput);
this._register(this._editorModel.onDidChangeContent(e => { this._register(this._editorModel.onDidChangeContent(e => {
this.content = this._editorModel.getValue();
this._editor.setHeightToScrollHeight(); this._editor.setHeightToScrollHeight();
this.cellModel.source = this._editorModel.getValue();
this.onContentChanged.emit();
})); }));
this.layout(); this.layout();
} }
@@ -105,22 +105,22 @@ export class CodeComponent extends AngularDisposable implements OnInit {
} }
private createUri(): URI { private createUri(): URI {
let uri = URI.from({ scheme: Schemas.untitled, path: `notebook-editor-${this.id}` }); let uri = URI.from({ scheme: Schemas.untitled, path: `notebook-editor-${this.cellModel.id}` });
// Use this to set the internal (immutable) and public (shared with extension) uri properties // Use this to set the internal (immutable) and public (shared with extension) uri properties
this._uri = uri.toString(); this.cellModel.cellUri = uri;
return uri; return uri;
} }
/// Editor Functions /// Editor Functions
private updateModel() { private updateModel() {
if (this._editorModel) { if (this._editorModel) {
this._modelService.updateModel(this._editorModel, this.content); this._modelService.updateModel(this._editorModel, this.cellModel.source);
} }
} }
private updateLanguageMode() { private updateLanguageMode() {
if (this._editorModel && this._editor) { if (this._editorModel && this._editor) {
this._modeService.getOrCreateMode(this.language).then((modeValue) => { this._modeService.getOrCreateMode(this.cellModel.language).then((modeValue) => {
this._modelService.setMode(this._editorModel, modeValue); this._modelService.setMode(this._editorModel, modeValue);
}); });
} }

View File

@@ -6,7 +6,7 @@
--> -->
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column"> <div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
<div class="notebook-code" style="flex: 0 0 auto;"> <div class="notebook-code" style="flex: 0 0 auto;">
<code-component [id]="cellModel.id" [content]="cellModel.source" [language]="cellModel.language"></code-component> <code-component [cellModel]="cellModel"></code-component>
</div> </div>
<div #output class="notebook-output" style="flex: 0 0 auto;"> <div #output class="notebook-output" style="flex: 0 0 auto;">
Place Holder for output area Place Holder for output area

View File

@@ -5,7 +5,7 @@
import { OnDestroy } from '@angular/core'; import { OnDestroy } from '@angular/core';
import { AngularDisposable } from 'sql/base/common/lifecycle'; import { AngularDisposable } from 'sql/base/common/lifecycle';
import URI from 'vs/base/common/uri';
export abstract class CellView extends AngularDisposable implements OnDestroy { export abstract class CellView extends AngularDisposable implements OnDestroy {
constructor() { constructor() {
@@ -21,6 +21,7 @@ export interface ICellModel {
source: string; source: string;
cellType: CellType; cellType: CellType;
active: boolean; active: boolean;
cellUri?: URI;
} }
export type CellType = 'code' | 'markdown' | 'raw'; export type CellType = 'code' | 'markdown' | 'raw';

View File

@@ -0,0 +1,13 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
<div class="notebook-text" style="flex: 0 0 auto;">
<code-component [cellModel]="cellModel" (onContentChanged)="handleContentChanged()"></code-component>
</div>
<div #preview class="notebook-preview" style="flex: 0 0 auto;">
</div>
</div>

View File

@@ -0,0 +1,67 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./textCell';
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild } from '@angular/core';
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
import { CellView, ICellModel } from 'sql/parts/notebook/cellViews/interfaces';
import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import * as themeColors from 'vs/workbench/common/theme';
import { ICommandService } from 'vs/platform/commands/common/commands';
export const TEXT_SELECTOR: string = 'text-cell-component';
@Component({
selector: TEXT_SELECTOR,
templateUrl: decodeURI(require.toUrl('./textCell.component.html'))
})
export class TextCellComponent extends CellView implements OnInit {
@ViewChild('preview', { read: ElementRef }) private output: ElementRef;
@Input() cellModel: ICellModel;
private _content: string;
constructor(
@Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface,
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
@Inject(ICommandService) private _commandService: ICommandService
) {
super();
}
ngOnChanges() {
this.updatePreview();
}
private updatePreview() {
if (this._content !== this.cellModel.source) {
this._content = this.cellModel.source;
// todo: pass in the notebook filename instead of undefined value
this._commandService.executeCommand('notebook.showPreview', undefined, this._content).then((htmlcontent) => {
let outputElement = <HTMLElement>this.output.nativeElement;
outputElement.innerHTML = htmlcontent;
});
}
}
ngOnInit() {
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
this.updateTheme(this.themeService.getColorTheme());
}
// Todo: implement layout
public layout() {
}
private updateTheme(theme: IColorTheme): void {
let outputElement = <HTMLElement>this.output.nativeElement;
outputElement.style.borderTopColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
}
public handleContentChanged(): void {
this.updatePreview();
}
}

View File

@@ -0,0 +1,13 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
text-cell-component {
display: block;
}
text-cell-component .notebook-preview {
border-top-width: 1px;
border-top-style: solid;
}

View File

@@ -12,8 +12,10 @@
</div> </div>
<div style="flex: 1 1 auto; position: relative"> <div style="flex: 1 1 auto; position: relative">
<div class="notebook-cell" *ngFor="let cell of cells" (click)="selectCell(cell)" [class.active]="cell.active" > <div class="notebook-cell" *ngFor="let cell of cells" (click)="selectCell(cell)" [class.active]="cell.active" >
<code-cell-component [cellModel]="cell"> <code-cell-component *ngIf="cell.cellType === 'code'" [cellModel]="cell">
</code-cell-component> </code-cell-component>
<text-cell-component *ngIf="cell.cellType === 'markdown'" [cellModel]="cell">
</text-cell-component>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -38,7 +38,10 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
let cell2: ICellModel = { let cell2: ICellModel = {
id: '2', language: 'sql', source: 'select 1', cellType: CellTypes.Code, active: false id: '2', language: 'sql', source: 'select 1', cellType: CellTypes.Code, active: false
}; };
this.cells.push(cell1, cell2); let cell3: ICellModel = {
id: '3', language: 'markdown', source: '## This is test!', cellType: CellTypes.Markdown, active: false
};
this.cells.push(cell1, cell2, cell3);
} }
ngOnInit() { ngOnInit() {

View File

@@ -25,6 +25,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { Registry } from 'vs/platform/registry/common/platform'; import { Registry } from 'vs/platform/registry/common/platform';
import { CodeComponent } from 'sql/parts/notebook/cellViews/code.component'; import { CodeComponent } from 'sql/parts/notebook/cellViews/code.component';
import { CodeCellComponent } from 'sql/parts/notebook/cellViews/codeCell.component'; import { CodeCellComponent } from 'sql/parts/notebook/cellViews/codeCell.component';
import { TextCellComponent } from 'sql/parts/notebook/cellViews/textCell.component';
export const NotebookModule = (params, selector: string, instantiationService: IInstantiationService): any => { export const NotebookModule = (params, selector: string, instantiationService: IInstantiationService): any => {
@NgModule({ @NgModule({
@@ -35,6 +36,7 @@ export const NotebookModule = (params, selector: string, instantiationService: I
InputBox, InputBox,
CodeComponent, CodeComponent,
CodeCellComponent, CodeCellComponent,
TextCellComponent,
NotebookComponent, NotebookComponent,
ComponentHostDirective ComponentHostDirective
], ],