Implement Session support through the extension host (#3228)

Full plumb through of Session support. Also fixed some test issues

- Load session and get necessary information in kernels list
- Run Cell button now works as expected
- Added a ToggleAction base class which can be used for anything that switches icons. I'd still prefer to have this be dynamic and as clean as the extension classes
- Fixed account test unhandled promise rejections (caused by incorrect / invalid tests) that made it hard to see all the test run output.
This commit is contained in:
Kevin Cunnane
2018-11-16 10:35:03 -08:00
committed by GitHub
parent f3525cc555
commit 90dc788893
23 changed files with 939 additions and 212 deletions

View File

@@ -29,7 +29,7 @@ import { IModelService } from 'vs/editor/common/services/modelService';
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces';
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
import { RunCellAction, DeleteCellAction, AddCellAction } from 'sql/parts/notebook/cellViews/codeActions';
import { RunCellAction, DeleteCellAction, AddCellAction, CellContext } from 'sql/parts/notebook/cellViews/codeActions';
import { NotebookModel } from 'sql/parts/notebook/models/notebookModel';
import { ToggleMoreWidgetAction } from 'sql/parts/dashboard/common/actions';
import { CellTypes } from 'sql/parts/notebook/models/contracts';
@@ -97,7 +97,7 @@ export class CodeComponent extends AngularDisposable implements OnInit {
get model(): NotebookModel {
return this._model;
}
private createEditor(): void {
let instantiationService = this._instantiationService.createChild(new ServiceCollection([IProgressService, new SimpleProgressService()]));
this._editor = instantiationService.createInstance(QueryTextEditor);
@@ -130,12 +130,12 @@ export class CodeComponent extends AngularDisposable implements OnInit {
}
protected initActionBar() {
let context = new CellContext(this.model, this.cellModel);
let runCellAction = this._instantiationService.createInstance(RunCellAction);
let taskbar = <HTMLElement>this.toolbarElement.nativeElement;
this._actionBar = new Taskbar(taskbar, this.contextMenuService);
this._actionBar.context = this;
this._actionBar.context = context;
this._actionBar.setContent([
{ action: runCellAction }
]);
@@ -145,13 +145,13 @@ export class CodeComponent extends AngularDisposable implements OnInit {
this._moreActions.context = { target: moreActionsElement };
let actions: Action[] = [];
actions.push(this._instantiationService.createInstance(AddCellAction, 'codeBefore', localize('codeBefore', 'Insert Code before'), CellTypes.Code, false, this.notificationService));
actions.push(this._instantiationService.createInstance(AddCellAction, 'codeAfter', localize('codeAfter', 'Insert Code after'), CellTypes.Code, true, this.notificationService));
actions.push(this._instantiationService.createInstance(AddCellAction, 'markdownBefore', localize('markdownBefore', 'Insert Markdown before'), CellTypes.Markdown, false, this.notificationService));
actions.push(this._instantiationService.createInstance(AddCellAction, 'markdownAfter', localize('markdownAfter', 'Insert Markdown after'), CellTypes.Markdown, true, this.notificationService));
actions.push(this._instantiationService.createInstance(DeleteCellAction, 'delete', localize('delete', 'Delete'), CellTypes.Code, false, this.notificationService));
actions.push(this._instantiationService.createInstance(AddCellAction, 'codeBefore', localize('codeBefore', 'Insert Code before'), CellTypes.Code, false));
actions.push(this._instantiationService.createInstance(AddCellAction, 'codeAfter', localize('codeAfter', 'Insert Code after'), CellTypes.Code, true));
actions.push(this._instantiationService.createInstance(AddCellAction, 'markdownBefore', localize('markdownBefore', 'Insert Markdown before'), CellTypes.Markdown, false));
actions.push(this._instantiationService.createInstance(AddCellAction, 'markdownAfter', localize('markdownAfter', 'Insert Markdown after'), CellTypes.Markdown, true));
actions.push(this._instantiationService.createInstance(DeleteCellAction, 'delete', localize('delete', 'Delete')));
this._moreActions.push(this._instantiationService.createInstance(ToggleMoreWidgetAction, actions, this.model, this.contextMenuService), { icon: true, label: false });
this._moreActions.push(this._instantiationService.createInstance(ToggleMoreWidgetAction, actions, context), { icon: true, label: false });
}
private createUri(): URI {

View File

@@ -25,6 +25,17 @@ code-component .toolbarIconRun {
background-image: url('../media/dark/execute_cell_inverse.svg');
}
code-component .toolbarIconStop {
height: 20px;
background-image: url('../media/light/stop_cell.svg');
padding-bottom: 10px;
}
.vs-dark code-component .toolbarIconStop,
.hc-black code-component .toolbarIconStop {
background-image: url('../media/dark/stop_cell_inverse.svg');
}
/* overview ruler */
code-component .monaco-editor .decorationsOverviewRuler {
visibility: hidden;

View File

@@ -1,7 +1,9 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { nb } from 'sqlops';
import { Action } from 'vs/base/common/actions';
import { TPromise } from 'vs/base/common/winjs.base';
@@ -10,97 +12,176 @@ import { CellType } from 'sql/parts/notebook/models/contracts';
import { NotebookModel } from 'sql/parts/notebook/models/notebookModel';
import { getErrorMessage } from 'sql/parts/notebook/notebookUtils';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { NOTFOUND } from 'dns';
import { NsfwWatcherService } from 'vs/workbench/services/files/node/watcher/nsfw/nsfwWatcherService';
import { ICellModel, FutureInternal } from 'sql/parts/notebook/models/modelInterfaces';
import { ToggleableAction } from 'sql/parts/notebook/notebookActions';
let notebookMoreActionMsg = localize('notebook.failed', "Please select active cell and try again");
export class RunCellAction extends Action {
public static ID = 'jobaction.notebookRunCell';
function hasModelAndCell(context: CellContext, notificationService: INotificationService): boolean {
if (!context || !context.model) {
return false;
}
if (context.cell === undefined) {
notificationService.notify({
severity: Severity.Error,
message: notebookMoreActionMsg
});
return false;
}
return true;
}
export class CellContext {
constructor(public model: NotebookModel, private _cell?: ICellModel) {
}
public get cell(): ICellModel {
return this._cell ? this._cell : this.model.activeCell;
}
}
abstract class CellActionBase extends Action {
constructor(id: string, label: string, icon: string, protected notificationService: INotificationService) {
super(id, label, icon);
}
public run(context: CellContext): TPromise<boolean> {
if (hasModelAndCell(context, this.notificationService)) {
return TPromise.wrap(this.runCellAction(context).then(() => true));
}
return TPromise.as(true);
}
abstract runCellAction(context: CellContext): Promise<void>;
}
export class RunCellAction extends ToggleableAction {
public static ID = 'notebook.runCell';
public static LABEL = 'Run cell';
constructor(
) {
super(RunCellAction.ID, '', 'toolbarIconRun');
this.tooltip = localize('runCell', 'Run cell');
constructor(@INotificationService private notificationService: INotificationService) {
super(RunCellAction.ID, {
shouldToggleTooltip: true,
toggleOnLabel: localize('runCell', 'Run cell'),
toggleOnClass: 'toolbarIconRun',
toggleOffLabel: localize('stopCell', 'Cancel execution'),
toggleOffClass: 'toolbarIconStop',
isOn: true
});
}
public run(context: any): TPromise<boolean> {
return new TPromise<boolean>((resolve, reject) => {
try {
resolve(true);
} catch (e) {
reject(e);
public run(context: CellContext): TPromise<boolean> {
if (hasModelAndCell(context, this.notificationService)) {
return TPromise.wrap(this.runCellAction(context).then(() => true));
}
return TPromise.as(true);
}
public async runCellAction(context: CellContext): Promise<void> {
try {
let model = context.model;
let cell = context.cell;
let kernel = await this.getOrStartKernel(model);
if (!kernel) {
return;
}
});
// If cell is currently running and user clicks the stop/cancel button, call kernel.interrupt()
// This matches the same behavior as JupyterLab
if (cell.future && cell.future.inProgress) {
cell.future.inProgress = false;
await kernel.interrupt();
} else {
// TODO update source based on editor component contents
let content = cell.source;
if (content) {
this.toggle(false);
let future = await kernel.requestExecute({
code: content,
stop_on_error: true
}, false);
cell.setFuture(future as FutureInternal);
// For now, await future completion. Later we should just track and handle cancellation based on model notifications
let reply = await future.done;
}
}
} catch (error) {
let message = getErrorMessage(error);
this.notificationService.error(message);
} finally {
this.toggle(true);
}
}
private async getOrStartKernel(model: NotebookModel): Promise<nb.IKernel> {
let clientSession = model && model.clientSession;
if (!clientSession) {
this.notificationService.error(localize('notebookNotReady', 'The session for this notebook is not yet ready'));
return undefined;
} else if (!clientSession.isReady || clientSession.status === 'dead') {
this.notificationService.info(localize('sessionNotReady', 'The session for this notebook will start momentarily'));
await clientSession.kernelChangeCompleted;
}
if (!clientSession.kernel) {
let defaultKernel = model && model.defaultKernel && model.defaultKernel.name;
if (!defaultKernel) {
this.notificationService.error(localize('noDefaultKernel', 'No kernel is available for this notebook'));
return undefined;
}
await clientSession.changeKernel({
name: defaultKernel
});
}
return clientSession.kernel;
}
}
export class AddCellAction extends CellActionBase {
constructor(
id: string, label: string, private cellType: CellType, private isAfter: boolean,
@INotificationService notificationService: INotificationService
) {
super(id, label, undefined, notificationService);
}
runCellAction(context: CellContext): Promise<void> {
try {
let model = context.model;
let index = model.cells.findIndex((cell) => cell.id === context.cell.id);
if (index !== undefined && this.isAfter) {
index += 1;
}
model.addCell(this.cellType, index);
} catch (error) {
let message = getErrorMessage(error);
this.notificationService.notify({
severity: Severity.Error,
message: message
});
}
return Promise.resolve();
}
}
export class AddCellAction extends Action {
constructor(
id: string, label: string, private cellType: CellType, private isAfter: boolean, private notificationService: INotificationService
export class DeleteCellAction extends CellActionBase {
constructor(id: string, label: string,
@INotificationService notificationService: INotificationService
) {
super(id, label);
super(id, label, undefined, notificationService);
}
public run(model: NotebookModel): TPromise<boolean> {
return new TPromise<boolean>((resolve, reject) => {
try {
if (!model) {
return;
}
if (model.activeCell === undefined) {
this.notificationService.notify({
severity: Severity.Error,
message: notebookMoreActionMsg
});
}
else {
let index = model.cells.findIndex((cell) => cell.id === model.activeCell.id);
if (index !== undefined && this.isAfter) {
index += 1;
}
model.addCell(this.cellType, index);
}
} catch (error) {
let message = getErrorMessage(error);
this.notificationService.notify({
severity: Severity.Error,
message: message
});
}
});
}
}
runCellAction(context: CellContext): Promise<void> {
try {
context.model.deleteCell(context.cell);
} catch (error) {
let message = getErrorMessage(error);
export class DeleteCellAction extends Action {
constructor(
id: string, label: string, private cellType: CellType, private isAfter: boolean, private notificationService: INotificationService
) {
super(id, label);
}
public run(model: NotebookModel): TPromise<boolean> {
return new TPromise<boolean>((resolve, reject) => {
try {
if (!model) {
return;
}
if (model.activeCell === undefined) {
this.notificationService.notify({
severity: Severity.Error,
message: notebookMoreActionMsg
});
}
else {
model.deleteCell(model.activeCell);
}
} catch (error) {
let message = getErrorMessage(error);
this.notificationService.notify({
severity: Severity.Error,
message: message
});
}
});
this.notificationService.notify({
severity: Severity.Error,
message: message
});
}
return Promise.resolve();
}
}

View File

@@ -39,6 +39,11 @@ export class CodeCellComponent extends CellView implements OnInit {
ngOnInit() {
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
this.updateTheme(this.themeService.getColorTheme());
if (this.cellModel) {
this.cellModel.onOutputsChanged(() => {
this._changeRef.detectChanges();
});
}
}
// Todo: implement layout

View File

@@ -3,9 +3,8 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./code';
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, Output, EventEmitter } from '@angular/core';
import { OnInit, Component, Input, Inject, forwardRef, ChangeDetectorRef } from '@angular/core';
import { AngularDisposable } from 'sql/base/common/lifecycle';
import { IModeService } from 'vs/editor/common/services/modeService';
import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces';
export const OUTPUT_AREA_SELECTOR: string = 'output-area-component';
@@ -20,11 +19,15 @@ export class OutputAreaComponent extends AngularDisposable implements OnInit {
private readonly _minimumHeight = 30;
constructor(
@Inject(IModeService) private _modeService: IModeService
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
) {
super();
}
ngOnInit(): void {
if (this.cellModel) {
this.cellModel.onOutputsChanged(() => {
this._changeRef.detectChanges();
});
}
}
}