Render the entire editor with React. Handle vertical scrolling.

The space-pen view is now a simple wrapper around the entire React
component to integrate it cleanly into our existing system. React
components can't adopt existing DOM nodes, otherwise I would just have
the react component take over the entire view instead of wrapping.
This commit is contained in:
Nathan Sobo 2014-03-27 19:02:24 -06:00
parent 9c49a2d970
commit a134a60ce8
8 changed files with 142 additions and 122 deletions

View File

@ -0,0 +1,39 @@
{React} = require 'reactionary'
EditorComponent = require '../src/editor-component'
describe "EditorComponent", ->
[editor, component, node, lineHeight] = []
beforeEach ->
editor = atom.project.openSync('sample.js')
container = document.querySelector('#jasmine-content')
component = React.renderComponent(EditorComponent({editor}), container)
node = component.getDOMNode()
fontSize = 20
lineHeight = 1.3 * fontSize
node.style.lineHeight = 1.3
node.style.fontSize = fontSize + 'px'
it "renders only the currently-visible lines", ->
node.style.height = 4.5 * lineHeight + 'px'
component.updateAllDimensions()
lines = node.querySelectorAll('.line')
expect(lines.length).toBe 5
expect(lines[0].textContent).toBe editor.lineForScreenRow(0).text
expect(lines[4].textContent).toBe editor.lineForScreenRow(4).text
node.querySelector('.vertical-scrollbar').scrollTop = 2.5 * lineHeight
component.onVerticalScroll()
expect(node.querySelector('.lines').offsetTop).toBe -2.5 * lineHeight
lines = node.querySelectorAll('.line')
expect(lines.length).toBe 5
expect(lines[0].textContent).toBe editor.lineForScreenRow(2).text
expect(lines[4].textContent).toBe editor.lineForScreenRow(6).text
spacers = node.querySelectorAll('.spacer')
expect(spacers[0].offsetHeight).toBe 2 * lineHeight
expect(spacers[1].offsetHeight).toBe (editor.getScreenLineCount() - 7) * lineHeight

View File

@ -1,19 +0,0 @@
{React} = require 'reactionary'
EditorContentsComponent = require '../src/editor-contents-component'
describe "EditorComponent", ->
container = null
beforeEach ->
container = document.querySelector('#jasmine-content')
waitsForPromise -> atom.packages.activatePackage('language-javascript')
it "renders the lines that are in view based on the relevant dimensions", ->
editor = atom.project.openSync('sample.js')
lineHeight = 20
component = React.renderComponent(EditorContentsComponent({editor}), container)
component.setState
lineHeight: lineHeight
height: 5 * lineHeight
scrollTop: 3 * lineHeight
console.log component.getDOMNode()

View File

@ -1,26 +0,0 @@
ReactEditorView = require '../src/react-editor-view'
describe "ReactEditorView", ->
[editorView, editor, lineHeight] = []
beforeEach ->
editor = atom.project.openSync('sample.js')
editorView = new ReactEditorView(editor)
fontSize = 20
lineHeight = 1.3 * fontSize
editorView.css({lineHeight: 1.3, fontSize})
it "renders only the currently-visible lines", ->
editorView.height(4.5 * lineHeight)
editorView.attachToDom()
lines = editorView.element.querySelectorAll('.line')
expect(lines.length).toBe 5
expect(lines[0].textContent).toBe editor.lineForScreenRow(0).text
expect(lines[4].textContent).toBe editor.lineForScreenRow(4).text
editorView.setScrollTop(2.5 * lineHeight)
lines = editorView.element.querySelectorAll('.line')
expect(lines.length).toBe 5
expect(lines[0].textContent).toBe editor.lineForScreenRow(2).text
expect(lines[4].textContent).toBe editor.lineForScreenRow(6).text

View File

@ -0,0 +1,97 @@
{React, div, span} = require 'reactionary'
{last} = require 'underscore-plus'
{$$} = require 'space-pencil'
DummyLineNode = $$ ->
@div class: 'line', style: 'position: absolute; visibility: hidden;', -> @span 'x'
module.exports =
React.createClass
render: ->
div class: 'editor',
div class: 'scroll-view', ref: 'scrollView',
div @renderVisibleLines()
div class: 'vertical-scrollbar', ref: 'verticalScrollbar', onScroll: @onVerticalScroll,
div outlet: 'verticalScrollbarContent', style: {height: @getScrollHeight()}
renderVisibleLines: ->
[startRow, endRow] = @getVisibleRowRange()
precedingHeight = startRow * @state.lineHeight
lineCount = @props.editor.getScreenLineCount()
followingHeight = (lineCount - endRow) * @state.lineHeight
div class: 'lines', ref: 'lines', style: {top: -@state.scrollTop},
div class: 'spacer', style: {height: precedingHeight}
for tokenizedLine in @props.editor.linesForScreenRows(startRow, endRow - 1)
LineComponent({tokenizedLine, key: tokenizedLine.id})
div class: 'spacer', style: {height: followingHeight}
getInitialState: ->
height: 0
width: 0
lineHeight: 0
scrollTop: 0
componentDidMount: ->
@props.editor.on 'screen-lines-changed', @onScreenLinesChanged
@updateAllDimensions()
componentWillUnmount: ->
@props.editor.off 'screen-lines-changed', @onScreenLinesChanged
componentWilUpdate: (nextProps, nextState) ->
if nextState.scrollTop?
@refs.verticalScrollbar.getDOMNode().scrollTop = nextState.scrollTop
onVerticalScroll: ->
scrollTop = @refs.verticalScrollbar.getDOMNode().scrollTop
@setState({scrollTop})
onScreenLinesChanged: ({start, end}) =>
[visibleStart, visibleEnd] = @getVisibleRowRange()
@forceUpdate() unless end < visibleStart or visibleEnd <= start
getVisibleRowRange: ->
return [0, 0] unless @state.lineHeight > 0
heightInLines = @state.height / @state.lineHeight
startRow = Math.floor(@state.scrollTop / @state.lineHeight)
endRow = Math.ceil(startRow + heightInLines)
[startRow, endRow]
getScrollHeight: ->
@props.editor.getLineCount() * @state.lineHeight
updateAllDimensions: ->
lineHeight = @measureLineHeight()
{height, width} = @measureScrollViewDimensions()
console.log "updating dimensions", {lineHeight, height, width}
@setState({lineHeight, height, width})
measureScrollViewDimensions: ->
scrollViewNode = @refs.scrollView.getDOMNode()
{height: scrollViewNode.clientHeight, width: scrollViewNode.clientWidth}
measureLineHeight: ->
linesNode = @refs.lines.getDOMNode()
linesNode.appendChild(DummyLineNode)
lineHeight = DummyLineNode.getBoundingClientRect().height
linesNode.removeChild(DummyLineNode)
lineHeight
LineComponent = React.createClass
render: ->
div class: 'line',
if @props.tokenizedLine.text.length is 0
span String.fromCharCode(160) # non-breaking space; bypasses escaping
else
@renderScopeTree(@props.tokenizedLine.getScopeTree())
renderScopeTree: (scopeTree) ->
if scopeTree.scope?
span class: scopeTree.scope.split('.').join(' '),
scopeTree.children.map (child) => @renderScopeTree(child)
else
span scopeTree.value

View File

@ -1,45 +0,0 @@
{React, div, span} = require 'reactionary'
{last} = require 'underscore-plus'
module.exports =
React.createClass
render: ->
div class: 'lines', @renderVisibleLines()
renderVisibleLines: ->
return [] unless @props.lineHeight > 0
[startRow, endRow] = @getVisibleRowRange()
for tokenizedLine in @props.editor.linesForScreenRows(startRow, endRow - 1)
LineComponent({tokenizedLine, key: tokenizedLine.id})
getDefaultProps: ->
height: 0
lineHeight: 0
scrollTop: 0
getVisibleRowRange: ->
heightInLines = @props.height / @props.lineHeight
startRow = Math.floor(@props.scrollTop / @props.lineHeight)
endRow = Math.ceil(startRow + heightInLines)
[startRow, endRow]
onScreenLinesChanged: ({start, end}) ->
[visibleStart, visibleEnd] = @getVisibleRowRange()
@forceUpdate() unless end < visibleStart or visibleEnd <= start
LineComponent = React.createClass
render: ->
{tokenizedLine} = @props
div class: 'line',
if tokenizedLine.text.length is 0
span {}, String.fromCharCode(160) # non-breaking space; bypasses escaping
else
@renderScopeTree(tokenizedLine.getScopeTree())
renderScopeTree: (scopeTree) ->
if scopeTree.scope?
span class: scopeTree.scope.split('.').join(' '),
scopeTree.children.map (child) => @renderScopeTree(child)
else
span scopeTree.value

View File

@ -4,7 +4,6 @@ GutterView = require './gutter-view'
Editor = require './editor'
CursorView = require './cursor-view'
SelectionView = require './selection-view'
EditorContentsComponent = require './editor-contents-component'
fs = require 'fs-plus'
_ = require 'underscore-plus'

View File

@ -1,43 +1,18 @@
{View} = require 'space-pen'
{$$} = require 'space-pencil'
{React} = require 'reactionary'
EditorContentsComponent = require './editor-contents-component'
EditorComponent = require './editor-component'
module.exports =
class ReactEditorView extends View
@content: ->
@div class: 'editor', =>
@div class: 'scroll-view', outlet: 'scrollView'
@content: -> @div class: 'react-wrapper'
constructor: (@editor) ->
super
@scrollView = @scrollView.element
@contents = React.renderComponent(EditorContentsComponent({@editor}), @scrollView)
@subscribe @editor, 'screen-lines-changed', (change) => @contents.onScreenLinesChanged(change)
afterAttach: (onDom) ->
return unless onDom
@component = React.renderComponent(EditorComponent({@editor}), @element)
@editor.setVisible(true)
@measureLineHeight()
@contents.setProps
height: @scrollView.clientHeight
scrollTop: @scrollView.scrollTop
lineHeight: @lineHeight
setScrollTop: (scrollTop) ->
@contents.setProps({scrollTop})
measureLineHeight: ->
fragment = $$ ->
@div class: 'lines', ->
@div class: 'line', style: 'position: absolute; visibility: hidden;', -> @span 'x'
@scrollView.appendChild(fragment)
lineRect = fragment.firstChild.getBoundingClientRect()
charRect = fragment.firstChild.firstChild.getBoundingClientRect()
@lineHeight = lineRect.height
@charWidth = charRect.width
@charHeight = charRect.height
@scrollView.removeChild(fragment)
beforeDetach: ->
React.unmountComponentAtNode(@element)

View File

@ -38,7 +38,7 @@
background-color: @pane-item-background-color;
}
> * {
> *, > .react-wrapper > * {
position: absolute;
top: 0;
right: 0;