mirror of
https://github.com/atom/atom.git
synced 2026-04-28 03:01:47 -04:00
Fixes #5187. Giving up after 90 minutes on producing a failing test case because this is all changing soon anyway and getting simpler. I’m willing to risk this regressing to solve it now and move on.
1091 lines
39 KiB
CoffeeScript
1091 lines
39 KiB
CoffeeScript
_ = require 'underscore-plus'
|
|
React = require 'react-atom-fork'
|
|
{div, span} = require 'reactionary-atom-fork'
|
|
{debounce, defaults, isEqualForProperties} = require 'underscore-plus'
|
|
scrollbarStyle = require 'scrollbar-style'
|
|
{Range, Point} = require 'text-buffer'
|
|
grim = require 'grim'
|
|
{CompositeDisposable} = require 'event-kit'
|
|
ipc = require 'ipc'
|
|
|
|
GutterComponent = require './gutter-component'
|
|
InputComponent = require './input-component'
|
|
LinesComponent = require './lines-component'
|
|
ScrollbarComponent = require './scrollbar-component'
|
|
ScrollbarCornerComponent = require './scrollbar-corner-component'
|
|
SubscriberMixin = require './subscriber-mixin'
|
|
|
|
module.exports =
|
|
TextEditorComponent = React.createClass
|
|
displayName: 'TextEditorComponent'
|
|
mixins: [SubscriberMixin]
|
|
|
|
visible: false
|
|
autoHeight: false
|
|
backgroundColor: null
|
|
gutterBackgroundColor: null
|
|
pendingScrollTop: null
|
|
pendingScrollLeft: null
|
|
selectOnMouseMove: false
|
|
updateRequested: false
|
|
updatesPaused: false
|
|
updateRequestedWhilePaused: false
|
|
cursorMoved: false
|
|
selectionChanged: false
|
|
scrollingVertically: false
|
|
mouseWheelScreenRow: null
|
|
mouseWheelScreenRowClearDelay: 150
|
|
scrollSensitivity: 0.4
|
|
heightAndWidthMeasurementRequested: false
|
|
inputEnabled: true
|
|
scopedCharacterWidthsChangeCount: null
|
|
domPollingInterval: 100
|
|
domPollingIntervalId: null
|
|
domPollingPaused: false
|
|
measureScrollbarsWhenShown: true
|
|
measureLineHeightAndDefaultCharWidthWhenShown: true
|
|
remeasureCharacterWidthsWhenShown: false
|
|
|
|
render: ->
|
|
{focused, showIndentGuide, showLineNumbers, visible} = @state
|
|
{editor, cursorBlinkPeriod, cursorBlinkResumeDelay, hostElement, useShadowDOM} = @props
|
|
maxLineNumberDigits = editor.getLineCount().toString().length
|
|
hasSelection = editor.getLastSelection()? and !editor.getLastSelection().isEmpty()
|
|
style = {}
|
|
|
|
@performedInitialMeasurement = false if editor.isDestroyed()
|
|
|
|
if @performedInitialMeasurement
|
|
renderedRowRange = @getRenderedRowRange()
|
|
[renderedStartRow, renderedEndRow] = renderedRowRange
|
|
cursorPixelRects = @getCursorPixelRects(renderedRowRange)
|
|
|
|
tokenizedLines = editor.tokenizedLinesForScreenRows(renderedStartRow, renderedEndRow - 1)
|
|
|
|
decorations = editor.decorationsForScreenRowRange(renderedStartRow, renderedEndRow)
|
|
highlightDecorations = @getHighlightDecorations(decorations)
|
|
overlayDecorations = @getOverlayDecorations(decorations)
|
|
lineDecorations = @getLineDecorations(decorations)
|
|
placeholderText = editor.getPlaceholderText() if editor.isEmpty()
|
|
visible = @isVisible()
|
|
|
|
scrollHeight = editor.getScrollHeight()
|
|
scrollWidth = editor.getScrollWidth()
|
|
scrollTop = editor.getScrollTop()
|
|
scrollLeft = editor.getScrollLeft()
|
|
lineHeightInPixels = editor.getLineHeightInPixels()
|
|
defaultCharWidth = editor.getDefaultCharWidth()
|
|
scrollViewHeight = editor.getHeight()
|
|
lineWidth = Math.max(scrollWidth, editor.getWidth())
|
|
horizontalScrollbarHeight = editor.getHorizontalScrollbarHeight()
|
|
verticalScrollbarWidth = editor.getVerticalScrollbarWidth()
|
|
verticallyScrollable = editor.verticallyScrollable()
|
|
horizontallyScrollable = editor.horizontallyScrollable()
|
|
hiddenInputStyle = @getHiddenInputPosition()
|
|
hiddenInputStyle.WebkitTransform = 'translateZ(0)' if @useHardwareAcceleration
|
|
if @mouseWheelScreenRow? and not (renderedStartRow <= @mouseWheelScreenRow < renderedEndRow)
|
|
mouseWheelScreenRow = @mouseWheelScreenRow
|
|
|
|
style.height = scrollViewHeight if @autoHeight
|
|
|
|
if useShadowDOM
|
|
className = 'editor-contents--private'
|
|
else
|
|
className = 'editor-contents'
|
|
className += ' is-focused' if focused
|
|
className += ' has-selection' if hasSelection
|
|
|
|
div {className, style},
|
|
if @gutterVisible
|
|
GutterComponent {
|
|
ref: 'gutter', onMouseDown: @onGutterMouseDown, lineDecorations,
|
|
defaultCharWidth, editor, renderedRowRange, maxLineNumberDigits, scrollViewHeight,
|
|
scrollTop, scrollHeight, lineHeightInPixels, @pendingChanges, mouseWheelScreenRow,
|
|
@useHardwareAcceleration, @performedInitialMeasurement, @backgroundColor, @gutterBackgroundColor
|
|
}
|
|
|
|
div ref: 'scrollView', className: 'scroll-view',
|
|
InputComponent
|
|
ref: 'input'
|
|
className: 'hidden-input'
|
|
style: hiddenInputStyle
|
|
|
|
LinesComponent {
|
|
ref: 'lines',
|
|
editor, lineHeightInPixels, defaultCharWidth, tokenizedLines,
|
|
lineDecorations, highlightDecorations, overlayDecorations, hostElement,
|
|
showIndentGuide, renderedRowRange, @pendingChanges, scrollTop, scrollLeft,
|
|
@scrollingVertically, scrollHeight, scrollWidth, mouseWheelScreenRow,
|
|
visible, scrollViewHeight, @scopedCharacterWidthsChangeCount, lineWidth, @useHardwareAcceleration,
|
|
placeholderText, @performedInitialMeasurement, @backgroundColor, cursorPixelRects,
|
|
cursorBlinkPeriod, cursorBlinkResumeDelay, useShadowDOM
|
|
}
|
|
|
|
ScrollbarComponent
|
|
ref: 'horizontalScrollbar'
|
|
className: 'horizontal-scrollbar'
|
|
orientation: 'horizontal'
|
|
onScroll: @onHorizontalScroll
|
|
scrollLeft: scrollLeft
|
|
scrollWidth: scrollWidth
|
|
visible: horizontallyScrollable
|
|
scrollableInOppositeDirection: verticallyScrollable
|
|
verticalScrollbarWidth: verticalScrollbarWidth
|
|
horizontalScrollbarHeight: horizontalScrollbarHeight
|
|
useHardwareAcceleration: @useHardwareAcceleration
|
|
|
|
ScrollbarComponent
|
|
ref: 'verticalScrollbar'
|
|
className: 'vertical-scrollbar'
|
|
orientation: 'vertical'
|
|
onScroll: @onVerticalScroll
|
|
scrollTop: scrollTop
|
|
scrollHeight: scrollHeight
|
|
visible: verticallyScrollable
|
|
scrollableInOppositeDirection: horizontallyScrollable
|
|
verticalScrollbarWidth: verticalScrollbarWidth
|
|
horizontalScrollbarHeight: horizontalScrollbarHeight
|
|
useHardwareAcceleration: @useHardwareAcceleration
|
|
|
|
# Also used to measure the height/width of scrollbars after the initial render
|
|
ScrollbarCornerComponent
|
|
ref: 'scrollbarCorner'
|
|
visible: horizontallyScrollable and verticallyScrollable
|
|
measuringScrollbars: @measuringScrollbars
|
|
height: horizontalScrollbarHeight
|
|
width: verticalScrollbarWidth
|
|
|
|
getPageRows: ->
|
|
{editor} = @props
|
|
Math.max(1, Math.ceil(editor.getHeight() / editor.getLineHeightInPixels()))
|
|
|
|
getInitialState: -> {}
|
|
|
|
getDefaultProps: ->
|
|
cursorBlinkPeriod: 800
|
|
cursorBlinkResumeDelay: 100
|
|
lineOverdrawMargin: 15
|
|
|
|
componentWillMount: ->
|
|
@pendingChanges = []
|
|
@props.editor.manageScrollPosition = true
|
|
@observeConfig()
|
|
@setScrollSensitivity(atom.config.get('editor.scrollSensitivity'))
|
|
|
|
componentDidMount: ->
|
|
{editor, stylesElement} = @props
|
|
|
|
@observeEditor()
|
|
@listenForDOMEvents()
|
|
|
|
@subscribe stylesElement.onDidAddStyleElement @onStylesheetsChanged
|
|
@subscribe stylesElement.onDidUpdateStyleElement @onStylesheetsChanged
|
|
@subscribe stylesElement.onDidRemoveStyleElement @onStylesheetsChanged
|
|
unless atom.themes.isInitialLoadComplete()
|
|
@subscribe atom.themes.onDidChangeActiveThemes @onAllThemesLoaded
|
|
@subscribe scrollbarStyle.changes, @refreshScrollbars
|
|
|
|
@domPollingIntervalId = setInterval(@pollDOM, @domPollingInterval)
|
|
@updateParentViewFocusedClassIfNeeded({})
|
|
@updateParentViewMiniClass()
|
|
@checkForVisibilityChange()
|
|
|
|
componentWillUnmount: ->
|
|
{editor, hostElement} = @props
|
|
|
|
@unsubscribe()
|
|
@scopedConfigSubscriptions.dispose()
|
|
window.removeEventListener 'resize', @requestHeightAndWidthMeasurement
|
|
clearInterval(@domPollingIntervalId)
|
|
@domPollingIntervalId = null
|
|
|
|
componentDidUpdate: (prevProps, prevState) ->
|
|
cursorMoved = @cursorMoved
|
|
selectionChanged = @selectionChanged
|
|
@pendingChanges.length = 0
|
|
@cursorMoved = false
|
|
@selectionChanged = false
|
|
|
|
if @props.editor.isAlive()
|
|
@updateParentViewFocusedClassIfNeeded(prevState)
|
|
@updateParentViewMiniClass()
|
|
@props.hostElement.__spacePenView.trigger 'cursor:moved' if cursorMoved
|
|
@props.hostElement.__spacePenView.trigger 'selection:changed' if selectionChanged
|
|
@props.hostElement.__spacePenView.trigger 'editor:display-updated'
|
|
|
|
becameVisible: ->
|
|
@updatesPaused = true
|
|
@sampleFontStyling()
|
|
@sampleBackgroundColors()
|
|
@measureHeightAndWidth()
|
|
@measureScrollbars() if @measureScrollbarsWhenShown
|
|
@measureLineHeightAndDefaultCharWidth() if @measureLineHeightAndDefaultCharWidthWhenShown
|
|
@remeasureCharacterWidths() if @remeasureCharacterWidthsWhenShown
|
|
@props.editor.setVisible(true)
|
|
@performedInitialMeasurement = true
|
|
@updatesPaused = false
|
|
@forceUpdate() if @canUpdate()
|
|
|
|
requestUpdate: ->
|
|
return unless @canUpdate()
|
|
|
|
if @updatesPaused
|
|
@updateRequestedWhilePaused = true
|
|
return
|
|
|
|
if @props.hostElement.isUpdatedSynchronously()
|
|
@forceUpdate()
|
|
else unless @updateRequested
|
|
@updateRequested = true
|
|
requestAnimationFrame =>
|
|
@updateRequested = false
|
|
@forceUpdate() if @canUpdate()
|
|
|
|
canUpdate: ->
|
|
@isMounted() and @props.editor.isAlive()
|
|
|
|
requestAnimationFrame: (fn) ->
|
|
@updatesPaused = true
|
|
@pauseDOMPolling()
|
|
requestAnimationFrame =>
|
|
fn()
|
|
@updatesPaused = false
|
|
if @updateRequestedWhilePaused and @canUpdate()
|
|
@updateRequestedWhilePaused = false
|
|
@forceUpdate()
|
|
|
|
getTopmostDOMNode: ->
|
|
@props.hostElement
|
|
|
|
getRenderedRowRange: ->
|
|
{editor, lineOverdrawMargin} = @props
|
|
[visibleStartRow, visibleEndRow] = editor.getVisibleRowRange()
|
|
renderedStartRow = Math.max(0, visibleStartRow - lineOverdrawMargin)
|
|
renderedEndRow = Math.min(editor.getScreenLineCount(), visibleEndRow + lineOverdrawMargin)
|
|
[renderedStartRow, renderedEndRow]
|
|
|
|
getHiddenInputPosition: ->
|
|
{editor} = @props
|
|
{focused} = @state
|
|
return {top: 0, left: 0} unless @isMounted() and focused and editor.getLastCursor()?
|
|
|
|
{top, left, height, width} = editor.getLastCursor().getPixelRect()
|
|
width = 2 if width is 0 # Prevent autoscroll at the end of longest line
|
|
top -= editor.getScrollTop()
|
|
left -= editor.getScrollLeft()
|
|
top = Math.max(0, Math.min(editor.getHeight() - height, top))
|
|
left = Math.max(0, Math.min(editor.getWidth() - width, left))
|
|
{top, left}
|
|
|
|
getCursorScreenRanges: (renderedRowRange) ->
|
|
{editor} = @props
|
|
[renderedStartRow, renderedEndRow] = renderedRowRange
|
|
|
|
cursorScreenRanges = {}
|
|
for selection in editor.getSelections() when selection.isEmpty()
|
|
{cursor} = selection
|
|
screenRange = cursor.getScreenRange()
|
|
if renderedStartRow <= screenRange.start.row < renderedEndRow
|
|
cursorScreenRanges[cursor.id] = screenRange
|
|
cursorScreenRanges
|
|
|
|
getCursorPixelRects: (renderedRowRange) ->
|
|
{editor} = @props
|
|
[renderedStartRow, renderedEndRow] = renderedRowRange
|
|
|
|
cursorPixelRects = {}
|
|
for selection in editor.getSelections() when selection.isEmpty()
|
|
{cursor} = selection
|
|
screenRange = cursor.getScreenRange()
|
|
if renderedStartRow <= screenRange.start.row < renderedEndRow
|
|
cursorPixelRects[cursor.id] = editor.pixelRectForScreenRange(screenRange)
|
|
cursorPixelRects
|
|
|
|
getLineDecorations: (decorationsByMarkerId) ->
|
|
{editor} = @props
|
|
return {} if editor.isMini()
|
|
|
|
decorationsByScreenRow = {}
|
|
for markerId, decorations of decorationsByMarkerId
|
|
marker = editor.getMarker(markerId)
|
|
screenRange = null
|
|
headScreenRow = null
|
|
if marker.isValid()
|
|
for decoration in decorations
|
|
if decoration.isType('line-number') or decoration.isType('line')
|
|
decorationParams = decoration.getProperties()
|
|
screenRange ?= marker.getScreenRange()
|
|
headScreenRow ?= marker.getHeadScreenPosition().row
|
|
startRow = screenRange.start.row
|
|
endRow = screenRange.end.row
|
|
endRow-- if not screenRange.isEmpty() and screenRange.end.column == 0
|
|
for screenRow in [startRow..endRow]
|
|
continue if decorationParams.onlyHead and screenRow isnt headScreenRow
|
|
if screenRange.isEmpty()
|
|
continue if decorationParams.onlyNonEmpty
|
|
else
|
|
continue if decorationParams.onlyEmpty
|
|
|
|
decorationsByScreenRow[screenRow] ?= {}
|
|
decorationsByScreenRow[screenRow][decoration.id] = decorationParams
|
|
|
|
decorationsByScreenRow
|
|
|
|
getHighlightDecorations: (decorationsByMarkerId) ->
|
|
{editor} = @props
|
|
filteredDecorations = {}
|
|
for markerId, decorations of decorationsByMarkerId
|
|
marker = editor.getMarker(markerId)
|
|
screenRange = marker.getScreenRange()
|
|
if marker.isValid() and not screenRange.isEmpty()
|
|
for decoration in decorations
|
|
if decoration.isType('highlight')
|
|
decorationParams = decoration.getProperties()
|
|
filteredDecorations[markerId] ?=
|
|
id: markerId
|
|
startPixelPosition: editor.pixelPositionForScreenPosition(screenRange.start, true)
|
|
endPixelPosition: editor.pixelPositionForScreenPosition(screenRange.end, true)
|
|
decorations: []
|
|
filteredDecorations[markerId].decorations.push decorationParams
|
|
filteredDecorations
|
|
|
|
getOverlayDecorations: (decorationsByMarkerId) ->
|
|
{editor} = @props
|
|
filteredDecorations = {}
|
|
for markerId, decorations of decorationsByMarkerId
|
|
marker = editor.getMarker(markerId)
|
|
headScreenPosition = marker.getHeadScreenPosition()
|
|
tailScreenPosition = marker.getTailScreenPosition()
|
|
if marker.isValid()
|
|
for decoration in decorations
|
|
if decoration.isType('overlay')
|
|
decorationParams = decoration.getProperties()
|
|
filteredDecorations[markerId] ?=
|
|
id: markerId
|
|
headPixelPosition: editor.pixelPositionForScreenPosition(headScreenPosition, true)
|
|
tailPixelPosition: editor.pixelPositionForScreenPosition(tailScreenPosition, true)
|
|
decorations: []
|
|
filteredDecorations[markerId].decorations.push decorationParams
|
|
filteredDecorations
|
|
|
|
observeEditor: ->
|
|
{editor} = @props
|
|
@subscribe editor.onDidChange(@onScreenLinesChanged)
|
|
@subscribe editor.onDidChangeGutterVisible(@updateGutterVisible)
|
|
@subscribe editor.onDidChangeMini(@setMini)
|
|
@subscribe editor.observeGrammar(@onGrammarChanged)
|
|
@subscribe editor.observeCursors(@onCursorAdded)
|
|
@subscribe editor.observeSelections(@onSelectionAdded)
|
|
@subscribe editor.observeDecorations(@onDecorationAdded)
|
|
@subscribe editor.onDidRemoveDecoration(@onDecorationRemoved)
|
|
@subscribe editor.onDidChangeCharacterWidths(@onCharacterWidthsChanged)
|
|
@subscribe editor.onDidChangePlaceholderText(@onPlaceholderTextChanged)
|
|
@subscribe editor.$scrollTop.changes, @onScrollTopChanged
|
|
@subscribe editor.$scrollLeft.changes, @requestUpdate
|
|
@subscribe editor.$verticalScrollbarWidth.changes, @requestUpdate
|
|
@subscribe editor.$horizontalScrollbarHeight.changes, @requestUpdate
|
|
@subscribe editor.$height.changes, @requestUpdate
|
|
@subscribe editor.$width.changes, @requestUpdate
|
|
@subscribe editor.$defaultCharWidth.changes, @requestUpdate
|
|
@subscribe editor.$lineHeightInPixels.changes, @requestUpdate
|
|
|
|
listenForDOMEvents: ->
|
|
node = @getDOMNode()
|
|
node.addEventListener 'mousewheel', @onMouseWheel
|
|
node.addEventListener 'textInput', @onTextInput
|
|
@refs.scrollView.getDOMNode().addEventListener 'mousedown', @onMouseDown
|
|
|
|
scrollViewNode = @refs.scrollView.getDOMNode()
|
|
scrollViewNode.addEventListener 'scroll', @onScrollViewScroll
|
|
window.addEventListener 'resize', @requestHeightAndWidthMeasurement
|
|
|
|
@listenForIMEEvents()
|
|
@trackSelectionClipboard() if process.platform is 'linux'
|
|
|
|
listenForIMEEvents: ->
|
|
node = @getDOMNode()
|
|
{editor} = @props
|
|
|
|
# The IME composition events work like this:
|
|
#
|
|
# User types 's', chromium pops up the completion helper
|
|
# 1. compositionstart fired
|
|
# 2. compositionupdate fired; event.data == 's'
|
|
# User hits arrow keys to move around in completion helper
|
|
# 3. compositionupdate fired; event.data == 's' for each arry key press
|
|
# User escape to cancel
|
|
# 4. compositionend fired
|
|
# OR User chooses a completion
|
|
# 4. compositionend fired
|
|
# 5. textInput fired; event.data == the completion string
|
|
|
|
selectedText = null
|
|
node.addEventListener 'compositionstart', ->
|
|
selectedText = editor.getSelectedText()
|
|
node.addEventListener 'compositionupdate', (event) ->
|
|
editor.insertText(event.data, select: true, undo: 'skip')
|
|
node.addEventListener 'compositionend', (event) ->
|
|
editor.insertText(selectedText, select: true, undo: 'skip')
|
|
event.target.value = ''
|
|
|
|
# Listen for selection changes and store the currently selected text
|
|
# in the selection clipboard. This is only applicable on Linux.
|
|
trackSelectionClipboard: ->
|
|
timeoutId = null
|
|
{editor} = @props
|
|
writeSelectedTextToSelectionClipboard = ->
|
|
return if editor.isDestroyed()
|
|
if selectedText = editor.getSelectedText()
|
|
# This uses ipc.send instead of clipboard.writeText because
|
|
# clipboard.writeText is a sync ipc call on Linux and that
|
|
# will slow down selections.
|
|
ipc.send('write-text-to-selection-clipboard', selectedText)
|
|
@subscribe editor.onDidChangeSelectionRange ->
|
|
clearTimeout(timeoutId)
|
|
timeoutId = setTimeout(writeSelectedTextToSelectionClipboard)
|
|
|
|
observeConfig: ->
|
|
@subscribe atom.config.observe 'editor.useHardwareAcceleration', @setUseHardwareAcceleration
|
|
@subscribe atom.config.onDidChange 'editor.fontSize', @sampleFontStyling
|
|
@subscribe atom.config.onDidChange 'editor.fontFamily', @sampleFontStyling
|
|
@subscribe atom.config.onDidChange 'editor.lineHeight', @sampleFontStyling
|
|
|
|
onGrammarChanged: ->
|
|
{editor} = @props
|
|
|
|
@scopedConfigSubscriptions?.dispose()
|
|
@scopedConfigSubscriptions = subscriptions = new CompositeDisposable
|
|
|
|
scopeDescriptor = editor.getRootScopeDescriptor()
|
|
|
|
subscriptions.add atom.config.observe 'editor.showIndentGuide', scope: scopeDescriptor, @setShowIndentGuide
|
|
subscriptions.add atom.config.observe 'editor.showLineNumbers', scope: scopeDescriptor, @updateGutterVisible
|
|
subscriptions.add atom.config.observe 'editor.scrollSensitivity', scope: scopeDescriptor, @setScrollSensitivity
|
|
|
|
focused: ->
|
|
if @isMounted()
|
|
@setState(focused: true)
|
|
@refs.input.focus()
|
|
|
|
blurred: ->
|
|
if @isMounted()
|
|
@setState(focused: false)
|
|
|
|
onTextInput: (event) ->
|
|
event.stopPropagation()
|
|
|
|
# If we prevent the insertion of a space character, then the browser
|
|
# interprets the spacebar keypress as a page-down command.
|
|
event.preventDefault() unless event.data is ' '
|
|
|
|
return unless @isInputEnabled()
|
|
|
|
{editor} = @props
|
|
inputNode = event.target
|
|
|
|
# Work around of the accented character suggestion feature in OS X.
|
|
# Text input fires before a character is inserted, and if the browser is
|
|
# replacing the previous un-accented character with an accented variant, it
|
|
# will select backward over it.
|
|
selectedLength = inputNode.selectionEnd - inputNode.selectionStart
|
|
editor.selectLeft() if selectedLength is 1
|
|
|
|
insertedRange = editor.transact atom.config.get('editor.undoGroupingInterval'), ->
|
|
editor.insertText(event.data)
|
|
inputNode.value = event.data if insertedRange
|
|
|
|
onVerticalScroll: (scrollTop) ->
|
|
{editor} = @props
|
|
|
|
return if @updateRequested or scrollTop is editor.getScrollTop()
|
|
|
|
animationFramePending = @pendingScrollTop?
|
|
@pendingScrollTop = scrollTop
|
|
unless animationFramePending
|
|
@requestAnimationFrame =>
|
|
pendingScrollTop = @pendingScrollTop
|
|
@pendingScrollTop = null
|
|
@props.editor.setScrollTop(pendingScrollTop)
|
|
|
|
onHorizontalScroll: (scrollLeft) ->
|
|
{editor} = @props
|
|
|
|
return if @updateRequested or scrollLeft is editor.getScrollLeft()
|
|
|
|
animationFramePending = @pendingScrollLeft?
|
|
@pendingScrollLeft = scrollLeft
|
|
unless animationFramePending
|
|
@requestAnimationFrame =>
|
|
@props.editor.setScrollLeft(@pendingScrollLeft)
|
|
@pendingScrollLeft = null
|
|
|
|
onMouseWheel: (event) ->
|
|
{editor} = @props
|
|
|
|
# Only scroll in one direction at a time
|
|
{wheelDeltaX, wheelDeltaY} = event
|
|
|
|
# Ctrl+MouseWheel adjusts font size.
|
|
if event.ctrlKey and atom.config.get('editor.zoomFontWhenCtrlScrolling')
|
|
if wheelDeltaY > 0
|
|
atom.workspace.increaseFontSize()
|
|
else if wheelDeltaY < 0
|
|
atom.workspace.decreaseFontSize()
|
|
event.preventDefault()
|
|
return
|
|
|
|
if Math.abs(wheelDeltaX) > Math.abs(wheelDeltaY)
|
|
# Scrolling horizontally
|
|
previousScrollLeft = editor.getScrollLeft()
|
|
editor.setScrollLeft(previousScrollLeft - Math.round(wheelDeltaX * @scrollSensitivity))
|
|
event.preventDefault() unless previousScrollLeft is editor.getScrollLeft()
|
|
else
|
|
# Scrolling vertically
|
|
@mouseWheelScreenRow = @screenRowForNode(event.target)
|
|
@clearMouseWheelScreenRowAfterDelay ?= debounce(@clearMouseWheelScreenRow, @mouseWheelScreenRowClearDelay)
|
|
@clearMouseWheelScreenRowAfterDelay()
|
|
previousScrollTop = editor.getScrollTop()
|
|
editor.setScrollTop(previousScrollTop - Math.round(wheelDeltaY * @scrollSensitivity))
|
|
event.preventDefault() unless previousScrollTop is editor.getScrollTop()
|
|
|
|
onScrollViewScroll: ->
|
|
if @isMounted()
|
|
console.warn "TextEditorScrollView scrolled when it shouldn't have."
|
|
scrollViewNode = @refs.scrollView.getDOMNode()
|
|
scrollViewNode.scrollTop = 0
|
|
scrollViewNode.scrollLeft = 0
|
|
|
|
onMouseDown: (event) ->
|
|
unless event.button is 0 or (event.button is 1 and process.platform is 'linux')
|
|
# Only handle mouse down events for left mouse button on all platforms
|
|
# and middle mouse button on Linux since it pastes the selection clipboard
|
|
return
|
|
|
|
return if event.target?.classList.contains('horizontal-scrollbar')
|
|
|
|
{editor} = @props
|
|
{detail, shiftKey, metaKey, ctrlKey} = event
|
|
|
|
# CTRL+click brings up the context menu on OSX, so don't handle those either
|
|
return if ctrlKey and process.platform is 'darwin'
|
|
|
|
# Prevent focusout event on hidden input if editor is already focused
|
|
event.preventDefault() if @state.focused
|
|
|
|
screenPosition = @screenPositionForMouseEvent(event)
|
|
|
|
if event.target?.classList.contains('fold-marker')
|
|
bufferRow = editor.bufferRowForScreenRow(screenPosition.row)
|
|
editor.unfoldBufferRow(bufferRow)
|
|
return
|
|
|
|
switch detail
|
|
when 1
|
|
if shiftKey
|
|
editor.selectToScreenPosition(screenPosition)
|
|
else if metaKey or (ctrlKey and process.platform isnt 'darwin')
|
|
editor.addCursorAtScreenPosition(screenPosition)
|
|
else
|
|
editor.setCursorScreenPosition(screenPosition)
|
|
when 2
|
|
editor.getLastSelection().selectWord()
|
|
when 3
|
|
editor.getLastSelection().selectLine()
|
|
|
|
@handleDragUntilMouseUp event, (screenPosition) ->
|
|
editor.selectToScreenPosition(screenPosition)
|
|
|
|
onGutterMouseDown: (event) ->
|
|
return unless event.button is 0 # only handle the left mouse button
|
|
|
|
{shiftKey, metaKey, ctrlKey} = event
|
|
|
|
if shiftKey
|
|
@onGutterShiftClick(event)
|
|
else if metaKey or (ctrlKey and process.platform isnt 'darwin')
|
|
@onGutterMetaClick(event)
|
|
else
|
|
@onGutterClick(event)
|
|
|
|
onGutterClick: (event) ->
|
|
{editor} = @props
|
|
clickedRow = @screenPositionForMouseEvent(event).row
|
|
|
|
editor.setSelectedScreenRange([[clickedRow, 0], [clickedRow + 1, 0]], preserveFolds: true)
|
|
|
|
@handleDragUntilMouseUp event, (screenPosition) ->
|
|
dragRow = screenPosition.row
|
|
if dragRow < clickedRow # dragging up
|
|
editor.setSelectedScreenRange([[dragRow, 0], [clickedRow + 1, 0]], preserveFolds: true)
|
|
else
|
|
editor.setSelectedScreenRange([[clickedRow, 0], [dragRow + 1, 0]], preserveFolds: true)
|
|
|
|
onGutterMetaClick: (event) ->
|
|
{editor} = @props
|
|
clickedRow = @screenPositionForMouseEvent(event).row
|
|
|
|
bufferRange = editor.bufferRangeForScreenRange([[clickedRow, 0], [clickedRow + 1, 0]])
|
|
rowSelection = editor.addSelectionForBufferRange(bufferRange, preserveFolds: true)
|
|
|
|
@handleDragUntilMouseUp event, (screenPosition) ->
|
|
dragRow = screenPosition.row
|
|
|
|
if dragRow < clickedRow # dragging up
|
|
rowSelection.setScreenRange([[dragRow, 0], [clickedRow + 1, 0]], preserveFolds: true)
|
|
else
|
|
rowSelection.setScreenRange([[clickedRow, 0], [dragRow + 1, 0]], preserveFolds: true)
|
|
|
|
# After updating the selected screen range, merge overlapping selections
|
|
editor.mergeIntersectingSelections(preserveFolds: true)
|
|
|
|
# The merge process will possibly destroy the current selection because
|
|
# it will be merged into another one. Therefore, we need to obtain a
|
|
# reference to the new selection that contains the originally selected row
|
|
rowSelection = _.find editor.getSelections(), (selection) ->
|
|
selection.intersectsBufferRange(bufferRange)
|
|
|
|
onGutterShiftClick: (event) ->
|
|
{editor} = @props
|
|
clickedRow = @screenPositionForMouseEvent(event).row
|
|
tailPosition = editor.getLastSelection().getTailScreenPosition()
|
|
|
|
if clickedRow < tailPosition.row
|
|
editor.selectToScreenPosition([clickedRow, 0])
|
|
else
|
|
editor.selectToScreenPosition([clickedRow + 1, 0])
|
|
|
|
@handleDragUntilMouseUp event, (screenPosition) ->
|
|
dragRow = screenPosition.row
|
|
if dragRow < tailPosition.row # dragging up
|
|
editor.setSelectedScreenRange([[dragRow, 0], tailPosition], preserveFolds: true)
|
|
else
|
|
editor.setSelectedScreenRange([tailPosition, [dragRow + 1, 0]], preserveFolds: true)
|
|
|
|
onStylesheetsChanged: (styleElement) ->
|
|
return unless @performedInitialMeasurement
|
|
return unless atom.themes.isInitialLoadComplete()
|
|
|
|
# This delay prevents the styling from going haywire when stylesheets are
|
|
# reloaded in dev mode. It seems like a workaround for a browser bug, but
|
|
# not totally sure.
|
|
requestAnimationFrame =>
|
|
if @isMounted()
|
|
@refreshScrollbars() if not styleElement.sheet? or @containsScrollbarSelector(styleElement.sheet)
|
|
@handleStylingChange()
|
|
|
|
onAllThemesLoaded: ->
|
|
@refreshScrollbars()
|
|
@handleStylingChange()
|
|
|
|
handleStylingChange: ->
|
|
@sampleFontStyling()
|
|
@sampleBackgroundColors()
|
|
@remeasureCharacterWidths()
|
|
|
|
onScreenLinesChanged: (change) ->
|
|
{editor} = @props
|
|
@pendingChanges.push(change)
|
|
@requestUpdate() if editor.intersectsVisibleRowRange(change.start, change.end + 1) # TODO: Use closed-open intervals for change events
|
|
|
|
onSelectionAdded: (selection) ->
|
|
{editor} = @props
|
|
|
|
@subscribe selection.onDidChangeRange => @onSelectionChanged(selection)
|
|
@subscribe selection.onDidDestroy =>
|
|
@onSelectionChanged(selection)
|
|
@unsubscribe(selection)
|
|
|
|
if editor.selectionIntersectsVisibleRowRange(selection)
|
|
@selectionChanged = true
|
|
@requestUpdate()
|
|
|
|
onSelectionChanged: (selection) ->
|
|
{editor} = @props
|
|
if editor.selectionIntersectsVisibleRowRange(selection)
|
|
@selectionChanged = true
|
|
@requestUpdate()
|
|
|
|
onScrollTopChanged: ->
|
|
@scrollingVertically = true
|
|
@requestUpdate()
|
|
@onStoppedScrollingAfterDelay ?= debounce(@onStoppedScrolling, 200)
|
|
@onStoppedScrollingAfterDelay()
|
|
|
|
onStoppedScrolling: ->
|
|
return unless @isMounted()
|
|
|
|
@scrollingVertically = false
|
|
@mouseWheelScreenRow = null
|
|
@requestUpdate()
|
|
|
|
onStoppedScrollingAfterDelay: null # created lazily
|
|
|
|
onCursorAdded: (cursor) ->
|
|
@subscribe cursor.onDidChangePosition @onCursorMoved
|
|
|
|
onCursorMoved: ->
|
|
@cursorMoved = true
|
|
@requestUpdate()
|
|
|
|
onDecorationAdded: (decoration) ->
|
|
@subscribe decoration.onDidChangeProperties(@onDecorationChanged)
|
|
@subscribe decoration.getMarker().onDidChange(@onDecorationChanged)
|
|
@requestUpdate()
|
|
|
|
onDecorationChanged: ->
|
|
@requestUpdate()
|
|
|
|
onDecorationRemoved: ->
|
|
@requestUpdate()
|
|
|
|
onCharacterWidthsChanged: (@scopedCharacterWidthsChangeCount) ->
|
|
@requestUpdate()
|
|
|
|
onPlaceholderTextChanged: ->
|
|
@requestUpdate()
|
|
|
|
handleDragUntilMouseUp: (event, dragHandler) ->
|
|
{editor} = @props
|
|
dragging = false
|
|
lastMousePosition = {}
|
|
animationLoop = =>
|
|
@requestAnimationFrame =>
|
|
if dragging and @isMounted()
|
|
screenPosition = @screenPositionForMouseEvent(lastMousePosition)
|
|
dragHandler(screenPosition)
|
|
animationLoop()
|
|
else if not @isMounted()
|
|
stopDragging()
|
|
|
|
onMouseMove = (event) ->
|
|
lastMousePosition.clientX = event.clientX
|
|
lastMousePosition.clientY = event.clientY
|
|
|
|
# Start the animation loop when the mouse moves prior to a mouseup event
|
|
unless dragging
|
|
dragging = true
|
|
animationLoop()
|
|
|
|
# Stop dragging when cursor enters dev tools because we can't detect mouseup
|
|
onMouseUp() if event.which is 0
|
|
|
|
onMouseUp = (event) ->
|
|
stopDragging()
|
|
editor.finalizeSelections()
|
|
pasteSelectionClipboard(event)
|
|
|
|
stopDragging = ->
|
|
dragging = false
|
|
window.removeEventListener('mousemove', onMouseMove)
|
|
window.removeEventListener('mouseup', onMouseUp)
|
|
|
|
pasteSelectionClipboard = (event) ->
|
|
if event?.which is 2 and process.platform is 'linux'
|
|
if selection = require('clipboard').readText('selection')
|
|
editor.insertText(selection)
|
|
|
|
window.addEventListener('mousemove', onMouseMove)
|
|
window.addEventListener('mouseup', onMouseUp)
|
|
|
|
isVisible: ->
|
|
node = @getDOMNode()
|
|
node.offsetHeight > 0 or node.offsetWidth > 0
|
|
|
|
pauseDOMPolling: ->
|
|
@domPollingPaused = true
|
|
@resumeDOMPollingAfterDelay ?= debounce(@resumeDOMPolling, 100)
|
|
@resumeDOMPollingAfterDelay()
|
|
|
|
resumeDOMPolling: ->
|
|
@domPollingPaused = false
|
|
|
|
resumeDOMPollingAfterDelay: null # created lazily
|
|
|
|
pollDOM: ->
|
|
return if @domPollingPaused or @updateRequested or not @isMounted()
|
|
|
|
unless @checkForVisibilityChange()
|
|
@sampleBackgroundColors()
|
|
@measureHeightAndWidth()
|
|
@sampleFontStyling()
|
|
|
|
checkForVisibilityChange: ->
|
|
if @isVisible()
|
|
if @wasVisible
|
|
false
|
|
else
|
|
@becameVisible()
|
|
@wasVisible = true
|
|
else
|
|
@wasVisible = false
|
|
|
|
requestHeightAndWidthMeasurement: ->
|
|
return if @heightAndWidthMeasurementRequested
|
|
|
|
@heightAndWidthMeasurementRequested = true
|
|
requestAnimationFrame =>
|
|
@heightAndWidthMeasurementRequested = false
|
|
@measureHeightAndWidth()
|
|
|
|
# Measure explicitly-styled height and width and relay them to the model. If
|
|
# these values aren't explicitly styled, we assume the editor is unconstrained
|
|
# and use the scrollHeight / scrollWidth as its height and width in
|
|
# calculations.
|
|
measureHeightAndWidth: ->
|
|
return unless @isMounted()
|
|
|
|
{editor, hostElement} = @props
|
|
scrollViewNode = @refs.scrollView.getDOMNode()
|
|
{position} = getComputedStyle(hostElement)
|
|
{height} = hostElement.style
|
|
|
|
if position is 'absolute' or height
|
|
if @autoHeight
|
|
@autoHeight = false
|
|
@forceUpdate() if not @updatesPaused and @canUpdate()
|
|
|
|
clientHeight = scrollViewNode.clientHeight
|
|
editor.setHeight(clientHeight) if clientHeight > 0
|
|
else
|
|
editor.setHeight(null)
|
|
@autoHeight = true
|
|
|
|
clientWidth = scrollViewNode.clientWidth
|
|
paddingLeft = parseInt(getComputedStyle(scrollViewNode).paddingLeft)
|
|
clientWidth -= paddingLeft
|
|
editor.setWidth(clientWidth) if clientWidth > 0
|
|
|
|
sampleFontStyling: ->
|
|
oldFontSize = @fontSize
|
|
oldFontFamily = @fontFamily
|
|
oldLineHeight = @lineHeight
|
|
|
|
{@fontSize, @fontFamily, @lineHeight} = getComputedStyle(@getTopmostDOMNode())
|
|
|
|
if @fontSize isnt oldFontSize or @fontFamily isnt oldFontFamily or @lineHeight isnt oldLineHeight
|
|
@measureLineHeightAndDefaultCharWidth()
|
|
|
|
if (@fontSize isnt oldFontSize or @fontFamily isnt oldFontFamily) and @performedInitialMeasurement
|
|
@remeasureCharacterWidths()
|
|
|
|
sampleBackgroundColors: (suppressUpdate) ->
|
|
{hostElement} = @props
|
|
{showLineNumbers} = @state
|
|
{backgroundColor} = getComputedStyle(hostElement)
|
|
|
|
if backgroundColor isnt @backgroundColor
|
|
@backgroundColor = backgroundColor
|
|
@requestUpdate() unless suppressUpdate
|
|
|
|
if @refs.gutter?
|
|
gutterBackgroundColor = getComputedStyle(@refs.gutter.getDOMNode()).backgroundColor
|
|
if gutterBackgroundColor isnt @gutterBackgroundColor
|
|
@gutterBackgroundColor = gutterBackgroundColor
|
|
@requestUpdate() unless suppressUpdate
|
|
|
|
measureLineHeightAndDefaultCharWidth: ->
|
|
if @isVisible()
|
|
@measureLineHeightAndDefaultCharWidthWhenShown = false
|
|
@refs.lines.measureLineHeightAndDefaultCharWidth()
|
|
else
|
|
@measureLineHeightAndDefaultCharWidthWhenShown = true
|
|
|
|
remeasureCharacterWidths: ->
|
|
if @isVisible()
|
|
@remeasureCharacterWidthsWhenShown = false
|
|
@refs.lines.remeasureCharacterWidths()
|
|
else
|
|
@remeasureCharacterWidthsWhenShown = true
|
|
|
|
measureScrollbars: ->
|
|
@measureScrollbarsWhenShown = false
|
|
|
|
{editor} = @props
|
|
cornerNode = @refs.scrollbarCorner.getDOMNode()
|
|
originalDisplayValue = cornerNode.style.display
|
|
|
|
cornerNode.style.display = 'block'
|
|
|
|
width = (cornerNode.offsetWidth - cornerNode.clientWidth) or 15
|
|
height = (cornerNode.offsetHeight - cornerNode.clientHeight) or 15
|
|
|
|
editor.setVerticalScrollbarWidth(width)
|
|
editor.setHorizontalScrollbarHeight(height)
|
|
|
|
cornerNode.style.display = originalDisplayValue
|
|
|
|
containsScrollbarSelector: (stylesheet) ->
|
|
for rule in stylesheet.cssRules
|
|
if rule.selectorText?.indexOf('scrollbar') > -1
|
|
return true
|
|
false
|
|
|
|
refreshScrollbars: ->
|
|
if @isVisible()
|
|
@measureScrollbarsWhenShown = false
|
|
else
|
|
@measureScrollbarsWhenShown = true
|
|
return
|
|
|
|
{verticalScrollbar, horizontalScrollbar, scrollbarCorner} = @refs
|
|
|
|
verticalNode = verticalScrollbar.getDOMNode()
|
|
horizontalNode = horizontalScrollbar.getDOMNode()
|
|
cornerNode = scrollbarCorner.getDOMNode()
|
|
|
|
originalVerticalDisplayValue = verticalNode.style.display
|
|
originalHorizontalDisplayValue = horizontalNode.style.display
|
|
originalCornerDisplayValue = cornerNode.style.display
|
|
|
|
# First, hide all scrollbars in case they are visible so they take on new
|
|
# styles when they are shown again.
|
|
verticalNode.style.display = 'none'
|
|
horizontalNode.style.display = 'none'
|
|
cornerNode.style.display = 'none'
|
|
|
|
# Force a reflow
|
|
cornerNode.offsetWidth
|
|
|
|
# Now measure the new scrollbar dimensions
|
|
@measureScrollbars()
|
|
|
|
# Now restore the display value for all scrollbars, since they were
|
|
# previously hidden
|
|
verticalNode.style.display = originalVerticalDisplayValue
|
|
horizontalNode.style.display = originalHorizontalDisplayValue
|
|
cornerNode.style.display = originalCornerDisplayValue
|
|
|
|
clearMouseWheelScreenRow: ->
|
|
if @mouseWheelScreenRow?
|
|
@mouseWheelScreenRow = null
|
|
@requestUpdate()
|
|
|
|
clearMouseWheelScreenRowAfterDelay: null # created lazily
|
|
|
|
consolidateSelections: (e) ->
|
|
e.abortKeyBinding() unless @props.editor.consolidateSelections()
|
|
|
|
lineNodeForScreenRow: (screenRow) -> @refs.lines.lineNodeForScreenRow(screenRow)
|
|
|
|
lineNumberNodeForScreenRow: (screenRow) -> @refs.gutter.lineNumberNodeForScreenRow(screenRow)
|
|
|
|
screenRowForNode: (node) ->
|
|
while node?
|
|
if screenRow = node.dataset.screenRow
|
|
return parseInt(screenRow)
|
|
node = node.parentElement
|
|
null
|
|
|
|
getFontSize: ->
|
|
parseInt(getComputedStyle(@getTopmostDOMNode()).fontSize)
|
|
|
|
setFontSize: (fontSize) ->
|
|
@getTopmostDOMNode().style.fontSize = fontSize + 'px'
|
|
@sampleFontStyling()
|
|
|
|
getFontFamily: ->
|
|
getComputedStyle(@getTopmostDOMNode()).fontFamily
|
|
|
|
setFontFamily: (fontFamily) ->
|
|
@getTopmostDOMNode().style.fontFamily = fontFamily
|
|
@sampleFontStyling()
|
|
|
|
setLineHeight: (lineHeight) ->
|
|
@getTopmostDOMNode().style.lineHeight = lineHeight
|
|
@sampleFontStyling()
|
|
|
|
setShowIndentGuide: (showIndentGuide) ->
|
|
@setState({showIndentGuide})
|
|
|
|
setMini: ->
|
|
@updateGutterVisible()
|
|
@requestUpdate()
|
|
|
|
updateGutterVisible: ->
|
|
gutterVisible = not @props.editor.isMini() and @props.editor.isGutterVisible() and atom.config.get('editor.showLineNumbers')
|
|
if gutterVisible isnt @gutterVisible
|
|
@gutterVisible = gutterVisible
|
|
@requestUpdate()
|
|
|
|
# Deprecated
|
|
setInvisibles: (invisibles={}) ->
|
|
grim.deprecate "Use config.set('editor.invisibles', invisibles) instead"
|
|
atom.config.set('editor.invisibles', invisibles)
|
|
|
|
# Deprecated
|
|
setShowInvisibles: (showInvisibles) ->
|
|
atom.config.set('editor.showInvisibles', showInvisibles)
|
|
|
|
setScrollSensitivity: (scrollSensitivity) ->
|
|
if scrollSensitivity = parseInt(scrollSensitivity)
|
|
@scrollSensitivity = Math.abs(scrollSensitivity) / 100
|
|
|
|
setUseHardwareAcceleration: (useHardwareAcceleration=true) ->
|
|
unless @useHardwareAcceleration is useHardwareAcceleration
|
|
@useHardwareAcceleration = useHardwareAcceleration
|
|
@requestUpdate()
|
|
|
|
screenPositionForMouseEvent: (event) ->
|
|
pixelPosition = @pixelPositionForMouseEvent(event)
|
|
@props.editor.screenPositionForPixelPosition(pixelPosition)
|
|
|
|
pixelPositionForMouseEvent: (event) ->
|
|
{editor} = @props
|
|
{clientX, clientY} = event
|
|
|
|
linesClientRect = @refs.lines.getDOMNode().getBoundingClientRect()
|
|
top = clientY - linesClientRect.top
|
|
left = clientX - linesClientRect.left
|
|
{top, left}
|
|
|
|
getModel: ->
|
|
@props.editor
|
|
|
|
isInputEnabled: -> @inputEnabled
|
|
|
|
setInputEnabled: (@inputEnabled) -> @inputEnabled
|
|
|
|
updateParentViewFocusedClassIfNeeded: (prevState) ->
|
|
if prevState.focused isnt @state.focused
|
|
@props.hostElement.classList.toggle('is-focused', @state.focused)
|
|
@props.rootElement.classList.toggle('is-focused', @state.focused)
|
|
|
|
updateParentViewMiniClass: ->
|
|
@props.hostElement.classList.toggle('mini', @props.editor.isMini())
|
|
@props.rootElement.classList.toggle('mini', @props.editor.isMini())
|
|
|
|
runScrollBenchmark: ->
|
|
unless process.env.NODE_ENV is 'production'
|
|
ReactPerf = require 'react-atom-fork/lib/ReactDefaultPerf'
|
|
ReactPerf.start()
|
|
|
|
node = @getDOMNode()
|
|
|
|
scroll = (delta, done) ->
|
|
dispatchMouseWheelEvent = ->
|
|
node.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -0, wheelDeltaY: -delta))
|
|
|
|
stopScrolling = ->
|
|
clearInterval(interval)
|
|
done?()
|
|
|
|
interval = setInterval(dispatchMouseWheelEvent, 10)
|
|
setTimeout(stopScrolling, 500)
|
|
|
|
console.timeline('scroll')
|
|
scroll 50, ->
|
|
scroll 100, ->
|
|
scroll 200, ->
|
|
scroll 400, ->
|
|
scroll 800, ->
|
|
scroll 1600, ->
|
|
console.timelineEnd('scroll')
|
|
unless process.env.NODE_ENV is 'production'
|
|
ReactPerf.stop()
|
|
console.log "Inclusive"
|
|
ReactPerf.printInclusive()
|
|
console.log "Exclusive"
|
|
ReactPerf.printExclusive()
|
|
console.log "Wasted"
|
|
ReactPerf.printWasted()
|