WIP: Render gutters before initial measurement

The shouldUpdate method is just returning true for now. We probably need
to find a new approach to representing line number decorations that's
easier to diff, perhaps a sparse array?
This commit is contained in:
Nathan Sobo
2017-03-24 17:59:16 -06:00
committed by Antonio Scandurra
parent d5d3cfc5a9
commit 4e834da3e3
3 changed files with 188 additions and 164 deletions

View File

@@ -1,6 +1,4 @@
/** @babel */
import {it, fit, ffit, fffit, beforeEach, afterEach, conditionPromise} from './async-spec-helpers'
const {it, fit, ffit, fffit, beforeEach, afterEach, conditionPromise} = require('./async-spec-helpers')
const TextEditorComponent = require('../src/text-editor-component')
const TextEditor = require('../src/text-editor')
@@ -21,7 +19,7 @@ document.registerElement('text-editor-component-test-element', {
})
})
describe('TextEditorComponent', () => {
fdescribe('TextEditorComponent', () => {
beforeEach(() => {
jasmine.useRealClock()
})
@@ -30,12 +28,12 @@ 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)
expect(element.querySelectorAll('.line-number').length).toBe(13 + 1) // +1 for placeholder line number
expect(element.querySelectorAll('.line').length).toBe(13)
element.style.height = 4 * component.measurements.lineHeight + 'px'
await component.getNextUpdatePromise()
expect(element.querySelectorAll('.line-number').length).toBe(9)
expect(element.querySelectorAll('.line-number').length).toBe(9 + 1) // +1 for placeholder line number
expect(element.querySelectorAll('.line').length).toBe(9)
await setScrollTop(component, 5 * component.getLineHeight())
@@ -43,7 +41,7 @@ describe('TextEditorComponent', () => {
// 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')).map(element => element.textContent.trim())).toEqual([
expect(Array.from(element.querySelectorAll('.line-number')).slice(1).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([
@@ -59,7 +57,7 @@ describe('TextEditorComponent', () => {
])
await setScrollTop(component, 2.5 * component.getLineHeight())
expect(Array.from(element.querySelectorAll('.line-number')).map(element => element.textContent.trim())).toEqual([
expect(Array.from(element.querySelectorAll('.line-number')).slice(1).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([
@@ -107,18 +105,19 @@ describe('TextEditorComponent', () => {
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', () => {
it('gives the line number tiles an explicit width and height so their layout can be strictly contained', async () => {
const {component, element, editor} = buildComponent({rowsPerTile: 3})
const gutterElement = element.querySelector('.gutter.line-numbers')
expect(gutterElement.style.width).toBe(element.querySelector('.line-number').offsetWidth + 'px')
expect(gutterElement.style.height).toBe(editor.getScreenLineCount() * component.measurements.lineHeight + 'px')
expect(gutterElement.style.contain).toBe('strict')
const gutterElement = component.refs.lineNumberGutter.element
for (const child of gutterElement.children) {
expect(child.offsetWidth).toBe(gutterElement.offsetWidth)
}
// Tile nodes also have explicit width and height assignment
expect(gutterElement.firstChild.style.width).toBe(element.querySelector('.line-number').offsetWidth + 'px')
expect(gutterElement.firstChild.style.height).toBe(3 * component.measurements.lineHeight + 'px')
expect(gutterElement.firstChild.style.contain).toBe('strict')
editor.setText('\n'.repeat(99))
await component.getNextUpdatePromise()
for (const child of gutterElement.children) {
expect(child.offsetWidth).toBe(gutterElement.offsetWidth)
}
})
it('renders dummy vertical and horizontal scrollbars when content overflows', async () => {
@@ -994,26 +993,30 @@ describe('TextEditorComponent', () => {
const {component, element, editor} = buildComponent()
const {scrollContainer, gutterContainer} = component.refs
expect(scrollContainer.getBoundingClientRect().left).toBe(gutterContainer.getBoundingClientRect().right)
function checkScrollContainerLeft () {
expect(scrollContainer.getBoundingClientRect().left).toBe(Math.round(gutterContainer.getBoundingClientRect().right))
}
checkScrollContainerLeft()
const gutterA = editor.addGutter({name: 'a'})
await component.getNextUpdatePromise()
expect(scrollContainer.getBoundingClientRect().left).toBe(gutterContainer.getBoundingClientRect().right)
checkScrollContainerLeft()
const gutterB = editor.addGutter({name: 'b'})
await component.getNextUpdatePromise()
expect(scrollContainer.getBoundingClientRect().left).toBe(gutterContainer.getBoundingClientRect().right)
checkScrollContainerLeft()
gutterA.getElement().style.width = 100 + 'px'
await component.getNextUpdatePromise()
expect(scrollContainer.getBoundingClientRect().left).toBe(gutterContainer.getBoundingClientRect().right)
checkScrollContainerLeft()
gutterA.destroy()
await component.getNextUpdatePromise()
expect(scrollContainer.getBoundingClientRect().left).toBe(gutterContainer.getBoundingClientRect().right)
checkScrollContainerLeft()
gutterB.destroy()
await component.getNextUpdatePromise()
expect(scrollContainer.getBoundingClientRect().left).toBe(gutterContainer.getBoundingClientRect().right)
checkScrollContainerLeft()
})
it('allows the element of custom gutters to be retrieved before being rendered in the editor component', async () => {
@@ -1858,7 +1861,7 @@ function lineNumberNodeForScreenRow (component, row) {
const gutterElement = component.refs.lineNumberGutter.element
const tileStartRow = component.tileStartRowForRow(row)
const tileIndex = component.tileIndexForTileStartRow(tileStartRow)
return gutterElement.children[tileIndex].children[row - tileStartRow]
return gutterElement.children[tileIndex + 1].children[row - tileStartRow]
}
function lineNodeForScreenRow (component, row) {

View File

@@ -74,8 +74,14 @@ class TextEditorComponent {
this.lastKeydown = null
this.lastKeydownBeforeKeypress = null
this.accentedCharacterMenuIsOpen = false
this.remeasureGutterContainer = false
this.guttersToRender = []
this.remeasureGutterDimensions = false
this.guttersToRender = [this.props.model.getLineNumberGutter()]
this.lineNumbersToRender = {
maxDigits: 2,
numbers: [],
keys: [],
foldableFlags: []
}
this.decorationsToRender = {
lineNumbers: new Map(),
lines: new Map(),
@@ -89,6 +95,9 @@ class TextEditorComponent {
cursors: []
}
this.queryGuttersToRender()
this.queryMaxLineNumberDigits()
etch.updateSync(this)
this.observeModel()
@@ -140,6 +149,7 @@ class TextEditorComponent {
if (this.pendingAutoscroll) this.autoscrollVertically()
this.populateVisibleRowRange()
this.queryScreenLinesToRender()
this.queryLineNumbersToRender()
this.queryGuttersToRender()
this.queryDecorationsToRender()
this.shouldRenderDummyScrollbars = !this.refreshedScrollbarStyle
@@ -150,9 +160,9 @@ class TextEditorComponent {
measureContentDuringUpdateSync () {
this.measureHorizontalPositions()
this.updateAbsolutePositionedDecorations()
if (this.remeasureGutterContainer) {
if (this.remeasureGutterDimensions) {
this.measureGutterDimensions()
this.remeasureGutterContainer = false
this.remeasureGutterDimensions = false
}
const wasHorizontalScrollbarVisible = this.isHorizontalScrollbarVisible()
this.measureLongestLineWidth()
@@ -243,25 +253,10 @@ class TextEditorComponent {
display: 'flex'
}
let gutterNodes
let scrollHeight
if (this.measurements) {
innerStyle.transform = `translateY(${-this.getScrollTop()}px)`
gutterNodes = this.guttersToRender.map((gutter) => {
if (gutter.name === 'line-number') {
return this.renderLineNumberGutter(gutter)
} else {
return $(CustomGutterComponent, {
key: gutter,
element: gutter.getElement(),
name: gutter.name,
visible: gutter.isVisible(),
height: this.getScrollHeight(),
decorations: this.decorationsToRender.customGutter.get(gutter.name)
})
}
})
} else {
gutterNodes = this.renderLineNumberGutter()
scrollHeight = this.getScrollHeight()
}
return $.div(
@@ -274,68 +269,52 @@ class TextEditorComponent {
backgroundColor: 'inherit'
}
},
$.div({style: innerStyle}, gutterNodes)
$.div({style: innerStyle},
this.guttersToRender.map((gutter) => {
if (gutter.name === 'line-number') {
return this.renderLineNumberGutter(gutter)
} else {
return $(CustomGutterComponent, {
key: gutter,
element: gutter.getElement(),
name: gutter.name,
visible: gutter.isVisible(),
height: scrollHeight,
decorations: this.decorationsToRender.customGutter.get(gutter.name)
})
}
})
)
)
}
renderLineNumberGutter (gutter) {
const {model} = this.props
if (!model.isLineNumberGutterVisible()) return null
if (this.currentFrameLineNumberGutterProps) {
return $(LineNumberGutterComponent, this.currentFrameLineNumberGutterProps)
}
const maxLineNumberDigits = Math.max(2, model.getLineCount().toString().length)
if (!this.props.model.isLineNumberGutterVisible()) return null
if (this.measurements) {
const startRow = this.getRenderedStartRow()
const endRow = this.getRenderedEndRow()
const renderedRowCount = this.getRenderedRowCount()
const bufferRows = new Array(renderedRowCount)
const foldableFlags = new Array(renderedRowCount)
const softWrappedFlags = new Array(renderedRowCount)
const lineNumberDecorations = new Array(renderedRowCount)
let previousBufferRow = (startRow > 0) ? model.bufferRowForScreenRow(startRow - 1) : -1
for (let row = startRow; row < endRow; row++) {
const i = row - startRow
const bufferRow = model.bufferRowForScreenRow(row)
bufferRows[i] = bufferRow
softWrappedFlags[i] = bufferRow === previousBufferRow
foldableFlags[i] = model.isFoldableAtBufferRow(bufferRow)
lineNumberDecorations[i] = this.decorationsToRender.lineNumbers.get(row)
previousBufferRow = bufferRow
}
const rowsPerTile = this.getRowsPerTile()
this.currentFrameLineNumberGutterProps = {
const {maxDigits, keys, numbers, foldableFlags} = this.lineNumbersToRender
return $(LineNumberGutterComponent, {
ref: 'lineNumberGutter',
element: gutter.getElement(),
parentComponent: this,
startRow: this.getRenderedStartRow(),
endRow: this.getRenderedEndRow(),
rowsPerTile: this.getRowsPerTile(),
maxDigits: maxDigits,
keys: keys,
numbers: numbers,
foldableFlags: foldableFlags,
decorations: this.decorationsToRender.lineNumbers,
height: this.getScrollHeight(),
width: this.getLineNumberGutterWidth(),
lineHeight: this.getLineHeight(),
startRow, endRow, rowsPerTile, maxLineNumberDigits,
bufferRows, lineNumberDecorations, softWrappedFlags,
foldableFlags
}
return $(LineNumberGutterComponent, this.currentFrameLineNumberGutterProps)
})
} else {
return $.div(
{
ref: 'lineNumberGutter',
className: 'gutter line-numbers',
attributes: {'gutter-name': 'line-number'}
},
$.div({className: 'line-number'},
'0'.repeat(maxLineNumberDigits),
$.div({className: 'icon-right'})
)
)
return $(LineNumberGutterComponent, {
ref: 'lineNumberGutter',
element: gutter.getElement(),
maxDigits: this.lineNumbersToRender.maxDigits
})
}
}
@@ -639,6 +618,51 @@ class TextEditorComponent {
}
}
queryLineNumbersToRender () {
const {model} = this.props
if (!model.isLineNumberGutterVisible()) return
this.queryMaxLineNumberDigits()
const startRow = this.getRenderedStartRow()
const endRow = this.getRenderedEndRow()
const renderedRowCount = this.getRenderedRowCount()
const {numbers, keys, foldableFlags} = this.lineNumbersToRender
numbers.length = renderedRowCount
keys.length = renderedRowCount
foldableFlags.length = renderedRowCount
let previousBufferRow = (startRow > 0) ? model.bufferRowForScreenRow(startRow - 1) : -1
let softWrapCount = 0
for (let row = startRow; row < endRow; row++) {
const i = row - startRow
const bufferRow = model.bufferRowForScreenRow(row)
if (bufferRow === previousBufferRow) {
numbers[i] = -1
keys[i] = bufferRow + 1 + '-' + softWrapCount++
foldableFlags[i] = false
} else {
softWrapCount = 0
numbers[i] = bufferRow + 1
keys[i] = bufferRow + 1
foldableFlags[i] = model.isFoldableAtBufferRow(bufferRow)
}
previousBufferRow = bufferRow
}
}
queryMaxLineNumberDigits () {
const {model} = this.props
if (model.isLineNumberGutterVisible()) {
const maxDigits = Math.max(2, model.getLineCount().toString().length)
if (maxDigits !== this.lineNumbersToRender.maxDigits) {
this.remeasureGutterDimensions = true
this.lineNumbersToRender.maxDigits = maxDigits
}
}
}
renderedScreenLineForRow (row) {
return this.renderedScreenLines[row - this.getRenderedStartRow()]
}
@@ -648,11 +672,11 @@ class TextEditorComponent {
this.guttersToRender = this.props.model.getGutters()
if (!oldGuttersToRender || oldGuttersToRender.length !== this.guttersToRender.length) {
this.remeasureGutterContainer = true
this.remeasureGutterDimensions = true
} else {
for (let i = 0, length = this.guttersToRender.length; i < length; i++) {
if (this.guttersToRender[i] !== oldGuttersToRender[i]) {
this.remeasureGutterContainer = true
this.remeasureGutterDimensions = true
break
}
}
@@ -1027,7 +1051,6 @@ class TextEditorComponent {
}
didResizeGutterContainer () {
console.log('didResizeGutterContainer');
if (this.measureGutterDimensions()) {
this.scheduleUpdate()
}
@@ -1476,10 +1499,10 @@ class TextEditorComponent {
}
if (this.refs.lineNumberGutter) {
const lineNumberGutterWidth = this.refs.lineNumberGutter.offsetWidth
const lineNumberGutterWidth = this.refs.lineNumberGutter.element.offsetWidth
if (lineNumberGutterWidth !== this.measurements.lineNumberGutterWidth) {
dimensionsChanged = true
this.measurements.lineNumberGutterWidth = this.refs.lineNumberGutter.offsetWidth
this.measurements.lineNumberGutterWidth = lineNumberGutterWidth
}
} else {
this.measurements.lineNumberGutterWidth = 0
@@ -2068,85 +2091,81 @@ class LineNumberGutterComponent {
render () {
const {
parentComponent, height, width, lineHeight, startRow, endRow, rowsPerTile,
maxLineNumberDigits, bufferRows, softWrappedFlags, foldableFlags,
lineNumberDecorations
maxDigits, keys, numbers, foldableFlags, decorations
} = this.props
const renderedTileCount = parentComponent.getRenderedTileCount()
const children = new Array(renderedTileCount)
const tileHeight = rowsPerTile * lineHeight + 'px'
const tileWidth = width + 'px'
let children = null
let softWrapCount = 0
for (let tileStartRow = startRow; tileStartRow < endRow; tileStartRow += rowsPerTile) {
const tileEndRow = Math.min(endRow, tileStartRow + rowsPerTile)
const tileChildren = new Array(tileEndRow - tileStartRow)
for (let row = tileStartRow; row < tileEndRow; row++) {
const i = row - startRow
const bufferRow = bufferRows[i]
const softWrapped = softWrappedFlags[i]
const foldable = foldableFlags[i]
let key, lineNumber
let className = 'line-number'
if (softWrapped) {
softWrapCount++
key = `${bufferRow}-${softWrapCount}`
lineNumber = '•'
} else {
softWrapCount = 0
key = bufferRow
lineNumber = (bufferRow + 1).toString()
if (numbers) {
const renderedTileCount = parentComponent.getRenderedTileCount()
children = new Array(renderedTileCount)
const tileHeight = rowsPerTile * lineHeight + 'px'
const tileWidth = width + 'px'
let softWrapCount = 0
for (let tileStartRow = startRow; tileStartRow < endRow; tileStartRow += rowsPerTile) {
const tileEndRow = Math.min(endRow, tileStartRow + rowsPerTile)
const tileChildren = new Array(tileEndRow - tileStartRow)
for (let row = tileStartRow; row < tileEndRow; row++) {
const i = row - startRow
const key = keys[i]
const foldable = foldableFlags[i]
let number = numbers[i]
let className = 'line-number'
if (foldable) className += ' foldable'
const decorationsForRow = decorations.get(row)
if (decorationsForRow) className += ' ' + decorationsForRow
if (number === -1) number = '•'
number = NBSP_CHARACTER.repeat(maxDigits - number.length) + number
tileChildren[row - tileStartRow] = $.div({key, className},
number,
$.div({className: 'icon-right'})
)
}
const lineNumberDecoration = lineNumberDecorations[i]
if (lineNumberDecoration != null) className += ' ' + lineNumberDecoration
const tileIndex = parentComponent.tileIndexForTileStartRow(tileStartRow)
const top = tileStartRow * lineHeight
lineNumber = NBSP_CHARACTER.repeat(maxLineNumberDigits - lineNumber.length) + lineNumber
tileChildren[row - tileStartRow] = $.div({key, className},
lineNumber,
$.div({className: 'icon-right'})
)
children[tileIndex] = $.div({
key: tileIndex,
style: {
contain: 'strict',
overflow: 'hidden',
position: 'absolute',
top: 0,
height: tileHeight,
width: tileWidth,
willChange: 'transform',
transform: `translateY(${top}px)`,
backgroundColor: 'inherit'
}
}, ...tileChildren)
}
const tileIndex = parentComponent.tileIndexForTileStartRow(tileStartRow)
const top = tileStartRow * lineHeight
children[tileIndex] = $.div({
key: tileIndex,
on: {
mousedown: this.didMouseDown
},
style: {
contain: 'strict',
overflow: 'hidden',
position: 'absolute',
height: tileHeight,
width: tileWidth,
willChange: 'transform',
transform: `translateY(${top}px)`,
backgroundColor: 'inherit'
}
}, ...tileChildren)
}
return $.div(
{
className: 'gutter line-numbers',
attributes: {'gutter-name': 'line-number'},
style: {
contain: 'strict',
overflow: 'hidden',
height: height + 'px',
width: tileWidth
}
style: {position: 'relative'},
on: {
mousedown: this.didMouseDown
},
},
...children
$.div({key: 'placeholder', className: 'line-number', style: {visibility: 'hidden'}},
'0'.repeat(maxDigits),
$.div({className: 'icon-right'})
),
children
)
}
shouldUpdate (newProps) {
return true
const oldProps = this.props
if (oldProps.height !== newProps.height) return true
@@ -2155,11 +2174,11 @@ class LineNumberGutterComponent {
if (oldProps.startRow !== newProps.startRow) return true
if (oldProps.endRow !== newProps.endRow) return true
if (oldProps.rowsPerTile !== newProps.rowsPerTile) return true
if (oldProps.maxLineNumberDigits !== newProps.maxLineNumberDigits) return true
if (!arraysEqual(oldProps.bufferRows, newProps.bufferRows)) return true
if (!arraysEqual(oldProps.softWrappedFlags, newProps.softWrappedFlags)) return true
if (oldProps.maxDigits !== newProps.maxDigits) return true
if (!arraysEqual(oldProps.keys, newProps.keys)) return true
if (!arraysEqual(oldProps.numbers, newProps.numbers)) return true
if (!arraysEqual(oldProps.foldableFlags, newProps.foldableFlags)) return true
if (!arraysEqual(oldProps.lineNumberDecorations, newProps.lineNumberDecorations)) return true
if (!arraysEqual(oldProps.decorations, newProps.decorations)) return true
return false
}

View File

@@ -2671,7 +2671,6 @@ class TextEditor extends Model
_.last(@selections)
getSelectionAtScreenPosition: (position) ->
debugger if global.debug
markers = @selectionsMarkerLayer.findMarkers(containsScreenPosition: position)
if markers.length > 0
@cursorsByMarkerId.get(markers[0].id).selection
@@ -3405,6 +3404,9 @@ class TextEditor extends Model
getGutters: ->
@gutterContainer.getGutters()
getLineNumberGutter: ->
@lineNumberGutter
# Essential: Get the gutter with the given name.
#
# Returns a {Gutter}, or `null` if no gutter exists for the given name.