diff --git a/spec/dom-element-pool-spec.coffee b/spec/dom-element-pool-spec.coffee
new file mode 100644
index 000000000..1399b17fc
--- /dev/null
+++ b/spec/dom-element-pool-spec.coffee
@@ -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()
diff --git a/spec/gutter-container-component-spec.coffee b/spec/gutter-container-component-spec.coffee
index 5d815fea8..595067de5 100644
--- a/spec/gutter-container-component-spec.coffee
+++ b/spec/gutter-container-component-spec.coffee
@@ -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
diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee
index 2baa90ac7..638ab6917 100644
--- a/spec/text-editor-component-spec.coffee
+++ b/spec/text-editor-component-spec.coffee
@@ -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
diff --git a/spec/text-utils-spec.coffee b/spec/text-utils-spec.coffee
index f5c2c87f9..6a03bb02f 100644
--- a/spec/text-utils-spec.coffee
+++ b/spec/text-utils-spec.coffee
@@ -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
diff --git a/src/dom-element-pool.coffee b/src/dom-element-pool.coffee
new file mode 100644
index 000000000..98049a9f6
--- /dev/null
+++ b/src/dom-element-pool.coffee
@@ -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()
diff --git a/src/gutter-container-component.coffee b/src/gutter-container-component.coffee
index 09ab43f24..40e2c8c26 100644
--- a/src/gutter-container-component.coffee
+++ b/src/gutter-container-component.coffee
@@ -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})
diff --git a/src/highlights-component.coffee b/src/highlights-component.coffee
index 522dcb304..4c5e138d8 100644
--- a/src/highlights-component.coffee
+++ b/src/highlights-component.coffee
@@ -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)
diff --git a/src/line-number-gutter-component.coffee b/src/line-number-gutter-component.coffee
index 8eef6bd27..f73068c30 100644
--- a/src/line-number-gutter-component.coffee
+++ b/src/line-number-gutter-component.coffee
@@ -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
diff --git a/src/line-numbers-tile-component.coffee b/src/line-numbers-tile-component.coffee
index f00c968fc..19a9868ba 100644
--- a/src/line-numbers-tile-component.coffee
+++ b/src/line-numbers-tile-component.coffee
@@ -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
- "
#{innerHTML}
"
+ @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(' ', maxLineNumberDigits - lineNumber.length)
- iconHTML = ''
- 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
diff --git a/src/lines-component.coffee b/src/lines-component.coffee
index 2b80235dc..b618563be 100644
--- a/src/lines-component.coffee
+++ b/src/lines-component.coffee
@@ -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: {}}
diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee
index 7f6de6397..627630e03 100644
--- a/src/lines-tile-component.coffee
+++ b/src/lines-tile-component.coffee
@@ -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 = ""
+ lineNode.classList.add(decorationClass)
if text is ""
- lineHTML += @buildEmptyLineInnerHTML(id)
+ @setEmptyLineInnerNodes(id, lineNode)
else
- lineHTML += @buildLineInnerHTML(id)
+ @setLineInnerNodes(id, lineNode)
- lineHTML += '' if fold
- lineHTML += "
"
- 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 += ""
+ indentGuide = @domElementPool.build("span", "indent-guide")
for j in [0...tabLength]
if invisible = endOfLineInvisibles?[invisibleIndex++]
- lineHTML += "#{invisible}"
+ indentGuide.appendChild(
+ @domElementPool.build("span", "invisible-character", invisible)
+ )
else
- lineHTML += ' '
- lineHTML += ""
+ indentGuide.insertAdjacentText("beforeend", " ")
+ lineNode.appendChild(indentGuide)
while invisibleIndex < endOfLineInvisibles?.length
- lineHTML += "#{endOfLineInvisibles[invisibleIndex++]}"
-
- lineHTML
+ invisible = endOfLineInvisibles[invisibleIndex++]
+ lineNode.appendChild(
+ @domElementPool.build("span", "invisible-character", invisible)
+ )
else
- @buildEndOfLineHTML(id) or ' '
+ 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 += ""
+ openScopeNode = openScopeNode.parentElement
for scope in @tokenIterator.getScopeStarts()
- innerHTML += ""
+ 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 += ""
+ @appendEndOfLineNodes(id, lineNode)
- for scope in @tokenIterator.getScopes()
- innerHTML += ""
-
- 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 "#{@escapeTokenText(tokenText)}"
+ 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 = "#{leadingWhitespace}"
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 = "#{trailingWhitespace}"
+ 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 += "" + @escapeTokenText(tokenText, startIndex, startIndex + MaxTokenLength) + ""
+ 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 '&'
- when '"' then '"'
- when "'" then '''
- when '<' then '<'
- when '>' then '>'
- else match
-
- buildEndOfLineHTML: (id) ->
+ appendEndOfLineNodes: (id, lineNode) ->
{endOfLineInvisibles} = @newTileState.lines[id]
- html = ''
+ hasInvisibles = false
if endOfLineInvisibles?
for invisible in endOfLineInvisibles
- html += "#{invisible}"
- 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()
diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee
index 73b03ce88..7ce212f34 100644
--- a/src/text-editor-component.coffee
+++ b/src/text-editor-component.coffee
@@ -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
diff --git a/src/text-utils.coffee b/src/text-utils.coffee
index ec3ca0c29..37955dcd6 100644
--- a/src/text-utils.coffee
+++ b/src/text-utils.coffee
@@ -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
diff --git a/src/tiled-component.coffee b/src/tiled-component.coffee
index 06433f8be..2e8dc7149 100644
--- a/src/tiled-component.coffee
+++ b/src/tiled-component.coffee
@@ -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]