Files
atom/src/lines-tile-component.coffee
2015-10-06 10:35:01 +02:00

353 lines
13 KiB
CoffeeScript

_ = require 'underscore-plus'
HighlightsComponent = require './highlights-component'
TokenIterator = require './token-iterator'
AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT}
TokenTextEscapeRegex = /[&"'<>]/g
MaxTokenLength = 20000
cloneObject = (object) ->
clone = {}
clone[key] = value for key, value of object
clone
module.exports =
class LinesTileComponent
constructor: ({@presenter, @id, @domElementPool}) ->
@tokenIterator = new TokenIterator
@measuredLines = new Set
@lineNodesByLineId = {}
@screenRowsByLineId = {}
@lineIdsByScreenRow = {}
@textNodesByLineId = {}
@domNode = @domElementPool.buildElement("div")
@domNode.style.position = "absolute"
@domNode.style.display = "block"
@highlightsComponent = new HighlightsComponent(@domElementPool)
@domNode.appendChild(@highlightsComponent.getDomNode())
destroy: ->
@domElementPool.freeElementAndDescendants(@domNode)
getDomNode: ->
@domNode
updateSync: (state) ->
@newState = state
unless @oldState
@oldState = {tiles: {}}
@oldState.tiles[@id] = {lines: {}}
@newTileState = @newState.tiles[@id]
@oldTileState = @oldState.tiles[@id]
if @newState.backgroundColor isnt @oldState.backgroundColor
@domNode.style.backgroundColor = @newState.backgroundColor
@oldState.backgroundColor = @newState.backgroundColor
if @newTileState.zIndex isnt @oldTileState.zIndex
@domNode.style.zIndex = @newTileState.zIndex
@oldTileState.zIndex = @newTileState.zIndex
if @newTileState.display isnt @oldTileState.display
@domNode.style.display = @newTileState.display
@oldTileState.display = @newTileState.display
if @newTileState.height isnt @oldTileState.height
@domNode.style.height = @newTileState.height + 'px'
@oldTileState.height = @newTileState.height
if @newState.width isnt @oldState.width
@domNode.style.width = @newState.width + 'px'
@oldTileState.width = @newTileState.width
if @newTileState.top isnt @oldTileState.top or @newTileState.left isnt @oldTileState.left
@domNode.style['-webkit-transform'] = "translate3d(#{@newTileState.left}px, #{@newTileState.top}px, 0px)"
@oldTileState.top = @newTileState.top
@oldTileState.left = @newTileState.left
@removeLineNodes() unless @oldState.indentGuidesVisible is @newState.indentGuidesVisible
@updateLineNodes()
@highlightsComponent.updateSync(@newTileState)
@oldState.indentGuidesVisible = @newState.indentGuidesVisible
removeLineNodes: ->
@removeLineNode(id) for id of @oldTileState.lines
return
removeLineNode: (id) ->
@domElementPool.freeElementAndDescendants(@lineNodesByLineId[id])
delete @lineNodesByLineId[id]
delete @textNodesByLineId[id]
delete @lineIdsByScreenRow[@screenRowsByLineId[id]]
delete @screenRowsByLineId[id]
delete @oldTileState.lines[id]
updateLineNodes: ->
for id of @oldTileState.lines
unless @newTileState.lines.hasOwnProperty(id)
@removeLineNode(id)
newLineIds = null
newLineNodes = null
for id, lineState of @newTileState.lines
if @oldTileState.lines.hasOwnProperty(id)
@updateLineNode(id)
else
newLineIds ?= []
newLineNodes ?= []
newLineIds.push(id)
newLineNodes.push(@buildLineNode(id))
@screenRowsByLineId[id] = lineState.screenRow
@lineIdsByScreenRow[lineState.screenRow] = id
@oldTileState.lines[id] = cloneObject(lineState)
return unless newLineIds?
for id, i in newLineIds
lineNode = newLineNodes[i]
@lineNodesByLineId[id] = lineNode
if nextNode = @findNodeNextTo(lineNode)
@domNode.insertBefore(lineNode, nextNode)
else
@domNode.appendChild(lineNode)
findNodeNextTo: (node) ->
for nextNode, index in @domNode.children
continue if index is 0 # skips highlights node
return nextNode if @screenRowForNode(node) < @screenRowForNode(nextNode)
return
screenRowForNode: (node) -> parseInt(node.dataset.screenRow)
buildLineNode: (id) ->
{width} = @newState
{screenRow, tokens, text, top, lineEnding, fold, isSoftWrapped, indentLevel, decorationClasses} = @newTileState.lines[id]
lineNode = @domElementPool.buildElement("div", "line")
lineNode.dataset.screenRow = screenRow
if decorationClasses?
for decorationClass in decorationClasses
lineNode.classList.add(decorationClass)
@currentLineTextNodes = []
if text is ""
@setEmptyLineInnerNodes(id, lineNode)
else
@setLineInnerNodes(id, lineNode)
@textNodesByLineId[id] = @currentLineTextNodes
lineNode.appendChild(@domElementPool.buildElement("span", "fold-marker")) if fold
lineNode
setEmptyLineInnerNodes: (id, lineNode) ->
{indentGuidesVisible} = @newState
{indentLevel, tabLength, endOfLineInvisibles} = @newTileState.lines[id]
if indentGuidesVisible and indentLevel > 0
invisibleIndex = 0
for i in [0...indentLevel]
indentGuide = @domElementPool.buildElement("span", "indent-guide")
for j in [0...tabLength]
if invisible = endOfLineInvisibles?[invisibleIndex++]
invisibleSpan = @domElementPool.buildElement("span", "invisible-character")
textNode = @domElementPool.buildText(invisible)
invisibleSpan.appendChild(textNode)
indentGuide.appendChild(invisibleSpan)
@currentLineTextNodes.push(textNode)
else
textNode = @domElementPool.buildText(" ")
indentGuide.appendChild(textNode)
@currentLineTextNodes.push(textNode)
lineNode.appendChild(indentGuide)
while invisibleIndex < endOfLineInvisibles?.length
invisible = endOfLineInvisibles[invisibleIndex++]
invisibleSpan = @domElementPool.buildElement("span", "invisible-character")
textNode = @domElementPool.buildText(invisible)
invisibleSpan.appendChild(textNode)
lineNode.appendChild(invisibleSpan)
@currentLineTextNodes.push(textNode)
else
unless @appendEndOfLineNodes(id, lineNode)
textNode = @domElementPool.buildText("\u00a0")
lineNode.appendChild(textNode)
@currentLineTextNodes.push(textNode)
setLineInnerNodes: (id, lineNode) ->
lineState = @newTileState.lines[id]
{firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, invisibles} = lineState
lineIsWhitespaceOnly = firstTrailingWhitespaceIndex is 0
@tokenIterator.reset(lineState)
openScopeNode = lineNode
while @tokenIterator.next()
for scope in @tokenIterator.getScopeEnds()
openScopeNode = openScopeNode.parentElement
for scope in @tokenIterator.getScopeStarts()
newScopeNode = @domElementPool.buildElement("span", scope.replace(/\.+/g, ' '))
openScopeNode.appendChild(newScopeNode)
openScopeNode = newScopeNode
tokenStart = @tokenIterator.getScreenStart()
tokenEnd = @tokenIterator.getScreenEnd()
tokenText = @tokenIterator.getText()
isHardTab = @tokenIterator.isHardTab()
if hasLeadingWhitespace = tokenStart < firstNonWhitespaceIndex
tokenFirstNonWhitespaceIndex = firstNonWhitespaceIndex - tokenStart
else
tokenFirstNonWhitespaceIndex = null
if hasTrailingWhitespace = tokenEnd > firstTrailingWhitespaceIndex
tokenFirstTrailingWhitespaceIndex = Math.max(0, firstTrailingWhitespaceIndex - tokenStart)
else
tokenFirstTrailingWhitespaceIndex = null
hasIndentGuide =
@newState.indentGuidesVisible and
(hasLeadingWhitespace or lineIsWhitespaceOnly)
hasInvisibleCharacters =
(invisibles?.tab and isHardTab) or
(invisibles?.space and (hasLeadingWhitespace or hasTrailingWhitespace))
@appendTokenNodes(tokenText, isHardTab, tokenFirstNonWhitespaceIndex, tokenFirstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters, openScopeNode)
@appendEndOfLineNodes(id, lineNode)
appendTokenNodes: (tokenText, isHardTab, firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters, scopeNode) ->
if isHardTab
textNode = @domElementPool.buildText(tokenText)
hardTabNode = @domElementPool.buildElement("span", "hard-tab")
hardTabNode.classList.add("leading-whitespace") if firstNonWhitespaceIndex?
hardTabNode.classList.add("trailing-whitespace") if firstTrailingWhitespaceIndex?
hardTabNode.classList.add("indent-guide") if hasIndentGuide
hardTabNode.classList.add("invisible-character") if hasInvisibleCharacters
hardTabNode.appendChild(textNode)
scopeNode.appendChild(hardTabNode)
@currentLineTextNodes.push(textNode)
else
startIndex = 0
endIndex = tokenText.length
leadingWhitespaceNode = null
leadingWhitespaceTextNode = null
trailingWhitespaceNode = null
trailingWhitespaceTextNode = null
if firstNonWhitespaceIndex?
leadingWhitespaceTextNode =
@domElementPool.buildText(tokenText.substring(0, firstNonWhitespaceIndex))
leadingWhitespaceNode = @domElementPool.buildElement("span", "leading-whitespace")
leadingWhitespaceNode.classList.add("indent-guide") if hasIndentGuide
leadingWhitespaceNode.classList.add("invisible-character") if hasInvisibleCharacters
leadingWhitespaceNode.appendChild(leadingWhitespaceTextNode)
startIndex = firstNonWhitespaceIndex
if firstTrailingWhitespaceIndex?
tokenIsOnlyWhitespace = firstTrailingWhitespaceIndex is 0
trailingWhitespaceTextNode =
@domElementPool.buildText(tokenText.substring(firstTrailingWhitespaceIndex))
trailingWhitespaceNode = @domElementPool.buildElement("span", "trailing-whitespace")
trailingWhitespaceNode.classList.add("indent-guide") if hasIndentGuide and not firstNonWhitespaceIndex? and tokenIsOnlyWhitespace
trailingWhitespaceNode.classList.add("invisible-character") if hasInvisibleCharacters
trailingWhitespaceNode.appendChild(trailingWhitespaceTextNode)
endIndex = firstTrailingWhitespaceIndex
if leadingWhitespaceNode?
scopeNode.appendChild(leadingWhitespaceNode)
@currentLineTextNodes.push(leadingWhitespaceTextNode)
if tokenText.length > MaxTokenLength
while startIndex < endIndex
textNode = @domElementPool.buildText(
@sliceText(tokenText, startIndex, startIndex + MaxTokenLength)
)
textSpan = @domElementPool.buildElement("span")
textSpan.appendChild(textNode)
scopeNode.appendChild(textSpan)
startIndex += MaxTokenLength
@currentLineTextNodes.push(textNode)
else
textNode = @domElementPool.buildText(@sliceText(tokenText, startIndex, endIndex))
scopeNode.appendChild(textNode)
@currentLineTextNodes.push(textNode)
if trailingWhitespaceNode?
scopeNode.appendChild(trailingWhitespaceNode)
@currentLineTextNodes.push(trailingWhitespaceTextNode)
sliceText: (tokenText, startIndex, endIndex) ->
if startIndex? and endIndex? and startIndex > 0 or endIndex < tokenText.length
tokenText = tokenText.slice(startIndex, endIndex)
tokenText
appendEndOfLineNodes: (id, lineNode) ->
{endOfLineInvisibles} = @newTileState.lines[id]
hasInvisibles = false
if endOfLineInvisibles?
for invisible in endOfLineInvisibles
hasInvisibles = true
invisibleSpan = @domElementPool.buildElement("span", "invisible-character")
textNode = @domElementPool.buildText(invisible)
invisibleSpan.appendChild(textNode)
lineNode.appendChild(invisibleSpan)
@currentLineTextNodes.push(textNode)
hasInvisibles
updateLineNode: (id) ->
oldLineState = @oldTileState.lines[id]
newLineState = @newTileState.lines[id]
lineNode = @lineNodesByLineId[id]
newDecorationClasses = newLineState.decorationClasses
oldDecorationClasses = oldLineState.decorationClasses
if oldDecorationClasses?
for decorationClass in oldDecorationClasses
unless newDecorationClasses? and decorationClass in newDecorationClasses
lineNode.classList.remove(decorationClass)
if newDecorationClasses?
for decorationClass in newDecorationClasses
unless oldDecorationClasses? and decorationClass in oldDecorationClasses
lineNode.classList.add(decorationClass)
oldLineState.decorationClasses = newLineState.decorationClasses
if newLineState.screenRow isnt oldLineState.screenRow
lineNode.dataset.screenRow = newLineState.screenRow
oldLineState.screenRow = newLineState.screenRow
@lineIdsByScreenRow[newLineState.screenRow] = id
@screenRowsByLineId[id] = newLineState.screenRow
lineNodeForScreenRow: (screenRow) ->
@lineNodesByLineId[@lineIdsByScreenRow[screenRow]]
lineNodeForLineId: (lineId) ->
@lineNodesByLineId[lineId]
textNodesForLineId: (lineId) ->
@textNodesByLineId[lineId].slice()