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.
This commit is contained in:
Ben Ogle
2013-10-02 18:34:03 -07:00
parent 8463c759b5
commit d0be7fbf8e

View File

@@ -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()