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]