diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts index 48bb2090071..ad209c97a85 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts @@ -48,6 +48,12 @@ async function saveAllFilesAndCloseAll(resource: vscode.Uri | undefined) { await documentClosed; } +async function updateCellMetadata(uri: vscode.Uri, cell: vscode.NotebookCell, newMetadata: vscode.NotebookCellMetadata) { + const edit = new vscode.WorkspaceEdit(); + edit.replaceNotebookCellMetadata(uri, cell.index, newMetadata); + await vscode.workspace.applyEdit(edit); +} + async function updateNotebookMetadata(uri: vscode.Uri, newMetadata: vscode.NotebookDocumentMetadata) { const edit = new vscode.WorkspaceEdit(); edit.replaceNotebookMetadata(uri, newMetadata); @@ -483,7 +489,7 @@ suite('Notebook API tests', function () { const version = vscode.window.activeNotebookEditor!.document.version; await vscode.window.activeNotebookEditor!.edit(editBuilder => { editBuilder.replaceCells(1, 0, [{ cellKind: vscode.NotebookCellKind.Code, language: 'javascript', source: 'test 2', outputs: [], metadata: undefined }]); - editBuilder.replaceCellMetadata(0, new vscode.NotebookCellMetadata().with({ breakpointMargin: false })); + editBuilder.replaceCellMetadata(0, new vscode.NotebookCellMetadata().with({ runnable: false })); }); await cellsChangeEvent; @@ -501,18 +507,18 @@ suite('Notebook API tests', function () { const version = vscode.window.activeNotebookEditor!.document.version; await vscode.window.activeNotebookEditor!.edit(editBuilder => { editBuilder.replaceCells(1, 0, [{ cellKind: vscode.NotebookCellKind.Code, language: 'javascript', source: 'test 2', outputs: [], metadata: undefined }]); - editBuilder.replaceCellMetadata(0, new vscode.NotebookCellMetadata().with({ breakpointMargin: false })); + editBuilder.replaceCellMetadata(0, new vscode.NotebookCellMetadata().with({ runnable: false })); }); await cellsChangeEvent; await cellMetadataChangeEvent; assert.strictEqual(vscode.window.activeNotebookEditor!.document.cells.length, 3); - assert.strictEqual(vscode.window.activeNotebookEditor!.document.cells[0]?.metadata?.breakpointMargin, false); + assert.strictEqual(vscode.window.activeNotebookEditor!.document.cells[0]?.metadata?.runnable, false); assert.strictEqual(version + 1, vscode.window.activeNotebookEditor!.document.version); await vscode.commands.executeCommand('undo'); assert.strictEqual(version + 2, vscode.window.activeNotebookEditor!.document.version); - assert.strictEqual(vscode.window.activeNotebookEditor!.document.cells[0]?.metadata?.breakpointMargin, undefined); + assert.strictEqual(vscode.window.activeNotebookEditor!.document.cells[0]?.metadata?.runnable, undefined); assert.strictEqual(vscode.window.activeNotebookEditor!.document.cells.length, 2); await saveAllFilesAndCloseAll(resource); @@ -680,6 +686,35 @@ suite('Notebook API tests', function () { await saveFileAndCloseAll(resource); }); + + test('cell runnable metadata is respected', async () => { + const resource = await createRandomFile('', undefined, '.vsctestnb'); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + assert.strictEqual(vscode.window.activeNotebookEditor !== undefined, true, 'notebook first'); + const editor = vscode.window.activeNotebookEditor!; + + await vscode.commands.executeCommand('notebook.focusTop'); + const cell = editor.document.cells[0]; + assert.strictEqual(cell.outputs.length, 0); + + let metadataChangeEvent = asPromise(vscode.notebook.onDidChangeCellMetadata); + await updateCellMetadata(resource, cell, cell.metadata.with({ runnable: false })); + await metadataChangeEvent; + + await vscode.commands.executeCommand('notebook.cell.execute'); + assert.strictEqual(cell.outputs.length, 0, 'should not execute'); // not runnable, didn't work + + metadataChangeEvent = asPromise(vscode.notebook.onDidChangeCellMetadata); + await updateCellMetadata(resource, cell, cell.metadata.with({ runnable: true })); + await metadataChangeEvent; + + await vscode.commands.executeCommand('notebook.cell.execute'); + assert.strictEqual(cell.outputs.length, 1, 'should execute'); // runnable, it worked + + await vscode.commands.executeCommand('workbench.action.files.save'); + await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + }); + test('document runnable metadata is respected', async () => { const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); diff --git a/src/vs/base/test/common/utils.ts b/src/vs/base/test/common/utils.ts index 6ca97d3f683..63b0541b482 100644 --- a/src/vs/base/test/common/utils.ts +++ b/src/vs/base/test/common/utils.ts @@ -28,14 +28,3 @@ export function testRepeat(n: number, description: string, callback: (this: any) test(`${description} (iteration ${i})`, callback); } } - -export async function assertThrowsAsync(block: () => any, message: string | Error = 'Missing expected exception'): Promise { - try { - await block(); - } catch { - return; - } - - const err = message instanceof Error ? message : new Error(message); - throw err; -} diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index e2d0e3a2cb2..7be645fcff0 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -142,7 +142,6 @@ export class MenuId { static readonly NotebookCellInsert = new MenuId('NotebookCellInsert'); static readonly NotebookCellBetween = new MenuId('NotebookCellBetween'); static readonly NotebookCellListTop = new MenuId('NotebookCellTop'); - static readonly NotebookCellExecute = new MenuId('NotebookCellExecute'); static readonly NotebookDiffCellInputTitle = new MenuId('NotebookDiffCellInputTitle'); static readonly NotebookDiffCellMetadataTitle = new MenuId('NotebookDiffCellMetadataTitle'); static readonly NotebookDiffCellOutputsTitle = new MenuId('NotebookDiffCellOutputsTitle'); diff --git a/src/vs/platform/quickinput/test/testQuickInputService.ts b/src/vs/platform/quickinput/test/testQuickInputService.ts deleted file mode 100644 index 5240b9d4d35..00000000000 --- a/src/vs/platform/quickinput/test/testQuickInputService.ts +++ /dev/null @@ -1,65 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import { CancellationToken } from 'vs/base/common/cancellation'; -import { Event } from 'vs/base/common/event'; -import * as Types from 'vs/base/common/types'; -import { IInputBox, IInputOptions, IPickOptions, IQuickInputButton, IQuickInputService, IQuickNavigateConfiguration, IQuickPick, IQuickPickItem, Omit, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; - -export class TestQuickInputService implements IQuickInputService { - declare readonly _serviceBrand: undefined; - - readonly onShow = Event.None; - readonly onHide = Event.None; - - readonly quickAccess = undefined!; - - public pick(picks: Promise[]> | QuickPickInput[], options?: IPickOptions & { canPickMany: true }, token?: CancellationToken): Promise; - public pick(picks: Promise[]> | QuickPickInput[], options?: IPickOptions & { canPickMany: false }, token?: CancellationToken): Promise; - public pick(picks: Promise[]> | QuickPickInput[], options?: Omit, 'canPickMany'>, token?: CancellationToken): Promise { - if (Types.isArray(picks)) { - return Promise.resolve({ label: 'selectedPick', description: 'pick description', value: 'selectedPick' }); - } else { - return Promise.resolve(undefined); - } - } - - public input(options?: IInputOptions, token?: CancellationToken): Promise { - return Promise.resolve(options ? 'resolved' + options.prompt : 'resolved'); - } - - backButton!: IQuickInputButton; - - createQuickPick(): IQuickPick { - throw new Error('not implemented.'); - } - - createInputBox(): IInputBox { - throw new Error('not implemented.'); - } - - focus(): void { - throw new Error('not implemented.'); - } - - toggle(): void { - throw new Error('not implemented.'); - } - - navigate(next: boolean, quickNavigate?: IQuickNavigateConfiguration): void { - throw new Error('not implemented.'); - } - - accept(): Promise { - throw new Error('not implemented.'); - } - - back(): Promise { - throw new Error('not implemented.'); - } - - cancel(): Promise { - throw new Error('not implemented.'); - } -} diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 978554e63c0..2552d5904cf 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1118,15 +1118,16 @@ declare module 'vscode' { readonly statusMessage?: string; // run related API, will be removed + readonly runnable?: boolean; readonly hasExecutionOrder?: boolean; readonly executionOrder?: number; readonly runState?: NotebookCellRunState; readonly runStartTime?: number; readonly lastRunDuration?: number; - constructor(editable?: boolean, breakpointMargin?: boolean, hasExecutionOrder?: boolean, executionOrder?: number, runState?: NotebookCellRunState, runStartTime?: number, statusMessage?: string, lastRunDuration?: number, inputCollapsed?: boolean, outputCollapsed?: boolean, custom?: Record) + constructor(editable?: boolean, breakpointMargin?: boolean, runnable?: boolean, hasExecutionOrder?: boolean, executionOrder?: number, runState?: NotebookCellRunState, runStartTime?: number, statusMessage?: string, lastRunDuration?: number, inputCollapsed?: boolean, outputCollapsed?: boolean, custom?: Record) - with(change: { editable?: boolean | null, breakpointMargin?: boolean | null, hasExecutionOrder?: boolean | null, executionOrder?: number | null, runState?: NotebookCellRunState | null, runStartTime?: number | null, statusMessage?: string | null, lastRunDuration?: number | null, inputCollapsed?: boolean | null, outputCollapsed?: boolean | null, custom?: Record | null, }): NotebookCellMetadata; + with(change: { editable?: boolean | null, breakpointMargin?: boolean | null, runnable?: boolean | null, hasExecutionOrder?: boolean | null, executionOrder?: number | null, runState?: NotebookCellRunState | null, runStartTime?: number | null, statusMessage?: string | null, lastRunDuration?: number | null, inputCollapsed?: boolean | null, outputCollapsed?: boolean | null, custom?: Record | null, }): NotebookCellMetadata; } // todo@API support ids https://github.com/jupyter/enhancement-proposals/blob/master/62-cell-id/cell-id.md @@ -1173,9 +1174,10 @@ declare module 'vscode' { // todo@API infer from kernel // todo@API remove readonly runnable: boolean; + readonly cellRunnable: boolean; readonly runState: NotebookRunState; - constructor(editable?: boolean, runnable?: boolean, cellEditable?: boolean, cellHasExecutionOrder?: boolean, displayOrder?: GlobPattern[], custom?: { [key: string]: any; }, runState?: NotebookRunState, trusted?: boolean); + constructor(editable?: boolean, runnable?: boolean, cellEditable?: boolean, cellRunnable?: boolean, cellHasExecutionOrder?: boolean, displayOrder?: GlobPattern[], custom?: { [key: string]: any; }, runState?: NotebookRunState, trusted?: boolean); with(change: { editable?: boolean | null, runnable?: boolean | null, cellEditable?: boolean | null, cellRunnable?: boolean | null, cellHasExecutionOrder?: boolean | null, displayOrder?: GlobPattern[] | null, custom?: { [key: string]: any; } | null, runState?: NotebookRunState | null, trusted?: boolean | null, }): NotebookDocumentMetadata } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 1518282ef12..ae84d8eb41a 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -1419,7 +1419,7 @@ export namespace NotebookCellRange { export namespace NotebookCellMetadata { export function to(data: notebooks.NotebookCellMetadata): types.NotebookCellMetadata { - return new types.NotebookCellMetadata(data.editable, data.breakpointMargin, data.hasExecutionOrder, data.executionOrder, data.runState, data.runStartTime, data.statusMessage, data.lastRunDuration, data.inputCollapsed, data.outputCollapsed, data.custom); + return new types.NotebookCellMetadata(data.editable, data.breakpointMargin, data.runnable, data.hasExecutionOrder, data.executionOrder, data.runState, data.runStartTime, data.statusMessage, data.lastRunDuration, data.inputCollapsed, data.outputCollapsed, data.custom); } } @@ -1430,7 +1430,7 @@ export namespace NotebookDocumentMetadata { } export function to(data: notebooks.NotebookDocumentMetadata): types.NotebookDocumentMetadata { - return new types.NotebookDocumentMetadata(data.editable, data.runnable, data.cellEditable, data.cellHasExecutionOrder, data.displayOrder, data.custom, data.runState, data.trusted); + return new types.NotebookDocumentMetadata(data.editable, data.runnable, data.cellEditable, data.cellRunnable, data.cellHasExecutionOrder, data.displayOrder, data.custom, data.runState, data.trusted); } } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 7e96c68f62d..42d11a118cc 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2924,6 +2924,7 @@ export class NotebookCellMetadata { constructor( readonly editable?: boolean, readonly breakpointMargin?: boolean, + readonly runnable?: boolean, readonly hasExecutionOrder?: boolean, readonly executionOrder?: number, readonly runState?: NotebookCellRunState, @@ -2938,6 +2939,7 @@ export class NotebookCellMetadata { with(change: { editable?: boolean | null, breakpointMargin?: boolean | null, + runnable?: boolean | null, hasExecutionOrder?: boolean | null, executionOrder?: number | null, runState?: NotebookCellRunState | null, @@ -2949,7 +2951,7 @@ export class NotebookCellMetadata { custom?: Record | null, }): NotebookCellMetadata { - let { editable, breakpointMargin, hasExecutionOrder, executionOrder, runState, runStartTime, statusMessage, lastRunDuration, inputCollapsed, outputCollapsed, custom } = change; + let { editable, breakpointMargin, runnable, hasExecutionOrder, executionOrder, runState, runStartTime, statusMessage, lastRunDuration, inputCollapsed, outputCollapsed, custom } = change; if (editable === undefined) { editable = this.editable; @@ -2961,6 +2963,11 @@ export class NotebookCellMetadata { } else if (breakpointMargin === null) { breakpointMargin = undefined; } + if (runnable === undefined) { + runnable = this.runnable; + } else if (runnable === null) { + runnable = undefined; + } if (hasExecutionOrder === undefined) { hasExecutionOrder = this.hasExecutionOrder; } else if (hasExecutionOrder === null) { @@ -3009,6 +3016,7 @@ export class NotebookCellMetadata { if (editable === this.editable && breakpointMargin === this.breakpointMargin && + runnable === this.runnable && hasExecutionOrder === this.hasExecutionOrder && executionOrder === this.executionOrder && runState === this.runState && @@ -3025,6 +3033,7 @@ export class NotebookCellMetadata { return new NotebookCellMetadata( editable, breakpointMargin, + runnable, hasExecutionOrder, executionOrder, runState, @@ -3044,6 +3053,7 @@ export class NotebookDocumentMetadata { readonly editable: boolean = true, readonly runnable: boolean = true, readonly cellEditable: boolean = true, + readonly cellRunnable: boolean = true, readonly cellHasExecutionOrder: boolean = true, readonly displayOrder: vscode.GlobPattern[] = NOTEBOOK_DISPLAY_ORDER, readonly custom: { [key: string]: any; } = {}, @@ -3055,6 +3065,7 @@ export class NotebookDocumentMetadata { editable?: boolean | null, runnable?: boolean | null, cellEditable?: boolean | null, + cellRunnable?: boolean | null, cellHasExecutionOrder?: boolean | null, displayOrder?: vscode.GlobPattern[] | null, custom?: { [key: string]: any; } | null, @@ -3062,7 +3073,7 @@ export class NotebookDocumentMetadata { trusted?: boolean | null, }): NotebookDocumentMetadata { - let { editable, runnable, cellEditable, cellHasExecutionOrder, displayOrder, custom, runState, trusted } = change; + let { editable, runnable, cellEditable, cellRunnable, cellHasExecutionOrder, displayOrder, custom, runState, trusted } = change; if (editable === undefined) { editable = this.editable; @@ -3079,6 +3090,11 @@ export class NotebookDocumentMetadata { } else if (cellEditable === null) { cellEditable = undefined; } + if (cellRunnable === undefined) { + cellRunnable = this.cellRunnable; + } else if (cellRunnable === null) { + cellRunnable = undefined; + } if (cellHasExecutionOrder === undefined) { cellHasExecutionOrder = this.cellHasExecutionOrder; } else if (cellHasExecutionOrder === null) { @@ -3108,6 +3124,7 @@ export class NotebookDocumentMetadata { if (editable === this.editable && runnable === this.runnable && cellEditable === this.cellEditable && + cellRunnable === this.cellRunnable && cellHasExecutionOrder === this.cellHasExecutionOrder && displayOrder === this.displayOrder && custom === this.custom && @@ -3122,6 +3139,7 @@ export class NotebookDocumentMetadata { editable, runnable, cellEditable, + cellRunnable, cellHasExecutionOrder, displayOrder, custom, diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts index c8b84d93729..61e55c38886 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; import * as glob from 'vs/base/common/glob'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import * as platform from 'vs/base/common/platform'; @@ -22,18 +20,20 @@ import { InputFocusedContext, InputFocusedContextKey } from 'vs/platform/context import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { CATEGORIES } from 'vs/workbench/common/actions'; -import { EditorsOrder } from 'vs/workbench/common/editor'; -import { BaseCellRenderTemplate, CellEditState, CellFocusMode, EXECUTE_CELL_COMMAND_ID, EXPAND_CELL_CONTENT_COMMAND_ID, IActiveNotebookEditor, ICellViewModel, INotebookEditor, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_RUN_STATE, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; -import { INotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidgetService'; -import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { BaseCellRenderTemplate, CellEditState, CellFocusMode, EXECUTE_CELL_COMMAND_ID, EXPAND_CELL_CONTENT_COMMAND_ID, IActiveNotebookEditor, ICellViewModel, INotebookEditor, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { CellEditType, CellKind, ICellEditOperation, ICellRange, INotebookDocumentFilter, isDocumentExcludePattern, NotebookCellMetadata, NotebookCellRunState, NOTEBOOK_EDITOR_CURSOR_BEGIN_END, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, TransientMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; +import { EditorsOrder } from 'vs/workbench/common/editor'; +import { INotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidgetService'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; // Notebook Commands const EXECUTE_NOTEBOOK_COMMAND_ID = 'notebook.execute'; @@ -228,17 +228,11 @@ export function getWidgetFromUri(accessor: ServicesAccessor, uri: URI) { return undefined; } -const executeCellCondition = ContextKeyExpr.or( - ContextKeyExpr.and( - ContextKeyExpr.equals(NOTEBOOK_CELL_RUN_STATE.key, NotebookCellRunState[NotebookCellRunState.Idle]), - ContextKeyExpr.greater(NOTEBOOK_KERNEL_COUNT.key, 0)), - NOTEBOOK_CELL_TYPE.isEqualTo('markdown')); - registerAction2(class ExecuteCell extends NotebookCellAction { constructor() { super({ id: EXECUTE_CELL_COMMAND_ID, - precondition: executeCellCondition, + precondition: ContextKeyExpr.or(ContextKeyExpr.greater(NOTEBOOK_KERNEL_COUNT.key, 0), NOTEBOOK_CELL_TYPE.isEqualTo('markdown')), title: localize('notebookActions.execute', "Execute Cell"), keybinding: { when: NOTEBOOK_CELL_LIST_FOCUSED, @@ -248,11 +242,6 @@ registerAction2(class ExecuteCell extends NotebookCellAction { }, weight: EDITOR_WIDGET_ACTION_WEIGHT }, - menu: { - id: MenuId.NotebookCellExecute, - when: executeCellCondition, - group: 'inline' - }, description: { description: localize('notebookActions.execute', "Execute Cell"), args: [ @@ -336,14 +325,8 @@ registerAction2(class StopExecuteCell extends NotebookCellAction { constructor() { super({ id: CANCEL_CELL_COMMAND_ID, - precondition: ContextKeyExpr.equals(NOTEBOOK_CELL_RUN_STATE.key, NotebookCellRunState[NotebookCellRunState.Running]), title: localize('notebookActions.cancel', "Stop Cell Execution"), icon: icons.stopIcon, - menu: { - id: MenuId.NotebookCellExecute, - when: ContextKeyExpr.equals(NOTEBOOK_CELL_RUN_STATE.key, NotebookCellRunState[NotebookCellRunState.Running]), - group: 'inline' - }, description: { description: localize('notebookActions.cancel', "Stop Cell Execution"), args: [ @@ -414,6 +397,42 @@ registerAction2(class StopExecuteCell extends NotebookCellAction { } }); +export class ExecuteCellAction extends MenuItemAction { + constructor( + @IContextKeyService contextKeyService: IContextKeyService, + @ICommandService commandService: ICommandService + ) { + super( + { + id: EXECUTE_CELL_COMMAND_ID, + title: localize('notebookActions.executeCell', "Execute Cell"), + icon: icons.executeIcon + }, + undefined, + { shouldForwardArgs: true }, + contextKeyService, + commandService); + } +} + +export class CancelCellAction extends MenuItemAction { + constructor( + @IContextKeyService contextKeyService: IContextKeyService, + @ICommandService commandService: ICommandService + ) { + super( + { + id: CANCEL_CELL_COMMAND_ID, + title: localize('notebookActions.CancelCell', "Cancel Execution"), + icon: icons.stopIcon + }, + undefined, + { shouldForwardArgs: true }, + contextKeyService, + commandService); + } +} + export class DeleteCellAction extends MenuItemAction { constructor( @IContextKeyService contextKeyService: IContextKeyService, @@ -436,7 +455,7 @@ registerAction2(class ExecuteCellSelectBelow extends NotebookCellAction { constructor() { super({ id: EXECUTE_CELL_SELECT_BELOW, - precondition: executeCellCondition, + precondition: ContextKeyExpr.or(ContextKeyExpr.greater(NOTEBOOK_KERNEL_COUNT.key, 0), NOTEBOOK_CELL_TYPE.isEqualTo('markdown')), title: localize('notebookActions.executeAndSelectBelow', "Execute Notebook Cell and Select Below"), keybinding: { when: NOTEBOOK_CELL_LIST_FOCUSED, @@ -473,7 +492,7 @@ registerAction2(class ExecuteCellInsertBelow extends NotebookCellAction { constructor() { super({ id: EXECUTE_CELL_INSERT_BELOW, - precondition: executeCellCondition, + precondition: ContextKeyExpr.or(ContextKeyExpr.greater(NOTEBOOK_KERNEL_COUNT.key, 0), NOTEBOOK_CELL_TYPE.isEqualTo('markdown')), title: localize('notebookActions.executeAndInsertBelow', "Execute Notebook Cell and Insert Below"), keybinding: { when: NOTEBOOK_CELL_LIST_FOCUSED, @@ -1242,6 +1261,7 @@ registerAction2(class extends NotebookAction { outputs: cell.outputs, metadata: { editable: cell.metadata?.editable, + runnable: cell.metadata?.runnable, breakpointMargin: cell.metadata?.breakpointMargin, hasExecutionOrder: cell.metadata?.hasExecutionOrder, inputCollapsed: cell.metadata?.inputCollapsed, @@ -1299,6 +1319,7 @@ registerAction2(class extends NotebookCellAction { outputs: cell.outputs, metadata: { editable: cell.metadata?.editable, + runnable: cell.metadata?.runnable, breakpointMargin: cell.metadata?.breakpointMargin, hasExecutionOrder: cell.metadata?.hasExecutionOrder, inputCollapsed: cell.metadata?.inputCollapsed, diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/fold/test/notebookFolding.test.ts b/src/vs/workbench/contrib/notebook/browser/contrib/fold/test/notebookFolding.test.ts index e9e26d64a43..51d77a18fbd 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/fold/test/notebookFolding.test.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/fold/test/notebookFolding.test.ts @@ -21,8 +21,8 @@ suite('Notebook Folding', () => { const undoRedoService = instantiationService.stub(IUndoRedoService, () => { }); instantiationService.spy(IUndoRedoService, 'pushElement'); - test('Folding based on markdown cells', async function () { - await withTestNotebook( + test('Folding based on markdown cells', function () { + withTestNotebook( instantiationService, blukEditService, undoRedoService, @@ -50,8 +50,8 @@ suite('Notebook Folding', () => { ); }); - test('Top level header in a cell wins', async function () { - await withTestNotebook( + test('Top level header in a cell wins', function () { + withTestNotebook( instantiationService, blukEditService, undoRedoService, @@ -84,8 +84,8 @@ suite('Notebook Folding', () => { ); }); - test('Folding', async function () { - await withTestNotebook( + test('Folding', function () { + withTestNotebook( instantiationService, blukEditService, undoRedoService, @@ -109,7 +109,7 @@ suite('Notebook Folding', () => { } ); - await withTestNotebook( + withTestNotebook( instantiationService, blukEditService, undoRedoService, @@ -134,7 +134,7 @@ suite('Notebook Folding', () => { } ); - await withTestNotebook( + withTestNotebook( instantiationService, blukEditService, undoRedoService, @@ -160,8 +160,8 @@ suite('Notebook Folding', () => { ); }); - test('Nested Folding', async function () { - await withTestNotebook( + test('Nested Folding', function () { + withTestNotebook( instantiationService, blukEditService, undoRedoService, @@ -217,8 +217,8 @@ suite('Notebook Folding', () => { ); }); - test('Folding Memento', async function () { - await withTestNotebook( + test('Folding Memento', function () { + withTestNotebook( instantiationService, blukEditService, undoRedoService, @@ -249,7 +249,7 @@ suite('Notebook Folding', () => { } ); - await withTestNotebook( + withTestNotebook( instantiationService, blukEditService, undoRedoService, @@ -284,7 +284,7 @@ suite('Notebook Folding', () => { } ); - await withTestNotebook( + withTestNotebook( instantiationService, blukEditService, undoRedoService, @@ -320,8 +320,8 @@ suite('Notebook Folding', () => { ); }); - test('View Index', async function () { - await withTestNotebook( + test('View Index', function () { + withTestNotebook( instantiationService, blukEditService, undoRedoService, @@ -360,7 +360,7 @@ suite('Notebook Folding', () => { } ); - await withTestNotebook( + withTestNotebook( instantiationService, blukEditService, undoRedoService, diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts index cad780288e5..235dd429996 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts @@ -394,6 +394,7 @@ abstract class AbstractElementRenderer extends Disposable { case 'hasExecutionOrder': case 'inputCollapsed': case 'outputCollapsed': + case 'runnable': // boolean if (typeof newMetadataObj[key] === 'boolean') { result[key] = newMetadataObj[key]; diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index a22c105f586..e6e30cc1b27 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -488,9 +488,9 @@ justify-content: center; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .run-button-container .monaco-toolbar, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused .run-button-container .monaco-toolbar, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover .run-button-container .monaco-toolbar { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .runnable .run-button-container .monaco-toolbar, +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused .runnable .run-button-container .monaco-toolbar, +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover .runnable .run-button-container .monaco-toolbar { visibility: visible; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index e3325608815..4d001e37956 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -658,6 +658,10 @@ class RegisterSchemasContribution extends Disposable implements IWorkbenchContri type: 'boolean', description: `Controls whether a cell's editor is editable/readonly` }, + ['runnable']: { + type: 'boolean', + description: 'Controls if the cell is executable' + }, ['breakpointMargin']: { type: 'boolean', description: 'Controls if the cell has a margin to support the breakpoint UI' diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 2d118cfa384..140369f61cf 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -56,8 +56,9 @@ export const NOTEBOOK_CELL_TYPE = new RawContextKey('notebookCellType', export const NOTEBOOK_CELL_EDITABLE = new RawContextKey('notebookCellEditable', false); // bool export const NOTEBOOK_CELL_FOCUSED = new RawContextKey('notebookCellFocused', false); // bool export const NOTEBOOK_CELL_EDITOR_FOCUSED = new RawContextKey('notebookCellEditorFocused', false); // bool +export const NOTEBOOK_CELL_RUNNABLE = new RawContextKey('notebookCellRunnable', false); // bool export const NOTEBOOK_CELL_MARKDOWN_EDIT_MODE = new RawContextKey('notebookCellMarkdownEditMode', false); // bool -export const NOTEBOOK_CELL_RUN_STATE = new RawContextKey('notebookCellRunState', undefined); // Idle, Running +export const NOTEBOOK_CELL_RUN_STATE = new RawContextKey('notebookCellRunState', undefined); // idle, running export const NOTEBOOK_CELL_HAS_OUTPUTS = new RawContextKey('notebookCellHasOutputs', false); // bool export const NOTEBOOK_CELL_INPUT_COLLAPSED = new RawContextKey('notebookCellInputIsCollapsed', false); // bool export const NOTEBOOK_CELL_OUTPUT_COLLAPSED = new RawContextKey('notebookCellOutputIsCollapsed', false); // bool diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorKernelManager.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorKernelManager.ts deleted file mode 100644 index 3caee9e9650..00000000000 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorKernelManager.ts +++ /dev/null @@ -1,445 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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; - loadKernelPreloads(extensionLocation: URI, kernel: INotebookKernel): Promise; -} - -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()); - readonly onDidChangeKernel: Event = this._onDidChangeKernel.event; - private readonly _onDidChangeAvailableKernels = this._register(new Emitter()); - readonly onDidChangeAvailableKernels: Event = this._onDidChangeAvailableKernels.event; - - private _contributedKernelsComputePromise: CancelablePromise | null = null; - private _initialKernelComputationDone: boolean = false; - - private readonly _editorRunnable: IContextKey; - private readonly _notebookExecuting: IContextKey; - private readonly _notebookHasMultipleKernels: IContextKey; - private readonly _notebookKernelCount: IContextKey; - - 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 | 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(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[] = 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(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 { - 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 { - 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 { - if (!this._delegate.viewModel) { - return; - } - - if (cell.cellKind !== CellKind.Code) { - return; - } - - const metadata = cell.getEvaluatedMetadata(this._delegate.viewModel.metadata); - if (metadata.runState !== NotebookCellRunState.Running) { - return; - } - - await this._ensureActiveKernel(); - await this._activeKernel?.cancelNotebookCell!(this._delegate.viewModel.uri, cell.handle); - } - - async executeNotebookCell(cell: ICellViewModel): Promise { - if (!this._delegate.viewModel) { - return; - } - - if (!this.canExecuteCell(cell)) { - throw new Error('Cell is not executable: ' + cell.uri); - } - - await this._ensureActiveKernel(); - this._activeKernelExecuted = true; - await this._activeKernel?.executeNotebookCell!(this._delegate.viewModel.uri, cell.handle); - } - - private canExecuteCell(cell: ICellViewModel): boolean { - if (!this.activeKernel) { - return false; - } - - if (cell.cellKind !== CellKind.Code) { - return false; - } - - if (!this.activeKernel.supportedLanguages) { - return true; - } - - if (this.activeKernel.supportedLanguages.includes(cell.language)) { - return true; - } - - return false; - } -} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index f9a9604b35a..5258d2f70a2 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -5,18 +5,17 @@ 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 { SequencerByKey } from 'vs/base/common/async'; -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CancelablePromise, createCancelablePromise, SequencerByKey } from 'vs/base/common/async'; +import { 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'; @@ -25,7 +24,6 @@ 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'; @@ -34,40 +32,45 @@ 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_FOCUSED, 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_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 { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; -import { IKernelManagerDelegate, NotebookEditorKernelManager } from 'vs/workbench/contrib/notebook/browser/notebookEditorKernelManager'; -import { errorStateIcon, successStateIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { NotebookKernelProviderAssociation, NotebookKernelProviderAssociations, notebookKernelProviderAssociationsSettingId } from 'vs/workbench/contrib/notebook/browser/notebookKernelAssociation'; 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 { 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 { CellDragAndDropController } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellDnd'; 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, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, CellToolbarLocKey, ICellRange, INotebookDecorationRenderOptions, INotebookKernel, NotebookCellRunState, NotebookRunState, 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>(); @@ -92,11 +95,16 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private readonly _editorFocus: IContextKey; private readonly _outputFocus: IContextKey; private readonly _editorEditable: IContextKey; + private readonly _editorRunnable: IContextKey; + private readonly _notebookExecuting: IContextKey; + private readonly _notebookHasMultipleKernels: IContextKey; + private readonly _notebookKernelCount: IContextKey; private _outputRenderer: OutputRenderer; protected readonly _contributions = new Map(); private _scrollBeyondLastLine: boolean; private readonly _memento: Memento; + private readonly _activeKernelMemento: Memento; private readonly _onDidFocusEmitter = this._register(new Emitter()); public readonly onDidFocus = this._onDidFocusEmitter.event; private readonly _onWillScroll = this._register(new Emitter()); @@ -110,7 +118,6 @@ 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(); @@ -145,29 +152,57 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return this._notebookViewModel?.notebookDocument; } - get onDidChangeKernel(): Event { - return this._kernelManger.onDidChangeKernel; - } - get onDidChangeAvailableKernels(): Event { - return this._kernelManger.onDidChangeAvailableKernels; - } + private _activeKernelExecuted: boolean = false; + private _activeKernel: INotebookKernel | undefined = undefined; + private readonly _onDidChangeKernel = this._register(new Emitter()); + readonly onDidChangeKernel: Event = this._onDidChangeKernel.event; + private readonly _onDidChangeAvailableKernels = this._register(new Emitter()); + readonly onDidChangeAvailableKernels: Event = this._onDidChangeAvailableKernels.event; + + private _contributedKernelsComputePromise: CancelablePromise | null = null; + private _initialKernelComputationDone: boolean = false; get activeKernel() { - return this._kernelManger.activeKernel; + return this._activeKernel; } set activeKernel(kernel: INotebookKernel | undefined) { - this._kernelManger.activeKernel = kernel; + 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); + } } + private _activeKernelResolvePromise: Promise | undefined = undefined; + private _currentKernelTokenSource: CancellationTokenSource | undefined = undefined; + private _multipleKernelsAvailable: boolean = false; get multipleKernelsAvailable() { - return this._kernelManger.multipleKernelsAvailable; + return this._multipleKernelsAvailable; } set multipleKernelsAvailable(state: boolean) { - this._kernelManger.multipleKernelsAvailable = state; + this._multipleKernelsAvailable = state; + this._onDidChangeAvailableKernels.fire(); } private readonly _onDidChangeActiveEditor = this._register(new Emitter()); @@ -223,6 +258,7 @@ 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, @@ -234,23 +270,8 @@ 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, { - 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 { - 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('editor.scrollBeyondLastLine'); @@ -283,6 +304,10 @@ 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)) { @@ -752,10 +777,24 @@ 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() { - return this._kernelManger.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; } private async _setKernels(tokenSource: CancellationTokenSource) { @@ -763,7 +802,127 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return; } - this._kernelManger.setKernels(tokenSource); + 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(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(); } private async _loadKernelPreloads(extensionLocation: URI, kernel: INotebookKernel) { @@ -780,10 +939,11 @@ 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._kernelManger.updateForMetadata(); + this._notebookExecuting.set(notebookMetadata.runState === NotebookRunState.Running); } private async _resolveWebview(): Promise | null> { @@ -1338,7 +1498,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._kernelManger.activeKernel?.supportedLanguages ?? this.modeService.getRegisteredModes(); + const supportedLanguages = this._activeKernel?.supportedLanguages ?? this.modeService.getRegisteredModes(); const defaultLanguage = supportedLanguages[0] || 'plaintext'; if (cell?.cellKind === CellKind.Code) { language = cell.language; @@ -1566,16 +1726,152 @@ 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[] = 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(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 { - return this._kernelManger.cancelNotebookExecution(); + if (!this.viewModel) { + return; + } + + if (this.viewModel.metadata.runState !== NotebookRunState.Running) { + return; + } + + await this._ensureActiveKernel(); + await this._activeKernel?.cancelNotebookCell!(this.viewModel.uri, undefined); } async executeNotebook(): Promise { - return this._kernelManger.executeNotebook(); + if (!this.viewModel) { + return; + } + + if (!this.viewModel.runnable) { + return; + } + + await this._ensureActiveKernel(); + this._activeKernelExecuted = true; + await this._activeKernel?.executeNotebookCell!(this.viewModel.uri, undefined); } async cancelNotebookCellExecution(cell: ICellViewModel): Promise { - return this._kernelManger.cancelNotebookCellExecution(cell); + 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); } async executeNotebookCell(cell: ICellViewModel): Promise { @@ -1583,13 +1879,18 @@ 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; } - return this._kernelManger.executeNotebookCell(cell); + if (!cell.getEvaluatedMetadata(this.viewModel.metadata).runnable) { + return; + } + + await this._ensureActiveKernel(); + this._activeKernelExecuted = true; + await this._activeKernel?.executeNotebookCell!(this.viewModel.uri, cell.handle); } focusNotebookCell(cell: ICellViewModel, focusItem: 'editor' | 'container' | 'output', options?: IFocusNotebookCellOptions) { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts index 4fe345670a7..a8993be0823 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts @@ -520,6 +520,7 @@ export class NotebookService extends Disposable implements INotebookService, IEd const cloneMetadata = (cell: NotebookCellTextModel) => { return { editable: cell.metadata?.editable, + runnable: cell.metadata?.runnable, breakpointMargin: cell.metadata?.breakpointMargin, hasExecutionOrder: cell.metadata?.hasExecutionOrder, inputCollapsed: cell.metadata?.inputCollapsed, diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys.ts index 219ac783336..bd6cacbaead 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys.ts @@ -5,7 +5,7 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { INotebookTextModel, NotebookCellRunState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { NOTEBOOK_CELL_TYPE, NOTEBOOK_VIEW_TYPE, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_RUN_STATE, NOTEBOOK_CELL_HAS_OUTPUTS, CellViewModelStateChangeEvent, CellEditState, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_FOCUSED, INotebookEditor, NOTEBOOK_CELL_EDITOR_FOCUSED, CellFocusMode } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NOTEBOOK_CELL_TYPE, NOTEBOOK_VIEW_TYPE, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_RUNNABLE, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_RUN_STATE, NOTEBOOK_CELL_HAS_OUTPUTS, CellViewModelStateChangeEvent, CellEditState, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_FOCUSED, INotebookEditor, NOTEBOOK_CELL_EDITOR_FOCUSED, CellFocusMode } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; @@ -15,6 +15,7 @@ export class CellContextKeyManager extends Disposable { private cellType!: IContextKey; private viewType!: IContextKey; private cellEditable!: IContextKey; + private cellRunnable!: IContextKey; private cellFocused!: IContextKey; private cellEditorFocused!: IContextKey; private cellRunState!: IContextKey; @@ -40,6 +41,7 @@ export class CellContextKeyManager extends Disposable { this.cellEditable = NOTEBOOK_CELL_EDITABLE.bindTo(this.contextKeyService); this.cellFocused = NOTEBOOK_CELL_FOCUSED.bindTo(this.contextKeyService); this.cellEditorFocused = NOTEBOOK_CELL_EDITOR_FOCUSED.bindTo(this.contextKeyService); + this.cellRunnable = NOTEBOOK_CELL_RUNNABLE.bindTo(this.contextKeyService); this.markdownEditMode = NOTEBOOK_CELL_MARKDOWN_EDIT_MODE.bindTo(this.contextKeyService); this.cellRunState = NOTEBOOK_CELL_RUN_STATE.bindTo(this.contextKeyService); this.cellHasOutputs = NOTEBOOK_CELL_HAS_OUTPUTS.bindTo(this.contextKeyService); @@ -114,6 +116,7 @@ export class CellContextKeyManager extends Disposable { private updateForMetadata() { const metadata = this.element.getEvaluatedMetadata(this.notebookTextModel.metadata); this.cellEditable.set(!!metadata.editable); + this.cellRunnable.set(!!metadata.runnable); const runState = metadata.runState ?? NotebookCellRunState.Idle; this.cellRunState.set(NotebookCellRunState[runState]); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellMenus.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellMenus.ts index 89571f6cb62..acd53605379 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellMenus.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellMenus.ts @@ -6,7 +6,6 @@ import { IMenu, IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -// TODO@roblourens Is this class overkill now? export class CellMenus { constructor( @IMenuService private readonly menuService: IMenuService, @@ -24,12 +23,10 @@ export class CellMenus { return this.getMenu(MenuId.NotebookCellListTop, contextKeyService); } - getCellExecuteMenu(contextKeyService: IContextKeyService): IMenu { - return this.getMenu(MenuId.NotebookCellExecute, contextKeyService); - } - private getMenu(menuId: MenuId, contextKeyService: IContextKeyService): IMenu { const menu = this.menuService.createMenu(menuId, contextKeyService); + + return menu; } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index e57c770e168..d03031337fd 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -6,11 +6,11 @@ import { getPixelRatio, getZoomLevel } from 'vs/base/browser/browser'; import * as DOM from 'vs/base/browser/dom'; import { domEvent } from 'vs/base/browser/event'; -import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { IAction } from 'vs/base/common/actions'; +import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -36,13 +36,13 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { BOTTOM_CELL_TOOLBAR_GAP, CELL_BOTTOM_MARGIN, CELL_TOP_MARGIN, EDITOR_BOTTOM_PADDING, EDITOR_BOTTOM_PADDING_WITHOUT_STATUSBAR, EDITOR_TOOLBAR_HEIGHT } from 'vs/workbench/contrib/notebook/browser/constants'; -import { DeleteCellAction, INotebookCellActionContext } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; +import { CancelCellAction, DeleteCellAction, ExecuteCellAction, INotebookCellActionContext } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; import { BaseCellRenderTemplate, CellEditState, CodeCellRenderTemplate, EditorTopPaddingChangeEvent, EXPAND_CELL_CONTENT_COMMAND_ID, getEditorTopPadding, ICellViewModel, INotebookEditor, isCodeCellRenderTemplate, MarkdownCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellContextKeyManager } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys'; -import { CellDragAndDropController, DRAGGING_CLASS } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellDnd'; import { CellMenus } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellMenus'; import { CellEditorStatusBar } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets'; import { CodeCell } from 'vs/workbench/contrib/notebook/browser/view/renderers/codeCell'; +import { CellDragAndDropController, DRAGGING_CLASS } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellDnd'; import { StatefulMarkdownCell } from 'vs/workbench/contrib/notebook/browser/view/renderers/markdownCell'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; @@ -243,7 +243,7 @@ abstract class AbstractCellRenderer { return toolbar; } - protected getCellToolbarActions(menu: IMenu, alwaysFillSecondaryActions: boolean): { primary: IAction[], secondary: IAction[] } { + private getCellToolbarActions(menu: IMenu, alwaysFillSecondaryActions: boolean): { primary: IAction[], secondary: IAction[] } { const primary: IAction[] = []; const secondary: IAction[] = []; const result = { primary, secondary }; @@ -742,8 +742,8 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende const cellContainer = DOM.append(container, $('.cell.code')); const runButtonContainer = DOM.append(cellContainer, $('.run-button-container')); + const runToolbar = disposables.add(this.createToolbar(runButtonContainer)); - const runToolbar = disposables.add(this.setupRunToolbar(runButtonContainer, contextKeyService, disposables)); const executionOrderLabel = DOM.append(cellContainer, $('div.execution-count-label')); const editorPart = DOM.append(cellContainer, $('.cell-editor-part')); @@ -772,7 +772,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende const statusBar = disposables.add(this.instantiationService.createInstance(CellEditorStatusBar, editorPart)); const timer = new TimerRenderer(statusBar.durationContainer); - const cellRunState = new RunStateRenderer(statusBar.cellRunStatusContainer); + const cellRunState = new RunStateRenderer(statusBar.cellRunStatusContainer, runToolbar, this.instantiationService); const outputContainer = DOM.append(container, $('.output')); const outputShowMoreContainer = DOM.append(container, $('.output-show-more-container')); @@ -834,20 +834,6 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende return templateData; } - private setupRunToolbar(runButtonContainer: HTMLElement, contextKeyService: IContextKeyService, disposables: DisposableStore): ToolBar { - const runToolbar = this.createToolbar(runButtonContainer); - const runMenu = this.cellMenus.getCellExecuteMenu(contextKeyService); - const update = () => { - const actions = this.getCellToolbarActions(runMenu, false); - runToolbar.setActions(actions.primary, actions.secondary); - }; - disposables.add(runMenu.onDidChange(() => { - update(); - })); - update(); - return runToolbar; - } - private updateForOutputs(element: CodeCellViewModel, templateData: CodeCellRenderTemplate): void { if (element.outputsViewModels.length) { DOM.show(templateData.focusSinkElement); @@ -862,6 +848,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende } const metadata = element.getEvaluatedMetadata(this.notebookEditor.viewModel.notebookDocument.metadata); + templateData.container.classList.toggle('runnable', !!metadata.runnable); this.updateExecutionOrder(metadata, templateData); templateData.statusBar.cellStatusMessageContainer.textContent = metadata?.statusMessage || ''; @@ -1075,7 +1062,7 @@ export class RunStateRenderer { private spinnerTimer: any | undefined; private pendingNewState: NotebookCellRunState | undefined; - constructor(private readonly element: HTMLElement) { + constructor(private readonly element: HTMLElement, private readonly runToolbar: ToolBar, private readonly instantiationService: IInstantiationService) { DOM.hide(element); } @@ -1092,6 +1079,12 @@ export class RunStateRenderer { return; } + if (runState === NotebookCellRunState.Running) { + this.runToolbar.setActions([this.instantiationService.createInstance(CancelCellAction)]); + } else { + this.runToolbar.setActions([this.instantiationService.createInstance(ExecuteCellAction)]); + } + if (runState === NotebookCellRunState.Success) { DOM.reset(this.element, renderIcon(successStateIcon)); } else if (runState === NotebookCellRunState.Error) { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts index 1b664f13289..ab0a9e52b05 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts @@ -229,6 +229,7 @@ export class CodeCell extends Disposable { const updatePlaceholder = () => { if (this.notebookEditor.viewModel && this.notebookEditor.getActiveCell() === this.viewCell + && viewCell.getEvaluatedMetadata(this.notebookEditor.viewModel.metadata).runnable && viewCell.metadata.runState === undefined && viewCell.metadata.lastRunDuration === undefined ) { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index e15cc59cf91..3d4b3b4e522 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -453,6 +453,9 @@ export abstract class BaseCellViewModel extends Disposable { const editable = this.metadata?.editable ?? documentMetadata.cellEditable; + const runnable = (this.metadata?.runnable ?? + documentMetadata.cellRunnable) && !!documentMetadata.trusted; + const hasExecutionOrder = this.metadata?.hasExecutionOrder ?? documentMetadata.cellHasExecutionOrder; @@ -460,6 +463,7 @@ export abstract class BaseCellViewModel extends Disposable { ...(this.metadata || {}), ...{ editable, + runnable, hasExecutionOrder } }; diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts index 08eb5a6a5c4..ab1142f62d5 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts @@ -153,6 +153,9 @@ export class NotebookCellTextModel extends Disposable implements ICell { const editable = this.metadata?.editable ?? documentMetadata.cellEditable; + const runnable = this.metadata?.runnable ?? + documentMetadata.cellRunnable; + const hasExecutionOrder = this.metadata?.hasExecutionOrder ?? documentMetadata.cellHasExecutionOrder; @@ -160,6 +163,7 @@ export class NotebookCellTextModel extends Disposable implements ICell { ...(this.metadata || {}), ...{ editable, + runnable, hasExecutionOrder } }; diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 009a587a4b8..934568101c5 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -62,6 +62,7 @@ export const notebookDocumentMetadataDefaults: Required { - const instantiationService = setupInstantiationService(); - instantiationService.stub(IStorageService, new TestStorageService()); - instantiationService.stub(IContextKeyService, new MockContextKeyService()); - instantiationService.stub(IQuickInputService, new TestQuickInputService()); - - const bulkEditService = instantiationService.get(IBulkEditService); - const undoRedoService = instantiationService.get(IUndoRedoService); - const loadKernelPreloads = async () => { }; - - async function withTestNotebook(cells: [string, string, CellKind, IOutputDto[], NotebookCellMetadata][], callback: (viewModel: NotebookViewModel, textModel: NotebookTextModel) => void | Promise) { - return _withTestNotebook(instantiationService, bulkEditService, undoRedoService, cells, (_editor, viewModel, textModel) => callback(viewModel, textModel)); - } - - test('ctor', () => { - instantiationService.createInstance(NotebookEditorKernelManager, {}); - const contextKeyService = instantiationService.get(IContextKeyService); - - assert.strictEqual(contextKeyService.getContextKeyValue(NOTEBOOK_KERNEL_COUNT.key), 0); - }); - - test('cell is not runnable when no kernel is selected', async () => { - await withTestNotebook( - [], - async (viewModel) => { - const kernelManager: NotebookEditorKernelManager = instantiationService.createInstance(NotebookEditorKernelManager, { viewModel, loadKernelPreloads }); - - const cell = viewModel.createCell(1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); - await assertThrowsAsync(async () => await kernelManager.executeNotebookCell(cell)); - }); - }); - - test('cell is not runnable when kernel does not support the language', async () => { - await withTestNotebook( - [], - async (viewModel) => { - const kernelManager: NotebookEditorKernelManager = instantiationService.createInstance(NotebookEditorKernelManager, { viewModel, loadKernelPreloads }); - kernelManager.activeKernel = new TestNotebookKernel({ languages: ['testlang'] }); - - const cell = viewModel.createCell(1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); - await assertThrowsAsync(async () => await kernelManager.executeNotebookCell(cell)); - }); - }); - - test('cell is runnable when kernel does support the language', async () => { - await withTestNotebook( - [], - async (viewModel) => { - const kernelManager: NotebookEditorKernelManager = instantiationService.createInstance(NotebookEditorKernelManager, { viewModel, loadKernelPreloads }); - const kernel = new TestNotebookKernel({ languages: ['javascript'] }); - const executeSpy = sinon.spy(); - kernel.executeNotebookCell = executeSpy; - kernelManager.activeKernel = kernel; - - const cell = viewModel.createCell(0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); - await kernelManager.executeNotebookCell(cell); - assert.strictEqual(executeSpy.calledOnce, true); - }); - }); -}); - -class TestNotebookKernel implements INotebookKernel { - id?: string | undefined; - friendlyId: string = ''; - label: string = ''; - extension: ExtensionIdentifier = new ExtensionIdentifier('test'); - extensionLocation: URI = URI.file('/test'); - providerHandle?: number | undefined; - description?: string | undefined; - detail?: string | undefined; - isPreferred?: boolean | undefined; - preloads?: URI[] | undefined; - supportedLanguages?: string[] | undefined; - async resolve(uri: URI, editorId: string, token: CancellationToken): Promise { } - executeNotebookCell(uri: URI, handle: number | undefined): Promise { - throw new Error('Method not implemented.'); - } - cancelNotebookCell(uri: URI, handle: number | undefined): Promise { - throw new Error('Method not implemented.'); - } - - constructor(opts?: { languages?: string[] }) { - this.supportedLanguages = opts?.languages; - } -} diff --git a/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts index 4b417238cd3..df4d6a86b6c 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts @@ -17,8 +17,8 @@ suite('NotebookTextModel', () => { const undoRedoService = instantiationService.stub(IUndoRedoService, () => { }); instantiationService.spy(IUndoRedoService, 'pushElement'); - test('insert', async function () { - await withTestNotebook( + test('insert', function () { + withTestNotebook( instantiationService, blukEditService, undoRedoService, @@ -42,8 +42,8 @@ suite('NotebookTextModel', () => { ); }); - test('multiple inserts at same position', async function () { - await withTestNotebook( + test('multiple inserts at same position', function () { + withTestNotebook( instantiationService, blukEditService, undoRedoService, @@ -67,8 +67,8 @@ suite('NotebookTextModel', () => { ); }); - test('delete', async function () { - await withTestNotebook( + test('delete', function () { + withTestNotebook( instantiationService, blukEditService, undoRedoService, @@ -90,8 +90,8 @@ suite('NotebookTextModel', () => { ); }); - test('delete + insert', async function () { - await withTestNotebook( + test('delete + insert', function () { + withTestNotebook( instantiationService, blukEditService, undoRedoService, @@ -115,8 +115,8 @@ suite('NotebookTextModel', () => { ); }); - test('delete + insert at same position', async function () { - await withTestNotebook( + test('delete + insert at same position', function () { + withTestNotebook( instantiationService, blukEditService, undoRedoService, @@ -140,8 +140,8 @@ suite('NotebookTextModel', () => { ); }); - test('(replace) delete + insert at same position', async function () { - await withTestNotebook( + test('(replace) delete + insert at same position', function () { + withTestNotebook( instantiationService, blukEditService, undoRedoService, @@ -164,8 +164,8 @@ suite('NotebookTextModel', () => { ); }); - test('output', async function () { - await withTestNotebook( + test('output', function () { + withTestNotebook( instantiationService, blukEditService, undoRedoService, @@ -239,8 +239,8 @@ suite('NotebookTextModel', () => { ); }); - test('metadata', async function () { - await withTestNotebook( + test('metadata', function () { + withTestNotebook( instantiationService, blukEditService, undoRedoService, @@ -279,8 +279,8 @@ suite('NotebookTextModel', () => { ); }); - test('multiple inserts in one edit', async function () { - await withTestNotebook( + test('multiple inserts in one edit', function () { + withTestNotebook( instantiationService, blukEditService, undoRedoService, @@ -316,8 +316,8 @@ suite('NotebookTextModel', () => { ); }); - test('insert and metadata change in one edit', async function () { - await withTestNotebook( + test('insert and metadata change in one edit', function () { + withTestNotebook( instantiationService, blukEditService, undoRedoService, diff --git a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts index 93c18a7bd7d..b8af391edaf 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts @@ -19,21 +19,21 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService'; suite('NotebookViewModel', () => { const instantiationService = setupInstantiationService(); const textModelService = instantiationService.get(ITextModelService); - const bulkEditService = instantiationService.get(IBulkEditService); + const blukEditService = instantiationService.get(IBulkEditService); const undoRedoService = instantiationService.get(IUndoRedoService); test('ctor', function () { const notebook = new NotebookTextModel('notebook', URI.parse('test'), [], notebookDocumentMetadataDefaults, { transientMetadata: {}, transientOutputs: false }, undoRedoService, textModelService); const model = new NotebookEditorTestModel(notebook); const eventDispatcher = new NotebookEventDispatcher(); - const viewModel = new NotebookViewModel('notebook', model.notebook, eventDispatcher, null, instantiationService, bulkEditService, undoRedoService); + const viewModel = new NotebookViewModel('notebook', model.notebook, eventDispatcher, null, instantiationService, blukEditService, undoRedoService); assert.equal(viewModel.viewType, 'notebook'); }); - test('insert/delete', async function () { - await withTestNotebook( + test('insert/delete', function () { + withTestNotebook( instantiationService, - bulkEditService, + blukEditService, undoRedoService, [ ['var a = 1;', 'javascript', CellKind.Code, [], { editable: true }], @@ -56,10 +56,10 @@ suite('NotebookViewModel', () => { ); }); - test('move cells down', async function () { - await withTestNotebook( + test('move cells down', function () { + withTestNotebook( instantiationService, - bulkEditService, + blukEditService, undoRedoService, [ ['//a', 'javascript', CellKind.Code, [], { editable: true }], @@ -87,10 +87,10 @@ suite('NotebookViewModel', () => { ); }); - test('move cells up', async function () { - await withTestNotebook( + test('move cells up', function () { + withTestNotebook( instantiationService, - bulkEditService, + blukEditService, undoRedoService, [ ['//a', 'javascript', CellKind.Code, [], { editable: true }], @@ -112,10 +112,10 @@ suite('NotebookViewModel', () => { ); }); - test('index', async function () { - await withTestNotebook( + test('index', function () { + withTestNotebook( instantiationService, - bulkEditService, + blukEditService, undoRedoService, [ ['var a = 1;', 'javascript', CellKind.Code, [], { editable: true }], @@ -141,79 +141,90 @@ suite('NotebookViewModel', () => { ); }); - test('metadata', async function () { - await withTestNotebook( + test('metadata', function () { + withTestNotebook( instantiationService, - bulkEditService, + blukEditService, undoRedoService, [ ['var a = 1;', 'javascript', CellKind.Code, [], {}], - ['var b = 2;', 'javascript', CellKind.Code, [], { editable: true }], - ['var c = 3;', 'javascript', CellKind.Code, [], { editable: true }], - ['var d = 4;', 'javascript', CellKind.Code, [], { editable: false }], - ['var e = 5;', 'javascript', CellKind.Code, [], { editable: false }], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: true, runnable: true }], + ['var c = 3;', 'javascript', CellKind.Code, [], { editable: true, runnable: false }], + ['var d = 4;', 'javascript', CellKind.Code, [], { editable: false, runnable: true }], + ['var e = 5;', 'javascript', CellKind.Code, [], { editable: false, runnable: false }], ], (editor, viewModel) => { - viewModel.notebookDocument.metadata = { editable: true, runnable: true, cellEditable: true, cellHasExecutionOrder: true, trusted: true }; + viewModel.notebookDocument.metadata = { editable: true, runnable: true, cellRunnable: true, cellEditable: true, cellHasExecutionOrder: true, trusted: true }; const defaults = { hasExecutionOrder: true }; assert.deepEqual(viewModel.viewCells[0].getEvaluatedMetadata(viewModel.metadata), { editable: true, + runnable: true, ...defaults }); assert.deepEqual(viewModel.viewCells[1].getEvaluatedMetadata(viewModel.metadata), { editable: true, + runnable: true, ...defaults }); assert.deepEqual(viewModel.viewCells[2].getEvaluatedMetadata(viewModel.metadata), { editable: true, + runnable: false, ...defaults }); assert.deepEqual(viewModel.viewCells[3].getEvaluatedMetadata(viewModel.metadata), { editable: false, + runnable: true, ...defaults }); assert.deepEqual(viewModel.viewCells[4].getEvaluatedMetadata(viewModel.metadata), { editable: false, + runnable: false, ...defaults }); - viewModel.notebookDocument.metadata = { editable: true, runnable: true, cellEditable: true, cellHasExecutionOrder: true, trusted: true }; + viewModel.notebookDocument.metadata = { editable: true, runnable: true, cellRunnable: false, cellEditable: true, cellHasExecutionOrder: true, trusted: true }; assert.deepEqual(viewModel.viewCells[0].getEvaluatedMetadata(viewModel.metadata), { editable: true, + runnable: false, ...defaults }); assert.deepEqual(viewModel.viewCells[1].getEvaluatedMetadata(viewModel.metadata), { editable: true, + runnable: true, ...defaults }); assert.deepEqual(viewModel.viewCells[2].getEvaluatedMetadata(viewModel.metadata), { editable: true, + runnable: false, ...defaults }); assert.deepEqual(viewModel.viewCells[3].getEvaluatedMetadata(viewModel.metadata), { editable: false, + runnable: true, ...defaults }); assert.deepEqual(viewModel.viewCells[4].getEvaluatedMetadata(viewModel.metadata), { editable: false, + runnable: false, ...defaults }); - viewModel.notebookDocument.metadata = { editable: true, runnable: true, cellEditable: false, cellHasExecutionOrder: true, trusted: true }; + viewModel.notebookDocument.metadata = { editable: true, runnable: true, cellRunnable: false, cellEditable: false, cellHasExecutionOrder: true, trusted: true }; assert.deepEqual(viewModel.viewCells[0].getEvaluatedMetadata(viewModel.metadata), { editable: false, + runnable: false, ...defaults }); } @@ -251,17 +262,17 @@ suite('NotebookViewModel Decorations', () => { const blukEditService = instantiationService.get(IBulkEditService); const undoRedoService = instantiationService.get(IUndoRedoService); - test('tracking range', async function () { - await withTestNotebook( + test('tracking range', function () { + withTestNotebook( instantiationService, blukEditService, undoRedoService, [ ['var a = 1;', 'javascript', CellKind.Code, [], {}], - ['var b = 2;', 'javascript', CellKind.Code, [], { editable: true }], - ['var c = 3;', 'javascript', CellKind.Code, [], { editable: true }], - ['var d = 4;', 'javascript', CellKind.Code, [], { editable: false }], - ['var e = 5;', 'javascript', CellKind.Code, [], { editable: false }], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: true, runnable: true }], + ['var c = 3;', 'javascript', CellKind.Code, [], { editable: true, runnable: false }], + ['var d = 4;', 'javascript', CellKind.Code, [], { editable: false, runnable: true }], + ['var e = 5;', 'javascript', CellKind.Code, [], { editable: false, runnable: false }], ], (editor, viewModel) => { const trackedId = viewModel.setTrackedRange('test', { start: 1, end: 2 }, TrackedRangeStickiness.GrowsOnlyWhenTypingAfter); @@ -309,19 +320,19 @@ suite('NotebookViewModel Decorations', () => { ); }); - test('tracking range 2', async function () { - await withTestNotebook( + test('tracking range 2', function () { + withTestNotebook( instantiationService, blukEditService, undoRedoService, [ ['var a = 1;', 'javascript', CellKind.Code, [], {}], - ['var b = 2;', 'javascript', CellKind.Code, [], { editable: true }], - ['var c = 3;', 'javascript', CellKind.Code, [], { editable: true }], - ['var d = 4;', 'javascript', CellKind.Code, [], { editable: false }], - ['var e = 5;', 'javascript', CellKind.Code, [], { editable: false }], - ['var e = 6;', 'javascript', CellKind.Code, [], { editable: false }], - ['var e = 7;', 'javascript', CellKind.Code, [], { editable: false }], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: true, runnable: true }], + ['var c = 3;', 'javascript', CellKind.Code, [], { editable: true, runnable: false }], + ['var d = 4;', 'javascript', CellKind.Code, [], { editable: false, runnable: true }], + ['var e = 5;', 'javascript', CellKind.Code, [], { editable: false, runnable: false }], + ['var e = 6;', 'javascript', CellKind.Code, [], { editable: false, runnable: false }], + ['var e = 7;', 'javascript', CellKind.Code, [], { editable: false, runnable: false }], ], (editor, viewModel) => { const trackedId = viewModel.setTrackedRange('test', { start: 1, end: 3 }, TrackedRangeStickiness.GrowsOnlyWhenTypingAfter); @@ -348,7 +359,7 @@ suite('NotebookViewModel Decorations', () => { ); }); - test('reduce range', async function () { + test('reduce range', function () { assert.deepEqual(reduceCellRanges([ { start: 0, end: 1 }, { start: 1, end: 2 }, @@ -367,7 +378,7 @@ suite('NotebookViewModel Decorations', () => { ]); }); - test('diff hidden ranges', async function () { + test('diff hidden ranges', function () { assert.deepEqual(getVisibleCells([1, 2, 3, 4, 5], []), [1, 2, 3, 4, 5]); assert.deepEqual( @@ -409,7 +420,7 @@ suite('NotebookViewModel Decorations', () => { }), [{ start: 1, deleteCount: 1, toInsert: [2, 6] }]); }); - test('hidden ranges', async function () { + test('hidden ranges', function () { }); }); diff --git a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts index 3b446e06015..0a4a677bad6 100644 --- a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts @@ -456,8 +456,7 @@ export function setupInstantiationService() { return instantiationService; } -// TODO await all usages -export async function withTestNotebook(instantiationService: TestInstantiationService, blukEditService: IBulkEditService, undoRedoService: IUndoRedoService, cells: [string, string, CellKind, IOutputDto[], NotebookCellMetadata][], callback: (editor: TestNotebookEditor, viewModel: NotebookViewModel, textModel: NotebookTextModel) => void | Promise) { +export function withTestNotebook(instantiationService: TestInstantiationService, blukEditService: IBulkEditService, undoRedoService: IUndoRedoService, cells: [string, string, CellKind, IOutputDto[], NotebookCellMetadata][], callback: (editor: TestNotebookEditor, viewModel: NotebookViewModel, textModel: NotebookTextModel) => void) { const textModelService = instantiationService.get(ITextModelService); const viewType = 'notebook'; @@ -475,7 +474,7 @@ export async function withTestNotebook(instantiationService: TestInstantiationSe const eventDispatcher = new NotebookEventDispatcher(); const viewModel = new NotebookViewModel(viewType, model.notebook, eventDispatcher, null, instantiationService, blukEditService, undoRedoService); - await callback(editor, viewModel, notebook); + callback(editor, viewModel, notebook); viewModel.dispose(); return; diff --git a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts index 854fa27930f..a6267a2cbb5 100644 --- a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts +++ b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts @@ -4,27 +4,29 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { normalize } from 'vs/base/common/path'; -import * as platform from 'vs/base/common/platform'; +import { Emitter, Event } from 'vs/base/common/event'; import { URI as uri } from 'vs/base/common/uri'; -import { Selection } from 'vs/editor/common/core/selection'; -import { EditorType } from 'vs/editor/common/editorCommon'; -import { ICommandService } from 'vs/platform/commands/common/commands'; +import * as platform from 'vs/base/common/platform'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { IFormatterChangeEvent, ILabelService, ResourceLabelFormatter } from 'vs/platform/label/common/label'; -import { TestQuickInputService } from 'vs/platform/quickinput/test/testQuickInputService'; -import { IWorkspace, IWorkspaceFolder, Workspace } from 'vs/platform/workspace/common/workspace'; -import { testWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; -import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; -import { BaseConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/configurationResolverService'; +import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; -import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; +import { BaseConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/configurationResolverService'; +import { Workspace, IWorkspaceFolder, IWorkspace } from 'vs/platform/workspace/common/workspace'; import { TestEditorService, TestProductService } from 'vs/workbench/test/browser/workbenchTestServices'; -import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; import { TestWorkbenchConfiguration } from 'vs/workbench/test/electron-browser/workbenchTestServices'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { IQuickInputService, IQuickPickItem, QuickPickInput, IPickOptions, Omit, IInputOptions, IQuickInputButton, IQuickPick, IInputBox, IQuickNavigateConfiguration } from 'vs/platform/quickinput/common/quickInput'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import * as Types from 'vs/base/common/types'; +import { EditorType } from 'vs/editor/common/editorCommon'; +import { Selection } from 'vs/editor/common/core/selection'; +import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; +import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; +import { testWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; +import { IFormatterChangeEvent, ILabelService, ResourceLabelFormatter } from 'vs/platform/label/common/label'; +import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; const mockLineNumber = 10; class TestEditorServiceWithActiveEditor extends TestEditorService { @@ -64,13 +66,13 @@ suite('Configuration Resolver Service', () => { let editorService: TestEditorServiceWithActiveEditor; let containingWorkspace: Workspace; let workspace: IWorkspaceFolder; - let quickInputService: TestQuickInputService; + let quickInputService: MockQuickInputService; let labelService: MockLabelService; setup(() => { mockCommandService = new MockCommandService(); editorService = new TestEditorServiceWithActiveEditor(); - quickInputService = new TestQuickInputService(); + quickInputService = new MockQuickInputService(); environmentService = new MockWorkbenchEnvironmentService(envVariables); labelService = new MockLabelService(); containingWorkspace = testWorkspace(uri.parse('file:///VSCode/workspaceLocation')); @@ -646,6 +648,63 @@ class MockCommandService implements ICommandService { } } +class MockQuickInputService implements IQuickInputService { + declare readonly _serviceBrand: undefined; + + readonly onShow = Event.None; + readonly onHide = Event.None; + + readonly quickAccess = undefined!; + + public pick(picks: Promise[]> | QuickPickInput[], options?: IPickOptions & { canPickMany: true }, token?: CancellationToken): Promise; + public pick(picks: Promise[]> | QuickPickInput[], options?: IPickOptions & { canPickMany: false }, token?: CancellationToken): Promise; + public pick(picks: Promise[]> | QuickPickInput[], options?: Omit, 'canPickMany'>, token?: CancellationToken): Promise { + if (Types.isArray(picks)) { + return Promise.resolve({ label: 'selectedPick', description: 'pick description', value: 'selectedPick' }); + } else { + return Promise.resolve(undefined); + } + } + + public input(options?: IInputOptions, token?: CancellationToken): Promise { + return Promise.resolve(options ? 'resolved' + options.prompt : 'resolved'); + } + + backButton!: IQuickInputButton; + + createQuickPick(): IQuickPick { + throw new Error('not implemented.'); + } + + createInputBox(): IInputBox { + throw new Error('not implemented.'); + } + + focus(): void { + throw new Error('not implemented.'); + } + + toggle(): void { + throw new Error('not implemented.'); + } + + navigate(next: boolean, quickNavigate?: IQuickNavigateConfiguration): void { + throw new Error('not implemented.'); + } + + accept(): Promise { + throw new Error('not implemented.'); + } + + back(): Promise { + throw new Error('not implemented.'); + } + + cancel(): Promise { + throw new Error('not implemented.'); + } +} + class MockLabelService implements ILabelService { _serviceBrand: undefined; getUriLabel(resource: uri, options?: { relative?: boolean | undefined; noPrefix?: boolean | undefined; endWithSeparator?: boolean | undefined; }): string { diff --git a/src/vs/workbench/test/browser/api/extHostTypes.test.ts b/src/vs/workbench/test/browser/api/extHostTypes.test.ts index d09b89f1ecd..4313ba4d168 100644 --- a/src/vs/workbench/test/browser/api/extHostTypes.test.ts +++ b/src/vs/workbench/test/browser/api/extHostTypes.test.ts @@ -653,6 +653,7 @@ suite('ExtHostTypes', function () { const obj = new types.NotebookDocumentMetadata(); assert.strictEqual(obj.cellEditable, notebookDocumentMetadataDefaults.cellEditable); assert.strictEqual(obj.cellHasExecutionOrder, notebookDocumentMetadataDefaults.cellHasExecutionOrder); + assert.strictEqual(obj.cellRunnable, notebookDocumentMetadataDefaults.cellRunnable); assert.deepStrictEqual(obj.custom, notebookDocumentMetadataDefaults.custom); assert.deepStrictEqual(obj.displayOrder, notebookDocumentMetadataDefaults.displayOrder); assert.strictEqual(obj.editable, notebookDocumentMetadataDefaults.editable);