From ad3c3452db4cf83df84013ad1cf6c3fbdd7609b7 Mon Sep 17 00:00:00 2001 From: Pranay Prakash Date: Fri, 8 Jun 2018 17:34:13 -0700 Subject: [PATCH 1/6] initial commit --- xray_core/src/buffer_view.rs | 44 +++++++++++++++++ xray_ui/lib/text_editor/text_editor.js | 68 +++++++++++++++++--------- 2 files changed, 90 insertions(+), 22 deletions(-) diff --git a/xray_core/src/buffer_view.rs b/xray_core/src/buffer_view.rs index b62f134c..e34eb0f6 100644 --- a/xray_core/src/buffer_view.rs +++ b/xray_core/src/buffer_view.rs @@ -70,6 +70,10 @@ enum BufferViewAction { SelectDown, SelectLeft, SelectRight, + SelectTo { + row: u32, + column: u32, + }, SelectToBeginningOfWord, SelectToEndOfWord, SelectToBeginningOfLine, @@ -509,6 +513,42 @@ impl BufferView { self.autoscroll_to_cursor(false); } + + // pub fn set_cursor_position(&mut self, position: Point, autoscroll: bool) { + // self.buffer + // .borrow_mut() + // .mutate_selections(self.selection_set_id, |buffer, selections| { + // // TODO: Clip point or return a result. + // let anchor = buffer.anchor_before_point(position).unwrap(); + // selections.clear(); + // selections.push(Selection { + // start: anchor.clone(), + // end: anchor, + // reversed: false, + // goal_column: None, + // }); + // }) + // .unwrap(); + // if autoscroll { + // self.autoscroll_to_cursor(false); + // } + // } + + pub fn select_to(&mut self, position: Point) { + self.buffer + .borrow_mut() + .mutate_selections(self.selection_set_id, |buffer, selections| { + for selection in selections.iter_mut() { + let old_head = buffer.point_for_anchor(selection.head()).unwrap(); + let anchor = buffer.anchor_before_point(position).unwrap(); + selection.set_head(buffer, anchor); + selection.goal_column = None; + } + }) + .unwrap(); + self.autoscroll_to_cursor(false); + } + pub fn move_up(&mut self) { self.buffer .borrow_mut() @@ -1086,6 +1126,10 @@ impl View for BufferView { Ok(BufferViewAction::SelectDown) => self.select_down(), Ok(BufferViewAction::SelectLeft) => self.select_left(), Ok(BufferViewAction::SelectRight) => self.select_right(), + Ok(BufferViewAction::SelectTo { + row, + column + }) => self.select_to(Point::new(row, column)), Ok(BufferViewAction::SelectToBeginningOfWord) => self.select_to_beginning_of_word(), Ok(BufferViewAction::SelectToEndOfWord) => self.select_to_end_of_word(), Ok(BufferViewAction::SelectToBeginningOfLine) => self.select_to_beginning_of_line(), diff --git a/xray_ui/lib/text_editor/text_editor.js b/xray_ui/lib/text_editor/text_editor.js index 95ae9a75..a641f75f 100644 --- a/xray_ui/lib/text_editor/text_editor.js +++ b/xray_ui/lib/text_editor/text_editor.js @@ -38,6 +38,8 @@ class TextEditor extends React.Component { constructor(props) { super(props); + this.handleMouseMove = this.handleMouseMove.bind(this); + this.handleMouseUp = this.handleMouseUp.bind(this); this.handleMouseDown = this.handleMouseDown.bind(this); this.handleMouseWheel = this.handleMouseWheel.bind(this); this.handleKeyDown = this.handleKeyDown.bind(this); @@ -47,7 +49,7 @@ class TextEditor extends React.Component { CURSOR_BLINK_RESUME_DELAY ); this.paddingLeft = 5; - this.state = { scrollLeft: 0, showLocalCursors: true }; + this.state = { scrollLeft: 0, showLocalCursors: true, mouseDown: false }; } componentDidMount() { @@ -70,6 +72,12 @@ class TextEditor extends React.Component { } element.addEventListener("wheel", this.handleMouseWheel, { passive: true }); + element.addEventListener("mousemove", this.handleMouseMove, { + passive: true + }); + element.addEventListener("mouseup", this.handleMouseUp, { + passive: true + }); element.addEventListener("mousedown", this.handleMouseDown, { passive: true }); @@ -210,21 +218,7 @@ class TextEditor extends React.Component { ); } - handleMouseDown(event) { - if (this.canUseTextPlane()) { - this.handleClick(event); - switch (event.detail) { - case 2: - this.handleDoubleClick(); - break; - case 3: - this.handleTripleClick(); - break; - } - } - } - - handleClick({ clientX, clientY }) { + getPositionFromMouseEvent({ clientX, clientY}) { const { scroll_top, line_height, first_visible_row, lines } = this.props; const { scrollLeft } = this.state; const targetX = @@ -245,15 +239,45 @@ class TextEditor extends React.Component { break; } } + return { row, column } + } else { + return null; + } + } + + handleMouseMove(event) { + if (this.canUseTextPlane() && this.state.mouseDown) { + this.props.dispatch(Object.assign({ + type: "SelectTo", + }, this.getPositionFromMouseEvent(event))); + } + } + + handleMouseUp(ecent) { + this.setState({mouseDown: false}) + } + + handleMouseDown(event) { + this.setState({mouseDown: true}) + if (this.canUseTextPlane()) { + this.handleClick(event); + switch (event.detail) { + case 2: + this.handleDoubleClick(); + break; + case 3: + this.handleTripleClick(); + break; + } + } + } - this.pauseCursorBlinking(); - this.props.dispatch({ + handleClick(event) { + this.pauseCursorBlinking(); + this.props.dispatch(Object.assign({ type: "SetCursorPosition", - row, - column, autoscroll: false - }); - } + }, this.getPositionFromMouseEvent(event))); } handleDoubleClick() { From 8cca5ce5ab3ac34cc347b3328277f3a1588ddaac Mon Sep 17 00:00:00 2001 From: Pranay Prakash Date: Fri, 8 Jun 2018 17:42:09 -0700 Subject: [PATCH 2/6] remove stray comment --- xray_core/src/buffer_view.rs | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/xray_core/src/buffer_view.rs b/xray_core/src/buffer_view.rs index e34eb0f6..4db0a5ed 100644 --- a/xray_core/src/buffer_view.rs +++ b/xray_core/src/buffer_view.rs @@ -513,27 +513,6 @@ impl BufferView { self.autoscroll_to_cursor(false); } - - // pub fn set_cursor_position(&mut self, position: Point, autoscroll: bool) { - // self.buffer - // .borrow_mut() - // .mutate_selections(self.selection_set_id, |buffer, selections| { - // // TODO: Clip point or return a result. - // let anchor = buffer.anchor_before_point(position).unwrap(); - // selections.clear(); - // selections.push(Selection { - // start: anchor.clone(), - // end: anchor, - // reversed: false, - // goal_column: None, - // }); - // }) - // .unwrap(); - // if autoscroll { - // self.autoscroll_to_cursor(false); - // } - // } - pub fn select_to(&mut self, position: Point) { self.buffer .borrow_mut() From 189838c28d7e2b3134c4713f7afa2f759adbc48b Mon Sep 17 00:00:00 2001 From: Pranay Prakash Date: Fri, 8 Jun 2018 18:31:04 -0700 Subject: [PATCH 3/6] add tests --- xray_core/src/buffer_view.rs | 22 +++++++++++++++++++++- xray_ui/lib/text_editor/text_editor.js | 14 ++++++++++---- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/xray_core/src/buffer_view.rs b/xray_core/src/buffer_view.rs index 4db0a5ed..18ea49c9 100644 --- a/xray_core/src/buffer_view.rs +++ b/xray_core/src/buffer_view.rs @@ -518,7 +518,6 @@ impl BufferView { .borrow_mut() .mutate_selections(self.selection_set_id, |buffer, selections| { for selection in selections.iter_mut() { - let old_head = buffer.point_for_anchor(selection.head()).unwrap(); let anchor = buffer.anchor_before_point(position).unwrap(); selection.set_head(buffer, anchor); selection.goal_column = None; @@ -1323,6 +1322,27 @@ mod tests { editor.move_up(); editor.move_up(); assert_eq!(render_selections(&editor), vec![empty_selection(0, 1)]); + + // Select to a direct point work in front of cursor position + editor.select_to(Point::new(1, 0)); + assert_eq!(render_selections(&editor), vec![selection((0, 1), (1, 0))]); + editor.move_right(); // cancel selection + assert_eq!(render_selections(&editor), vec![empty_selection(1, 0)]); + editor.move_right(); + editor.move_right(); + assert_eq!(render_selections(&editor), vec![empty_selection(2, 1)]); + + // Selection can go to a point before the cursor + editor.select_to(Point::new(0, 0)); + assert_eq!(render_selections(&editor), vec![rev_selection((0, 0), (2, 1))]); + + // A selection can switch to a new point and the selection will update + editor.select_to(Point::new(0, 3)); + assert_eq!(render_selections(&editor), vec![rev_selection((0, 3), (2, 1))]); + + // A selection can even swing around the cursor without having to unselect + editor.select_to(Point::new(2, 3)); + assert_eq!(render_selections(&editor), vec![selection((2, 1), (2, 3))]); } #[test] diff --git a/xray_ui/lib/text_editor/text_editor.js b/xray_ui/lib/text_editor/text_editor.js index a641f75f..b3b83c6e 100644 --- a/xray_ui/lib/text_editor/text_editor.js +++ b/xray_ui/lib/text_editor/text_editor.js @@ -247,9 +247,12 @@ class TextEditor extends React.Component { handleMouseMove(event) { if (this.canUseTextPlane() && this.state.mouseDown) { - this.props.dispatch(Object.assign({ - type: "SelectTo", - }, this.getPositionFromMouseEvent(event))); + const pos = this.getPositionFromMouseEvent(event); + if (pos) { + this.props.dispatch(Object.assign({ + type: "SelectTo", + }, pos)); + } } } @@ -274,10 +277,13 @@ class TextEditor extends React.Component { handleClick(event) { this.pauseCursorBlinking(); + const pos = this.getPositionFromMouseEvent(event); + if (pos) { this.props.dispatch(Object.assign({ type: "SetCursorPosition", autoscroll: false - }, this.getPositionFromMouseEvent(event))); + }, pos)); + } } handleDoubleClick() { From f1da4b8081ec5938767a89cb5ba17756b1af9d00 Mon Sep 17 00:00:00 2001 From: Pranay Prakash Date: Fri, 8 Jun 2018 18:33:18 -0700 Subject: [PATCH 4/6] fix typo in comment --- xray_core/src/buffer_view.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xray_core/src/buffer_view.rs b/xray_core/src/buffer_view.rs index 18ea49c9..33f88509 100644 --- a/xray_core/src/buffer_view.rs +++ b/xray_core/src/buffer_view.rs @@ -1323,7 +1323,7 @@ mod tests { editor.move_up(); assert_eq!(render_selections(&editor), vec![empty_selection(0, 1)]); - // Select to a direct point work in front of cursor position + // Select to a direct point in front of cursor position editor.select_to(Point::new(1, 0)); assert_eq!(render_selections(&editor), vec![selection((0, 1), (1, 0))]); editor.move_right(); // cancel selection @@ -1332,7 +1332,7 @@ mod tests { editor.move_right(); assert_eq!(render_selections(&editor), vec![empty_selection(2, 1)]); - // Selection can go to a point before the cursor + // Selection can even go to a point before the cursor (with reverse) editor.select_to(Point::new(0, 0)); assert_eq!(render_selections(&editor), vec![rev_selection((0, 0), (2, 1))]); From 8abb218cb96464981465f8ccfa3609b66987ebb9 Mon Sep 17 00:00:00 2001 From: Pranay Prakash Date: Fri, 8 Jun 2018 19:47:10 -0700 Subject: [PATCH 5/6] Support "Shift" key as an alternative to click and drag --- xray_ui/lib/text_editor/text_editor.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/xray_ui/lib/text_editor/text_editor.js b/xray_ui/lib/text_editor/text_editor.js index b3b83c6e..3975452e 100644 --- a/xray_ui/lib/text_editor/text_editor.js +++ b/xray_ui/lib/text_editor/text_editor.js @@ -279,10 +279,16 @@ class TextEditor extends React.Component { this.pauseCursorBlinking(); const pos = this.getPositionFromMouseEvent(event); if (pos) { - this.props.dispatch(Object.assign({ - type: "SetCursorPosition", - autoscroll: false - }, pos)); + if (event.shiftKey) { + this.props.dispatch(Object.assign({ + type: "SelectTo" + }, pos)); + } else { + this.props.dispatch(Object.assign({ + type: "SetCursorPosition", + autoscroll: false + }, pos)); + } } } From bc057949e75d921e5b3fda38cb5fd57485bad70c Mon Sep 17 00:00:00 2001 From: Pranay Prakash Date: Thu, 14 Jun 2018 09:52:57 -0700 Subject: [PATCH 6/6] implemented velocity-scroll along edges --- xray_ui/lib/text_editor/text_editor.js | 77 +++++++++++++++++++++++--- 1 file changed, 69 insertions(+), 8 deletions(-) diff --git a/xray_ui/lib/text_editor/text_editor.js b/xray_ui/lib/text_editor/text_editor.js index 3975452e..344486d8 100644 --- a/xray_ui/lib/text_editor/text_editor.js +++ b/xray_ui/lib/text_editor/text_editor.js @@ -9,6 +9,11 @@ const { ActionContext, Action } = require("../action_dispatcher"); const CURSOR_BLINK_RESUME_DELAY = 300; const CURSOR_BLINK_PERIOD = 800; +const MOUSE_DRAG_AUTOSCROLL_MARGIN = 40; + +function scaleMouseDragAutoscrollDelta (delta) { + return Math.pow(delta / 3, 3) / 280 +} const Root = styled("div", { width: "100%", @@ -72,13 +77,27 @@ class TextEditor extends React.Component { } element.addEventListener("wheel", this.handleMouseWheel, { passive: true }); - element.addEventListener("mousemove", this.handleMouseMove, { + element.addEventListener("mousedown", this.handleMouseDown, { passive: true }); - element.addEventListener("mouseup", this.handleMouseUp, { + + let lastMousemoveEvent + const animationFrameLoop = () => { + window.requestAnimationFrame(() => { + if (this.state.mouseDown) { + this.handleMouseMove(lastMousemoveEvent) + animationFrameLoop() + } + }) + } + + document.addEventListener("mousemove", event => { + lastMousemoveEvent = event; + animationFrameLoop() + }, { passive: true }); - element.addEventListener("mousedown", this.handleMouseDown, { + document.addEventListener("mouseup", this.handleMouseUp, { passive: true }); @@ -240,14 +259,52 @@ class TextEditor extends React.Component { } } return { row, column } - } else { - return null; + } + } + + autoscrollOnMouseDrag ({clientX, clientY}) { + const top = 0 + MOUSE_DRAG_AUTOSCROLL_MARGIN + const bottom = this.props.height - MOUSE_DRAG_AUTOSCROLL_MARGIN + const left = 0 + MOUSE_DRAG_AUTOSCROLL_MARGIN + const right = this.props.width - MOUSE_DRAG_AUTOSCROLL_MARGIN + + let yDelta, yDirection + if (clientY < top) { + yDelta = top - clientY + yDirection = -1 + } else if (clientY > bottom) { + yDelta = clientY - bottom + yDirection = 1 + } + + let xDelta, xDirection + if (clientX < left) { + xDelta = left - clientX + xDirection = -1 + } else if (clientX > right) { + xDelta = clientX - right + xDirection = 1 + } + + if (yDelta != null) { + const scaledDelta = scaleMouseDragAutoscrollDelta(yDelta) * yDirection + this.updateScrollTop(scaledDelta) + } + + if (xDelta != null) { + const scaledDelta = scaleMouseDragAutoscrollDelta(xDelta) * xDirection + this.setScrollLeft(this.getScrollLeft() + scaledDelta) } } handleMouseMove(event) { if (this.canUseTextPlane() && this.state.mouseDown) { - const pos = this.getPositionFromMouseEvent(event); + const boundedPositions = { + clientX: Math.min(Math.max(event.clientX, 0), this.props.width), + clientY: Math.min(Math.max(event.clientY, 0), this.props.height), + } + this.autoscrollOnMouseDrag(event) + const pos = this.getPositionFromMouseEvent(boundedPositions); if (pos) { this.props.dispatch(Object.assign({ type: "SelectTo", @@ -256,7 +313,7 @@ class TextEditor extends React.Component { } } - handleMouseUp(ecent) { + handleMouseUp() { this.setState({mouseDown: false}) } @@ -306,7 +363,7 @@ class TextEditor extends React.Component { if (Math.abs(event.deltaX) > Math.abs(event.deltaY)) { this.setScrollLeft(this.state.scrollLeft + event.deltaX); } else { - this.props.dispatch({ type: "UpdateScrollTop", delta: event.deltaY }); + this.updateScrollTop(event.deltaY); } } @@ -404,6 +461,10 @@ class TextEditor extends React.Component { } } + updateScrollTop(deltaY) { + this.props.dispatch({ type: "UpdateScrollTop", delta: deltaY }); + } + setScrollLeft(scrollLeft) { this.setState({ scrollLeft: this.constrainScrollLeft(scrollLeft)