diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 0fa4d9b9a..def910f35 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -317,7 +317,7 @@ describe "EditorComponent", -> expect(cursorNodes[0].offsetWidth).toBe charWidth expect(cursorNodes[0].style['-webkit-transform']).toBe "translate3d(#{5 * charWidth}px, #{0 * lineHeightInPixels}px, 0px)" - cursor2 = editor.addCursorAtScreenPosition([6, 11]) + cursor2 = editor.addCursorAtScreenPosition([8, 11]) cursor3 = editor.addCursorAtScreenPosition([4, 10]) cursorNodes = node.querySelectorAll('.cursor') @@ -326,15 +326,15 @@ describe "EditorComponent", -> expect(cursorNodes[0].style['-webkit-transform']).toBe "translate3d(#{5 * charWidth}px, #{0 * lineHeightInPixels}px, 0px)" expect(cursorNodes[1].style['-webkit-transform']).toBe "translate3d(#{10 * charWidth}px, #{4 * lineHeightInPixels}px, 0px)" - verticalScrollbarNode.scrollTop = 2.5 * lineHeightInPixels + verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) horizontalScrollbarNode.scrollLeft = 3.5 * charWidth horizontalScrollbarNode.dispatchEvent(new UIEvent('scroll')) cursorNodes = node.querySelectorAll('.cursor') expect(cursorNodes.length).toBe 2 - expect(cursorNodes[0].style['-webkit-transform']).toBe "translate3d(#{(11 - 3.5) * charWidth}px, #{(6 - 2.5) * lineHeightInPixels}px, 0px)" - expect(cursorNodes[1].style['-webkit-transform']).toBe "translate3d(#{(10 - 3.5) * charWidth}px, #{(4 - 2.5) * lineHeightInPixels}px, 0px)" + expect(cursorNodes[0].style['-webkit-transform']).toBe "translate3d(#{(11 - 3.5) * charWidth}px, #{(8 - 4.5) * lineHeightInPixels}px, 0px)" + expect(cursorNodes[1].style['-webkit-transform']).toBe "translate3d(#{(10 - 3.5) * charWidth}px, #{(4 - 4.5) * lineHeightInPixels}px, 0px)" cursor3.destroy() cursorNodes = node.querySelectorAll('.cursor') @@ -364,6 +364,7 @@ describe "EditorComponent", -> expect(cursorsNode.classList.contains('blink-off')).toBe false advanceClock(component.props.cursorBlinkPeriod / 2) expect(cursorsNode.classList.contains('blink-off')).toBe true + advanceClock(component.props.cursorBlinkPeriod / 2) expect(cursorsNode.classList.contains('blink-off')).toBe false diff --git a/src/cursor-component.coffee b/src/cursor-component.coffee index 902be9d28..5fa7f10b2 100644 --- a/src/cursor-component.coffee +++ b/src/cursor-component.coffee @@ -6,8 +6,8 @@ CursorComponent = React.createClass displayName: 'CursorComponent' render: -> - {cursor, scrollTop, scrollLeft} = @props - {top, left, height, width} = cursor.getPixelRect() + {editor, screenRange, scrollTop, scrollLeft} = @props + {top, left, height, width} = editor.pixelRectForScreenRange(screenRange) top -= scrollTop left -= scrollLeft WebkitTransform = "translate3d(#{left}px, #{top}px, 0px)" diff --git a/src/cursors-component.coffee b/src/cursors-component.coffee index 4d3e14deb..8f81bc647 100644 --- a/src/cursors-component.coffee +++ b/src/cursors-component.coffee @@ -1,6 +1,6 @@ React = require 'react-atom-fork' {div} = require 'reactionary-atom-fork' -{debounce, toArray} = require 'underscore-plus' +{debounce, toArray, isEqualForProperties, isEqual} = require 'underscore-plus' SubscriberMixin = require './subscriber-mixin' CursorComponent = require './cursor-component' @@ -12,7 +12,7 @@ CursorsComponent = React.createClass cursorBlinkIntervalHandle: null render: -> - {editor, scrollTop, scrollLeft} = @props + {editor, cursorScreenRanges, scrollTop, scrollLeft} = @props {blinkOff} = @state className = 'cursors' @@ -20,10 +20,8 @@ CursorsComponent = React.createClass div {className}, if @isMounted() - for selection in editor.getSelections() - if selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection) - {cursor} = selection - CursorComponent({key: cursor.id, cursor, scrollTop, scrollLeft}) + for key, screenRange of cursorScreenRanges + CursorComponent({key, editor, screenRange, scrollTop, scrollLeft}) getInitialState: -> blinkOff: false @@ -34,8 +32,12 @@ CursorsComponent = React.createClass componentWillUnmount: -> @stopBlinkingCursors() - componentWillUpdate: ({cursorsMoved}) -> - @pauseCursorBlinking() if cursorsMoved + shouldComponentUpdate: (newProps, newState) -> + not isEqualForProperties(newProps, @props, 'cursorScreenRanges', 'scrollTop', 'scrollLeft') or + not newState.blinkOff is @state.blinkOff + + componentWillUpdate: (newProps) -> + @pauseCursorBlinking() if @props.cursorScreenRanges and not isEqual(newProps.cursorScreenRanges, @props.cursorScreenRanges) startBlinkingCursors: -> @toggleCursorBlinkHandle = setInterval(@toggleCursorBlink, @props.cursorBlinkPeriod / 2) if @isMounted() diff --git a/src/editor-component.coffee b/src/editor-component.coffee index b2958cc70..a86098068 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -45,6 +45,8 @@ EditorComponent = React.createClass if @isMounted() renderedRowRange = @getRenderedRowRange() [renderedStartRow, renderedEndRow] = renderedRowRange + cursorScreenRanges = @getCursorScreenRanges(renderedRowRange) + selectionScreenRanges = @getSelectionScreenRanges(renderedRowRange) scrollHeight = editor.getScrollHeight() scrollWidth = editor.getScrollWidth() scrollTop = editor.getScrollTop() @@ -79,11 +81,14 @@ EditorComponent = React.createClass onFocus: @onInputFocused onBlur: @onInputBlurred - CursorsComponent({editor, scrollTop, scrollLeft, @cursorsMoved, @selectionAdded, cursorBlinkPeriod, cursorBlinkResumeDelay}) + CursorsComponent { + editor, scrollTop, scrollLeft, cursorScreenRanges, cursorBlinkPeriod, cursorBlinkResumeDelay, + fontSize, fontFamily, lineHeightInPixels + } LinesComponent { ref: 'lines', editor, fontSize, fontFamily, lineHeight, lineHeightInPixels, showIndentGuide, renderedRowRange, @pendingChanges, scrollTop, scrollLeft, @scrollingVertically, - @selectionChanged, scrollHeight, scrollWidth, mouseWheelScreenRow, invisibles, + selectionScreenRanges, scrollHeight, scrollWidth, mouseWheelScreenRow, invisibles, visible, scrollViewHeight } @@ -159,13 +164,61 @@ EditorComponent = React.createClass componentDidUpdate: -> @pendingChanges.length = 0 @cursorsMoved = false - @selectionChanged = false - @selectionAdded = false @refreshingScrollbars = false @measureScrollbars() if @measuringScrollbars @pauseOverflowChangedEvents() @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 + + getSelectionScreenRanges: (renderedRowRange) -> + {editor} = @props + [renderedStartRow, renderedEndRow] = renderedRowRange + + selectionScreenRanges = {} + for selection, index in editor.getSelections() + # Rendering artifacts occur on the lines GPU layer if we remove the last selection + screenRange = selection.getScreenRange() + if index is 0 or (not screenRange.isEmpty() and screenRange.intersectsRowRange(renderedStartRow, renderedEndRow)) + selectionScreenRanges[selection.id] = screenRange + selectionScreenRanges + observeEditor: -> {editor} = @props @subscribe editor, 'batched-updates-started', @onBatchedUpdatesStarted @@ -549,12 +602,6 @@ EditorComponent = React.createClass # if the editor's content and dimensions require them to be visible. @requestUpdate() - requestUpdate: -> - if @batchingUpdates - @updateRequested = true - else - @forceUpdate() - pauseOverflowChangedEvents: -> @overflowChangedEventsPaused = true @resumeOverflowChangedEventsAfterDelay ?= debounce(@resumeOverflowChangedEvents, 500) @@ -672,23 +719,3 @@ EditorComponent = React.createClass top = clientY - scrollViewClientRect.top + editor.getScrollTop() left = clientX - scrollViewClientRect.left + editor.getScrollLeft() {top, left} - - 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} - - getRenderedRowRange: -> - {editor, lineOverdrawMargin} = @props - [visibleStartRow, visibleEndRow] = editor.getVisibleRowRange() - renderedStartRow = Math.max(0, visibleStartRow - lineOverdrawMargin) - renderedEndRow = Math.min(editor.getScreenLineCount(), visibleEndRow + lineOverdrawMargin) - [renderedStartRow, renderedEndRow] diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 4e2741214..cc90ab149 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -17,14 +17,14 @@ LinesComponent = React.createClass render: -> if @isMounted() - {editor, scrollTop, scrollLeft, scrollHeight, scrollWidth, lineHeightInPixels, scrollViewHeight} = @props + {editor, selectionScreenRanges, scrollTop, scrollLeft, scrollHeight, scrollWidth, lineHeightInPixels, scrollViewHeight} = @props style = height: Math.max(scrollHeight, scrollViewHeight) width: scrollWidth WebkitTransform: "translate3d(#{-scrollLeft}px, #{-scrollTop}px, 0px)" div {className: 'lines', style}, - SelectionsComponent({editor, lineHeightInPixels}) if @isMounted() + SelectionsComponent({editor, selectionScreenRanges, lineHeightInPixels}) if @isMounted() componentWillMount: -> @measuredLines = new WeakSet @@ -36,11 +36,10 @@ LinesComponent = React.createClass @measureLineHeightInPixelsAndCharWidth() shouldComponentUpdate: (newProps) -> - return true if newProps.selectionChanged return true unless isEqualForProperties(newProps, @props, - 'renderedRowRange', 'fontSize', 'fontFamily', 'lineHeight', 'lineHeightInPixels', - 'scrollTop', 'scrollLeft', 'showIndentGuide', 'scrollingVertically', 'invisibles', - 'visible', 'scrollViewHeight', 'mouseWheelScreenRow' + 'renderedRowRange', 'selectionScreenRanges', 'fontSize', 'fontFamily', 'lineHeight', + 'lineHeightInPixels', 'scrollTop', 'scrollLeft', 'showIndentGuide', 'scrollingVertically', + 'invisibles', 'visible', 'scrollViewHeight', 'mouseWheelScreenRow' ) {renderedRowRange, pendingChanges} = newProps diff --git a/src/selections-component.coffee b/src/selections-component.coffee index a32f9bdc7..80744755a 100644 --- a/src/selections-component.coffee +++ b/src/selections-component.coffee @@ -1,5 +1,6 @@ React = require 'react-atom-fork' {div} = require 'reactionary-atom-fork' +{isEqual} = require 'underscore-plus' SelectionComponent = require './selection-component' module.exports = @@ -10,34 +11,15 @@ SelectionsComponent = React.createClass div className: 'selections', @renderSelections() renderSelections: -> - {editor, lineHeightInPixels} = @props + {editor, selectionScreenRanges, lineHeightInPixels} = @props selectionComponents = [] - for selectionId, screenRange of @selectionRanges + for selectionId, screenRange of selectionScreenRanges selectionComponents.push(SelectionComponent({key: selectionId, screenRange, editor, lineHeightInPixels})) selectionComponents componentWillMount: -> @selectionRanges = {} - shouldComponentUpdate: -> - {editor} = @props - oldSelectionRanges = @selectionRanges - newSelectionRanges = {} - @selectionRanges = newSelectionRanges - - for selection, index in editor.getSelections() - # Rendering artifacts occur on the lines GPU layer if we remove the last selection - if index is 0 or (not selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection)) - newSelectionRanges[selection.id] = selection.getScreenRange() - - for id, range of newSelectionRanges - if oldSelectionRanges.hasOwnProperty(id) - return true unless range.isEqual(oldSelectionRanges[id]) - else - return true - - for id of oldSelectionRanges - return true unless newSelectionRanges.hasOwnProperty(id) - - false + shouldComponentUpdate: (newProps) -> + not isEqual(newProps.selectionScreenRanges, @props.selectionScreenRanges)