diff --git a/src/sql/workbench/browser/modelComponents/queryTextEditor.ts b/src/sql/workbench/browser/modelComponents/queryTextEditor.ts index 13f8946c9e..c939d15196 100644 --- a/src/sql/workbench/browser/modelComponents/queryTextEditor.ts +++ b/src/sql/workbench/browser/modelComponents/queryTextEditor.ts @@ -37,6 +37,7 @@ export class QueryTextEditor extends BaseTextEditor { private _hideLineNumbers: boolean; private _scrollbarHeight: number; private _lineHeight: number; + private _shouldAddHorizontalScrollbarHeight: boolean = false; constructor( @ITelemetryService telemetryService: ITelemetryService, @@ -123,6 +124,10 @@ export class QueryTextEditor extends BaseTextEditor { return editorWidget.getScrollHeight(); } + public get shouldAddHorizontalScrollbar(): boolean { + return this._shouldAddHorizontalScrollbarHeight; + } + public setHeightToScrollHeight(configChanged?: boolean, isEditorCollapsed?: boolean,) { let editorWidget = this.getControl() as ICodeEditor; let layoutInfo = editorWidget.getLayoutInfo(); @@ -146,7 +151,7 @@ export class QueryTextEditor extends BaseTextEditor { // number of lines that wrap). Finally, viewportColumn is calculated on editor resizing automatically; we can use it to ensure // that the viewportColumn will always be greater than any character's column in an editor. let numberWrappedLines = 0; - let shouldAddHorizontalScrollbarHeight = false; + this._shouldAddHorizontalScrollbarHeight = false; if (!this._lineHeight || configChanged) { this._lineHeight = editorWidget.getOption(EditorOption.lineHeight) || 18; } @@ -162,14 +167,14 @@ export class QueryTextEditor extends BaseTextEditor { for (let line = 1; line <= lineCount; line++) { // The horizontal scrollbar always appears 1 column past the viewport column when word wrap is disabled if (editorWidgetModel.getLineMaxColumn(line) >= layoutInfo.viewportColumn + 1) { - shouldAddHorizontalScrollbarHeight = true; + this._shouldAddHorizontalScrollbarHeight = true; break; } } } let editorHeightUsingLines = this._lineHeight * (lineCount + numberWrappedLines); let editorHeightUsingMinHeight = Math.max(Math.min(editorHeightUsingLines, this._maxHeight), this._minHeight); - editorHeightUsingMinHeight = shouldAddHorizontalScrollbarHeight ? editorHeightUsingMinHeight + this._scrollbarHeight : editorHeightUsingMinHeight; + editorHeightUsingMinHeight = this._shouldAddHorizontalScrollbarHeight ? editorHeightUsingMinHeight + this._scrollbarHeight : editorHeightUsingMinHeight; this.setHeight(editorHeightUsingMinHeight); } diff --git a/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.ts b/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.ts index a951bf0e06..613bc09430 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.ts @@ -242,6 +242,8 @@ export class CodeComponent extends CellView implements OnInit, OnChanges { this.onContentChanged.emit(); this.checkForLanguageMagics(); + // When content is updated we have to also update the horizontal scrollbar + this.horizontalScrollbar(); })); this._register(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('editor.wordWrap') || e.affectsConfiguration('editor.fontSize')) { @@ -249,6 +251,9 @@ export class CodeComponent extends CellView implements OnInit, OnChanges { } })); this._register(this.model.layoutChanged(() => this._layoutEmitter.fire(), this)); + // Handles mouse wheel and scrollbar events + this._register(Event.debounce(this.model.onScroll.event, (l, e) => e, 250, /*leading=*/false) + (() => this.horizontalScrollbar())); this._register(this.cellModel.onExecutionStateChange(event => { if (event === CellExecutionState.Running && !this.cellModel.stdInVisible) { this.setFocusAndScroll(); @@ -264,7 +269,6 @@ export class CodeComponent extends CellView implements OnInit, OnChanges { } this._layoutEmitter.fire(); })); - this.layout(); if (this._cellModel.isCollapsed) { @@ -277,6 +281,51 @@ export class CodeComponent extends CellView implements OnInit, OnChanges { DOM.getContentWidth(this.codeElement.nativeElement), DOM.getContentHeight(this.codeElement.nativeElement))); this._editor.setHeightToScrollHeight(false, this._cellModel.isCollapsed); + this.horizontalScrollbar(); + } + + /** + * Horizontal Scrollbar function will ensure we only calculate and trigger this if word wrap is off and it is a markdown cell + * This will adjust the horizontal scrollbar to either a fixed position at the bottom of the viewport (visible area) + * or it will set it to the bottom of the markdown editor if it is in the viewport (visible area) + */ + public horizontalScrollbar(): void { + let showScrollbar: boolean = this._editor.shouldAddHorizontalScrollbar; + let horizontalScrollbar: HTMLElement = this.codeElement.nativeElement.querySelector('div.scrollbar.horizontal'); + if (this._configurationService.getValue('editor.wordWrap') === 'off' && this.cellModel.cellType !== CellTypes.Code && this.cellModel.source.length > 0 && showScrollbar) { + // Get markdown split view horizontal scrollbar + let viewport: HTMLElement = document.querySelector('.scrollable'); + let markdownEditor: HTMLElement = this.codeElement.nativeElement.closest('.show-markdown .editor'); + + //Get values based on current context of the editor and ADS window + let markdownEditorBottom = Math.floor(markdownEditor.getBoundingClientRect().bottom); + let viewportBottom = Math.floor(viewport.getBoundingClientRect().bottom); + let viewportHeight = DOM.getTotalHeight(viewport); + let viewportTop = Math.floor(document.querySelector('.scrollable').getBoundingClientRect().top); + + // Have to offset the height based on the contents viewport and the additional scrollbars that are present in markdown editor and notebook + let horizontalTop = Math.floor(Math.abs(viewportTop + viewportHeight) - Math.abs(2 * horizontalScrollbar.scrollHeight)); + + // Set opacity for both fixed and absolute + horizontalScrollbar.style.opacity = '1'; + + // If the bottom of the editor is in the viewport, then set the horizontal scrollbar to the bottom of the editor space + if (markdownEditorBottom < viewportBottom) { + horizontalScrollbar.style.position = 'absolute'; + horizontalScrollbar.style.left = '0px'; + horizontalScrollbar.style.top = ''; + horizontalScrollbar.style.bottom = '0px'; + // If the bottom of the editor is not in the viewport, then set the horizontal scrollbar to the bottom of the viewport + } else { + horizontalScrollbar.style.position = 'fixed'; + horizontalScrollbar.style.left = ''; + horizontalScrollbar.style.top = horizontalTop + 'px'; + horizontalScrollbar.style.bottom = ''; + } + } else { + // If horizontal scrollbar is not needed then set do not show it + horizontalScrollbar.style.opacity = '0'; + } } protected initActionBar() { diff --git a/src/sql/workbench/contrib/notebook/browser/notebook.component.ts b/src/sql/workbench/contrib/notebook/browser/notebook.component.ts index c79a4cde9b..24d9c32180 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebook.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebook.component.ts @@ -54,6 +54,7 @@ import { CellToolbarComponent } from 'sql/workbench/contrib/notebook/browser/cel import { NotebookViewsExtension } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension'; import { MaskedLabeledMenuItemActionItem } from 'sql/platform/actions/browser/menuEntryActionViewItem'; import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { Emitter } from 'vs/base/common/event'; export const NOTEBOOK_SELECTOR: string = 'notebook-component'; @@ -84,6 +85,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe private navigationResult: nb.NavigationResult; public previewFeaturesEnabled: boolean = false; public doubleClickEditEnabled: boolean; + private _onScroll = new Emitter(); constructor( @Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef, @@ -223,6 +225,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe //Saves scrollTop value on scroll change public scrollHandler(event: Event) { this._scrollTop = (event.srcElement).scrollTop; + this.model.onScroll.fire(); } public unselectActiveCell() { @@ -339,6 +342,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe this._register(this._model.kernelChanged((kernelArgs) => this.handleKernelChanged(kernelArgs))); this._register(this._model.onCellTypeChanged(() => this.detectChanges())); this._register(this._model.layoutChanged(() => this.detectChanges())); + this._register(this.model.onScroll.event(() => this._onScroll.fire())); this.setLoading(false); // Check if URI fragment is present; if it is, navigate to section by default diff --git a/src/sql/workbench/services/notebook/browser/models/notebookModel.ts b/src/sql/workbench/services/notebook/browser/models/notebookModel.ts index cc48777538..294c941f63 100644 --- a/src/sql/workbench/services/notebook/browser/models/notebookModel.ts +++ b/src/sql/workbench/services/notebook/browser/models/notebookModel.ts @@ -80,6 +80,7 @@ export class NotebookModel extends Disposable implements INotebookModel { private _trustedMode: boolean; private _onActiveCellChanged = new Emitter(); private _onCellTypeChanged = new Emitter(); + private _onScrollEmitter = new Emitter(); private _cells: ICellModel[] | undefined; private _defaultLanguageInfo: nb.ILanguageInfo | undefined; @@ -213,6 +214,10 @@ export class NotebookModel extends Disposable implements INotebookModel { return this._contextsChangedEmitter.event; } + public get onScroll(): Emitter { + return this._onScrollEmitter; + } + public get contextsLoading(): Event { return this._contextsLoadingEmitter.event; }