diff --git a/src/vs/workbench/api/browser/mainThreadNotebookDto.ts b/src/vs/workbench/api/browser/mainThreadNotebookDto.ts index d63c6412198..893100761b0 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookDto.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookDto.ts @@ -6,7 +6,8 @@ import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import * as notebookCommon from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { CellExecutionUpdateType, ICellExecuteUpdate, ICellExecutionComplete } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; +import { CellExecutionUpdateType } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; +import { ICellExecuteUpdate, ICellExecutionComplete } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; export namespace NotebookDto { diff --git a/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts b/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts index 71aa6d1b0a4..c53fec123a0 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts @@ -15,7 +15,7 @@ import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/notebookEditorService'; import { CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { INotebookExecutionService } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; +import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { INotebookKernel, INotebookKernelChangeEvent, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import { ExtHostContext, ExtHostNotebookKernelsShape, ICellExecuteUpdateDto, ICellExecutionCompleteDto, IExtHostContext, INotebookKernelDto2, MainContext, MainThreadNotebookKernelsShape } from '../common/extHost.protocol'; @@ -113,8 +113,7 @@ export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape extHostContext: IExtHostContext, @ILanguageService private readonly _languageService: ILanguageService, @INotebookKernelService private readonly _notebookKernelService: INotebookKernelService, - @INotebookExecutionService private readonly _notebookExecutionService: INotebookExecutionService, - // @INotebookService private readonly _notebookService: INotebookService, + @INotebookExecutionStateService private readonly _notebookExecutionStateService: INotebookExecutionStateService, @INotebookEditorService notebookEditorService: INotebookEditorService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostNotebookKernels); @@ -128,7 +127,7 @@ export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape this._executions.forEach(e => { const uri = CellUri.parse(URI.parse(e)); if (uri) { - this._notebookExecutionService.completeNotebookCellExecution(uri.notebook, uri.handle, { }); + this._notebookExecutionStateService.completeNotebookCellExecution(uri.notebook, uri.handle, { }); } }); })); @@ -248,7 +247,7 @@ export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape $addExecution(rawUri: UriComponents, cellHandle: number): void { const uri = URI.revive(rawUri); - this._notebookExecutionService.createNotebookCellExecution(uri, cellHandle); + this._notebookExecutionStateService.createNotebookCellExecution(uri, cellHandle); const cellUri = CellUri.generateCellUri(uri, cellHandle, uri.scheme); this._executions.add(cellUri.toString()); @@ -262,7 +261,7 @@ export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape try { const uri = URI.revive(first.uri); - this._notebookExecutionService.updateNotebookCellExecution(uri, first.cellHandle, datas.map(NotebookDto.fromCellExecuteUpdateDto)); + this._notebookExecutionStateService.updateNotebookCellExecution(uri, first.cellHandle, datas.map(NotebookDto.fromCellExecuteUpdateDto)); } catch (e) { onUnexpectedError(e); } @@ -271,7 +270,7 @@ export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape $completeExecution(rawUri: UriComponents, cellHandle: number, data: SerializableObjectWithBuffers): void { const uri = URI.revive(rawUri); - this._notebookExecutionService.completeNotebookCellExecution(uri, cellHandle, NotebookDto.fromCellExecuteCompleteDto(data.value)); + this._notebookExecutionStateService.completeNotebookCellExecution(uri, cellHandle, NotebookDto.fromCellExecuteCompleteDto(data.value)); const cellUri = CellUri.generateCellUri(uri, cellHandle, uri.scheme); this._executions.delete(cellUri.toString()); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 4742f3bdf5d..a06e93b8668 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -54,7 +54,8 @@ import { IRevealOptions, ITreeItem } from 'vs/workbench/common/views'; import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { IAdapterDescriptor, IConfig, IDebugSessionReplMode } from 'vs/workbench/contrib/debug/common/debug'; import * as notebookCommon from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { CellExecutionUpdateType, ICellExecutionComplete, ICellExecutionStateUpdate } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; +import { CellExecutionUpdateType } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; +import { ICellExecutionComplete, ICellExecutionStateUpdate } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { OutputChannelUpdateMode } from 'vs/workbench/contrib/output/common/output'; import { InputValidationType } from 'vs/workbench/contrib/scm/common/scm'; diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 7449d1148ec..369f05b2a9d 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -95,12 +95,14 @@ import 'vs/workbench/contrib/notebook/browser/diff/notebookDiffActions'; // Output renderers registration import 'vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform'; import { editorOptionsRegistry } from 'vs/editor/common/config/editorOptions'; +import { NotebookExecutionStateService } from 'vs/workbench/contrib/notebook/browser/notebookExecutionStateServiceImpl'; import { NotebookExecutionService } from 'vs/workbench/contrib/notebook/browser/notebookExecutionServiceImpl'; import { INotebookExecutionService } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; import { INotebookKeymapService } from 'vs/workbench/contrib/notebook/common/notebookKeymapService'; import { NotebookKeymapService } from 'vs/workbench/contrib/notebook/browser/notebookKeymapServiceImpl'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/modes/modesRegistry'; +import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; /*--------------------------------------------------------------------------------------------- */ @@ -627,6 +629,7 @@ registerSingleton(INotebookCellStatusBarService, NotebookCellStatusBarService, t registerSingleton(INotebookEditorService, NotebookEditorWidgetService, true); registerSingleton(INotebookKernelService, NotebookKernelService, true); registerSingleton(INotebookExecutionService, NotebookExecutionService, true); +registerSingleton(INotebookExecutionStateService, NotebookExecutionStateService, true); registerSingleton(INotebookRendererMessagingService, NotebookRendererMessagingService, true); registerSingleton(INotebookKeymapService, NotebookKeymapService, true); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookExecutionServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookExecutionServiceImpl.ts index 445612437f7..ab7580d55b8 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookExecutionServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookExecutionServiceImpl.ts @@ -3,55 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; -import { ResourceMap } from 'vs/base/common/map'; -import { URI } from 'vs/base/common/uri'; import * as nls from 'vs/nls'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; import { SELECT_KERNEL_ID } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { CellEditType, CellKind, ICellEditOperation, INotebookTextModel, NotebookCellExecutionState, NotebookCellInternalMetadata, NotebookTextModelWillAddRemoveEvent } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { CellExecutionUpdateType, ICellExecuteUpdate, ICellExecutionComplete, INotebookExecutionService } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; +import { CellKind, INotebookTextModel, NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookExecutionService } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; import { INotebookKernel, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; -import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; -export class NotebookExecutionService extends Disposable implements INotebookExecutionService { +export class NotebookExecutionService implements INotebookExecutionService { declare _serviceBrand: undefined; - private readonly _executions = new ResourceMap(); - constructor( @ICommandService private readonly _commandService: ICommandService, @INotebookKernelService private readonly _notebookKernelService: INotebookKernelService, @IWorkspaceTrustRequestService private readonly _workspaceTrustRequestService: IWorkspaceTrustRequestService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, @ILogService private readonly _logService: ILogService, ) { - super(); - - this._register(this._notebookKernelService.onDidChangeSelectedNotebooks(e => { - if (e.newKernel) { - const notebookExecution = this._executions.get(e.notebook); - if (notebookExecution) { - notebookExecution.cancelAll(); - this.checkNotebookExecutionEmpty(e.notebook); - } - } - })); - } - - createNotebookCellExecution(notebook: URI, cellHandle: number): void { - let notebookExecution = this._executions.get(notebook); - if (!notebookExecution) { - notebookExecution = this._instantiationService.createInstance(NotebookExecution, notebook); - this._executions.set(notebook, notebookExecution); - } - - notebookExecution.addExecution(cellHandle); } getSelectedOrSuggestedKernel(notebook: INotebookTextModel): INotebookKernel | undefined { @@ -109,231 +79,4 @@ export class NotebookExecutionService extends Disposable implements INotebookExe async cancelNotebookCells(notebook: INotebookTextModel, cells: Iterable): Promise { this.cancelNotebookCellHandles(notebook, Array.from(cells, cell => cell.handle)); } - - updateNotebookCellExecution(notebook: URI, cellHandle: number, updates: ICellExecuteUpdate[]): void { - const notebookExecution = this._executions.get(notebook); - if (!notebookExecution) { - this._logService.error(`notebook execution not found for ${notebook}`); - return; - } - - notebookExecution.updateExecution(cellHandle, updates); - } - - completeNotebookCellExecution(notebook: URI, cellHandle: number, complete: ICellExecutionComplete): void { - const notebookExecution = this._executions.get(notebook); - if (!notebookExecution) { - this._logService.error(`notebook execution not found for ${notebook}`); - return; - } - - notebookExecution.completeExecution(cellHandle, complete); - this.checkNotebookExecutionEmpty(notebook); - } - - private checkNotebookExecutionEmpty(notebook: URI): void { - const notebookExecution = this._executions.get(notebook); - if (!notebookExecution) { - return; - } - - if (notebookExecution.isEmpty()) { - this._logService.debug(`NotebookExecution#dispose ${notebook.toString()}`); - notebookExecution.dispose(); - this._executions.delete(notebook); - } - } - - override dispose(): void { - super.dispose(); - this._executions.forEach(e => e.dispose()); - this._executions.clear(); - } -} - -class NotebookExecution extends Disposable { - private readonly _notebookModel: NotebookTextModel; - - private readonly _cellExecutions = new Map(); - - constructor( - notebook: URI, - @INotebookService private readonly _notebookService: INotebookService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, - @INotebookExecutionService private readonly _notebookExecutionService: INotebookExecutionService, - @ILogService private readonly _logService: ILogService, - ) { - super(); - this._logService.debug(`NotebookExecution#ctor ${notebook.toString()}`); - - const notebookModel = this._notebookService.getNotebookTextModel(notebook); - if (!notebookModel) { - throw new Error('Notebook not found: ' + notebook); - } - - this._notebookModel = notebookModel; - this._register(this._notebookModel.onWillAddRemoveCells(e => this.onWillAddRemoveCells(e))); - this._register(this._notebookModel.onWillDispose(() => this.onWillDisposeDocument())); - } - - private cellLog(cellHandle: number): string { - return `${this._notebookModel.uri.toString()}, ${cellHandle}`; - } - - isEmpty(): boolean { - return this._cellExecutions.size === 0; - } - - cancelAll(): void { - this._logService.debug(`NotebookExecution#cancelAll`); - this._notebookExecutionService.cancelNotebookCellHandles(this._notebookModel, this._cellExecutions.keys()); - } - - addExecution(cellHandle: number) { - this._logService.debug(`NotebookExecution#addExecution ${this.cellLog(cellHandle)}`); - const execution = this._instantiationService.createInstance(CellExecution, cellHandle, this._notebookModel); - this._cellExecutions.set(cellHandle, execution); - } - - updateExecution(cellHandle: number, updates: ICellExecuteUpdate[]): void { - this.logUpdates(cellHandle, updates); - const execution = this._cellExecutions.get(cellHandle); - if (!execution) { - this._logService.error(`no execution for cell ${cellHandle}`); - return; - } - - execution.update(updates); - } - - private logUpdates(cellHandle: number, updates: ICellExecuteUpdate[]): void { - const updateTypes = updates.map(u => CellExecutionUpdateType[u.editType]).join(', '); - this._logService.debug(`NotebookExecution#updateExecution ${this.cellLog(cellHandle)}, [${updateTypes}]`); - } - - completeExecution(cellHandle: number, complete: ICellExecutionComplete): void { - this._logService.debug(`NotebookExecution#completeExecution ${this.cellLog(cellHandle)}`); - - const execution = this._cellExecutions.get(cellHandle); - if (!execution) { - this._logService.error(`no execution for cell ${cellHandle}`); - return; - } - - try { - execution.complete(complete); - } finally { - this._cellExecutions.delete(cellHandle); - } - } - - private onWillDisposeDocument(): void { - this._logService.debug(`NotebookExecution#onWillDisposeDocument`); - this.cancelAll(); - } - - private onWillAddRemoveCells(e: NotebookTextModelWillAddRemoveEvent): void { - const handles = new Set(this._cellExecutions.keys()); - const myDeletedHandles = new Set(); - e.rawEvent.changes.forEach(([start, deleteCount]) => { - if (deleteCount) { - const deletedHandles = this._notebookModel.cells.slice(start, start + deleteCount).map(c => c.handle); - deletedHandles.forEach(h => { - if (handles.has(h)) { - myDeletedHandles.add(h); - } - }); - } - - return false; - }); - - if (myDeletedHandles.size) { - this._logService.debug(`NotebookExecution#onWillAddRemoveCells, ${JSON.stringify([...myDeletedHandles])}`); - this._notebookExecutionService.cancelNotebookCellHandles(this._notebookModel, myDeletedHandles); - } - } -} - -function updateToEdit(update: ICellExecuteUpdate, cellHandle: number): ICellEditOperation { - if (update.editType === CellExecutionUpdateType.Output) { - return { - editType: CellEditType.Output, - handle: update.cellHandle, - append: update.append, - outputs: update.outputs, - }; - } else if (update.editType === CellExecutionUpdateType.OutputItems) { - return { - editType: CellEditType.OutputItems, - items: update.items, - append: update.append, - outputId: update.outputId - }; - } else if (update.editType === CellExecutionUpdateType.ExecutionState) { - const newInternalMetadata: Partial = { - runState: NotebookCellExecutionState.Executing, - }; - if (typeof update.executionOrder !== 'undefined') { - newInternalMetadata.executionOrder = update.executionOrder; - } - if (typeof update.runStartTime !== 'undefined') { - newInternalMetadata.runStartTime = update.runStartTime; - } - return { - editType: CellEditType.PartialInternalMetadata, - handle: cellHandle, - internalMetadata: newInternalMetadata - }; - } - - throw new Error('Unknown cell update type'); -} - -class CellExecution { - constructor( - private readonly _cellHandle: number, - private readonly _notebookModel: NotebookTextModel, - ) { - const startExecuteEdit: ICellEditOperation = { - editType: CellEditType.PartialInternalMetadata, - handle: this._cellHandle, - internalMetadata: { - runState: NotebookCellExecutionState.Pending, - executionOrder: null, - didPause: false - } - }; - this._applyExecutionEdits([startExecuteEdit]); - } - - update(updates: ICellExecuteUpdate[]): void { - const edits = updates.map(update => updateToEdit(update, this._cellHandle)); - this._applyExecutionEdits(edits); - } - - complete(completionData: ICellExecutionComplete): void { - const cellModel = this._notebookModel.cells.find(c => c.handle === this._cellHandle); - if (!cellModel) { - throw new Error('Cell not found: ' + this._cellHandle); - } - - const edit: ICellEditOperation = { - editType: CellEditType.PartialInternalMetadata, - handle: this._cellHandle, - internalMetadata: { - runState: null, - lastRunSuccess: completionData.lastRunSuccess, - runStartTime: cellModel.internalMetadata.didPause ? null : cellModel.internalMetadata.runStartTime, - runEndTime: cellModel.internalMetadata.didPause ? null : completionData.runEndTime, - isPaused: false, - didPause: false - } - }; - this._applyExecutionEdits([edit]); - } - - private _applyExecutionEdits(edits: ICellEditOperation[]): void { - this._notebookModel.applyEdits(edits, true, undefined, () => undefined, undefined, false); - } } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookExecutionStateServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookExecutionStateServiceImpl.ts new file mode 100644 index 00000000000..327e42e8307 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookExecutionStateServiceImpl.ts @@ -0,0 +1,277 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { ResourceMap } from 'vs/base/common/map'; +import { URI } from 'vs/base/common/uri'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { CellEditType, ICellEditOperation, NotebookCellExecutionState, NotebookCellInternalMetadata, NotebookTextModelWillAddRemoveEvent } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellExecutionUpdateType, INotebookExecutionService } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; +import { ICellExecuteUpdate, ICellExecutionComplete, INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; +import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; + +export class NotebookExecutionStateService extends Disposable implements INotebookExecutionStateService { + declare _serviceBrand: undefined; + + private readonly _executions = new ResourceMap(); + + constructor( + @INotebookKernelService private readonly _notebookKernelService: INotebookKernelService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @ILogService private readonly _logService: ILogService, + ) { + super(); + + this._register(this._notebookKernelService.onDidChangeSelectedNotebooks(e => { + if (e.newKernel) { + const notebookExecution = this._executions.get(e.notebook); + if (notebookExecution) { + notebookExecution.cancelAll(); + this.checkNotebookExecutionEmpty(e.notebook); + } + } + })); + } + + createNotebookCellExecution(notebook: URI, cellHandle: number): void { + let notebookExecution = this._executions.get(notebook); + if (!notebookExecution) { + notebookExecution = this._instantiationService.createInstance(NotebookExecution, notebook); + this._executions.set(notebook, notebookExecution); + } + + notebookExecution.addExecution(cellHandle); + } + + updateNotebookCellExecution(notebook: URI, cellHandle: number, updates: ICellExecuteUpdate[]): void { + const notebookExecution = this._executions.get(notebook); + if (!notebookExecution) { + this._logService.error(`notebook execution not found for ${notebook}`); + return; + } + + notebookExecution.updateExecution(cellHandle, updates); + } + + completeNotebookCellExecution(notebook: URI, cellHandle: number, complete: ICellExecutionComplete): void { + const notebookExecution = this._executions.get(notebook); + if (!notebookExecution) { + this._logService.error(`notebook execution not found for ${notebook}`); + return; + } + + notebookExecution.completeExecution(cellHandle, complete); + this.checkNotebookExecutionEmpty(notebook); + } + + private checkNotebookExecutionEmpty(notebook: URI): void { + const notebookExecution = this._executions.get(notebook); + if (!notebookExecution) { + return; + } + + if (notebookExecution.isEmpty()) { + this._logService.debug(`NotebookExecution#dispose ${notebook.toString()}`); + notebookExecution.dispose(); + this._executions.delete(notebook); + } + } + + override dispose(): void { + super.dispose(); + this._executions.forEach(e => e.dispose()); + this._executions.clear(); + } +} + +class NotebookExecution extends Disposable { + private readonly _notebookModel: NotebookTextModel; + + private readonly _cellExecutions = new Map(); + + constructor( + notebook: URI, + @INotebookService private readonly _notebookService: INotebookService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @INotebookExecutionService private readonly _notebookExecutionService: INotebookExecutionService, + @ILogService private readonly _logService: ILogService, + ) { + super(); + this._logService.debug(`NotebookExecution#ctor ${notebook.toString()}`); + + const notebookModel = this._notebookService.getNotebookTextModel(notebook); + if (!notebookModel) { + throw new Error('Notebook not found: ' + notebook); + } + + this._notebookModel = notebookModel; + this._register(this._notebookModel.onWillAddRemoveCells(e => this.onWillAddRemoveCells(e))); + this._register(this._notebookModel.onWillDispose(() => this.onWillDisposeDocument())); + } + + private cellLog(cellHandle: number): string { + return `${this._notebookModel.uri.toString()}, ${cellHandle}`; + } + + isEmpty(): boolean { + return this._cellExecutions.size === 0; + } + + cancelAll(): void { + this._logService.debug(`NotebookExecution#cancelAll`); + this._notebookExecutionService.cancelNotebookCellHandles(this._notebookModel, this._cellExecutions.keys()); + } + + addExecution(cellHandle: number) { + this._logService.debug(`NotebookExecution#addExecution ${this.cellLog(cellHandle)}`); + const execution = this._instantiationService.createInstance(CellExecution, cellHandle, this._notebookModel); + this._cellExecutions.set(cellHandle, execution); + } + + updateExecution(cellHandle: number, updates: ICellExecuteUpdate[]): void { + this.logUpdates(cellHandle, updates); + const execution = this._cellExecutions.get(cellHandle); + if (!execution) { + this._logService.error(`no execution for cell ${cellHandle}`); + return; + } + + execution.update(updates); + } + + private logUpdates(cellHandle: number, updates: ICellExecuteUpdate[]): void { + const updateTypes = updates.map(u => CellExecutionUpdateType[u.editType]).join(', '); + this._logService.debug(`NotebookExecution#updateExecution ${this.cellLog(cellHandle)}, [${updateTypes}]`); + } + + completeExecution(cellHandle: number, complete: ICellExecutionComplete): void { + this._logService.debug(`NotebookExecution#completeExecution ${this.cellLog(cellHandle)}`); + + const execution = this._cellExecutions.get(cellHandle); + if (!execution) { + this._logService.error(`no execution for cell ${cellHandle}`); + return; + } + + try { + execution.complete(complete); + } finally { + this._cellExecutions.delete(cellHandle); + } + } + + private onWillDisposeDocument(): void { + this._logService.debug(`NotebookExecution#onWillDisposeDocument`); + this.cancelAll(); + } + + private onWillAddRemoveCells(e: NotebookTextModelWillAddRemoveEvent): void { + const handles = new Set(this._cellExecutions.keys()); + const myDeletedHandles = new Set(); + e.rawEvent.changes.forEach(([start, deleteCount]) => { + if (deleteCount) { + const deletedHandles = this._notebookModel.cells.slice(start, start + deleteCount).map(c => c.handle); + deletedHandles.forEach(h => { + if (handles.has(h)) { + myDeletedHandles.add(h); + } + }); + } + + return false; + }); + + if (myDeletedHandles.size) { + this._logService.debug(`NotebookExecution#onWillAddRemoveCells, ${JSON.stringify([...myDeletedHandles])}`); + this._notebookExecutionService.cancelNotebookCellHandles(this._notebookModel, myDeletedHandles); + } + } +} + +function updateToEdit(update: ICellExecuteUpdate, cellHandle: number): ICellEditOperation { + if (update.editType === CellExecutionUpdateType.Output) { + return { + editType: CellEditType.Output, + handle: update.cellHandle, + append: update.append, + outputs: update.outputs, + }; + } else if (update.editType === CellExecutionUpdateType.OutputItems) { + return { + editType: CellEditType.OutputItems, + items: update.items, + append: update.append, + outputId: update.outputId + }; + } else if (update.editType === CellExecutionUpdateType.ExecutionState) { + const newInternalMetadata: Partial = { + runState: NotebookCellExecutionState.Executing, + }; + if (typeof update.executionOrder !== 'undefined') { + newInternalMetadata.executionOrder = update.executionOrder; + } + if (typeof update.runStartTime !== 'undefined') { + newInternalMetadata.runStartTime = update.runStartTime; + } + return { + editType: CellEditType.PartialInternalMetadata, + handle: cellHandle, + internalMetadata: newInternalMetadata + }; + } + + throw new Error('Unknown cell update type'); +} + +class CellExecution { + constructor( + private readonly _cellHandle: number, + private readonly _notebookModel: NotebookTextModel, + ) { + const startExecuteEdit: ICellEditOperation = { + editType: CellEditType.PartialInternalMetadata, + handle: this._cellHandle, + internalMetadata: { + runState: NotebookCellExecutionState.Pending, + executionOrder: null, + didPause: false + } + }; + this._applyExecutionEdits([startExecuteEdit]); + } + + update(updates: ICellExecuteUpdate[]): void { + const edits = updates.map(update => updateToEdit(update, this._cellHandle)); + this._applyExecutionEdits(edits); + } + + complete(completionData: ICellExecutionComplete): void { + const cellModel = this._notebookModel.cells.find(c => c.handle === this._cellHandle); + if (!cellModel) { + throw new Error('Cell not found: ' + this._cellHandle); + } + + const edit: ICellEditOperation = { + editType: CellEditType.PartialInternalMetadata, + handle: this._cellHandle, + internalMetadata: { + runState: null, + lastRunSuccess: completionData.lastRunSuccess, + runStartTime: cellModel.internalMetadata.didPause ? null : cellModel.internalMetadata.runStartTime, + runEndTime: cellModel.internalMetadata.didPause ? null : completionData.runEndTime, + isPaused: false, + didPause: false + } + }; + this._applyExecutionEdits([edit]); + } + + private _applyExecutionEdits(edits: ICellEditOperation[]): void { + this._notebookModel.applyEdits(edits, true, undefined, () => undefined, undefined, false); + } +} diff --git a/src/vs/workbench/contrib/notebook/common/notebookExecutionService.ts b/src/vs/workbench/contrib/notebook/common/notebookExecutionService.ts index 72568f60fa5..0013cfbfbfa 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookExecutionService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookExecutionService.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INotebookTextModel, IOutputDto, IOutputItemDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -29,27 +28,11 @@ export interface ICellExecuteOutputItemEdit { items: IOutputItemDto[] } -export type ICellExecuteUpdate = ICellExecuteOutputEdit | ICellExecuteOutputItemEdit | ICellExecutionStateUpdate; - -export interface ICellExecutionStateUpdate { - editType: CellExecutionUpdateType.ExecutionState; - executionOrder?: number; - runStartTime?: number; -} - -export interface ICellExecutionComplete { - runEndTime?: number; - lastRunSuccess?: boolean; -} - export const INotebookExecutionService = createDecorator('INotebookExecutionService'); export interface INotebookExecutionService { _serviceBrand: undefined; - createNotebookCellExecution(notebook: URI, cellHandle: number): void; - updateNotebookCellExecution(notebook: URI, cellHandle: number, updates: ICellExecuteUpdate[]): void; - completeNotebookCellExecution(notebook: URI, cellHandle: number, complete: ICellExecutionComplete): void; getSelectedOrSuggestedKernel(notebook: INotebookTextModel): INotebookKernel | undefined; executeNotebookCells(notebook: INotebookTextModel, cells: Iterable): Promise; cancelNotebookCells(notebook: INotebookTextModel, cells: Iterable): Promise; diff --git a/src/vs/workbench/contrib/notebook/common/notebookExecutionStateService.ts b/src/vs/workbench/contrib/notebook/common/notebookExecutionStateService.ts new file mode 100644 index 00000000000..b39ff197eae --- /dev/null +++ b/src/vs/workbench/contrib/notebook/common/notebookExecutionStateService.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from 'vs/base/common/uri'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { CellExecutionUpdateType, ICellExecuteOutputEdit, ICellExecuteOutputItemEdit } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; + +export type ICellExecuteUpdate = ICellExecuteOutputEdit | ICellExecuteOutputItemEdit | ICellExecutionStateUpdate; + +export interface ICellExecutionStateUpdate { + editType: CellExecutionUpdateType.ExecutionState; + executionOrder?: number; + runStartTime?: number; +} + +export interface ICellExecutionComplete { + runEndTime?: number; + lastRunSuccess?: boolean; +} + +export const INotebookExecutionStateService = createDecorator('INotebookExecutionStateService'); + +export interface INotebookExecutionStateService { + _serviceBrand: undefined; + + createNotebookCellExecution(notebook: URI, cellHandle: number): void; + updateNotebookCellExecution(notebook: URI, cellHandle: number, updates: ICellExecuteUpdate[]): void; + completeNotebookCellExecution(notebook: URI, cellHandle: number, complete: ICellExecutionComplete): void; +} diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookEditorKernelManager.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts similarity index 85% rename from src/vs/workbench/contrib/notebook/test/browser/notebookEditorKernelManager.test.ts rename to src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts index 185848f5a3c..adc4ade9386 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookEditorKernelManager.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts @@ -5,25 +5,25 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; +import { Event } from 'vs/base/common/event'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; +import { mock } from 'vs/base/test/common/mock'; import { assertThrowsAsync } from 'vs/base/test/common/utils'; +import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/modes/modesRegistry'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { NotebookEditorKernelManager } from 'vs/workbench/contrib/notebook/browser/notebookEditorKernelManager'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { insertCellAtIndex } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; +import { NotebookExecutionService } from 'vs/workbench/contrib/notebook/browser/notebookExecutionServiceImpl'; +import { NotebookKernelService } from 'vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl'; import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { CellKind, IOutputDto, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { setupInstantiationService, withTestNotebook as _withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; -import { Event } from 'vs/base/common/event'; -import { ISelectedNotebooksChangeEvent, INotebookKernelService, INotebookKernel } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; -import { NotebookKernelService } from 'vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl'; +import { INotebookKernel, INotebookKernelService, ISelectedNotebooksChangeEvent } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; -import { mock } from 'vs/base/test/common/mock'; -import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/modes/modesRegistry'; -import { insertCellAtIndex } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; +import { setupInstantiationService, withTestNotebook as _withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; -suite('NotebookEditorKernelManager', () => { +suite('NotebookExecutionService', () => { let instantiationService: TestInstantiationService; let kernelService: INotebookKernelService; @@ -65,10 +65,10 @@ suite('NotebookEditorKernelManager', () => { await withTestNotebook( [], async (viewModel) => { - const kernelManager = instantiationService.createInstance(NotebookEditorKernelManager); + const executionService = instantiationService.createInstance(NotebookExecutionService); const cell = insertCellAtIndex(viewModel, 1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true); - await assertThrowsAsync(async () => await kernelManager.executeNotebookCell(cell)); + await assertThrowsAsync(async () => await executionService.executeNotebookCell(cell)); }); }); @@ -78,9 +78,9 @@ suite('NotebookEditorKernelManager', () => { async (viewModel) => { kernelService.registerKernel(new TestNotebookKernel({ languages: ['testlang'] })); - const kernelManager = instantiationService.createInstance(NotebookEditorKernelManager); + const executionService = instantiationService.createInstance(NotebookExecutionService); const cell = insertCellAtIndex(viewModel, 1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true); - await assertThrowsAsync(async () => await kernelManager.executeNotebookCell(cell)); + await assertThrowsAsync(async () => await executionService.executeNotebookCell(cell)); }); }); @@ -91,12 +91,12 @@ suite('NotebookEditorKernelManager', () => { async (viewModel) => { const kernel = new TestNotebookKernel({ languages: ['javascript'] }); kernelService.registerKernel(kernel); - const kernelManager = instantiationService.createInstance(NotebookEditorKernelManager); + const executionService = instantiationService.createInstance(NotebookExecutionService); const executeSpy = sinon.spy(); kernel.executeNotebookCellsRequest = executeSpy; const cell = insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true); - await kernelManager.executeNotebookCells(viewModel.notebookDocument, [cell]); + await executionService.executeNotebookCells(viewModel.notebookDocument, [cell]); assert.strictEqual(executeSpy.calledOnce, true); }); }); @@ -121,13 +121,13 @@ suite('NotebookEditorKernelManager', () => { }; kernelService.registerKernel(kernel); - const kernelManager = instantiationService.createInstance(NotebookEditorKernelManager); + const executionService = instantiationService.createInstance(NotebookExecutionService); let event: ISelectedNotebooksChangeEvent | undefined; kernelService.onDidChangeSelectedNotebooks(e => event = e); const cell = insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); - await kernelManager.executeNotebookCells(viewModel.notebookDocument, [cell]); + await executionService.executeNotebookCells(viewModel.notebookDocument, [cell]); assert.strictEqual(didExecute, true); assert.ok(event !== undefined); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts new file mode 100644 index 00000000000..de512bd4011 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts @@ -0,0 +1,116 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { timeout } from 'vs/base/common/async'; +import { Event } from 'vs/base/common/event'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { mock } from 'vs/base/test/common/mock'; +import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { insertCellAtIndex } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; +import { NotebookExecutionService } from 'vs/workbench/contrib/notebook/browser/notebookExecutionServiceImpl'; +import { NotebookExecutionStateService } from 'vs/workbench/contrib/notebook/browser/notebookExecutionStateServiceImpl'; +import { NotebookKernelService } from 'vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl'; +import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { CellEditType, CellKind, IOutputDto, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookExecutionService } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; +import { INotebookKernel, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { setupInstantiationService, withTestNotebook as _withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; + +suite('NotebookExecutionStateService', () => { + + let instantiationService: TestInstantiationService; + let kernelService: INotebookKernelService; + let disposables: DisposableStore; + + setup(function () { + + disposables = new DisposableStore(); + + instantiationService = setupInstantiationService(disposables); + + instantiationService.stub(INotebookService, new class extends mock() { + override onDidAddNotebookDocument = Event.None; + override onWillRemoveNotebookDocument = Event.None; + override getNotebookTextModels() { return []; } + }); + + kernelService = instantiationService.createInstance(NotebookKernelService); + instantiationService.set(INotebookKernelService, kernelService); + instantiationService.set(INotebookExecutionService, instantiationService.createInstance(NotebookExecutionService)); + }); + + teardown(() => { + disposables.dispose(); + }); + + async function withTestNotebook(cells: [string, string, CellKind, IOutputDto[], NotebookCellMetadata][], callback: (viewModel: NotebookViewModel, textModel: NotebookTextModel) => void | Promise) { + return _withTestNotebook(cells, (editor, viewModel) => callback(viewModel, viewModel.notebookDocument)); + } + + test('cancel execution when cell is deleted', async function () { + return withTestNotebook([], async viewModel => { + instantiationService.stub(INotebookService, new class extends mock() { + override getNotebookTextModel(uri: URI): NotebookTextModel | undefined { + return viewModel.notebookDocument; + } + }); + + let didCancel = false; + const kernel = new class extends TestNotebookKernel { + constructor() { + super({ languages: ['javascript'] }); + } + + override async executeNotebookCellsRequest(): Promise { } + + override async cancelNotebookCellExecution(): Promise { + didCancel = true; + } + }; + kernelService.registerKernel(kernel); + + const executionStateService: NotebookExecutionStateService = instantiationService.createInstance(NotebookExecutionStateService); + + const cell = insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); + executionStateService.createNotebookCellExecution(viewModel.uri, cell.handle); + assert.strictEqual(didCancel, false); + viewModel.notebookDocument.applyEdits([{ + editType: CellEditType.Replace, index: 0, count: 1, cells: [] + }], true, undefined, () => undefined, undefined, false); + await timeout(0); + assert.strictEqual(didCancel, true); + }); + }); +}); + +class TestNotebookKernel implements INotebookKernel { + id: string = 'test'; + label: string = ''; + viewType = '*'; + onDidChange = Event.None; + extension: ExtensionIdentifier = new ExtensionIdentifier('test'); + localResourceRoot: URI = URI.file('/test'); + description?: string | undefined; + detail?: string | undefined; + preloadUris: URI[] = []; + preloadProvides: string[] = []; + supportedLanguages: string[] = []; + executeNotebookCellsRequest(): Promise { + throw new Error('Method not implemented.'); + } + cancelNotebookCellExecution(): Promise { + throw new Error('Method not implemented.'); + } + + constructor(opts?: { languages: string[] }) { + this.supportedLanguages = opts?.languages ?? [PLAINTEXT_MODE_ID]; + } +}