Merge master into jr-fix-observe-text-editor-methods

This commit is contained in:
Jason Rudolph
2017-06-06 13:59:35 -04:00
10 changed files with 289 additions and 66 deletions

View File

@@ -447,6 +447,7 @@ class Pane
# Public: Make the given item *active*, causing it to be displayed by
# the pane's view.
#
# * `item` The item to activate
# * `options` (optional) {Object}
# * `pending` (optional) {Boolean} indicating that the item should be added
# in a pending state if it does not yet exist in the pane. Existing pending

View File

@@ -86,6 +86,7 @@ class TextEditorComponent {
(this.props.cursorBlinkResumeDelay || CURSOR_BLINK_RESUME_DELAY)
)
this.lineTopIndex = new LineTopIndex()
this.lineNodesPool = new NodePool()
this.updateScheduled = false
this.suppressUpdates = false
this.hasInitialMeasurements = false
@@ -116,6 +117,7 @@ class TextEditorComponent {
this.lineNodesByScreenLineId = new Map()
this.textNodesByScreenLineId = new Map()
this.overlayComponents = new Set()
this.overlayDimensionsByElement = new WeakMap()
this.shouldRenderDummyScrollbars = true
this.remeasureScrollbars = false
this.pendingAutoscroll = null
@@ -134,6 +136,7 @@ class TextEditorComponent {
this.idsByTileStartRow = new Map()
this.nextTileId = 0
this.renderedTileStartRows = []
this.showLineNumbers = this.props.model.doesShowLineNumbers()
this.lineNumbersToRender = {
maxDigits: 2,
bufferRows: [],
@@ -481,6 +484,7 @@ class TextEditorComponent {
guttersToRender: this.guttersToRender,
decorationsToRender: this.decorationsToRender,
isLineNumberGutterVisible: this.props.model.isLineNumberGutterVisible(),
showLineNumbers: this.showLineNumbers,
lineNumbersToRender: this.lineNumbersToRender,
didMeasureVisibleBlockDecoration: this.didMeasureVisibleBlockDecoration
})
@@ -582,6 +586,7 @@ class TextEditorComponent {
blockDecorations: this.decorationsToRender.blocks.get(tileStartRow),
highlightDecorations: this.decorationsToRender.highlights.get(tileStartRow),
displayLayer,
nodePool: this.lineNodesPool,
lineNodesByScreenLineId,
textNodesByScreenLineId
})
@@ -595,6 +600,7 @@ class TextEditorComponent {
screenLine,
screenRow,
displayLayer,
nodePool: this.lineNodesPool,
lineNodesByScreenLineId,
textNodesByScreenLineId
}))
@@ -754,6 +760,7 @@ class TextEditorComponent {
{
key: overlayProps.element,
overlayComponents: this.overlayComponents,
measuredDimensions: this.overlayDimensionsByElement.get(overlayProps.element),
didResize: () => { this.updateSync() }
},
overlayProps
@@ -816,6 +823,10 @@ class TextEditorComponent {
queryLineNumbersToRender () {
const {model} = this.props
if (!model.isLineNumberGutterVisible()) return
if (this.showLineNumbers !== model.doesShowLineNumbers()) {
this.remeasureGutterDimensions = true
this.showLineNumbers = model.doesShowLineNumbers()
}
this.queryMaxLineNumberDigits()
@@ -1300,15 +1311,16 @@ class TextEditorComponent {
const {row, column} = screenPosition
let wrapperTop = contentClientRect.top + this.pixelPositionAfterBlocksForRow(row) + this.getLineHeight()
let wrapperLeft = contentClientRect.left + this.pixelLeftForRowAndColumn(row, column)
const clientRect = element.getBoundingClientRect()
this.overlayDimensionsByElement.set(element, clientRect)
if (avoidOverflow !== false) {
const computedStyle = window.getComputedStyle(element)
const elementHeight = element.offsetHeight
const elementTop = wrapperTop + parseInt(computedStyle.marginTop)
const elementBottom = elementTop + elementHeight
const flippedElementTop = wrapperTop - this.getLineHeight() - elementHeight - parseInt(computedStyle.marginBottom)
const elementBottom = elementTop + clientRect.height
const flippedElementTop = wrapperTop - this.getLineHeight() - clientRect.height - parseInt(computedStyle.marginBottom)
const elementLeft = wrapperLeft + parseInt(computedStyle.marginLeft)
const elementRight = elementLeft + element.offsetWidth
const elementRight = elementLeft + clientRect.width
if (elementBottom > windowInnerHeight && flippedElementTop >= 0) {
wrapperTop -= (elementTop - flippedElementTop)
@@ -1320,8 +1332,8 @@ class TextEditorComponent {
}
}
decoration.pixelTop = wrapperTop
decoration.pixelLeft = wrapperLeft
decoration.pixelTop = Math.round(wrapperTop)
decoration.pixelLeft = Math.round(wrapperLeft)
}
}
@@ -1410,24 +1422,8 @@ class TextEditorComponent {
this.scheduleUpdate()
}
// Transfer focus to the hidden input, but first ensure the input is in the
// visible part of the scrolled content to avoid the browser trying to
// auto-scroll to the form-field.
const {hiddenInput} = this.refs.cursorsAndInput.refs
hiddenInput.style.top = this.getScrollTop() + 'px'
hiddenInput.style.left = this.getScrollLeft() + 'px'
hiddenInput.focus()
// Restore the previous position of the field now that it is already focused
// and won't cause unwanted scrolling.
if (this.hiddenInputPosition) {
hiddenInput.style.top = this.hiddenInputPosition.pixelTop + 'px'
hiddenInput.style.left = this.hiddenInputPosition.pixelLeft + 'px'
} else {
hiddenInput.style.top = 0
hiddenInput.style.left = 0
}
}
// Called by TextEditorElement so that this function is always the first
@@ -1450,6 +1446,12 @@ class TextEditorComponent {
}
didFocusHiddenInput () {
// Focusing the hidden input when it is off-screen causes the browser to
// scroll it into view. Since we use synthetic scrolling this behavior
// causes all the lines to disappear so we counteract it by always setting
// the scroll position to 0.
this.refs.scrollContainer.scrollTop = 0
this.refs.scrollContainer.scrollLeft = 0
if (!this.focused) {
this.focused = true
this.startCursorBlinking()
@@ -2899,7 +2901,7 @@ class GutterContainerComponent {
renderLineNumberGutter (gutter) {
const {
rootComponent, isLineNumberGutterVisible, hasInitialMeasurements, lineNumbersToRender,
rootComponent, isLineNumberGutterVisible, showLineNumbers, hasInitialMeasurements, lineNumbersToRender,
renderedStartRow, renderedEndRow, rowsPerTile, decorationsToRender, didMeasureVisibleBlockDecoration,
scrollHeight, lineNumberGutterWidth, lineHeight
} = this.props
@@ -2925,13 +2927,15 @@ class GutterContainerComponent {
didMeasureVisibleBlockDecoration: didMeasureVisibleBlockDecoration,
height: scrollHeight,
width: lineNumberGutterWidth,
lineHeight: lineHeight
lineHeight: lineHeight,
showLineNumbers
})
} else {
return $(LineNumberGutterComponent, {
ref: 'lineNumberGutter',
element: gutter.getElement(),
maxDigits: lineNumbersToRender.maxDigits
maxDigits: lineNumbersToRender.maxDigits,
showLineNumbers
})
}
}
@@ -2943,6 +2947,7 @@ class LineNumberGutterComponent {
this.element = this.props.element
this.virtualNode = $.div(null)
this.virtualNode.domNode = this.element
this.nodePool = new NodePool()
etch.updateSync(this)
}
@@ -2955,7 +2960,7 @@ class LineNumberGutterComponent {
render () {
const {
rootComponent, height, width, lineHeight, startRow, endRow, rowsPerTile,
rootComponent, showLineNumbers, height, width, lineHeight, startRow, endRow, rowsPerTile,
maxDigits, keys, bufferRows, softWrappedFlags, foldableFlags, decorations
} = this.props
@@ -2980,25 +2985,27 @@ class LineNumberGutterComponent {
const decorationsForRow = decorations[row - startRow]
if (decorationsForRow) className = className + ' ' + decorationsForRow
let number = softWrapped ? '•' : bufferRow + 1
number = NBSP_CHARACTER.repeat(maxDigits - number.length) + number
let number = null
if (showLineNumbers) {
number = softWrapped ? '•' : bufferRow + 1
number = NBSP_CHARACTER.repeat(maxDigits - number.length) + number
}
const lineNumberProps = {
key,
className,
style: {width: width + 'px'},
dataset: {bufferRow}
width,
bufferRow,
number,
nodePool: this.nodePool
}
const currentRowTop = rootComponent.pixelPositionAfterBlocksForRow(row)
const previousRowBottom = rootComponent.pixelPositionAfterBlocksForRow(row - 1) + lineHeight
if (currentRowTop > previousRowBottom) {
lineNumberProps.style.marginTop = (currentRowTop - previousRowBottom) + 'px'
lineNumberProps.marginTop = currentRowTop - previousRowBottom
}
tileChildren[row - tileStartRow] = $.div(lineNumberProps,
number,
$.div({className: 'icon-right'})
)
tileChildren[row - tileStartRow] = $(LineNumberComponent, lineNumberProps)
}
const tileTop = rootComponent.pixelPositionBeforeBlocksForRow(tileStartRow)
@@ -3024,7 +3031,7 @@ class LineNumberGutterComponent {
return $.div(
{
className: 'gutter line-bufferRows',
className: 'gutter line-numbers',
attributes: {'gutter-name': 'line-number'},
style: {position: 'relative', height: height + 'px'},
on: {
@@ -3032,7 +3039,7 @@ class LineNumberGutterComponent {
}
},
$.div({key: 'placeholder', className: 'line-number dummy', style: {visibility: 'hidden'}},
'0'.repeat(maxDigits),
showLineNumbers ? '0'.repeat(maxDigits) : null,
$.div({className: 'icon-right'})
),
children
@@ -3042,6 +3049,7 @@ class LineNumberGutterComponent {
shouldUpdate (newProps) {
const oldProps = this.props
if (oldProps.showLineNumbers !== newProps.showLineNumbers) return true
if (oldProps.height !== newProps.height) return true
if (oldProps.width !== newProps.width) return true
if (oldProps.lineHeight !== newProps.lineHeight) return true
@@ -3099,6 +3107,49 @@ class LineNumberGutterComponent {
}
}
class LineNumberComponent {
constructor (props) {
const {className, width, marginTop, bufferRow, number, nodePool} = props
this.props = props
const style = {width: width + 'px'}
if (marginTop != null) style.marginTop = marginTop + 'px'
this.element = nodePool.getElement('DIV', className, style)
this.element.dataset.bufferRow = bufferRow
if (number) this.element.appendChild(nodePool.getTextNode(number))
this.element.appendChild(nodePool.getElement('DIV', 'icon-right', null))
}
destroy () {
this.element.remove()
this.props.nodePool.release(this.element)
}
update (props) {
const {nodePool, className, width, marginTop, number} = props
if (this.props.className !== className) this.element.className = className
if (this.props.width !== width) this.element.style.width = width + 'px'
if (this.props.marginTop !== marginTop) {
if (marginTop != null) {
this.element.style.marginTop = marginTop + 'px'
} else {
this.element.style.marginTop = ''
}
}
if (this.props.number !== number) {
if (number) {
this.element.insertBefore(nodePool.getTextNode(number), this.element.firstChild)
} else {
const numberNode = this.element.firstChild
numberNode.remove()
nodePool.release(numberNode)
}
}
this.props = props
}
}
class CustomGutterComponent {
constructor (props) {
this.props = props
@@ -3362,7 +3413,7 @@ class LinesTileComponent {
createLines () {
const {
tileStartRow, screenLines, lineDecorations, textDecorations,
displayLayer, lineNodesByScreenLineId, textNodesByScreenLineId
nodePool, displayLayer, lineNodesByScreenLineId, textNodesByScreenLineId
} = this.props
this.lineComponents = []
@@ -3373,6 +3424,7 @@ class LinesTileComponent {
lineDecoration: lineDecorations[i],
textDecorations: textDecorations[i],
displayLayer,
nodePool,
lineNodesByScreenLineId,
textNodesByScreenLineId
})
@@ -3384,7 +3436,7 @@ class LinesTileComponent {
updateLines (oldProps, newProps) {
var {
screenLines, tileStartRow, lineDecorations, textDecorations,
displayLayer, lineNodesByScreenLineId, textNodesByScreenLineId
nodePool, displayLayer, lineNodesByScreenLineId, textNodesByScreenLineId
} = newProps
var oldScreenLines = oldProps.screenLines
@@ -3406,6 +3458,7 @@ class LinesTileComponent {
lineDecoration: lineDecorations[newScreenLineIndex],
textDecorations: textDecorations[newScreenLineIndex],
displayLayer,
nodePool,
lineNodesByScreenLineId,
textNodesByScreenLineId
})
@@ -3442,6 +3495,7 @@ class LinesTileComponent {
lineDecoration: lineDecorations[newScreenLineIndex],
textDecorations: textDecorations[newScreenLineIndex],
displayLayer,
nodePool,
lineNodesByScreenLineId,
textNodesByScreenLineId
})
@@ -3468,6 +3522,7 @@ class LinesTileComponent {
lineDecoration: lineDecorations[newScreenLineIndex],
textDecorations: textDecorations[newScreenLineIndex],
displayLayer,
nodePool,
lineNodesByScreenLineId,
textNodesByScreenLineId
})
@@ -3611,10 +3666,9 @@ class LinesTileComponent {
class LineComponent {
constructor (props) {
const {screenRow, screenLine, lineNodesByScreenLineId} = props
const {nodePool, screenRow, screenLine, lineNodesByScreenLineId} = props
this.props = props
this.element = document.createElement('div')
this.element.className = this.buildClassName()
this.element = nodePool.getElement('DIV', this.buildClassName(), null)
this.element.dataset.screenRow = screenRow
lineNodesByScreenLineId.set(screenLine.id, this.element)
this.appendContents()
@@ -3639,23 +3693,24 @@ class LineComponent {
}
destroy () {
const {lineNodesByScreenLineId, textNodesByScreenLineId, screenLine} = this.props
const {nodePool, lineNodesByScreenLineId, textNodesByScreenLineId, screenLine} = this.props
if (lineNodesByScreenLineId.get(screenLine.id) === this.element) {
lineNodesByScreenLineId.delete(screenLine.id)
textNodesByScreenLineId.delete(screenLine.id)
}
this.element.remove()
nodePool.release(this.element)
}
appendContents () {
const {displayLayer, screenLine, textDecorations, textNodesByScreenLineId} = this.props
const {displayLayer, nodePool, screenLine, textDecorations, textNodesByScreenLineId} = this.props
const textNodes = []
textNodesByScreenLineId.set(screenLine.id, textNodes)
const {lineText, tags} = screenLine
let openScopeNode = document.createElement('span')
let openScopeNode = nodePool.getElement('SPAN', null, null)
this.element.appendChild(openScopeNode)
let decorationIndex = 0
@@ -3676,8 +3731,7 @@ class LineComponent {
if (displayLayer.isCloseTag(tag)) {
openScopeNode = openScopeNode.parentElement
} else if (displayLayer.isOpenTag(tag)) {
const newScopeNode = document.createElement('span')
newScopeNode.className = displayLayer.classNameForTag(tag)
const newScopeNode = nodePool.getElement('SPAN', displayLayer.classNameForTag(tag), null)
openScopeNode.appendChild(newScopeNode)
openScopeNode = newScopeNode
} else {
@@ -3701,7 +3755,7 @@ class LineComponent {
}
if (column === 0) {
const textNode = document.createTextNode(' ')
const textNode = nodePool.getTextNode(' ')
this.element.appendChild(textNode)
textNodes.push(textNode)
}
@@ -3710,22 +3764,22 @@ class LineComponent {
// Insert a zero-width non-breaking whitespace, so that LinesYardstick can
// take the fold-marker::after pseudo-element into account during
// measurements when such marker is the last character on the line.
const textNode = document.createTextNode(ZERO_WIDTH_NBSP_CHARACTER)
const textNode = nodePool.getTextNode(ZERO_WIDTH_NBSP_CHARACTER)
this.element.appendChild(textNode)
textNodes.push(textNode)
}
}
appendTextNode (textNodes, openScopeNode, text, activeClassName, activeStyle) {
const {nodePool} = this.props
if (activeClassName || activeStyle) {
const decorationNode = document.createElement('span')
if (activeClassName) decorationNode.className = activeClassName
if (activeStyle) Object.assign(decorationNode.style, activeStyle)
const decorationNode = nodePool.getElement('SPAN', activeClassName, activeStyle)
openScopeNode.appendChild(decorationNode)
openScopeNode = decorationNode
}
const textNode = document.createTextNode(text)
const textNode = nodePool.getTextNode(text)
openScopeNode.appendChild(textNode)
textNodes.push(textNode)
}
@@ -3855,10 +3909,13 @@ class OverlayComponent {
// Synchronous DOM updates in response to resize events might trigger a
// "loop limit exceeded" error. We disconnect the observer before
// potentially mutating the DOM, and then reconnect it on the next tick.
this.resizeObserver = new ResizeObserver(() => {
this.resizeObserver.disconnect()
this.props.didResize()
process.nextTick(() => { this.resizeObserver.observe(this.element) })
this.resizeObserver = new ResizeObserver((entries) => {
const {contentRect} = entries[0]
if (contentRect.width !== this.props.measuredDimensions.width || contentRect.height !== this.props.measuredDimensions.height) {
this.resizeObserver.disconnect()
this.props.didResize()
process.nextTick(() => { this.resizeObserver.observe(this.element) })
}
})
this.didAttach()
this.props.overlayComponents.add(this)
@@ -3966,3 +4023,83 @@ function debounce (fn, wait) {
if (!timeout) timeout = setTimeout(later, wait)
}
}
class NodePool {
constructor () {
this.elementsByType = {}
this.textNodes = []
this.stylesByNode = new WeakMap()
}
getElement (type, className, style) {
var element
var elementsByDepth = this.elementsByType[type]
if (elementsByDepth) {
while (elementsByDepth.length > 0) {
var elements = elementsByDepth[elementsByDepth.length - 1]
if (elements && elements.length > 0) {
element = elements.pop()
if (elements.length === 0) elementsByDepth.pop()
break
} else {
elementsByDepth.pop()
}
}
}
if (element) {
element.className = className
var existingStyle = this.stylesByNode.get(element)
if (existingStyle) {
for (var key in existingStyle) {
if (!style || !style[key]) element.style[key] = ''
}
}
if (style) Object.assign(element.style, style)
this.stylesByNode.set(element, style)
while (element.firstChild) element.firstChild.remove()
return element
} else {
var newElement = document.createElement(type)
if (className) newElement.className = className
if (style) Object.assign(newElement.style, style)
this.stylesByNode.set(newElement, style)
return newElement
}
}
getTextNode (text) {
if (this.textNodes.length > 0) {
var node = this.textNodes.pop()
node.textContent = text
return node
} else {
return document.createTextNode(text)
}
}
release (node, depth = 0) {
var {nodeName} = node
if (nodeName === '#text') {
this.textNodes.push(node)
} else {
var elementsByDepth = this.elementsByType[nodeName]
if (!elementsByDepth) {
elementsByDepth = []
this.elementsByType[nodeName] = elementsByDepth
}
var elements = elementsByDepth[depth]
if (!elements) {
elements = []
elementsByDepth[depth] = elements
}
elements.push(node)
for (var i = 0; i < node.childNodes.length; i++) {
this.release(node.childNodes[i], depth + 1)
}
}
}
}

View File

@@ -98,7 +98,6 @@ class TextEditor extends Model
registered: false
atomicSoftTabs: true
invisibles: null
showLineNumbers: true
scrollSensitivity: 40
Object.defineProperty @prototype, "element",
@@ -156,7 +155,7 @@ class TextEditor extends Model
{
@softTabs, @initialScrollTopRow, @initialScrollLeftColumn, initialLine, initialColumn, tabLength,
@softWrapped, @decorationManager, @selectionsMarkerLayer, @buffer, suppressCursorCreation,
@mini, @placeholderText, lineNumberGutterVisible, @largeFileMode,
@mini, @placeholderText, lineNumberGutterVisible, @showLineNumbers, @largeFileMode,
@assert, grammar, @showInvisibles, @autoHeight, @autoWidth, @scrollPastEnd, @editorWidthInChars,
@tokenizedBuffer, @displayLayer, @invisibles, @showIndentGuide,
@softWrapped, @softWrapAtPreferredLineLength, @preferredLineLength,
@@ -184,6 +183,7 @@ class TextEditor extends Model
@softWrapped ?= false
@softWrapAtPreferredLineLength ?= false
@preferredLineLength ?= 80
@showLineNumbers ?= true
@buffer ?= new TextBuffer({shouldDestroyOnFileDelete: ->
atom.config.get('core.closeDeletedFileTabs')})
@@ -357,6 +357,7 @@ class TextEditor extends Model
when 'showLineNumbers'
if value isnt @showLineNumbers
@showLineNumbers = value
@component?.scheduleUpdate()
when 'showInvisibles'
if value isnt @showInvisibles