diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index e44dac272..10043a185 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -86,6 +86,31 @@ describe('TextEditorComponent', () => { // TODO: Confirm that we'll update this value as indexing proceeds }) + it('honors the scrollPastEnd option by adding empty space equivalent to the clientHeight to the end of the content area', async () => { + const {component, element, editor} = buildComponent() + const {scroller} = component.refs + + await editor.update({scrollPastEnd: true}) + await setEditorHeightInLines(component, 6) + + // scroll to end + scroller.scrollTop = scroller.scrollHeight - scroller.clientHeight + await component.getNextUpdatePromise() + expect(component.getFirstVisibleRow()).toBe(editor.getScreenLineCount() - 3) + + editor.update({scrollPastEnd: false}) + await component.getNextUpdatePromise() // wait for scrollable content resize + await component.getNextUpdatePromise() // wait for async scroll event due to scrollbar shrinking + expect(component.getFirstVisibleRow()).toBe(editor.getScreenLineCount() - 6) + + // Always allows at least 3 lines worth of overscroll if the editor is short + await setEditorHeightInLines(component, 2) + await editor.update({scrollPastEnd: true}) + scroller.scrollTop = scroller.scrollHeight - scroller.clientHeight + await component.getNextUpdatePromise() + expect(component.getFirstVisibleRow()).toBe(editor.getScreenLineCount() + 1) + }) + it('gives the line number gutter an explicit width and height so its layout can be strictly contained', () => { const {component, element, editor} = buildComponent({rowsPerTile: 3}) @@ -187,7 +212,7 @@ describe('TextEditorComponent', () => { ' right = [];' ) - await setBaseCharacterWidth(component, 45) + await setEditorWidthInCharacters(component, 45) expect(lineNodeForScreenRow(component, 3).textContent).toBe( ' var pivot = items.shift(), current, left ' ) @@ -331,7 +356,7 @@ describe('TextEditorComponent', () => { 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 - await setBaseCharacterWidth(component, 1.5 * editor.horizontalScrollMargin) + await setEditorWidthInCharacters(component, 1.5 * editor.horizontalScrollMargin) const contentWidth = scroller.clientWidth - gutterContainer.offsetWidth const contentWidthInCharacters = Math.floor(contentWidth / component.measurements.baseCharacterWidth) @@ -591,7 +616,12 @@ function getBaseCharacterWidth (component) { ) } -async function setBaseCharacterWidth (component, widthInCharacters) { +async function setEditorHeightInLines(component, heightInLines) { + component.element.style.height = component.measurements.lineHeight * heightInLines + 'px' + await component.getNextUpdatePromise() +} + +async function setEditorWidthInCharacters (component, widthInCharacters) { component.element.style.width = component.getGutterContainerWidth() + widthInCharacters * component.measurements.baseCharacterWidth + diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 491c82657..aae13af36 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.contentWidthOrHeightChanged = false + this.contentDimensionsChanged = false this.previousScrollWidth = 0 this.previousScrollHeight = 0 this.lastKeydown = null @@ -60,6 +60,8 @@ class TextEditorComponent { } scheduleUpdate () { + if (!this.visible) return + if (this.updatedSynchronously) { this.updateSync() } else if (!this.updateScheduled) { @@ -79,7 +81,7 @@ class TextEditorComponent { } this.horizontalPositionsToMeasure.clear() - if (this.contentWidthOrHeightChanged) this.measureClientDimensions() + if (this.contentDimensionsChanged) this.measureClientDimensions() if (this.pendingAutoscroll) this.initiateAutoscroll() this.populateVisibleRowRange() const longestLineToMeasure = this.checkForNewLongestLine() @@ -175,7 +177,7 @@ class TextEditorComponent { if (this.measurements) { const startRow = this.getRenderedStartRow() const endRow = this.getRenderedEndRow() - const renderedRowCount = endRow - startRow + const renderedRowCount = this.getRenderedRowCount() const bufferRows = new Array(renderedRowCount) const foldableFlags = new Array(renderedRowCount) const softWrappedFlags = new Array(renderedRowCount) @@ -231,7 +233,7 @@ class TextEditorComponent { const contentWidth = this.getContentWidth() const scrollHeight = this.getScrollHeight() if (contentWidth !== this.previousScrollWidth || scrollHeight !== this.previousScrollHeight) { - this.contentWidthOrHeightChanged = true + this.contentDimensionsChanged = true this.previousScrollWidth = contentWidth this.previousScrollHeight = scrollHeight } @@ -866,7 +868,7 @@ class TextEditorComponent { this.getModel().setWidth(clientWidth - this.getGutterContainerWidth(), true) clientDimensionsChanged = true } - this.contentWidthOrHeightChanged = false + this.contentDimensionsChanged = false return clientDimensionsChanged } @@ -1006,6 +1008,7 @@ class TextEditorComponent { observeModel () { const {model} = this.props + model.component = this const scheduleUpdate = this.scheduleUpdate.bind(this) this.disposables.add(model.selectionsMarkerLayer.onDidUpdate(scheduleUpdate)) this.disposables.add(model.displayLayer.onDidChangeSync(scheduleUpdate)) @@ -1044,7 +1047,17 @@ class TextEditorComponent { } getScrollHeight () { - return this.getModel().getApproximateScreenLineCount() * this.measurements.lineHeight + const model = this.getModel() + const contentHeight = model.getApproximateScreenLineCount() * this.measurements.lineHeight + if (model.getScrollPastEnd()) { + const extraScrollHeight = Math.max( + 3 * this.measurements.lineHeight, + this.getClientHeight() - 3 * this.measurements.lineHeight + ) + return contentHeight + extraScrollHeight + } else { + return contentHeight + } } getScrollWidth () { @@ -1098,8 +1111,12 @@ class TextEditorComponent { ) } + getRenderedRowCount () { + return Math.max(0, this.getRenderedEndRow() - this.getRenderedStartRow()) + } + getRenderedTileCount () { - return Math.ceil((this.getRenderedEndRow() - this.getRenderedStartRow()) / this.getRowsPerTile()) + return Math.ceil(this.getRenderedRowCount() / this.getRowsPerTile()) } getFirstVisibleRow () { diff --git a/src/text-editor.coffee b/src/text-editor.coffee index b1d281abb..4b1a510e2 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -344,7 +344,7 @@ class TextEditor extends Model when 'scrollPastEnd' if value isnt @scrollPastEnd @scrollPastEnd = value - @presenter?.didChangeScrollPastEnd() + @component?.scheduleUpdate() when 'autoHeight' if value isnt @autoHeight @@ -367,8 +367,8 @@ class TextEditor extends Model @displayLayer.reset(displayLayerParams) - if @editorElement? - @editorElement.views.getNextUpdatePromise() + if @component? + @component.getNextUpdatePromise() else Promise.resolve() @@ -3541,7 +3541,7 @@ class TextEditor extends Model # Get the Element for the editor. getElement: -> - @component ?= new TextEditorComponent({model: this}) + new TextEditorComponent({model: this}) @component.element # Essential: Retrieves the greyed out placeholder of a mini editor.