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' GutterComponent = require './gutter-component' InputComponent = require './input-component' CursorsComponent = require './cursors-component' LinesComponent = require './lines-component' ScrollbarComponent = require './scrollbar-component' ScrollbarCornerComponent = require './scrollbar-corner-component' SubscriberMixin = require './subscriber-mixin' module.exports = EditorComponent = React.createClass displayName: 'EditorComponent' mixins: [SubscriberMixin] pendingScrollTop: null pendingScrollLeft: null selectOnMouseMove: false batchingUpdates: false updateRequested: false cursorsMoved: false selectionChanged: false selectionAdded: false scrollingVertically: false gutterWidth: 0 refreshingScrollbars: false measuringScrollbars: true pendingVerticalScrollDelta: 0 pendingHorizontalScrollDelta: 0 mouseWheelScreenRow: null mouseWheelScreenRowClearDelay: 150 scrollSensitivity: 0.4 scrollViewMeasurementRequested: false measureLineHeightAndDefaultCharWidthWhenShown: false remeasureCharacterWidthsWhenShown: false inputEnabled: true scrollViewMeasurementInterval: 100 scopedCharacterWidthsChangeCount: null render: -> {focused, fontSize, lineHeight, fontFamily, showIndentGuide, showInvisibles, visible} = @state {editor, cursorBlinkPeriod, cursorBlinkResumeDelay} = @props maxLineNumberDigits = editor.getLineCount().toString().length invisibles = if showInvisibles then @state.invisibles else {} hasSelection = editor.getSelection()? and !editor.getSelection().isEmpty() if @isMounted() renderedRowRange = @getRenderedRowRange() [renderedStartRow, renderedEndRow] = renderedRowRange cursorScreenRanges = @getCursorScreenRanges(renderedRowRange) decorations = editor.decorationsForScreenRowRange(renderedStartRow, renderedEndRow) highlightDecorations = @getHighlightDecorations(decorations) lineDecorations = @getLineDecorations(decorations) scrollHeight = editor.getScrollHeight() scrollWidth = editor.getScrollWidth() scrollTop = editor.getScrollTop() scrollLeft = editor.getScrollLeft() lineHeightInPixels = editor.getLineHeightInPixels() defaultCharWidth = editor.getDefaultCharWidth() scrollViewHeight = editor.getHeight() horizontalScrollbarHeight = editor.getHorizontalScrollbarHeight() verticalScrollbarWidth = editor.getVerticalScrollbarWidth() verticallyScrollable = editor.verticallyScrollable() horizontallyScrollable = editor.horizontallyScrollable() hiddenInputStyle = @getHiddenInputPosition() hiddenInputStyle.WebkitTransform = 'translateZ(0)' if @mouseWheelScreenRow? and not (renderedStartRow <= @mouseWheelScreenRow < renderedEndRow) mouseWheelScreenRow = @mouseWheelScreenRow className = 'editor-contents editor-colors' className += ' is-focused' if focused className += ' has-selection' if hasSelection div className: className, style: {fontSize, lineHeight, fontFamily}, tabIndex: -1, GutterComponent { ref: 'gutter', onWidthChanged: @onGutterWidthChanged, lineDecorations, defaultCharWidth, editor, renderedRowRange, maxLineNumberDigits, scrollViewHeight, scrollTop, scrollHeight, lineHeightInPixels, @pendingChanges, mouseWheelScreenRow } div ref: 'scrollView', className: 'scroll-view', onMouseDown: @onMouseDown, InputComponent ref: 'input' className: 'hidden-input' style: hiddenInputStyle onFocus: @onInputFocused onBlur: @onInputBlurred CursorsComponent { editor, scrollTop, scrollLeft, cursorScreenRanges, cursorBlinkPeriod, cursorBlinkResumeDelay, lineHeightInPixels, defaultCharWidth, @scopedCharacterWidthsChangeCount } LinesComponent { ref: 'lines', editor, lineHeightInPixels, defaultCharWidth, lineDecorations, highlightDecorations, showIndentGuide, renderedRowRange, @pendingChanges, scrollTop, scrollLeft, @scrollingVertically, scrollHeight, scrollWidth, mouseWheelScreenRow, invisibles, visible, scrollViewHeight, @scopedCharacterWidthsChangeCount } ScrollbarComponent ref: 'verticalScrollbar' className: 'vertical-scrollbar' orientation: 'vertical' onScroll: @onVerticalScroll scrollTop: scrollTop scrollHeight: scrollHeight visible: verticallyScrollable and not @refreshingScrollbars and not @measuringScrollbars scrollableInOppositeDirection: horizontallyScrollable verticalScrollbarWidth: verticalScrollbarWidth horizontalScrollbarHeight: horizontalScrollbarHeight ScrollbarComponent ref: 'horizontalScrollbar' className: 'horizontal-scrollbar' orientation: 'horizontal' onScroll: @onHorizontalScroll scrollLeft: scrollLeft scrollWidth: scrollWidth + @gutterWidth visible: horizontallyScrollable and not @refreshingScrollbars and not @measuringScrollbars scrollableInOppositeDirection: verticallyScrollable verticalScrollbarWidth: verticalScrollbarWidth horizontalScrollbarHeight: horizontalScrollbarHeight # Also used to measure the height/width of scrollbars after the initial render ScrollbarCornerComponent ref: 'scrollbarCorner' visible: not @refreshingScrollbars and (@measuringScrollbars or horizontallyScrollable and verticallyScrollable) measuringScrollbars: @measuringScrollbars height: horizontalScrollbarHeight width: verticalScrollbarWidth getInitialState: -> visible: true getDefaultProps: -> cursorBlinkPeriod: 800 cursorBlinkResumeDelay: 100 lineOverdrawMargin: 8 componentWillMount: -> @pendingChanges = [] @props.editor.manageScrollPosition = true @observeConfig() @setScrollSensitivity(atom.config.get('editor.scrollSensitivity')) componentDidMount: -> {editor} = @props @scrollViewMeasurementIntervalId = setInterval(@requestScrollViewMeasurement, @scrollViewMeasurementInterval) @observeEditor() @listenForDOMEvents() @listenForCommands() @subscribe atom.themes, 'stylesheet-added stylsheet-removed', @onStylesheetsChanged @subscribe scrollbarStyle.changes, @refreshScrollbars editor.setVisible(true) editor.batchUpdates => @measureLineHeightAndDefaultCharWidth() @measureScrollView() @measureScrollbars() componentWillUnmount: -> @unsubscribe() clearInterval(@scrollViewMeasurementIntervalId) @scrollViewMeasurementIntervalId = null componentWillUpdate: -> if @props.editor.isAlive() @props.parentView.trigger 'cursor:moved' if @cursorsMoved @props.parentView.trigger 'selection:changed' if @selectionChanged componentDidUpdate: (prevProps, prevState) -> @pendingChanges.length = 0 @refreshingScrollbars = false @updateParentViewFocusedClassIfNeeded(prevState) @measureScrollbars() if @measuringScrollbars @measureLineHeightAndCharWidthsIfNeeded(prevState) @remeasureCharacterWidthsIfNeeded(prevState) @props.parentView.trigger 'editor:display-updated' requestUpdate: -> if @batchingUpdates @updateRequested = true else @forceUpdate() 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.getCursor()? {top, left, height, width} = editor.getCursor().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 getLineDecorations: (decorationsByMarkerId) -> {editor} = @props decorationsByScreenRow = {} for markerId, decorations of decorationsByMarkerId marker = editor.getMarker(markerId) screenRange = null if marker.isValid() for decoration in decorations if editor.decorationMatchesType(decoration, 'gutter') or editor.decorationMatchesType(decoration, 'line') screenRange ?= marker.getScreenRange() for screenRow in [screenRange.start.row..screenRange.end.row] decorationsByScreenRow[screenRow] ?= [] decorationsByScreenRow[screenRow].push decoration decorationsByScreenRow getHighlightDecorations: (decorationsByMarkerId) -> {editor} = @props filteredDecorations = {} for markerId, decorations of decorationsByMarkerId marker = editor.getMarker(markerId) if marker.isValid() and not marker.getScreenRange().isEmpty() for decoration in decorations if editor.decorationMatchesType(decoration, 'highlight') filteredDecorations[markerId] ?= {id: markerId, screenRange: marker.getScreenRange(), decorations: []} filteredDecorations[markerId].decorations.push decoration filteredDecorations observeEditor: -> {editor} = @props @subscribe editor, 'batched-updates-started', @onBatchedUpdatesStarted @subscribe editor, 'batched-updates-ended', @onBatchedUpdatesEnded @subscribe editor, 'screen-lines-changed', @onScreenLinesChanged @subscribe editor, 'cursors-moved', @onCursorsMoved @subscribe editor, 'selection-removed selection-screen-range-changed', @onSelectionChanged @subscribe editor, 'selection-added', @onSelectionAdded @subscribe editor, 'decoration-added', @onDecorationChanged @subscribe editor, 'decoration-removed', @onDecorationChanged @subscribe editor, 'character-widths-changed', @onCharacterWidthsChanged @subscribe editor.$scrollTop.changes, @onScrollTopChanged @subscribe editor.$scrollLeft.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 'focus', @onFocus # For some reason, React's built in focus events seem to bubble node.addEventListener 'textInput', @onTextInput scrollViewNode = @refs.scrollView.getDOMNode() scrollViewNode.addEventListener 'scroll', @onScrollViewScroll window.addEventListener 'resize', @requestScrollViewMeasurement listenForCommands: -> {parentView, editor, mini} = @props @addCommandListeners 'core:move-left': => editor.moveCursorLeft() 'core:move-right': => editor.moveCursorRight() 'core:select-left': => editor.selectLeft() 'core:select-right': => editor.selectRight() 'core:select-all': => editor.selectAll() 'core:backspace': => editor.backspace() 'core:delete': => editor.delete() 'core:undo': => editor.undo() 'core:redo': => editor.redo() 'core:cut': => editor.cutSelectedText() 'core:copy': => editor.copySelectedText() 'core:paste': => editor.pasteText() 'editor:move-to-previous-word': => editor.moveCursorToPreviousWord() 'editor:select-word': => editor.selectWord() 'editor:consolidate-selections': @consolidateSelections 'editor:delete-to-beginning-of-word': => editor.deleteToBeginningOfWord() 'editor:delete-to-beginning-of-line': => editor.deleteToBeginningOfLine() 'editor:delete-to-end-of-line': => editor.deleteToEndOfLine() 'editor:delete-to-end-of-word': => editor.deleteToEndOfWord() 'editor:delete-line': => editor.deleteLine() 'editor:cut-to-end-of-line': => editor.cutToEndOfLine() 'editor:move-to-beginning-of-next-paragraph': => editor.moveCursorToBeginningOfNextParagraph() 'editor:move-to-beginning-of-previous-paragraph': => editor.moveCursorToBeginningOfPreviousParagraph() 'editor:move-to-beginning-of-screen-line': => editor.moveCursorToBeginningOfScreenLine() 'editor:move-to-beginning-of-line': => editor.moveCursorToBeginningOfLine() 'editor:move-to-end-of-screen-line': => editor.moveCursorToEndOfScreenLine() 'editor:move-to-end-of-line': => editor.moveCursorToEndOfLine() 'editor:move-to-first-character-of-line': => editor.moveCursorToFirstCharacterOfLine() 'editor:move-to-beginning-of-word': => editor.moveCursorToBeginningOfWord() 'editor:move-to-end-of-word': => editor.moveCursorToEndOfWord() 'editor:move-to-beginning-of-next-word': => editor.moveCursorToBeginningOfNextWord() 'editor:move-to-previous-word-boundary': => editor.moveCursorToPreviousWordBoundary() 'editor:move-to-next-word-boundary': => editor.moveCursorToNextWordBoundary() 'editor:select-to-beginning-of-next-paragraph': => editor.selectToBeginningOfNextParagraph() 'editor:select-to-beginning-of-previous-paragraph': => editor.selectToBeginningOfPreviousParagraph() 'editor:select-to-end-of-line': => editor.selectToEndOfLine() 'editor:select-to-beginning-of-line': => editor.selectToBeginningOfLine() 'editor:select-to-end-of-word': => editor.selectToEndOfWord() 'editor:select-to-beginning-of-word': => editor.selectToBeginningOfWord() 'editor:select-to-beginning-of-next-word': => editor.selectToBeginningOfNextWord() 'editor:select-to-next-word-boundary': => editor.selectToNextWordBoundary() 'editor:select-to-previous-word-boundary': => editor.selectToPreviousWordBoundary() 'editor:select-to-first-character-of-line': => editor.selectToFirstCharacterOfLine() 'editor:select-line': => editor.selectLine() 'editor:transpose': => editor.transpose() 'editor:upper-case': => editor.upperCase() 'editor:lower-case': => editor.lowerCase() unless mini @addCommandListeners 'core:move-up': => editor.moveCursorUp() 'core:move-down': => editor.moveCursorDown() 'core:move-to-top': => editor.moveCursorToTop() 'core:move-to-bottom': => editor.moveCursorToBottom() 'core:select-up': => editor.selectUp() 'core:select-down': => editor.selectDown() 'core:select-to-top': => editor.selectToTop() 'core:select-to-bottom': => editor.selectToBottom() 'editor:indent': => editor.indent() 'editor:auto-indent': => editor.autoIndentSelectedRows() 'editor:indent-selected-rows': => editor.indentSelectedRows() 'editor:outdent-selected-rows': => editor.outdentSelectedRows() 'editor:newline': => editor.insertNewline() 'editor:newline-below': => editor.insertNewlineBelow() 'editor:newline-above': => editor.insertNewlineAbove() 'editor:add-selection-below': => editor.addSelectionBelow() 'editor:add-selection-above': => editor.addSelectionAbove() 'editor:split-selections-into-lines': => editor.splitSelectionsIntoLines() 'editor:toggle-soft-tabs': => editor.toggleSoftTabs() 'editor:toggle-soft-wrap': => editor.toggleSoftWrap() 'editor:fold-all': => editor.foldAll() 'editor:unfold-all': => editor.unfoldAll() 'editor:fold-current-row': => editor.foldCurrentRow() 'editor:unfold-current-row': => editor.unfoldCurrentRow() 'editor:fold-selection': => neditor.foldSelectedLines() 'editor:fold-at-indent-level-1': => editor.foldAllAtIndentLevel(0) 'editor:fold-at-indent-level-2': => editor.foldAllAtIndentLevel(1) 'editor:fold-at-indent-level-3': => editor.foldAllAtIndentLevel(2) 'editor:fold-at-indent-level-4': => editor.foldAllAtIndentLevel(3) 'editor:fold-at-indent-level-5': => editor.foldAllAtIndentLevel(4) 'editor:fold-at-indent-level-6': => editor.foldAllAtIndentLevel(5) 'editor:fold-at-indent-level-7': => editor.foldAllAtIndentLevel(6) 'editor:fold-at-indent-level-8': => editor.foldAllAtIndentLevel(7) 'editor:fold-at-indent-level-9': => editor.foldAllAtIndentLevel(8) 'editor:toggle-line-comments': => editor.toggleLineCommentsInSelection() 'editor:log-cursor-scope': => editor.logCursorScope() 'editor:checkout-head-revision': => editor.checkoutHead() 'editor:copy-path': => editor.copyPathToClipboard() 'editor:move-line-up': => editor.moveLineUp() 'editor:move-line-down': => editor.moveLineDown() 'editor:duplicate-lines': => editor.duplicateLines() 'editor:join-lines': => editor.joinLines() 'editor:toggle-indent-guide': => atom.config.toggle('editor.showIndentGuide') 'editor:toggle-line-numbers': => atom.config.toggle('editor.showLineNumbers') 'editor:scroll-to-cursor': => editor.scrollToCursorPosition() 'core:page-up': => editor.pageUp() 'core:page-down': => editor.pageDown() 'benchmark:scroll': @runScrollBenchmark addCommandListeners: (listenersByCommandName) -> {parentView} = @props for command, listener of listenersByCommandName parentView.command command, listener observeConfig: -> @subscribe atom.config.observe 'editor.fontFamily', @setFontFamily @subscribe atom.config.observe 'editor.fontSize', @setFontSize @subscribe atom.config.observe 'editor.lineHeight', @setLineHeight @subscribe atom.config.observe 'editor.showIndentGuide', @setShowIndentGuide @subscribe atom.config.observe 'editor.invisibles', @setInvisibles @subscribe atom.config.observe 'editor.showInvisibles', @setShowInvisibles @subscribe atom.config.observe 'editor.scrollSensitivity', @setScrollSensitivity onFocus: -> @refs.input.focus() onTextInput: (event) -> 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 editor.insertText(event.data) inputNode.value = event.data # 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 ' ' onInputFocused: -> @setState(focused: true) onInputBlurred: -> @setState(focused: false) onVerticalScroll: (scrollTop) -> {editor} = @props return if scrollTop is editor.getScrollTop() animationFramePending = @pendingScrollTop? @pendingScrollTop = scrollTop unless animationFramePending requestAnimationFrame => @props.editor.setScrollTop(@pendingScrollTop) @pendingScrollTop = null onHorizontalScroll: (scrollLeft) -> {editor} = @props return if scrollLeft is editor.getScrollLeft() animationFramePending = @pendingScrollLeft? @pendingScrollLeft = scrollLeft unless animationFramePending requestAnimationFrame => @props.editor.setScrollLeft(@pendingScrollLeft) @pendingScrollLeft = null onMouseWheel: (event) -> event.preventDefault() animationFramePending = @pendingHorizontalScrollDelta isnt 0 or @pendingVerticalScrollDelta isnt 0 # Only scroll in one direction at a time {wheelDeltaX, wheelDeltaY} = event if Math.abs(wheelDeltaX) > Math.abs(wheelDeltaY) # Scrolling horizontally @pendingHorizontalScrollDelta -= Math.round(wheelDeltaX * @scrollSensitivity) else # Scrolling vertically @pendingVerticalScrollDelta -= Math.round(wheelDeltaY * @scrollSensitivity) @mouseWheelScreenRow = @screenRowForNode(event.target) @clearMouseWheelScreenRowAfterDelay ?= debounce(@clearMouseWheelScreenRow, @mouseWheelScreenRowClearDelay) @clearMouseWheelScreenRowAfterDelay() unless animationFramePending requestAnimationFrame => {editor} = @props editor.setScrollTop(editor.getScrollTop() + @pendingVerticalScrollDelta) editor.setScrollLeft(editor.getScrollLeft() + @pendingHorizontalScrollDelta) @pendingVerticalScrollDelta = 0 @pendingHorizontalScrollDelta = 0 onScrollViewScroll: -> console.warn "EditorScrollView scroll position changed, and it shouldn't have. If you can reproduce this, please report it." scrollViewNode = @refs.scrollView.getDOMNode() scrollViewNode.scrollTop = 0 scrollViewNode.scrollLeft = 0 onMouseDown: (event) -> return unless event.button is 0 # only handle the left mouse button {editor} = @props {detail, shiftKey, metaKey} = event screenPosition = @screenPositionForMouseEvent(event) if event.target?.classList.contains('fold-marker') bufferRow = editor.bufferRowForScreenRow(screenPosition.row) editor.unfoldBufferRow(bufferRow) return if shiftKey editor.selectToScreenPosition(screenPosition) else if metaKey editor.addCursorAtScreenPosition(screenPosition) else editor.setCursorScreenPosition(screenPosition) switch detail when 2 then editor.selectWord() when 3 then editor.selectLine() @selectToMousePositionUntilMouseUp(event) onStylesheetsChanged: (stylesheet) -> @refreshScrollbars() if @containsScrollbarSelector(stylesheet) onBatchedUpdatesStarted: -> @batchingUpdates = true onBatchedUpdatesEnded: -> updateRequested = @updateRequested @updateRequested = false @batchingUpdates = false if updateRequested @requestUpdate() onScreenLinesChanged: (change) -> {editor} = @props @pendingChanges.push(change) @requestUpdate() if editor.intersectsVisibleRowRange(change.start, change.end + 1) # TODO: Use closed-open intervals for change events onSelectionChanged: (selection) -> {editor} = @props if editor.selectionIntersectsVisibleRowRange(selection) @selectionChanged = true @requestUpdate() onSelectionAdded: (selection) -> {editor} = @props if editor.selectionIntersectsVisibleRowRange(selection) @selectionChanged = true @selectionAdded = true @requestUpdate() onScrollTopChanged: -> @scrollingVertically = true @requestUpdate() @onStoppedScrollingAfterDelay ?= debounce(@onStoppedScrolling, 100) @onStoppedScrollingAfterDelay() onStoppedScrolling: -> return unless @isMounted() @scrollingVertically = false @mouseWheelScreenRow = null @requestUpdate() onStoppedScrollingAfterDelay: null # created lazily onCursorsMoved: -> @cursorsMoved = true @requestUpdate() onDecorationChanged: -> @decorationChangedImmediate ?= setImmediate => @requestUpdate() if @isMounted() @decorationChangedImmediate = null onCharacterWidthsChanged: (@scopedCharacterWidthsChangeCount) -> @requestUpdate() selectToMousePositionUntilMouseUp: (event) -> {editor} = @props dragging = false lastMousePosition = {} animationLoop = => requestAnimationFrame => if dragging @selectToMousePosition(lastMousePosition) 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) selectToMousePosition: (event) -> @props.editor.selectToScreenPosition(@screenPositionForMouseEvent(event)) requestScrollViewMeasurement: -> return if @measurementPending @scrollViewMeasurementRequested = true requestAnimationFrame => @scrollViewMeasurementRequested = false @measureScrollView() # 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. measureScrollView: -> return unless @isMounted() {editor} = @props editorNode = @getDOMNode() scrollViewNode = @refs.scrollView.getDOMNode() {position} = getComputedStyle(editorNode) {width, height} = editorNode.style editor.batchUpdates -> if position is 'absolute' or height clientHeight = scrollViewNode.clientHeight editor.setHeight(clientHeight) if clientHeight > 0 if position is 'absolute' or width clientWidth = scrollViewNode.clientWidth editor.setWidth(clientWidth) if clientWidth > 0 measureLineHeightAndCharWidthsIfNeeded: (prevState) -> if not isEqualForProperties(prevState, @state, 'lineHeight', 'fontSize', 'fontFamily') if @state.visible @measureLineHeightAndDefaultCharWidth() else @measureLineHeightAndDefaultCharWidthWhenShown = true else if @measureLineHeightAndDefaultCharWidthWhenShown and @state.visible and not prevState.visible @measureLineHeightAndDefaultCharWidth() measureLineHeightAndDefaultCharWidth: -> @measureLineHeightAndDefaultCharWidthWhenShown = false @refs.lines.measureLineHeightAndDefaultCharWidth() remeasureCharacterWidthsIfNeeded: (prevState) -> if not isEqualForProperties(prevState, @state, 'fontSize', 'fontFamily') if @state.visible @remeasureCharacterWidths() else @remeasureCharacterWidthsWhenShown = true else if @remeasureCharacterWidthsWhenShown and @state.visible and not prevState.visible @remeasureCharacterWidths() remeasureCharacterWidths: -> @remeasureCharacterWidthsWhenShown = false @refs.lines.remeasureCharacterWidths() onGutterWidthChanged: (@gutterWidth) -> @requestUpdate() measureScrollbars: -> @measuringScrollbars = false {editor} = @props scrollbarCornerNode = @refs.scrollbarCorner.getDOMNode() width = (scrollbarCornerNode.offsetWidth - scrollbarCornerNode.clientWidth) or 15 height = (scrollbarCornerNode.offsetHeight - scrollbarCornerNode.clientHeight) or 15 editor.setVerticalScrollbarWidth(width) editor.setHorizontalScrollbarHeight(height) containsScrollbarSelector: (stylesheet) -> for rule in stylesheet.cssRules if rule.selectorText?.indexOf('scrollbar') > -1 return true false refreshScrollbars: -> # Believe it or not, proper handling of changes to scrollbar styles requires # three DOM updates. # Scrollbar style changes won't apply to scrollbars that are already # visible, so first we need to hide scrollbars so we can redisplay them and # force Chromium to apply updates. @refreshingScrollbars = true @requestUpdate() # Next, we display only the scrollbar corner so we can measure the new # scrollbar dimensions. The ::measuringScrollbars property will be set back # to false after the scrollbars are measured. @measuringScrollbars = true @requestUpdate() # Finally, we restore the scrollbars based on the newly-measured dimensions # if the editor's content and dimensions require them to be visible. @requestUpdate() 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 isnt document if screenRow = node.dataset.screenRow return parseInt(screenRow) node = node.parentNode null hide: -> @setState(visible: false) show: -> @setState(visible: true) getFontSize: -> @state.fontSize setFontSize: (fontSize) -> @setState({fontSize}) getFontFamily: -> @state.fontFamily setFontFamily: (fontFamily) -> @setState({fontFamily}) setLineHeight: (lineHeight) -> @setState({lineHeight}) setShowIndentGuide: (showIndentGuide) -> @setState({showIndentGuide}) # Public: Defines which characters are invisible. # # invisibles - An {Object} defining the invisible characters: # :eol - The end of line invisible {String} (default: `\u00ac`). # :space - The space invisible {String} (default: `\u00b7`). # :tab - The tab invisible {String} (default: `\u00bb`). # :cr - The carriage return invisible {String} (default: `\u00a4`). setInvisibles: (invisibles={}) -> defaults invisibles, eol: '\u00ac' space: '\u00b7' tab: '\u00bb' cr: '\u00a4' @setState({invisibles}) setShowInvisibles: (showInvisibles) -> @setState({showInvisibles}) setScrollSensitivity: (scrollSensitivity) -> if scrollSensitivity = parseInt(scrollSensitivity) @scrollSensitivity = Math.abs(scrollSensitivity) / 100 screenPositionForMouseEvent: (event) -> pixelPosition = @pixelPositionForMouseEvent(event) @props.editor.screenPositionForPixelPosition(pixelPosition) pixelPositionForMouseEvent: (event) -> {editor} = @props {clientX, clientY} = event scrollViewClientRect = @refs.scrollView.getDOMNode().getBoundingClientRect() top = clientY - scrollViewClientRect.top + editor.getScrollTop() left = clientX - scrollViewClientRect.left + editor.getScrollLeft() {top, left} getModel: -> @props.editor isInputEnabled: -> @inputEnabled setInputEnabled: (@inputEnabled) -> @inputEnabled updateParentViewFocusedClassIfNeeded: (prevState) -> if prevState.focused isnt @state.focused @props.parentView.toggleClass('is-focused', @props.focused) 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()