Move kernel/execution code out of NotebookEditorWidget for testability

This commit is contained in:
Rob Lourens 2021-02-19 09:39:32 -08:00
parent 6a6b9a5cc8
commit b9f20c79cb
2 changed files with 479 additions and 350 deletions

View file

@ -0,0 +1,430 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput';
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
import { Memento } from 'vs/workbench/common/memento';
import { ICellViewModel, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_HAS_MULTIPLE_KERNELS, NOTEBOOK_KERNEL_COUNT } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { configureKernelIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons';
import { NotebookKernelProviderAssociation, NotebookKernelProviderAssociations, notebookKernelProviderAssociationsSettingId } from 'vs/workbench/contrib/notebook/browser/notebookKernelAssociation';
import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel';
import { CellKind, INotebookKernel, NotebookCellRunState, NotebookRunState } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider';
const NotebookEditorActiveKernelCache = 'workbench.editor.notebook.activeKernel';
export interface IKernelManagerDelegate {
viewModel: NotebookViewModel | undefined;
getId(): string;
getContributedNotebookProviders(resource?: URI): readonly NotebookProviderInfo[];
getContributedNotebookProvider(viewType: string): NotebookProviderInfo | undefined;
getNotebookKernels(viewType: string, resource: URI, token: CancellationToken): Promise<INotebookKernel[]>;
loadKernelPreloads(extensionLocation: URI, kernel: INotebookKernel): Promise<void>;
}
export class NotebookEditorKernelManager extends Disposable {
private _isDisposed: boolean = false;
private _activeKernelExecuted: boolean = false;
private _activeKernel: INotebookKernel | undefined = undefined;
private readonly _onDidChangeKernel = this._register(new Emitter<void>());
readonly onDidChangeKernel: Event<void> = this._onDidChangeKernel.event;
private readonly _onDidChangeAvailableKernels = this._register(new Emitter<void>());
readonly onDidChangeAvailableKernels: Event<void> = this._onDidChangeAvailableKernels.event;
private _contributedKernelsComputePromise: CancelablePromise<INotebookKernel[]> | null = null;
private _initialKernelComputationDone: boolean = false;
private readonly _editorRunnable: IContextKey<boolean>;
private readonly _notebookExecuting: IContextKey<boolean>;
private readonly _notebookHasMultipleKernels: IContextKey<boolean>;
private readonly _notebookKernelCount: IContextKey<number>;
get activeKernel() {
return this._activeKernel;
}
set activeKernel(kernel: INotebookKernel | undefined) {
if (this._isDisposed) {
return;
}
if (!this._delegate.viewModel) {
return;
}
if (this._activeKernel === kernel) {
return;
}
this._activeKernel = kernel;
this._activeKernelResolvePromise = undefined;
const memento = this._activeKernelMemento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE);
memento[this._delegate.viewModel.viewType] = this._activeKernel?.friendlyId;
this._activeKernelMemento.saveMemento();
this._onDidChangeKernel.fire();
if (this._activeKernel) {
this._delegate.loadKernelPreloads(this._activeKernel.extensionLocation, this._activeKernel);
}
}
private _activeKernelResolvePromise: Promise<void> | undefined = undefined;
private _multipleKernelsAvailable: boolean = false;
get multipleKernelsAvailable() {
return this._multipleKernelsAvailable;
}
set multipleKernelsAvailable(state: boolean) {
this._multipleKernelsAvailable = state;
this._onDidChangeAvailableKernels.fire();
}
private readonly _activeKernelMemento: Memento;
constructor(
private readonly _delegate: IKernelManagerDelegate,
@IStorageService storageService: IStorageService,
@IContextKeyService contextKeyService: IContextKeyService,
@IQuickInputService private readonly _quickInputService: IQuickInputService,
@IConfigurationService private readonly _configurationService: IConfigurationService,) {
super();
this._activeKernelMemento = new Memento(NotebookEditorActiveKernelCache, storageService);
this._editorRunnable = NOTEBOOK_EDITOR_RUNNABLE.bindTo(contextKeyService);
this._notebookExecuting = NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK.bindTo(contextKeyService);
this._notebookHasMultipleKernels = NOTEBOOK_HAS_MULTIPLE_KERNELS.bindTo(contextKeyService);
this._notebookKernelCount = NOTEBOOK_KERNEL_COUNT.bindTo(contextKeyService);
}
public async setKernels(tokenSource: CancellationTokenSource) {
if (!this._delegate.viewModel) {
return;
}
if (this._activeKernel !== undefined && this._activeKernelExecuted) {
// kernel already executed, we should not change it automatically
return;
}
const provider = this._delegate.getContributedNotebookProvider(this._delegate.viewModel.viewType) || this._delegate.getContributedNotebookProviders(this._delegate.viewModel.uri)[0];
const availableKernels = await this.beginComputeContributedKernels();
if (tokenSource.token.isCancellationRequested) {
return;
}
this._notebookKernelCount.set(availableKernels.length);
if (availableKernels.length > 1) {
this._notebookHasMultipleKernels.set(true);
this.multipleKernelsAvailable = true;
} else {
this._notebookHasMultipleKernels.set(false);
this.multipleKernelsAvailable = false;
}
const activeKernelStillExist = [...availableKernels].find(kernel => kernel.friendlyId === this.activeKernel?.friendlyId && this.activeKernel?.friendlyId !== undefined);
if (activeKernelStillExist) {
// the kernel still exist, we don't want to modify the selection otherwise user's temporary preference is lost
return;
}
if (availableKernels.length) {
return this._setKernelsFromProviders(provider, availableKernels, tokenSource);
}
this._initialKernelComputationDone = true;
tokenSource.dispose();
}
async beginComputeContributedKernels() {
if (this._contributedKernelsComputePromise) {
return this._contributedKernelsComputePromise;
}
this._contributedKernelsComputePromise = createCancelablePromise(token => {
return this._delegate.getNotebookKernels(this._delegate.viewModel!.viewType, this._delegate.viewModel!.uri, token);
});
const result = await this._contributedKernelsComputePromise;
this._initialKernelComputationDone = true;
this._contributedKernelsComputePromise = null;
return result;
}
updateForMetadata(): void {
if (!this._delegate.viewModel) {
return;
}
const notebookMetadata = this._delegate.viewModel.metadata;
this._editorRunnable.set(this._delegate.viewModel.runnable);
this._notebookExecuting.set(notebookMetadata.runState === NotebookRunState.Running);
}
private async _setKernelsFromProviders(provider: NotebookProviderInfo, kernels: INotebookKernel[], tokenSource: CancellationTokenSource) {
const rawAssociations = this._configurationService.getValue<NotebookKernelProviderAssociations>(notebookKernelProviderAssociationsSettingId) || [];
const userSetKernelProvider = rawAssociations.filter(e => e.viewType === this._delegate.viewModel?.viewType)[0]?.kernelProvider;
const memento = this._activeKernelMemento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE);
if (userSetKernelProvider) {
const filteredKernels = kernels.filter(kernel => kernel.extension.value === userSetKernelProvider);
if (filteredKernels.length) {
const cachedKernelId = memento[provider.id];
this.activeKernel =
filteredKernels.find(kernel => kernel.isPreferred)
|| filteredKernels.find(kernel => kernel.friendlyId === cachedKernelId)
|| filteredKernels[0];
} else {
this.activeKernel = undefined;
}
if (this.activeKernel) {
await this._delegate.loadKernelPreloads(this.activeKernel.extensionLocation, this.activeKernel);
if (tokenSource.token.isCancellationRequested) {
return;
}
this._activeKernelResolvePromise = this.activeKernel.resolve(this._delegate.viewModel!.uri, this._delegate.getId(), tokenSource.token);
await this._activeKernelResolvePromise;
if (tokenSource.token.isCancellationRequested) {
return;
}
}
memento[provider.id] = this._activeKernel?.friendlyId;
this._activeKernelMemento.saveMemento();
tokenSource.dispose();
return;
}
// choose a preferred kernel
const kernelsFromSameExtension = kernels.filter(kernel => kernel.extension.value === provider.providerExtensionId);
if (kernelsFromSameExtension.length) {
const cachedKernelId = memento[provider.id];
const preferedKernel = kernelsFromSameExtension.find(kernel => kernel.isPreferred)
|| kernelsFromSameExtension.find(kernel => kernel.friendlyId === cachedKernelId)
|| kernelsFromSameExtension[0];
this.activeKernel = preferedKernel;
if (this.activeKernel) {
await this._delegate.loadKernelPreloads(this.activeKernel.extensionLocation, this.activeKernel);
}
if (tokenSource.token.isCancellationRequested) {
return;
}
await preferedKernel.resolve(this._delegate.viewModel!.uri, this._delegate.getId(), tokenSource.token);
if (tokenSource.token.isCancellationRequested) {
return;
}
memento[provider.id] = this._activeKernel?.friendlyId;
this._activeKernelMemento.saveMemento();
tokenSource.dispose();
return;
}
// the provider doesn't have a builtin kernel, choose a kernel
this.activeKernel = kernels[0];
if (this.activeKernel) {
await this._delegate.loadKernelPreloads(this.activeKernel.extensionLocation, this.activeKernel);
if (tokenSource.token.isCancellationRequested) {
return;
}
await this.activeKernel.resolve(this._delegate.viewModel!.uri, this._delegate.getId(), tokenSource.token);
if (tokenSource.token.isCancellationRequested) {
return;
}
}
tokenSource.dispose();
}
private async _ensureActiveKernel() {
if (this._activeKernel) {
return;
}
if (this._activeKernelResolvePromise) {
await this._activeKernelResolvePromise;
if (this._activeKernel) {
return;
}
}
if (!this._initialKernelComputationDone) {
await this.setKernels(new CancellationTokenSource());
if (this._activeKernel) {
return;
}
}
// pick active kernel
const picker = this._quickInputService.createQuickPick<(IQuickPickItem & { run(): void; kernelProviderId?: string })>();
picker.placeholder = nls.localize('notebook.runCell.selectKernel', "Select a notebook kernel to run this notebook");
picker.matchOnDetail = true;
const tokenSource = new CancellationTokenSource();
const availableKernels = await this.beginComputeContributedKernels();
const picks: QuickPickInput<IQuickPickItem & { run(): void; kernelProviderId?: string; }>[] = availableKernels.map((a) => {
return {
id: a.friendlyId,
label: a.label,
picked: false,
description:
a.description
? a.description
: a.extension.value,
detail: a.detail,
kernelProviderId: a.extension.value,
run: async () => {
this.activeKernel = a;
this._activeKernelResolvePromise = this.activeKernel.resolve(this._delegate.viewModel!.uri, this._delegate.getId(), tokenSource.token);
},
buttons: [{
iconClass: ThemeIcon.asClassName(configureKernelIcon),
tooltip: nls.localize('notebook.promptKernel.setDefaultTooltip', "Set as default kernel provider for '{0}'", this._delegate.viewModel!.viewType)
}]
};
});
picker.items = picks;
picker.busy = false;
const pickedItem = await new Promise<(IQuickPickItem & { run(): void; kernelProviderId?: string; }) | undefined>(resolve => {
picker.onDidAccept(() => {
resolve(picker.selectedItems.length === 1 ? picker.selectedItems[0] : undefined);
picker.dispose();
});
picker.onDidTriggerItemButton(e => {
const pick = e.item;
const id = pick.id;
resolve(pick); // open the view
picker.dispose();
// And persist the setting
if (pick && id && pick.kernelProviderId) {
const newAssociation: NotebookKernelProviderAssociation = { viewType: this._delegate.viewModel!.viewType, kernelProvider: pick.kernelProviderId };
const currentAssociations = [...this._configurationService.getValue<NotebookKernelProviderAssociations>(notebookKernelProviderAssociationsSettingId)];
// First try updating existing association
for (let i = 0; i < currentAssociations.length; ++i) {
const existing = currentAssociations[i];
if (existing.viewType === newAssociation.viewType) {
currentAssociations.splice(i, 1, newAssociation);
this._configurationService.updateValue(notebookKernelProviderAssociationsSettingId, currentAssociations);
return;
}
}
// Otherwise, create a new one
currentAssociations.unshift(newAssociation);
this._configurationService.updateValue(notebookKernelProviderAssociationsSettingId, currentAssociations);
}
});
});
tokenSource.dispose();
if (pickedItem) {
await pickedItem.run();
}
return;
}
async cancelNotebookExecution(): Promise<void> {
if (!this._delegate.viewModel) {
return;
}
if (this._delegate.viewModel.metadata.runState !== NotebookRunState.Running) {
return;
}
await this._ensureActiveKernel();
await this._activeKernel?.cancelNotebookCell!(this._delegate.viewModel.uri, undefined);
}
async executeNotebook(): Promise<void> {
if (!this._delegate.viewModel) {
return;
}
if (!this._delegate.viewModel.runnable) {
return;
}
await this._ensureActiveKernel();
this._activeKernelExecuted = true;
await this._activeKernel?.executeNotebookCell!(this._delegate.viewModel.uri, undefined);
}
async cancelNotebookCellExecution(cell: ICellViewModel): Promise<void> {
if (!this._delegate.viewModel) {
return;
}
if (cell.cellKind !== CellKind.Code) {
return;
}
const metadata = cell.getEvaluatedMetadata(this._delegate.viewModel.metadata);
if (!metadata.runnable) {
return;
}
if (metadata.runState !== NotebookCellRunState.Running) {
return;
}
await this._ensureActiveKernel();
await this._activeKernel?.cancelNotebookCell!(this._delegate.viewModel.uri, cell.handle);
}
async executeNotebookCell(cell: ICellViewModel): Promise<void> {
if (!this._delegate.viewModel) {
return;
}
if (!cell.getEvaluatedMetadata(this._delegate.viewModel.metadata).runnable) {
return;
}
await this._ensureActiveKernel();
this._activeKernelExecuted = true;
await this._activeKernel?.executeNotebookCell!(this._delegate.viewModel.uri, cell.handle);
}
}

View file

@ -5,17 +5,18 @@
import { getPixelRatio, getZoomLevel } from 'vs/base/browser/browser';
import * as DOM from 'vs/base/browser/dom';
import * as strings from 'vs/base/common/strings';
import { IMouseWheelEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { IListContextMenuEvent } from 'vs/base/browser/ui/list/list';
import { IAction, Separator } from 'vs/base/common/actions';
import { CancelablePromise, createCancelablePromise, SequencerByKey } from 'vs/base/common/async';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { SequencerByKey } from 'vs/base/common/async';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { Color, RGBA } from 'vs/base/common/color';
import { onUnexpectedError } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { combinedDisposable, Disposable, DisposableStore, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { extname } from 'vs/base/common/resources';
import { ScrollEvent } from 'vs/base/common/scrollable';
import * as strings from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri';
import { generateUuid } from 'vs/base/common/uuid';
import 'vs/css!./media/notebook';
@ -24,6 +25,7 @@ import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
import { BareFontInfo } from 'vs/editor/common/config/fontInfo';
import { Range } from 'vs/editor/common/core/range';
import { IContentDecorationRenderOptions, IEditor, isThemeColor } from 'vs/editor/common/editorCommon';
import { IModeService } from 'vs/editor/common/services/modeService';
import * as nls from 'vs/nls';
import { IMenuService, MenuId } from 'vs/platform/actions/common/actions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
@ -32,45 +34,40 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { contrastBorder, diffInserted, diffRemoved, editorBackground, errorForeground, focusBorder, foreground, listInactiveSelectionBackground, registerColor, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground, textPreformatForeground, transparent } from 'vs/platform/theme/common/colorRegistry';
import { IColorTheme, IThemeService, registerThemingParticipant, ThemeColor, ThemeIcon } from 'vs/platform/theme/common/themeService';
import { EditorMemento } from 'vs/workbench/browser/parts/editor/editorPane';
import { IEditorMemento } from 'vs/workbench/common/editor';
import { Memento, MementoObject } from 'vs/workbench/common/memento';
import { PANEL_BORDER } from 'vs/workbench/common/theme';
import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/debugColors';
import { BOTTOM_CELL_TOOLBAR_GAP, BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_BOTTOM_MARGIN, CELL_MARGIN, CELL_OUTPUT_PADDING, CELL_RUN_GUTTER, CELL_TOP_MARGIN, CODE_CELL_LEFT_MARGIN, COLLAPSED_INDICATOR_HEIGHT, MARKDOWN_CELL_BOTTOM_MARGIN, MARKDOWN_CELL_TOP_MARGIN, SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants';
import { CellEditState, CellFocusMode, IActiveNotebookEditor, ICellOutputViewModel, ICellViewModel, ICommonCellInfo, IDisplayOutputLayoutUpdateRequest, IFocusNotebookCellOptions, IGenericCellViewModel, IInsetRenderOutput, INotebookCellList, INotebookCellOutputLayoutInfo, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorMouseEvent, NotebookEditorOptions, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_HAS_MULTIPLE_KERNELS, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { CellEditState, CellFocusMode, IActiveNotebookEditor, ICellOutputViewModel, ICellViewModel, ICommonCellInfo, IDisplayOutputLayoutUpdateRequest, IFocusNotebookCellOptions, IGenericCellViewModel, IInsetRenderOutput, INotebookCellList, INotebookCellOutputLayoutInfo, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorMouseEvent, NotebookEditorOptions, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions';
import { NotebookKernelProviderAssociation, NotebookKernelProviderAssociations, notebookKernelProviderAssociationsSettingId } from 'vs/workbench/contrib/notebook/browser/notebookKernelAssociation';
import { IKernelManagerDelegate, NotebookEditorKernelManager } from 'vs/workbench/contrib/notebook/browser/notebookEditorKernelManager';
import { errorStateIcon, successStateIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons';
import { NotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList';
import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer';
import { BackLayerWebView } from 'vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView';
import { CellContextKeyManager } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys';
import { CodeCellRenderer, ListTopCellToolbar, MarkdownCellRenderer, NotebookCellListDelegate } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer';
import { CellDragAndDropController } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellDnd';
import { CodeCellRenderer, ListTopCellToolbar, MarkdownCellRenderer, NotebookCellListDelegate } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer';
import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel';
import { NotebookEventDispatcher, NotebookLayoutChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher';
import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel';
import { CellViewModel, IModelDecorationsChangeAccessor, INotebookEditorViewState, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel';
import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel';
import { CellKind, CellToolbarLocKey, ICellRange, INotebookDecorationRenderOptions, INotebookKernel, NotebookCellRunState, NotebookRunState, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { CellKind, CellToolbarLocKey, ICellRange, INotebookDecorationRenderOptions, INotebookKernel, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider';
import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService';
import { editorGutterModifiedBackground } from 'vs/workbench/contrib/scm/browser/dirtydiffDecorator';
import { Webview } from 'vs/workbench/contrib/webview/browser/webview';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { configureKernelIcon, errorStateIcon, successStateIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons';
import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/debugColors';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { extname } from 'vs/base/common/resources';
import { IModeService } from 'vs/editor/common/services/modeService';
import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel';
const $ = DOM.$;
const NotebookEditorActiveKernelCache = 'workbench.editor.notebook.activeKernel';
export class NotebookEditorWidget extends Disposable implements INotebookEditor {
static readonly ID: string = 'workbench.editor.notebook';
private static readonly EDITOR_MEMENTOS = new Map<string, EditorMemento<unknown>>();
@ -95,16 +92,11 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
private readonly _editorFocus: IContextKey<boolean>;
private readonly _outputFocus: IContextKey<boolean>;
private readonly _editorEditable: IContextKey<boolean>;
private readonly _editorRunnable: IContextKey<boolean>;
private readonly _notebookExecuting: IContextKey<boolean>;
private readonly _notebookHasMultipleKernels: IContextKey<boolean>;
private readonly _notebookKernelCount: IContextKey<number>;
private _outputRenderer: OutputRenderer;
protected readonly _contributions = new Map<string, INotebookEditorContribution>();
private _scrollBeyondLastLine: boolean;
private readonly _memento: Memento;
private readonly _activeKernelMemento: Memento;
private readonly _onDidFocusEmitter = this._register(new Emitter<void>());
public readonly onDidFocus = this._onDidFocusEmitter.event;
private readonly _onWillScroll = this._register(new Emitter<ScrollEvent>());
@ -118,6 +110,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
this._list.scrollTop = top;
}
private _kernelManger: NotebookEditorKernelManager;
private _cellContextKeyManager: CellContextKeyManager | null = null;
private _isVisible = false;
private readonly _uuid = generateUuid();
@ -152,57 +145,29 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
return this._notebookViewModel?.notebookDocument;
}
private _activeKernelExecuted: boolean = false;
private _activeKernel: INotebookKernel | undefined = undefined;
private readonly _onDidChangeKernel = this._register(new Emitter<void>());
readonly onDidChangeKernel: Event<void> = this._onDidChangeKernel.event;
private readonly _onDidChangeAvailableKernels = this._register(new Emitter<void>());
readonly onDidChangeAvailableKernels: Event<void> = this._onDidChangeAvailableKernels.event;
private _contributedKernelsComputePromise: CancelablePromise<INotebookKernel[]> | null = null;
private _initialKernelComputationDone: boolean = false;
get onDidChangeKernel(): Event<void> {
return this._kernelManger.onDidChangeKernel;
}
get onDidChangeAvailableKernels(): Event<void> {
return this._kernelManger.onDidChangeAvailableKernels;
}
get activeKernel() {
return this._activeKernel;
return this._kernelManger.activeKernel;
}
set activeKernel(kernel: INotebookKernel | undefined) {
if (this._isDisposed) {
return;
}
if (!this.viewModel) {
return;
}
if (this._activeKernel === kernel) {
return;
}
this._activeKernel = kernel;
this._activeKernelResolvePromise = undefined;
const memento = this._activeKernelMemento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE);
memento[this.viewModel.viewType] = this._activeKernel?.friendlyId;
this._activeKernelMemento.saveMemento();
this._onDidChangeKernel.fire();
if (this._activeKernel) {
this._loadKernelPreloads(this._activeKernel.extensionLocation, this._activeKernel);
}
this._kernelManger.activeKernel = kernel;
}
private _activeKernelResolvePromise: Promise<void> | undefined = undefined;
private _currentKernelTokenSource: CancellationTokenSource | undefined = undefined;
private _multipleKernelsAvailable: boolean = false;
get multipleKernelsAvailable() {
return this._multipleKernelsAvailable;
return this._kernelManger.multipleKernelsAvailable;
}
set multipleKernelsAvailable(state: boolean) {
this._multipleKernelsAvailable = state;
this._onDidChangeAvailableKernels.fire();
this._kernelManger.multipleKernelsAvailable = state;
}
private readonly _onDidChangeActiveEditor = this._register(new Emitter<this>());
@ -258,7 +223,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
@ILayoutService private readonly layoutService: ILayoutService,
@IContextMenuService private readonly contextMenuService: IContextMenuService,
@IMenuService private readonly menuService: IMenuService,
@IQuickInputService private readonly quickInputService: IQuickInputService,
@IThemeService private readonly themeService: IThemeService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IModeService private readonly modeService: IModeService,
@ -270,8 +234,23 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
this.scopedContextKeyService = contextKeyService.createScoped(this._overlayContainer);
this.instantiationService = instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService]));
const that = this;
this._kernelManger = instantiationService.createInstance(NotebookEditorKernelManager, <IKernelManagerDelegate>{
getId() { return that.getId(); },
loadKernelPreloads: that._loadKernelPreloads.bind(that),
get viewModel() { return that.viewModel; },
getContributedNotebookProviders(resource?: URI) {
return that.notebookService.getContributedNotebookProviders(resource);
},
getContributedNotebookProvider(viewType: string): NotebookProviderInfo | undefined {
return that.notebookService.getContributedNotebookProvider(viewType);
},
getNotebookKernels(viewType: string, resource: URI, token: CancellationToken): Promise<INotebookKernel[]> {
return that.notebookService.getNotebookKernels(viewType, resource, token);
}
});
this._memento = new Memento(NotebookEditorWidget.ID, storageService);
this._activeKernelMemento = new Memento(NotebookEditorActiveKernelCache, storageService);
this._outputRenderer = new OutputRenderer(this, this.instantiationService);
this._scrollBeyondLastLine = this.configurationService.getValue<boolean>('editor.scrollBeyondLastLine');
@ -304,10 +283,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
this._editorFocus = NOTEBOOK_EDITOR_FOCUSED.bindTo(this.scopedContextKeyService);
this._outputFocus = NOTEBOOK_OUTPUT_FOCUSED.bindTo(this.scopedContextKeyService);
this._editorEditable = NOTEBOOK_EDITOR_EDITABLE.bindTo(this.scopedContextKeyService);
this._editorRunnable = NOTEBOOK_EDITOR_RUNNABLE.bindTo(this.scopedContextKeyService);
this._notebookExecuting = NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK.bindTo(this.scopedContextKeyService);
this._notebookHasMultipleKernels = NOTEBOOK_HAS_MULTIPLE_KERNELS.bindTo(this.scopedContextKeyService);
this._notebookKernelCount = NOTEBOOK_KERNEL_COUNT.bindTo(this.scopedContextKeyService);
let contributions: INotebookEditorContributionDescription[];
if (Array.isArray(this.creationOptions.contributions)) {
@ -777,24 +752,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
this._webview?.element.remove();
this._webview = null;
this._list.clear();
this._activeKernel = undefined;
this._activeKernelExecuted = false;
}
async beginComputeContributedKernels() {
if (this._contributedKernelsComputePromise) {
return this._contributedKernelsComputePromise;
}
this._contributedKernelsComputePromise = createCancelablePromise(token => {
return this.notebookService.getNotebookKernels(this.viewModel!.viewType, this.viewModel!.uri, token);
});
const result = await this._contributedKernelsComputePromise;
this._initialKernelComputationDone = true;
this._contributedKernelsComputePromise = null;
return result;
return this._kernelManger.beginComputeContributedKernels();
}
private async _setKernels(tokenSource: CancellationTokenSource) {
@ -802,127 +763,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
return;
}
if (this._activeKernel !== undefined && this._activeKernelExecuted) {
// kernel already executed, we should not change it automatically
return;
}
const provider = this.notebookService.getContributedNotebookProvider(this.viewModel.viewType) || this.notebookService.getContributedNotebookProviders(this.viewModel.uri)[0];
const availableKernels = await this.beginComputeContributedKernels();
if (tokenSource.token.isCancellationRequested) {
return;
}
this._notebookKernelCount.set(availableKernels.length);
if (availableKernels.length > 1) {
this._notebookHasMultipleKernels.set(true);
this.multipleKernelsAvailable = true;
} else {
this._notebookHasMultipleKernels.set(false);
this.multipleKernelsAvailable = false;
}
const activeKernelStillExist = [...availableKernels].find(kernel => kernel.friendlyId === this.activeKernel?.friendlyId && this.activeKernel?.friendlyId !== undefined);
if (activeKernelStillExist) {
// the kernel still exist, we don't want to modify the selection otherwise user's temporary preference is lost
return;
}
if (availableKernels.length) {
return this._setKernelsFromProviders(provider, availableKernels, tokenSource);
}
this._initialKernelComputationDone = true;
tokenSource.dispose();
}
private async _setKernelsFromProviders(provider: NotebookProviderInfo, kernels: INotebookKernel[], tokenSource: CancellationTokenSource) {
const rawAssociations = this.configurationService.getValue<NotebookKernelProviderAssociations>(notebookKernelProviderAssociationsSettingId) || [];
const userSetKernelProvider = rawAssociations.filter(e => e.viewType === this.viewModel?.viewType)[0]?.kernelProvider;
const memento = this._activeKernelMemento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE);
if (userSetKernelProvider) {
const filteredKernels = kernels.filter(kernel => kernel.extension.value === userSetKernelProvider);
if (filteredKernels.length) {
const cachedKernelId = memento[provider.id];
this.activeKernel =
filteredKernels.find(kernel => kernel.isPreferred)
|| filteredKernels.find(kernel => kernel.friendlyId === cachedKernelId)
|| filteredKernels[0];
} else {
this.activeKernel = undefined;
}
if (this.activeKernel) {
await this._loadKernelPreloads(this.activeKernel.extensionLocation, this.activeKernel);
if (tokenSource.token.isCancellationRequested) {
return;
}
this._activeKernelResolvePromise = this.activeKernel.resolve(this.viewModel!.uri, this.getId(), tokenSource.token);
await this._activeKernelResolvePromise;
if (tokenSource.token.isCancellationRequested) {
return;
}
}
memento[provider.id] = this._activeKernel?.friendlyId;
this._activeKernelMemento.saveMemento();
tokenSource.dispose();
return;
}
// choose a preferred kernel
const kernelsFromSameExtension = kernels.filter(kernel => kernel.extension.value === provider.providerExtensionId);
if (kernelsFromSameExtension.length) {
const cachedKernelId = memento[provider.id];
const preferedKernel = kernelsFromSameExtension.find(kernel => kernel.isPreferred)
|| kernelsFromSameExtension.find(kernel => kernel.friendlyId === cachedKernelId)
|| kernelsFromSameExtension[0];
this.activeKernel = preferedKernel;
if (this.activeKernel) {
await this._loadKernelPreloads(this.activeKernel.extensionLocation, this.activeKernel);
}
if (tokenSource.token.isCancellationRequested) {
return;
}
await preferedKernel.resolve(this.viewModel!.uri, this.getId(), tokenSource.token);
if (tokenSource.token.isCancellationRequested) {
return;
}
memento[provider.id] = this._activeKernel?.friendlyId;
this._activeKernelMemento.saveMemento();
tokenSource.dispose();
return;
}
// the provider doesn't have a builtin kernel, choose a kernel
this.activeKernel = kernels[0];
if (this.activeKernel) {
await this._loadKernelPreloads(this.activeKernel.extensionLocation, this.activeKernel);
if (tokenSource.token.isCancellationRequested) {
return;
}
await this.activeKernel.resolve(this.viewModel!.uri, this.getId(), tokenSource.token);
if (tokenSource.token.isCancellationRequested) {
return;
}
}
tokenSource.dispose();
this._kernelManger.setKernels(tokenSource);
}
private async _loadKernelPreloads(extensionLocation: URI, kernel: INotebookKernel) {
@ -939,11 +780,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
const notebookMetadata = this.viewModel.metadata;
this._editorEditable.set(!!notebookMetadata?.editable);
this._editorRunnable.set(this.viewModel.runnable);
this._overflowContainer.classList.toggle('notebook-editor-editable', !!notebookMetadata?.editable);
this.getDomNode().classList.toggle('notebook-editor-editable', !!notebookMetadata?.editable);
this._notebookExecuting.set(notebookMetadata.runState === NotebookRunState.Running);
this._kernelManger.updateForMetadata();
}
private async _resolveWebview(): Promise<BackLayerWebView<ICommonCellInfo> | null> {
@ -1498,7 +1338,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
const nextIndex = ui ? this.viewModel.getNextVisibleCellIndex(index) : index + 1;
let language;
if (type === CellKind.Code) {
const supportedLanguages = this._activeKernel?.supportedLanguages ?? this.modeService.getRegisteredModes();
const supportedLanguages = this._kernelManger.activeKernel?.supportedLanguages ?? this.modeService.getRegisteredModes();
const defaultLanguage = supportedLanguages[0] || 'plaintext';
if (cell?.cellKind === CellKind.Code) {
language = cell.language;
@ -1726,152 +1566,16 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
return undefined;
}
private async _ensureActiveKernel() {
if (this._activeKernel) {
return;
}
if (this._activeKernelResolvePromise) {
await this._activeKernelResolvePromise;
if (this._activeKernel) {
return;
}
}
if (!this._initialKernelComputationDone) {
await this._setKernels(new CancellationTokenSource());
if (this._activeKernel) {
return;
}
}
// pick active kernel
const picker = this.quickInputService.createQuickPick<(IQuickPickItem & { run(): void; kernelProviderId?: string })>();
picker.placeholder = nls.localize('notebook.runCell.selectKernel', "Select a notebook kernel to run this notebook");
picker.matchOnDetail = true;
const tokenSource = new CancellationTokenSource();
const availableKernels = await this.beginComputeContributedKernels();
const picks: QuickPickInput<IQuickPickItem & { run(): void; kernelProviderId?: string; }>[] = availableKernels.map((a) => {
return {
id: a.friendlyId,
label: a.label,
picked: false,
description:
a.description
? a.description
: a.extension.value,
detail: a.detail,
kernelProviderId: a.extension.value,
run: async () => {
this.activeKernel = a;
this._activeKernelResolvePromise = this.activeKernel.resolve(this.viewModel!.uri, this.getId(), tokenSource.token);
},
buttons: [{
iconClass: ThemeIcon.asClassName(configureKernelIcon),
tooltip: nls.localize('notebook.promptKernel.setDefaultTooltip', "Set as default kernel provider for '{0}'", this.viewModel!.viewType)
}]
};
});
picker.items = picks;
picker.busy = false;
const pickedItem = await new Promise<(IQuickPickItem & { run(): void; kernelProviderId?: string; }) | undefined>(resolve => {
picker.onDidAccept(() => {
resolve(picker.selectedItems.length === 1 ? picker.selectedItems[0] : undefined);
picker.dispose();
});
picker.onDidTriggerItemButton(e => {
const pick = e.item;
const id = pick.id;
resolve(pick); // open the view
picker.dispose();
// And persist the setting
if (pick && id && pick.kernelProviderId) {
const newAssociation: NotebookKernelProviderAssociation = { viewType: this.viewModel!.viewType, kernelProvider: pick.kernelProviderId };
const currentAssociations = [...this.configurationService.getValue<NotebookKernelProviderAssociations>(notebookKernelProviderAssociationsSettingId)];
// First try updating existing association
for (let i = 0; i < currentAssociations.length; ++i) {
const existing = currentAssociations[i];
if (existing.viewType === newAssociation.viewType) {
currentAssociations.splice(i, 1, newAssociation);
this.configurationService.updateValue(notebookKernelProviderAssociationsSettingId, currentAssociations);
return;
}
}
// Otherwise, create a new one
currentAssociations.unshift(newAssociation);
this.configurationService.updateValue(notebookKernelProviderAssociationsSettingId, currentAssociations);
}
});
});
tokenSource.dispose();
if (pickedItem) {
await pickedItem.run();
}
return;
}
async cancelNotebookExecution(): Promise<void> {
if (!this.viewModel) {
return;
}
if (this.viewModel.metadata.runState !== NotebookRunState.Running) {
return;
}
await this._ensureActiveKernel();
await this._activeKernel?.cancelNotebookCell!(this.viewModel.uri, undefined);
return this._kernelManger.cancelNotebookExecution();
}
async executeNotebook(): Promise<void> {
if (!this.viewModel) {
return;
}
if (!this.viewModel.runnable) {
return;
}
await this._ensureActiveKernel();
this._activeKernelExecuted = true;
await this._activeKernel?.executeNotebookCell!(this.viewModel.uri, undefined);
return this._kernelManger.executeNotebook();
}
async cancelNotebookCellExecution(cell: ICellViewModel): Promise<void> {
if (!this.viewModel) {
return;
}
if (cell.cellKind !== CellKind.Code) {
return;
}
const metadata = cell.getEvaluatedMetadata(this.viewModel.metadata);
if (!metadata.runnable) {
return;
}
if (metadata.runState !== NotebookCellRunState.Running) {
return;
}
await this._ensureActiveKernel();
await this._activeKernel?.cancelNotebookCell!(this.viewModel.uri, cell.handle);
return this._kernelManger.cancelNotebookCellExecution(cell);
}
async executeNotebookCell(cell: ICellViewModel): Promise<void> {
@ -1879,18 +1583,13 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
return;
}
// TODO@roblourens, don't use the "execute" command for this
if (cell.cellKind === CellKind.Markdown) {
this.focusNotebookCell(cell, 'container');
return;
}
if (!cell.getEvaluatedMetadata(this.viewModel.metadata).runnable) {
return;
}
await this._ensureActiveKernel();
this._activeKernelExecuted = true;
await this._activeKernel?.executeNotebookCell!(this.viewModel.uri, cell.handle);
return this._kernelManger.executeNotebookCell(cell);
}
focusNotebookCell(cell: ICellViewModel, focusItem: 'editor' | 'container' | 'output', options?: IFocusNotebookCellOptions) {