Adding Rendered Notebook Diff Option (#14860)

* First attempt nb diff preview

* First attempt nb diff preview

* Simplify everything

* Interim scroll one way

* Double scroll

* Add setting

* Add tests

* Add comment

* Fix tests

* first round PR comments

* Ensure scrollable portion has scrollbar in diff

* Fix sqllint errors, register events

* Fix scrolling, readonly
This commit is contained in:
Chris LaFreniere
2021-04-02 14:49:52 -07:00
committed by GitHub
parent e7be1daf5c
commit 0d3112ef35
9 changed files with 186 additions and 12 deletions

View File

@@ -0,0 +1,111 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput';
import { SideBySideEditorInput } from 'vs/workbench/common/editor';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { FileNotebookInput } from 'sql/workbench/contrib/notebook/browser/models/fileNotebookInput';
import { INotebookService } from 'sql/workbench/services/notebook/browser/notebookService';
import { Deferred } from 'sql/base/common/promise';
import { ILogService } from 'vs/platform/log/common/log';
export class DiffNotebookInput extends SideBySideEditorInput {
public static ID: string = 'workbench.editorinputs.DiffNotebookInput';
private _notebookService: INotebookService;
private _logService: ILogService;
constructor(
title: string,
diffInput: DiffEditorInput,
@IInstantiationService instantiationService: IInstantiationService,
@INotebookService notebookService: INotebookService,
@ILogService logService: ILogService
) {
let originalInput = instantiationService.createInstance(FileNotebookInput, diffInput.primary.getName(), diffInput.primary.resource, diffInput.originalInput as FileEditorInput);
let modifiedInput = instantiationService.createInstance(FileNotebookInput, diffInput.secondary.getName(), diffInput.secondary.resource, diffInput.modifiedInput as FileEditorInput);
super(title, diffInput.getTitle(), modifiedInput, originalInput);
this._notebookService = notebookService;
this._logService = logService;
this.setupScrollListeners(originalInput, modifiedInput);
}
public getTypeId(): string {
return DiffNotebookInput.ID;
}
/**
* Setup scroll listeners so that both the original and modified editors scroll together
* @param originalInput original notebook input
* @param modifiedInput modified notebook input
*/
private setupScrollListeners(originalInput: FileNotebookInput, modifiedInput: FileNotebookInput): void {
Promise.all([originalInput.containerResolved, modifiedInput.containerResolved]).then(() => {
// Setting container height to 100% ensures that scrollbars will be added when in diff mode
originalInput.container.parentElement.style.height = '100%';
modifiedInput.container.parentElement.style.height = '100%';
// Keep track of when original and modified notebooks are shown
const originalNotebookEditorShown: Deferred<void> = new Deferred<void>();
const modifiedNotebookEditorShown: Deferred<void> = new Deferred<void>();
// Possible for notebooks to have been shown already, so check this case
if (this._notebookService.findNotebookEditor(originalInput.notebookUri)) {
originalNotebookEditorShown.resolve();
} else if (this._notebookService.findNotebookEditor(modifiedInput.notebookUri)) {
modifiedNotebookEditorShown.resolve();
}
// If not already shown, listen for add events
this._register(this._notebookService.onNotebookEditorAdd((e) => {
if (e.notebookParams.input === originalInput) {
originalNotebookEditorShown.resolve();
} else if (e.notebookParams.input === modifiedInput) {
modifiedNotebookEditorShown.resolve();
}
}));
// Once both are shown, look for scrollable DIV. Add scroll listeners here
Promise.all([originalNotebookEditorShown.promise, modifiedNotebookEditorShown.promise]).then(() => {
const originalScrollableNode = originalInput.container?.querySelector('.scrollable');
const modifiedScrollableNode = modifiedInput.container?.querySelector('.scrollable');
if (originalScrollableNode && modifiedScrollableNode) {
// origTarget/modTarget track the scrollTop for the other editor.
// This ensures that events are getting fired while a scroll is ongoing, which can
// result in scrolling speed that seems much slower than expected
let origTarget: number | undefined;
let modTarget: number | undefined;
originalScrollableNode.addEventListener('scroll', () => {
if (origTarget !== undefined) {
if (origTarget === originalScrollableNode.scrollTop) {
origTarget = undefined;
}
return;
}
modTarget = originalScrollableNode.scrollTop;
modifiedScrollableNode.scroll({ top: originalScrollableNode.scrollTop });
});
modifiedScrollableNode.addEventListener('scroll', () => {
if (modTarget !== undefined) {
if (modTarget === modifiedScrollableNode.scrollTop) {
modTarget = undefined;
}
return;
}
origTarget = modifiedScrollableNode.scrollTop;
originalScrollableNode.scroll({ top: modifiedScrollableNode.scrollTop });
});
}
}).catch(error => {
this._logService.error(`Issue encountered waiting for original and modified notebook editors to be shown in notebook diff input: ${error}`);
});
}).catch(error => {
this._logService.error(`Issue encountered waiting for original and modified notebook containers to exist in notebook diff input: ${error}`);
});
}
}

View File

@@ -13,22 +13,30 @@ import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileE
import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput';
import { ILanguageAssociation } from 'sql/workbench/services/languageAssociation/common/languageAssociation';
import { NotebookInput } from 'sql/workbench/contrib/notebook/browser/models/notebookInput';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
import { DiffNotebookInput } from 'sql/workbench/contrib/notebook/browser/models/diffNotebookInput';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
const editorInputFactoryRegistry = Registry.as<IEditorInputFactoryRegistry>(EditorInputExtensions.EditorInputFactories);
export class NotebookEditorInputAssociation implements ILanguageAssociation {
static readonly languages = ['notebook', 'ipynb'];
constructor(@IInstantiationService private readonly instantiationService: IInstantiationService) { }
constructor(@IInstantiationService private readonly instantiationService: IInstantiationService, @IConfigurationService private readonly configurationService: IConfigurationService) { }
convertInput(activeEditor: IEditorInput): NotebookInput {
convertInput(activeEditor: IEditorInput): NotebookInput | DiffNotebookInput {
if (activeEditor instanceof FileEditorInput) {
return this.instantiationService.createInstance(FileNotebookInput, activeEditor.getName(), activeEditor.resource, activeEditor);
} else if (activeEditor instanceof UntitledTextEditorInput) {
return this.instantiationService.createInstance(UntitledNotebookInput, activeEditor.getName(), activeEditor.resource, activeEditor);
} else {
return undefined;
} else if (activeEditor instanceof DiffEditorInput) {
// Only show rendered notebook in diff editor if setting is true (otherwise, defaults back to text diff)
// Note: intentionally not listening to config changes here, given that inputs won't be converted dynamically if the setting is changed
if (this.configurationService.getValue('notebook.showRenderedNotebookInDiffEditor') === true) {
return this.instantiationService.createInstance(DiffNotebookInput, activeEditor.getName(), activeEditor);
}
}
return undefined;
}
createBase(activeEditor: NotebookInput): IEditorInput {

View File

@@ -118,6 +118,11 @@ export class NotebookEditorModel extends EditorModel {
}
public updateModel(contentChange?: NotebookContentChange, type?: NotebookChangeType): void {
// If text editor model is readonly, exit early as no changes need to occur on the model
// Note: this follows what happens in fileCommands where update/save logic is skipped for readonly text editor models
if (this.textEditorModel?.isReadonly()) {
return;
}
if (type === NotebookChangeType.KernelChanged && this._isFirstKernelChange) {
this._isFirstKernelChange = false;
return;
@@ -222,6 +227,7 @@ export abstract class NotebookInput extends EditorInput {
private _notebookEditorOpenedTimestamp: number;
private _modelResolveInProgress: boolean = false;
private _modelResolved: Deferred<void> = new Deferred<void>();
private _containerResolved: Deferred<void> = new Deferred<void>();
private _notebookFindModel: NotebookFindModel;
@@ -456,12 +462,17 @@ export abstract class NotebookInput extends EditorInput {
set container(container: HTMLElement) {
this._disposeContainer();
this._parentContainer = container;
this._containerResolved.resolve();
}
get container(): HTMLElement {
return this._parentContainer;
}
get containerResolved(): Promise<void> {
return this._containerResolved.promise;
}
/**
* An editor that is dirty will be asked to be saved once it closes.
*/