mirror of
https://github.com/atom/atom.git
synced 2026-01-23 05:48:10 -05:00
When lines are inserted or removed, we need to manually shift the on-screen lines since everything is absolutely positioned now.
141 lines
4.8 KiB
CoffeeScript
141 lines
4.8 KiB
CoffeeScript
React = require 'react'
|
|
{div, span} = require 'reactionary'
|
|
{debounce, isEqual, isEqualForProperties, multiplyString} = require 'underscore-plus'
|
|
{$$} = require 'space-pen'
|
|
|
|
DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibility: hidden;', => @span 'x')[0]
|
|
AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT}
|
|
|
|
module.exports =
|
|
LinesComponent = React.createClass
|
|
displayName: 'LinesComponent'
|
|
|
|
render: ->
|
|
if @isMounted()
|
|
{editor, renderedRowRange, lineHeight, showIndentGuide} = @props
|
|
[startRow, endRow] = renderedRowRange
|
|
|
|
lines =
|
|
for tokenizedLine, i in editor.linesForScreenRows(startRow, endRow - 1)
|
|
LineComponent({key: tokenizedLine.id, tokenizedLine, showIndentGuide, lineHeight, screenRow: startRow + i})
|
|
|
|
div {className: 'lines'}, lines
|
|
|
|
componentWillMount: ->
|
|
@measuredLines = new WeakSet
|
|
|
|
componentDidMount: ->
|
|
@measureLineHeightAndCharWidth()
|
|
|
|
shouldComponentUpdate: (newProps) ->
|
|
return true unless isEqualForProperties(newProps, @props, 'renderedRowRange', 'fontSize', 'fontFamily', 'lineHeight', 'showIndentGuide')
|
|
|
|
{renderedRowRange, pendingChanges} = newProps
|
|
for change in pendingChanges
|
|
return true unless change.end <= renderedRowRange.start or renderedRowRange.end <= change.start
|
|
|
|
false
|
|
|
|
componentDidUpdate: (prevProps) ->
|
|
@measureLineHeightAndCharWidth() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily', 'lineHeight')
|
|
@clearScopedCharWidths() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily')
|
|
@measureCharactersInNewLines() unless @props.scrollingVertically
|
|
|
|
measureLineHeightAndCharWidth: ->
|
|
node = @getDOMNode()
|
|
node.appendChild(DummyLineNode)
|
|
lineHeight = DummyLineNode.getBoundingClientRect().height
|
|
charWidth = DummyLineNode.firstChild.getBoundingClientRect().width
|
|
node.removeChild(DummyLineNode)
|
|
|
|
{editor} = @props
|
|
editor.setLineHeight(lineHeight)
|
|
editor.setDefaultCharWidth(charWidth)
|
|
|
|
measureCharactersInNewLines: ->
|
|
[visibleStartRow, visibleEndRow] = @props.renderedRowRange
|
|
node = @getDOMNode()
|
|
|
|
for tokenizedLine, i in @props.editor.linesForScreenRows(visibleStartRow, visibleEndRow - 1)
|
|
unless @measuredLines.has(tokenizedLine)
|
|
lineNode = node.children[i]
|
|
@measureCharactersInLine(tokenizedLine, lineNode)
|
|
|
|
measureCharactersInLine: (tokenizedLine, lineNode) ->
|
|
{editor} = @props
|
|
rangeForMeasurement = null
|
|
iterator = null
|
|
charIndex = 0
|
|
|
|
for {value, scopes}, tokenIndex in tokenizedLine.tokens
|
|
charWidths = editor.getScopedCharWidths(scopes)
|
|
|
|
for char in value
|
|
unless charWidths[char]?
|
|
unless textNode?
|
|
rangeForMeasurement ?= document.createRange()
|
|
iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT, AcceptFilter)
|
|
textNode = iterator.nextNode()
|
|
textNodeIndex = 0
|
|
nextTextNodeIndex = textNode.textContent.length
|
|
|
|
while nextTextNodeIndex <= charIndex
|
|
textNode = iterator.nextNode()
|
|
textNodeIndex = nextTextNodeIndex
|
|
nextTextNodeIndex = textNodeIndex + textNode.textContent.length
|
|
|
|
i = charIndex - textNodeIndex
|
|
rangeForMeasurement.setStart(textNode, i)
|
|
rangeForMeasurement.setEnd(textNode, i + 1)
|
|
charWidth = rangeForMeasurement.getBoundingClientRect().width
|
|
editor.setScopedCharWidth(scopes, char, charWidth)
|
|
|
|
charIndex++
|
|
|
|
@measuredLines.add(tokenizedLine)
|
|
|
|
clearScopedCharWidths: ->
|
|
@measuredLines.clear()
|
|
@props.editor.clearScopedCharWidths()
|
|
|
|
|
|
LineComponent = React.createClass
|
|
displayName: 'LineComponent'
|
|
|
|
render: ->
|
|
{screenRow, lineHeight} = @props
|
|
|
|
style =
|
|
top: screenRow * lineHeight
|
|
position: 'absolute'
|
|
|
|
div className: 'line', style: style, 'data-screen-row': screenRow, dangerouslySetInnerHTML: {__html: @buildInnerHTML()}
|
|
|
|
buildInnerHTML: ->
|
|
if @props.tokenizedLine.text.length is 0
|
|
@buildEmptyLineHTML()
|
|
else
|
|
@buildScopeTreeHTML(@props.tokenizedLine.getScopeTree())
|
|
|
|
buildEmptyLineHTML: ->
|
|
{showIndentGuide, tokenizedLine} = @props
|
|
{indentLevel, tabLength} = tokenizedLine
|
|
|
|
if showIndentGuide and indentLevel > 0
|
|
indentSpan = "<span class='indent-guide'>#{multiplyString(' ', tabLength)}</span>"
|
|
multiplyString(indentSpan, indentLevel + 1)
|
|
else
|
|
" "
|
|
|
|
buildScopeTreeHTML: (scopeTree) ->
|
|
if scopeTree.children?
|
|
html = "<span class='#{scopeTree.scope.replace(/\./g, ' ')}'>"
|
|
html += @buildScopeTreeHTML(child) for child in scopeTree.children
|
|
html += "</span>"
|
|
html
|
|
else
|
|
"<span>#{scopeTree.getValueAsHtml({hasIndentGuide: @props.showIndentGuide})}</span>"
|
|
|
|
shouldComponentUpdate: (newProps) ->
|
|
not isEqualForProperties(newProps, @props, 'showIndentGuide', 'lineHeight', 'screenRow')
|