diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index af10c304a..8e1452827 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -850,7 +850,7 @@ describe('TextEditorComponent', () => { }, clientPositionForCharacter(component, 1, 4))) { - const [didDrag, didStopDragging] = component.handleMouseDragUntilMouseUp.argsForCall[0] + const {didDrag, didStopDragging} = component.handleMouseDragUntilMouseUp.argsForCall[0][0] didDrag(clientPositionForCharacter(component, 8, 8)) expect(editor.getSelectedScreenRange()).toEqual([[1, 4], [8, 8]]) didDrag(clientPositionForCharacter(component, 4, 8)) @@ -867,7 +867,7 @@ describe('TextEditorComponent', () => { metaKey: 1, }, clientPositionForCharacter(component, 8, 8))) { - const [didDrag, didStopDragging] = component.handleMouseDragUntilMouseUp.argsForCall[1] + const {didDrag, didStopDragging} = component.handleMouseDragUntilMouseUp.argsForCall[1][0] didDrag(clientPositionForCharacter(component, 2, 8)) expect(editor.getSelectedScreenRanges()).toEqual([ [[1, 4], [4, 8]], @@ -903,7 +903,7 @@ describe('TextEditorComponent', () => { button: 0, }, clientPositionForCharacter(component, 1, 4))) - const [didDrag, didStopDragging] = component.handleMouseDragUntilMouseUp.argsForCall[1] + const {didDrag, didStopDragging} = component.handleMouseDragUntilMouseUp.argsForCall[1][0] didDrag(clientPositionForCharacter(component, 0, 8)) expect(editor.getSelectedScreenRange()).toEqual([[0, 4], [1, 5]]) didDrag(clientPositionForCharacter(component, 2, 10)) @@ -919,13 +919,172 @@ describe('TextEditorComponent', () => { component.didMouseDownOnContent(Object.assign({detail: 2, button: 0}, tripleClickPosition)) component.didMouseDownOnContent(Object.assign({detail: 3, button: 0}, tripleClickPosition)) - const [didDrag, didStopDragging] = component.handleMouseDragUntilMouseUp.argsForCall[2] + const {didDrag, didStopDragging} = component.handleMouseDragUntilMouseUp.argsForCall[2][0] didDrag(clientPositionForCharacter(component, 1, 8)) expect(editor.getSelectedScreenRange()).toEqual([[1, 0], [3, 0]]) didDrag(clientPositionForCharacter(component, 4, 10)) expect(editor.getSelectedScreenRange()).toEqual([[2, 0], [5, 0]]) }) + describe('on the line number gutter', () => { + it('selects all buffer rows intersecting the clicked screen row when a line number is clicked', async () => { + const {component, editor} = buildComponent() + spyOn(component, 'handleMouseDragUntilMouseUp') + editor.setSoftWrapped(true) + await setEditorWidthInCharacters(component, 50) + editor.foldBufferRange([[4, Infinity], [7, Infinity]]) + await component.getNextUpdatePromise() + + // Selects entire buffer line when clicked screen line is soft-wrapped + component.didMouseDownOnLineNumberGutter({ + button: 0, + clientY: clientTopForLine(component, 3) + }) + expect(editor.getSelectedScreenRange()).toEqual([[3, 0], [5, 0]]) + expect(editor.getSelectedBufferRange()).toEqual([[3, 0], [4, 0]]) + + // Selects entire screen line, even if folds cause that selection to + // span multiple buffer lines + component.didMouseDownOnLineNumberGutter({ + button: 0, + clientY: clientTopForLine(component, 5) + }) + expect(editor.getSelectedScreenRange()).toEqual([[5, 0], [6, 0]]) + expect(editor.getSelectedBufferRange()).toEqual([[4, 0], [8, 0]]) + }) + + it('adds new selections when a line number is meta-clicked', async () => { + const {component, editor} = buildComponent() + editor.setSoftWrapped(true) + await setEditorWidthInCharacters(component, 50) + editor.foldBufferRange([[4, Infinity], [7, Infinity]]) + await component.getNextUpdatePromise() + + // Selects entire buffer line when clicked screen line is soft-wrapped + component.didMouseDownOnLineNumberGutter({ + button: 0, + metaKey: true, + clientY: clientTopForLine(component, 3) + }) + expect(editor.getSelectedScreenRanges()).toEqual([ + [[0, 0], [0, 0]], + [[3, 0], [5, 0]] + ]) + expect(editor.getSelectedBufferRanges()).toEqual([ + [[0, 0], [0, 0]], + [[3, 0], [4, 0]] + ]) + + // Selects entire screen line, even if folds cause that selection to + // span multiple buffer lines + component.didMouseDownOnLineNumberGutter({ + button: 0, + metaKey: true, + clientY: clientTopForLine(component, 5) + }) + expect(editor.getSelectedScreenRanges()).toEqual([ + [[0, 0], [0, 0]], + [[3, 0], [5, 0]], + [[5, 0], [6, 0]] + ]) + expect(editor.getSelectedBufferRanges()).toEqual([ + [[0, 0], [0, 0]], + [[3, 0], [4, 0]], + [[4, 0], [8, 0]] + ]) + }) + + it('expands the last selection when a line number is shift-clicked', async () => { + const {component, editor} = buildComponent() + spyOn(component, 'handleMouseDragUntilMouseUp') + editor.setSoftWrapped(true) + await setEditorWidthInCharacters(component, 50) + editor.foldBufferRange([[4, Infinity], [7, Infinity]]) + await component.getNextUpdatePromise() + + editor.setSelectedScreenRange([[3, 4], [3, 8]]) + editor.addCursorAtScreenPosition([2, 10]) + component.didMouseDownOnLineNumberGutter({ + button: 0, + shiftKey: true, + clientY: clientTopForLine(component, 5) + }) + + expect(editor.getSelectedBufferRanges()).toEqual([ + [[3, 4], [3, 8]], + [[2, 10], [8, 0]] + ]) + + // Original selection is preserved when shift-click-dragging + const {didDrag, didStopDragging} = component.handleMouseDragUntilMouseUp.argsForCall[0][0] + didDrag({ + clientY: clientTopForLine(component, 1) + }) + expect(editor.getSelectedBufferRanges()).toEqual([ + [[3, 4], [3, 8]], + [[1, 0], [2, 10]] + ]) + + didDrag({ + clientY: clientTopForLine(component, 5) + }) + + didStopDragging() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[2, 10], [8, 0]] + ]) + }) + + it('expands the selection when dragging', async () => { + const {component, editor} = buildComponent() + spyOn(component, 'handleMouseDragUntilMouseUp') + editor.setSoftWrapped(true) + await setEditorWidthInCharacters(component, 50) + editor.foldBufferRange([[4, Infinity], [7, Infinity]]) + await component.getNextUpdatePromise() + + editor.setSelectedScreenRange([[3, 4], [3, 6]]) + + component.didMouseDownOnLineNumberGutter({ + button: 0, + metaKey: true, + clientY: clientTopForLine(component, 2) + }) + + const {didDrag, didStopDragging} = component.handleMouseDragUntilMouseUp.argsForCall[0][0] + + didDrag({ + clientY: clientTopForLine(component, 1) + }) + expect(editor.getSelectedScreenRanges()).toEqual([ + [[3, 4], [3, 6]], + [[1, 0], [3, 0]] + ]) + + didDrag({ + clientY: clientTopForLine(component, 5) + }) + expect(editor.getSelectedScreenRanges()).toEqual([ + [[3, 4], [3, 6]], + [[2, 0], [6, 0]] + ]) + expect(editor.isFoldedAtBufferRow(4)).toBe(true) + + didDrag({ + clientY: clientTopForLine(component, 3) + }) + expect(editor.getSelectedScreenRanges()).toEqual([ + [[3, 4], [3, 6]], + [[2, 0], [4, 4]] + ]) + + didStopDragging() + expect(editor.getSelectedScreenRanges()).toEqual([ + [[2, 0], [4, 4]] + ]) + }) + }) + it('autoscrolls the content when dragging near the edge of the screen', async () => { const {component, editor} = buildComponent({width: 200, height: 200}) const {scroller} = component.refs @@ -948,7 +1107,7 @@ describe('TextEditorComponent', () => { } component.didMouseDownOnContent({detail: 1, button: 0, clientX: 100, clientY: 100}) - const [didDrag, didStopDragging] = component.handleMouseDragUntilMouseUp.argsForCall[0] + const {didDrag, didStopDragging} = component.handleMouseDragUntilMouseUp.argsForCall[0][0] didDrag({clientX: 199, clientY: 199}) assertScrolledDownAndRight() didDrag({clientX: 199, clientY: 199}) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index ee115f087..72342530c 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -1,6 +1,6 @@ const etch = require('etch') const {CompositeDisposable} = require('event-kit') -const {Point} = require('text-buffer') +const {Point, Range} = require('text-buffer') const resizeDetector = require('element-resize-detector')({strategy: 'scroll'}) const TextEditor = require('./text-editor') const {isPairedCharacter} = require('./text-utils') @@ -14,6 +14,7 @@ const DOUBLE_WIDTH_CHARACTER = '我' const HALF_WIDTH_CHARACTER = 'ハ' const KOREAN_CHARACTER = '세' const NBSP_CHARACTER = '\u00a0' +const ZERO_WIDTH_NBSP_CHARACTER = '\ufeff' const MOUSE_DRAG_AUTOSCROLL_MARGIN = 40 function scaleMouseDragAutoscrollDelta (delta) { @@ -796,29 +797,83 @@ class TextEditorComponent { break } - this.handleMouseDragUntilMouseUp( - (event) => { + this.handleMouseDragUntilMouseUp({ + didDrag: (event) => { this.autoscrollOnMouseDrag(event) const screenPosition = this.screenPositionForMouseEvent(event) model.selectToScreenPosition(screenPosition, {suppressSelectionMerge: true, autoscroll: false}) this.updateSync() }, - () => { + didStopDragging: () => { model.finalizeSelections() model.mergeIntersectingSelections() this.updateSync() } - ) + }) } - handleMouseDragUntilMouseUp (didDragCallback, didStopDragging) { + didMouseDownOnLineNumberGutter (event) { + if (global.debug) debugger + + const {model} = this.props + const {button, ctrlKey, shiftKey, metaKey} = event + + // Only handle mousedown events for left mouse button + if (button !== 0) return + + const addOrRemoveSelection = metaKey || (ctrlKey && this.getPlatform() !== 'darwin') + const clickedScreenRow = this.screenPositionForMouseEvent(event).row + const startBufferRow = model.bufferPositionForScreenPosition([clickedScreenRow, 0]).row + const endBufferRow = model.bufferPositionForScreenPosition([clickedScreenRow, Infinity]).row + const clickedLineBufferRange = Range(Point(startBufferRow, 0), Point(endBufferRow + 1, 0)) + + let initialBufferRange + if (shiftKey) { + const lastSelection = model.getLastSelection() + initialBufferRange = lastSelection.getBufferRange() + lastSelection.setBufferRange(initialBufferRange.union(clickedLineBufferRange), { + reversed: clickedScreenRow < lastSelection.getScreenRange().start.row, + autoscroll: false, + preserveFolds: true, + suppressSelectionMerge: true + }) + } else { + initialBufferRange = clickedLineBufferRange + if (addOrRemoveSelection) { + model.addSelectionForBufferRange(clickedLineBufferRange, {autoscroll: false, preserveFolds: true}) + } else { + model.setSelectedBufferRange(clickedLineBufferRange, {autoscroll: false, preserveFolds: true}) + } + } + + const initialScreenRange = model.screenRangeForBufferRange(initialBufferRange) + this.handleMouseDragUntilMouseUp({ + didDrag: (event) => { + const dragRow = this.screenPositionForMouseEvent(event).row + const draggedLineScreenRange = Range(Point(dragRow, 0), Point(dragRow + 1, 0)) + model.getLastSelection().setScreenRange(draggedLineScreenRange.union(initialScreenRange), { + reversed: dragRow < initialScreenRange.start.row, + autoscroll: false, + preserveFolds: true + }) + this.updateSync() + }, + didStopDragging: () => { + model.mergeIntersectingSelections() + this.updateSync() + } + }) + + } + + handleMouseDragUntilMouseUp ({didDrag, didStopDragging}) { let dragging = false let lastMousemoveEvent const animationFrameLoop = () => { window.requestAnimationFrame(() => { if (dragging && this.visible) { - didDragCallback(lastMousemoveEvent) + didDrag(lastMousemoveEvent) animationFrameLoop() } }) @@ -1503,6 +1558,9 @@ class LineNumberGutterComponent { children[tileIndex] = $.div({ key: tileIndex, + on: { + mousedown: this.didMouseDown + }, style: { contain: 'strict', overflow: 'hidden', @@ -1547,6 +1605,10 @@ class LineNumberGutterComponent { if (!arraysEqual(oldProps.lineNumberDecorations, newProps.lineNumberDecorations)) return true return false } + + didMouseDown (event) { + this.props.parentComponent.didMouseDownOnLineNumberGutter(event) + } } class LinesTileComponent { @@ -1718,7 +1780,7 @@ class LineComponent { // Insert a zero-width non-breaking whitespace, so that LinesYardstick can // take the fold-marker::after pseudo-element into account during // measurements when such marker is the last character on the line. - const textNode = document.createTextNode(ZERO_WIDTH_NBSP) + const textNode = document.createTextNode(ZERO_WIDTH_NBSP_CHARACTER) this.element.appendChild(textNode) textNodes.push(textNode) }