mirror of
https://github.com/atom/atom.git
synced 2026-01-22 21:38:10 -05:00
Extract a LinesComponent
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 = "<span class='indent-guide'>#{multiplyString(' ', tabLength)}</span>"
|
||||
multiplyString(indentSpan, indentLevel + 1)
|
||||
else
|
||||
" "
|
||||
|
||||
buildScopeTreeHTML: (scopeTree) ->
|
||||
if scopeTree.children?
|
||||
html = "<span class='#{scopeTree.scope.replace(/\./g, ' ')}'>"
|
||||
html += @buildScopeTreeHTML(child) for child in scopeTree.children
|
||||
html += "</span>"
|
||||
html
|
||||
else
|
||||
"<span>#{scopeTree.getValueAsHtml({hasIndentGuide: @props.showIndentGuide})}</span>"
|
||||
|
||||
shouldComponentUpdate: (newProps, newState) ->
|
||||
newProps.showIndentGuide isnt @props.showIndentGuide
|
||||
editor.setHeight(node.clientHeight)
|
||||
editor.setWidth(node.clientWidth)
|
||||
|
||||
112
src/lines-component.coffee
Normal file
112
src/lines-component.coffee
Normal file
@@ -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 = "<span class='indent-guide'>#{multiplyString(' ', tabLength)}</span>"
|
||||
multiplyString(indentSpan, indentLevel + 1)
|
||||
else
|
||||
" "
|
||||
|
||||
buildScopeTreeHTML: (scopeTree) ->
|
||||
if scopeTree.children?
|
||||
html = "<span class='#{scopeTree.scope.replace(/\./g, ' ')}'>"
|
||||
html += @buildScopeTreeHTML(child) for child in scopeTree.children
|
||||
html += "</span>"
|
||||
html
|
||||
else
|
||||
"<span>#{scopeTree.getValueAsHtml({hasIndentGuide: @props.showIndentGuide})}</span>"
|
||||
|
||||
shouldComponentUpdate: (newProps, newState) ->
|
||||
newProps.showIndentGuide isnt @props.showIndentGuide
|
||||
Reference in New Issue
Block a user