diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index fd068f656..de7439917 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -19,7 +19,7 @@ describe "EditorComponent", -> spyOn(window, "clearInterval").andCallFake window.fakeClearInterval delayAnimationFrames = false - nextAnimationFrame = null + nextAnimationFrame = -> throw new Error('No animation frame requested') spyOn(window, 'requestAnimationFrame').andCallFake (fn) -> if delayAnimationFrames nextAnimationFrame = fn @@ -916,7 +916,7 @@ describe "EditorComponent", -> expect(inputNode.offsetTop).toBe 0 expect(inputNode.offsetLeft).toBe 0 - describe "mouse interactions", -> + describe "mouse interactions on the scrollView", -> linesNode = null beforeEach -> @@ -1017,12 +1017,100 @@ describe "EditorComponent", -> linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([4, 8]), {target})) expect(editor.isFoldedAtBufferRow 4).toBe false - clientCoordinatesForScreenPosition = (screenPosition) -> - positionOffset = editor.pixelPositionForScreenPosition(screenPosition) - scrollViewClientRect = node.querySelector('.scroll-view').getBoundingClientRect() - clientX = scrollViewClientRect.left + positionOffset.left - editor.getScrollLeft() - clientY = scrollViewClientRect.top + positionOffset.top - editor.getScrollTop() - {clientX, clientY} + describe "mouse interactions on the gutter", -> + gutterNode = null + + beforeEach -> + gutterNode = node.querySelector('.gutter') + + describe "when the gutter is clicked", -> + it "moves the cursor to the beginning of the clicked row", -> + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(4))) + expect(editor.getCursorScreenPosition()).toEqual [4, 0] + + describe "when the gutter is shift-clicked", -> + beforeEach -> + editor.setSelectedScreenRange([[3, 4], [4, 5]]) + + describe "when the clicked row is before the current selection's tail", -> + it "selects to the beginning of the clicked row", -> + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), shiftKey: true)) + expect(editor.getSelectedScreenRange()).toEqual [[1, 0], [3, 4]] + + describe "when the clicked row is after the current selection's tail", -> + it "selects to the beginning of the row following the clicked row", -> + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6), shiftKey: true)) + expect(editor.getSelectedScreenRange()).toEqual [[3, 4], [7, 0]] + + describe "when the gutter is clicked and dragged", -> + beforeEach -> + delayAnimationFrames = true + + describe "when dragging downward", -> + it "selects the rows between the start and end of the drag", -> + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2))) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6))) + nextAnimationFrame() + gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(6))) + expect(editor.getSelectedScreenRange()).toEqual [[2, 0], [7, 0]] + + describe "when dragging upward", -> + it "selects the rows between the start and end of the drag", -> + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6))) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2))) + nextAnimationFrame() + gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(2))) + expect(editor.getSelectedScreenRange()).toEqual [[2, 0], [7, 0]] + + describe "when the gutter is shift-clicked and dragged", -> + beforeEach -> + delayAnimationFrames = true + + describe "when the shift-click is below the existing selection's tail", -> + describe "when dragging downward", -> + it "selects the rows between the existing selection's tail and the end of the drag", -> + editor.setSelectedScreenRange([[3, 4], [4, 5]]) + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(7), shiftKey: true)) + + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(8))) + nextAnimationFrame() + expect(editor.getSelectedScreenRange()).toEqual [[3, 4], [9, 0]] + + describe "when dragging upward", -> + it "selects the rows between the end of the drag and the tail of the existing selection", -> + editor.setSelectedScreenRange([[4, 4], [5, 5]]) + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(7), shiftKey: true)) + + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(5))) + nextAnimationFrame() + expect(editor.getSelectedScreenRange()).toEqual [[4, 4], [6, 0]] + + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(1))) + nextAnimationFrame() + expect(editor.getSelectedScreenRange()).toEqual [[1, 0], [4, 4]] + + describe "when the shift-click is above the existing selection's tail", -> + describe "when dragging upward", -> + it "selects the rows between the end of the drag and the tail of the existing selection", -> + editor.setSelectedScreenRange([[4, 4], [5, 5]]) + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2), shiftKey: true)) + + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(1))) + nextAnimationFrame() + expect(editor.getSelectedScreenRange()).toEqual [[1, 0], [4, 4]] + + describe "when dragging downward", -> + it "selects the rows between the existing selection's tail and the end of the drag", -> + editor.setSelectedScreenRange([[3, 4], [4, 5]]) + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), shiftKey: true)) + + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2))) + nextAnimationFrame() + expect(editor.getSelectedScreenRange()).toEqual [[2, 0], [3, 4]] + + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(8))) + nextAnimationFrame() + expect(editor.getSelectedScreenRange()).toEqual [[3, 4], [9, 0]] describe "focus handling", -> inputNode = null @@ -1443,3 +1531,17 @@ describe "EditorComponent", -> Object.defineProperty(event, 'target', get: -> properties.target) Object.defineProperty(event, 'srcObject', get: -> properties.target) event + + clientCoordinatesForScreenPosition = (screenPosition) -> + positionOffset = editor.pixelPositionForScreenPosition(screenPosition) + scrollViewClientRect = node.querySelector('.scroll-view').getBoundingClientRect() + clientX = scrollViewClientRect.left + positionOffset.left - editor.getScrollLeft() + clientY = scrollViewClientRect.top + positionOffset.top - editor.getScrollTop() + {clientX, clientY} + + clientCoordinatesForScreenRowInGutter = (screenRow) -> + positionOffset = editor.pixelPositionForScreenPosition([screenRow, 1]) + gutterClientRect = node.querySelector('.gutter').getBoundingClientRect() + clientX = gutterClientRect.left + positionOffset.left - editor.getScrollLeft() + clientY = gutterClientRect.top + positionOffset.top - editor.getScrollTop() + {clientX, clientY} diff --git a/src/editor-component.coffee b/src/editor-component.coffee index d520ad1dc..7398f7632 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -79,8 +79,8 @@ EditorComponent = React.createClass div className: className, style: {fontSize, lineHeight, fontFamily}, tabIndex: -1, GutterComponent { - ref: 'gutter', onWidthChanged: @onGutterWidthChanged, lineDecorations, defaultCharWidth, - editor, renderedRowRange, maxLineNumberDigits, scrollViewHeight, + ref: 'gutter', onMouseDown: @onGutterMouseDown, onWidthChanged: @onGutterWidthChanged, + lineDecorations, defaultCharWidth, editor, renderedRowRange, maxLineNumberDigits, scrollViewHeight, scrollTop, scrollHeight, lineHeightInPixels, @pendingChanges, mouseWheelScreenRow } @@ -504,7 +504,46 @@ EditorComponent = React.createClass when 2 then editor.selectWord() when 3 then editor.selectLine() - @selectToMousePositionUntilMouseUp(event) + @handleDragUntilMouseUp event, (screenPosition) -> + editor.selectToScreenPosition(screenPosition) + + onGutterMouseDown: (event) -> + return unless event.button is 0 # only handle the left mouse button + + if event.shiftKey + @onGutterShiftClick(event) + else + @onGutterClick(event) + + onGutterClick: (event) -> + {editor} = @props + clickedRow = @screenPositionForMouseEvent(event).row + + editor.setCursorScreenPosition([clickedRow, 0]) + + @handleDragUntilMouseUp event, (screenPosition) -> + dragRow = screenPosition.row + if dragRow < clickedRow # dragging up + editor.setSelectedScreenRange([[dragRow, 0], [clickedRow + 1, 0]]) + else + editor.setSelectedScreenRange([[clickedRow, 0], [dragRow + 1, 0]]) + + onGutterShiftClick: (event) -> + {editor} = @props + clickedRow = @screenPositionForMouseEvent(event).row + tailPosition = editor.getSelection().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]) + else + editor.setSelectedScreenRange([tailPosition, [dragRow + 1, 0]]) onStylesheetsChanged: (stylesheet) -> @refreshScrollbars() if @containsScrollbarSelector(stylesheet) @@ -564,15 +603,15 @@ EditorComponent = React.createClass onCharacterWidthsChanged: (@scopedCharacterWidthsChangeCount) -> @requestUpdate() - selectToMousePositionUntilMouseUp: (event) -> + handleDragUntilMouseUp: (event, dragHandler) -> {editor} = @props dragging = false lastMousePosition = {} - animationLoop = => requestAnimationFrame => if dragging - @selectToMousePosition(lastMousePosition) + screenPosition = @screenPositionForMouseEvent(lastMousePosition) + dragHandler(screenPosition) animationLoop() onMouseMove = (event) -> @@ -596,9 +635,6 @@ EditorComponent = React.createClass window.addEventListener('mousemove', onMouseMove) window.addEventListener('mouseup', onMouseUp) - selectToMousePosition: (event) -> - @props.editor.selectToScreenPosition(@screenPositionForMouseEvent(event)) - requestScrollViewMeasurement: -> return if @measurementPending diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index 0f9a55c75..5fc1a1730 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -15,9 +15,9 @@ GutterComponent = React.createClass measuredWidth: null render: -> - {scrollHeight, scrollViewHeight, scrollTop} = @props + {scrollHeight, scrollViewHeight, scrollTop, onMouseDown} = @props - div className: 'gutter', onClick: @onClick, + div className: 'gutter', onClick: @onClick, onMouseDown: onMouseDown, # The line-numbers div must have the 'editor-colors' class so it has an # opaque background to avoid sub-pixel anti-aliasing problems on the GPU div className: 'gutter line-numbers editor-colors', ref: 'lineNumbers', style: diff --git a/src/selection.coffee b/src/selection.coffee index 850725fe6..c27f63f92 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -91,6 +91,18 @@ class Selection extends Model end = Math.max(start, end - 1) if range.end.column == 0 [start, end] + getTailScreenPosition: -> + @marker.getTailScreenPosition() + + getTailBufferPosition: -> + @marker.getTailBufferPosition() + + getHeadScreenPosition: -> + @marker.getHeadScreenPosition() + + getHeadBufferPosition: -> + @marker.getHeadBufferPosition() + autoscroll: -> @editor.scrollToScreenRange(@getScreenRange())