From 3e87f9f88932a89969584c673c4e85765278f5f5 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 2 Mar 2017 14:59:41 -0700 Subject: [PATCH] Add horizontal autoscroll --- spec/text-editor-component-spec.js | 51 +++++++++++++ src/text-editor-component.js | 119 +++++++++++++++++++++++------ src/text-editor.coffee | 1 + 3 files changed, 148 insertions(+), 23 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 9892a4c1e..e6256a0bd 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -273,6 +273,57 @@ describe('TextEditorComponent', () => { await component.getNextUpdatePromise() expect(scroller.scrollTop).toBe((4 - scrollMarginInLines) * component.measurements.lineHeight) }) + + it('automatically scrolls horizontally when the cursor is within horizontal scroll margin of the right edge of the gutter or right edge of the screen', async () => { + const {component, element, editor} = buildComponent() + const {scroller} = component.refs + element.style.width = + component.getGutterContainerWidth() + + 3 * editor.horizontalScrollMargin * component.measurements.baseCharacterWidth + 'px' + await component.getNextUpdatePromise() + + editor.scrollToScreenRange([[1, 12], [2, 28]]) + await component.getNextUpdatePromise() + let expectedScrollLeft = Math.floor( + clientLeftForCharacter(component, 1, 12) - + lineNodeForScreenRow(component, 1).getBoundingClientRect().left - + (editor.horizontalScrollMargin * component.measurements.baseCharacterWidth) + ) + expect(scroller.scrollLeft).toBe(expectedScrollLeft) + + editor.scrollToScreenRange([[1, 12], [2, 28]], {reversed: false}) + await component.getNextUpdatePromise() + expectedScrollLeft = Math.floor( + component.getGutterContainerWidth() + + clientLeftForCharacter(component, 2, 28) - + lineNodeForScreenRow(component, 2).getBoundingClientRect().left + + (editor.horizontalScrollMargin * component.measurements.baseCharacterWidth) - + scroller.clientWidth + ) + expect(scroller.scrollLeft).toBe(expectedScrollLeft) + }) + + it('does not horizontally autoscroll by more than half of the visible "base-width" characters if the editor is narrower than twice the scroll margin', async () => { + const {component, element, editor} = buildComponent() + const {scroller, gutterContainer} = component.refs + element.style.width = + component.getGutterContainerWidth() + + 1.5 * editor.horizontalScrollMargin * component.measurements.baseCharacterWidth + 'px' + await component.getNextUpdatePromise() + + const contentWidth = scroller.clientWidth - gutterContainer.offsetWidth + const contentWidthInCharacters = Math.floor(contentWidth / component.measurements.baseCharacterWidth) + expect(contentWidthInCharacters).toBe(9) + + editor.scrollToScreenRange([[6, 10], [6, 15]]) + await component.getNextUpdatePromise() + let expectedScrollLeft = Math.floor( + clientLeftForCharacter(component, 6, 10) - + lineNodeForScreenRow(component, 1).getBoundingClientRect().left - + (4 * component.measurements.baseCharacterWidth) + ) + expect(scroller.scrollLeft).toBe(expectedScrollLeft) + }) }) }) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index acb91e4f4..fbb176684 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -31,7 +31,7 @@ class TextEditorComponent { this.textNodesByScreenLineId = new Map() this.pendingAutoscroll = null this.autoscrollTop = null - this.scrollWidthOrHeightChanged = false + this.contentWidthOrHeightChanged = false this.previousScrollWidth = 0 this.previousScrollHeight = 0 this.lastKeydown = null @@ -69,26 +69,22 @@ class TextEditorComponent { this.resolveNextUpdatePromise = null } - if (this.scrollWidthOrHeightChanged) this.measureClientDimensions() + this.horizontalPositionsToMeasure.clear() + if (this.contentWidthOrHeightChanged) this.measureClientDimensions() + if (this.pendingAutoscroll) this.initiateAutoscroll() this.populateVisibleRowRange() const longestLineToMeasure = this.checkForNewLongestLine() - if (this.pendingAutoscroll) this.autoscrollVertically() - this.horizontalPositionsToMeasure.clear() + this.queryCursorsToRender() etch.updateSync(this) - if (this.autoscrollTop != null) { - this.refs.scroller.scrollTop = this.autoscrollTop - this.autoscrollTop = null - } - if (longestLineToMeasure) this.measureLongestLineWidth(longestLineToMeasure) - this.queryCursorsToRender() this.measureHorizontalPositions() + if (longestLineToMeasure) this.measureLongestLineWidth(longestLineToMeasure) + if (this.pendingAutoscroll) this.finalizeAutoscroll() this.positionCursorsToRender() etch.updateSync(this) - this.pendingAutoscroll = null this.currentFrameLineNumberGutterProps = null } @@ -198,15 +194,15 @@ class TextEditorComponent { overflow: 'hidden' } if (this.measurements) { - const scrollWidth = this.getScrollWidth() + const contentWidth = this.getContentWidth() const scrollHeight = this.getScrollHeight() - if (scrollWidth !== this.previousScrollWidth || scrollHeight !== this.previousScrollHeight) { - this.scrollWidthOrHeightChanged = true - this.previousScrollWidth = scrollWidth + if (contentWidth !== this.previousScrollWidth || scrollHeight !== this.previousScrollHeight) { + this.contentWidthOrHeightChanged = true + this.previousScrollWidth = contentWidth this.previousScrollHeight = scrollHeight } - const width = scrollWidth + 'px' + const width = contentWidth + 'px' const height = scrollHeight + 'px' style.width = width style.height = height @@ -238,7 +234,7 @@ class TextEditorComponent { // const lastTileStartRow = this.getLastTileStartRow() const rowsPerTile = this.getRowsPerTile() const tileHeight = this.measurements.lineHeight * rowsPerTile - const tileWidth = this.getScrollWidth() + const tileWidth = this.getContentWidth() const displayLayer = this.getModel().displayLayer const screenLines = displayLayer.getScreenLines(startRow, endRow) @@ -565,13 +561,16 @@ class TextEditorComponent { this.scheduleUpdate() } - autoscrollVertically () { + initiateAutoscroll () { const {screenRange, options} = this.pendingAutoscroll const screenRangeTop = this.pixelTopForScreenRow(screenRange.start.row) const screenRangeBottom = this.pixelTopForScreenRow(screenRange.end.row) + this.measurements.lineHeight const verticalScrollMargin = this.getVerticalScrollMargin() + this.requestHorizontalMeasurement(screenRange.start.row, screenRange.start.column) + this.requestHorizontalMeasurement(screenRange.end.row, screenRange.end.column) + let desiredScrollTop, desiredScrollBottom if (options && options.center) { const desiredScrollCenter = (screenRangeTop + screenRangeBottom) / 2 @@ -595,22 +594,65 @@ class TextEditorComponent { if (!options || options.reversed !== false) { if (desiredScrollBottom > this.getScrollBottom()) { this.autoscrollTop = desiredScrollBottom - this.measurements.clientHeight + this.measurements.scrollTop = this.autoscrollTop } if (desiredScrollTop < this.getScrollTop()) { this.autoscrollTop = desiredScrollTop + this.measurements.scrollTop = this.autoscrollTop } } else { if (desiredScrollTop < this.getScrollTop()) { this.autoscrollTop = desiredScrollTop + this.measurements.scrollTop = this.autoscrollTop } if (desiredScrollBottom > this.getScrollBottom()) { this.autoscrollTop = desiredScrollBottom - this.measurements.clientHeight + this.measurements.scrollTop = this.autoscrollTop + } + } + } + + finalizeAutoscroll () { + const horizontalScrollMargin = this.getHorizontalScrollMargin() + + const {screenRange, options} = this.pendingAutoscroll + const gutterContainerWidth = this.getGutterContainerWidth() + let left = this.pixelLeftForScreenRowAndColumn(screenRange.start.row, screenRange.start.column) + gutterContainerWidth + let right = this.pixelLeftForScreenRowAndColumn(screenRange.end.row, screenRange.end.column) + gutterContainerWidth + const desiredScrollLeft = Math.max(0, left - horizontalScrollMargin - gutterContainerWidth) + const desiredScrollRight = Math.min(this.getScrollWidth(), right + horizontalScrollMargin) + + let autoscrollLeft + if (!options || options.reversed !== false) { + if (desiredScrollRight > this.getScrollRight()) { + autoscrollLeft = desiredScrollRight - this.getClientWidth() + this.measurements.scrollLeft = autoscrollLeft + } + if (desiredScrollLeft < this.getScrollLeft()) { + autoscrollLeft = desiredScrollLeft + this.measurements.scrollLeft = autoscrollLeft + } + } else { + if (desiredScrollLeft < this.getScrollLeft()) { + autoscrollLeft = desiredScrollLeft + this.measurements.scrollLeft = autoscrollLeft + } + if (desiredScrollRight > this.getScrollRight()) { + autoscrollLeft = desiredScrollRight - this.getClientWidth() + this.measurements.scrollLeft = autoscrollLeft } } if (this.autoscrollTop != null) { - this.measurements.scrollTop = this.autoscrollTop + this.refs.scroller.scrollTop = this.autoscrollTop + this.autoscrollTop = null } + + if (autoscrollLeft != null) { + this.refs.scroller.scrollLeft = autoscrollLeft + } + + this.pendingAutoscroll = null } getVerticalScrollMargin () { @@ -619,7 +661,17 @@ class TextEditorComponent { this.getModel().verticalScrollMargin, Math.floor(((clientHeight / lineHeight) - 1) / 2) ) - return marginInLines * this.measurements.lineHeight + return marginInLines * lineHeight + } + + getHorizontalScrollMargin () { + const {clientWidth, baseCharacterWidth} = this.measurements + const contentClientWidth = clientWidth - this.getGutterContainerWidth() + const marginInBaseCharacters = Math.min( + this.getModel().horizontalScrollMargin, + Math.floor(((contentClientWidth / baseCharacterWidth) - 1) / 2) + ) + return marginInBaseCharacters * baseCharacterWidth } performInitialMeasurements () { @@ -671,7 +723,7 @@ class TextEditorComponent { this.measurements.clientWidth = clientWidth clientDimensionsChanged = true } - this.scrollWidthOrHeightChanged = false + this.contentWidthOrHeightChanged = false return clientDimensionsChanged } @@ -743,6 +795,9 @@ class TextEditorComponent { if (nextColumnToMeasure === 0) { positions.set(0, 0) continue columnLoop + } + if (nextColumnToMeasure >= lineNode.textContent.length) { + } if (positions.has(nextColumnToMeasure)) continue columnLoop const textNode = textNodes[textNodesIndex] @@ -815,7 +870,7 @@ class TextEditorComponent { getScrollBottom () { return this.measurements - ? this.getScrollTop() + this.measurements.clientHeight + ? this.measurements.scrollTop + this.measurements.clientHeight : null } @@ -823,18 +878,36 @@ class TextEditorComponent { return this.measurements ? this.measurements.scrollLeft : null } + getScrollRight () { + return this.measurements + ? this.measurements.scrollLeft + this.measurements.clientWidth + : null + } + getScrollHeight () { return this.getModel().getApproximateScreenLineCount() * this.measurements.lineHeight } getScrollWidth () { - return Math.round(this.measurements.longestLineWidth + this.measurements.baseCharacterWidth) + return this.getContentWidth() + this.getGutterContainerWidth() } getClientHeight () { return this.measurements.clientHeight } + getClientWidth () { + return this.measurements.clientWidth + } + + getGutterContainerWidth () { + return this.measurements.lineNumberGutterWidth + } + + getContentWidth () { + return Math.round(this.measurements.longestLineWidth + this.measurements.baseCharacterWidth) + } + getRowsPerTile () { return this.props.rowsPerTile || DEFAULT_ROWS_PER_TILE } diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 737e33ed9..6fa67400d 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -3426,6 +3426,7 @@ class TextEditor extends Model @getElement().scrollToBottom() scrollToScreenRange: (screenRange, options = {}) -> + screenRange = @clipScreenRange(screenRange) scrollEvent = {screenRange, options} @emitter.emit "did-request-autoscroll", scrollEvent