From d0be7fbf8eb39992c8e9bd26a8528985a58d8673 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 2 Oct 2013 18:34:03 -0700 Subject: [PATCH] Add a character width cache based on scopes. This is slower than the position cache in the best case, but faster in the worst and average case. With this, you can search for a space in find and replace, and still scroll the buffer. In editor.coffee, there are 10,500 spaces. To highlight all of them, the previous cache method took 7 seconds, this takes 2 when the cache is empty, and about 10ms when the entire file is cached. --- src/editor.coffee | 103 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 84 insertions(+), 19 deletions(-) diff --git a/src/editor.coffee b/src/editor.coffee index f5df9f6ef..b67474591 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -10,12 +10,15 @@ $ = require './jquery-extensions' _ = require './underscore-extensions' MEASURE_RANGE = document.createRange() +TEXT_NODE_FILTER = { acceptNode: -> NodeFilter.FILTER_ACCEPT } +NO_SCOPE = ['no-scope'] # Private: Represents the entire visual pane in Atom. # # The Editor manages the {EditSession}, which manages the file buffers. module.exports = class Editor extends View + @characterWidthCache: {} @configDefaults: fontSize: 20 showInvisibles: false @@ -96,7 +99,6 @@ class Editor extends View @pendingChanges = [] @newCursors = [] @newSelections = [] - @pixelLeftCache = new WeakMap() if editSession? @edit(editSession) @@ -969,6 +971,9 @@ class Editor extends View # fontSize - A {Number} indicating the font size in pixels. setFontSize: (fontSize) -> @css('font-size', "#{fontSize}px}") + + @clearCharacterWidthCache() + if @isOnDom() @redraw() else @@ -985,6 +990,9 @@ class Editor extends View # fontFamily - A {String} identifying the CSS `font-family`, setFontFamily: (fontFamily='') -> @css('font-family', fontFamily) + + @clearCharacterWidthCache() + @redraw() # Gets the font family for the editor. @@ -1366,7 +1374,6 @@ class Editor extends View currentLine = clearLine(currentLine) clearLine: (lineElement) => - @pixelLeftCache.delete(lineElement) next = lineElement.nextSibling @renderedLines[0].removeChild(lineElement) next @@ -1545,33 +1552,91 @@ class Editor extends View unless existingLineElement lineElement = @buildLineElementForScreenRow(actualRow) @renderedLines.append(lineElement) - left = @positionLeftForLineAndColumn(lineElement, column) + left = @positionLeftForLineAndColumn(lineElement, actualRow, column) unless existingLineElement @renderedLines[0].removeChild(lineElement) { top: row * @lineHeight, left } - positionLeftForLineAndColumn: (lineElement, column) -> - lineCache = @pixelLeftCache.get(lineElement) - @pixelLeftCache.set(lineElement, lineCache = {}) unless lineCache? + positionLeftForLineAndColumn: (lineElement, screenRow, column) -> + return 0 if column == 0 - return lineCache[column] if lineCache[column]? + bufferRow = @bufferRowsForScreenRows(screenRow)[0] ? screenRow + tokenizedLine = @activeEditSession.displayBuffer.tokenizedBuffer.tokenizedLines[bufferRow] - delta = 0 - iterator = document.createNodeIterator(lineElement, NodeFilter.SHOW_TEXT, acceptNode: -> NodeFilter.FILTER_ACCEPT) - while textNode = iterator.nextNode() - nextDelta = delta + textNode.textContent.length - if nextDelta >= column - offset = column - delta - break - delta = nextDelta + left = 0 + index = 0 + for token in tokenizedLine.tokens + for char in token.value + return left if index >= column - MEASURE_RANGE.setEnd(textNode, offset) - MEASURE_RANGE.collapse() - left = MEASURE_RANGE.getClientRects()[0].left - Math.floor(@scrollView.offset().left) + Math.floor(@scrollLeft()) + val = @checkCharacterWidthCache(token.scopes, char) + if val? + left += val + else + return @measureToColumn(lineElement, tokenizedLine, column) - lineCache[column] = left + index++ left + scopesForColumn: (tokenizedLine, column) -> + index = 0 + for token in tokenizedLine.tokens + for char in token.value + return token.scopes if index == column + index++ + null + + measureToColumn: (lineElement, tokenizedLine, column) -> + left = oldLeft = index = 0 + iterator = document.createNodeIterator(lineElement, NodeFilter.SHOW_TEXT, TEXT_NODE_FILTER) + + returnLeft = null + + while textNode = iterator.nextNode() + content = textNode.textContent + + for char, i in content + + # Dont return right away, finish caching the whole line + returnLeft = left if index == column + oldLeft = left + + scopes = @scopesForColumn(tokenizedLine, index) + cachedVal = @checkCharacterWidthCache(scopes, char) + + if cachedVal? + left = oldLeft + cachedVal + else + # i + 1 to measure to the end of the current character + MEASURE_RANGE.setEnd(textNode, i + 1) + MEASURE_RANGE.collapse() + left = MEASURE_RANGE.getClientRects()[0].left - Math.floor(@scrollView.offset().left) + Math.floor(@scrollLeft()) + + @setCharacterWidthCache(scopes, char, left - oldLeft) if scopes? + + index++ + + returnLeft ? left + + checkCharacterWidthCache: (scopes, char) -> + scopes ?= NO_SCOPE + obj = Editor.characterWidthCache + for scope in scopes + obj = obj[scope] + return null unless obj? + obj[char] + + setCharacterWidthCache: (scopes, char, val) -> + scopes ?= NO_SCOPE + obj = Editor.characterWidthCache + for scope in scopes + obj[scope] ?= {} + obj = obj[scope] + obj[char] = val + + clearCharacterWidthCache: -> + Editor.characterWidthCache = {} + pixelOffsetForScreenPosition: (position) -> {top, left} = @pixelPositionForScreenPosition(position) offset = @renderedLines.offset()