_ = 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()