support revert notebook cell metadata in diff view.

This commit is contained in:
rebornix 2020-08-27 11:25:25 -07:00
parent 342899271d
commit 883749806b
8 changed files with 326 additions and 56 deletions

View file

@ -78,6 +78,12 @@
"group": "inline@1"
}
]
}
},
"jsonValidation": [
{
"fileMatch": "vscode://vscode-notebook-cell-metadata/*",
"url": "vscode://schemas/notebook/cellmetadata"
}
]
}
}

View file

@ -123,6 +123,8 @@ export class MenuId {
static readonly NotebookCellInsert = new MenuId('NotebookCellInsert');
static readonly NotebookCellBetween = new MenuId('NotebookCellBetween');
static readonly NotebookCellListTop = new MenuId('NotebookCellTop');
static readonly NotebookDiffCellMetadataTitle = new MenuId('NotebookDiffCellMetadataTitle');
static readonly NotebookDiffCellOutputsTitle = new MenuId('NotebookDiffCellOutputsTitle');
static readonly BulkEditTitle = new MenuId('BulkEditTitle');
static readonly BulkEditContext = new MenuId('BulkEditContext');
static readonly TimelineItemContext = new MenuId('TimelineItemContext');

View file

@ -1179,7 +1179,7 @@ declare module 'vscode' {
export interface NotebookCellMetadata {
/**
* Controls if the content of a cell is editable or not.
* Controls whether a cell's editor is editable/readonly.
*/
editable?: boolean;

View file

@ -17,8 +17,17 @@ import { IModelService } from 'vs/editor/common/services/modelService';
import { IModeService } from 'vs/editor/common/services/modeService';
import { format } from 'vs/base/common/jsonFormatter';
import { applyEdits } from 'vs/base/common/jsonEdit';
import { NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { CellUri, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { hash } from 'vs/base/common/hash';
import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IMenu, IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions';
import { CodiconActionViewItem } from 'vs/workbench/contrib/notebook/browser/view/renderers/commonViewComponents';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IAction } from 'vs/base/common/actions';
import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
const fixedEditorOptions: IEditorOptions = {
padding: {
@ -61,6 +70,8 @@ const fixedDiffEditorOptions: IDiffEditorOptions = {
class PropertyHeader extends Disposable {
protected _foldingIndicator!: HTMLElement;
protected _statusSpan!: HTMLElement;
protected _toolbar!: ToolBar;
protected _menu!: IMenu;
constructor(
readonly cell: CellDiffViewModel,
@ -74,7 +85,13 @@ class PropertyHeader extends Disposable {
unChangedLabel: string;
changedLabel: string;
prefix: string;
}
menuId: MenuId;
},
@IContextMenuService readonly contextMenuService: IContextMenuService,
@IKeybindingService readonly keybindingService: IKeybindingService,
@INotificationService readonly notificationService: INotificationService,
@IMenuService readonly menuService: IMenuService,
@IContextKeyService readonly contextKeyService: IContextKeyService
) {
super();
}
@ -83,8 +100,6 @@ class PropertyHeader extends Disposable {
let metadataChanged = this.accessor.checkIfModified(this.cell);
this._foldingIndicator = DOM.append(this.metadataHeaderContainer, DOM.$('.property-folding-indicator'));
DOM.addClass(this._foldingIndicator, this.accessor.prefix);
this._updateFoldingIcon();
const metadataStatus = DOM.append(this.metadataHeaderContainer, DOM.$('div.property-status'));
this._statusSpan = DOM.append(metadataStatus, DOM.$('span'));
@ -97,6 +112,29 @@ class PropertyHeader extends Disposable {
this._statusSpan.textContent = this.accessor.unChangedLabel;
}
const cellToolbarContainer = DOM.append(this.metadataHeaderContainer, DOM.$('div.property-toolbar'));
this._toolbar = new ToolBar(cellToolbarContainer, this.contextMenuService, {
actionViewItemProvider: action => {
if (action instanceof MenuItemAction) {
const item = new CodiconActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService);
return item;
}
return undefined;
}
});
this._toolbar.context = {
cell: this.cell
};
this._menu = this.menuService.createMenu(this.accessor.menuId, this.contextKeyService);
if (metadataChanged) {
const actions: IAction[] = [];
createAndFillInActionBarActions(this._menu, { shouldForwardArgs: true }, actions);
this._toolbar.setActions(actions);
}
this._register(this.notebookEditor.onMouseUp(e => {
if (!e.event.target) {
return;
@ -138,6 +176,22 @@ class PropertyHeader extends Disposable {
this.accessor.updateInfoRendering();
}
refresh() {
let metadataChanged = this.accessor.checkIfModified(this.cell);
if (metadataChanged) {
this._statusSpan.textContent = this.accessor.changedLabel;
this._statusSpan.style.fontWeight = 'bold';
DOM.addClass(this.metadataHeaderContainer, 'modified');
const actions: IAction[] = [];
createAndFillInActionBarActions(this._menu, undefined, actions);
this._toolbar.setActions(actions);
} else {
this._statusSpan.textContent = this.accessor.unChangedLabel;
this._statusSpan.style.fontWeight = 'normal';
this._toolbar.setActions([]);
}
}
private _updateFoldingIcon() {
if (this.accessor.getFoldingState(this.cell) === PropertyFoldingState.Collapsed) {
this._foldingIndicator.innerHTML = renderCodicons('$(chevron-right)');
@ -229,14 +283,15 @@ abstract class AbstractCellRenderer extends Disposable {
this._metadataHeaderContainer = DOM.append(this._diffEditorContainer, DOM.$('.metadata-header-container'));
this._metadataInfoContainer = DOM.append(this._diffEditorContainer, DOM.$('.metadata-info-container'));
this._metadataHeader = new PropertyHeader(
this._metadataHeader = this.instantiationService.createInstance(
PropertyHeader,
this.cell,
this._metadataHeaderContainer,
this.notebookEditor,
{
updateInfoRendering: this.updateMetadataRendering.bind(this),
checkIfModified: (cell) => {
return cell.type === 'modified' && hash(this._getFormatedMetadataJSON(cell.original?.metadata || {})) !== hash(this._getFormatedMetadataJSON(cell.modified?.metadata ?? {}));
return hash(this._getFormatedMetadataJSON(cell.original?.metadata || {}, cell.original?.language)) !== hash(this._getFormatedMetadataJSON(cell.modified?.metadata ?? {}, cell.modified?.language));
},
getFoldingState: (cell) => {
return cell.metadataFoldingState;
@ -246,7 +301,8 @@ abstract class AbstractCellRenderer extends Disposable {
},
unChangedLabel: 'Metadata',
changedLabel: 'Metadata changed',
prefix: 'metadata'
prefix: 'metadata',
menuId: MenuId.NotebookDiffCellMetadataTitle
}
);
this._register(this._metadataHeader);
@ -255,7 +311,8 @@ abstract class AbstractCellRenderer extends Disposable {
this._outputHeaderContainer = DOM.append(this._diffEditorContainer, DOM.$('.output-header-container'));
this._outputInfoContainer = DOM.append(this._diffEditorContainer, DOM.$('.output-info-container'));
this._outputHeader = new PropertyHeader(
this._outputHeader = this.instantiationService.createInstance(
PropertyHeader,
this.cell,
this._outputHeaderContainer,
this.notebookEditor,
@ -272,7 +329,8 @@ abstract class AbstractCellRenderer extends Disposable {
},
unChangedLabel: 'Outputs',
changedLabel: 'Outputs changed',
prefix: 'output'
prefix: 'output',
menuId: MenuId.NotebookDiffCellOutputsTitle
}
);
this._register(this._outputHeader);
@ -310,7 +368,6 @@ abstract class AbstractCellRenderer extends Disposable {
this._outputEditorContainer = DOM.append(this._outputInfoContainer, DOM.$('.output-editor-container'));
this._buildOutputEditor();
} else {
console.log(this.cell);
this._layoutInfo.outputHeight = this._outputEditor.getContentHeight();
this.layout({ outputEditor: true });
}
@ -323,21 +380,7 @@ abstract class AbstractCellRenderer extends Disposable {
}
protected _getFormatedMetadataJSON(metadata: NotebookCellMetadata, language?: string) {
let filteredMetadata: { [key: string]: any } = {};
if (this.notebookEditor.textModel) {
const transientMetadata = this.notebookEditor.textModel!.transientOptions.transientMetadata;
const keys = new Set([...Object.keys(metadata)]);
for (let key of keys) {
if (!(transientMetadata[key as keyof NotebookCellMetadata])
) {
filteredMetadata[key] = metadata[key as keyof NotebookCellMetadata];
}
}
} else {
filteredMetadata = metadata;
}
const filteredMetadata: { [key: string]: any } = metadata;
const content = JSON.stringify({
language,
...filteredMetadata
@ -349,39 +392,124 @@ abstract class AbstractCellRenderer extends Disposable {
return metadataSource;
}
private _applySanitizedMetadataChanges(currentMetadata: NotebookCellMetadata, newMetadata: any) {
let result: { [key: string]: any } = {};
let newLangauge: string | undefined = undefined;
try {
const newMetadataObj = JSON.parse(newMetadata);
const keys = new Set([...Object.keys(newMetadataObj)]);
for (let key of keys) {
switch (key as keyof NotebookCellMetadata) {
case 'breakpointMargin':
case 'editable':
case 'hasExecutionOrder':
case 'inputCollapsed':
case 'outputCollapsed':
case 'runnable':
// boolean
if (typeof newMetadataObj[key] === 'boolean') {
result[key] = newMetadataObj[key];
} else {
result[key] = currentMetadata[key as keyof NotebookCellMetadata];
}
break;
case 'executionOrder':
case 'lastRunDuration':
// number
if (typeof newMetadataObj[key] === 'number') {
result[key] = newMetadataObj[key];
} else {
result[key] = currentMetadata[key as keyof NotebookCellMetadata];
}
break;
case 'runState':
// enum
if (typeof newMetadataObj[key] === 'number' && [1, 2, 3, 4].indexOf(newMetadataObj[key]) >= 0) {
result[key] = newMetadataObj[key];
} else {
result[key] = currentMetadata[key as keyof NotebookCellMetadata];
}
break;
case 'statusMessage':
// string
if (typeof newMetadataObj[key] === 'string') {
result[key] = newMetadataObj[key];
} else {
result[key] = currentMetadata[key as keyof NotebookCellMetadata];
}
break;
default:
if (key === 'language') {
newLangauge = newMetadataObj[key];
}
result[key] = newMetadataObj[key];
break;
}
}
if (newLangauge !== undefined && newLangauge !== this.cell.modified!.language) {
this.notebookEditor.textModel!.changeCellLanguage(this.cell.modified!.handle, newLangauge);
}
this.notebookEditor.textModel!.changeCellMetadata(this.cell.modified!.handle, result, false);
} catch {
}
}
private _buildMetadataEditor() {
if (this.cell.type === 'modified') {
if (this.cell.type === 'modified' || this.cell.type === 'unchanged') {
const originalMetadataSource = this._getFormatedMetadataJSON(this.cell.original?.metadata || {}, this.cell.original?.language);
const modifiedMetadataSource = this._getFormatedMetadataJSON(this.cell.modified?.metadata || {}, this.cell.modified?.language);
if (originalMetadataSource !== modifiedMetadataSource) {
this._metadataEditor = this.instantiationService.createInstance(DiffEditorWidget, this._metadataEditorContainer!, {
...fixedDiffEditorOptions,
overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(),
readOnly: true
});
this._metadataEditor = this.instantiationService.createInstance(DiffEditorWidget, this._metadataEditorContainer!, {
...fixedDiffEditorOptions,
overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(),
readOnly: false,
originalEditable: false
});
DOM.addClass(this._metadataEditorContainer!, 'diff');
DOM.addClass(this._metadataEditorContainer!, 'diff');
const mode = this.modeService.create('json');
const originalMetadataModel = this.modelService.createModel(originalMetadataSource, mode, undefined, true);
const modifiedMetadataModel = this.modelService.createModel(modifiedMetadataSource, mode, undefined, true);
this._metadataEditor.setModel({
original: originalMetadataModel,
modified: modifiedMetadataModel
});
const mode = this.modeService.create('json');
const originalMetadataModel = this.modelService.createModel(originalMetadataSource, mode, CellUri.generateCellMetadataUri(this.cell.original!.uri, this.cell.original!.handle), false);
const modifiedMetadataModel = this.modelService.createModel(modifiedMetadataSource, mode, CellUri.generateCellMetadataUri(this.cell.modified!.uri, this.cell.modified!.handle), false);
this._metadataEditor.setModel({
original: originalMetadataModel,
modified: modifiedMetadataModel
});
this._layoutInfo.metadataHeight = this._metadataEditor.getContentHeight();
this.layout({ metadataEditor: true });
this._register(originalMetadataModel);
this._register(modifiedMetadataModel);
this._register(this._metadataEditor.onDidContentSizeChange((e) => {
if (e.contentHeightChanged && this.cell.metadataFoldingState === PropertyFoldingState.Expanded) {
this._layoutInfo.metadataHeight = e.contentHeight;
this.layout({ metadataEditor: true });
}
}));
this._layoutInfo.metadataHeight = this._metadataEditor.getContentHeight();
this.layout({ metadataEditor: true });
return;
}
this._register(this._metadataEditor.onDidContentSizeChange((e) => {
if (e.contentHeightChanged && this.cell.metadataFoldingState === PropertyFoldingState.Expanded) {
this._layoutInfo.metadataHeight = e.contentHeight;
this.layout({ metadataEditor: true });
}
}));
let respondingToContentChange = false;
this._register(modifiedMetadataModel.onDidChangeContent(() => {
respondingToContentChange = true;
const value = modifiedMetadataModel.getValue();
this._applySanitizedMetadataChanges(this.cell.modified!.metadata, value);
this._metadataHeader.refresh();
respondingToContentChange = false;
}));
this._register(this.cell.modified!.onDidChangeMetadata(() => {
if (respondingToContentChange) {
return;
}
const modifiedMetadataSource = this._getFormatedMetadataJSON(this.cell.modified?.metadata || {}, this.cell.modified?.language);
modifiedMetadataModel.setValue(modifiedMetadataSource);
}));
return;
}
this._metadataEditor = this.instantiationService.createInstance(CodeEditorWidget, this._metadataEditorContainer!, {
@ -390,16 +518,26 @@ abstract class AbstractCellRenderer extends Disposable {
width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true),
height: 0
},
overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode()
overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(),
readOnly: false
}, {});
const mode = this.modeService.create('json');
const mode = this.modeService.create('jsonc');
const originalMetadataSource = this._getFormatedMetadataJSON(
this.cell.type === 'insert'
? this.cell.modified!.metadata || {}
: this.cell.original!.metadata || {});
const metadataModel = this.modelService.createModel(originalMetadataSource, mode, undefined, true);
const uri = this.cell.type === 'insert'
? this.cell.modified!.uri
: this.cell.original!.uri;
const handle = this.cell.type === 'insert'
? this.cell.modified!.handle
: this.cell.original!.handle;
const modelUri = CellUri.generateCellMetadataUri(uri, handle);
const metadataModel = this.modelService.createModel(originalMetadataSource, mode, modelUri, false);
this._metadataEditor.setModel(metadataModel);
this._register(metadataModel);
this._layoutInfo.metadataHeight = this._metadataEditor.getContentHeight();
this.layout({ metadataEditor: true });
@ -422,7 +560,7 @@ abstract class AbstractCellRenderer extends Disposable {
}
private _buildOutputEditor() {
if (this.cell.type === 'modified' && !this.notebookEditor.textModel!.transientOptions.transientOutputs) {
if ((this.cell.type === 'modified' || this.cell.type === 'unchanged') && !this.notebookEditor.textModel!.transientOptions.transientOutputs) {
const originalOutputsSource = this._getFormatedOutputJSON(this.cell.original?.outputs || []);
const modifiedOutputsSource = this._getFormatedOutputJSON(this.cell.modified?.outputs || []);
if (originalOutputsSource !== modifiedOutputsSource) {

View file

@ -79,6 +79,18 @@
cursor: pointer;
}
.notebook-text-diff-editor .cell-diff-editor-container .output-header-container,
.notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container {
display: flex;
flex-direction: row;
align-items: center;
}
.notebook-text-diff-editor .cell-diff-editor-container .output-header-container .property-toolbar,
.notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container .property-toolbar {
margin-left: auto;
}
.notebook-text-diff-editor .cell-diff-editor-container .output-header-container .property-status,
.notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container .property-status {
font-size: 12px;

View file

@ -8,6 +8,7 @@ import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/act
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { viewColumnToEditorGroup } from 'vs/workbench/api/common/shared/editor';
import { ActiveEditorContext } from 'vs/workbench/common/editor';
import { CellDiffViewModel } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel';
import { NotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor';
import { NotebookDiffEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookDiffEditorInput';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
@ -48,3 +49,33 @@ registerAction2(class extends Action2 {
}
}
});
registerAction2(class extends Action2 {
constructor() {
super(
{
id: 'notebook.diff.cell.revertMetadata',
title: localize('notebook.diff.cell.revertMetadata', "Revert Metadata"),
icon: { id: 'codicon/discard' },
f1: false,
menu: {
id: MenuId.NotebookDiffCellMetadataTitle
}
}
);
}
run(accessor: ServicesAccessor, context?: { cell: CellDiffViewModel }) {
if (!context) {
return;
}
const original = context.cell.original;
const modified = context.cell.modified;
if (!original || !modified) {
return;
}
modified.metadata = original.metadata;
}
});

View file

@ -48,6 +48,8 @@ import { INotebookEditorWorkerService } from 'vs/workbench/contrib/notebook/comm
import { NotebookEditorWorkerServiceImpl } from 'vs/workbench/contrib/notebook/common/services/notebookWorkerServiceImpl';
import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService';
import { NotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/browser/notebookCellStatusBarServiceImpl';
import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
// Editor Contribution
@ -440,9 +442,79 @@ class CellContentProvider implements ITextModelContentProvider {
}
}
class RegisterSchemasContribution extends Disposable implements IWorkbenchContribution {
constructor() {
super();
this.registerMetadataSchemas();
}
private registerMetadataSchemas(): void {
const jsonRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution);
const metadataSchema: IJSONSchema = {
properties: {
['language']: {
type: 'string',
description: 'The language for the cell'
},
['editable']: {
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'
},
['hasExecutionOrder']: {
type: 'boolean',
description: 'Whether the execution order indicator will be displayed'
},
['executionOrder']: {
type: 'number',
description: 'The order in which this cell was executed'
},
['statusMessage']: {
type: 'string',
description: `A status message to be shown in the cell's status bar`
},
['runState']: {
type: 'integer',
description: `The cell's current run state`
},
['runStartTime']: {
type: 'number',
description: 'If the cell is running, the time at which the cell started running'
},
['lastRunDuration']: {
type: 'number',
description: `The total duration of the cell's last run`
},
['inputCollapsed']: {
type: 'boolean',
description: `Whether a code cell's editor is collapsed`
},
['outputCollapsed']: {
type: 'boolean',
description: `Whether a code cell's outputs are collapsed`
}
},
// patternProperties: allSettings.patternProperties,
additionalProperties: true,
allowTrailingCommas: true,
allowComments: true
};
jsonRegistry.registerSchema('vscode://schemas/notebook/cellmetadata', metadataSchema);
}
}
const workbenchContributionsRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);
workbenchContributionsRegistry.registerWorkbenchContribution(NotebookContribution, LifecyclePhase.Starting);
workbenchContributionsRegistry.registerWorkbenchContribution(CellContentProvider, LifecyclePhase.Starting);
workbenchContributionsRegistry.registerWorkbenchContribution(RegisterSchemasContribution, LifecyclePhase.Starting);
registerSingleton(INotebookService, NotebookService);
registerSingleton(INotebookEditorWorkerService, NotebookEditorWorkerServiceImpl);

View file

@ -474,6 +474,7 @@ export function getCellUndoRedoComparisonKey(uri: URI) {
export namespace CellUri {
export const scheme = Schemas.vscodeNotebookCell;
const _regex = /^\d{7,}/;
export function generate(notebook: URI, handle: number): URI {
@ -483,6 +484,14 @@ export namespace CellUri {
});
}
export function generateCellMetadataUri(notebook: URI, handle: number): URI {
return notebook.with({
scheme: Schemas.vscode,
authority: 'vscode-notebook-cell-metadata',
fragment: `${handle.toString().padStart(7, '0')}${notebook.scheme !== Schemas.file ? notebook.scheme : ''}`
});
}
export function parse(cell: URI): { notebook: URI, handle: number } | undefined {
if (cell.scheme !== scheme) {
return undefined;