mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-09 17:52:34 -05:00
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:
@@ -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}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user