diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee
index efbe92a21..ab37e2d7c 100644
--- a/spec/editor-component-spec.coffee
+++ b/spec/editor-component-spec.coffee
@@ -27,7 +27,9 @@ describe "EditorComponent", ->
{component} = wrapperView
component.setLineHeight(1.3)
component.setFontSize(20)
- {lineHeightInPixels, charWidth} = component.measureLineDimensions()
+
+ lineHeightInPixels = editor.getLineHeight()
+ charWidth = editor.getDefaultCharWidth()
node = component.getDOMNode()
verticalScrollbarNode = node.querySelector('.vertical-scrollbar')
horizontalScrollbarNode = node.querySelector('.horizontal-scrollbar')
@@ -35,7 +37,7 @@ describe "EditorComponent", ->
describe "line rendering", ->
it "renders only the currently-visible lines", ->
node.style.height = 4.5 * lineHeightInPixels + 'px'
- component.updateAllDimensions()
+ component.updateModelDimensions()
lines = node.querySelectorAll('.line')
expect(lines.length).toBe 6
@@ -111,7 +113,7 @@ describe "EditorComponent", ->
it "renders the currently-visible line numbers", ->
node.style.height = 4.5 * lineHeightInPixels + 'px'
- component.updateAllDimensions()
+ component.updateModelDimensions()
lines = node.querySelectorAll('.line-number')
expect(lines.length).toBe 6
@@ -136,7 +138,7 @@ describe "EditorComponent", ->
editor.setSoftWrap(true)
node.style.height = 4.5 * lineHeightInPixels + 'px'
node.style.width = 30 * charWidth + 'px'
- component.updateAllDimensions()
+ component.updateModelDimensions()
lines = node.querySelectorAll('.line-number')
expect(lines.length).toBe 6
@@ -153,7 +155,7 @@ describe "EditorComponent", ->
cursor1.setScreenPosition([0, 5])
node.style.height = 4.5 * lineHeightInPixels + 'px'
- component.updateAllDimensions()
+ component.updateModelDimensions()
cursorNodes = node.querySelectorAll('.cursor')
expect(cursorNodes.length).toBe 1
@@ -248,7 +250,7 @@ describe "EditorComponent", ->
inputNode = node.querySelector('.hidden-input')
node.style.height = 5 * lineHeightInPixels + 'px'
node.style.width = 10 * charWidth + 'px'
- component.updateAllDimensions()
+ component.updateModelDimensions()
expect(editor.getCursorScreenPosition()).toEqual [0, 0]
editor.setScrollTop(3 * lineHeightInPixels)
@@ -332,7 +334,7 @@ describe "EditorComponent", ->
it "moves the cursor to the nearest screen position", ->
node.style.height = 4.5 * lineHeightInPixels + 'px'
node.style.width = 10 * charWidth + 'px'
- component.updateAllDimensions()
+ component.updateModelDimensions()
editor.setScrollTop(3.5 * lineHeightInPixels)
editor.setScrollLeft(2 * charWidth)
@@ -445,7 +447,7 @@ describe "EditorComponent", ->
describe "scrolling", ->
it "updates the vertical scrollbar when the scrollTop is changed in the model", ->
node.style.height = 4.5 * lineHeightInPixels + 'px'
- component.updateAllDimensions()
+ component.updateModelDimensions()
expect(verticalScrollbarNode.scrollTop).toBe 0
@@ -454,7 +456,7 @@ describe "EditorComponent", ->
it "updates the horizontal scrollbar and scroll view content x transform based on the scrollLeft of the model", ->
node.style.width = 30 * charWidth + 'px'
- component.updateAllDimensions()
+ component.updateModelDimensions()
scrollViewContentNode = node.querySelector('.scroll-view-content')
expect(scrollViewContentNode.style['-webkit-transform']).toBe "translate(0px, 0px)"
@@ -466,7 +468,7 @@ describe "EditorComponent", ->
it "updates the scrollLeft of the model when the scrollLeft of the horizontal scrollbar changes", ->
node.style.width = 30 * charWidth + 'px'
- component.updateAllDimensions()
+ component.updateModelDimensions()
expect(editor.getScrollLeft()).toBe 0
horizontalScrollbarNode.scrollLeft = 100
@@ -478,7 +480,7 @@ describe "EditorComponent", ->
it "updates the horizontal or vertical scrollbar depending on which delta is greater (x or y)", ->
node.style.height = 4.5 * lineHeightInPixels + 'px'
node.style.width = 20 * charWidth + 'px'
- component.updateAllDimensions()
+ component.updateModelDimensions()
expect(verticalScrollbarNode.scrollTop).toBe 0
expect(horizontalScrollbarNode.scrollLeft).toBe 0
diff --git a/src/editor-component.coffee b/src/editor-component.coffee
index 12f1f0f7f..2f4cd8cfb 100644
--- a/src/editor-component.coffee
+++ b/src/editor-component.coffee
@@ -4,7 +4,6 @@ React = require 'react'
GutterComponent = require './gutter-component'
EditorScrollViewComponent = require './editor-scroll-view-component'
-{DummyLineNode} = EditorScrollViewComponent
ScrollbarComponent = require './scrollbar-component'
SubscriberMixin = require './subscriber-mixin'
@@ -14,8 +13,6 @@ EditorCompont = React.createClass
pendingScrollLeft: null
selectOnMouseMove: false
- statics: {DummyLineNode}
-
mixins: [SubscriberMixin]
render: ->
@@ -286,11 +283,5 @@ EditorCompont = React.createClass
requestUpdate: ->
@forceUpdate()
- measureLineDimensions: ->
- @refs.scrollView.measureLineDimensions()
-
- updateAllDimensions: ->
- @refs.scrollView.updateAllDimensions()
-
- updateScrollViewDimensions: ->
- @refs.scrollView.updateScrollViewDimensions()
+ updateModelDimensions: ->
+ @refs.scrollView.updateModelDimensions()
diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee
index 722452b2b..a5a46c17d 100644
--- a/src/editor-scroll-view-component.coffee
+++ b/src/editor-scroll-view-component.coffee
@@ -2,22 +2,23 @@ React = require 'react'
ReactUpdates = require 'react/lib/ReactUpdates'
{div, span} = require 'reactionary'
{debounce, isEqual, multiplyString, pick} = require 'underscore-plus'
-{$$} = require 'space-pen'
InputComponent = require './input-component'
+LinesComponent = require './lines-component'
CursorComponent = require './cursor-component'
SelectionComponent = require './selection-component'
SubscriberMixin = require './subscriber-mixin'
-DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibility: hidden;', => @span 'x')[0]
-AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT}
-
module.exports =
EditorScrollViewComponent = React.createClass
mixins: [SubscriberMixin]
render: ->
- {onInputFocused, onInputBlurred} = @props
+ {editor, fontSize, fontFamily, lineHeight, showIndentGuide} = @props
+ {visibleRowRange, onInputFocused, onInputBlurred} = @props
+ contentStyle =
+ height: editor.getScrollHeight()
+ WebkitTransform: "translate(#{-editor.getScrollLeft()}px, #{-editor.getScrollTop()}px)"
div className: 'scroll-view', ref: 'scrollView',
InputComponent
@@ -27,18 +28,11 @@ EditorScrollViewComponent = React.createClass
onInput: @onInput
onFocus: onInputFocused
onBlur: onInputBlurred
- @renderScrollViewContent()
- renderScrollViewContent: ->
- {editor} = @props
- style =
- height: editor.getScrollHeight()
- WebkitTransform: "translate(#{-editor.getScrollLeft()}px, #{-editor.getScrollTop()}px)"
-
- div {className: 'scroll-view-content', style, @onMouseDown},
- @renderCursors()
- @renderVisibleLines()
- @renderUnderlayer()
+ div className: 'scroll-view-content', style: contentStyle, onMouseDown: @onMouseDown,
+ @renderCursors()
+ LinesComponent({ref: 'lines', editor, fontSize, fontFamily, lineHeight, visibleRowRange, showIndentGuide})
+ @renderUnderlayer()
renderCursors: ->
{editor} = @props
@@ -47,21 +41,6 @@ EditorScrollViewComponent = React.createClass
for selection in editor.getSelections() when editor.selectionIntersectsVisibleRowRange(selection)
CursorComponent(cursor: selection.cursor, blinkOff: blinkCursorsOff)
- renderVisibleLines: ->
- {editor, visibleRowRange} = @props
- {showIndentGuide} = @props
- [startRow, endRow] = visibleRowRange
- lineHeightInPixels = editor.getLineHeight()
- precedingHeight = startRow * lineHeightInPixels
- followingHeight = (editor.getScreenLineCount() - endRow) * lineHeightInPixels
-
- div className: 'lines', ref: 'lines', [
- div className: 'spacer', key: 'top-spacer', style: {height: precedingHeight}
- (for tokenizedLine in @props.editor.linesForScreenRows(startRow, endRow - 1)
- LineComponent({tokenizedLine, showIndentGuide, key: tokenizedLine.id}))...
- div className: 'spacer', key: 'bottom-spacer', style: {height: followingHeight}
- ]
-
renderUnderlayer: ->
{editor} = @props
@@ -73,25 +52,11 @@ EditorScrollViewComponent = React.createClass
blinkCursorsOff: false
componentDidMount: ->
- @measuredLines = new WeakSet
-
- @getDOMNode().addEventListener 'overflowchanged', @onOverflowChanged
-
+ @getDOMNode().addEventListener 'overflowchanged', @updateModelDimensions
@subscribe @props.editor, 'cursors-moved', @pauseCursorBlinking
-
-
- @updateAllDimensions()
+ @updateModelDimensions()
@startBlinkingCursors()
- componentDidUpdate: (prevProps) ->
- unless isEqual(pick(prevProps, 'fontSize', 'fontFamily', 'lineHeight'), pick(@props, 'fontSize', 'fontFamily', 'lineHeight'))
- @updateLineDimensions()
-
- unless isEqual(pick(prevProps, 'fontSize', 'fontFamily'), pick(@props, 'fontSize', 'fontFamily'))
- @clearScopedCharWidths()
-
- @measureNewLines()
-
focus: ->
@refs.input.focus()
@@ -196,98 +161,8 @@ EditorScrollViewComponent = React.createClass
left = clientX - editorClientRect.left + editor.getScrollLeft()
{top, left}
- onOverflowChanged: ->
+ updateModelDimensions: ->
{editor} = @props
- {height, width} = @measureScrollViewDimensions()
- editor.setHeight(height)
- editor.setWidth(width)
-
- updateAllDimensions: ->
- @updateScrollViewDimensions()
- @updateLineDimensions()
-
- updateScrollViewDimensions: ->
- {editor} = @props
- {height, width} = @measureScrollViewDimensions()
- editor.setHeight(height)
- editor.setWidth(width)
-
- updateLineDimensions: ->
- {editor} = @props
- {lineHeightInPixels, charWidth} = @measureLineDimensions()
- editor.setLineHeight(lineHeightInPixels)
- editor.setDefaultCharWidth(charWidth)
-
- measureScrollViewDimensions: ->
node = @getDOMNode()
- {height: node.clientHeight, width: node.clientWidth}
-
- measureLineDimensions: ->
- linesNode = @refs.lines.getDOMNode()
- linesNode.appendChild(DummyLineNode)
- lineHeightInPixels = DummyLineNode.getBoundingClientRect().height
- charWidth = DummyLineNode.firstChild.getBoundingClientRect().width
- linesNode.removeChild(DummyLineNode)
- {lineHeightInPixels, charWidth}
-
- measureNewLines: ->
- [visibleStartRow, visibleEndRow] = @props.visibleRowRange
- linesNode = @refs.lines.getDOMNode()
-
- for tokenizedLine, i in @props.editor.linesForScreenRows(visibleStartRow, visibleEndRow - 1)
- unless @measuredLines.has(tokenizedLine)
- lineNode = linesNode.children[i + 1]
- @measureCharactersInLine(tokenizedLine, lineNode)
-
- measureCharactersInLine: (tokenizedLine, lineNode) ->
- {editor} = @props
- iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT, AcceptFilter)
- rangeForMeasurement = document.createRange()
-
- for {value, scopes} in tokenizedLine.tokens
- textNode = iterator.nextNode()
- charWidths = editor.getScopedCharWidths(scopes)
- for char, i in value
- unless charWidths[char]?
- rangeForMeasurement.setStart(textNode, i)
- rangeForMeasurement.setEnd(textNode, i + 1)
- charWidth = rangeForMeasurement.getBoundingClientRect().width
- editor.setScopedCharWidth(scopes, char, charWidth)
-
- @measuredLines.add(tokenizedLine)
-
- clearScopedCharWidths: ->
- @measuredLines.clear()
- @props.editor.clearScopedCharWidths()
-
-LineComponent = React.createClass
- render: ->
- div className: 'line', dangerouslySetInnerHTML: {__html: @buildInnerHTML()}
-
- buildInnerHTML: ->
- if @props.tokenizedLine.text.length is 0
- @buildEmptyLineHTML()
- else
- @buildScopeTreeHTML(@props.tokenizedLine.getScopeTree())
-
- buildEmptyLineHTML: ->
- {showIndentGuide, tokenizedLine} = @props
- {indentLevel, tabLength} = tokenizedLine
-
- if showIndentGuide and indentLevel > 0
- indentSpan = "#{multiplyString(' ', tabLength)}"
- multiplyString(indentSpan, indentLevel + 1)
- else
- " "
-
- buildScopeTreeHTML: (scopeTree) ->
- if scopeTree.children?
- html = ""
- html += @buildScopeTreeHTML(child) for child in scopeTree.children
- html += ""
- html
- else
- "#{scopeTree.getValueAsHtml({hasIndentGuide: @props.showIndentGuide})}"
-
- shouldComponentUpdate: (newProps, newState) ->
- newProps.showIndentGuide isnt @props.showIndentGuide
+ editor.setHeight(node.clientHeight)
+ editor.setWidth(node.clientWidth)
diff --git a/src/lines-component.coffee b/src/lines-component.coffee
new file mode 100644
index 000000000..b700e91dd
--- /dev/null
+++ b/src/lines-component.coffee
@@ -0,0 +1,112 @@
+React = require 'react'
+{div, span} = require 'reactionary'
+{debounce, isEqual, multiplyString, pick} = require 'underscore-plus'
+{$$} = require 'space-pen'
+
+DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibility: hidden;', => @span 'x')[0]
+AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT}
+
+module.exports =
+LinesComponent = React.createClass
+ render: ->
+ {editor, visibleRowRange, showIndentGuide} = @props
+ [startRow, endRow] = visibleRowRange
+ lineHeightInPixels = editor.getLineHeight()
+ precedingHeight = startRow * lineHeightInPixels
+ followingHeight = (editor.getScreenLineCount() - endRow) * lineHeightInPixels
+
+ div className: 'lines', ref: 'lines', [
+ div className: 'spacer', key: 'top-spacer', style: {height: precedingHeight}
+ (for tokenizedLine in @props.editor.linesForScreenRows(startRow, endRow - 1)
+ LineComponent({tokenizedLine, showIndentGuide, key: tokenizedLine.id}))...
+ div className: 'spacer', key: 'bottom-spacer', style: {height: followingHeight}
+ ]
+
+ componentDidMount: ->
+ @measuredLines = new WeakSet
+ @updateModelDimensions()
+
+ componentDidUpdate: (prevProps) ->
+ @updateModelDimensions() unless @compareProps(prevProps, @props, 'fontSize', 'fontFamily', 'lineHeight')
+ @clearScopedCharWidths() unless @compareProps(prevProps, @props, 'fontSize', 'fontFamily')
+ @measureCharactersInNewLines()
+
+ compareProps: (a, b, whiteList...) ->
+ isEqual(pick(a, whiteList...), pick(b, whiteList...))
+
+ updateModelDimensions: ->
+ {editor} = @props
+ {lineHeightInPixels, charWidth} = @measureLineDimensions()
+ editor.setLineHeight(lineHeightInPixels)
+ editor.setDefaultCharWidth(charWidth)
+
+ measureLineDimensions: ->
+ linesNode = @refs.lines.getDOMNode()
+ linesNode.appendChild(DummyLineNode)
+ lineHeightInPixels = DummyLineNode.getBoundingClientRect().height
+ charWidth = DummyLineNode.firstChild.getBoundingClientRect().width
+ linesNode.removeChild(DummyLineNode)
+ {lineHeightInPixels, charWidth}
+
+ measureCharactersInNewLines: ->
+ [visibleStartRow, visibleEndRow] = @props.visibleRowRange
+ linesNode = @refs.lines.getDOMNode()
+
+ for tokenizedLine, i in @props.editor.linesForScreenRows(visibleStartRow, visibleEndRow - 1)
+ unless @measuredLines.has(tokenizedLine)
+ lineNode = linesNode.children[i + 1]
+ @measureCharactersInLine(tokenizedLine, lineNode)
+
+ measureCharactersInLine: (tokenizedLine, lineNode) ->
+ {editor} = @props
+ iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT, AcceptFilter)
+ rangeForMeasurement = document.createRange()
+
+ for {value, scopes} in tokenizedLine.tokens
+ textNode = iterator.nextNode()
+ charWidths = editor.getScopedCharWidths(scopes)
+ for char, i in value
+ unless charWidths[char]?
+ rangeForMeasurement.setStart(textNode, i)
+ rangeForMeasurement.setEnd(textNode, i + 1)
+ charWidth = rangeForMeasurement.getBoundingClientRect().width
+ editor.setScopedCharWidth(scopes, char, charWidth)
+
+ @measuredLines.add(tokenizedLine)
+
+ clearScopedCharWidths: ->
+ @measuredLines.clear()
+ @props.editor.clearScopedCharWidths()
+
+
+LineComponent = React.createClass
+ render: ->
+ div className: 'line', dangerouslySetInnerHTML: {__html: @buildInnerHTML()}
+
+ buildInnerHTML: ->
+ if @props.tokenizedLine.text.length is 0
+ @buildEmptyLineHTML()
+ else
+ @buildScopeTreeHTML(@props.tokenizedLine.getScopeTree())
+
+ buildEmptyLineHTML: ->
+ {showIndentGuide, tokenizedLine} = @props
+ {indentLevel, tabLength} = tokenizedLine
+
+ if showIndentGuide and indentLevel > 0
+ indentSpan = "#{multiplyString(' ', tabLength)}"
+ multiplyString(indentSpan, indentLevel + 1)
+ else
+ " "
+
+ buildScopeTreeHTML: (scopeTree) ->
+ if scopeTree.children?
+ html = ""
+ html += @buildScopeTreeHTML(child) for child in scopeTree.children
+ html += ""
+ html
+ else
+ "#{scopeTree.getValueAsHtml({hasIndentGuide: @props.showIndentGuide})}"
+
+ shouldComponentUpdate: (newProps, newState) ->
+ newProps.showIndentGuide isnt @props.showIndentGuide