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);