diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index e01fbb853..0fa4d9b9a 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -47,7 +47,7 @@ describe "EditorComponent", -> node.style.height = editor.getLineCount() * lineHeightInPixels + 'px' node.style.width = '1000px' - component.measureHeightAndWidth() + component.measureScrollView() afterEach -> contentNode.style.width = '' @@ -55,7 +55,7 @@ describe "EditorComponent", -> describe "line rendering", -> it "renders the currently-visible lines plus the overdraw margin", -> node.style.height = 4.5 * lineHeightInPixels + 'px' - component.measureHeightAndWidth() + component.measureScrollView() linesNode = node.querySelector('.lines') expect(linesNode.style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" @@ -108,7 +108,7 @@ describe "EditorComponent", -> it "updates the top position of lines when the font family changes", -> # Can't find a font that changes the line height, but we think one might exist - linesComponent = component.refs.scrollView.refs.lines + linesComponent = component.refs.lines spyOn(linesComponent, 'measureLineHeightInPixelsAndCharWidth').andCallFake -> editor.setLineHeightInPixels(10) initialLineHeightInPixels = editor.getLineHeightInPixels() @@ -122,7 +122,7 @@ describe "EditorComponent", -> it "renders the .lines div at the full height of the editor if there aren't enough lines to scroll vertically", -> editor.setText('') node.style.height = '300px' - component.measureHeightAndWidth() + component.measureScrollView() linesNode = node.querySelector('.lines') expect(linesNode.offsetHeight).toBe 300 @@ -166,7 +166,7 @@ describe "EditorComponent", -> editor.setText "a line that wraps " editor.setSoftWrap(true) node.style.width = 15 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureScrollView() it "doesn't show end of line invisibles at the end of wrapped lines", -> expect(component.lineNodeForScreenRow(0).textContent).toBe "a line that " @@ -230,7 +230,7 @@ describe "EditorComponent", -> describe "gutter rendering", -> it "renders the currently-visible line numbers", -> node.style.height = 4.5 * lineHeightInPixels + 'px' - component.measureHeightAndWidth() + component.measureScrollView() expect(node.querySelectorAll('.line-number').length).toBe 6 + 2 + 1 # line overdraw margin below + dummy line number expect(component.lineNumberNodeForScreenRow(0).textContent).toBe "#{nbsp}1" @@ -270,7 +270,7 @@ describe "EditorComponent", -> editor.setSoftWrap(true) node.style.height = 4.5 * lineHeightInPixels + 'px' node.style.width = 30 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureScrollView() expect(node.querySelectorAll('.line-number').length).toBe 6 + lineOverdrawMargin + 1 # 1 dummy line node expect(component.lineNumberNodeForScreenRow(0).textContent).toBe "#{nbsp}1" @@ -309,7 +309,7 @@ describe "EditorComponent", -> node.style.height = 4.5 * lineHeightInPixels + 'px' node.style.width = 20 * lineHeightInPixels + 'px' - component.measureHeightAndWidth() + component.measureScrollView() cursorNodes = node.querySelectorAll('.cursor') expect(cursorNodes.length).toBe 1 @@ -457,7 +457,7 @@ describe "EditorComponent", -> inputNode = node.querySelector('.hidden-input') node.style.height = 5 * lineHeightInPixels + 'px' node.style.width = 10 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureScrollView() expect(editor.getCursorScreenPosition()).toEqual [0, 0] editor.setScrollTop(3 * lineHeightInPixels) @@ -503,7 +503,7 @@ describe "EditorComponent", -> it "moves the cursor to the nearest screen position", -> node.style.height = 4.5 * lineHeightInPixels + 'px' node.style.width = 10 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureScrollView() editor.setScrollTop(3.5 * lineHeightInPixels) editor.setScrollLeft(2 * charWidth) @@ -616,7 +616,7 @@ describe "EditorComponent", -> describe "scrolling", -> it "updates the vertical scrollbar when the scrollTop is changed in the model", -> node.style.height = 4.5 * lineHeightInPixels + 'px' - component.measureHeightAndWidth() + component.measureScrollView() expect(verticalScrollbarNode.scrollTop).toBe 0 @@ -625,7 +625,7 @@ describe "EditorComponent", -> it "updates the horizontal scrollbar and the x transform of the lines based on the scrollLeft of the model", -> node.style.width = 30 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureScrollView() linesNode = node.querySelector('.lines') expect(linesNode.style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" @@ -637,7 +637,7 @@ describe "EditorComponent", -> it "updates the scrollLeft of the model when the scrollLeft of the horizontal scrollbar changes", -> node.style.width = 30 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureScrollView() expect(editor.getScrollLeft()).toBe 0 horizontalScrollbarNode.scrollLeft = 100 @@ -648,7 +648,7 @@ describe "EditorComponent", -> it "does not obscure the last line with the horizontal scrollbar", -> node.style.height = 4.5 * lineHeightInPixels + 'px' node.style.width = 10 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureScrollView() editor.setScrollBottom(editor.getScrollHeight()) lastLineNode = component.lineNodeForScreenRow(editor.getLastScreenRow()) bottomOfLastLine = lastLineNode.getBoundingClientRect().bottom @@ -657,7 +657,7 @@ describe "EditorComponent", -> # Scroll so there's no space below the last line when the horizontal scrollbar disappears node.style.width = 100 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureScrollView() bottomOfLastLine = lastLineNode.getBoundingClientRect().bottom bottomOfEditor = node.getBoundingClientRect().bottom expect(bottomOfLastLine).toBe bottomOfEditor @@ -665,7 +665,7 @@ describe "EditorComponent", -> it "does not obscure the last character of the longest line with the vertical scrollbar", -> node.style.height = 7 * lineHeightInPixels + 'px' node.style.width = 10 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureScrollView() editor.setScrollLeft(Infinity) @@ -679,19 +679,19 @@ describe "EditorComponent", -> node.style.height = 4.5 * lineHeightInPixels + 'px' node.style.width = '1000px' - component.measureHeightAndWidth() + component.measureScrollView() expect(verticalScrollbarNode.style.display).toBe '' expect(horizontalScrollbarNode.style.display).toBe 'none' node.style.width = 10 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureScrollView() expect(verticalScrollbarNode.style.display).toBe '' expect(horizontalScrollbarNode.style.display).toBe '' node.style.height = 20 * lineHeightInPixels + 'px' - component.measureHeightAndWidth() + component.measureScrollView() expect(verticalScrollbarNode.style.display).toBe 'none' expect(horizontalScrollbarNode.style.display).toBe '' @@ -699,7 +699,7 @@ describe "EditorComponent", -> it "makes the dummy scrollbar divs only as tall/wide as the actual scrollbars", -> node.style.height = 4 * lineHeightInPixels + 'px' node.style.width = 10 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureScrollView() atom.themes.applyStylesheet "test", """ ::-webkit-scrollbar { @@ -722,19 +722,19 @@ describe "EditorComponent", -> node.style.height = 4.5 * lineHeightInPixels + 'px' node.style.width = '1000px' - component.measureHeightAndWidth() + component.measureScrollView() expect(verticalScrollbarNode.style.bottom).toBe '' expect(horizontalScrollbarNode.style.right).toBe verticalScrollbarNode.offsetWidth + 'px' expect(scrollbarCornerNode.style.display).toBe 'none' node.style.width = 10 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureScrollView() expect(verticalScrollbarNode.style.bottom).toBe horizontalScrollbarNode.offsetHeight + 'px' expect(horizontalScrollbarNode.style.right).toBe verticalScrollbarNode.offsetWidth + 'px' expect(scrollbarCornerNode.style.display).toBe '' node.style.height = 20 * lineHeightInPixels + 'px' - component.measureHeightAndWidth() + component.measureScrollView() expect(verticalScrollbarNode.style.bottom).toBe horizontalScrollbarNode.offsetHeight + 'px' expect(horizontalScrollbarNode.style.right).toBe '' expect(scrollbarCornerNode.style.display).toBe 'none' @@ -742,7 +742,7 @@ describe "EditorComponent", -> it "accounts for the width of the gutter in the scrollWidth of the horizontal scrollbar", -> gutterNode = node.querySelector('.gutter') node.style.width = 10 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureScrollView() expect(horizontalScrollbarNode.scrollWidth).toBe gutterNode.offsetWidth + editor.getScrollWidth() @@ -752,7 +752,7 @@ describe "EditorComponent", -> it "updates the scrollLeft or scrollTop on mousewheel events depending on which delta is greater (x or y)", -> node.style.height = 4.5 * lineHeightInPixels + 'px' node.style.width = 20 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureScrollView() expect(verticalScrollbarNode.scrollTop).toBe 0 expect(horizontalScrollbarNode.scrollLeft).toBe 0 @@ -769,7 +769,7 @@ describe "EditorComponent", -> it "keeps the line on the DOM if it is scrolled off-screen", -> node.style.height = 4.5 * lineHeightInPixels + 'px' node.style.width = 20 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureScrollView() lineNode = node.querySelector('.line') wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -500) @@ -781,7 +781,7 @@ describe "EditorComponent", -> it "does not set the mouseWheelScreenRow if scrolling horizontally", -> node.style.height = 4.5 * lineHeightInPixels + 'px' node.style.width = 20 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureScrollView() lineNode = node.querySelector('.line') wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 10, wheelDeltaY: 0) @@ -825,7 +825,7 @@ describe "EditorComponent", -> it "keeps the line number on the DOM if it is scrolled off-screen", -> node.style.height = 4.5 * lineHeightInPixels + 'px' node.style.width = 20 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureScrollView() lineNumberNode = node.querySelectorAll('.line-number')[1] wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -500) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index ede23e203..b2958cc70 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -4,7 +4,9 @@ React = require 'react-atom-fork' scrollbarStyle = require 'scrollbar-style' GutterComponent = require './gutter-component' -EditorScrollViewComponent = require './editor-scroll-view-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' @@ -30,6 +32,9 @@ EditorComponent = React.createClass pendingHorizontalScrollDelta: 0 mouseWheelScreenRow: null mouseWheelScreenRowClearDelay: 150 + scrollViewMeasurementRequested: false + overflowChangedEventsPaused: false + overflowChangedWhilePaused: false render: -> {focused, fontSize, lineHeight, fontFamily, showIndentGuide, showInvisibles, visible} = @state @@ -50,6 +55,8 @@ EditorComponent = React.createClass verticalScrollbarWidth = editor.getVerticalScrollbarWidth() verticallyScrollable = editor.verticallyScrollable() horizontallyScrollable = editor.horizontallyScrollable() + hiddenInputStyle = @getHiddenInputPosition() + hiddenInputStyle.WebkitTransform = 'translateZ(0)' if @mouseWheelScreenRow? and not (renderedStartRow <= @mouseWheelScreenRow < renderedEndRow) mouseWheelScreenRow = @mouseWheelScreenRow @@ -63,14 +70,22 @@ EditorComponent = React.createClass @pendingChanges, onWidthChanged: @onGutterWidthChanged, mouseWheelScreenRow } - EditorScrollViewComponent { - ref: 'scrollView', editor, fontSize, fontFamily, showIndentGuide, - lineHeight, lineHeightInPixels, renderedRowRange, @pendingChanges, - scrollTop, scrollLeft, scrollHeight, scrollWidth, @scrollingVertically, - @cursorsMoved, @selectionChanged, @selectionAdded, cursorBlinkPeriod, - cursorBlinkResumeDelay, @onInputFocused, @onInputBlurred, mouseWheelScreenRow, - invisibles, visible, scrollViewHeight, focused - } + div ref: 'scrollView', className: 'scroll-view', onMouseDown: @onMouseDown, + InputComponent + ref: 'input' + className: 'hidden-input' + style: hiddenInputStyle + onInput: @onInput + onFocus: @onInputFocused + onBlur: @onInputBlurred + + CursorsComponent({editor, scrollTop, scrollLeft, @cursorsMoved, @selectionAdded, cursorBlinkPeriod, cursorBlinkResumeDelay}) + LinesComponent { + ref: 'lines', editor, fontSize, fontFamily, lineHeight, lineHeightInPixels, + showIndentGuide, renderedRowRange, @pendingChanges, scrollTop, scrollLeft, @scrollingVertically, + @selectionChanged, scrollHeight, scrollWidth, mouseWheelScreenRow, invisibles, + visible, scrollViewHeight + } ScrollbarComponent ref: 'verticalScrollbar' @@ -104,13 +119,6 @@ EditorComponent = React.createClass height: horizontalScrollbarHeight width: verticalScrollbarWidth - getRenderedRowRange: -> - {editor, lineOverdrawMargin} = @props - [visibleStartRow, visibleEndRow] = editor.getVisibleRowRange() - renderedStartRow = Math.max(0, visibleStartRow - lineOverdrawMargin) - renderedEndRow = Math.min(editor.getScreenLineCount(), visibleEndRow + lineOverdrawMargin) - [renderedStartRow, renderedEndRow] - getInitialState: -> visible: true @@ -132,11 +140,18 @@ EditorComponent = React.createClass @subscribe atom.themes, 'stylesheet-added stylsheet-removed', @onStylesheetsChanged @subscribe scrollbarStyle.changes, @refreshScrollbars @props.editor.setVisible(true) + + scrollViewNode = @refs.scrollView.getDOMNode() + scrollViewNode.addEventListener 'overflowchanged', @onScrollViewOverflowChanged + scrollViewNode.addEventListener 'scroll', @onScrollViewScroll + window.addEventListener('resize', @onWindowResize) + @measureScrollView() + @requestUpdate() componentWillUnmount: -> @unsubscribe() - @getDOMNode().removeEventListener 'mousewheel', @onMouseWheel + window.removeEventListener('resize', @onWindowResize) componentWillUpdate: -> @props.parentView.trigger 'cursor:moved' if @cursorsMoved @@ -148,6 +163,7 @@ EditorComponent = React.createClass @selectionAdded = false @refreshingScrollbars = false @measureScrollbars() if @measuringScrollbars + @pauseOverflowChangedEvents() @props.parentView.trigger 'editor:display-updated' observeEditor: -> @@ -284,49 +300,8 @@ EditorComponent = React.createClass @subscribe atom.config.observe 'editor.invisibles', @setInvisibles @subscribe atom.config.observe 'editor.showInvisibles', @setShowInvisibles - 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) - - setFontSize: (fontSize) -> - @setState({fontSize}) - - setLineHeight: (lineHeight) -> - @setState({lineHeight}) - - setFontFamily: (fontFamily) -> - @setState({fontFamily}) - - 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}) - onFocus: -> - @refs.scrollView.focus() + @refs.input.focus() onInputFocused: -> @setState(focused: true) @@ -382,49 +357,51 @@ EditorComponent = React.createClass @pendingVerticalScrollDelta = 0 @pendingHorizontalScrollDelta = 0 - clearMouseWheelScreenRow: -> - if @mouseWheelScreenRow? - @mouseWheelScreenRow = null - @requestUpdate() + onScrollViewOverflowChanged: -> + if @overflowChangedEventsPaused + @overflowChangedWhilePaused = true + else + @requestScrollViewMeasurement() - clearMouseWheelScreenRowAfterDelay: null # created lazily + onWindowResize: -> + @requestScrollViewMeasurement() - screenRowForNode: (node) -> - while node isnt document - if screenRow = node.dataset.screenRow - return parseInt(screenRow) - node = node.parentNode - null + 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 + + onInput: (char, replaceLastCharacter) -> + {editor} = @props + + if replaceLastCharacter + editor.transact -> + editor.selectLeft() + editor.insertText(char) + else + editor.insertText(char) + + onMouseDown: (event) -> + {editor} = @props + {detail, shiftKey, metaKey} = event + screenPosition = @screenPositionForMouseEvent(event) + + 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) - 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() - onBatchedUpdatesStarted: -> @batchingUpdates = true @@ -456,15 +433,15 @@ EditorComponent = React.createClass onScrollTopChanged: -> @scrollingVertically = true @requestUpdate() - @stopScrollingAfterDelay ?= debounce(@onStoppedScrolling, 100) - @stopScrollingAfterDelay() + @onStoppedScrollingAfterDelay ?= debounce(@onStoppedScrolling, 100) + @onStoppedScrollingAfterDelay() onStoppedScrolling: -> @scrollingVertically = false @mouseWheelScreenRow = null @requestUpdate() - stopScrollingAfterDelay: null # created lazily + onStoppedScrollingAfterDelay: null # created lazily onCursorsMoved: -> @cursorsMoved = true @@ -472,22 +449,145 @@ EditorComponent = React.createClass onGutterWidthChanged: (@gutterWidth) -> @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 + + 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 + + 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() + requestUpdate: -> if @batchingUpdates @updateRequested = true else @forceUpdate() - measureHeightAndWidth: -> - @refs.scrollView.measureHeightAndWidth() + pauseOverflowChangedEvents: -> + @overflowChangedEventsPaused = true + @resumeOverflowChangedEventsAfterDelay ?= debounce(@resumeOverflowChangedEvents, 500) + @resumeOverflowChangedEventsAfterDelay() + + resumeOverflowChangedEvents: -> + if @overflowChangedWhilePaused + @overflowChangedWhilePaused = false + @requestScrollViewMeasurement() + + resumeOverflowChangedEventsAfterDelay: null + + clearMouseWheelScreenRow: -> + if @mouseWheelScreenRow? + @mouseWheelScreenRow = null + @requestUpdate() + + clearMouseWheelScreenRowAfterDelay: null # created lazily consolidateSelections: (e) -> e.abortKeyBinding() unless @props.editor.consolidateSelections() - lineNodeForScreenRow: (screenRow) -> @refs.scrollView.lineNodeForScreenRow(screenRow) + 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) @@ -528,3 +628,67 @@ EditorComponent = React.createClass ReactPerf.printExclusive() console.log "Wasted" ReactPerf.printWasted() + + setFontSize: (fontSize) -> + @setState({fontSize}) + + setLineHeight: (lineHeight) -> + @setState({lineHeight}) + + setFontFamily: (fontFamily) -> + @setState({fontFamily}) + + 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}) + + 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} + + 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/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee deleted file mode 100644 index 92bc4a04e..000000000 --- a/src/editor-scroll-view-component.coffee +++ /dev/null @@ -1,203 +0,0 @@ -React = require 'react-atom-fork' -{div} = require 'reactionary-atom-fork' -{debounce} = require 'underscore-plus' - -InputComponent = require './input-component' -LinesComponent = require './lines-component' -CursorsComponent = require './cursors-component' -SelectionsComponent = require './selections-component' - -module.exports = -EditorScrollViewComponent = React.createClass - displayName: 'EditorScrollViewComponent' - - measurementPending: false - overflowChangedEventsPaused: false - overflowChangedWhilePaused: false - - render: -> - {editor, fontSize, fontFamily, lineHeight, lineHeightInPixels, showIndentGuide, invisibles, visible} = @props - {renderedRowRange, pendingChanges, scrollTop, scrollLeft, scrollHeight, scrollWidth, scrollViewHeight, scrollingVertically, mouseWheelScreenRow} = @props - {selectionChanged, selectionAdded, cursorBlinkPeriod, cursorBlinkResumeDelay, cursorsMoved, onInputFocused, onInputBlurred} = @props - - if @isMounted() - inputStyle = @getHiddenInputPosition() - inputStyle.WebkitTransform = 'translateZ(0)' - - div className: 'scroll-view', onMouseDown: @onMouseDown, - InputComponent - ref: 'input' - className: 'hidden-input' - style: inputStyle - onInput: @onInput - onFocus: onInputFocused - onBlur: onInputBlurred - - CursorsComponent({editor, scrollTop, scrollLeft, cursorsMoved, selectionAdded, cursorBlinkPeriod, cursorBlinkResumeDelay}) - LinesComponent { - ref: 'lines', editor, fontSize, fontFamily, lineHeight, lineHeightInPixels, - showIndentGuide, renderedRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically, - selectionChanged, scrollHeight, scrollWidth, mouseWheelScreenRow, invisibles, - visible, scrollViewHeight - } - - componentDidMount: -> - node = @getDOMNode() - - node.addEventListener 'overflowchanged', @onOverflowChanged - window.addEventListener('resize', @onWindowResize) - - node.addEventListener 'scroll', -> - console.warn "EditorScrollView scroll position changed, and it shouldn't have. If you can reproduce this, please report it." - node.scrollTop = 0 - node.scrollLeft = 0 - - @measureHeightAndWidth() - - componentDidUnmount: -> - window.removeEventListener('resize', @onWindowResize) - - componentDidUpdate: -> - @pauseOverflowChangedEvents() - - onOverflowChanged: -> - if @overflowChangedEventsPaused - @overflowChangedWhilePaused = true - else - @requestMeasurement() - - onWindowResize: -> - @requestMeasurement() - - pauseOverflowChangedEvents: -> - @overflowChangedEventsPaused = true - @resumeOverflowChangedEventsAfterDelay ?= debounce(@resumeOverflowChangedEvents, 500) - @resumeOverflowChangedEventsAfterDelay() - - resumeOverflowChangedEvents: -> - if @overflowChangedWhilePaused - @overflowChangedWhilePaused = false - @requestMeasurement() - - resumeOverflowChangedEventsAfterDelay: null - - requestMeasurement: -> - return if @measurementPending - - @measurementPending = true - requestAnimationFrame => - @measurementPending = false - @measureHeightAndWidth() - - onInput: (char, replaceLastCharacter) -> - {editor} = @props - - if replaceLastCharacter - editor.transact -> - editor.selectLeft() - editor.insertText(char) - else - editor.insertText(char) - - onMouseDown: (event) -> - {editor} = @props - {detail, shiftKey, metaKey} = event - screenPosition = @screenPositionForMouseEvent(event) - - 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) - - 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)) - - screenPositionForMouseEvent: (event) -> - pixelPosition = @pixelPositionForMouseEvent(event) - @props.editor.screenPositionForPixelPosition(pixelPosition) - - pixelPositionForMouseEvent: (event) -> - {editor} = @props - {clientX, clientY} = event - - editorClientRect = @getDOMNode().getBoundingClientRect() - top = clientY - editorClientRect.top + editor.getScrollTop() - left = clientX - editorClientRect.left + editor.getScrollLeft() - {top, left} - - getHiddenInputPosition: -> - {editor, focused} = @props - 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} - - # 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} = @props - node = @getDOMNode() - editorNode = node.parentNode - {position} = getComputedStyle(editorNode) - {width, height} = editorNode.style - - if position is 'absolute' or height - clientHeight = node.clientHeight - editor.setHeight(clientHeight) if clientHeight > 0 - - if position is 'absolute' or width - clientWidth = node.clientWidth - editor.setWidth(clientWidth) if clientWidth > 0 - - focus: -> - @refs.input.focus() - - lineNodeForScreenRow: (screenRow) -> @refs.lines.lineNodeForScreenRow(screenRow)