diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 9cc66fa20..a23cb5e00 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -28,23 +28,23 @@ describe('TextEditorComponent', () => { it('renders lines and line numbers for the visible region', async () => { const {component, element, editor} = buildComponent({rowsPerTile: 3, autoHeight: false}) - expect(element.querySelectorAll('.line-number').length).toBe(13 + 1) // +1 for placeholder line number - expect(element.querySelectorAll('.line').length).toBe(13) + expect(element.querySelectorAll('.line-number:not(.dummy)').length).toBe(13) + expect(element.querySelectorAll('.line:not(.dummy)').length).toBe(13) element.style.height = 4 * component.measurements.lineHeight + 'px' await component.getNextUpdatePromise() - expect(element.querySelectorAll('.line-number').length).toBe(9 + 1) // +1 for placeholder line number - expect(element.querySelectorAll('.line').length).toBe(9) + expect(element.querySelectorAll('.line-number:not(.dummy)').length).toBe(9) + expect(element.querySelectorAll('.line:not(.dummy)').length).toBe(9) await setScrollTop(component, 5 * component.getLineHeight()) // After scrolling down beyond > 3 rows, the order of line numbers and lines // in the DOM is a bit weird because the first tile is recycled to the bottom // when it is scrolled out of view - expect(Array.from(element.querySelectorAll('.line-number')).slice(1).map(element => element.textContent.trim())).toEqual([ + expect(Array.from(element.querySelectorAll('.line-number:not(.dummy)')).map(element => element.textContent.trim())).toEqual([ '10', '11', '12', '4', '5', '6', '7', '8', '9' ]) - expect(Array.from(element.querySelectorAll('.line')).map(element => element.textContent)).toEqual([ + expect(Array.from(element.querySelectorAll('.line:not(.dummy)')).map(element => element.textContent)).toEqual([ editor.lineTextForScreenRow(9), ' ', // this line is blank in the model, but we render a space to prevent the line from collapsing vertically editor.lineTextForScreenRow(11), @@ -57,10 +57,10 @@ describe('TextEditorComponent', () => { ]) await setScrollTop(component, 2.5 * component.getLineHeight()) - expect(Array.from(element.querySelectorAll('.line-number')).slice(1).map(element => element.textContent.trim())).toEqual([ + expect(Array.from(element.querySelectorAll('.line-number:not(.dummy)')).map(element => element.textContent.trim())).toEqual([ '1', '2', '3', '4', '5', '6', '7', '8', '9' ]) - expect(Array.from(element.querySelectorAll('.line')).map(element => element.textContent)).toEqual([ + expect(Array.from(element.querySelectorAll('.line:not(.dummy)')).map(element => element.textContent)).toEqual([ editor.lineTextForScreenRow(0), editor.lineTextForScreenRow(1), editor.lineTextForScreenRow(2), @@ -2191,6 +2191,53 @@ describe('TextEditorComponent', () => { }) }) }) + + describe('styling changes', () => { + it('updates the rendered content based on new measurements when the font dimensions change', async () => { + const {component, element, editor} = buildComponent({rowsPerTile: 1, autoHeight: false}) + await setEditorHeightInLines(component, 3) + editor.setCursorScreenPosition([1, 29], {autoscroll: false}) + await component.getNextUpdatePromise() + + let cursorNode = element.querySelector('.cursor') + const initialBaseCharacterWidth = editor.getDefaultCharWidth() + const initialDoubleCharacterWidth = editor.getDoubleWidthCharWidth() + const initialHalfCharacterWidth = editor.getHalfWidthCharWidth() + const initialKoreanCharacterWidth = editor.getKoreanCharWidth() + const initialRenderedLineCount = element.querySelectorAll('.line:not(.dummy)').length + const initialFontSize = parseInt(getComputedStyle(element).fontSize) + + expect(initialKoreanCharacterWidth).toBeDefined() + expect(initialDoubleCharacterWidth).toBeDefined() + expect(initialHalfCharacterWidth).toBeDefined() + expect(initialBaseCharacterWidth).toBeDefined() + expect(initialDoubleCharacterWidth).not.toBe(initialBaseCharacterWidth) + expect(initialHalfCharacterWidth).not.toBe(initialBaseCharacterWidth) + expect(initialKoreanCharacterWidth).not.toBe(initialBaseCharacterWidth) + verifyCursorPosition(component, cursorNode, 1, 29) + + console.log(initialFontSize); + element.style.fontSize = initialFontSize - 5 + 'px' + TextEditor.didUpdateStyles() + await component.getNextUpdatePromise() + expect(editor.getDefaultCharWidth()).toBeLessThan(initialBaseCharacterWidth) + expect(editor.getDoubleWidthCharWidth()).toBeLessThan(initialDoubleCharacterWidth) + expect(editor.getHalfWidthCharWidth()).toBeLessThan(initialHalfCharacterWidth) + expect(editor.getKoreanCharWidth()).toBeLessThan(initialKoreanCharacterWidth) + expect(element.querySelectorAll('.line:not(.dummy)').length).toBeGreaterThan(initialRenderedLineCount) + verifyCursorPosition(component, cursorNode, 1, 29) + + element.style.fontSize = initialFontSize + 5 + 'px' + TextEditor.didUpdateStyles() + await component.getNextUpdatePromise() + expect(editor.getDefaultCharWidth()).toBeGreaterThan(initialBaseCharacterWidth) + expect(editor.getDoubleWidthCharWidth()).toBeGreaterThan(initialDoubleCharacterWidth) + expect(editor.getHalfWidthCharWidth()).toBeGreaterThan(initialHalfCharacterWidth) + expect(editor.getKoreanCharWidth()).toBeGreaterThan(initialKoreanCharacterWidth) + expect(element.querySelectorAll('.line:not(.dummy)').length).toBeLessThan(initialRenderedLineCount) + verifyCursorPosition(component, cursorNode, 1, 29) + }) + }) }) function buildEditor (params = {}) { @@ -2227,7 +2274,7 @@ function getBaseCharacterWidth (component) { } async function setEditorHeightInLines(component, heightInLines) { - component.element.style.height = component.measurements.lineHeight * heightInLines + 'px' + component.element.style.height = component.getLineHeight() * heightInLines + 'px' await component.getNextUpdatePromise() } diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 1a6dd6cbe..06d5331ff 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -805,6 +805,7 @@ class AtomEnvironment extends Model @windowEventHandler = null didChangeStyles: (styleElement) -> + TextEditor.didUpdateStyles() if styleElement.textContent.indexOf('scrollbar') >= 0 TextEditor.didUpdateScrollbarStyles() diff --git a/src/text-editor-component.js b/src/text-editor-component.js index dbfb9f198..9d51bbd40 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -29,6 +29,13 @@ const BLOCK_DECORATION_MEASUREMENT_AREA_VNODE = $.div({ visibility: 'hidden' } }) +const CHARACTER_MEASUREMENT_LINE_VNODE = $.div( + {key: 'characterMeasurementLine', ref: 'characterMeasurementLine', className: 'line dummy'}, + $.span({ref: 'normalWidthCharacterSpan'}, NORMAL_WIDTH_CHARACTER), + $.span({ref: 'doubleWidthCharacterSpan'}, DOUBLE_WIDTH_CHARACTER), + $.span({ref: 'halfWidthCharacterSpan'}, HALF_WIDTH_CHARACTER), + $.span({ref: 'koreanCharacterSpan'}, KOREAN_CHARACTER) +) function scaleMouseDragAutoscrollDelta (delta) { return Math.pow(delta / 3, 3) / 280 @@ -40,6 +47,14 @@ class TextEditorComponent { etch.setScheduler(scheduler) } + static didUpdateStyles () { + if (this.attachedComponents) { + this.attachedComponents.forEach((component) => { + component.didUpdateStyles() + }) + } + } + static didUpdateScrollbarStyles () { if (this.attachedComponents) { this.attachedComponents.forEach((component) => { @@ -79,7 +94,7 @@ class TextEditorComponent { this.lineNodesByScreenLineId = new Map() this.textNodesByScreenLineId = new Map() this.shouldRenderDummyScrollbars = true - this.refreshedScrollbarStyle = false + this.remeasureScrollbars = false this.pendingAutoscroll = null this.scrollTopPending = false this.scrollLeftPending = false @@ -161,6 +176,12 @@ class TextEditorComponent { return } + if (this.remeasureCharacterDimensions) { + this.measureCharacterDimensions() + this.measureGutterDimensions() + this.remeasureCharacterDimensions = false + } + this.measureBlockDecorations() this.measuredContent = false @@ -254,7 +275,7 @@ class TextEditorComponent { this.queryLineNumbersToRender() this.queryGuttersToRender() this.queryDecorationsToRender() - this.shouldRenderDummyScrollbars = !this.refreshedScrollbarStyle + this.shouldRenderDummyScrollbars = !this.remeasureScrollbars etch.updateSync(this) this.shouldRenderDummyScrollbars = true this.didMeasureVisibleBlockDecoration = false @@ -286,9 +307,9 @@ class TextEditorComponent { this.currentFrameLineNumberGutterProps = null this.scrollTopPending = false this.scrollLeftPending = false - if (this.refreshedScrollbarStyle) { + if (this.remeasureScrollbars) { this.measureScrollbarDimensions() - this.refreshedScrollbarStyle = false + this.remeasureScrollbars = false etch.updateSync(this) } } @@ -480,17 +501,13 @@ class TextEditorComponent { this.renderCursorsAndInput(), this.renderLineTiles(), BLOCK_DECORATION_MEASUREMENT_AREA_VNODE, + CHARACTER_MEASUREMENT_LINE_VNODE, this.renderPlaceholderText() ] } else { children = [ BLOCK_DECORATION_MEASUREMENT_AREA_VNODE, - $.div({ref: 'characterMeasurementLine', className: 'line'}, - $.span({ref: 'normalWidthCharacterSpan'}, NORMAL_WIDTH_CHARACTER), - $.span({ref: 'doubleWidthCharacterSpan'}, DOUBLE_WIDTH_CHARACTER), - $.span({ref: 'halfWidthCharacterSpan'}, HALF_WIDTH_CHARACTER), - $.span({ref: 'koreanCharacterSpan'}, KOREAN_CHARACTER) - ) + CHARACTER_MEASUREMENT_LINE_VNODE ] } @@ -505,8 +522,6 @@ class TextEditorComponent { } renderLineTiles () { - if (!this.measurements) return [] - const {lineNodesByScreenLineId, textNodesByScreenLineId} = this const startRow = this.getRenderedStartRow() @@ -676,7 +691,7 @@ class TextEditorComponent { this.isVerticalScrollbarVisible() ? this.getVerticalScrollbarWidth() : 0 - forceScrollbarVisible = this.refreshedScrollbarStyle + forceScrollbarVisible = this.remeasureScrollbars } else { forceScrollbarVisible = true } @@ -1117,7 +1132,7 @@ class TextEditorComponent { } didShow () { - if (!this.visible) { + if (!this.visible && this.isVisible()) { this.visible = true if (!this.measurements) this.performInitialMeasurements() this.props.model.setVisible(true) @@ -1235,8 +1250,14 @@ class TextEditorComponent { if (scrollTopChanged || scrollLeftChanged) this.updateSync() } + didUpdateStyles () { + this.remeasureCharacterDimensions = true + this.horizontalPixelPositionsByScreenLineId.clear() + this.scheduleUpdate() + } + didUpdateScrollbarStyles () { - this.refreshedScrollbarStyle = true + this.remeasureScrollbars = true this.scheduleUpdate() } @@ -1680,7 +1701,7 @@ class TextEditorComponent { this.measurements.baseCharacterWidth = this.refs.normalWidthCharacterSpan.getBoundingClientRect().width this.measurements.doubleWidthCharacterWidth = this.refs.doubleWidthCharacterSpan.getBoundingClientRect().width this.measurements.halfWidthCharacterWidth = this.refs.halfWidthCharacterSpan.getBoundingClientRect().width - this.measurements.koreanCharacterWidth = this.refs.koreanCharacterSpan.getBoundingClientRect().widt + this.measurements.koreanCharacterWidth = this.refs.koreanCharacterSpan.getBoundingClientRect().width this.props.model.setDefaultCharWidth( this.measurements.baseCharacterWidth, @@ -2444,7 +2465,7 @@ class LineNumberGutterComponent { mousedown: this.didMouseDown }, }, - $.div({key: 'placeholder', className: 'line-number', style: {visibility: 'hidden'}}, + $.div({key: 'placeholder', className: 'line-number dummy', style: {visibility: 'hidden'}}, '0'.repeat(maxDigits), $.div({className: 'icon-right'}) ), diff --git a/src/text-editor.coffee b/src/text-editor.coffee index bf3979a36..7196e2118 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -65,6 +65,10 @@ class TextEditor extends Model TextEditorComponent ?= require './text-editor-component' TextEditorComponent.setScheduler(scheduler) + @didUpdateStyles: -> + TextEditorComponent ?= require './text-editor-component' + TextEditorComponent.didUpdateStyles() + @didUpdateScrollbarStyles: -> TextEditorComponent ?= require './text-editor-component' TextEditorComponent.didUpdateScrollbarStyles()