mirror of
https://github.com/pulsar-edit/pulsar.git
synced 2025-01-07 23:59:22 +03:00
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:
parent
9c49a2d970
commit
a134a60ce8
39
spec/editor-component-spec.coffee
Normal file
39
spec/editor-component-spec.coffee
Normal 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
|
@ -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()
|
@ -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
|
97
src/editor-component.coffee
Normal file
97
src/editor-component.coffee
Normal 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
|
@ -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
|
@ -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'
|
||||
|
@ -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)
|
||||
|
@ -38,7 +38,7 @@
|
||||
background-color: @pane-item-background-color;
|
||||
}
|
||||
|
||||
> * {
|
||||
> *, > .react-wrapper > * {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
Loading…
Reference in New Issue
Block a user