diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 82764c438..fa72e42ef 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -286,6 +286,31 @@ describe('TextEditorComponent', () => { expect(lineNumberNodeForScreenRow(component, 0).querySelector('.foldable')).toBeNull() }) + it('gracefully handles folds that change the soft-wrap boundary by causing the vertical scrollbar to disappear (regression)', async () => { + const text = ('x'.repeat(100) + '\n') + 'y\n'.repeat(28) + ' z\n'.repeat(50) + const {component, element, editor} = buildComponent({text, height: 1000, width: 500}) + + element.addEventListener('scroll', (event) => { + event.stopPropagation() + }, true) + + editor.setSoftWrapped(true) + jasmine.attachToDOM(element) + await component.getNextUpdatePromise() + + const firstScreenLineLengthWithVerticalScrollbar = element.querySelector('.line').textContent.length + + setScrollTop(component, 620) + await component.getNextUpdatePromise() + + editor.foldBufferRow(28) + await component.getNextUpdatePromise() + + const firstLineElement = element.querySelector('.line') + expect(firstLineElement.dataset.screenRow).toBe('0') + expect(firstLineElement.textContent.length).toBeGreaterThan(firstScreenLineLengthWithVerticalScrollbar) + }) + it('shows the foldable icon on the last screen row of a buffer row that can be folded', async () => { const {component, element, editor} = buildComponent({text: 'abc\n de\nfghijklm\n no', softWrapped: true}) await setEditorWidthInCharacters(component, 5) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 5c3b6d1bc..3060b6857 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -362,7 +362,7 @@ class TextEditorComponent { this.requestHorizontalMeasurement(screenRange.start.row, screenRange.start.column) this.requestHorizontalMeasurement(screenRange.end.row, screenRange.end.column) } - this.populateVisibleRowRange() + this.populateVisibleRowRange(this.getRenderedStartRow()) this.populateVisibleTiles() this.queryScreenLinesToRender() this.queryLongestLine() @@ -2096,14 +2096,29 @@ class TextEditorComponent { return marginInBaseCharacters * this.getBaseCharacterWidth() } + // This method is called at the beginning of a frame render to relay any + // potential changes in the editor's width into the model before proceeding. updateModelSoftWrapColumn () { const {model} = this.props const newEditorWidthInChars = this.getScrollContainerClientWidthInBaseCharacters() if (newEditorWidthInChars !== model.getEditorWidthInChars()) { this.suppressUpdates = true + + const renderedStartRow = this.getRenderedStartRow() this.props.model.setEditorWidthInChars(newEditorWidthInChars) - // Wrapping may cause a vertical scrollbar to appear, which will change the width again. + + // Relaying a change in to the editor's client width may cause the + // vertical scrollbar to appear or disappear, which causes the editor's + // client width to change *again*. Make sure the display layer is fully + // populated for the visible area before recalculating the editor's + // width in characters. Then update the display layer *again* just in + // case a change in scrollbar visibility causes lines to wrap + // differently. We capture the renderedStartRow before resetting the + // display layer because once it has been reset, we can't compute the + // rendered start row accurately. 😥 + this.populateVisibleRowRange(renderedStartRow) this.props.model.setEditorWidthInChars(this.getScrollContainerClientWidthInBaseCharacters()) + this.suppressUpdates = false } } @@ -2867,12 +2882,11 @@ class TextEditorComponent { } } - // Ensure the spatial index is populated with rows that are currently - // visible so we *at least* get the longest row in the visible range. - populateVisibleRowRange () { + // Ensure the spatial index is populated with rows that are currently visible + populateVisibleRowRange (renderedStartRow) { const editorHeightInTiles = this.getScrollContainerHeight() / this.getLineHeight() const visibleTileCount = Math.ceil(editorHeightInTiles) + 1 - const lastRenderedRow = this.getRenderedStartRow() + (visibleTileCount * this.getRowsPerTile()) + const lastRenderedRow = renderedStartRow + (visibleTileCount * this.getRowsPerTile()) this.props.model.displayLayer.populateSpatialIndexIfNeeded(Infinity, lastRenderedRow) }