Merge branch 'ns-manual-dom-updates'

This commit is contained in:
Nathan Sobo 2015-02-20 15:29:33 -07:00
commit 964809373b
21 changed files with 1004 additions and 773 deletions

View File

@ -82,6 +82,7 @@ beforeEach ->
atom.keymaps.keyBindings = _.clone(keyBindingsToRestore)
atom.commands.restoreSnapshot(commandsToRestore)
atom.styles.restoreSnapshot(styleElementsToRestore)
atom.views.clearDocumentRequests()
atom.workspaceViewParentSelector = '#jasmine-content'

View File

@ -46,7 +46,7 @@ describe "TextEditorComponent", ->
lineHeightInPixels = editor.getLineHeightInPixels()
charWidth = editor.getDefaultCharWidth()
componentNode = component.getDOMNode()
componentNode = component.domNode
verticalScrollbarNode = componentNode.querySelector('.vertical-scrollbar')
horizontalScrollbarNode = componentNode.querySelector('.horizontal-scrollbar')
@ -193,7 +193,8 @@ describe "TextEditorComponent", ->
expect(linesNode.style.backgroundColor).toBe backgroundColor
wrapperNode.style.backgroundColor = 'rgb(255, 0, 0)'
advanceClock(component.domPollingInterval)
advanceClock(atom.views.documentPollingInterval)
nextAnimationFrame()
expect(linesNode.style.backgroundColor).toBe 'rgb(255, 0, 0)'
@ -466,11 +467,6 @@ describe "TextEditorComponent", ->
expect(foldedLineNode.querySelector('.fold-marker')).toBeFalsy()
describe "gutter rendering", ->
[gutter] = []
beforeEach ->
{gutter} = component.refs
it "renders the currently-visible line numbers", ->
wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px'
component.measureHeightAndWidth()
@ -567,32 +563,32 @@ describe "TextEditorComponent", ->
# favor gutter color if it's assigned
gutterNode.style.backgroundColor = 'rgb(255, 0, 0)'
advanceClock(component.domPollingInterval)
advanceClock(atom.views.documentPollingInterval)
nextAnimationFrame()
expect(lineNumbersNode.style.backgroundColor).toBe 'rgb(255, 0, 0)'
it "hides or shows the gutter based on the '::isGutterVisible' property on the model and the global 'editor.showLineNumbers' config setting", ->
expect(component.refs.gutter?).toBe true
expect(component.gutterComponent?).toBe true
editor.setGutterVisible(false)
nextAnimationFrame()
expect(component.refs.gutter?).toBe false
expect(componentNode.querySelector('.gutter')).toBeNull()
atom.config.set("editor.showLineNumbers", false)
expect(nextAnimationFrame).toBe noAnimationFrame
nextAnimationFrame()
expect(component.refs.gutter?).toBe false
expect(componentNode.querySelector('.gutter')).toBeNull()
editor.setGutterVisible(true)
expect(nextAnimationFrame).toBe noAnimationFrame
nextAnimationFrame()
expect(component.refs.gutter?).toBe false
expect(componentNode.querySelector('.gutter')).toBeNull()
atom.config.set("editor.showLineNumbers", true)
nextAnimationFrame()
expect(component.refs.gutter?).toBe true
expect(componentNode.querySelector('.gutter')).toBeDefined()
expect(component.lineNumberNodeForScreenRow(3)?).toBe true
describe "fold decorations", ->
@ -706,13 +702,13 @@ describe "TextEditorComponent", ->
cursorNodes = componentNode.querySelectorAll('.cursor')
expect(cursorNodes.length).toBe 2
expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{11 * charWidth}px, #{8 * lineHeightInPixels}px)"
expect(cursorNodes[1].style['-webkit-transform']).toBe "translate(#{10 * charWidth}px, #{4 * lineHeightInPixels}px)"
expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{10 * charWidth}px, #{4 * lineHeightInPixels}px)"
expect(cursorNodes[1].style['-webkit-transform']).toBe "translate(#{11 * charWidth}px, #{8 * lineHeightInPixels}px)"
wrapperView.on 'cursor:moved', cursorMovedListener = jasmine.createSpy('cursorMovedListener')
cursor3.setScreenPosition([4, 11], autoscroll: false)
nextAnimationFrame()
expect(cursorNodes[1].style['-webkit-transform']).toBe "translate(#{11 * charWidth}px, #{4 * lineHeightInPixels}px)"
expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{11 * charWidth}px, #{4 * lineHeightInPixels}px)"
expect(cursorMovedListener).toHaveBeenCalled()
cursor3.destroy()
@ -800,11 +796,11 @@ describe "TextEditorComponent", ->
expect(cursorsNode.classList.contains('blink-off')).toBe false
advanceClock(component.props.cursorBlinkPeriod / 2)
advanceClock(component.cursorBlinkPeriod / 2)
nextAnimationFrame()
expect(cursorsNode.classList.contains('blink-off')).toBe true
advanceClock(component.props.cursorBlinkPeriod / 2)
advanceClock(component.cursorBlinkPeriod / 2)
nextAnimationFrame()
expect(cursorsNode.classList.contains('blink-off')).toBe false
@ -813,8 +809,8 @@ describe "TextEditorComponent", ->
nextAnimationFrame()
expect(cursorsNode.classList.contains('blink-off')).toBe false
advanceClock(component.props.cursorBlinkResumeDelay)
advanceClock(component.props.cursorBlinkPeriod / 2)
advanceClock(component.cursorBlinkResumeDelay)
advanceClock(component.cursorBlinkPeriod / 2)
nextAnimationFrame()
expect(cursorsNode.classList.contains('blink-off')).toBe true
@ -1501,12 +1497,14 @@ describe "TextEditorComponent", ->
expect(inputNode.offsetLeft).toBe 0
# In bounds and focused
inputNode.focus() # updates via state change
wrapperNode.focus() # updates via state change
nextAnimationFrame()
expect(inputNode.offsetTop).toBe (5 * lineHeightInPixels) - editor.getScrollTop()
expect(inputNode.offsetLeft).toBe (4 * charWidth) - editor.getScrollLeft()
# In bounds, not focused
inputNode.blur() # updates via state change
nextAnimationFrame()
expect(inputNode.offsetTop).toBe 0
expect(inputNode.offsetLeft).toBe 0
@ -1518,6 +1516,7 @@ describe "TextEditorComponent", ->
# Out of bounds, focused
inputNode.focus() # updates via state change
nextAnimationFrame()
expect(inputNode.offsetTop).toBe 0
expect(inputNode.offsetLeft).toBe 0
@ -1839,9 +1838,11 @@ describe "TextEditorComponent", ->
it "adds the 'is-focused' class to the editor when the hidden input is focused", ->
expect(document.activeElement).toBe document.body
inputNode.focus()
nextAnimationFrame()
expect(componentNode.classList.contains('is-focused')).toBe true
expect(wrapperView.hasClass('is-focused')).toBe true
inputNode.blur()
nextAnimationFrame()
expect(componentNode.classList.contains('is-focused')).toBe false
expect(wrapperView.hasClass('is-focused')).toBe false
@ -2351,11 +2352,11 @@ describe "TextEditorComponent", ->
wrapperView.appendTo(hiddenParent)
{component} = wrapperView
componentNode = component.getDOMNode()
componentNode = component.domNode
expect(componentNode.querySelectorAll('.line').length).toBe 0
hiddenParent.style.display = 'block'
advanceClock(component.domPollingInterval)
advanceClock(atom.views.documentPollingInterval)
expect(componentNode.querySelectorAll('.line').length).toBeGreaterThan 0
@ -2465,14 +2466,15 @@ describe "TextEditorComponent", ->
expect(parseInt(newHeight)).toBeLessThan wrapperNode.offsetHeight
wrapperNode.style.height = newHeight
advanceClock(component.domPollingInterval)
advanceClock(atom.views.documentPollingInterval)
nextAnimationFrame()
expect(componentNode.querySelectorAll('.line')).toHaveLength(4 + lineOverdrawMargin + 1)
gutterWidth = componentNode.querySelector('.gutter').offsetWidth
componentNode.style.width = gutterWidth + 14 * charWidth + editor.getVerticalScrollbarWidth() + 'px'
advanceClock(component.domPollingInterval)
nextAnimationFrame()
advanceClock(atom.views.documentPollingInterval)
nextAnimationFrame() # won't poll until cursor blinks
nextAnimationFrame() # handle update requested by poll
expect(componentNode.querySelector('.line').textContent).toBe "var quicksort "
it "accounts for the scroll view's padding when determining the wrap location", ->
@ -2480,7 +2482,7 @@ describe "TextEditorComponent", ->
scrollViewNode.style.paddingLeft = 20 + 'px'
componentNode.style.width = 30 * charWidth + 'px'
advanceClock(component.domPollingInterval)
advanceClock(atom.views.documentPollingInterval)
nextAnimationFrame()
expect(component.lineNodeForScreenRow(0).textContent).toBe "var quicksort = "
@ -2558,7 +2560,7 @@ describe "TextEditorComponent", ->
expect(wrapperNode.classList.contains('mini')).toBe true
it "does not have an opaque background on lines", ->
expect(component.refs.lines.getDOMNode().getAttribute('style')).not.toContain 'background-color'
expect(component.linesComponent.domNode.getAttribute('style')).not.toContain 'background-color'
it "does not render invisible characters", ->
atom.config.set('editor.invisibles', eol: 'E')

View File

@ -46,12 +46,12 @@ describe "TextEditorElement", ->
jasmine.attachToDOM(element)
component = element.component
expect(component.isMounted()).toBe true
expect(component.mounted).toBe true
element.remove()
expect(component.isMounted()).toBe false
expect(component.mounted).toBe false
jasmine.attachToDOM(element)
expect(element.component.isMounted()).toBe true
expect(element.component.mounted).toBe true
describe "when the editor.useShadowDOM config option is false", ->
it "mounts the react component and unmounts when removed from the dom", ->
@ -61,9 +61,9 @@ describe "TextEditorElement", ->
jasmine.attachToDOM(element)
component = element.component
expect(component.isMounted()).toBe true
expect(component.mounted).toBe true
element.getModel().destroy()
expect(component.isMounted()).toBe false
expect(component.mounted).toBe false
describe "focus and blur handling", ->
describe "when the editor.useShadowDOM config option is true", ->

View File

@ -323,6 +323,69 @@ describe "TextEditorPresenter", ->
expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", false)
expect(presenter.state.verticalScrollbar.scrollTop).toBe presenter.contentHeight - presenter.clientHeight
describe ".hiddenInput", ->
describe ".top/.left", ->
it "is positioned over the last cursor it is in view and the editor is focused", ->
editor.setCursorBufferPosition([3, 6])
presenter = buildPresenter(focused: false, explicitHeight: 50, contentFrameWidth: 300, horizontalScrollbarHeight: 0, verticalScrollbarWidth: 0)
expectValues presenter.state.hiddenInput, {top: 0, left: 0}
expectStateUpdate presenter, -> presenter.setFocused(true)
expectValues presenter.state.hiddenInput, {top: 3 * 10, left: 6 * 10}
expectStateUpdate presenter, -> presenter.setScrollTop(15)
expectValues presenter.state.hiddenInput, {top: (3 * 10) - 15, left: 6 * 10}
expectStateUpdate presenter, -> presenter.setScrollLeft(35)
expectValues presenter.state.hiddenInput, {top: (3 * 10) - 15, left: (6 * 10) - 35}
expectStateUpdate presenter, -> presenter.setScrollTop(40)
expectValues presenter.state.hiddenInput, {top: 0, left: (6 * 10) - 35}
expectStateUpdate presenter, -> presenter.setScrollLeft(70)
expectValues presenter.state.hiddenInput, {top: 0, left: 0}
expectStateUpdate presenter, -> editor.setCursorBufferPosition([11, 43])
expectValues presenter.state.hiddenInput, {top: 50 - 10, left: 300 - 10}
newCursor = null
expectStateUpdate presenter, -> newCursor = editor.addCursorAtBufferPosition([6, 10])
expectValues presenter.state.hiddenInput, {top: (6 * 10) - 40, left: (10 * 10) - 70}
expectStateUpdate presenter, -> newCursor.destroy()
expectValues presenter.state.hiddenInput, {top: 50 - 10, left: 300 - 10}
expectStateUpdate presenter, -> presenter.setFocused(false)
expectValues presenter.state.hiddenInput, {top: 0, left: 0}
describe ".height", ->
it "is assigned based on the line height", ->
presenter = buildPresenter()
expect(presenter.state.hiddenInput.height).toBe 10
expectStateUpdate presenter, -> presenter.setLineHeight(20)
expect(presenter.state.hiddenInput.height).toBe 20
describe ".width", ->
it "is assigned based on the width of the character following the cursor", ->
waitsForPromise -> atom.packages.activatePackage('language-javascript')
runs ->
editor.setCursorBufferPosition([3, 6])
presenter = buildPresenter()
expect(presenter.state.hiddenInput.width).toBe 10
expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(15)
expect(presenter.state.hiddenInput.width).toBe 15
expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'r', 20)
expect(presenter.state.hiddenInput.width).toBe 20
it "is 2px at the end of lines", ->
presenter = buildPresenter()
editor.setCursorBufferPosition([3, Infinity])
expect(presenter.state.hiddenInput.width).toBe 2
describe ".content", ->
describe ".scrollingVertically", ->
it "is true for ::stoppedScrollingDelay milliseconds following a changes to ::scrollTop", ->
@ -1907,6 +1970,42 @@ describe "TextEditorPresenter", ->
editor.undo()
expect(lineNumberStateForScreenRow(presenter, 11).foldable).toBe false
describe ".visible", ->
it "is true iff the editor isn't mini, ::isGutterVisible is true on the editor, and 'editor.showLineNumbers' is enabled in config", ->
presenter = buildPresenter()
expect(editor.isGutterVisible()).toBe true
expect(presenter.state.gutter.visible).toBe true
expectStateUpdate presenter, -> editor.setMini(true)
expect(presenter.state.gutter.visible).toBe false
expectStateUpdate presenter, -> editor.setMini(false)
expect(presenter.state.gutter.visible).toBe true
expectStateUpdate presenter, -> editor.setGutterVisible(false)
expect(presenter.state.gutter.visible).toBe false
expectStateUpdate presenter, -> editor.setGutterVisible(true)
expect(presenter.state.gutter.visible).toBe true
expectStateUpdate presenter, -> atom.config.set('editor.showLineNumbers', false)
expect(presenter.state.gutter.visible).toBe false
it "updates when the editor's grammar changes", ->
presenter = buildPresenter()
atom.config.set('editor.showLineNumbers', false, scopeSelector: '.source.js')
expect(presenter.state.gutter.visible).toBe true
stateUpdated = false
presenter.onDidUpdateState -> stateUpdated = true
waitsForPromise -> atom.packages.activatePackage('language-javascript')
runs ->
expect(stateUpdated).toBe true
expect(presenter.state.gutter.visible).toBe false
describe ".height", ->
it "tracks the computed content height if ::autoHeight is true so the editor auto-expands vertically", ->
presenter = buildPresenter(explicitHeight: null, autoHeight: true)
@ -1924,6 +2023,15 @@ describe "TextEditorPresenter", ->
expectStateUpdate presenter, -> editor.getBuffer().append("\n\n\n")
expect(presenter.state.height).toBe editor.getScreenLineCount() * 20
describe ".focused", ->
it "tracks the value of ::focused", ->
presenter = buildPresenter(focused: false)
expect(presenter.state.focused).toBe false
expectStateUpdate presenter, -> presenter.setFocused(true)
expect(presenter.state.focused).toBe true
expectStateUpdate presenter, -> presenter.setFocused(false)
expect(presenter.state.focused).toBe false
# disabled until we fix an issue with display buffer markers not updating when
# they are moved on screen but not in the buffer
xdescribe "when the model and view measurements are mutated randomly", ->

View File

@ -85,3 +85,78 @@ describe "ViewRegistry", ->
expect(registry.getView(new TestModel) instanceof TestView).toBe true
disposable.dispose()
expect(-> registry.getView(new TestModel)).toThrow()
describe "::updateDocument(fn) and ::readDocument(fn)", ->
frameRequests = null
beforeEach ->
frameRequests = []
spyOn(window, 'requestAnimationFrame').andCallFake (fn) -> frameRequests.push(fn)
it "performs all pending writes before all pending reads on the next animation frame", ->
events = []
registry.updateDocument -> events.push('write 1')
registry.readDocument -> events.push('read 1')
registry.readDocument -> events.push('read 2')
registry.updateDocument -> events.push('write 2')
expect(events).toEqual []
expect(frameRequests.length).toBe 1
frameRequests[0]()
expect(events).toEqual ['write 1', 'write 2', 'read 1', 'read 2']
frameRequests = []
events = []
disposable = registry.updateDocument -> events.push('write 3')
registry.updateDocument -> events.push('write 4')
registry.readDocument -> events.push('read 3')
disposable.dispose()
expect(frameRequests.length).toBe 1
frameRequests[0]()
expect(events).toEqual ['write 4', 'read 3']
it "pauses DOM polling when reads or writes are pending", ->
spyOn(window, 'setInterval').andCallFake(fakeSetInterval)
spyOn(window, 'clearInterval').andCallFake(fakeClearInterval)
events = []
registry.pollDocument -> events.push('poll')
registry.updateDocument -> events.push('write')
registry.readDocument -> events.push('read')
advanceClock(registry.documentPollingInterval)
expect(events).toEqual []
frameRequests[0]()
expect(events).toEqual ['write', 'read', 'poll']
advanceClock(registry.documentPollingInterval)
expect(events).toEqual ['write', 'read', 'poll', 'poll']
describe "::pollDocument(fn)", ->
it "calls all registered reader functions on an interval until they are disabled via a returned disposable", ->
spyOn(window, 'setInterval').andCallFake(fakeSetInterval)
events = []
disposable1 = registry.pollDocument -> events.push('poll 1')
disposable2 = registry.pollDocument -> events.push('poll 2')
expect(events).toEqual []
advanceClock(registry.documentPollingInterval)
expect(events).toEqual ['poll 1', 'poll 2']
advanceClock(registry.documentPollingInterval)
expect(events).toEqual ['poll 1', 'poll 2', 'poll 1', 'poll 2']
disposable1.dispose()
advanceClock(registry.documentPollingInterval)
expect(events).toEqual ['poll 1', 'poll 2', 'poll 1', 'poll 2', 'poll 2']
disposable2.dispose()
advanceClock(registry.documentPollingInterval)
expect(events).toEqual ['poll 1', 'poll 2', 'poll 1', 'poll 2', 'poll 2']

View File

@ -162,10 +162,6 @@ module.exports =
default: 300
minimum: 0
description: 'Time interval in milliseconds within which operations will be grouped together in the undo history'
useHardwareAcceleration:
type: 'boolean'
default: true
description: 'Disabling will improve editor font rendering but reduce scrolling performance.'
useShadowDOM:
type: 'boolean'
default: true

View File

@ -1,14 +0,0 @@
React = require 'react-atom-fork'
{div} = require 'reactionary-atom-fork'
{isEqualForProperties} = require 'underscore-plus'
module.exports =
CursorComponent = React.createClass
displayName: 'CursorComponent'
render: ->
{pixelRect} = @props
{top, left, height, width} = pixelRect
WebkitTransform = "translate(#{left}px, #{top}px)"
div className: 'cursor', style: {height, width, WebkitTransform}

View File

@ -1,19 +1,54 @@
React = require 'react-atom-fork'
{div} = require 'reactionary-atom-fork'
{debounce, toArray, isEqualForProperties, isEqual} = require 'underscore-plus'
SubscriberMixin = require './subscriber-mixin'
CursorComponent = require './cursor-component'
module.exports =
CursorsComponent = React.createClass
displayName: 'CursorsComponent'
class CursorsComponent
oldState: null
render: ->
{presenter} = @props
constructor: (@presenter) ->
@cursorNodesById = {}
@domNode = document.createElement('div')
@domNode.classList.add('cursors')
@updateSync()
className = 'cursors'
className += ' blink-off' if presenter.state.content.blinkCursorsOff
updateSync: ->
newState = @presenter.state.content
@oldState ?= {cursors: {}}
div {className},
for key, pixelRect of presenter.state.content.cursors
CursorComponent({key, pixelRect})
# update blink class
if newState.blinkCursorsOff isnt @oldState.blinkCursorsOff
if newState.blinkCursorsOff
@domNode.classList.add 'blink-off'
else
@domNode.classList.remove 'blink-off'
@oldState.blinkCursorsOff = newState.blinkCursorsOff
# remove cursors
for id of @oldState.cursors
unless newState.cursors[id]?
@cursorNodesById[id].remove()
delete @cursorNodesById[id]
delete @oldState.cursors[id]
# add or update cursors
for id, cursorState of newState.cursors
unless @oldState.cursors[id]?
cursorNode = document.createElement('div')
cursorNode.classList.add('cursor')
@cursorNodesById[id] = cursorNode
@domNode.appendChild(cursorNode)
@updateCursorNode(id, cursorState)
updateCursorNode: (id, newCursorState) ->
cursorNode = @cursorNodesById[id]
oldCursorState = (@oldState.cursors[id] ?= {})
if newCursorState.top isnt oldCursorState.top or newCursorState.left isnt oldCursorState.left
cursorNode.style['-webkit-transform'] = "translate(#{newCursorState.left}px, #{newCursorState.top}px)"
oldCursorState.top = newCursorState.top
oldCursorState.left = newCursorState.left
if newCursorState.height isnt oldCursorState.height
cursorNode.style.height = newCursorState.height + 'px'
oldCursorState.height = newCursorState.height
if newCursorState.width isnt oldCursorState.width
cursorNode.style.width = newCursorState.width + 'px'
oldCursorState.width = newCursorState.width

View File

@ -1,62 +1,47 @@
_ = require 'underscore-plus'
React = require 'react-atom-fork'
{div} = require 'reactionary-atom-fork'
{isEqual, isEqualForProperties, multiplyString, toArray} = _
Decoration = require './decoration'
SubscriberMixin = require './subscriber-mixin'
WrapperDiv = document.createElement('div')
module.exports =
GutterComponent = React.createClass
displayName: 'GutterComponent'
mixins: [SubscriberMixin]
maxLineNumberDigits: null
class GutterComponent
dummyLineNumberNode: null
measuredWidth: null
render: ->
{presenter} = @props
@newState = presenter.state.gutter
@oldState ?= {lineNumbers: {}}
{scrollHeight, backgroundColor} = @newState
div className: 'gutter',
div className: 'line-numbers', ref: 'lineNumbers', style:
height: scrollHeight
WebkitTransform: @getTransform()
backgroundColor: backgroundColor
getTransform: ->
{useHardwareAcceleration} = @props
{scrollTop} = @newState
if useHardwareAcceleration
"translate3d(0px, #{-scrollTop}px, 0px)"
else
"translate(0px, #{-scrollTop}px)"
componentWillMount: ->
constructor: ({@presenter, @onMouseDown, @editor}) ->
@lineNumberNodesById = {}
componentDidMount: ->
{@maxLineNumberDigits} = @newState
@appendDummyLineNumber()
@updateLineNumbers()
@domNode = document.createElement('div')
@domNode.classList.add('gutter')
@lineNumbersNode = document.createElement('div')
@lineNumbersNode.classList.add('line-numbers')
@domNode.appendChild(@lineNumbersNode)
node = @getDOMNode()
node.addEventListener 'click', @onClick
node.addEventListener 'mousedown', @onMouseDown
@domNode.addEventListener 'click', @onClick
@domNode.addEventListener 'mousedown', @onMouseDown
componentDidUpdate: (oldProps) ->
{maxLineNumberDigits} = @newState
unless maxLineNumberDigits is @maxLineNumberDigits
@maxLineNumberDigits = maxLineNumberDigits
@updateSync()
updateSync: ->
@newState = @presenter.state.gutter
@oldState ?= {lineNumbers: {}}
@appendDummyLineNumber() unless @dummyLineNumberNode?
if @newState.scrollHeight isnt @oldState.scrollHeight
@lineNumbersNode.style.height = @newState.scrollHeight + 'px'
@oldState.scrollHeight = @newState.scrollHeight
if @newState.scrollTop isnt @oldState.scrollTop
@lineNumbersNode.style['-webkit-transform'] = "translate3d(0px, #{-@newState.scrollTop}px, 0px)"
@oldState.scrollTop = @newState.scrollTop
if @newState.backgroundColor isnt @oldState.backgroundColor
@lineNumbersNode.style.backgroundColor = @newState.backgroundColor
@oldState.backgroundColor = @newState.backgroundColor
if @newState.maxLineNumberDigits isnt @oldState.maxLineNumberDigits
@updateDummyLineNumber()
node.remove() for id, node of @lineNumberNodesById
@oldState = {lineNumbers: {}}
@oldState = {maxLineNumberDigits: @newState.maxLineNumberDigits, lineNumbers: {}}
@lineNumberNodesById = {}
@updateLineNumbers()
@ -66,7 +51,7 @@ GutterComponent = React.createClass
appendDummyLineNumber: ->
WrapperDiv.innerHTML = @buildLineNumberHTML({bufferRow: -1})
@dummyLineNumberNode = WrapperDiv.children[0]
@refs.lineNumbers.getDOMNode().appendChild(@dummyLineNumberNode)
@lineNumbersNode.appendChild(@dummyLineNumberNode)
updateDummyLineNumber: ->
@dummyLineNumberNode.innerHTML = @buildLineNumberInnerHTML(0, false)
@ -87,9 +72,9 @@ GutterComponent = React.createClass
if newLineNumberIds?
WrapperDiv.innerHTML = newLineNumbersHTML
newLineNumberNodes = toArray(WrapperDiv.children)
newLineNumberNodes = _.toArray(WrapperDiv.children)
node = @refs.lineNumbers.getDOMNode()
node = @lineNumbersNode
for id, i in newLineNumberIds
lineNumberNode = newLineNumberNodes[i]
@lineNumberNodesById[id] = lineNumberNode
@ -120,7 +105,7 @@ GutterComponent = React.createClass
else
lineNumber = (bufferRow + 1).toString()
padding = multiplyString(' ', maxLineNumberDigits - lineNumber.length)
padding = _.multiplyString(' ', maxLineNumberDigits - lineNumber.length)
iconHTML = '<div class="icon-right"></div>'
padding + lineNumber + iconHTML
@ -151,21 +136,20 @@ GutterComponent = React.createClass
return @lineNumberNodesById[id]
null
onMouseDown: (event) ->
onMouseDown: (event) =>
{target} = event
lineNumber = target.parentNode
unless target.classList.contains('icon-right') and lineNumber.classList.contains('foldable')
@props.onMouseDown(event)
@onMouseDown(event)
onClick: (event) ->
{editor} = @props
onClick: (event) =>
{target} = event
lineNumber = target.parentNode
if target.classList.contains('icon-right') and lineNumber.classList.contains('foldable')
bufferRow = parseInt(lineNumber.getAttribute('data-buffer-row'))
if lineNumber.classList.contains('folded')
editor.unfoldBufferRow(bufferRow)
@editor.unfoldBufferRow(bufferRow)
else
editor.foldBufferRow(bufferRow)
@editor.foldBufferRow(bufferRow)

View File

@ -1,50 +0,0 @@
React = require 'react-atom-fork'
{div} = require 'reactionary-atom-fork'
{isEqualForProperties} = require 'underscore-plus'
module.exports =
HighlightComponent = React.createClass
displayName: 'HighlightComponent'
currentFlashCount: 0
currentFlashClass: null
render: ->
{state} = @props
className = 'highlight'
className += " #{state.class}" if state.class?
div {className},
for region, i in state.regions
regionClassName = 'region'
regionClassName += " #{state.deprecatedRegionClass}" if state.deprecatedRegionClass?
div className: regionClassName, key: i, style: region
componentDidMount: ->
@flashIfRequested()
componentDidUpdate: ->
@flashIfRequested()
flashIfRequested: ->
if @props.state.flashCount > @currentFlashCount
@currentFlashCount = @props.state.flashCount
node = @getDOMNode()
{flashClass, flashDuration} = @props.state
addFlashClass = =>
node.classList.add(flashClass)
@currentFlashClass = flashClass
@flashTimeoutId = setTimeout(removeFlashClass, flashDuration)
removeFlashClass = =>
node.classList.remove(@currentFlashClass)
@currentFlashClass = null
clearTimeout(@flashTimeoutId)
if @currentFlashClass?
removeFlashClass()
requestAnimationFrame(addFlashClass)
else
addFlashClass()

View File

@ -1,25 +1,107 @@
React = require 'react-atom-fork'
{div} = require 'reactionary-atom-fork'
{isEqualForProperties} = require 'underscore-plus'
HighlightComponent = require './highlight-component'
RegionStyleProperties = ['top', 'left', 'right', 'width', 'height']
module.exports =
HighlightsComponent = React.createClass
displayName: 'HighlightsComponent'
class HighlightsComponent
oldState: null
render: ->
div className: 'highlights',
@renderHighlights()
constructor: (@presenter) ->
@highlightNodesById = {}
@regionNodesByHighlightId = {}
renderHighlights: ->
{presenter} = @props
highlightComponents = []
for key, state of presenter.state.content.highlights
highlightComponents.push(HighlightComponent({key, state}))
highlightComponents
@domNode = document.createElement('div')
@domNode.classList.add('highlights')
componentDidMount: ->
if atom.config.get('editor.useShadowDOM')
insertionPoint = document.createElement('content')
insertionPoint.setAttribute('select', '.underlayer')
@getDOMNode().appendChild(insertionPoint)
@domNode.appendChild(insertionPoint)
updateSync: ->
newState = @presenter.state.content.highlights
@oldState ?= {}
# remove highlights
for id of @oldState
unless newState[id]?
@highlightNodesById[id].remove()
delete @highlightNodesById[id]
delete @regionNodesByHighlightId[id]
delete @oldState[id]
# add or update highlights
for id, highlightState of newState
unless @oldState[id]?
highlightNode = document.createElement('div')
highlightNode.classList.add('highlight')
@highlightNodesById[id] = highlightNode
@regionNodesByHighlightId[id] = {}
@domNode.appendChild(highlightNode)
@updateHighlightNode(id, highlightState)
updateHighlightNode: (id, newHighlightState) ->
highlightNode = @highlightNodesById[id]
oldHighlightState = (@oldState[id] ?= {regions: [], flashCount: 0})
# update class
if newHighlightState.class isnt oldHighlightState.class
highlightNode.classList.remove(oldHighlightState.class) if oldHighlightState.class?
highlightNode.classList.add(newHighlightState.class)
oldHighlightState.class = newHighlightState.class
@updateHighlightRegions(id, newHighlightState)
@flashHighlightNodeIfRequested(id, newHighlightState)
updateHighlightRegions: (id, newHighlightState) ->
oldHighlightState = @oldState[id]
highlightNode = @highlightNodesById[id]
# remove regions
while oldHighlightState.regions.length > newHighlightState.regions.length
oldHighlightState.regions.pop()
@regionNodesByHighlightId[id][oldHighlightState.regions.length].remove()
delete @regionNodesByHighlightId[id][oldHighlightState.regions.length]
# add or update regions
for newRegionState, i in newHighlightState.regions
unless oldHighlightState.regions[i]?
oldHighlightState.regions[i] = {}
regionNode = document.createElement('div')
regionNode.classList.add('region')
regionNode.classList.add(newHighlightState.deprecatedRegionClass) if newHighlightState.deprecatedRegionClass?
@regionNodesByHighlightId[id][i] = regionNode
highlightNode.appendChild(regionNode)
oldRegionState = oldHighlightState.regions[i]
regionNode = @regionNodesByHighlightId[id][i]
for property in RegionStyleProperties
if newRegionState[property] isnt oldRegionState[property]
oldRegionState[property] = newRegionState[property]
if newRegionState[property]?
regionNode.style[property] = newRegionState[property] + 'px'
else
regionNode.style[property] = ''
flashHighlightNodeIfRequested: (id, newHighlightState) ->
oldHighlightState = @oldState[id]
return unless newHighlightState.flashCount > oldHighlightState.flashCount
highlightNode = @highlightNodesById[id]
addFlashClass = =>
highlightNode.classList.add(newHighlightState.flashClass)
oldHighlightState.flashClass = newHighlightState.flashClass
@flashTimeoutId = setTimeout(removeFlashClass, newHighlightState.flashDuration)
removeFlashClass = =>
highlightNode.classList.remove(oldHighlightState.flashClass)
oldHighlightState.flashClass = null
clearTimeout(@flashTimeoutId)
if oldHighlightState.flashClass?
removeFlashClass()
requestAnimationFrame(addFlashClass)
else
addFlashClass()
oldHighlightState.flashCount = newHighlightState.flashCount

View File

@ -1,39 +1,29 @@
{last, isEqual} = require 'underscore-plus'
React = require 'react-atom-fork'
{input} = require 'reactionary-atom-fork'
module.exports =
InputComponent = React.createClass
displayName: 'InputComponent'
class InputComponent
constructor: (@presenter) ->
@domNode = document.createElement('input')
@domNode.classList.add('hidden-input')
@domNode.setAttribute('data-react-skip-selection-restoration', true)
@domNode.style['-webkit-transform'] = 'translateZ(0)'
@domNode.addEventListener 'paste', (event) => event.preventDefault()
@updateSync()
render: ->
{className, style} = @props
updateSync: ->
@oldState ?= {}
newState = @presenter.state.hiddenInput
input {className, style, 'data-react-skip-selection-restoration': true}
if newState.top isnt @oldState.top
@domNode.style.top = newState.top + 'px'
@oldState.top = newState.top
getInitialState: ->
{lastChar: ''}
if newState.left isnt @oldState.left
@domNode.style.left = newState.left + 'px'
@oldState.left = newState.left
componentDidMount: ->
node = @getDOMNode()
node.addEventListener 'paste', @onPaste
node.addEventListener 'compositionupdate', @onCompositionUpdate
if newState.width isnt @oldState.width
@domNode.style.width = newState.width + 'px'
@oldState.width = newState.width
# Don't let text accumulate in the input forever, but avoid excessive reflows
componentDidUpdate: ->
if @lastValueLength > 500 and not @isPressAndHoldCharacter(@state.lastChar)
@getDOMNode().value = ''
@lastValueLength = 0
# This should actually consult the property lists in /System/Library/Input Methods/PressAndHold.app
isPressAndHoldCharacter: (char) ->
@state.lastChar.match /[aeiouAEIOU]/
shouldComponentUpdate: (newProps) ->
not isEqual(newProps.style, @props.style)
onPaste: (e) ->
e.preventDefault()
focus: ->
@getDOMNode().focus()
if newState.height isnt @oldState.height
@domNode.style.height = newState.height + 'px'
@oldState.height = newState.height

View File

@ -1,7 +1,5 @@
_ = require 'underscore-plus'
React = require 'react-atom-fork'
{div, span} = require 'reactionary-atom-fork'
{debounce, isEqual, isEqualForProperties, multiplyString, toArray} = require 'underscore-plus'
{toArray} = require 'underscore-plus'
{$$} = require 'space-pen'
CursorsComponent = require './cursors-component'
@ -12,73 +10,85 @@ DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibi
AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT}
WrapperDiv = document.createElement('div')
cloneObject = (object) ->
clone = {}
clone[key] = value for key, value of object
clone
module.exports =
LinesComponent = React.createClass
displayName: 'LinesComponent'
class LinesComponent
placeholderTextDiv: null
render: ->
{editor, presenter} = @props
@oldState ?= {lines: {}}
@newState = presenter.state.content
{scrollHeight, scrollWidth, backgroundColor, placeholderText} = @newState
style =
height: scrollHeight
width: scrollWidth
WebkitTransform: @getTransform()
backgroundColor: backgroundColor
div {className: 'lines', style},
div className: 'placeholder-text', placeholderText if placeholderText?
CursorsComponent {presenter}
HighlightsComponent {presenter}
getTransform: ->
{scrollTop, scrollLeft} = @newState
{useHardwareAcceleration} = @props
if useHardwareAcceleration
"translate3d(#{-scrollLeft}px, #{-scrollTop}px, 0px)"
else
"translate(#{-scrollLeft}px, #{-scrollTop}px)"
componentWillMount: ->
constructor: ({@presenter, @hostElement, @useShadowDOM, visible}) ->
@measuredLines = new Set
@lineNodesByLineId = {}
@screenRowsByLineId = {}
@lineIdsByScreenRow = {}
@renderedDecorationsByLineId = {}
componentDidMount: ->
if @props.useShadowDOM
@domNode = document.createElement('div')
@domNode.classList.add('lines')
@cursorsComponent = new CursorsComponent(@presenter)
@domNode.appendChild(@cursorsComponent.domNode)
@highlightsComponent = new HighlightsComponent(@presenter)
@domNode.appendChild(@highlightsComponent.domNode)
if @useShadowDOM
insertionPoint = document.createElement('content')
insertionPoint.setAttribute('select', '.overlayer')
@getDOMNode().appendChild(insertionPoint)
@domNode.appendChild(insertionPoint)
insertionPoint = document.createElement('content')
insertionPoint.setAttribute('select', 'atom-overlay')
@overlayManager = new OverlayManager(@props.hostElement)
@getDOMNode().appendChild(insertionPoint)
@overlayManager = new OverlayManager(@hostElement)
@domNode.appendChild(insertionPoint)
else
@overlayManager = new OverlayManager(@getDOMNode())
@overlayManager = new OverlayManager(@domNode)
componentDidUpdate: ->
{visible, presenter} = @props
@updateSync(visible)
updateSync: ->
@newState = @presenter.state.content
@oldState ?= {lines: {}}
if @newState.scrollHeight isnt @oldState.scrollHeight
@domNode.style.height = @newState.scrollHeight + 'px'
@oldState.scrollHeight = @newState.scrollHeight
if @newState.scrollTop isnt @oldState.scrollTop or @newState.scrollLeft isnt @oldState.scrollLeft
@domNode.style['-webkit-transform'] = "translate3d(#{-@newState.scrollLeft}px, #{-@newState.scrollTop}px, 0px)"
@oldState.scrollTop = @newState.scrollTop
@oldState.scrollLeft = @newState.scrollLeft
if @newState.backgroundColor isnt @oldState.backgroundColor
@domNode.style.backgroundColor = @newState.backgroundColor
@oldState.backgroundColor = @newState.backgroundColor
if @newState.placeholderText isnt @oldState.placeholderText
@placeholderTextDiv?.remove()
if @newState.placeholderText?
@placeholderTextDiv = document.createElement('div')
@placeholderTextDiv.classList.add('placeholder-text')
@placeholderTextDiv.textContent = @newState.placeholderText
@domNode.appendChild(@placeholderTextDiv)
@removeLineNodes() unless @oldState.indentGuidesVisible is @newState.indentGuidesVisible
@updateLineNodes()
@measureCharactersInNewLines() if visible and not @newState.scrollingVertically
@overlayManager?.render(@props)
if @newState.scrollWidth isnt @oldState.scrollWidth
@domNode.style.width = @newState.scrollWidth + 'px'
@oldState.scrollWidth = @newState.scrollWidth
@cursorsComponent.updateSync()
@highlightsComponent.updateSync()
@overlayManager?.render(@presenter)
@oldState.indentGuidesVisible = @newState.indentGuidesVisible
@oldState.scrollWidth = @newState.scrollWidth
clearScreenRowCaches: ->
@screenRowsByLineId = {}
@lineIdsByScreenRow = {}
removeLineNodes: ->
@removeLineNode(id) for id of @oldState.lines
@ -90,8 +100,6 @@ LinesComponent = React.createClass
delete @oldState.lines[id]
updateLineNodes: ->
{presenter} = @props
for id of @oldState.lines
unless @newState.lines.hasOwnProperty(id)
@removeLineNode(id)
@ -109,20 +117,18 @@ LinesComponent = React.createClass
newLinesHTML += @buildLineHTML(id)
@screenRowsByLineId[id] = lineState.screenRow
@lineIdsByScreenRow[lineState.screenRow] = id
@oldState.lines[id] = _.clone(lineState)
@oldState.lines[id] = cloneObject(lineState)
return unless newLineIds?
WrapperDiv.innerHTML = newLinesHTML
newLineNodes = toArray(WrapperDiv.children)
node = @getDOMNode()
newLineNodes = _.toArray(WrapperDiv.children)
for id, i in newLineIds
lineNode = newLineNodes[i]
@lineNodesByLineId[id] = lineNode
node.appendChild(lineNode)
@domNode.appendChild(lineNode)
buildLineHTML: (id) ->
{presenter} = @props
{scrollWidth} = @newState
{screenRow, tokens, text, top, lineEnding, fold, isSoftWrapped, indentLevel, decorationClasses} = @newState.lines[id]
@ -167,7 +173,6 @@ LinesComponent = React.createClass
@buildEndOfLineHTML(id) or '&nbsp;'
buildLineInnerHTML: (id) ->
{editor} = @props
{indentGuidesVisible} = @newState
{tokens, text, isOnlyWhitespace} = @newState.lines[id]
innerHTML = ""
@ -217,13 +222,16 @@ LinesComponent = React.createClass
"<span class=\"#{scope.replace(/\.+/g, ' ')}\">"
updateLineNode: (id) ->
{scrollWidth} = @newState
{screenRow, top} = @newState.lines[id]
oldLineState = @oldState.lines[id]
newLineState = @newState.lines[id]
lineNode = @lineNodesByLineId[id]
newDecorationClasses = @newState.lines[id].decorationClasses
oldDecorationClasses = @oldState.lines[id].decorationClasses
if @newState.scrollWidth isnt @oldState.scrollWidth
lineNode.style.width = @newState.scrollWidth + 'px'
newDecorationClasses = newLineState.decorationClasses
oldDecorationClasses = oldLineState.decorationClasses
if oldDecorationClasses?
for decorationClass in oldDecorationClasses
@ -235,36 +243,37 @@ LinesComponent = React.createClass
unless oldDecorationClasses? and decorationClass in oldDecorationClasses
lineNode.classList.add(decorationClass)
lineNode.style.width = scrollWidth + 'px'
lineNode.style.top = top + 'px'
lineNode.dataset.screenRow = screenRow
@screenRowsByLineId[id] = screenRow
@lineIdsByScreenRow[screenRow] = id
oldLineState.decorationClasses = newLineState.decorationClasses
if newLineState.top isnt oldLineState.top
lineNode.style.top = newLineState.top + 'px'
oldLineState.top = newLineState.cop
if newLineState.screenRow isnt oldLineState.screenRow
lineNode.dataset.screenRow = newLineState.screenRow
oldLineState.screenRow = newLineState.screenRow
@lineIdsByScreenRow[newLineState.screenRow] = id
lineNodeForScreenRow: (screenRow) ->
@lineNodesByLineId[@lineIdsByScreenRow[screenRow]]
measureLineHeightAndDefaultCharWidth: ->
node = @getDOMNode()
node.appendChild(DummyLineNode)
@domNode.appendChild(DummyLineNode)
lineHeightInPixels = DummyLineNode.getBoundingClientRect().height
charWidth = DummyLineNode.firstChild.getBoundingClientRect().width
node.removeChild(DummyLineNode)
@domNode.removeChild(DummyLineNode)
{editor, presenter} = @props
presenter.setLineHeight(lineHeightInPixels)
presenter.setBaseCharacterWidth(charWidth)
@presenter.setLineHeight(lineHeightInPixels)
@presenter.setBaseCharacterWidth(charWidth)
remeasureCharacterWidths: ->
return unless @props.presenter.baseCharacterWidth
return unless @presenter.baseCharacterWidth
@clearScopedCharWidths()
@measureCharactersInNewLines()
measureCharactersInNewLines: ->
{presenter} = @props
presenter.batchCharacterMeasurement =>
@presenter.batchCharacterMeasurement =>
for id, lineState of @oldState.lines
unless @measuredLines.has(id)
lineNode = @lineNodesByLineId[id]
@ -272,13 +281,12 @@ LinesComponent = React.createClass
return
measureCharactersInLine: (tokenizedLine, lineNode) ->
{editor} = @props
rangeForMeasurement = null
iterator = null
charIndex = 0
for {value, scopes, hasPairedCharacter} in tokenizedLine.tokens
charWidths = editor.getScopedCharWidths(scopes)
charWidths = @presenter.getScopedCharacterWidths(scopes)
valueIndex = 0
while valueIndex < value.length
@ -310,7 +318,7 @@ LinesComponent = React.createClass
rangeForMeasurement.setStart(textNode, i)
rangeForMeasurement.setEnd(textNode, i + charLength)
charWidth = rangeForMeasurement.getBoundingClientRect().width
@props.presenter.setScopedCharacterWidth(scopes, char, charWidth)
@presenter.setScopedCharacterWidth(scopes, char, charWidth)
charIndex += charLength
@ -318,5 +326,4 @@ LinesComponent = React.createClass
clearScopedCharWidths: ->
@measuredLines.clear()
@props.editor.clearScopedCharWidths()
@props.presenter.clearScopedCharacterWidths()
@presenter.clearScopedCharacterWidths()

View File

@ -3,16 +3,14 @@ class OverlayManager
constructor: (@container) ->
@overlayNodesById = {}
render: (props) ->
{presenter} = props
render: (presenter) ->
for decorationId, {pixelPosition, item} of presenter.state.content.overlays
@renderOverlay(presenter, decorationId, item, pixelPosition)
for id, overlayNode of @overlayNodesById
unless presenter.state.content.overlays.hasOwnProperty(id)
overlayNode.remove()
delete @overlayNodesById[id]
overlayNode.remove()
return

View File

@ -1,69 +1,74 @@
React = require 'react-atom-fork'
{div} = require 'reactionary-atom-fork'
{extend, isEqualForProperties} = require 'underscore-plus'
module.exports =
ScrollbarComponent = React.createClass
displayName: 'ScrollbarComponent'
class ScrollbarComponent
constructor: ({@presenter, @orientation, @onScroll}) ->
@domNode = document.createElement('div')
@domNode.classList.add "#{@orientation}-scrollbar"
@domNode.style['-webkit-transform'] = 'translateZ(0)' # See atom/atom#3559
@domNode.style.left = 0 if @orientation is 'horizontal'
render: ->
{presenter, orientation, className, useHardwareAcceleration} = @props
@contentNode = document.createElement('div')
@contentNode.classList.add "scrollbar-content"
@domNode.appendChild(@contentNode)
switch orientation
@domNode.addEventListener 'scroll', @onScrollCallback
@updateSync()
updateSync: ->
@oldState ?= {}
switch @orientation
when 'vertical'
@newState = presenter.state.verticalScrollbar
@newState = @presenter.state.verticalScrollbar
@updateVertical()
when 'horizontal'
@newState = presenter.state.horizontalScrollbar
@newState = @presenter.state.horizontalScrollbar
@updateHorizontal()
style = {}
if @newState.visible isnt @oldState.visible
if @newState.visible
@domNode.style.display = ''
else
@domNode.style.display = 'none'
@oldState.visible = @newState.visible
style.display = 'none' unless @newState.visible
style.transform = 'translateZ(0)' if useHardwareAcceleration # See atom/atom#3559
switch orientation
updateVertical: ->
if @newState.width isnt @oldState.width
@domNode.style.width = @newState.width + 'px'
@oldState.width = @newState.width
if @newState.bottom isnt @oldState.bottom
@domNode.style.bottom = @newState.bottom + 'px'
@oldState.bottom = @newState.bottom
if @newState.scrollHeight isnt @oldState.scrollHeight
@contentNode.style.height = @newState.scrollHeight + 'px'
@oldState.scrollHeight = @newState.scrollHeight
if @newState.scrollTop isnt @oldState.scrollTop
@domNode.scrollTop = @newState.scrollTop
@oldState.scrollTop = @newState.scrollTop
updateHorizontal: ->
if @newState.height isnt @oldState.height
@domNode.style.height = @newState.height + 'px'
@oldState.height = @newState.height
if @newState.right isnt @oldState.right
@domNode.style.right = @newState.right + 'px'
@oldState.right = @newState.right
if @newState.scrollWidth isnt @oldState.scrollWidth
@contentNode.style.width = @newState.scrollWidth + 'px'
@oldState.scrollWidth = @newState.scrollWidth
if @newState.scrollLeft isnt @oldState.scrollLeft
@domNode.scrollLeft = @newState.scrollLeft
@oldState.scrollLeft = @newState.scrollLeft
onScrollCallback: =>
switch @orientation
when 'vertical'
style.width = @newState.width
style.bottom = @newState.bottom
@onScroll(@domNode.scrollTop)
when 'horizontal'
style.left = 0
style.right = @newState.right
style.height = @newState.height
div {className, style},
switch orientation
when 'vertical'
div className: 'scrollbar-content', style: {height: @newState.scrollHeight}
when 'horizontal'
div className: 'scrollbar-content', style: {width: @newState.scrollWidth}
componentDidMount: ->
{orientation} = @props
unless orientation is 'vertical' or orientation is 'horizontal'
throw new Error("Must specify an orientation property of 'vertical' or 'horizontal'")
@getDOMNode().addEventListener 'scroll', @onScroll
componentWillUnmount: ->
@getDOMNode().removeEventListener 'scroll', @onScroll
componentDidUpdate: ->
{orientation} = @props
node = @getDOMNode()
switch orientation
when 'vertical'
node.scrollTop = @newState.scrollTop
when 'horizontal'
node.scrollLeft = @newState.scrollLeft
onScroll: ->
{orientation, onScroll} = @props
node = @getDOMNode()
switch orientation
when 'vertical'
scrollTop = node.scrollTop
onScroll(scrollTop)
when 'horizontal'
scrollLeft = node.scrollLeft
onScroll(scrollLeft)
@onScroll(@domNode.scrollLeft)

View File

@ -1,25 +1,37 @@
React = require 'react-atom-fork'
{div} = require 'reactionary-atom-fork'
{isEqualForProperties} = require 'underscore-plus'
module.exports =
ScrollbarCornerComponent = React.createClass
displayName: 'ScrollbarCornerComponent'
class ScrollbarCornerComponent
constructor: (@presenter) ->
@domNode = document.createElement('div')
@domNode.classList.add('scrollbar-corner')
render: ->
{presenter, measuringScrollbars} = @props
@contentNode = document.createElement('div')
@domNode.appendChild(@contentNode)
visible = presenter.state.horizontalScrollbar.visible and presenter.state.verticalScrollbar.visible
width = presenter.state.verticalScrollbar.width
height = presenter.state.horizontalScrollbar.height
@updateSync()
if measuringScrollbars
height = 25
width = 25
updateSync: ->
@oldState ?= {}
@newState ?= {}
display = 'none' unless visible
newHorizontalState = @presenter.state.horizontalScrollbar
newVerticalState = @presenter.state.verticalScrollbar
@newState.visible = newHorizontalState.visible and newVerticalState.visible
@newState.height = newHorizontalState.height
@newState.width = newVerticalState.width
div className: 'scrollbar-corner', style: {display, width, height},
div style:
height: height + 1
width: width + 1
if @newState.visible isnt @oldState.visible
if @newState.visible
@domNode.style.display = ''
else
@domNode.style.display = 'none'
@oldState.visible = @newState.visible
if @newState.height isnt @oldState.height
@domNode.style.height = @newState.height + 'px'
@contentNode.style.height = @newState.height + 1 + 'px'
@oldState.height = @newState.height
if @newState.width isnt @oldState.width
@domNode.style.width = @newState.width + 'px'
@contentNode.style.width = @newState.width + 1 + 'px'
@oldState.width = @newState.width

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,5 @@
{Emitter} = require 'event-kit'
{View, $, callRemoveHooks} = require 'space-pen'
React = require 'react-atom-fork'
Path = require 'path'
{defaults} = require 'underscore-plus'
TextBuffer = require 'text-buffer'
@ -61,7 +60,7 @@ class TextEditorElement extends HTMLElement
attachedCallback: ->
@buildModel() unless @getModel()?
@mountComponent() unless @component?.isMounted()
@mountComponent() unless @component?
@component.checkForVisibilityChange()
if this is document.activeElement
@focused()
@ -105,7 +104,7 @@ class TextEditorElement extends HTMLElement
))
mountComponent: ->
@componentDescriptor ?= TextEditorComponent(
@component = new TextEditorComponent(
hostElement: this
rootElement: @rootElement
stylesElement: @stylesElement
@ -113,27 +112,28 @@ class TextEditorElement extends HTMLElement
lineOverdrawMargin: @lineOverdrawMargin
useShadowDOM: @useShadowDOM
)
@component = React.renderComponent(@componentDescriptor, @rootElement)
@rootElement.appendChild(@component.domNode)
if @useShadowDOM
@shadowRoot.addEventListener('blur', @shadowRootBlurred.bind(this), true)
else
inputNode = @component.refs.input.getDOMNode()
inputNode = @component.hiddenInputComponent.domNode
inputNode.addEventListener 'focus', @focused.bind(this)
inputNode.addEventListener 'blur', => @dispatchEvent(new FocusEvent('blur', bubbles: false))
unmountComponent: ->
return unless @component?.isMounted()
callRemoveHooks(this)
React.unmountComponentAtNode(@rootElement)
@component = null
if @component?
@component.destroy()
@component.domNode.remove()
@component = null
focused: ->
@component?.focused()
blurred: (event) ->
unless @useShadowDOM
if event.relatedTarget is @component?.refs.input?.getDOMNode()
if event.relatedTarget is @component.hiddenInputComponent.domNode
event.stopImmediatePropagation()
return

View File

@ -14,7 +14,7 @@ class TextEditorPresenter
{@model, @autoHeight, @explicitHeight, @contentFrameWidth, @scrollTop, @scrollLeft} = params
{horizontalScrollbarHeight, verticalScrollbarWidth} = params
{@lineHeight, @baseCharacterWidth, @lineOverdrawMargin, @backgroundColor, @gutterBackgroundColor} = params
{@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay} = params
{@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay, @focused} = params
@measuredHorizontalScrollbarHeight = horizontalScrollbarHeight
@measuredVerticalScrollbarWidth = verticalScrollbarWidth
@ -56,7 +56,7 @@ class TextEditorPresenter
@updateLinesState()
@updateGutterState()
@updateLineNumbersState()
@disposables.add @model.onDidChangeGrammar(@updateContentState.bind(this))
@disposables.add @model.onDidChangeGrammar(@didChangeGrammar.bind(this))
@disposables.add @model.onDidChangePlaceholderText(@updateContentState.bind(this))
@disposables.add @model.onDidChangeMini =>
@updateScrollbarDimensions()
@ -64,7 +64,10 @@ class TextEditorPresenter
@updateContentState()
@updateDecorations()
@updateLinesState()
@updateGutterState()
@updateLineNumbersState()
@disposables.add @model.onDidChangeGutterVisible =>
@updateGutterState()
@disposables.add @model.onDidAddDecoration(@didAddDecoration.bind(this))
@disposables.add @model.onDidAddCursor(@didAddCursor.bind(this))
@disposables.add @model.onDidChangeScrollTop(@setScrollTop.bind(this))
@ -73,19 +76,41 @@ class TextEditorPresenter
@observeCursor(cursor) for cursor in @model.getCursors()
observeConfig: ->
@scrollPastEnd = atom.config.get('editor.scrollPastEnd')
configParams = {scope: @model.getRootScopeDescriptor()}
@disposables.add atom.config.onDidChange 'editor.showIndentGuide', scope: @model.getRootScopeDescriptor(), @updateContentState.bind(this)
@disposables.add atom.config.onDidChange 'editor.scrollPastEnd', scope: @model.getRootScopeDescriptor(), ({newValue}) =>
@scrollPastEnd = atom.config.get('editor.scrollPastEnd', configParams)
@showLineNumbers = atom.config.get('editor.showLineNumbers', configParams)
@showIndentGuide = atom.config.get('editor.showIndentGuide', configParams)
if @configDisposables?
@configDisposables?.dispose()
@disposables.remove(@configDisposables)
@configDisposables = new CompositeDisposable
@disposables.add(@configDisposables)
@configDisposables.add atom.config.onDidChange 'editor.showIndentGuide', configParams, ({newValue}) =>
@showIndentGuide = newValue
@updateContentState()
@configDisposables.add atom.config.onDidChange 'editor.scrollPastEnd', configParams, ({newValue}) =>
@scrollPastEnd = newValue
@updateScrollHeight()
@updateVerticalScrollState()
@updateScrollbarsState()
@configDisposables.add atom.config.onDidChange 'editor.showLineNumbers', configParams, ({newValue}) =>
@showLineNumbers = newValue
@updateGutterState()
didChangeGrammar: ->
@observeConfig()
@updateContentState()
@updateGutterState()
buildState: ->
@state =
horizontalScrollbar: {}
verticalScrollbar: {}
hiddenInput: {}
content:
scrollingVertically: false
blinkCursorsOff: false
@ -102,10 +127,12 @@ class TextEditorPresenter
@updateStartRow()
@updateEndRow()
@updateFocusedState()
@updateHeightState()
@updateVerticalScrollState()
@updateHorizontalScrollState()
@updateScrollbarsState()
@updateHiddenInputState()
@updateContentState()
@updateDecorations()
@updateLinesState()
@ -114,6 +141,9 @@ class TextEditorPresenter
@updateGutterState()
@updateLineNumbersState()
updateFocusedState: ->
@state.focused = @focused
updateHeightState: ->
if @autoHeight
@state.height = @contentHeight
@ -153,10 +183,29 @@ class TextEditorPresenter
@emitter.emit 'did-update-state'
updateHiddenInputState: ->
return unless lastCursor = @model.getLastCursor()
{top, left, height, width} = @pixelRectForScreenRange(lastCursor.getScreenRange())
if @focused
top -= @scrollTop
left -= @scrollLeft
@state.hiddenInput.top = Math.max(Math.min(top, @clientHeight - height), 0)
@state.hiddenInput.left = Math.max(Math.min(left, @clientWidth - width), 0)
else
@state.hiddenInput.top = 0
@state.hiddenInput.left = 0
@state.hiddenInput.height = height
@state.hiddenInput.width = Math.max(width, 2)
@emitter.emit 'did-update-state'
updateContentState: ->
@state.content.scrollWidth = @scrollWidth
@state.content.scrollLeft = @scrollLeft
@state.content.indentGuidesVisible = not @model.isMini() and atom.config.get('editor.showIndentGuide', scope: @model.getRootScopeDescriptor())
@state.content.indentGuidesVisible = not @model.isMini() and @showIndentGuide
@state.content.backgroundColor = if @model.isMini() then null else @backgroundColor
@state.content.placeholderText = if @model.isEmpty() then @model.getPlaceholderText() else null
@emitter.emit 'did-update-state'
@ -244,6 +293,7 @@ class TextEditorPresenter
@emitter.emit "did-update-state"
updateGutterState: ->
@state.gutter.visible = not @model.isMini() and (@model.isGutterVisible() ? true) and @showLineNumbers
@state.gutter.maxLineNumberDigits = @model.getLineCount().toString().length
@state.gutter.backgroundColor = if @gutterBackgroundColor isnt "rgba(0, 0, 0, 0)"
@gutterBackgroundColor
@ -448,6 +498,12 @@ class TextEditorPresenter
getCursorBlinkResumeDelay: -> @cursorBlinkResumeDelay
setFocused: (focused) ->
unless @focused is focused
@focused = focused
@updateFocusedState()
@updateHiddenInputState()
setScrollTop: (scrollTop) ->
scrollTop = @constrainScrollTop(scrollTop)
@ -458,6 +514,7 @@ class TextEditorPresenter
@updateEndRow()
@didStartScrolling()
@updateVerticalScrollState()
@updateHiddenInputState()
@updateDecorations()
@updateLinesState()
@updateCursorsState()
@ -487,6 +544,7 @@ class TextEditorPresenter
@scrollLeft = scrollLeft
@model.setScrollLeft(scrollLeft)
@updateHorizontalScrollState()
@updateHiddenInputState()
@updateCursorsState() unless oldScrollLeft?
setHorizontalScrollbarHeight: (horizontalScrollbarHeight) ->
@ -576,6 +634,7 @@ class TextEditorPresenter
@updateHorizontalScrollState()
@updateVerticalScrollState()
@updateScrollbarsState()
@updateHiddenInputState()
@updateDecorations()
@updateLinesState()
@updateCursorsState()
@ -623,6 +682,7 @@ class TextEditorPresenter
@updateHorizontalScrollState()
@updateVerticalScrollState()
@updateScrollbarsState()
@updateHiddenInputState()
@updateContentState()
@updateDecorations()
@updateLinesState()
@ -631,6 +691,7 @@ class TextEditorPresenter
clearScopedCharacterWidths: ->
@characterWidthsByScope = {}
@model.clearScopedCharWidths()
hasPixelPositionRequirements: ->
@lineHeight? and @baseCharacterWidth?
@ -885,6 +946,7 @@ class TextEditorPresenter
observeCursor: (cursor) ->
didChangePositionDisposable = cursor.onDidChangePosition =>
@updateHiddenInputState() if cursor.isLastCursor()
@pauseCursorBlinking()
@updateCursorsState()
@ -894,6 +956,7 @@ class TextEditorPresenter
@disposables.remove(didChangePositionDisposable)
@disposables.remove(didChangeVisibilityDisposable)
@disposables.remove(didDestroyDisposable)
@updateHiddenInputState()
@updateCursorsState()
@disposables.add(didChangePositionDisposable)
@ -902,6 +965,7 @@ class TextEditorPresenter
didAddCursor: (cursor) ->
@observeCursor(cursor)
@updateHiddenInputState()
@pauseCursorBlinking()
@updateCursorsState()

View File

@ -126,7 +126,7 @@ class TextEditorView extends View
Object.defineProperty @::, 'firstRenderedScreenRow', get: -> @component.getRenderedRowRange()[0]
Object.defineProperty @::, 'lastRenderedScreenRow', get: -> @component.getRenderedRowRange()[1]
Object.defineProperty @::, 'active', get: -> @is(@getPaneView()?.activeView)
Object.defineProperty @::, 'isFocused', get: -> document.activeElement is @element or document.activeElement is @element.component?.refs.input.getDOMNode()
Object.defineProperty @::, 'isFocused', get: -> document.activeElement is @element or document.activeElement is @element.component?.hiddenInputComponent?.domNode
Object.defineProperty @::, 'mini', get: -> @model?.isMini()
Object.defineProperty @::, 'component', get: -> @element?.component

View File

@ -42,9 +42,17 @@ Grim = require 'grim'
# ```
module.exports =
class ViewRegistry
documentPollingInterval: 200
documentUpdateRequested: false
performDocumentPollAfterUpdate: false
pollIntervalHandle: null
constructor: ->
@views = new WeakMap
@providers = []
@documentWriters = []
@documentReaders = []
@documentPollers = []
# Essential: Add a provider that will be used to construct views in the
# workspace's view layer based on model objects in its model layer.
@ -150,3 +158,53 @@ class ViewRegistry
findProvider: (object) ->
find @providers, ({modelConstructor}) -> object instanceof modelConstructor
updateDocument: (fn) ->
@documentWriters.push(fn)
@requestDocumentUpdate()
new Disposable =>
@documentWriters = @documentWriters.filter (writer) -> writer isnt fn
readDocument: (fn) ->
@documentReaders.push(fn)
@requestDocumentUpdate()
new Disposable =>
@documentReaders = @documentReaders.filter (reader) -> reader isnt fn
pollDocument: (fn) ->
@startPollingDocument() if @documentPollers.length is 0
@documentPollers.push(fn)
new Disposable =>
@documentPollers = @documentPollers.filter (poller) -> poller isnt fn
@stopPollingDocument() if @documentPollers.length is 0
clearDocumentRequests: ->
@documentReaders = []
@documentWriters = []
@documentPollers = []
@documentUpdateRequested = false
requestDocumentUpdate: ->
unless @documentUpdateRequested
@documentUpdateRequested = true
requestAnimationFrame(@performDocumentUpdate)
performDocumentUpdate: =>
@documentUpdateRequested = false
writer() while writer = @documentWriters.shift()
reader() while reader = @documentReaders.shift()
@performDocumentPoll() if @performDocumentPollAfterUpdate
startPollingDocument: ->
@pollIntervalHandle = window.setInterval(@performDocumentPoll, @documentPollingInterval)
stopPollingDocument: ->
window.clearInterval(@pollIntervalHandle)
performDocumentPoll: =>
if @documentUpdateRequested
@performDocumentPollAfterUpdate = true
else
@performDocumentPollAfterUpdate = false
poller() for poller in @documentPollers
return