mirror of
https://github.com/atom/atom.git
synced 2026-01-24 14:28:14 -05:00
Merge pull request #8811 from atom/as-double-reflow-measurements
DOM-based measurements
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) ->
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
187
src/lines-yardstick.coffee
Normal 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}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user