Handle clicking, shift-clicking, cmd-clicking and dragging in gutter

This commit is contained in:
Nathan Sobo
2017-03-11 12:58:18 -07:00
committed by Antonio Scandurra
parent 17d579f949
commit ffc2025df5
2 changed files with 234 additions and 13 deletions

View File

@@ -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})

View File

@@ -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)
}