Skip to content
This repository has been archived by the owner on Jul 23, 2019. It is now read-only.

Add multi cursor click selections #108

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 105 additions & 10 deletions xray_core/src/buffer_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ enum BufferViewAction {
SelectDown,
SelectLeft,
SelectRight,
SelectTo {
row: u32,
column: u32,
},
SelectToBeginningOfWord,
SelectToEndOfWord,
SelectToBeginningOfLine,
Expand All @@ -84,6 +88,7 @@ enum BufferViewAction {
row: u32,
column: u32,
autoscroll: bool,
add: bool
},
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(),
Expand All @@ -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),
}
}
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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))]);
}
Expand All @@ -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))]);
}
Expand Down Expand Up @@ -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);
Expand Down
83 changes: 60 additions & 23 deletions xray_ui/lib/text_editor/text_editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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() {
Expand All @@ -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
});
Expand Down Expand Up @@ -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 =
Expand All @@ -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));
}
}
}

Expand Down