Merge pull request #8783 from atom/as-recycle-nodes

Recycle DOM nodes
This commit is contained in:
Antonio Scandurra
2015-09-17 11:17:01 +02:00
14 changed files with 240 additions and 145 deletions

View File

@@ -0,0 +1,55 @@
DOMElementPool = require '../src/dom-element-pool'
describe "DOMElementPool", ->
domElementPool = null
beforeEach ->
domElementPool = new DOMElementPool
it "builds DOM nodes, recycling them when they are freed", ->
[div, span1, span2, span3, span4, span5] = elements = [
domElementPool.build("div")
domElementPool.build("span")
domElementPool.build("span")
domElementPool.build("span")
domElementPool.build("span")
domElementPool.build("span")
]
div.appendChild(span1)
span1.appendChild(span2)
div.appendChild(span3)
span3.appendChild(span4)
domElementPool.freeElementAndDescendants(div)
domElementPool.freeElementAndDescendants(span5)
expect(elements).toContain(domElementPool.build("div"))
expect(elements).toContain(domElementPool.build("span"))
expect(elements).toContain(domElementPool.build("span"))
expect(elements).toContain(domElementPool.build("span"))
expect(elements).toContain(domElementPool.build("span"))
expect(elements).toContain(domElementPool.build("span"))
expect(elements).not.toContain(domElementPool.build("div"))
expect(elements).not.toContain(domElementPool.build("span"))
it "forgets free nodes after being cleared", ->
span = domElementPool.build("span")
div = domElementPool.build("div")
domElementPool.freeElementAndDescendants(span)
domElementPool.freeElementAndDescendants(div)
domElementPool.clear()
expect(domElementPool.build("span")).not.toBe(span)
expect(domElementPool.build("div")).not.toBe(div)
it "throws an error when trying to free the same node twice", ->
div = domElementPool.build("div")
domElementPool.freeElementAndDescendants(div)
expect(-> domElementPool.freeElementAndDescendants(div)).toThrow()
it "throws an error when trying to free an invalid element", ->
expect(-> domElementPool.freeElementAndDescendants(null)).toThrow()
expect(-> domElementPool.freeElementAndDescendants(undefined)).toThrow()

View File

@@ -1,5 +1,6 @@
Gutter = require '../src/gutter'
GutterContainerComponent = require '../src/gutter-container-component'
DOMElementPool = require '../src/dom-element-pool'
describe "GutterContainerComponent", ->
[gutterContainerComponent] = []
@@ -22,9 +23,10 @@ describe "GutterContainerComponent", ->
mockTestState
beforeEach ->
domElementPool = new DOMElementPool
mockEditor = {}
mockMouseDown = ->
gutterContainerComponent = new GutterContainerComponent({editor: mockEditor, onMouseDown: mockMouseDown})
gutterContainerComponent = new GutterContainerComponent({editor: mockEditor, onMouseDown: mockMouseDown, domElementPool})
it "creates a DOM node with no child gutter nodes when it is initialized", ->
expect(gutterContainerComponent.getDomNode() instanceof HTMLElement).toBe true

View File

@@ -986,10 +986,11 @@ describe "TextEditorComponent", ->
cursor = componentNode.querySelector('.cursor')
cursorRect = cursor.getBoundingClientRect()
cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.source.js').firstChild
cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.source.js').childNodes[2]
range = document.createRange()
range.setStart(cursorLocationTextNode, 3)
range.setEnd(cursorLocationTextNode, 4)
range.setStart(cursorLocationTextNode, 0)
range.setEnd(cursorLocationTextNode, 1)
rangeRect = range.getBoundingClientRect()
expect(cursorRect.left).toBe rangeRect.left

View File

@@ -17,9 +17,6 @@ describe 'text utilities', ->
expect(textUtils.hasPairedCharacter('\uFE0E\uFE0E')).toBe false
expect(textUtils.hasPairedCharacter('\u0301\u0301')).toBe false
expect(textUtils.hasPairedCharacter('\0\u0301')).toBe false
expect(textUtils.hasPairedCharacter('\0\uFE0E')).toBe false
describe '.isPairedCharacter(string, index)', ->
it 'returns true when the index is the start of a high/low surrogate pair, variation sequence, or combined character', ->
expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 0)).toBe false
@@ -47,6 +44,3 @@ describe 'text utilities', ->
expect(textUtils.isPairedCharacter('ae\u0301c', 2)).toBe false
expect(textUtils.isPairedCharacter('ae\u0301c', 3)).toBe false
expect(textUtils.isPairedCharacter('ae\u0301c', 4)).toBe false
expect(textUtils.isPairedCharacter('\0\u0301c', 0)).toBe false
expect(textUtils.isPairedCharacter('\0\uFE0E', 0)).toBe false

View File

@@ -0,0 +1,40 @@
module.exports =
class DOMElementPool
constructor: ->
@freeElementsByTagName = {}
@freedElements = new Set
clear: ->
@freedElements.clear()
for tagName, freeElements of @freeElementsByTagName
freeElements.length = 0
build: (tagName, className, textContent = "") ->
element = @freeElementsByTagName[tagName]?.pop()
element ?= document.createElement(tagName)
delete element.dataset[dataId] for dataId of element.dataset
element.removeAttribute("class")
element.removeAttribute("style")
element.className = className if className?
element.textContent = textContent
@freedElements.delete(element)
element
freeElementAndDescendants: (element) ->
@free(element)
for index in [element.children.length - 1..0] by -1
child = element.children[index]
@freeElementAndDescendants(child)
free: (element) ->
throw new Error("The element cannot be null or undefined.") unless element?
throw new Error("The element has already been freed!") if @freedElements.has(element)
tagName = element.tagName.toLowerCase()
@freeElementsByTagName[tagName] ?= []
@freeElementsByTagName[tagName].push(element)
@freedElements.add(element)
element.remove()

View File

@@ -7,7 +7,7 @@ LineNumberGutterComponent = require './line-number-gutter-component'
module.exports =
class GutterContainerComponent
constructor: ({@onLineNumberGutterMouseDown, @editor}) ->
constructor: ({@onLineNumberGutterMouseDown, @editor, @domElementPool}) ->
# An array of objects of the form: {name: {String}, component: {Object}}
@gutterComponents = []
@gutterComponentsByGutterName = {}
@@ -39,7 +39,7 @@ class GutterContainerComponent
gutterComponent = @gutterComponentsByGutterName[gutter.name]
if not gutterComponent
if gutter.name is 'line-number'
gutterComponent = new LineNumberGutterComponent({onMouseDown: @onLineNumberGutterMouseDown, @editor, gutter})
gutterComponent = new LineNumberGutterComponent({onMouseDown: @onLineNumberGutterMouseDown, @editor, gutter, @domElementPool})
@lineNumberGutterComponent = gutterComponent
else
gutterComponent = new CustomGutterComponent({gutter})

View File

@@ -5,12 +5,11 @@ module.exports =
class HighlightsComponent
oldState: null
constructor: ->
constructor: (@domElementPool) ->
@highlightNodesById = {}
@regionNodesByHighlightId = {}
@domNode = document.createElement('div')
@domNode.classList.add('highlights')
@domNode = @domElementPool.build("div", "highlights")
getDomNode: ->
@domNode
@@ -30,8 +29,7 @@ class HighlightsComponent
# add or update highlights
for id, highlightState of newState
unless @oldState[id]?
highlightNode = document.createElement('div')
highlightNode.classList.add('highlight')
highlightNode = @domElementPool.build("div", "highlight")
@highlightNodesById[id] = highlightNode
@regionNodesByHighlightId[id] = {}
@domNode.appendChild(highlightNode)
@@ -75,12 +73,11 @@ class HighlightsComponent
for newRegionState, i in newHighlightState.regions
unless oldHighlightState.regions[i]?
oldHighlightState.regions[i] = {}
regionNode = document.createElement('div')
regionNode = @domElementPool.build("div", "region")
# This prevents highlights at the tiles boundaries to be hidden by the
# subsequent tile. When this happens, subpixel anti-aliasing gets
# disabled.
regionNode.style.boxSizing = "border-box"
regionNode.classList.add('region')
regionNode.classList.add(newHighlightState.deprecatedRegionClass) if newHighlightState.deprecatedRegionClass?
@regionNodesByHighlightId[id][i] = regionNode
highlightNode.appendChild(regionNode)

View File

@@ -1,15 +1,17 @@
TiledComponent = require './tiled-component'
LineNumbersTileComponent = require './line-numbers-tile-component'
WrapperDiv = document.createElement('div')
DummyLineNumberComponent = LineNumbersTileComponent.createDummy()
DOMElementPool = require './dom-element-pool'
module.exports =
class LineNumberGutterComponent extends TiledComponent
dummyLineNumberNode: null
constructor: ({@onMouseDown, @editor, @gutter}) ->
constructor: ({@onMouseDown, @editor, @gutter, @domElementPool}) ->
@visible = true
@dummyLineNumberComponent = LineNumbersTileComponent.createDummy(@domElementPool)
@domNode = atom.views.getView(@gutter)
@lineNumbersNode = @domNode.firstChild
@lineNumbersNode.innerHTML = ''
@@ -60,7 +62,7 @@ class LineNumberGutterComponent extends TiledComponent
@oldState.styles = {}
@oldState.maxLineNumberDigits = @newState.maxLineNumberDigits
buildComponentForTile: (id) -> new LineNumbersTileComponent({id})
buildComponentForTile: (id) -> new LineNumbersTileComponent({id, @domElementPool})
###
Section: Private Methods
@@ -69,14 +71,13 @@ class LineNumberGutterComponent extends TiledComponent
# This dummy line number element holds the gutter to the appropriate width,
# since the real line numbers are absolutely positioned for performance reasons.
appendDummyLineNumber: ->
DummyLineNumberComponent.newState = @newState
WrapperDiv.innerHTML = DummyLineNumberComponent.buildLineNumberHTML({bufferRow: -1})
@dummyLineNumberNode = WrapperDiv.children[0]
@dummyLineNumberComponent.newState = @newState
@dummyLineNumberNode = @dummyLineNumberComponent.buildLineNumberNode({bufferRow: -1})
@lineNumbersNode.appendChild(@dummyLineNumberNode)
updateDummyLineNumber: ->
DummyLineNumberComponent.newState = @newState
@dummyLineNumberNode.innerHTML = DummyLineNumberComponent.buildLineNumberInnerHTML(0, false)
@dummyLineNumberComponent.newState = @newState
@dummyLineNumberComponent.setLineNumberInnerNodes(0, false, @dummyLineNumberNode)
onMouseDown: (event) =>
{target} = event

View File

@@ -1,18 +1,20 @@
_ = require 'underscore-plus'
WrapperDiv = document.createElement('div')
module.exports =
class LineNumbersTileComponent
@createDummy: ->
new LineNumbersTileComponent({id: -1})
@createDummy: (domElementPool) ->
new LineNumbersTileComponent({id: -1, domElementPool})
constructor: ({@id}) ->
constructor: ({@id, @domElementPool}) ->
@lineNumberNodesById = {}
@domNode = document.createElement("div")
@domNode = @domElementPool.build("div")
@domNode.style.position = "absolute"
@domNode.style.display = "block"
@domNode.style.top = 0 # Cover the space occupied by a dummy lineNumber
destroy: ->
@domElementPool.freeElementAndDescendants(@domNode)
getDomNode: ->
@domNode
@@ -46,7 +48,9 @@ class LineNumbersTileComponent
@oldTileState.zIndex = @newTileState.zIndex
if @newState.maxLineNumberDigits isnt @oldState.maxLineNumberDigits
node.remove() for id, node of @lineNumberNodesById
for id, node of @lineNumberNodesById
@domElementPool.freeElementAndDescendants(node)
@oldState.tiles[@id] = {lineNumbers: {}}
@oldTileState = @oldState.tiles[@id]
@lineNumberNodesById = {}
@@ -56,11 +60,11 @@ class LineNumbersTileComponent
updateLineNumbers: ->
newLineNumberIds = null
newLineNumbersHTML = null
newLineNumberNodes = null
for id, lineNumberState of @oldTileState.lineNumbers
unless @newTileState.lineNumbers.hasOwnProperty(id)
@lineNumberNodesById[id].remove()
@domElementPool.freeElementAndDescendants(@lineNumberNodesById[id])
delete @lineNumberNodesById[id]
delete @oldTileState.lineNumbers[id]
@@ -69,16 +73,13 @@ class LineNumbersTileComponent
@updateLineNumberNode(id, lineNumberState)
else
newLineNumberIds ?= []
newLineNumbersHTML ?= ""
newLineNumberNodes ?= []
newLineNumberIds.push(id)
newLineNumbersHTML += @buildLineNumberHTML(lineNumberState)
newLineNumberNodes.push(@buildLineNumberNode(lineNumberState))
@oldTileState.lineNumbers[id] = _.clone(lineNumberState)
return unless newLineNumberIds?
WrapperDiv.innerHTML = newLineNumbersHTML
newLineNumberNodes = _.toArray(WrapperDiv.children)
for id, i in newLineNumberIds
lineNumberNode = newLineNumberNodes[i]
@lineNumberNodesById[id] = lineNumberNode
@@ -94,14 +95,18 @@ class LineNumbersTileComponent
screenRowForNode: (node) -> parseInt(node.dataset.screenRow)
buildLineNumberHTML: (lineNumberState) ->
{screenRow, bufferRow, softWrapped, top, decorationClasses} = lineNumberState
buildLineNumberNode: (lineNumberState) ->
{screenRow, bufferRow, softWrapped, top, decorationClasses, zIndex} = lineNumberState
className = @buildLineNumberClassName(lineNumberState)
innerHTML = @buildLineNumberInnerHTML(bufferRow, softWrapped)
lineNumberNode = @domElementPool.build("div", className)
lineNumberNode.dataset.screenRow = screenRow
lineNumberNode.dataset.bufferRow = bufferRow
"<div class=\"#{className}\" data-buffer-row=\"#{bufferRow}\" data-screen-row=\"#{screenRow}\">#{innerHTML}</div>"
@setLineNumberInnerNodes(bufferRow, softWrapped, lineNumberNode)
lineNumberNode
buildLineNumberInnerHTML: (bufferRow, softWrapped) ->
setLineNumberInnerNodes: (bufferRow, softWrapped, lineNumberNode) ->
{maxLineNumberDigits} = @newState
if softWrapped
@@ -109,9 +114,11 @@ class LineNumbersTileComponent
else
lineNumber = (bufferRow + 1).toString()
padding = _.multiplyString('&nbsp;', maxLineNumberDigits - lineNumber.length)
iconHTML = '<div class="icon-right"></div>'
padding + lineNumber + iconHTML
padding = _.multiplyString("\u00a0", maxLineNumberDigits - lineNumber.length)
iconRight = @domElementPool.build("div", "icon-right")
lineNumberNode.textContent = padding + lineNumber
lineNumberNode.appendChild(iconRight)
updateLineNumberNode: (lineNumberId, newLineNumberState) ->
oldLineNumberState = @oldTileState.lineNumbers[lineNumberId]
@@ -123,7 +130,7 @@ class LineNumbersTileComponent
oldLineNumberState.decorationClasses = _.clone(newLineNumberState.decorationClasses)
unless oldLineNumberState.screenRow is newLineNumberState.screenRow and oldLineNumberState.bufferRow is newLineNumberState.bufferRow
node.innerHTML = @buildLineNumberInnerHTML(newLineNumberState.bufferRow, newLineNumberState.softWrapped)
@setLineNumberInnerNodes(newLineNumberState.bufferRow, newLineNumberState.softWrapped, node)
node.dataset.screenRow = newLineNumberState.screenRow
node.dataset.bufferRow = newLineNumberState.bufferRow
oldLineNumberState.screenRow = newLineNumberState.screenRow

View File

@@ -10,7 +10,7 @@ module.exports =
class LinesComponent extends TiledComponent
placeholderTextDiv: null
constructor: ({@presenter, @hostElement, @useShadowDOM, visible}) ->
constructor: ({@presenter, @hostElement, @useShadowDOM, visible, @domElementPool}) ->
@domNode = document.createElement('div')
@domNode.classList.add('lines')
@tilesNode = document.createElement("div")
@@ -60,7 +60,7 @@ class LinesComponent extends TiledComponent
@oldState.indentGuidesVisible = @newState.indentGuidesVisible
buildComponentForTile: (id) -> new LinesTileComponent({id, @presenter})
buildComponentForTile: (id) -> new LinesTileComponent({id, @presenter, @domElementPool})
buildEmptyState: ->
{tiles: {}}

View File

@@ -3,7 +3,6 @@ _ = require 'underscore-plus'
HighlightsComponent = require './highlights-component'
TokenIterator = require './token-iterator'
AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT}
WrapperDiv = document.createElement('div')
TokenTextEscapeRegex = /[&"'<>]/g
MaxTokenLength = 20000
@@ -14,19 +13,22 @@ cloneObject = (object) ->
module.exports =
class LinesTileComponent
constructor: ({@presenter, @id}) ->
constructor: ({@presenter, @id, @domElementPool}) ->
@tokenIterator = new TokenIterator
@measuredLines = new Set
@lineNodesByLineId = {}
@screenRowsByLineId = {}
@lineIdsByScreenRow = {}
@domNode = document.createElement("div")
@domNode = @domElementPool.build("div")
@domNode.style.position = "absolute"
@domNode.style.display = "block"
@highlightsComponent = new HighlightsComponent
@highlightsComponent = new HighlightsComponent(@domElementPool)
@domNode.appendChild(@highlightsComponent.getDomNode())
destroy: ->
@domElementPool.freeElementAndDescendants(@domNode)
getDomNode: ->
@domNode
@@ -76,7 +78,7 @@ class LinesTileComponent
return
removeLineNode: (id) ->
@lineNodesByLineId[id].remove()
@domElementPool.freeElementAndDescendants(@lineNodesByLineId[id])
delete @lineNodesByLineId[id]
delete @lineIdsByScreenRow[@screenRowsByLineId[id]]
delete @screenRowsByLineId[id]
@@ -88,24 +90,22 @@ class LinesTileComponent
@removeLineNode(id)
newLineIds = null
newLinesHTML = null
newLineNodes = null
for id, lineState of @newTileState.lines
if @oldTileState.lines.hasOwnProperty(id)
@updateLineNode(id)
else
newLineIds ?= []
newLinesHTML ?= ""
newLineNodes ?= []
newLineIds.push(id)
newLinesHTML += @buildLineHTML(id)
newLineNodes.push(@buildLineNode(id))
@screenRowsByLineId[id] = lineState.screenRow
@lineIdsByScreenRow[lineState.screenRow] = id
@oldTileState.lines[id] = cloneObject(lineState)
return unless newLineIds?
WrapperDiv.innerHTML = newLinesHTML
newLineNodes = _.toArray(WrapperDiv.children)
for id, i in newLineIds
lineNode = newLineNodes[i]
@lineNodesByLineId[id] = lineNode
@@ -122,64 +122,67 @@ class LinesTileComponent
screenRowForNode: (node) -> parseInt(node.dataset.screenRow)
buildLineHTML: (id) ->
buildLineNode: (id) ->
{width} = @newState
{screenRow, tokens, text, top, lineEnding, fold, isSoftWrapped, indentLevel, decorationClasses} = @newTileState.lines[id]
classes = ''
lineNode = @domElementPool.build("div", "line")
lineNode.dataset.screenRow = screenRow
if decorationClasses?
for decorationClass in decorationClasses
classes += decorationClass + ' '
classes += 'line'
lineHTML = "<div class=\"#{classes}\" data-screen-row=\"#{screenRow}\">"
lineNode.classList.add(decorationClass)
if text is ""
lineHTML += @buildEmptyLineInnerHTML(id)
@setEmptyLineInnerNodes(id, lineNode)
else
lineHTML += @buildLineInnerHTML(id)
@setLineInnerNodes(id, lineNode)
lineHTML += '<span class="fold-marker"></span>' if fold
lineHTML += "</div>"
lineHTML
lineNode.appendChild(@domElementPool.build("span", "fold-marker")) if fold
lineNode
buildEmptyLineInnerHTML: (id) ->
setEmptyLineInnerNodes: (id, lineNode) ->
{indentGuidesVisible} = @newState
{indentLevel, tabLength, endOfLineInvisibles} = @newTileState.lines[id]
if indentGuidesVisible and indentLevel > 0
invisibleIndex = 0
lineHTML = ''
for i in [0...indentLevel]
lineHTML += "<span class='indent-guide'>"
indentGuide = @domElementPool.build("span", "indent-guide")
for j in [0...tabLength]
if invisible = endOfLineInvisibles?[invisibleIndex++]
lineHTML += "<span class='invisible-character'>#{invisible}</span>"
indentGuide.appendChild(
@domElementPool.build("span", "invisible-character", invisible)
)
else
lineHTML += ' '
lineHTML += "</span>"
indentGuide.insertAdjacentText("beforeend", " ")
lineNode.appendChild(indentGuide)
while invisibleIndex < endOfLineInvisibles?.length
lineHTML += "<span class='invisible-character'>#{endOfLineInvisibles[invisibleIndex++]}</span>"
lineHTML
invisible = endOfLineInvisibles[invisibleIndex++]
lineNode.appendChild(
@domElementPool.build("span", "invisible-character", invisible)
)
else
@buildEndOfLineHTML(id) or '&nbsp;'
unless @appendEndOfLineNodes(id, lineNode)
lineNode.textContent = "\u00a0"
buildLineInnerHTML: (id) ->
setLineInnerNodes: (id, lineNode) ->
lineState = @newTileState.lines[id]
{firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, invisibles} = lineState
lineIsWhitespaceOnly = firstTrailingWhitespaceIndex is 0
innerHTML = ""
@tokenIterator.reset(lineState)
openScopeNode = lineNode
while @tokenIterator.next()
for scope in @tokenIterator.getScopeEnds()
innerHTML += "</span>"
openScopeNode = openScopeNode.parentElement
for scope in @tokenIterator.getScopeStarts()
innerHTML += "<span class=\"#{scope.replace(/\.+/g, ' ')}\">"
newScopeNode = @domElementPool.build("span", scope.replace(/\.+/g, ' '))
openScopeNode.appendChild(newScopeNode)
openScopeNode = newScopeNode
tokenStart = @tokenIterator.getScreenStart()
tokenEnd = @tokenIterator.getScreenEnd()
@@ -204,87 +207,79 @@ class LinesTileComponent
(invisibles?.tab and isHardTab) or
(invisibles?.space and (hasLeadingWhitespace or hasTrailingWhitespace))
innerHTML += @buildTokenHTML(tokenText, isHardTab, tokenFirstNonWhitespaceIndex, tokenFirstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters)
@appendTokenNodes(tokenText, isHardTab, tokenFirstNonWhitespaceIndex, tokenFirstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters, openScopeNode)
for scope in @tokenIterator.getScopeEnds()
innerHTML += "</span>"
@appendEndOfLineNodes(id, lineNode)
for scope in @tokenIterator.getScopes()
innerHTML += "</span>"
innerHTML += @buildEndOfLineHTML(id)
innerHTML
buildTokenHTML: (tokenText, isHardTab, firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters) ->
appendTokenNodes: (tokenText, isHardTab, firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters, scopeNode) ->
if isHardTab
classes = 'hard-tab'
classes += ' leading-whitespace' if firstNonWhitespaceIndex?
classes += ' trailing-whitespace' if firstTrailingWhitespaceIndex?
classes += ' indent-guide' if hasIndentGuide
classes += ' invisible-character' if hasInvisibleCharacters
return "<span class='#{classes}'>#{@escapeTokenText(tokenText)}</span>"
hardTabNode = @domElementPool.build("span", "hard-tab", tokenText)
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
scopeNode.appendChild(hardTabNode)
else
startIndex = 0
endIndex = tokenText.length
leadingHtml = ''
trailingHtml = ''
leadingWhitespaceNode = null
trailingWhitespaceNode = null
if firstNonWhitespaceIndex?
leadingWhitespace = tokenText.substring(0, firstNonWhitespaceIndex)
leadingWhitespaceNode = @domElementPool.build(
"span",
"leading-whitespace",
tokenText.substring(0, firstNonWhitespaceIndex)
)
leadingWhitespaceNode.classList.add("indent-guide") if hasIndentGuide
leadingWhitespaceNode.classList.add("invisible-character") if hasInvisibleCharacters
classes = 'leading-whitespace'
classes += ' indent-guide' if hasIndentGuide
classes += ' invisible-character' if hasInvisibleCharacters
leadingHtml = "<span class='#{classes}'>#{leadingWhitespace}</span>"
startIndex = firstNonWhitespaceIndex
if firstTrailingWhitespaceIndex?
tokenIsOnlyWhitespace = firstTrailingWhitespaceIndex is 0
trailingWhitespace = tokenText.substring(firstTrailingWhitespaceIndex)
classes = 'trailing-whitespace'
classes += ' indent-guide' if hasIndentGuide and not firstNonWhitespaceIndex? and tokenIsOnlyWhitespace
classes += ' invisible-character' if hasInvisibleCharacters
trailingHtml = "<span class='#{classes}'>#{trailingWhitespace}</span>"
trailingWhitespaceNode = @domElementPool.build(
"span",
"trailing-whitespace",
tokenText.substring(firstTrailingWhitespaceIndex)
)
trailingWhitespaceNode.classList.add("indent-guide") if hasIndentGuide and not firstNonWhitespaceIndex? and tokenIsOnlyWhitespace
trailingWhitespaceNode.classList.add("invisible-character") if hasInvisibleCharacters
endIndex = firstTrailingWhitespaceIndex
html = leadingHtml
scopeNode.appendChild(leadingWhitespaceNode) if leadingWhitespaceNode?
if tokenText.length > MaxTokenLength
while startIndex < endIndex
html += "<span>" + @escapeTokenText(tokenText, startIndex, startIndex + MaxTokenLength) + "</span>"
text = @sliceText(tokenText, startIndex, startIndex + MaxTokenLength)
scopeNode.appendChild(@domElementPool.build("span", null, text))
startIndex += MaxTokenLength
else
html += @escapeTokenText(tokenText, startIndex, endIndex)
scopeNode.insertAdjacentText("beforeend", @sliceText(tokenText, startIndex, endIndex))
html += trailingHtml
html
scopeNode.appendChild(trailingWhitespaceNode) if trailingWhitespaceNode?
escapeTokenText: (tokenText, startIndex, endIndex) ->
sliceText: (tokenText, startIndex, endIndex) ->
if startIndex? and endIndex? and startIndex > 0 or endIndex < tokenText.length
tokenText = tokenText.slice(startIndex, endIndex)
tokenText.replace(TokenTextEscapeRegex, @escapeTokenTextReplace)
tokenText
escapeTokenTextReplace: (match) ->
switch match
when '&' then '&amp;'
when '"' then '&quot;'
when "'" then '&#39;'
when '<' then '&lt;'
when '>' then '&gt;'
else match
buildEndOfLineHTML: (id) ->
appendEndOfLineNodes: (id, lineNode) ->
{endOfLineInvisibles} = @newTileState.lines[id]
html = ''
hasInvisibles = false
if endOfLineInvisibles?
for invisible in endOfLineInvisibles
html += "<span class='invisible-character'>#{invisible}</span>"
html
hasInvisibles = true
lineNode.appendChild(
@domElementPool.build("span", "invisible-character", invisible)
)
hasInvisibles
updateLineNode: (id) ->
oldLineState = @oldTileState.lines[id]
@@ -344,8 +339,6 @@ class LinesTileComponent
charLength = 1
textIndex++
continue if char is '\0'
unless charWidths[char]?
unless textNode?
rangeForMeasurement ?= document.createRange()

View File

@@ -12,6 +12,7 @@ LinesComponent = require './lines-component'
ScrollbarComponent = require './scrollbar-component'
ScrollbarCornerComponent = require './scrollbar-corner-component'
OverlayManager = require './overlay-manager'
DOMElementPool = require './dom-element-pool'
module.exports =
class TextEditorComponent
@@ -54,6 +55,8 @@ class TextEditorComponent
@presenter.onDidUpdateState(@requestUpdate)
@domElementPool = new DOMElementPool
@domNode = document.createElement('div')
if @useShadowDOM
@domNode.classList.add('editor-contents--private')
@@ -75,7 +78,7 @@ class TextEditorComponent
@hiddenInputComponent = new InputComponent
@scrollViewNode.appendChild(@hiddenInputComponent.getDomNode())
@linesComponent = new LinesComponent({@presenter, @hostElement, @useShadowDOM})
@linesComponent = new LinesComponent({@presenter, @hostElement, @useShadowDOM, @domElementPool})
@scrollViewNode.appendChild(@linesComponent.getDomNode())
@horizontalScrollbarComponent = new ScrollbarComponent({orientation: 'horizontal', onScroll: @onHorizontalScroll})
@@ -107,6 +110,7 @@ class TextEditorComponent
@disposables.dispose()
@presenter.destroy()
@gutterContainerComponent?.destroy()
@domElementPool.clear()
getDomNode: ->
@domNode
@@ -152,6 +156,10 @@ class TextEditorComponent
@overlayManager?.render(@newState)
if @clearPoolAfterUpdate
@domElementPool.clear()
@clearPoolAfterUpdate = false
if @editor.isAlive()
@updateParentViewFocusedClassIfNeeded()
@updateParentViewMiniClass()
@@ -165,7 +173,7 @@ class TextEditorComponent
@overlayManager?.measureOverlays()
mountGutterContainerComponent: ->
@gutterContainerComponent = new GutterContainerComponent({@editor, @onLineNumberGutterMouseDown})
@gutterContainerComponent = new GutterContainerComponent({@editor, @onLineNumberGutterMouseDown, @domElementPool})
@domNode.insertBefore(@gutterContainerComponent.getDomNode(), @domNode.firstChild)
becameVisible: ->
@@ -649,6 +657,7 @@ class TextEditorComponent
{@fontSize, @fontFamily, @lineHeight} = getComputedStyle(@getTopmostDOMNode())
if @fontSize isnt oldFontSize or @fontFamily isnt oldFontFamily or @lineHeight isnt oldLineHeight
@clearPoolAfterUpdate = true
@measureLineHeightAndDefaultCharWidth()
if (@fontSize isnt oldFontSize or @fontFamily isnt oldFontFamily) and @performedInitialMeasurement

View File

@@ -30,7 +30,6 @@ isSurrogatePair = (charCodeA, charCodeB) ->
#
# Return a {Boolean}.
isVariationSequence = (charCodeA, charCodeB) ->
return false if charCodeA is 0
not isVariationSelector(charCodeA) and isVariationSelector(charCodeB)
# Are the given character codes a combined character pair?
@@ -40,7 +39,6 @@ isVariationSequence = (charCodeA, charCodeB) ->
#
# Return a {Boolean}.
isCombinedCharacter = (charCodeA, charCodeB) ->
return false if charCodeA is 0
not isCombiningCharacter(charCodeA) and isCombiningCharacter(charCodeB)
# Is the character at the given index the start of high/low surrogate pair

View File

@@ -23,9 +23,7 @@ class TiledComponent
return
removeTileNode: (tileRow) ->
node = @componentsByTileId[tileRow].getDomNode()
node.remove()
@componentsByTileId[tileRow].destroy()
delete @componentsByTileId[tileRow]
delete @oldState.tiles[tileRow]