From ad3c3452db4cf83df84013ad1cf6c3fbdd7609b7 Mon Sep 17 00:00:00 2001 From: Pranay Prakash <pranay.gp@gmail.com> Date: Fri, 8 Jun 2018 17:34:13 -0700 Subject: [PATCH 1/8] 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 <pranay.gp@gmail.com> Date: Fri, 8 Jun 2018 17:42:09 -0700 Subject: [PATCH 2/8] 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 <pranay.gp@gmail.com> Date: Fri, 8 Jun 2018 18:31:04 -0700 Subject: [PATCH 3/8] 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 <pranay.gp@gmail.com> Date: Fri, 8 Jun 2018 18:33:18 -0700 Subject: [PATCH 4/8] 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 <pranay.gp@gmail.com> Date: Fri, 8 Jun 2018 19:47:10 -0700 Subject: [PATCH 5/8] 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 45375f2233b509d7f72680650dac15cd270fff7f Mon Sep 17 00:00:00 2001 From: Pranay Prakash <pranay.gp@gmail.com> Date: Fri, 8 Jun 2018 20:23:20 -0700 Subject: [PATCH 6/8] implement multi-cursor selections --- xray_core/src/buffer_view.rs | 16 ++++++++++------ xray_ui/lib/text_editor/text_editor.js | 3 ++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/xray_core/src/buffer_view.rs b/xray_core/src/buffer_view.rs index 33f88509..6c0139e5 100644 --- a/xray_core/src/buffer_view.rs +++ b/xray_core/src/buffer_view.rs @@ -88,6 +88,7 @@ enum BufferViewAction { row: u32, column: u32, autoscroll: bool, + add: bool }, } @@ -273,13 +274,15 @@ impl BufferView { Ok(()) } - pub fn set_cursor_position(&mut self, position: Point, autoscroll: bool) { + pub fn set_cursor_position(&mut self, position: Point, autoscroll: bool, add: 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(); + if !add { + selections.clear(); + } selections.push(Selection { start: anchor.clone(), end: anchor, @@ -517,7 +520,7 @@ impl BufferView { self.buffer .borrow_mut() .mutate_selections(self.selection_set_id, |buffer, selections| { - for selection in selections.iter_mut() { + if let Some(selection) = selections.last_mut() { let anchor = buffer.anchor_before_point(position).unwrap(); selection.set_head(buffer, anchor); selection.goal_column = None; @@ -675,7 +678,7 @@ impl BufferView { self.buffer .borrow_mut() .mutate_selections(self.selection_set_id, |buffer, selections| { - for selection in selections.iter_mut() { + if let Some(selection) = selections.last_mut() { let old_head = buffer.point_for_anchor(selection.head()).unwrap(); let new_start = movement::beginning_of_word(buffer, old_head); let new_end = movement::end_of_word(buffer, new_start); @@ -761,7 +764,7 @@ impl BufferView { .borrow_mut() .mutate_selections(self.selection_set_id, |buffer, selections| { let max_point = buffer.max_point(); - for selection in selections.iter_mut() { + if let Some(selection) = selections.last_mut() { let old_head = buffer.point_for_anchor(selection.head()).unwrap(); let new_start = movement::beginning_of_line(old_head); let new_end = cmp::min(Point::new(new_start.row + 1, 0), max_point); @@ -1122,7 +1125,8 @@ impl View for BufferView { row, column, autoscroll, - }) => self.set_cursor_position(Point::new(row, column), autoscroll), + add + }) => self.set_cursor_position(Point::new(row, column), autoscroll, add), Err(action) => eprintln!("Unrecognized action {:?}", action), } } diff --git a/xray_ui/lib/text_editor/text_editor.js b/xray_ui/lib/text_editor/text_editor.js index 3975452e..8f531e2f 100644 --- a/xray_ui/lib/text_editor/text_editor.js +++ b/xray_ui/lib/text_editor/text_editor.js @@ -286,7 +286,8 @@ class TextEditor extends React.Component { } else { this.props.dispatch(Object.assign({ type: "SetCursorPosition", - autoscroll: false + autoscroll: false, + add: event.altKey }, pos)); } } From 0897ac585217a89a6ad460f618b4de233134f38a Mon Sep 17 00:00:00 2001 From: Pranay Prakash <pranay.gp@gmail.com> Date: Fri, 8 Jun 2018 20:38:56 -0700 Subject: [PATCH 7/8] fix tests --- xray_core/src/buffer_view.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/xray_core/src/buffer_view.rs b/xray_core/src/buffer_view.rs index 6c0139e5..ad668252 100644 --- a/xray_core/src/buffer_view.rs +++ b/xray_core/src/buffer_view.rs @@ -1527,11 +1527,11 @@ mod tests { let mut editor = BufferView::new(Rc::new(RefCell::new(Buffer::new(0))), 0, None); editor.buffer.borrow_mut().edit(&[0..0], "abc.def---ghi"); - editor.set_cursor_position(Point::new(0, 5), false); + editor.set_cursor_position(Point::new(0, 5), false, false); editor.select_word(); assert_eq!(render_selections(&editor), vec![selection((0, 4), (0, 7))]); - editor.set_cursor_position(Point::new(0, 8), false); + editor.set_cursor_position(Point::new(0, 8), false, false); editor.select_word(); assert_eq!(render_selections(&editor), vec![selection((0, 7), (0, 10))]); } @@ -1541,11 +1541,11 @@ mod tests { let mut editor = BufferView::new(Rc::new(RefCell::new(Buffer::new(0))), 0, None); editor.buffer.borrow_mut().edit(&[0..0], "abc\ndef\nghi"); - editor.set_cursor_position(Point::new(0, 2), false); + editor.set_cursor_position(Point::new(0, 2), false, false); editor.select_line(); assert_eq!(render_selections(&editor), vec![selection((0, 0), (1, 0))]); - editor.set_cursor_position(Point::new(2, 1), false); + editor.set_cursor_position(Point::new(2, 1), false, false); editor.select_line(); assert_eq!(render_selections(&editor), vec![selection((2, 0), (2, 3))]); } @@ -1801,7 +1801,7 @@ mod tests { ] ); - editor.set_cursor_position(Point::new(1, 2), false); + editor.set_cursor_position(Point::new(1, 2), false, false); assert_eq!(render_selections(&editor), vec![empty_selection(1, 2)]); } From e7ba1501d624a0c21c754146f8c4fed5974bfa12 Mon Sep 17 00:00:00 2001 From: Pranay Prakash <pranay.gp@gmail.com> Date: Fri, 8 Jun 2018 21:01:38 -0700 Subject: [PATCH 8/8] add tests --- xray_core/src/buffer_view.rs | 48 ++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/xray_core/src/buffer_view.rs b/xray_core/src/buffer_view.rs index ad668252..b710e402 100644 --- a/xray_core/src/buffer_view.rs +++ b/xray_core/src/buffer_view.rs @@ -1805,6 +1805,54 @@ mod tests { assert_eq!(render_selections(&editor), vec![empty_selection(1, 2)]); } + #[test] + fn test_multi_cursor_selections() { + let mut editor = BufferView::new(Rc::new(RefCell::new(Buffer::new(0))), 0, None); + editor + .buffer + .borrow_mut() + .edit(&[0..0], "abcd\nefgh\nijkl\nmnop"); + assert_eq!(render_selections(&editor), vec![empty_selection(0, 0)]); + + editor.move_right(); + editor.move_right(); + + // Add a second cursor + editor.set_cursor_position(Point::new(1, 2), false, true); + assert_eq!( + render_selections(&editor), + vec![ + empty_selection(0, 2), + empty_selection(1, 2), + ] + ); + + // Add a third cursor and select the work + editor.set_cursor_position(Point::new(2, 2), false, true); + editor.select_word(); + assert_eq!( + render_selections(&editor), + vec![ + empty_selection(0, 2), + empty_selection(1, 2), + selection((2, 0), (2, 4)), + ] + ); + + // Add a fourth cursor and select the line + editor.set_cursor_position(Point::new(3, 2), false, true); + editor.select_line(); + assert_eq!( + render_selections(&editor), + vec![ + empty_selection(0, 2), + empty_selection(1, 2), + selection((2, 0), (2, 4)), + selection((3, 0), (3, 4)), + ] + ); + } + #[test] fn test_edit() { let mut editor = BufferView::new(Rc::new(RefCell::new(Buffer::new(0))), 0, None);