diff --git a/spec/lines-yardstick-spec.coffee b/spec/lines-yardstick-spec.coffee
new file mode 100644
index 00000000000..48d446f2362
--- /dev/null
+++ b/spec/lines-yardstick-spec.coffee
@@ -0,0 +1,101 @@
+LinesYardstick = require '../src/lines-yardstick'
+{Point} = require 'text-buffer'
+
+describe "LinesYardstick", ->
+ [linesYardstick, editor, styleNodesToRemove] = []
+
+ styleSheetWithSelectorAndFont = (selector, font) ->
+ styleNode = document.createElement("style")
+ styleNode.innerHTML = """
+ #{selector} {
+ font: #{font};
+ }
+ """
+ document.body.appendChild(styleNode)
+ styleNodesToRemove ?= []
+ styleNodesToRemove.push(styleNode)
+ styleNode
+
+ cleanupStyleSheets = ->
+ styleNode.remove() while styleNode = styleNodesToRemove?.pop()
+
+ beforeEach ->
+ waitsForPromise ->
+ atom.packages.activatePackage('language-javascript')
+
+ waitsForPromise ->
+ atom.project.open('sample.js').then (o) -> editor = o
+
+ runs ->
+ linesYardstick = new LinesYardstick(editor)
+ document.body.appendChild(linesYardstick.getDomNode())
+
+ waitsFor ->
+ linesYardstick.canMeasure()
+
+ afterEach ->
+ linesYardstick.getDomNode().remove()
+ cleanupStyleSheets()
+
+ describe "::buildDomNodesForScreenRows(screenRows)", ->
+ it "asks for a line HTML only once", ->
+ requestedLinesByScreenRow = {}
+ linesYardstick.setLineHtmlProvider (screenRow, line) ->
+ requestedLinesByScreenRow[screenRow] ?= 0
+ requestedLinesByScreenRow[screenRow]++
+
+ "
"
+ else
+ lineHTML = "
"
+
+ if text is ""
+ lineHTML += @buildEmptyLineInnerHTML(indentGuidesVisible, lineState)
+ else
+ lineHTML += @buildLineInnerHTML(indentGuidesVisible, lineState)
+
+ lineHTML += '' if fold and not @fastVersion
+
+ lineHTML += "
"
+ lineHTML
+
+ buildEmptyLineInnerHTML: (indentGuidesVisible, lineState) ->
+ {indentLevel, tabLength, endOfLineInvisibles} = lineState
+
+ if indentGuidesVisible and indentLevel > 0
+ invisibleIndex = 0
+ lineHTML = ''
+ for i in [0...indentLevel]
+ lineHTML += "
"
+ for j in [0...tabLength]
+ if invisible = endOfLineInvisibles?[invisibleIndex++]
+ lineHTML += "#{invisible}"
+ else
+ lineHTML += ' '
+ lineHTML += ""
+
+ while invisibleIndex < endOfLineInvisibles?.length
+ lineHTML += "
#{endOfLineInvisibles[invisibleIndex++]}"
+
+ lineHTML
+ else
+ @buildEndOfLineHTML(lineState) or ' '
+
+ buildLineInnerHTML: (indentGuidesVisible, lineState) ->
+ {firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, invisibles} = lineState
+ lineIsWhitespaceOnly = firstTrailingWhitespaceIndex is 0
+
+ innerHTML = ""
+ @tokenIterator.reset(lineState)
+
+ while @tokenIterator.next()
+ for scope in @tokenIterator.getScopeEnds()
+ innerHTML += ""
+
+ for scope in @tokenIterator.getScopeStarts()
+ innerHTML += "
"
+
+ tokenStart = @tokenIterator.getScreenStart()
+ tokenEnd = @tokenIterator.getScreenEnd()
+ tokenText = @tokenIterator.getText()
+ isHardTab = @tokenIterator.isHardTab()
+
+ if hasLeadingWhitespace = tokenStart < firstNonWhitespaceIndex
+ tokenFirstNonWhitespaceIndex = firstNonWhitespaceIndex - tokenStart
+ else
+ tokenFirstNonWhitespaceIndex = null
+
+ if hasTrailingWhitespace = tokenEnd > firstTrailingWhitespaceIndex
+ tokenFirstTrailingWhitespaceIndex = Math.max(0, firstTrailingWhitespaceIndex - tokenStart)
+ else
+ tokenFirstTrailingWhitespaceIndex = null
+
+ hasIndentGuide =
+ indentGuidesVisible and
+ (hasLeadingWhitespace or lineIsWhitespaceOnly)
+
+ hasInvisibleCharacters =
+ (invisibles?.tab and isHardTab) or
+ (invisibles?.space and (hasLeadingWhitespace or hasTrailingWhitespace))
+
+ innerHTML += @buildTokenHTML(tokenText, isHardTab, tokenFirstNonWhitespaceIndex, tokenFirstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters, tokenStart, tokenEnd)
+
+ for scope in @tokenIterator.getScopeEnds()
+ innerHTML += ""
+
+ for scope in @tokenIterator.getScopes()
+ innerHTML += ""
+
+ innerHTML += @buildEndOfLineHTML(lineState) unless @fastVersion
+ innerHTML
+
+ buildTokenHTML: (tokenText, isHardTab, firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters, tokenStart, tokenEnd) ->
+ if isHardTab
+ classes = 'hard-tab'
+ classes += ' leading-whitespace' if firstNonWhitespaceIndex?
+ classes += ' trailing-whitespace' if firstTrailingWhitespaceIndex?
+ classes += ' indent-guide' if hasIndentGuide
+ classes += ' invisible-character' if hasInvisibleCharacters
+ return "
#{@escapeTokenText(tokenText)}"
+ else
+ tokenText = tokenText.replace("\0", "")
+
+ startIndex = 0
+ endIndex = tokenText.length
+
+ leadingHtml = ''
+ trailingHtml = ''
+
+ if firstNonWhitespaceIndex?
+ leadingWhitespace = tokenText.substring(0, firstNonWhitespaceIndex)
+
+ classes = 'leading-whitespace'
+ classes += ' indent-guide' if hasIndentGuide
+ classes += ' invisible-character' if hasInvisibleCharacters
+
+ leadingHtml = "
#{leadingWhitespace}"
+ startIndex = firstNonWhitespaceIndex
+
+ if firstTrailingWhitespaceIndex?
+ tokenIsOnlyWhitespace = firstTrailingWhitespaceIndex is 0
+ trailingWhitespace = tokenText.substring(firstTrailingWhitespaceIndex)
+
+ unless trailingWhitespace is ""
+ classes = 'trailing-whitespace'
+ classes += ' indent-guide' if hasIndentGuide and not firstNonWhitespaceIndex? and tokenIsOnlyWhitespace
+ classes += ' invisible-character' if hasInvisibleCharacters
+
+ trailingHtml = "
#{trailingWhitespace}"
+
+ endIndex = firstTrailingWhitespaceIndex
+
+ html = leadingHtml
+ if tokenText.length > MaxTokenLength
+ while startIndex < endIndex
+ text = @escapeTokenText(tokenText, startIndex, startIndex + MaxTokenLength)
+ html += "
#{text}"
+ startIndex += MaxTokenLength
+ else
+ text = @escapeTokenText(tokenText, startIndex, endIndex)
+ html += text
+
+ html += trailingHtml
+ html
+
+ escapeTokenText: (tokenText, startIndex, endIndex) ->
+ if startIndex? and endIndex? and startIndex > 0 or endIndex < tokenText.length
+ tokenText = tokenText.slice(startIndex, endIndex)
+ tokenText.replace(TokenTextEscapeRegex, @escapeTokenTextReplace)
+
+ escapeTokenTextReplace: (match) ->
+ switch match
+ when '&' then '&'
+ when '"' then '"'
+ when "'" then '''
+ when '<' then '<'
+ when '>' then '>'
+ else match
+
+ buildEndOfLineHTML: (lineState) ->
+ {endOfLineInvisibles} = lineState
+
+ html = ''
+ if endOfLineInvisibles?
+ for invisible in endOfLineInvisibles
+ html += "
#{invisible}"
+ html
diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee
index f823ea8ac5b..9e159caf198 100644
--- a/src/lines-tile-component.coffee
+++ b/src/lines-tile-component.coffee
@@ -1,11 +1,10 @@
_ = require 'underscore-plus'
HighlightsComponent = require './highlights-component'
-TokenIterator = require './token-iterator'
AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT}
WrapperDiv = document.createElement('div')
-TokenTextEscapeRegex = /[&"'<>]/g
-MaxTokenLength = 20000
+LineHtmlBuilder = require './line-html-builder'
+TokenIterator = require './token-iterator'
cloneObject = (object) ->
clone = {}
@@ -15,6 +14,7 @@ cloneObject = (object) ->
module.exports =
class LinesTileComponent
constructor: ({@presenter, @id}) ->
+ @lineHtmlBuilder = new LineHtmlBuilder
@tokenIterator = new TokenIterator
@measuredLines = new Set
@lineNodesByLineId = {}
@@ -98,7 +98,11 @@ class LinesTileComponent
newLineIds ?= []
newLinesHTML ?= ""
newLineIds.push(id)
- newLinesHTML += @buildLineHTML(id)
+ newLinesHTML += @lineHtmlBuilder.buildLineHTML(
+ @newState.indentGuidesVisible,
+ @newState.width,
+ lineState
+ )
@screenRowsByLineId[id] = lineState.screenRow
@lineIdsByScreenRow[lineState.screenRow] = id
@oldTileState.lines[id] = cloneObject(lineState)
@@ -114,170 +118,6 @@ class LinesTileComponent
return
- buildLineHTML: (id) ->
- {width} = @newState
- {screenRow, tokens, text, top, lineEnding, fold, isSoftWrapped, indentLevel, decorationClasses} = @newTileState.lines[id]
-
- classes = ''
- if decorationClasses?
- for decorationClass in decorationClasses
- classes += decorationClass + ' '
- classes += 'line'
-
- lineHTML = "
"
-
- if text is ""
- lineHTML += @buildEmptyLineInnerHTML(id)
- else
- lineHTML += @buildLineInnerHTML(id)
-
- lineHTML += '' if fold
- lineHTML += "
"
- lineHTML
-
- buildEmptyLineInnerHTML: (id) ->
- {indentGuidesVisible} = @newState
- {indentLevel, tabLength, endOfLineInvisibles} = @newTileState.lines[id]
-
- if indentGuidesVisible and indentLevel > 0
- invisibleIndex = 0
- lineHTML = ''
- for i in [0...indentLevel]
- lineHTML += "
"
- for j in [0...tabLength]
- if invisible = endOfLineInvisibles?[invisibleIndex++]
- lineHTML += "#{invisible}"
- else
- lineHTML += ' '
- lineHTML += ""
-
- while invisibleIndex < endOfLineInvisibles?.length
- lineHTML += "
#{endOfLineInvisibles[invisibleIndex++]}"
-
- lineHTML
- else
- @buildEndOfLineHTML(id) or ' '
-
- buildLineInnerHTML: (id) ->
- lineState = @newTileState.lines[id]
- {firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, invisibles} = lineState
- lineIsWhitespaceOnly = firstTrailingWhitespaceIndex is 0
-
- innerHTML = ""
- @tokenIterator.reset(lineState)
-
- while @tokenIterator.next()
- for scope in @tokenIterator.getScopeEnds()
- innerHTML += ""
-
- for scope in @tokenIterator.getScopeStarts()
- innerHTML += "
"
-
- tokenStart = @tokenIterator.getScreenStart()
- tokenEnd = @tokenIterator.getScreenEnd()
- tokenText = @tokenIterator.getText()
- isHardTab = @tokenIterator.isHardTab()
-
- if hasLeadingWhitespace = tokenStart < firstNonWhitespaceIndex
- tokenFirstNonWhitespaceIndex = firstNonWhitespaceIndex - tokenStart
- else
- tokenFirstNonWhitespaceIndex = null
-
- if hasTrailingWhitespace = tokenEnd > firstTrailingWhitespaceIndex
- tokenFirstTrailingWhitespaceIndex = Math.max(0, firstTrailingWhitespaceIndex - tokenStart)
- else
- tokenFirstTrailingWhitespaceIndex = null
-
- hasIndentGuide =
- @newState.indentGuidesVisible and
- (hasLeadingWhitespace or lineIsWhitespaceOnly)
-
- hasInvisibleCharacters =
- (invisibles?.tab and isHardTab) or
- (invisibles?.space and (hasLeadingWhitespace or hasTrailingWhitespace))
-
- innerHTML += @buildTokenHTML(tokenText, isHardTab, tokenFirstNonWhitespaceIndex, tokenFirstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters)
-
- for scope in @tokenIterator.getScopeEnds()
- innerHTML += ""
-
- for scope in @tokenIterator.getScopes()
- innerHTML += ""
-
- innerHTML += @buildEndOfLineHTML(id)
- innerHTML
-
- buildTokenHTML: (tokenText, isHardTab, firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters) ->
- if isHardTab
- classes = 'hard-tab'
- classes += ' leading-whitespace' if firstNonWhitespaceIndex?
- classes += ' trailing-whitespace' if firstTrailingWhitespaceIndex?
- classes += ' indent-guide' if hasIndentGuide
- classes += ' invisible-character' if hasInvisibleCharacters
- return "
#{@escapeTokenText(tokenText)}"
- else
- startIndex = 0
- endIndex = tokenText.length
-
- leadingHtml = ''
- trailingHtml = ''
-
- if firstNonWhitespaceIndex?
- leadingWhitespace = tokenText.substring(0, firstNonWhitespaceIndex)
-
- classes = 'leading-whitespace'
- classes += ' indent-guide' if hasIndentGuide
- classes += ' invisible-character' if hasInvisibleCharacters
-
- leadingHtml = "
#{leadingWhitespace}"
- startIndex = firstNonWhitespaceIndex
-
- if firstTrailingWhitespaceIndex?
- tokenIsOnlyWhitespace = firstTrailingWhitespaceIndex is 0
- trailingWhitespace = tokenText.substring(firstTrailingWhitespaceIndex)
-
- classes = 'trailing-whitespace'
- classes += ' indent-guide' if hasIndentGuide and not firstNonWhitespaceIndex? and tokenIsOnlyWhitespace
- classes += ' invisible-character' if hasInvisibleCharacters
-
- trailingHtml = "
#{trailingWhitespace}"
-
- endIndex = firstTrailingWhitespaceIndex
-
- html = leadingHtml
- if tokenText.length > MaxTokenLength
- while startIndex < endIndex
- html += "
" + @escapeTokenText(tokenText, startIndex, startIndex + MaxTokenLength) + ""
- startIndex += MaxTokenLength
- else
- html += @escapeTokenText(tokenText, startIndex, endIndex)
-
- html += trailingHtml
- html
-
- escapeTokenText: (tokenText, startIndex, endIndex) ->
- if startIndex? and endIndex? and startIndex > 0 or endIndex < tokenText.length
- tokenText = tokenText.slice(startIndex, endIndex)
- tokenText.replace(TokenTextEscapeRegex, @escapeTokenTextReplace)
-
- escapeTokenTextReplace: (match) ->
- switch match
- when '&' then '&'
- when '"' then '"'
- when "'" then '''
- when '<' then '<'
- when '>' then '>'
- else match
-
- buildEndOfLineHTML: (id) ->
- {endOfLineInvisibles} = @newTileState.lines[id]
-
- html = ''
- if endOfLineInvisibles?
- for invisible in endOfLineInvisibles
- html += "
#{invisible}"
- html
-
updateLineNode: (id) ->
oldLineState = @oldTileState.lines[id]
newLineState = @newTileState.lines[id]
diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee
new file mode 100644
index 00000000000..1820459c1a4
--- /dev/null
+++ b/src/lines-yardstick.coffee
@@ -0,0 +1,177 @@
+TokenIterator = require './token-iterator'
+AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT}
+rangeForMeasurement = document.createRange()
+{Emitter} = require 'event-kit'
+{Point} = require 'text-buffer'
+
+module.exports =
+class LinesYardstick
+ constructor: (@editor) ->
+ @initialized = false
+ @emitter = new Emitter
+ @defaultStyleNode = document.createElement("style")
+ @skinnyStyleNode = document.createElement("style")
+ @iframe = document.createElement("iframe")
+ @iframe.style.display = "none"
+ @iframe.onload = @setupIframe
+ @lineNodesByLineId = {}
+ @tokenIterator = new TokenIterator
+
+ getDomNode: -> @iframe
+
+ setLineHtmlProvider: (@htmlProviderFn) ->
+
+ setDefaultFont: (fontFamily, fontSize) ->
+ @defaultStyleNode.innerHTML = """
+ * {
+ margin: 0;
+ padding: 0;
+ }
+
+ body {
+ font-size: #{fontSize};
+ font-family: #{fontFamily};
+ white-space: pre;
+ }
+ """
+
+ onDidInitialize: (callback) ->
+ @emitter.on "did-initialize", callback
+
+ onWillMeasureScreenRows: (callback) ->
+ @emitter.on "will-measure-screen-rows", callback
+
+ canMeasure: ->
+ @initialized
+
+ setupIframe: =>
+ @initialized = true
+ @domNode = @iframe.contentDocument.body
+ @headNode = @iframe.contentDocument.head
+
+ @headNode.appendChild(@defaultStyleNode)
+ @headNode.appendChild(@skinnyStyleNode)
+
+ @emitter.emit "did-initialize"
+
+ resetStyleSheets: (styleSheets) ->
+ skinnyCss = ""
+
+ for style in styleSheets
+ for cssRule in style.sheet.cssRules when @hasFontStyling(cssRule)
+ skinnyCss += cssRule.cssText
+
+ @skinnyStyleNode.innerHTML = skinnyCss
+
+ hasFontStyling: (cssRule) ->
+ for styleProperty in cssRule.style
+ return true if styleProperty.indexOf("font") isnt -1
+
+ return false
+
+ buildDomNodesForScreenRows: (screenRows) ->
+ return unless @canMeasure()
+
+ @emitter.emit "will-measure-screen-rows", screenRows
+ visibleLines = {}
+ html = ""
+ newLinesIds = []
+ screenRows.forEach (screenRow) =>
+ line = @editor.tokenizedLineForScreenRow(screenRow)
+ return unless line?
+ visibleLines[line.id] = true
+ return if @lineNodesByLineId.hasOwnProperty(line.id)
+
+ if lineHtml = @htmlProviderFn(screenRow, line)
+ html += @htmlProviderFn(screenRow, line)
+ newLinesIds.push(line.id)
+
+ for lineId, lineNode of @lineNodesByLineId
+ continue if visibleLines.hasOwnProperty(lineId)
+
+ lineNode.remove()
+ delete @lineNodesByLineId[lineId]
+
+ @domNode.insertAdjacentHTML("beforeend", html)
+ index = @domNode.children.length - 1
+ while lineId = newLinesIds.pop()
+ lineNode = @domNode.children[index--]
+ @lineNodesByLineId[lineId] = lineNode
+
+ lineNodeForScreenRow: (screenRow) ->
+ line = @editor.tokenizedLineForScreenRow(screenRow)
+ lineNode = @lineNodesByLineId[line.id]
+ lineNode
+
+ pixelPositionForScreenPosition: (screenPosition, clip = true) ->
+ screenPosition = Point.fromObject(screenPosition)
+ screenPosition = @editor.clipScreenPosition(screenPosition) if clip
+
+ targetRow = screenPosition.row
+ targetColumn = screenPosition.column
+
+ top = targetRow * @editor.getLineHeightInPixels()
+ left = @leftPixelPositionForScreenPosition(screenPosition)
+
+ {top, left}
+
+ leftPixelPositionForScreenPosition: ({row, column}) ->
+ lineNode = @lineNodeForScreenRow(row)
+ return 0 unless lineNode?
+
+ tokenizedLine = @editor.tokenizedLineForScreenRow(row)
+ iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT, AcceptFilter)
+ charIndex = 0
+
+ @tokenIterator.reset(tokenizedLine)
+ 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++
+
+ continue if char is '\0'
+
+ unless textNode?
+ 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
+
+ if charIndex is column
+ indexWithinToken = charIndex - textNodeIndex
+ return @leftPixelPositionForCharInTextNode(textNode, indexWithinToken)
+
+ charIndex += charLength
+
+ if textNode?
+ @leftPixelPositionForCharInTextNode(textNode, textNode.textContent.length)
+ else
+ 0
+
+ leftPixelPositionForCharInTextNode: (textNode, charIndex) ->
+ rangeForMeasurement.setEnd(textNode, textNode.textContent.length)
+
+ if charIndex is 0
+ rangeForMeasurement.setStart(textNode, 0)
+ rangeForMeasurement.getBoundingClientRect().left
+ else if charIndex is textNode.textContent.length
+ rangeForMeasurement.setStart(textNode, 0)
+ rangeForMeasurement.getBoundingClientRect().right
+ else
+ rangeForMeasurement.setStart(textNode, charIndex)
+ rangeForMeasurement.getBoundingClientRect().left
diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee
index 81f92476f8d..f39a45fee9f 100644
--- a/src/text-editor-component.coffee
+++ b/src/text-editor-component.coffee
@@ -12,6 +12,8 @@ LinesComponent = require './lines-component'
ScrollbarComponent = require './scrollbar-component'
ScrollbarCornerComponent = require './scrollbar-corner-component'
OverlayManager = require './overlay-manager'
+LinesYardstick = require './lines-yardstick'
+LineHtmlBuilder = require './line-html-builder'
module.exports =
class TextEditorComponent
@@ -43,6 +45,13 @@ class TextEditorComponent
@observeConfig()
@setScrollSensitivity(atom.config.get('editor.scrollSensitivity'))
+ @lineHtmlBuilder = new LineHtmlBuilder(true)
+ @linesYardstick = new LinesYardstick(@editor)
+ @linesYardstick.onWillMeasureScreenRows =>
+ @showIndentGuide = atom.config.get('editor.showIndentGuide')
+ @linesYardstick.setLineHtmlProvider (screenRow, line) =>
+ @lineHtmlBuilder.buildLineHTML(@showIndentGuide, 0, line)
+
@presenter = new TextEditorPresenter
model: @editor
scrollTop: @editor.getScrollTop()
@@ -51,7 +60,7 @@ class TextEditorComponent
cursorBlinkPeriod: @cursorBlinkPeriod
cursorBlinkResumeDelay: @cursorBlinkResumeDelay
stoppedScrollingDelay: 200
-
+ linesYardstick: @linesYardstick
@presenter.onDidUpdateState(@requestUpdate)
@domNode = document.createElement('div')
@@ -66,6 +75,8 @@ class TextEditorComponent
@domNode.classList.add('editor-contents')
@overlayManager = new OverlayManager(@presenter, @domNode)
+ @domNode.appendChild(@linesYardstick.getDomNode())
+
@scrollViewNode = document.createElement('div')
@scrollViewNode.classList.add('scroll-view')
@domNode.appendChild(@scrollViewNode)
@@ -102,6 +113,11 @@ class TextEditorComponent
@updateSync()
@checkForVisibilityChange()
+ @disposables.add @linesYardstick.onDidInitialize =>
+ @linesYardstick.setDefaultFont(@fontFamily, @fontSize)
+ @linesYardstick.resetStyleSheets(@stylesElement.children)
+ @presenter.characterWidthsChanged()
+
destroy: ->
@mounted = false
@disposables.dispose()
@@ -479,6 +495,7 @@ class TextEditorComponent
@handleStylingChange()
handleStylingChange: =>
+ @linesYardstick.resetStyleSheets(@stylesElement.children)
@sampleFontStyling()
@sampleBackgroundColors()
@remeasureCharacterWidths()
@@ -617,6 +634,7 @@ class TextEditorComponent
if @fontSize isnt oldFontSize or @fontFamily isnt oldFontFamily or @lineHeight isnt oldLineHeight
@measureLineHeightAndDefaultCharWidth()
+ @linesYardstick.setDefaultFont(@fontFamily, @fontSize)
if (@fontSize isnt oldFontSize or @fontFamily isnt oldFontFamily) and @performedInitialMeasurement
@remeasureCharacterWidths()
@@ -760,6 +778,21 @@ class TextEditorComponent
left = clientX - linesClientRect.left + @presenter.scrollLeft
{top, left}
+ prepareScreenRowsForMeasurement: (screenRows) ->
+ @linesYardstick.buildDomNodesForScreenRows(screenRows)
+
+ pixelPositionForScreenPosition: (screenPosition, clip = true) ->
+ screenPosition = Point.fromObject(screenPosition)
+ screenPosition = @editor.clipScreenPosition(screenPosition) if clip
+
+ targetRow = screenPosition.row
+ targetColumn = screenPosition.column
+
+ top = targetRow * @editor.getLineHeightInPixels()
+ left = @linesYardstick.leftPixelPositionForScreenPosition(screenPosition)
+
+ {top, left}
+
getModel: ->
@editor
diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee
index 1e103f2217d..149f101f0ea 100644
--- a/src/text-editor-presenter.coffee
+++ b/src/text-editor-presenter.coffee
@@ -16,7 +16,7 @@ class TextEditorPresenter
{@model, @autoHeight, @explicitHeight, @contentFrameWidth, @scrollTop, @scrollLeft, @boundingClientRect, @windowWidth, @windowHeight, @gutterWidth} = params
{horizontalScrollbarHeight, verticalScrollbarWidth} = params
{@lineHeight, @baseCharacterWidth, @backgroundColor, @gutterBackgroundColor, @tileSize} = params
- {@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay, @focused} = params
+ {@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay, @focused, @linesYardstick} = params
@measuredHorizontalScrollbarHeight = horizontalScrollbarHeight
@measuredVerticalScrollbarWidth = verticalScrollbarWidth
@gutterWidth ?= 0
@@ -62,15 +62,56 @@ class TextEditorPresenter
isBatching: ->
@updating is false
+ getMeasurableScreenRows: ->
+ measurableRows = new Set
+
+ for decoration in @model.getOverlayDecorations()
+ marker = decoration.getMarker()
+ continue unless marker.isValid()
+
+ if decoration.getProperties().position is 'tail'
+ screenPosition = marker.getTailScreenPosition()
+ else
+ screenPosition = marker.getHeadScreenPosition()
+
+ measurableRows.add(screenPosition.row)
+
+ for markerId, decorations of @model.decorationsForScreenRowRange(@startRow, @endRow - 1)
+ hasHighlights =
+ decorations.some (decoration) -> decoration.isType('highlight')
+ continue unless hasHighlights
+
+ range = @model.getMarker(markerId).getScreenRange()
+ range.start.row = Math.max(@startRow, range.start.row)
+ range.end.row = Math.min(@endRow, range.end.row)
+ measurableRows.add(row) for row in range.getRows() by 1
+
+ for cursor in @model.cursors
+ screenRow = cursor.getScreenRow()
+ continue unless cursor.isVisible() and @startRow <= screenRow < @endRow
+ measurableRows.add(screenRow)
+
+ if lastCursorRange = @model.getLastCursor()?.getScreenRange()
+ measurableRows.add(lastCursorRange.start.row)
+ measurableRows.add(lastCursorRange.end.row)
+
+ if longestScreenRow = @model.getLongestScreenRow()
+ measurableRows.add(longestScreenRow)
+
+ measurableRows
+
# 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: ->
@updating = true
- @updateContentDimensions()
- @updateScrollbarDimensions()
+ @updateVerticalDimensions()
@updateStartRow()
@updateEndRow()
+ @linesYardstick.buildDomNodesForScreenRows(@getMeasurableScreenRows())
+
+ @updateHorizontalDimensions()
+ @updateScrollbarDimensions()
@updateCommonGutterState()
@updateFocusedState() if @shouldUpdateFocusedState
@@ -112,7 +153,7 @@ class TextEditorPresenter
observeModel: ->
@disposables.add @model.onDidChange =>
- @updateContentDimensions()
+ @updateVerticalDimensions()
@shouldUpdateHeightState = true
@shouldUpdateVerticalScrollState = true
@@ -231,10 +272,13 @@ class TextEditorPresenter
@shouldUpdateLinesState = true
@shouldUpdateLineNumbersState = true
- @updateContentDimensions()
- @updateScrollbarDimensions()
+ @updateVerticalDimensions()
@updateStartRow()
@updateEndRow()
+ @linesYardstick.buildDomNodesForScreenRows(@getMeasurableScreenRows())
+
+ @updateHorizontalDimensions()
+ @updateScrollbarDimensions()
@updateFocusedState()
@updateHeightState()
@@ -387,28 +431,33 @@ class TextEditorPresenter
lineState.top = (row - startRow) * @lineHeight
lineState.decorationClasses = @lineDecorationClassesForRow(row)
else
- tileState.lines[line.id] =
- screenRow: row
- text: line.text
- openScopes: line.openScopes
- tags: line.tags
- specialTokens: line.specialTokens
- firstNonWhitespaceIndex: line.firstNonWhitespaceIndex
- firstTrailingWhitespaceIndex: line.firstTrailingWhitespaceIndex
- invisibles: line.invisibles
- endOfLineInvisibles: line.endOfLineInvisibles
- isOnlyWhitespace: line.isOnlyWhitespace()
- indentLevel: line.indentLevel
- tabLength: line.tabLength
- fold: line.fold
- top: (row - startRow) * @lineHeight
- decorationClasses: @lineDecorationClassesForRow(row)
+ lineState = tileState.lines[line.id] = @buildLineState(row, line)
+ lineState.top = (row - startRow) * @lineHeight
+
row++
for id, line of tileState.lines
delete tileState.lines[id] unless visibleLineIds.hasOwnProperty(id)
return
+ buildLineState: (row, line) ->
+ {
+ screenRow: row
+ text: line.text
+ openScopes: line.openScopes
+ tags: line.tags
+ specialTokens: line.specialTokens
+ firstNonWhitespaceIndex: line.firstNonWhitespaceIndex
+ firstTrailingWhitespaceIndex: line.firstTrailingWhitespaceIndex
+ invisibles: line.invisibles
+ endOfLineInvisibles: line.endOfLineInvisibles
+ isOnlyWhitespace: line.isOnlyWhitespace()
+ indentLevel: line.indentLevel
+ tabLength: line.tabLength
+ fold: line.fold
+ decorationClasses: @lineDecorationClassesForRow(row)
+ }
+
updateCursorsState: ->
@state.content.cursors = {}
@updateCursorState(cursor) for cursor in @model.cursors # using property directly to avoid allocation
@@ -647,11 +696,17 @@ class TextEditorPresenter
@scrollHeight = scrollHeight
@updateScrollTop()
- 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()
@@ -659,15 +714,14 @@ 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()
+ updateContentDimensions: ->
+ @updateVerticalDimensions()
+ @updateHorizontalDimensions()
+
updateClientHeight: ->
return unless @height? and @horizontalScrollbarHeight?
@@ -1037,38 +1091,9 @@ class TextEditorPresenter
hasPixelPositionRequirements: ->
@lineHeight? and @baseCharacterWidth?
- pixelPositionForScreenPosition: (screenPosition, clip=true) ->
- screenPosition = Point.fromObject(screenPosition)
- screenPosition = @model.clipScreenPosition(screenPosition) if clip
-
- targetRow = screenPosition.row
- targetColumn = screenPosition.column
- baseCharacterWidth = @baseCharacterWidth
-
- 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
+ pixelPositionForScreenPosition: (screenPosition, clip) ->
+ {top, left} =
+ @linesYardstick.pixelPositionForScreenPosition(screenPosition, clip)
top -= @scrollTop
left -= @scrollLeft