diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 557293f8720..55243f6cfc5 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1635,6 +1635,10 @@ declare module 'vscode' { export type CellOutput = CellStreamOutput | CellErrorOutput | CellDisplayOutput; + export interface NotebookCellMetadata { + editable: boolean; + } + export interface NotebookCell { readonly uri: Uri; handle: number; @@ -1642,6 +1646,11 @@ declare module 'vscode' { cellKind: CellKind; outputs: CellOutput[]; getContent(): string; + metadata?: NotebookCellMetadata; + } + + export interface NotebookDocumentMetadata { + editable: boolean; } export interface NotebookDocument { @@ -1651,6 +1660,7 @@ declare module 'vscode' { languages: string[]; cells: NotebookCell[]; displayOrder?: GlobPattern[]; + metadata?: NotebookDocumentMetadata; } export interface NotebookEditor { @@ -1672,7 +1682,7 @@ declare module 'vscode' { /** * Create a notebook cell. The cell is not inserted into current document when created. Extensions should insert the cell into the document by [TextDocument.cells](#TextDocument.cells) */ - createCell(content: string, language: string, type: CellKind, outputs: CellOutput[]): NotebookCell; + createCell(content: string, language: string, type: CellKind, outputs: CellOutput[], metadata: NotebookCellMetadata): NotebookCell; } export interface NotebookProvider { diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index fd64b9ff1e6..8c2b12f2c9c 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -8,7 +8,7 @@ import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IEx import { Disposable } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; import { INotebookService, IMainNotebookController } from 'vs/workbench/contrib/notebook/browser/notebookService'; -import { INotebookTextModel, INotebookMimeTypeSelector, NOTEBOOK_DISPLAY_ORDER, NotebookCellsSplice, NotebookCellOutputsSplice, CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookTextModel, INotebookMimeTypeSelector, NOTEBOOK_DISPLAY_ORDER, NotebookCellsSplice, NotebookCellOutputsSplice, CellKind, NotebookDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; @@ -127,6 +127,14 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo } } + async $updateNotebookMetadata(viewType: string, resource: UriComponents, metadata: NotebookDocumentMetadata | undefined): Promise { + let controller = this._notebookProviders.get(viewType); + + if (controller) { + controller.updateNotebookMetadata(resource, metadata); + } + } + async resolveNotebook(viewType: string, uri: URI): Promise { let handle = await this._proxy.$resolveNotebook(viewType, uri); return handle; @@ -220,6 +228,11 @@ export class MainThreadNotebookController implements IMainNotebookController { document?.textModel.updateLanguages(languages); } + updateNotebookMetadata(resource: UriComponents, metadata: NotebookDocumentMetadata | undefined) { + let document = this._mapping.get(URI.from(resource).toString()); + document?.textModel.updateNotebookMetadata(metadata); + } + updateNotebookRenderers(resource: UriComponents, renderers: number[]): void { let document = this._mapping.get(URI.from(resource).toString()); document?.textModel.updateRenderers(renderers); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index a5b76f17618..8c12660c19b 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -51,7 +51,7 @@ import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; import { TunnelOptions } from 'vs/platform/remote/common/tunnel'; import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, InternalTimelineOptions } from 'vs/workbench/contrib/timeline/common/timeline'; import { revive } from 'vs/base/common/marshalling'; -import { INotebookMimeTypeSelector, IOutput, INotebookDisplayOrder } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookMimeTypeSelector, IOutput, INotebookDisplayOrder, NotebookCellMetadata, NotebookDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { Dto } from 'vs/base/common/types'; @@ -665,6 +665,7 @@ export interface ICellDto { language: string; cellKind: CellKind; outputs: IOutput[]; + metadata?: NotebookCellMetadata; } export type NotebookCellsSplice = [ @@ -686,6 +687,7 @@ export interface MainThreadNotebookShape extends IDisposable { $unregisterNotebookRenderer(handle: number): Promise; $createNotebookDocument(handle: number, viewType: string, resource: UriComponents): Promise; $updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise; + $updateNotebookMetadata(viewType: string, resource: UriComponents, metadata: NotebookDocumentMetadata | undefined): Promise; $spliceNotebookCells(viewType: string, resource: UriComponents, splices: NotebookCellsSplice[], renderers: number[]): Promise; $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): Promise; $postMessage(handle: number, value: any): Promise; diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 4b3234a5fae..af5687ea166 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -32,7 +32,8 @@ export class ExtHostCell implements vscode.NotebookCell { private _content: string, public cellKind: CellKind, public language: string, - outputs: any[] + outputs: any[], + public metadata: vscode.NotebookCellMetadata | undefined, ) { this.source = this._content.split(/\r|\n|\r\n/g); this._outputs = outputs; @@ -130,6 +131,17 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo this._proxy.$updateNotebookLanguages(this.viewType, this.uri, this._languages); } + private _metadata: vscode.NotebookDocumentMetadata | undefined = undefined; + + get metadata() { + return this._metadata; + } + + set metadata(newMetadata: vscode.NotebookDocumentMetadata | undefined) { + this._metadata = newMetadata; + this._proxy.$updateNotebookMetadata(this.viewType, this.uri, this._metadata); + } + private _displayOrder: string[] = []; get displayOrder() { @@ -366,10 +378,10 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook })); } - createCell(content: string, language: string, type: CellKind, outputs: vscode.CellOutput[]): vscode.NotebookCell { + createCell(content: string, language: string, type: CellKind, outputs: vscode.CellOutput[], metadata: vscode.NotebookCellMetadata | undefined): vscode.NotebookCell { const handle = ExtHostNotebookEditor._cellhandlePool++; const uri = CellUri.generate(this.document.uri, handle); - const cell = new ExtHostCell(handle, uri, content, type, language, outputs); + const cell = new ExtHostCell(handle, uri, content, type, language, outputs, metadata); return cell; } @@ -568,7 +580,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN let editor = this._editors.get(URI.revive(uri).toString()); let document = this._documents.get(URI.revive(uri).toString()); - let rawCell = editor?.editor.createCell('', language, type, []) as ExtHostCell; + let rawCell = editor?.editor.createCell('', language, type, [], undefined) as ExtHostCell; document?.insertCell(index, rawCell!); let allDocuments = this._documentsAndEditors.allDocuments(); @@ -586,6 +598,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN source: rawCell.source, language: rawCell.language, cellKind: rawCell.cellKind, + metadata: rawCell.metadata, outputs: [] }; } 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 e03abc5a61d..e84828e608e 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -119,6 +119,8 @@ abstract class AbstractCellRenderer { return menu; } + abstract getCellToolbarActions(element: CellViewModel): IAction[]; + showContextMenu(listIndex: number | undefined, element: CellViewModel, x: number, y: number) { const actions: IAction[] = [ this.instantiationService.createInstance(InsertCodeCellAboveAction), @@ -215,21 +217,16 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR const contextKeyService = this.contextKeyService.createScoped(templateData.container); contextKeyService.createKey(NOTEBOOK_CELL_TYPE_CONTEXT_KEY, 'markdown'); - const menu = this.createMenu().getCellTitleActions(this.contextKeyService); - const actions: IAction[] = []; - for (let [, actions] of menu.getActions({ shouldForwardArgs: true })) { - actions.push(...actions); - } + const toolbarActions = this.getCellToolbarActions(element); + templateData.toolbar!.setActions(toolbarActions)(); - templateData.toolbar!.setActions([ - ...actions, - this.instantiationService.createInstance(MoveCellUpAction), - this.instantiationService.createInstance(MoveCellDownAction), - this.instantiationService.createInstance(InsertCodeCellBelowAction), - this.instantiationService.createInstance(EditCellAction), - this.instantiationService.createInstance(SaveCellAction), - this.instantiationService.createInstance(DeleteCellAction) - ])(); + if (templateData.focusIndicator) { + if (!toolbarActions.length) { + templateData.focusIndicator.style.top = `8px`; + } else { + templateData.focusIndicator.style.top = `24px`; + } + } } templateData.toolbar!.context = { @@ -239,6 +236,44 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR }; } + getCellToolbarActions(element: CellViewModel): IAction[] { + const viewModel = this.notebookEditor.viewModel; + + if (!viewModel) { + return []; + } + + const menu = this.createMenu().getCellTitleActions(this.contextKeyService); + const actions: IAction[] = []; + for (let [, actions] of menu.getActions({ shouldForwardArgs: true })) { + actions.push(...actions); + } + + const metadata = viewModel.metadata; + + if (!metadata || metadata.editable) { + actions.push( + this.instantiationService.createInstance(MoveCellUpAction), + this.instantiationService.createInstance(MoveCellDownAction), + this.instantiationService.createInstance(InsertCodeCellBelowAction) + ); + } + + const cellMetadata = element.metadata; + if (!cellMetadata || cellMetadata.editable) { + actions.push( + this.instantiationService.createInstance(EditCellAction), + this.instantiationService.createInstance(SaveCellAction) + ); + } + + if (!metadata || metadata.editable) { + this.instantiationService.createInstance(DeleteCellAction); + } + + return actions; + } + getAdditionalContextMenuActions(): IAction[] { return [ this.instantiationService.createInstance(EditCellAction), @@ -363,24 +398,49 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende const contextKeyService = this.contextKeyService.createScoped(templateData.container); contextKeyService.createKey(NOTEBOOK_CELL_TYPE_CONTEXT_KEY, 'code'); - const menu = this.createMenu().getCellTitleActions(contextKeyService); - const actions: IAction[] = []; - for (let [, items] of menu.getActions({ shouldForwardArgs: true })) { - actions.push(...items); - } - - templateData.toolbar!.setActions([ - ...actions, - this.instantiationService.createInstance(MoveCellUpAction), - this.instantiationService.createInstance(MoveCellDownAction), - this.instantiationService.createInstance(InsertCodeCellBelowAction), - this.instantiationService.createInstance(DeleteCellAction) - ])(); - + const toolbarActions = this.getCellToolbarActions(element); + templateData.toolbar!.setActions(toolbarActions)(); templateData.toolbar!.context = toolbarContext; templateData.runToolbar!.context = toolbarContext; + + if (templateData.focusIndicator) { + if (!toolbarActions.length) { + templateData.focusIndicator.style.top = `8px`; + } else { + templateData.focusIndicator.style.top = `24px`; + } + } } + + getCellToolbarActions(element: CellViewModel): IAction[] { + const viewModel = this.notebookEditor.viewModel; + + if (!viewModel) { + return []; + } + + const menu = this.createMenu().getCellTitleActions(this.contextKeyService); + const actions: IAction[] = []; + for (let [, actions] of menu.getActions({ shouldForwardArgs: true })) { + actions.push(...actions); + } + + const metadata = viewModel.metadata; + + if (!metadata || metadata.editable) { + actions.push( + this.instantiationService.createInstance(MoveCellUpAction), + this.instantiationService.createInstance(MoveCellDownAction), + this.instantiationService.createInstance(InsertCodeCellBelowAction), + this.instantiationService.createInstance(DeleteCellAction) + ); + } + + return actions; + } + + getAdditionalContextMenuActions(): IAction[] { return []; } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel.ts index 318ff94c41d..cfbe176b7dc 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel.ts @@ -55,6 +55,10 @@ export class CellViewModel extends Disposable implements ICellViewModel { return this.cell.outputs; } + get metadata() { + return this.cell.metadata; + } + private _state: CellState = CellState.Preview; get state(): CellState { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts index 209a7116bd8..1fabc77efa0 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts @@ -81,6 +81,10 @@ export class NotebookViewModel extends Disposable { return this._model.notebook.uri; } + get metadata() { + return this._model.notebook.metadata; + } + private readonly _onDidChangeViewCells = new Emitter(); get onDidChangeViewCells(): Event { return this._onDidChangeViewCells.event; } diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index c976c610eeb..ccce0be30a4 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -7,7 +7,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { INotebookTextModel, NotebookCellOutputsSplice, NotebookCellsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookTextModel, NotebookCellOutputsSplice, NotebookCellsSplice, NotebookDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; export class NotebookTextModel extends Disposable implements INotebookTextModel { private readonly _onWillDispose: Emitter = this._register(new Emitter()); @@ -21,6 +21,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel cells: NotebookCellTextModel[]; activeCell: NotebookCellTextModel | undefined; languages: string[] = []; + metadata: NotebookDocumentMetadata | undefined = undefined; renderers = new Set(); constructor( @@ -36,6 +37,10 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this.languages = languages; } + updateNotebookMetadata(metadata: NotebookDocumentMetadata | undefined) { + this.metadata = metadata; + } + updateRenderers(renderers: number[]) { renderers.forEach(render => { this.renderers.add(render); diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 73afe15d585..a76f4cc9392 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -36,6 +36,14 @@ export const NOTEBOOK_DISPLAY_ORDER = [ 'text/plain' ]; +export interface NotebookDocumentMetadata { + editable: boolean; +} + +export interface NotebookCellMetadata { + editable: boolean; +} + export interface INotebookDisplayOrder { defaultOrder: string[]; userOrder?: string[]; @@ -122,6 +130,7 @@ export interface ICell { language: string; cellKind: CellKind; outputs: IOutput[]; + metadata?: NotebookCellMetadata; onDidChangeOutputs?: Event; resolveTextBufferFactory(): PieceTreeTextBufferFactory; // TODO@rebornix it should be later on replaced by moving textmodel resolution into CellTextModel