add output diff info when the visual change might be invisible.

This commit is contained in:
rebornix 2021-11-09 17:46:35 -08:00
parent dbdd50bc1c
commit 2b401e2fdf
No known key found for this signature in database
GPG key ID: 0299D52A1BBA52AB
4 changed files with 101 additions and 47 deletions

View file

@ -8,13 +8,13 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { DiffElementViewModelBase, getFormatedMetadataJSON, OUTPUT_EDITOR_HEIGHT_MAGIC, PropertyFoldingState, SideBySideDiffElementViewModel, SingleSideDiffElementViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel';
import { DiffElementViewModelBase, getFormatedMetadataJSON, getFormatedOutputJSON, OUTPUT_EDITOR_HEIGHT_MAGIC, PropertyFoldingState, SideBySideDiffElementViewModel, SingleSideDiffElementViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel';
import { CellDiffSideBySideRenderTemplate, CellDiffSingleSideRenderTemplate, DiffSide, DIFF_CELL_MARGIN, INotebookTextDiffEditor, NOTEBOOK_DIFF_CELL_INPUT, NOTEBOOK_DIFF_CELL_PROPERTY, NOTEBOOK_DIFF_CELL_PROPERTY_EXPANDED } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser';
import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget';
import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IModeService } from 'vs/editor/common/services/modeService';
import { CellEditType, CellUri, IOutputDto, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { CellEditType, CellUri, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon';
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';
@ -99,6 +99,7 @@ export const fixedDiffEditorOptions: IDiffEditorConstructionOptions = {
class PropertyHeader extends Disposable {
protected _foldingIndicator!: HTMLElement;
protected _statusSpan!: HTMLElement;
protected _description!: HTMLElement;
protected _toolbar!: ToolBar;
protected _menu!: IMenu;
protected _propertyExpanded?: IContextKey<boolean>;
@ -109,7 +110,7 @@ class PropertyHeader extends Disposable {
readonly notebookEditor: INotebookTextDiffEditor,
readonly accessor: {
updateInfoRendering: (renderOutput: boolean) => void;
checkIfModified: (cell: DiffElementViewModelBase) => boolean;
checkIfModified: (cell: DiffElementViewModelBase) => false | { reason: string | undefined };
getFoldingState: (cell: DiffElementViewModelBase) => PropertyFoldingState;
updateFoldingState: (cell: DiffElementViewModelBase, newState: PropertyFoldingState) => void;
unChangedLabel: string;
@ -132,14 +133,20 @@ class PropertyHeader extends Disposable {
this._foldingIndicator.classList.add(this.accessor.prefix);
this._updateFoldingIcon();
const metadataStatus = DOM.append(this.propertyHeaderContainer, DOM.$('div.property-status'));
this._statusSpan = DOM.append(metadataStatus, DOM.$('span'));
this._description = DOM.append(metadataStatus, DOM.$('span.property-description'));
if (metadataChanged) {
this._statusSpan.textContent = this.accessor.changedLabel;
this._statusSpan.style.fontWeight = 'bold';
if (metadataChanged.reason) {
this._description.textContent = metadataChanged.reason;
}
this.propertyHeaderContainer.classList.add('modified');
} else {
this._statusSpan.textContent = this.accessor.unChangedLabel;
this._description.textContent = '';
this.propertyHeaderContainer.classList.remove('modified');
}
@ -162,7 +169,7 @@ class PropertyHeader extends Disposable {
const scopedContextKeyService = this.contextKeyService.createScoped(cellToolbarContainer);
this._register(scopedContextKeyService);
const propertyChanged = NOTEBOOK_DIFF_CELL_PROPERTY.bindTo(scopedContextKeyService);
propertyChanged.set(metadataChanged);
propertyChanged.set(!!metadataChanged);
this._propertyExpanded = NOTEBOOK_DIFF_CELL_PROPERTY_EXPANDED.bindTo(scopedContextKeyService);
this._menu = this.menuService.createMenu(this.accessor.menuId, scopedContextKeyService);
@ -224,6 +231,9 @@ class PropertyHeader extends Disposable {
if (metadataChanged) {
this._statusSpan.textContent = this.accessor.changedLabel;
this._statusSpan.style.fontWeight = 'bold';
if (metadataChanged.reason) {
this._description.textContent = metadataChanged.reason;
}
this.propertyHeaderContainer.classList.add('modified');
const actions: IAction[] = [];
createAndFillInActionBarActions(this._menu, undefined, actions);
@ -231,6 +241,7 @@ class PropertyHeader extends Disposable {
} else {
this._statusSpan.textContent = this.accessor.unChangedLabel;
this._statusSpan.style.fontWeight = 'normal';
this._description.textContent = '';
this.propertyHeaderContainer.classList.remove('modified');
this._toolbar.setActions([]);
}
@ -612,16 +623,12 @@ abstract class AbstractElementRenderer extends Disposable {
}
}
private _getFormatedOutputJSON(outputs: IOutputDto[]) {
return JSON.stringify(outputs.map(op => ({ outputs: op.outputs })), undefined, '\t');
}
private _buildOutputEditor() {
this._outputEditorDisposeStore.clear();
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 || []);
const originalOutputsSource = getFormatedOutputJSON(this.cell.original?.outputs || []);
const modifiedOutputsSource = getFormatedOutputJSON(this.cell.modified?.outputs || []);
if (originalOutputsSource !== modifiedOutputsSource) {
const mode = this.modeService.create('json');
const originalModel = this.modelService.createModel(originalOutputsSource, mode, undefined, true);
@ -664,7 +671,7 @@ abstract class AbstractElementRenderer extends Disposable {
}));
this._outputEditorDisposeStore.add(this.cell.modified!.textModel.onDidChangeOutputs(() => {
const modifiedOutputsSource = this._getFormatedOutputJSON(this.cell.modified?.outputs || []);
const modifiedOutputsSource = getFormatedOutputJSON(this.cell.modified?.outputs || []);
modifiedModel.setValue(modifiedOutputsSource);
this._outputHeader.refresh();
}));
@ -684,7 +691,7 @@ abstract class AbstractElementRenderer extends Disposable {
this._outputEditorDisposeStore.add(this._outputEditor);
const mode = this.modeService.create('json');
const originaloutputSource = this._getFormatedOutputJSON(
const originaloutputSource = getFormatedOutputJSON(
this.notebookEditor.textModel!.transientOptions.transientOutputs
? []
: this.cell.type === 'insert'

View file

@ -12,7 +12,7 @@ import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/no
import { hash } from 'vs/base/common/hash';
import { format } from 'vs/base/common/jsonFormatter';
import { applyEdits } from 'vs/base/common/jsonEdit';
import { ICellOutput, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { ICellOutput, IOutputDto, IOutputItemDto, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { DiffNestedCellViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel';
import { URI } from 'vs/base/common/uri';
import { NotebookDiffEditorEventDispatcher, NotebookDiffViewEventType } from 'vs/workbench/contrib/notebook/browser/diff/eventDispatcher';
@ -274,8 +274,8 @@ export abstract class DiffElementViewModelBase extends Disposable {
this.editorEventDispatcher.emit([{ type: NotebookDiffViewEventType.CellLayoutChanged, source: this._layoutInfo }]);
}
abstract checkIfOutputsModified(): boolean;
abstract checkMetadataIfModified(): boolean;
abstract checkIfOutputsModified(): false | { reason: string | undefined; };
abstract checkMetadataIfModified(): false | { reason: string | undefined; };
abstract isOutputEmpty(): boolean;
abstract getRichOutputTotalHeight(): number;
abstract getCellByUri(cellUri: URI): IGenericCellViewModel;
@ -375,11 +375,28 @@ export class SideBySideDiffElementViewModel extends DiffElementViewModelBase {
}
checkIfOutputsModified() {
return !this.mainDocumentTextModel.transientOptions.transientOutputs && !outputsEqual(this.original?.outputs ?? [], this.modified?.outputs ?? []);
if (this.mainDocumentTextModel.transientOptions.transientOutputs) {
return false;
}
const ret = outputsEqual(this.original?.outputs ?? [], this.modified?.outputs ?? []);
if (ret === OutputComparison.Unchanged) {
return false;
}
return {
reason: ret === OutputComparison.Metadata ? 'Output metadata is changed' : undefined
};
}
checkMetadataIfModified(): boolean {
return hash(getFormatedMetadataJSON(this.mainDocumentTextModel, this.original?.metadata || {}, this.original?.language)) !== hash(getFormatedMetadataJSON(this.mainDocumentTextModel, this.modified?.metadata ?? {}, this.modified?.language));
checkMetadataIfModified() {
const modified = hash(getFormatedMetadataJSON(this.mainDocumentTextModel, this.original?.metadata || {}, this.original?.language)) !== hash(getFormatedMetadataJSON(this.mainDocumentTextModel, this.modified?.metadata ?? {}, this.modified?.language));
if (modified) {
return { reason: undefined };
} else {
return false;
}
}
updateOutputHeight(diffSide: DiffSide, index: number, height: number) {
@ -489,11 +506,11 @@ export class SingleSideDiffElementViewModel extends DiffElementViewModelBase {
}
checkIfOutputsModified(): boolean {
checkIfOutputsModified(): false | { reason: string | undefined } {
return false;
}
checkMetadataIfModified(): boolean {
checkMetadataIfModified(): false | { reason: string | undefined } {
return false;
}
@ -536,9 +553,15 @@ export class SingleSideDiffElementViewModel extends DiffElementViewModelBase {
}
}
const enum OutputComparison {
Unchanged = 0,
Metadata = 1,
Other = 2
}
function outputsEqual(original: ICellOutput[], modified: ICellOutput[]) {
if (original.length !== modified.length) {
return false;
return OutputComparison.Other;
}
const len = original.length;
@ -547,11 +570,11 @@ function outputsEqual(original: ICellOutput[], modified: ICellOutput[]) {
const b = modified[i];
if (hash(a.metadata) !== hash(b.metadata)) {
return false;
return OutputComparison.Metadata;
}
if (a.outputs.length !== b.outputs.length) {
return false;
return OutputComparison.Other;
}
for (let j = 0; j < a.outputs.length; j++) {
@ -559,22 +582,22 @@ function outputsEqual(original: ICellOutput[], modified: ICellOutput[]) {
const bOutputItem = b.outputs[j];
if (aOutputItem.mime !== bOutputItem.mime) {
return false;
return OutputComparison.Other;
}
if (aOutputItem.data.buffer.length !== bOutputItem.data.buffer.length) {
return false;
return OutputComparison.Other;
}
for (let k = 0; k < aOutputItem.data.buffer.length; k++) {
if (aOutputItem.data.buffer[k] !== bOutputItem.data.buffer[k]) {
return false;
return OutputComparison.Other;
}
}
}
}
return true;
return OutputComparison.Unchanged;
}
export function getFormatedMetadataJSON(documentTextModel: NotebookTextModel, metadata: NotebookCellMetadata, language?: string) {
@ -604,3 +627,38 @@ export function getFormatedMetadataJSON(documentTextModel: NotebookTextModel, me
return metadataSource;
}
export function getStreamOutputData(outputs: IOutputItemDto[]) {
if (!outputs.length) {
return null;
}
const first = outputs[0];
const mime = first.mime;
const sameStream = !outputs.find(op => op.mime !== mime);
if (sameStream) {
return outputs.map(opit => opit.data.toString()).join('');
} else {
return null;
}
}
export function getFormatedOutputJSON(outputs: IOutputDto[]) {
if (outputs.length === 1) {
const streamOutputData = getStreamOutputData(outputs[0].outputs);
if (streamOutputData) {
return streamOutputData;
}
}
return JSON.stringify(outputs.map(output => {
return ({
metadata: output.metadata,
outputItems: output.outputs.map(opit => ({
mimeType: opit.mime,
data: opit.data.toString()
}))
});
}), undefined, '\t');
}

View file

@ -129,10 +129,15 @@
.notebook-text-diff-editor .cell-diff-editor-container .output-header-container .property-status span,
.notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container .property-status span {
margin: 0 8px;
margin: 0 0 0 8px;
line-height: 21px;
}
.notebook-text-diff-editor .cell-diff-editor-container .output-header-container .property-status span.property-description,
.notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container .property-status span.property-description {
font-style: italic;
}
.notebook-text-diff-editor {
overflow: hidden;
}

View file

@ -30,7 +30,7 @@ import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEd
import { isCompositeNotebookEditorInput, NotebookEditorInput, NotebookEditorInputOptions } from 'vs/workbench/contrib/notebook/common/notebookEditorInput';
import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService';
import { NotebookService } from 'vs/workbench/contrib/notebook/browser/notebookServiceImpl';
import { CellKind, CellUri, IResolvedNotebookEditorModel, NotebookDocumentBackupData, NotebookWorkingCopyTypeIdentifier, NotebookSetting, IOutputItemDto } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { CellKind, CellUri, IResolvedNotebookEditorModel, NotebookDocumentBackupData, NotebookWorkingCopyTypeIdentifier, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService';
@ -45,7 +45,7 @@ import { NotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/brows
import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema';
import { Event } from 'vs/base/common/event';
import { getFormatedMetadataJSON } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel';
import { getFormatedMetadataJSON, getStreamOutputData } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel';
import { NotebookModelResolverServiceImpl } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl';
import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService';
import { NotebookKernelService } from 'vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl';
@ -372,22 +372,6 @@ class CellInfoContentProvider {
return result;
}
private _getStreamOutputData(outputs: IOutputItemDto[]) {
if (!outputs.length) {
return null;
}
const first = outputs[0];
const mime = first.mime;
const sameStream = !outputs.find(op => op.mime !== mime);
if (sameStream) {
return outputs.map(opit => opit.data.toString()).join('');
} else {
return null;
}
}
async provideOutputTextContent(resource: URI): Promise<ITextModel | null> {
const existing = this._modelService.getModel(resource);
if (existing) {
@ -408,7 +392,7 @@ class CellInfoContentProvider {
if (cell.handle === data.handle) {
if (cell.outputs.length === 1) {
// single output
const streamOutputData = this._getStreamOutputData(cell.outputs[0].outputs);
const streamOutputData = getStreamOutputData(cell.outputs[0].outputs);
if (streamOutputData) {
result = this._modelService.createModel(
streamOutputData,