Files
atom/src/text-editor-component.coffee
2014-11-10 17:59:41 -08:00

1031 lines
36 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'
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]
statics:
performSyncUpdates: false
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, mini, cursorBlinkPeriod, cursorBlinkResumeDelay} = @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)
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
className = 'editor-contents'
className += ' is-focused' if focused
className += ' has-selection' if hasSelection
div {className, style},
if @shouldRenderGutter()
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,
showIndentGuide, renderedRowRange, @pendingChanges, scrollTop, scrollLeft,
@scrollingVertically, scrollHeight, scrollWidth, mouseWheelScreenRow,
visible, scrollViewHeight, @scopedCharacterWidthsChangeCount, lineWidth, @useHardwareAcceleration,
placeholderText, @performedInitialMeasurement, @backgroundColor, cursorPixelRects,
cursorBlinkPeriod, cursorBlinkResumeDelay, mini
}
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()))
shouldRenderGutter: ->
not @props.mini and @state.showLineNumbers
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.onDidReloadAll @onAllThemesLoaded
@subscribe scrollbarStyle.changes, @refreshScrollbars
@domPollingIntervalId = setInterval(@pollDOM, @domPollingInterval)
@updateParentViewFocusedClassIfNeeded({})
@updateParentViewMiniClassIfNeeded({})
@checkForVisibilityChange()
componentWillUnmount: ->
{editor, hostElement} = @props
hostElement.__spacePenView.trigger 'editor:will-be-removed', [hostElement.__spacePenView]
@unsubscribe()
@scopedConfigSubscriptions.dispose()
window.removeEventListener 'resize', @requestHeightAndWidthMeasurement
clearInterval(@domPollingIntervalId)
@domPollingIntervalId = null
componentWillReceiveProps: (newProps) ->
@props.editor.setMini(newProps.mini)
componentDidUpdate: (prevProps, prevState) ->
cursorMoved = @cursorMoved
selectionChanged = @selectionChanged
@pendingChanges.length = 0
@cursorMoved = false
@selectionChanged = false
if @props.editor.isAlive()
@updateParentViewFocusedClassIfNeeded(prevState)
@updateParentViewMiniClassIfNeeded(prevState)
@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 @updateRequestedWhilePaused and @canUpdate()
requestUpdate: ->
return unless @canUpdate()
if @updatesPaused
@updateRequestedWhilePaused = true
return
if @performSyncUpdates ? TextEditorComponent.performSyncUpdates
@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, mini} = @props
return {} if mini
decorationsByScreenRow = {}
for markerId, decorations of decorationsByMarkerId
marker = editor.getMarker(markerId)
screenRange = null
headScreenRow = null
if marker.isValid()
for decoration in decorations
if decoration.isType('gutter') 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)
endPixelPosition: editor.pixelPositionForScreenPosition(screenRange.end)
decorations: []
filteredDecorations[markerId].decorations.push decorationParams
filteredDecorations
observeEditor: ->
{editor} = @props
@subscribe editor.onDidChange(@onScreenLinesChanged)
@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()
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 = ''
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 scopeDescriptor, 'editor.showIndentGuide', @setShowIndentGuide
subscriptions.add atom.config.observe scopeDescriptor, 'editor.showLineNumbers', @setShowLineNumbers
subscriptions.add atom.config.observe scopeDescriptor, 'editor.scrollSensitivity', @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) ->
return unless event.button is 0 # only handle the left mouse button
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()
@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
screenPosition = @screenPositionForMouseEvent(lastMousePosition)
dragHandler(screenPosition)
animationLoop()
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 = ->
dragging = false
window.removeEventListener('mousemove', onMouseMove)
window.removeEventListener('mouseup', onMouseUp)
editor.finalizeSelections()
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 @shouldRenderGutter()
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})
# 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)
setShowLineNumbers: (showLineNumbers) ->
@setState({showLineNumbers})
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)
updateParentViewMiniClassIfNeeded: (prevProps) ->
if prevProps.mini isnt @props.mini
@props.hostElement.classList.toggle('mini', @props.mini)
@props.rootElement.classList.toggle('mini', @props.mini)
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()