diff --git a/xray_core/src/buffer_view.rs b/xray_core/src/buffer_view.rs index b62f134c..b710e402 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, @@ -84,6 +88,7 @@ enum BufferViewAction { row: u32, column: u32, autoscroll: bool, + add: bool }, } @@ -269,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, @@ -509,6 +516,20 @@ impl BufferView { 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| { + if let Some(selection) = selections.last_mut() { + 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() @@ -657,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); @@ -743,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); @@ -1086,6 +1107,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(), @@ -1100,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), } } @@ -1300,6 +1326,27 @@ mod tests { editor.move_up(); editor.move_up(); assert_eq!(render_selections(&editor), vec![empty_selection(0, 1)]); + + // 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 + 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 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))]); + + // 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] @@ -1480,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))]); } @@ -1494,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))]); } @@ -1754,10 +1801,58 @@ 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)]); } + #[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); diff --git a/xray_ui/lib/text_editor/text_editor.js b/xray_ui/lib/text_editor/text_editor.js index 95ae9a75..8f531e2f 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,14 +239,57 @@ class TextEditor extends React.Component { break; } } + return { row, column } + } else { + return null; + } + } - this.pauseCursorBlinking(); - this.props.dispatch({ - type: "SetCursorPosition", - row, - column, - autoscroll: false - }); + handleMouseMove(event) { + if (this.canUseTextPlane() && this.state.mouseDown) { + const pos = this.getPositionFromMouseEvent(event); + if (pos) { + this.props.dispatch(Object.assign({ + type: "SelectTo", + }, pos)); + } + } + } + + 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; + } + } + } + + handleClick(event) { + this.pauseCursorBlinking(); + const pos = this.getPositionFromMouseEvent(event); + if (pos) { + if (event.shiftKey) { + this.props.dispatch(Object.assign({ + type: "SelectTo" + }, pos)); + } else { + this.props.dispatch(Object.assign({ + type: "SetCursorPosition", + autoscroll: false, + add: event.altKey + }, pos)); + } } }