diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index 5c2bf079b..3feb70da7 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -1850,6 +1850,40 @@ describe "TextEditorComponent", -> nextAnimationFrame() expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [10, 0]] + it "autoscrolls when the cursor exceeds the boundaries of the editor", -> + wrapperNode.style.height = '100px' + wrapperNode.style.width = '100px' + component.measureDimensions() + nextAnimationFrame() + + expect(editor.getScrollTop()).toBe(0) + expect(editor.getScrollLeft()).toBe(0) + + linesNode.dispatchEvent(buildMouseEvent('mousedown', {clientX: 0, clientY: 0}, which: 1)) + linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: 150, clientY: 50}, which: 1)) + nextAnimationFrame() + + expect(editor.getScrollTop()).toBe(0) + expect(editor.getScrollLeft()).toBeGreaterThan(0) + + linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: 150, clientY: 150}, which: 1)) + nextAnimationFrame() + expect(editor.getScrollTop()).toBeGreaterThan(0) + + previousScrollTop = editor.getScrollTop() + previousScrollLeft = editor.getScrollLeft() + + linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: -20, clientY: 50}, which: 1)) + nextAnimationFrame() + + expect(editor.getScrollTop()).toBe(previousScrollTop) + expect(editor.getScrollLeft()).toBeLessThan(previousScrollLeft) + + linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: -20, clientY: -20}, which: 1)) + nextAnimationFrame() + + expect(editor.getScrollTop()).toBeLessThan(previousScrollTop) + it "stops selecting if the mouse is dragged into the dev tools", -> linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), which: 1)) linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), which: 1)) @@ -1928,7 +1962,7 @@ describe "TextEditorComponent", -> linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([11, 11]), which: 1)) nextAnimationFrame() - expect(editor.getSelectedScreenRange()).toEqual [[5, 6], [11, 13]] + expect(editor.getSelectedScreenRange()).toEqual [[5, 6], [12, 2]] maximalScrollTop = editor.getScrollTop() @@ -1955,7 +1989,7 @@ describe "TextEditorComponent", -> linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([11, 11]), which: 1)) nextAnimationFrame() - expect(editor.getSelectedScreenRange()).toEqual [[5, 0], [12, 0]] + expect(editor.getSelectedScreenRange()).toEqual [[5, 0], [12, 2]] maximalScrollTop = editor.getScrollTop() @@ -2052,14 +2086,15 @@ describe "TextEditorComponent", -> nextAnimationFrame() expect(editor.getLastSelection().isReversed()).toBe false - it "autoscrolls to the cursor position, but not the entire selected range", -> + it "autoscrolls when the cursor exceeds the top or bottom of the editor", -> wrapperNode.style.height = 6 * lineHeightInPixels + 'px' component.measureDimensions() + nextAnimationFrame() expect(editor.getScrollTop()).toBe 0 gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2))) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6))) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(8))) nextAnimationFrame() expect(editor.getScrollTop()).toBeGreaterThan 0 @@ -2069,6 +2104,10 @@ describe "TextEditorComponent", -> nextAnimationFrame() expect(editor.getScrollTop()).toBe maxScrollTop + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2))) + nextAnimationFrame() + expect(editor.getScrollTop()).toBeLessThan maxScrollTop + it "stops selecting if a textInput event occurs during the drag", -> gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2))) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6))) diff --git a/src/selection.coffee b/src/selection.coffee index 6c6609a38..0e565cf33 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -190,7 +190,7 @@ class Selection extends Model # position. # # * `position` An instance of {Point}, with a given `row` and `column`. - selectToScreenPosition: (position) -> + selectToScreenPosition: (position, options) -> position = Point.fromObject(position) @modifySelection => @@ -200,12 +200,12 @@ class Selection extends Model else @marker.setScreenRange([@initialScreenRange.start, position], reversed: false) else - @cursor.setScreenPosition(position) + @cursor.setScreenPosition(position, options) if @linewise - @expandOverLine() + @expandOverLine(options) else if @wordwise - @expandOverWord() + @expandOverWord(options) # Public: Selects the text from the current cursor position to a given buffer # position. @@ -311,28 +311,28 @@ class Selection extends Model # Public: Modifies the selection to encompass the current word. # # Returns a {Range}. - selectWord: -> - options = {} + selectWord: (options={}) -> options.wordRegex = /[\t ]*/ if @cursor.isSurroundedByWhitespace() if @cursor.isBetweenWordAndNonWord() options.includeNonWordCharacters = false - @setBufferRange(@cursor.getCurrentWordBufferRange(options)) + @setBufferRange(@cursor.getCurrentWordBufferRange(options), options) @wordwise = true @initialScreenRange = @getScreenRange() # Public: Expands the newest selection to include the entire word on which # the cursors rests. - expandOverWord: -> + expandOverWord: (options) -> @setBufferRange(@getBufferRange().union(@cursor.getCurrentWordBufferRange()), autoscroll: false) - @cursor.autoscroll() + @cursor.autoscroll() if options?.autoscroll ? true # Public: Selects an entire line in the buffer. # # * `row` The line {Number} to select (default: the row of the cursor). - selectLine: (row=@cursor.getBufferPosition().row) -> + selectLine: (row, options) -> + row ?= @cursor.getBufferPosition().row range = @editor.bufferRangeForBufferRow(row, includeNewline: true) - @setBufferRange(@getBufferRange().union(range), autoscroll: true) + @setBufferRange(@getBufferRange().union(range), options) @linewise = true @wordwise = false @initialScreenRange = @getScreenRange() @@ -341,10 +341,10 @@ class Selection extends Model # the cursor currently rests. # # It also includes the newline character. - expandOverLine: -> + expandOverLine: (options) -> range = @getBufferRange().union(@cursor.getCurrentLineBufferRange(includeNewline: true)) @setBufferRange(range, autoscroll: false) - @cursor.autoscroll() + @cursor.autoscroll() if options?.autoscroll ? true ### Section: Modifying the selected text diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 81f92476f..fce46f35c 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -395,16 +395,16 @@ class TextEditorComponent if cursorAtScreenPosition and @editor.hasMultipleCursors() cursorAtScreenPosition.destroy() else - @editor.addCursorAtScreenPosition(screenPosition) + @editor.addCursorAtScreenPosition(screenPosition, autoscroll: false) else - @editor.setCursorScreenPosition(screenPosition) + @editor.setCursorScreenPosition(screenPosition, autoscroll: false) when 2 - @editor.getLastSelection().selectWord() + @editor.getLastSelection().selectWord(autoscroll: false) when 3 - @editor.getLastSelection().selectLine() + @editor.getLastSelection().selectLine(null, autoscroll: false) @handleDragUntilMouseUp (screenPosition) => - @editor.selectToScreenPosition(screenPosition, true) + @editor.selectToScreenPosition(screenPosition, suppressSelectionMerge: true, autoscroll: false) onLineNumberGutterMouseDown: (event) => return unless event.button is 0 # only handle the left mouse button @@ -422,14 +422,14 @@ class TextEditorComponent clickedScreenRow = @screenPositionForMouseEvent(event).row clickedBufferRow = @editor.bufferRowForScreenRow(clickedScreenRow) initialScreenRange = @editor.screenRangeForBufferRange([[clickedBufferRow, 0], [clickedBufferRow + 1, 0]]) - @editor.setSelectedScreenRange(initialScreenRange, preserveFolds: true) + @editor.setSelectedScreenRange(initialScreenRange, preserveFolds: true, autoscroll: false) @handleGutterDrag(initialScreenRange) onGutterMetaClick: (event) => clickedScreenRow = @screenPositionForMouseEvent(event).row clickedBufferRow = @editor.bufferRowForScreenRow(clickedScreenRow) initialScreenRange = @editor.screenRangeForBufferRange([[clickedBufferRow, 0], [clickedBufferRow + 1, 0]]) - @editor.addSelectionForScreenRange(initialScreenRange, preserveFolds: true) + @editor.addSelectionForScreenRange(initialScreenRange, preserveFolds: true, autoscroll: false) @handleGutterDrag(initialScreenRange) onGutterShiftClick: (event) => @@ -439,9 +439,9 @@ class TextEditorComponent clickedLineScreenRange = @editor.screenRangeForBufferRange([[clickedBufferRow, 0], [clickedBufferRow + 1, 0]]) if clickedScreenRow < tailScreenPosition.row - @editor.selectToScreenPosition(clickedLineScreenRange.start, true) + @editor.selectToScreenPosition(clickedLineScreenRange.start, suppressSelectionMerge: true, autoscroll: false) else - @editor.selectToScreenPosition(clickedLineScreenRange.end, true) + @editor.selectToScreenPosition(clickedLineScreenRange.end, suppressSelectionMerge: true, autoscroll: false) @handleGutterDrag(new Range(tailScreenPosition, tailScreenPosition)) @@ -456,7 +456,6 @@ class TextEditorComponent endPosition = [dragRow + 1, 0] screenRange = new Range(endPosition, endPosition).union(initialRange) @editor.getLastSelection().setScreenRange(screenRange, reversed: false, autoscroll: false, preserveFolds: true) - @editor.getLastCursor().autoscroll() onStylesheetsChanged: (styleElement) => return unless @performedInitialMeasurement @@ -512,7 +511,9 @@ class TextEditorComponent animationLoop = => @requestAnimationFrame => if dragging and @mounted - screenPosition = @screenPositionForMouseEvent(lastMousePosition) + linesClientRect = @linesComponent.getDomNode().getBoundingClientRect() + autoscroll(lastMousePosition, linesClientRect) + screenPosition = @screenPositionForMouseEvent(lastMousePosition, linesClientRect) dragHandler(screenPosition) animationLoop() else if not @mounted @@ -541,7 +542,33 @@ class TextEditorComponent dragging = false window.removeEventListener('mousemove', onMouseMove) window.removeEventListener('mouseup', onMouseUp) - willInsertTextSubscription.dispose() + disposables.dispose() + + autoscroll = (mouseClientPosition) => + editorClientRect = @domNode.getBoundingClientRect() + + if mouseClientPosition.clientY < editorClientRect.top + mouseYDelta = editorClientRect.top - mouseClientPosition.clientY + yDirection = -1 + else if mouseClientPosition.clientY > editorClientRect.bottom + mouseYDelta = mouseClientPosition.clientY - editorClientRect.bottom + yDirection = 1 + + if mouseClientPosition.clientX < editorClientRect.left + mouseXDelta = editorClientRect.left - mouseClientPosition.clientX + xDirection = -1 + else if mouseClientPosition.clientX > editorClientRect.right + mouseXDelta = mouseClientPosition.clientX - editorClientRect.right + xDirection = 1 + + if mouseYDelta? + @presenter.setScrollTop(@presenter.getScrollTop() + yDirection * scaleScrollDelta(mouseYDelta)) + + if mouseXDelta? + @presenter.setScrollLeft(@presenter.getScrollLeft() + xDirection * scaleScrollDelta(mouseXDelta)) + + scaleScrollDelta = (scrollDelta) -> + Math.pow(scrollDelta / 2, 3) / 280 pasteSelectionClipboard = (event) => if event?.which is 2 and process.platform is 'linux' @@ -550,7 +577,9 @@ class TextEditorComponent window.addEventListener('mousemove', onMouseMove) window.addEventListener('mouseup', onMouseUp) - willInsertTextSubscription = @editor.onWillInsertText(onMouseUp) + disposables = new CompositeDisposable + disposables.add(@editor.onWillInsertText(onMouseUp)) + disposables.add(@editor.onDidDestroy(stopDragging)) isVisible: -> @domNode.offsetHeight > 0 or @domNode.offsetWidth > 0 @@ -748,17 +777,20 @@ class TextEditorComponent if scrollSensitivity = parseInt(scrollSensitivity) @scrollSensitivity = Math.abs(scrollSensitivity) / 100 - screenPositionForMouseEvent: (event) -> - pixelPosition = @pixelPositionForMouseEvent(event) + screenPositionForMouseEvent: (event, linesClientRect) -> + pixelPosition = @pixelPositionForMouseEvent(event, linesClientRect) @editor.screenPositionForPixelPosition(pixelPosition) - pixelPositionForMouseEvent: (event) -> + pixelPositionForMouseEvent: (event, linesClientRect) -> {clientX, clientY} = event - linesClientRect = @linesComponent.getDomNode().getBoundingClientRect() + linesClientRect ?= @linesComponent.getDomNode().getBoundingClientRect() top = clientY - linesClientRect.top + @presenter.scrollTop left = clientX - linesClientRect.left + @presenter.scrollLeft - {top, left} + bottom = linesClientRect.top + @presenter.scrollTop + linesClientRect.height - clientY + right = linesClientRect.left + @presenter.scrollLeft + linesClientRect.width - clientX + + {top, left, bottom, right} getModel: -> @editor diff --git a/src/text-editor.coffee b/src/text-editor.coffee index b8c2a76fb..e2e50f5fb 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1943,10 +1943,10 @@ class TextEditor extends Model # This method may merge selections that end up intesecting. # # * `position` An instance of {Point}, with a given `row` and `column`. - selectToScreenPosition: (position, suppressMerge) -> + selectToScreenPosition: (position, options) -> lastSelection = @getLastSelection() - lastSelection.selectToScreenPosition(position) - unless suppressMerge + lastSelection.selectToScreenPosition(position, options) + unless options?.suppressSelectionMerge @mergeIntersectingSelections(reversed: lastSelection.isReversed()) # Essential: Move the cursor of each selection one character upward while