Merge pull request #8811 from atom/as-double-reflow-measurements

DOM-based measurements
This commit is contained in:
Nathan Sobo
2015-10-07 15:43:59 -05:00
16 changed files with 1002 additions and 626 deletions

View File

@@ -28,7 +28,7 @@ class AtomWindow
title: 'Atom'
'web-preferences':
'direct-write': true
'subpixel-font-scaling': false
'subpixel-font-scaling': true
# Don't set icon on Windows so the exe's ico will be used as window and
# taskbar's icon. See https://github.com/atom/atom/issues/4811 for more.
if process.platform is 'linux'

View File

@@ -18,7 +18,6 @@ module.exports =
class DisplayBuffer extends Model
verticalScrollMargin: 2
horizontalScrollMargin: 6
scopedCharacterWidthsChangeCount: 0
changeCount: 0
softWrapped: null
editorWidthInChars: null
@@ -198,35 +197,6 @@ class DisplayBuffer extends Model
getCursorWidth: -> 1
getScopedCharWidth: (scopeNames, char) ->
@getScopedCharWidths(scopeNames)[char]
getScopedCharWidths: (scopeNames) ->
scope = @charWidthsByScope
for scopeName in scopeNames
scope[scopeName] ?= {}
scope = scope[scopeName]
scope.charWidths ?= {}
scope.charWidths
batchCharacterMeasurement: (fn) ->
oldChangeCount = @scopedCharacterWidthsChangeCount
@batchingCharacterMeasurement = true
fn()
@batchingCharacterMeasurement = false
@characterWidthsChanged() if oldChangeCount isnt @scopedCharacterWidthsChangeCount
setScopedCharWidth: (scopeNames, char, width) ->
@getScopedCharWidths(scopeNames)[char] = width
@scopedCharacterWidthsChangeCount++
@characterWidthsChanged() unless @batchingCharacterMeasurement
characterWidthsChanged: ->
@emitter.emit 'did-change-character-widths', @scopedCharacterWidthsChangeCount
clearScopedCharWidths: ->
@charWidthsByScope = {}
scrollToScreenRange: (screenRange, options = {}) ->
scrollEvent = {screenRange, options}
@emitter.emit "did-request-autoscroll", scrollEvent

View File

@@ -10,31 +10,44 @@ class DOMElementPool
freeElements.length = 0
return
build: (tagName, className, textContent = "") ->
build: (tagName, factory, reset) ->
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
element ?= factory()
reset(element)
@freedElements.delete(element)
element
buildElement: (tagName, className) ->
factory = -> document.createElement(tagName)
reset = (element) ->
delete element.dataset[dataId] for dataId of element.dataset
element.removeAttribute("style")
if className?
element.className = className
else
element.removeAttribute("class")
@build(tagName, factory, reset)
buildText: (textContent) ->
factory = -> document.createTextNode(textContent)
reset = (element) -> element.textContent = textContent
@build("#text", factory, reset)
freeElementAndDescendants: (element) ->
@free(element)
for index in [element.children.length - 1..0] by -1
child = element.children[index]
@freeElementAndDescendants(child)
@freeDescendants(element)
freeDescendants: (element) ->
for descendant in element.childNodes by -1
@free(descendant)
@freeDescendants(descendant)
return
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()
tagName = element.nodeName.toLowerCase()
@freeElementsByTagName[tagName] ?= []
@freeElementsByTagName[tagName].push(element)
@freedElements.add(element)

View File

@@ -9,7 +9,7 @@ class HighlightsComponent
@highlightNodesById = {}
@regionNodesByHighlightId = {}
@domNode = @domElementPool.build("div", "highlights")
@domNode = @domElementPool.buildElement("div", "highlights")
getDomNode: ->
@domNode
@@ -21,7 +21,7 @@ class HighlightsComponent
# remove highlights
for id of @oldState
unless newState[id]?
@highlightNodesById[id].remove()
@domElementPool.freeElementAndDescendants(@highlightNodesById[id])
delete @highlightNodesById[id]
delete @regionNodesByHighlightId[id]
delete @oldState[id]
@@ -29,7 +29,7 @@ class HighlightsComponent
# add or update highlights
for id, highlightState of newState
unless @oldState[id]?
highlightNode = @domElementPool.build("div", "highlight")
highlightNode = @domElementPool.buildElement("div", "highlight")
@highlightNodesById[id] = highlightNode
@regionNodesByHighlightId[id] = {}
@domNode.appendChild(highlightNode)
@@ -66,14 +66,14 @@ class HighlightsComponent
# remove regions
while oldHighlightState.regions.length > newHighlightState.regions.length
oldHighlightState.regions.pop()
@regionNodesByHighlightId[id][oldHighlightState.regions.length].remove()
@domElementPool.freeElementAndDescendants(@regionNodesByHighlightId[id][oldHighlightState.regions.length])
delete @regionNodesByHighlightId[id][oldHighlightState.regions.length]
# add or update regions
for newRegionState, i in newHighlightState.regions
unless oldHighlightState.regions[i]?
oldHighlightState.regions[i] = {}
regionNode = @domElementPool.build("div", "region")
regionNode = @domElementPool.buildElement("div", "region")
# This prevents highlights at the tiles boundaries to be hidden by the
# subsequent tile. When this happens, subpixel anti-aliasing gets
# disabled.

View File

@@ -7,7 +7,7 @@ class LineNumbersTileComponent
constructor: ({@id, @domElementPool}) ->
@lineNumberNodesById = {}
@domNode = @domElementPool.build("div")
@domNode = @domElementPool.buildElement("div")
@domNode.style.position = "absolute"
@domNode.style.display = "block"
@domNode.style.top = 0 # Cover the space occupied by a dummy lineNumber
@@ -99,7 +99,7 @@ class LineNumbersTileComponent
{screenRow, bufferRow, softWrapped, top, decorationClasses, zIndex} = lineNumberState
className = @buildLineNumberClassName(lineNumberState)
lineNumberNode = @domElementPool.build("div", className)
lineNumberNode = @domElementPool.buildElement("div", className)
lineNumberNode.dataset.screenRow = screenRow
lineNumberNode.dataset.bufferRow = bufferRow
@@ -107,17 +107,20 @@ class LineNumbersTileComponent
lineNumberNode
setLineNumberInnerNodes: (bufferRow, softWrapped, lineNumberNode) ->
@domElementPool.freeDescendants(lineNumberNode)
{maxLineNumberDigits} = @newState
if softWrapped
lineNumber = ""
else
lineNumber = (bufferRow + 1).toString()
padding = _.multiplyString("\u00a0", maxLineNumberDigits - lineNumber.length)
iconRight = @domElementPool.build("div", "icon-right")
lineNumberNode.textContent = padding + lineNumber
textNode = @domElementPool.buildText(padding + lineNumber)
iconRight = @domElementPool.buildElement("div", "icon-right")
lineNumberNode.appendChild(textNode)
lineNumberNode.appendChild(iconRight)
updateLineNumberNode: (lineNumberId, newLineNumberState) ->

View File

@@ -13,7 +13,7 @@ module.exports =
class LinesComponent extends TiledComponent
placeholderTextDiv: null
constructor: ({@presenter, @hostElement, @useShadowDOM, visible, @domElementPool}) ->
constructor: ({@presenter, @useShadowDOM, @domElementPool}) ->
@domNode = document.createElement('div')
@domNode.classList.add('lines')
@tilesNode = document.createElement("div")
@@ -54,6 +54,7 @@ class LinesComponent extends TiledComponent
@placeholderTextDiv.classList.add('placeholder-text')
@placeholderTextDiv.textContent = @newState.placeholderText
@domNode.appendChild(@placeholderTextDiv)
@oldState.placeholderText = @newState.placeholderText
if @newState.width isnt @oldState.width
@domNode.style.width = @newState.width + 'px'
@@ -82,21 +83,10 @@ class LinesComponent extends TiledComponent
@presenter.setLineHeight(lineHeightInPixels)
@presenter.setBaseCharacterWidth(charWidth)
remeasureCharacterWidths: ->
return unless @presenter.baseCharacterWidth
lineNodeForLineIdAndScreenRow: (lineId, screenRow) ->
tile = @presenter.tileForRow(screenRow)
@getComponentForTile(tile)?.lineNodeForLineId(lineId)
@clearScopedCharWidths()
@measureCharactersInNewLines()
measureCharactersInNewLines: ->
@presenter.batchCharacterMeasurement =>
for id, component of @componentsByTileId
component.measureCharactersInNewLines()
return
clearScopedCharWidths: ->
for id, component of @componentsByTileId
component.clearMeasurements()
@presenter.clearScopedCharacterWidths()
textNodesForLineIdAndScreenRow: (lineId, screenRow) ->
tile = @presenter.tileForRow(screenRow)
@getComponentForTile(tile)?.textNodesForLineId(lineId)

View File

@@ -19,7 +19,8 @@ class LinesTileComponent
@lineNodesByLineId = {}
@screenRowsByLineId = {}
@lineIdsByScreenRow = {}
@domNode = @domElementPool.build("div")
@textNodesByLineId = {}
@domNode = @domElementPool.buildElement("div")
@domNode.style.position = "absolute"
@domNode.style.display = "block"
@@ -80,6 +81,7 @@ class LinesTileComponent
removeLineNode: (id) ->
@domElementPool.freeElementAndDescendants(@lineNodesByLineId[id])
delete @lineNodesByLineId[id]
delete @textNodesByLineId[id]
delete @lineIdsByScreenRow[@screenRowsByLineId[id]]
delete @screenRowsByLineId[id]
delete @oldTileState.lines[id]
@@ -126,19 +128,21 @@ class LinesTileComponent
{width} = @newState
{screenRow, tokens, text, top, lineEnding, fold, isSoftWrapped, indentLevel, decorationClasses} = @newTileState.lines[id]
lineNode = @domElementPool.build("div", "line")
lineNode = @domElementPool.buildElement("div", "line")
lineNode.dataset.screenRow = screenRow
if decorationClasses?
for decorationClass in decorationClasses
lineNode.classList.add(decorationClass)
@currentLineTextNodes = []
if text is ""
@setEmptyLineInnerNodes(id, lineNode)
else
@setLineInnerNodes(id, lineNode)
@textNodesByLineId[id] = @currentLineTextNodes
lineNode.appendChild(@domElementPool.build("span", "fold-marker")) if fold
lineNode.appendChild(@domElementPool.buildElement("span", "fold-marker")) if fold
lineNode
setEmptyLineInnerNodes: (id, lineNode) ->
@@ -148,24 +152,36 @@ class LinesTileComponent
if indentGuidesVisible and indentLevel > 0
invisibleIndex = 0
for i in [0...indentLevel]
indentGuide = @domElementPool.build("span", "indent-guide")
indentGuide = @domElementPool.buildElement("span", "indent-guide")
for j in [0...tabLength]
if invisible = endOfLineInvisibles?[invisibleIndex++]
indentGuide.appendChild(
@domElementPool.build("span", "invisible-character", invisible)
)
invisibleSpan = @domElementPool.buildElement("span", "invisible-character")
textNode = @domElementPool.buildText(invisible)
invisibleSpan.appendChild(textNode)
indentGuide.appendChild(invisibleSpan)
@currentLineTextNodes.push(textNode)
else
indentGuide.insertAdjacentText("beforeend", " ")
textNode = @domElementPool.buildText(" ")
indentGuide.appendChild(textNode)
@currentLineTextNodes.push(textNode)
lineNode.appendChild(indentGuide)
while invisibleIndex < endOfLineInvisibles?.length
invisible = endOfLineInvisibles[invisibleIndex++]
lineNode.appendChild(
@domElementPool.build("span", "invisible-character", invisible)
)
invisibleSpan = @domElementPool.buildElement("span", "invisible-character")
textNode = @domElementPool.buildText(invisible)
invisibleSpan.appendChild(textNode)
lineNode.appendChild(invisibleSpan)
@currentLineTextNodes.push(textNode)
else
unless @appendEndOfLineNodes(id, lineNode)
lineNode.textContent = "\u00a0"
textNode = @domElementPool.buildText("\u00a0")
lineNode.appendChild(textNode)
@currentLineTextNodes.push(textNode)
setLineInnerNodes: (id, lineNode) ->
lineState = @newTileState.lines[id]
@@ -180,7 +196,7 @@ class LinesTileComponent
openScopeNode = openScopeNode.parentElement
for scope in @tokenIterator.getScopeStarts()
newScopeNode = @domElementPool.build("span", scope.replace(/\.+/g, ' '))
newScopeNode = @domElementPool.buildElement("span", scope.replace(/\.+/g, ' '))
openScopeNode.appendChild(newScopeNode)
openScopeNode = newScopeNode
@@ -213,55 +229,70 @@ class LinesTileComponent
appendTokenNodes: (tokenText, isHardTab, firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters, scopeNode) ->
if isHardTab
hardTabNode = @domElementPool.build("span", "hard-tab", tokenText)
textNode = @domElementPool.buildText(tokenText)
hardTabNode = @domElementPool.buildElement("span", "hard-tab")
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
hardTabNode.appendChild(textNode)
scopeNode.appendChild(hardTabNode)
@currentLineTextNodes.push(textNode)
else
startIndex = 0
endIndex = tokenText.length
leadingWhitespaceNode = null
leadingWhitespaceTextNode = null
trailingWhitespaceNode = null
trailingWhitespaceTextNode = null
if firstNonWhitespaceIndex?
leadingWhitespaceNode = @domElementPool.build(
"span",
"leading-whitespace",
tokenText.substring(0, firstNonWhitespaceIndex)
)
leadingWhitespaceTextNode =
@domElementPool.buildText(tokenText.substring(0, firstNonWhitespaceIndex))
leadingWhitespaceNode = @domElementPool.buildElement("span", "leading-whitespace")
leadingWhitespaceNode.classList.add("indent-guide") if hasIndentGuide
leadingWhitespaceNode.classList.add("invisible-character") if hasInvisibleCharacters
leadingWhitespaceNode.appendChild(leadingWhitespaceTextNode)
startIndex = firstNonWhitespaceIndex
if firstTrailingWhitespaceIndex?
tokenIsOnlyWhitespace = firstTrailingWhitespaceIndex is 0
trailingWhitespaceNode = @domElementPool.build(
"span",
"trailing-whitespace",
tokenText.substring(firstTrailingWhitespaceIndex)
)
trailingWhitespaceTextNode =
@domElementPool.buildText(tokenText.substring(firstTrailingWhitespaceIndex))
trailingWhitespaceNode = @domElementPool.buildElement("span", "trailing-whitespace")
trailingWhitespaceNode.classList.add("indent-guide") if hasIndentGuide and not firstNonWhitespaceIndex? and tokenIsOnlyWhitespace
trailingWhitespaceNode.classList.add("invisible-character") if hasInvisibleCharacters
trailingWhitespaceNode.appendChild(trailingWhitespaceTextNode)
endIndex = firstTrailingWhitespaceIndex
scopeNode.appendChild(leadingWhitespaceNode) if leadingWhitespaceNode?
if leadingWhitespaceNode?
scopeNode.appendChild(leadingWhitespaceNode)
@currentLineTextNodes.push(leadingWhitespaceTextNode)
if tokenText.length > MaxTokenLength
while startIndex < endIndex
text = @sliceText(tokenText, startIndex, startIndex + MaxTokenLength)
scopeNode.appendChild(@domElementPool.build("span", null, text))
startIndex += MaxTokenLength
else
scopeNode.insertAdjacentText("beforeend", @sliceText(tokenText, startIndex, endIndex))
textNode = @domElementPool.buildText(
@sliceText(tokenText, startIndex, startIndex + MaxTokenLength)
)
textSpan = @domElementPool.buildElement("span")
scopeNode.appendChild(trailingWhitespaceNode) if trailingWhitespaceNode?
textSpan.appendChild(textNode)
scopeNode.appendChild(textSpan)
startIndex += MaxTokenLength
@currentLineTextNodes.push(textNode)
else
textNode = @domElementPool.buildText(@sliceText(tokenText, startIndex, endIndex))
scopeNode.appendChild(textNode)
@currentLineTextNodes.push(textNode)
if trailingWhitespaceNode?
scopeNode.appendChild(trailingWhitespaceNode)
@currentLineTextNodes.push(trailingWhitespaceTextNode)
sliceText: (tokenText, startIndex, endIndex) ->
if startIndex? and endIndex? and startIndex > 0 or endIndex < tokenText.length
@@ -275,9 +306,12 @@ class LinesTileComponent
if endOfLineInvisibles?
for invisible in endOfLineInvisibles
hasInvisibles = true
lineNode.appendChild(
@domElementPool.build("span", "invisible-character", invisible)
)
invisibleSpan = @domElementPool.buildElement("span", "invisible-character")
textNode = @domElementPool.buildText(invisible)
invisibleSpan.appendChild(textNode)
lineNode.appendChild(invisibleSpan)
@currentLineTextNodes.push(textNode)
hasInvisibles
@@ -306,88 +340,13 @@ class LinesTileComponent
lineNode.dataset.screenRow = newLineState.screenRow
oldLineState.screenRow = newLineState.screenRow
@lineIdsByScreenRow[newLineState.screenRow] = id
@screenRowsByLineId[id] = newLineState.screenRow
lineNodeForScreenRow: (screenRow) ->
@lineNodesByLineId[@lineIdsByScreenRow[screenRow]]
measureCharactersInNewLines: ->
for id, lineState of @oldTileState.lines
unless @measuredLines.has(id)
lineNode = @lineNodesByLineId[id]
@measureCharactersInLine(id, lineState, lineNode)
return
lineNodeForLineId: (lineId) ->
@lineNodesByLineId[lineId]
measureCharactersInLine: (lineId, tokenizedLine, lineNode) ->
rangeForMeasurement = null
iterator = null
charIndex = 0
@tokenIterator.reset(tokenizedLine)
while @tokenIterator.next()
scopes = @tokenIterator.getScopes()
text = @tokenIterator.getText()
charWidths = @presenter.getScopedCharacterWidths(scopes)
textIndex = 0
while textIndex < text.length
if @tokenIterator.isPairedCharacter()
char = text
charLength = 2
textIndex += 2
else
char = text[textIndex]
charLength = 1
textIndex++
unless charWidths[char]?
unless textNode?
rangeForMeasurement ?= document.createRange()
iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT, AcceptFilter)
textNode = iterator.nextNode()
textNodeLength = textNode.textContent.length
textNodeIndex = 0
nextTextNodeIndex = textNodeLength
while nextTextNodeIndex <= charIndex
textNode = iterator.nextNode()
textNodeLength = textNode.textContent.length
textNodeIndex = nextTextNodeIndex
nextTextNodeIndex = textNodeIndex + textNodeLength
i = charIndex - textNodeIndex
rangeForMeasurement.setStart(textNode, i)
if i + charLength <= textNodeLength
rangeForMeasurement.setEnd(textNode, i + charLength)
else
rangeForMeasurement.setEnd(textNode, textNodeLength)
atom.assert false, "Expected index to be less than the length of text node while measuring", (error) =>
editor = @presenter.model
screenRow = tokenizedLine.screenRow
bufferRow = editor.bufferRowForScreenRow(screenRow)
error.metadata = {
grammarScopeName: editor.getGrammar().scopeName
screenRow: screenRow
bufferRow: bufferRow
softWrapped: editor.isSoftWrapped()
softTabs: editor.getSoftTabs()
i: i
charLength: charLength
textNodeLength: textNode.length
}
error.privateMetadataDescription = "The contents of line #{bufferRow + 1}."
error.privateMetadata = {
lineText: editor.lineTextForBufferRow(bufferRow)
}
error.privateMetadataRequestName = "measured-line-text"
charWidth = rangeForMeasurement.getBoundingClientRect().width
@presenter.setScopedCharacterWidth(scopes, char, charWidth)
charIndex += charLength
@measuredLines.add(lineId)
clearMeasurements: ->
@measuredLines.clear()
textNodesForLineId: (lineId) ->
@textNodesByLineId[lineId].slice()

187
src/lines-yardstick.coffee Normal file
View File

@@ -0,0 +1,187 @@
TokenIterator = require './token-iterator'
{Point} = require 'text-buffer'
module.exports =
class LinesYardstick
constructor: (@model, @presenter, @lineNodesProvider) ->
@tokenIterator = new TokenIterator
@rangeForMeasurement = document.createRange()
@invalidateCache()
invalidateCache: ->
@pixelPositionsByLineIdAndColumn = {}
prepareScreenRowsForMeasurement: (screenRows) ->
@presenter.setScreenRowsToMeasure(screenRows)
@lineNodesProvider.updateSync(@presenter.getPreMeasurementState())
clearScreenRowsForMeasurement: ->
@presenter.clearScreenRowsToMeasure()
screenPositionForPixelPosition: (pixelPosition, measureVisibleLinesOnly) ->
targetTop = pixelPosition.top
targetLeft = pixelPosition.left
defaultCharWidth = @model.getDefaultCharWidth()
row = Math.floor(targetTop / @model.getLineHeightInPixels())
targetLeft = 0 if row < 0
targetLeft = Infinity if row > @model.getLastScreenRow()
row = Math.min(row, @model.getLastScreenRow())
row = Math.max(0, row)
@prepareScreenRowsForMeasurement([row]) unless measureVisibleLinesOnly
line = @model.tokenizedLineForScreenRow(row)
lineNode = @lineNodesProvider.lineNodeForLineIdAndScreenRow(line?.id, row)
return new Point(row, 0) unless lineNode? and line?
textNodes = @lineNodesProvider.textNodesForLineIdAndScreenRow(line.id, row)
column = 0
previousColumn = 0
previousLeft = 0
@tokenIterator.reset(line)
while @tokenIterator.next()
text = @tokenIterator.getText()
textIndex = 0
while textIndex < text.length
if @tokenIterator.isPairedCharacter()
char = text
charLength = 2
textIndex += 2
else
char = text[textIndex]
charLength = 1
textIndex++
unless textNode?
textNode = textNodes.shift()
textNodeLength = textNode.textContent.length
textNodeIndex = 0
nextTextNodeIndex = textNodeLength
while nextTextNodeIndex <= column
textNode = textNodes.shift()
textNodeLength = textNode.textContent.length
textNodeIndex = nextTextNodeIndex
nextTextNodeIndex = textNodeIndex + textNodeLength
indexWithinTextNode = column - textNodeIndex
left = @leftPixelPositionForCharInTextNode(lineNode, textNode, indexWithinTextNode)
charWidth = left - previousLeft
return new Point(row, previousColumn) if targetLeft <= previousLeft + (charWidth / 2)
previousLeft = left
previousColumn = column
column += charLength
@clearScreenRowsForMeasurement() unless measureVisibleLinesOnly
if targetLeft <= previousLeft + (charWidth / 2)
new Point(row, previousColumn)
else
new Point(row, column)
pixelPositionForScreenPosition: (screenPosition, clip=true, measureVisibleLinesOnly) ->
screenPosition = Point.fromObject(screenPosition)
screenPosition = @model.clipScreenPosition(screenPosition) if clip
targetRow = screenPosition.row
targetColumn = screenPosition.column
@prepareScreenRowsForMeasurement([targetRow]) unless measureVisibleLinesOnly
top = targetRow * @model.getLineHeightInPixels()
left = @leftPixelPositionForScreenPosition(targetRow, targetColumn)
@clearScreenRowsForMeasurement() unless measureVisibleLinesOnly
{top, left}
leftPixelPositionForScreenPosition: (row, column) ->
line = @model.tokenizedLineForScreenRow(row)
lineNode = @lineNodesProvider.lineNodeForLineIdAndScreenRow(line?.id, row)
return 0 unless line? and lineNode?
if cachedPosition = @pixelPositionsByLineIdAndColumn[line.id]?[column]
return cachedPosition
textNodes = @lineNodesProvider.textNodesForLineIdAndScreenRow(line.id, row)
indexWithinTextNode = null
charIndex = 0
@tokenIterator.reset(line)
while @tokenIterator.next()
break if foundIndexWithinTextNode?
text = @tokenIterator.getText()
textIndex = 0
while textIndex < text.length
if @tokenIterator.isPairedCharacter()
char = text
charLength = 2
textIndex += 2
else
char = text[textIndex]
charLength = 1
textIndex++
unless textNode?
textNode = textNodes.shift()
textNodeLength = textNode.textContent.length
textNodeIndex = 0
nextTextNodeIndex = textNodeLength
while nextTextNodeIndex <= charIndex
textNode = textNodes.shift()
textNodeLength = textNode.textContent.length
textNodeIndex = nextTextNodeIndex
nextTextNodeIndex = textNodeIndex + textNodeLength
if charIndex is column
foundIndexWithinTextNode = charIndex - textNodeIndex
break
charIndex += charLength
if textNode?
foundIndexWithinTextNode ?= textNode.textContent.length
position = @leftPixelPositionForCharInTextNode(
lineNode, textNode, foundIndexWithinTextNode
)
@pixelPositionsByLineIdAndColumn[line.id] ?= {}
@pixelPositionsByLineIdAndColumn[line.id][column] = position
position
else
0
leftPixelPositionForCharInTextNode: (lineNode, textNode, charIndex) ->
@rangeForMeasurement.setStart(textNode, 0)
@rangeForMeasurement.setEnd(textNode, charIndex)
width = @rangeForMeasurement.getBoundingClientRect().width
@rangeForMeasurement.setStart(textNode, 0)
@rangeForMeasurement.setEnd(textNode, textNode.textContent.length)
left = @rangeForMeasurement.getBoundingClientRect().left
offset = lineNode.getBoundingClientRect().left
left + width - offset
pixelRectForScreenRange: (screenRange, measureVisibleLinesOnly) ->
lineHeight = @model.getLineHeightInPixels()
if screenRange.end.row > screenRange.start.row
top = @pixelPositionForScreenPosition(screenRange.start, true, measureVisibleLinesOnly).top
left = 0
height = (screenRange.end.row - screenRange.start.row + 1) * lineHeight
width = @presenter.getScrollWidth()
else
{top, left} = @pixelPositionForScreenPosition(screenRange.start, false, measureVisibleLinesOnly)
height = lineHeight
width = @pixelPositionForScreenPosition(screenRange.end, false, measureVisibleLinesOnly).left - left
{top, left, width, height}

View File

@@ -12,6 +12,7 @@ ScrollbarComponent = require './scrollbar-component'
ScrollbarCornerComponent = require './scrollbar-corner-component'
OverlayManager = require './overlay-manager'
DOMElementPool = require './dom-element-pool'
LinesYardstick = require './lines-yardstick'
module.exports =
class TextEditorComponent
@@ -29,7 +30,6 @@ class TextEditorComponent
inputEnabled: true
measureScrollbarsWhenShown: true
measureLineHeightAndDefaultCharWidthWhenShown: true
remeasureCharacterWidthsWhenShown: false
stylingChangeAnimationFrameRequested: false
gutterComponent: null
mounted: true
@@ -79,14 +79,15 @@ class TextEditorComponent
@scrollViewNode.classList.add('scroll-view')
@domNode.appendChild(@scrollViewNode)
@mountGutterContainerComponent() if @presenter.getState().gutters.length
@hiddenInputComponent = new InputComponent
@scrollViewNode.appendChild(@hiddenInputComponent.getDomNode())
@linesComponent = new LinesComponent({@presenter, @hostElement, @useShadowDOM, @domElementPool})
@scrollViewNode.appendChild(@linesComponent.getDomNode())
@linesYardstick = new LinesYardstick(@editor, @presenter, @linesComponent)
@presenter.setLinesYardstick(@linesYardstick)
@horizontalScrollbarComponent = new ScrollbarComponent({orientation: 'horizontal', onScroll: @onHorizontalScroll})
@scrollViewNode.appendChild(@horizontalScrollbarComponent.getDomNode())
@@ -173,7 +174,6 @@ class TextEditorComponent
@updateParentViewMiniClass()
readAfterUpdateSync: =>
@linesComponent.measureCharactersInNewLines() if @isVisible() and not @newState.content.scrollingVertically
@overlayManager?.measureOverlays()
mountGutterContainerComponent: ->
@@ -188,7 +188,6 @@ class TextEditorComponent
@measureWindowSize()
@measureDimensions()
@measureLineHeightAndDefaultCharWidth() if @measureLineHeightAndDefaultCharWidthWhenShown
@remeasureCharacterWidths() if @remeasureCharacterWidthsWhenShown
@editor.setVisible(true)
@performedInitialMeasurement = true
@updatesPaused = false
@@ -276,9 +275,15 @@ class TextEditorComponent
timeoutId = setTimeout(writeSelectedTextToSelectionClipboard)
observeConfig: ->
@disposables.add atom.config.onDidChange 'editor.fontSize', @sampleFontStyling
@disposables.add atom.config.onDidChange 'editor.fontFamily', @sampleFontStyling
@disposables.add atom.config.onDidChange 'editor.lineHeight', @sampleFontStyling
@disposables.add atom.config.onDidChange 'editor.fontSize', =>
@sampleFontStyling()
@invalidateCharacterWidths()
@disposables.add atom.config.onDidChange 'editor.fontFamily', =>
@sampleFontStyling()
@invalidateCharacterWidths()
@disposables.add atom.config.onDidChange 'editor.lineHeight', =>
@sampleFontStyling()
@invalidateCharacterWidths()
onGrammarChanged: =>
if @scopedConfigDisposables?
@@ -424,22 +429,14 @@ class TextEditorComponent
getVisibleRowRange: ->
@presenter.getVisibleRowRange()
pixelPositionForScreenPosition: (screenPosition) ->
position = @presenter.pixelPositionForScreenPosition(screenPosition)
position.top += @presenter.getScrollTop()
position.left += @presenter.getScrollLeft()
position
pixelPositionForScreenPosition: ->
@linesYardstick.pixelPositionForScreenPosition(arguments...)
screenPositionForPixelPosition: (pixelPosition) ->
@presenter.screenPositionForPixelPosition(pixelPosition)
screenPositionForPixelPosition: ->
@linesYardstick.screenPositionForPixelPosition(arguments...)
pixelRectForScreenRange: (screenRange) ->
rect = @presenter.pixelRectForScreenRange(screenRange)
rect.top += @presenter.getScrollTop()
rect.bottom += @presenter.getScrollTop()
rect.left += @presenter.getScrollLeft()
rect.right += @presenter.getScrollLeft()
rect
pixelRectForScreenRange: ->
@linesYardstick.pixelRectForScreenRange(arguments...)
pixelRangeForScreenRange: (screenRange, clip=true) ->
{start, end} = Range.fromObject(screenRange)
@@ -567,7 +564,7 @@ class TextEditorComponent
handleStylingChange: =>
@sampleFontStyling()
@sampleBackgroundColors()
@remeasureCharacterWidths()
@invalidateCharacterWidths()
handleDragUntilMouseUp: (dragHandler) ->
dragging = false
@@ -721,9 +718,7 @@ class TextEditorComponent
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
@remeasureCharacterWidths()
@invalidateCharacterWidths()
sampleBackgroundColors: (suppressUpdate) ->
{backgroundColor} = getComputedStyle(@hostElement)
@@ -742,13 +737,6 @@ class TextEditorComponent
else
@measureLineHeightAndDefaultCharWidthWhenShown = true
remeasureCharacterWidths: ->
if @isVisible()
@remeasureCharacterWidthsWhenShown = false
@linesComponent.remeasureCharacterWidths()
else
@remeasureCharacterWidthsWhenShown = true
measureScrollbars: ->
@measureScrollbarsWhenShown = false
@@ -840,6 +828,7 @@ class TextEditorComponent
setFontSize: (fontSize) ->
@getTopmostDOMNode().style.fontSize = fontSize + 'px'
@sampleFontStyling()
@invalidateCharacterWidths()
getFontFamily: ->
getComputedStyle(@getTopmostDOMNode()).fontFamily
@@ -847,10 +836,16 @@ class TextEditorComponent
setFontFamily: (fontFamily) ->
@getTopmostDOMNode().style.fontFamily = fontFamily
@sampleFontStyling()
@invalidateCharacterWidths()
setLineHeight: (lineHeight) ->
@getTopmostDOMNode().style.lineHeight = lineHeight
@sampleFontStyling()
@invalidateCharacterWidths()
invalidateCharacterWidths: ->
@linesYardstick.invalidateCache()
@presenter.characterWidthsChanged()
setShowIndentGuide: (showIndentGuide) ->
atom.config.set("editor.showIndentGuide", showIndentGuide)
@@ -861,7 +856,7 @@ class TextEditorComponent
screenPositionForMouseEvent: (event, linesClientRect) ->
pixelPosition = @pixelPositionForMouseEvent(event, linesClientRect)
@presenter.screenPositionForPixelPosition(pixelPosition)
@screenPositionForPixelPosition(pixelPosition, true)
pixelPositionForMouseEvent: (event, linesClientRect) ->
{clientX, clientY} = event

View File

@@ -9,7 +9,6 @@ class TextEditorPresenter
startBlinkingCursorsAfterDelay: null
stoppedScrollingTimeoutId: null
mouseWheelScreenRow: null
scopedCharacterWidthsChangeCount: 0
overlayDimensions: {}
minimumReflowInterval: 200
@@ -31,15 +30,21 @@ class TextEditorPresenter
@lineDecorationsByScreenRow = {}
@lineNumberDecorationsByScreenRow = {}
@customGutterDecorationsByGutterNameAndScreenRow = {}
@screenRowsToMeasure = []
@transferMeasurementsToModel()
@transferMeasurementsFromModel()
@observeModel()
@observeConfig()
@buildState()
@invalidateState()
@startBlinkingCursors() if @focused
@startReflowing() if @continuousReflow
@updating = false
setLinesYardstick: (@linesYardstick) ->
getLinesYardstick: -> @linesYardstick
destroy: ->
@disposables.dispose()
@@ -62,20 +67,43 @@ class TextEditorPresenter
isBatching: ->
@updating is false
# Public: Gets this presenter's state, updating it just in time before returning from this function.
# Returns a state {Object}, useful for rendering to screen.
getState: ->
getPreMeasurementState: ->
@updating = true
@updateContentDimensions()
@updateVerticalDimensions()
@updateScrollbarDimensions()
@updateScrollPosition()
@restoreScrollPosition()
@commitPendingLogicalScrollTopPosition()
@commitPendingScrollTopPosition()
@updateStartRow()
@updateEndRow()
@updateRowsPerPage()
@updateCommonGutterState()
@updateReflowState()
if @shouldUpdateDecorations
@fetchDecorations()
@updateLineDecorations()
if @shouldUpdateLinesState or @shouldUpdateLineNumbersState
@updateTilesState()
@shouldUpdateLinesState = false
@shouldUpdateLineNumbersState = false
@shouldUpdateTilesState = true
@updating = false
@state
getPostMeasurementState: ->
@updating = true
@updateHorizontalDimensions()
@commitPendingLogicalScrollLeftPosition()
@commitPendingScrollLeftPosition()
@clearPendingScrollPosition()
@updateRowsPerPage()
@updateFocusedState() if @shouldUpdateFocusedState
@updateHeightState() if @shouldUpdateHeightState
@updateVerticalScrollState() if @shouldUpdateVerticalScrollState
@@ -83,8 +111,8 @@ class TextEditorPresenter
@updateScrollbarsState() if @shouldUpdateScrollbarsState
@updateHiddenInputState() if @shouldUpdateHiddenInputState
@updateContentState() if @shouldUpdateContentState
@updateDecorations() if @shouldUpdateDecorations
@updateTilesState() if @shouldUpdateLinesState or @shouldUpdateLineNumbersState
@updateHighlightDecorations() if @shouldUpdateDecorations
@updateTilesState() if @shouldUpdateTilesState
@updateCursorsState() if @shouldUpdateCursorsState
@updateOverlaysState() if @shouldUpdateOverlaysState
@updateLineNumberGutterState() if @shouldUpdateLineNumberGutterState
@@ -94,6 +122,13 @@ class TextEditorPresenter
@resetTrackedUpdates()
# Public: Gets this presenter's state, updating it just in time before returning from this function.
# Returns a state {Object}, useful for rendering to screen.
getState: ->
@linesYardstick.prepareScreenRowsForMeasurement()
@getPostMeasurementState()
@state
resetTrackedUpdates: ->
@@ -106,6 +141,7 @@ class TextEditorPresenter
@shouldUpdateContentState = false
@shouldUpdateDecorations = false
@shouldUpdateLinesState = false
@shouldUpdateTilesState = false
@shouldUpdateCursorsState = false
@shouldUpdateOverlaysState = false
@shouldUpdateLineNumberGutterState = false
@@ -113,6 +149,24 @@ class TextEditorPresenter
@shouldUpdateGutterOrderState = false
@shouldUpdateCustomGutterDecorationState = false
invalidateState: ->
@shouldUpdateFocusedState = true
@shouldUpdateHeightState = true
@shouldUpdateVerticalScrollState = true
@shouldUpdateHorizontalScrollState = true
@shouldUpdateScrollbarsState = true
@shouldUpdateHiddenInputState = true
@shouldUpdateContentState = true
@shouldUpdateDecorations = true
@shouldUpdateLinesState = true
@shouldUpdateTilesState = true
@shouldUpdateCursorsState = true
@shouldUpdateOverlaysState = true
@shouldUpdateLineNumberGutterState = true
@shouldUpdateLineNumbersState = true
@shouldUpdateGutterOrderState = true
@shouldUpdateCustomGutterDecorationState = true
observeModel: ->
@disposables.add @model.onDidChange =>
@shouldUpdateHeightState = true
@@ -218,6 +272,7 @@ class TextEditorPresenter
tiles: {}
highlights: {}
overlays: {}
cursors: {}
gutters: []
# Shared state that is copied into ``@state.gutters`.
@sharedGutterStyles = {}
@@ -225,36 +280,6 @@ class TextEditorPresenter
@lineNumberGutter =
tiles: {}
@updateState()
updateState: ->
@shouldUpdateLinesState = true
@shouldUpdateLineNumbersState = true
@updateContentDimensions()
@updateScrollPosition()
@updateScrollbarDimensions()
@updateStartRow()
@updateEndRow()
@updateFocusedState()
@updateHeightState()
@updateVerticalScrollState()
@updateHorizontalScrollState()
@updateScrollbarsState()
@updateHiddenInputState()
@updateContentState()
@updateDecorations()
@updateTilesState()
@updateCursorsState()
@updateOverlaysState()
@updateLineNumberGutterState()
@updateCommonGutterState()
@updateGutterOrderState()
@updateCustomGutterDecorationState()
@resetTrackedUpdates()
setContinuousReflow: (@continuousReflow) ->
if @continuousReflow
@startReflowing()
@@ -336,46 +361,83 @@ class TextEditorPresenter
tileForRow: (row) ->
row - (row % @tileSize)
constrainRow: (row) ->
Math.max(0, Math.min(row, @model.getScreenLineCount()))
getStartTileRow: ->
Math.max(0, @tileForRow(@startRow))
@constrainRow(@tileForRow(@startRow))
getEndTileRow: ->
Math.min(
@tileForRow(@model.getScreenLineCount()), @tileForRow(@endRow)
)
@constrainRow(@tileForRow(@endRow))
getTilesCount: ->
Math.ceil(
(@getEndTileRow() - @getStartTileRow() + 1) / @tileSize
)
isValidScreenRow: (screenRow) ->
screenRow >= 0 and screenRow < @model.getScreenLineCount()
getScreenRows: ->
startRow = @getStartTileRow()
endRow = @constrainRow(@getEndTileRow() + @tileSize)
screenRows = [startRow...endRow]
if longestScreenRow = @model.getLongestScreenRow()
screenRows.push(longestScreenRow)
if @screenRowsToMeasure?
screenRows.push(@screenRowsToMeasure...)
screenRows = screenRows.filter @isValidScreenRow.bind(this)
screenRows.sort (a, b) -> a - b
_.uniq(screenRows, true)
setScreenRowsToMeasure: (screenRows) ->
return if not screenRows? or screenRows.length is 0
@screenRowsToMeasure = screenRows
@shouldUpdateLinesState = true
@shouldUpdateLineNumbersState = true
@shouldUpdateDecorations = true
clearScreenRowsToMeasure: ->
@screenRowsToMeasure = []
updateTilesState: ->
return unless @startRow? and @endRow? and @lineHeight?
screenRows = @getScreenRows()
visibleTiles = {}
zIndex = @getTilesCount() - 1
for startRow in [@getStartTileRow()..@getEndTileRow()] by @tileSize
endRow = Math.min(@model.getScreenLineCount(), startRow + @tileSize)
startRow = screenRows[0]
endRow = screenRows[screenRows.length - 1]
screenRowIndex = screenRows.length - 1
zIndex = 0
tile = @state.content.tiles[startRow] ?= {}
tile.top = startRow * @lineHeight - @scrollTop
for tileStartRow in [@tileForRow(endRow)..@tileForRow(startRow)] by -@tileSize
rowsWithinTile = []
while screenRowIndex >= 0
currentScreenRow = screenRows[screenRowIndex]
break if currentScreenRow < tileStartRow
rowsWithinTile.push(currentScreenRow)
screenRowIndex--
continue if rowsWithinTile.length is 0
tile = @state.content.tiles[tileStartRow] ?= {}
tile.top = tileStartRow * @lineHeight - @scrollTop
tile.left = -@scrollLeft
tile.height = @tileSize * @lineHeight
tile.display = "block"
tile.zIndex = zIndex
tile.highlights ?= {}
gutterTile = @lineNumberGutter.tiles[startRow] ?= {}
gutterTile.top = startRow * @lineHeight - @scrollTop
gutterTile = @lineNumberGutter.tiles[tileStartRow] ?= {}
gutterTile.top = tileStartRow * @lineHeight - @scrollTop
gutterTile.height = @tileSize * @lineHeight
gutterTile.display = "block"
gutterTile.zIndex = zIndex
@updateLinesState(tile, startRow, endRow) if @shouldUpdateLinesState
@updateLineNumbersState(gutterTile, startRow, endRow) if @shouldUpdateLineNumbersState
@updateLinesState(tile, rowsWithinTile) if @shouldUpdateLinesState
@updateLineNumbersState(gutterTile, rowsWithinTile) if @shouldUpdateLineNumbersState
visibleTiles[startRow] = true
zIndex--
visibleTiles[tileStartRow] = true
zIndex++
if @mouseWheelScreenRow? and @model.tokenizedLineForScreenRow(@mouseWheelScreenRow)?
mouseWheelTile = @tileForRow(@mouseWheelScreenRow)
@@ -391,24 +453,22 @@ class TextEditorPresenter
delete @state.content.tiles[id]
delete @lineNumberGutter.tiles[id]
updateLinesState: (tileState, startRow, endRow) ->
updateLinesState: (tileState, screenRows) ->
tileState.lines ?= {}
visibleLineIds = {}
row = startRow
while row < endRow
line = @model.tokenizedLineForScreenRow(row)
for screenRow in screenRows
line = @model.tokenizedLineForScreenRow(screenRow)
unless line?
throw new Error("No line exists for row #{row}. Last screen row: #{@model.getLastScreenRow()}")
throw new Error("No line exists for row #{screenRow}. Last screen row: #{@model.getLastScreenRow()}")
visibleLineIds[line.id] = true
if tileState.lines.hasOwnProperty(line.id)
lineState = tileState.lines[line.id]
lineState.screenRow = row
lineState.top = (row - startRow) * @lineHeight
lineState.decorationClasses = @lineDecorationClassesForRow(row)
lineState.screenRow = screenRow
lineState.decorationClasses = @lineDecorationClassesForRow(screenRow)
else
tileState.lines[line.id] =
screenRow: row
screenRow: screenRow
text: line.text
openScopes: line.openScopes
tags: line.tags
@@ -421,9 +481,7 @@ class TextEditorPresenter
indentLevel: line.indentLevel
tabLength: line.tabLength
fold: line.fold
top: (row - startRow) * @lineHeight
decorationClasses: @lineDecorationClassesForRow(row)
row++
decorationClasses: @lineDecorationClassesForRow(screenRow)
for id, line of tileState.lines
delete tileState.lines[id] unless visibleLineIds.hasOwnProperty(id)
@@ -440,7 +498,7 @@ class TextEditorPresenter
return unless cursor.isVisible() and @startRow <= screenRange.start.row < @endRow
pixelRect = @pixelRectForScreenRange(screenRange)
pixelRect.width = @baseCharacterWidth if pixelRect.width is 0
pixelRect.width = Math.round(@baseCharacterWidth) if pixelRect.width is 0
@state.content.cursors[cursor.id] = pixelRect
updateOverlaysState: ->
@@ -595,10 +653,13 @@ class TextEditorPresenter
isVisible = isVisible and @showLineNumbers
isVisible
updateLineNumbersState: (tileState, startRow, endRow) ->
updateLineNumbersState: (tileState, screenRows) ->
tileState.lineNumbers ?= {}
visibleLineNumberIds = {}
startRow = screenRows[screenRows.length - 1]
endRow = Math.min(screenRows[0] + 1, @model.getScreenLineCount())
if startRow > 0
rowBeforeStartRow = startRow - 1
lastBufferRow = @model.bufferRowForScreenRow(rowBeforeStartRow)
@@ -615,13 +676,12 @@ class TextEditorPresenter
softWrapped = false
screenRow = startRow + i
top = (screenRow - startRow) * @lineHeight
line = @model.tokenizedLineForScreenRow(screenRow)
decorationClasses = @lineNumberDecorationClassesForRow(screenRow)
foldable = @model.isFoldableAtScreenRow(screenRow)
id = @model.tokenizedLineForScreenRow(screenRow).id
tileState.lineNumbers[id] = {screenRow, bufferRow, softWrapped, top, decorationClasses, foldable}
visibleLineNumberIds[id] = true
tileState.lineNumbers[line.id] = {screenRow, bufferRow, softWrapped, decorationClasses, foldable}
visibleLineNumberIds[line.id] = true
for id of tileState.lineNumbers
delete tileState.lineNumbers[id] unless visibleLineNumberIds[id]
@@ -669,11 +729,17 @@ class TextEditorPresenter
@scrollHeight = scrollHeight
@updateScrollTop(@scrollTop)
updateContentDimensions: ->
updateVerticalDimensions: ->
if @lineHeight?
oldContentHeight = @contentHeight
@contentHeight = @lineHeight * @model.getScreenLineCount()
if @contentHeight isnt oldContentHeight
@updateHeight()
@updateScrollbarDimensions()
@updateScrollHeight()
updateHorizontalDimensions: ->
if @baseCharacterWidth?
oldContentWidth = @contentWidth
clip = @model.tokenizedLineForScreenRow(@model.getLongestScreenRow())?.isSoftWrapped()
@@ -681,11 +747,6 @@ class TextEditorPresenter
@contentWidth += @scrollLeft
@contentWidth += 1 unless @model.isSoftWrapped() # account for cursor width
if @contentHeight isnt oldContentHeight
@updateHeight()
@updateScrollbarDimensions()
@updateScrollHeight()
if @contentWidth isnt oldContentWidth
@updateScrollbarDimensions()
@updateScrollWidth()
@@ -829,10 +890,10 @@ class TextEditorPresenter
@emitDidUpdateState()
setScrollTop: (scrollTop) ->
setScrollTop: (scrollTop, overrideScroll=true) ->
return unless scrollTop?
@pendingScrollLogicalPosition = null
@pendingScrollLogicalPosition = null if overrideScroll
@pendingScrollTop = scrollTop
@shouldUpdateVerticalScrollState = true
@@ -870,10 +931,10 @@ class TextEditorPresenter
@emitDidUpdateState()
setScrollLeft: (scrollLeft) ->
setScrollLeft: (scrollLeft, overrideScroll=true) ->
return unless scrollLeft?
@pendingScrollLogicalPosition = null
@pendingScrollLogicalPosition = null if overrideScroll
@pendingScrollLeft = scrollLeft
@shouldUpdateHorizontalScrollState = true
@@ -904,13 +965,13 @@ class TextEditorPresenter
@contentFrameWidth - @verticalScrollbarWidth
getScrollBottom: -> @getScrollTop() + @getClientHeight()
setScrollBottom: (scrollBottom) ->
@setScrollTop(scrollBottom - @getClientHeight())
setScrollBottom: (scrollBottom, overrideScroll) ->
@setScrollTop(scrollBottom - @getClientHeight(), overrideScroll)
@getScrollBottom()
getScrollRight: -> @getScrollLeft() + @getClientWidth()
setScrollRight: (scrollRight) ->
@setScrollLeft(scrollRight - @getClientWidth())
setScrollRight: (scrollRight, overrideScroll) ->
@setScrollLeft(scrollRight - @getClientWidth(), overrideScroll)
@getScrollRight()
getScrollHeight: ->
@@ -1065,30 +1126,6 @@ class TextEditorPresenter
@model.setDefaultCharWidth(baseCharacterWidth)
@characterWidthsChanged()
getScopedCharacterWidth: (scopeNames, char) ->
@getScopedCharacterWidths(scopeNames)[char]
getScopedCharacterWidths: (scopeNames) ->
scope = @characterWidthsByScope
for scopeName in scopeNames
scope[scopeName] ?= {}
scope = scope[scopeName]
scope.characterWidths ?= {}
scope.characterWidths
batchCharacterMeasurement: (fn) ->
oldChangeCount = @scopedCharacterWidthsChangeCount
@batchingCharacterMeasurement = true
@model.batchCharacterMeasurement(fn)
@batchingCharacterMeasurement = false
@characterWidthsChanged() if oldChangeCount isnt @scopedCharacterWidthsChangeCount
setScopedCharacterWidth: (scopeNames, character, width) ->
@getScopedCharacterWidths(scopeNames)[character] = width
@model.setScopedCharWidth(scopeNames, character, width)
@scopedCharacterWidthsChangeCount++
@characterWidthsChanged() unless @batchingCharacterMeasurement
characterWidthsChanged: ->
@shouldUpdateHorizontalScrollState = true
@shouldUpdateVerticalScrollState = true
@@ -1102,49 +1139,19 @@ class TextEditorPresenter
@emitDidUpdateState()
clearScopedCharacterWidths: ->
@characterWidthsByScope = {}
@model.clearScopedCharWidths()
hasPixelPositionRequirements: ->
@lineHeight? and @baseCharacterWidth?
pixelPositionForScreenPosition: (screenPosition, clip=true) ->
screenPosition = Point.fromObject(screenPosition)
screenPosition = @model.clipScreenPosition(screenPosition) if clip
position =
@linesYardstick.pixelPositionForScreenPosition(screenPosition, clip, true)
position.top -= @getScrollTop()
position.left -= @getScrollLeft()
targetRow = screenPosition.row
targetColumn = screenPosition.column
baseCharacterWidth = @baseCharacterWidth
position.top = Math.round(position.top)
position.left = Math.round(position.left)
top = targetRow * @lineHeight
left = 0
column = 0
iterator = @model.tokenizedLineForScreenRow(targetRow).getTokenIterator()
while iterator.next()
characterWidths = @getScopedCharacterWidths(iterator.getScopes())
valueIndex = 0
text = iterator.getText()
while valueIndex < text.length
if iterator.isPairedCharacter()
char = text
charLength = 2
valueIndex += 2
else
char = text[valueIndex]
charLength = 1
valueIndex++
break if column is targetColumn
left += characterWidths[char] ? baseCharacterWidth unless char is '\0'
column += charLength
top -= @scrollTop
left -= @scrollLeft
{top, left}
position
hasPixelRectRequirements: ->
@hasPixelPositionRequirements() and @scrollWidth?
@@ -1153,17 +1160,16 @@ class TextEditorPresenter
@hasPixelRectRequirements() and @boundingClientRect? and @windowWidth and @windowHeight
pixelRectForScreenRange: (screenRange) ->
if screenRange.end.row > screenRange.start.row
top = @pixelPositionForScreenPosition(screenRange.start).top
left = 0
height = (screenRange.end.row - screenRange.start.row + 1) * @lineHeight
width = @scrollWidth
else
{top, left} = @pixelPositionForScreenPosition(screenRange.start, false)
height = @lineHeight
width = @pixelPositionForScreenPosition(screenRange.end, false).left - left
rect = @linesYardstick.pixelRectForScreenRange(screenRange, true)
rect.top -= @getScrollTop()
rect.left -= @getScrollLeft()
{top, left, width, height}
rect.top = Math.round(rect.top)
rect.left = Math.round(rect.left)
rect.width = Math.round(rect.width)
rect.height = Math.round(rect.height)
rect
observeDecoration: (decoration) ->
decorationDisposables = new CompositeDisposable
@@ -1224,22 +1230,34 @@ class TextEditorPresenter
@emitDidUpdateState()
updateDecorations: ->
@rangesByDecorationId = {}
@lineDecorationsByScreenRow = {}
@lineNumberDecorationsByScreenRow = {}
@customGutterDecorationsByGutterNameAndScreenRow = {}
@visibleHighlights = {}
fetchDecorations: ->
@decorations = []
return unless 0 <= @startRow <= @endRow <= Infinity
for markerId, decorations of @model.decorationsForScreenRowRange(@startRow, @endRow - 1)
range = @model.getMarker(markerId).getScreenRange()
for decoration in decorations
if decoration.isType('line') or decoration.isType('gutter')
@addToLineDecorationCaches(decoration, range)
else if decoration.isType('highlight')
@updateHighlightState(decoration, range)
@decorations.push({decoration, range})
updateLineDecorations: ->
@rangesByDecorationId = {}
@lineDecorationsByScreenRow = {}
@lineNumberDecorationsByScreenRow = {}
@customGutterDecorationsByGutterNameAndScreenRow = {}
for {decoration, range} in @decorations
if decoration.isType('line') or decoration.isType('gutter')
@addToLineDecorationCaches(decoration, range)
return
updateHighlightDecorations: ->
@visibleHighlights = {}
for {decoration, range} in @decorations
if decoration.isType('highlight')
@updateHighlightState(decoration, range)
for tileId, tileState of @state.content.tiles
for id, highlight of tileState.highlights
@@ -1512,10 +1530,10 @@ class TextEditorPresenter
@emitDidUpdateState()
getVerticalScrollMarginInPixels: ->
@model.getVerticalScrollMargin() * @lineHeight
Math.round(@model.getVerticalScrollMargin() * @lineHeight)
getHorizontalScrollMarginInPixels: ->
@model.getHorizontalScrollMargin() * @baseCharacterWidth
Math.round(@model.getHorizontalScrollMargin() * @baseCharacterWidth)
getVerticalScrollbarWidth: ->
@verticalScrollbarWidth
@@ -1523,23 +1541,15 @@ class TextEditorPresenter
getHorizontalScrollbarHeight: ->
@horizontalScrollbarHeight
commitPendingLogicalScrollPosition: ->
commitPendingLogicalScrollTopPosition: ->
return unless @pendingScrollLogicalPosition?
{screenRange, options} = @pendingScrollLogicalPosition
verticalScrollMarginInPixels = @getVerticalScrollMarginInPixels()
horizontalScrollMarginInPixels = @getHorizontalScrollMarginInPixels()
{top, left} = @pixelRectForScreenRange(new Range(screenRange.start, screenRange.start))
{top: endTop, left: endLeft, height: endHeight} = @pixelRectForScreenRange(new Range(screenRange.end, screenRange.end))
bottom = endTop + endHeight
right = endLeft
top += @scrollTop
bottom += @scrollTop
left += @scrollLeft
right += @scrollLeft
top = screenRange.start.row * @lineHeight
bottom = (screenRange.end.row + 1) * @lineHeight
if options?.center
desiredScrollCenter = (top + bottom) / 2
@@ -1550,31 +1560,43 @@ class TextEditorPresenter
desiredScrollTop = top - verticalScrollMarginInPixels
desiredScrollBottom = bottom + verticalScrollMarginInPixels
if options?.reversed ? true
if desiredScrollBottom > @getScrollBottom()
@setScrollBottom(desiredScrollBottom, false)
if desiredScrollTop < @getScrollTop()
@setScrollTop(desiredScrollTop, false)
else
if desiredScrollTop < @getScrollTop()
@setScrollTop(desiredScrollTop, false)
if desiredScrollBottom > @getScrollBottom()
@setScrollBottom(desiredScrollBottom, false)
commitPendingLogicalScrollLeftPosition: ->
return unless @pendingScrollLogicalPosition?
{screenRange, options} = @pendingScrollLogicalPosition
horizontalScrollMarginInPixels = @getHorizontalScrollMarginInPixels()
{left} = @pixelRectForScreenRange(new Range(screenRange.start, screenRange.start))
{left: right} = @pixelRectForScreenRange(new Range(screenRange.end, screenRange.end))
left += @scrollLeft
right += @scrollLeft
desiredScrollLeft = left - horizontalScrollMarginInPixels
desiredScrollRight = right + horizontalScrollMarginInPixels
if options?.reversed ? true
if desiredScrollBottom > @getScrollBottom()
@setScrollBottom(desiredScrollBottom)
if desiredScrollTop < @getScrollTop()
@setScrollTop(desiredScrollTop)
if desiredScrollRight > @getScrollRight()
@setScrollRight(desiredScrollRight)
@setScrollRight(desiredScrollRight, false)
if desiredScrollLeft < @getScrollLeft()
@setScrollLeft(desiredScrollLeft)
@setScrollLeft(desiredScrollLeft, false)
else
if desiredScrollTop < @getScrollTop()
@setScrollTop(desiredScrollTop)
if desiredScrollBottom > @getScrollBottom()
@setScrollBottom(desiredScrollBottom)
if desiredScrollLeft < @getScrollLeft()
@setScrollLeft(desiredScrollLeft)
@setScrollLeft(desiredScrollLeft, false)
if desiredScrollRight > @getScrollRight()
@setScrollRight(desiredScrollRight)
@pendingScrollLogicalPosition = null
@setScrollRight(desiredScrollRight, false)
commitPendingScrollLeftPosition: ->
if @pendingScrollLeft?
@@ -1594,11 +1616,10 @@ class TextEditorPresenter
@hasRestoredScrollPosition = true
updateScrollPosition: ->
@restoreScrollPosition()
@commitPendingLogicalScrollPosition()
@commitPendingScrollLeftPosition()
@commitPendingScrollTopPosition()
clearPendingScrollPosition: ->
@pendingScrollLogicalPosition = null
@pendingScrollTop = null
@pendingScrollLeft = null
canScrollLeftTo: (scrollLeft) ->
@scrollLeft isnt @constrainScrollLeft(scrollLeft)
@@ -1614,38 +1635,3 @@ class TextEditorPresenter
getVisibleRowRange: ->
[@startRow, @endRow]
screenPositionForPixelPosition: (pixelPosition) ->
targetTop = pixelPosition.top
targetLeft = pixelPosition.left
defaultCharWidth = @baseCharacterWidth
row = Math.floor(targetTop / @lineHeight)
targetLeft = 0 if row < 0
targetLeft = Infinity if row > @model.getLastScreenRow()
row = Math.min(row, @model.getLastScreenRow())
row = Math.max(0, row)
left = 0
column = 0
iterator = @model.tokenizedLineForScreenRow(row).getTokenIterator()
while iterator.next()
charWidths = @getScopedCharacterWidths(iterator.getScopes())
value = iterator.getText()
valueIndex = 0
while valueIndex < value.length
if iterator.isPairedCharacter()
char = value
charLength = 2
valueIndex += 2
else
char = value[valueIndex]
charLength = 1
valueIndex++
charWidth = charWidths[char] ? defaultCharWidth
break if targetLeft <= left + (charWidth / 2)
left += charWidth
column += charLength
new Point(row, column)

View File

@@ -2976,15 +2976,6 @@ class TextEditor extends Model
getLineHeightInPixels: -> @displayBuffer.getLineHeightInPixels()
setLineHeightInPixels: (lineHeightInPixels) -> @displayBuffer.setLineHeightInPixels(lineHeightInPixels)
batchCharacterMeasurement: (fn) -> @displayBuffer.batchCharacterMeasurement(fn)
getScopedCharWidth: (scopeNames, char) -> @displayBuffer.getScopedCharWidth(scopeNames, char)
setScopedCharWidth: (scopeNames, char, width) -> @displayBuffer.setScopedCharWidth(scopeNames, char, width)
getScopedCharWidths: (scopeNames) -> @displayBuffer.getScopedCharWidths(scopeNames)
clearScopedCharWidths: -> @displayBuffer.clearScopedCharWidths()
getDefaultCharWidth: -> @displayBuffer.getDefaultCharWidth()
setDefaultCharWidth: (defaultCharWidth) -> @displayBuffer.setDefaultCharWidth(defaultCharWidth)