Implement `add_cursor_above` and `add_cursor_below` for buffer

Antonio Scandurra created

Change summary

zed/src/editor/buffer/mod.rs       |   4 
zed/src/editor/buffer/selection.rs |   9 +
zed/src/editor/buffer_view.rs      | 201 +++++++++++++++++++++++++------
zed/src/editor/movement.rs         |  30 +++-
4 files changed, 188 insertions(+), 56 deletions(-)

Detailed changes

zed/src/editor/buffer/mod.rs 🔗

@@ -3285,14 +3285,14 @@ mod tests {
                         start: self.anchor_before(range.end)?,
                         end: self.anchor_before(range.start)?,
                         reversed: true,
-                        goal_column: None,
+                        goal: SelectionGoal::None,
                     });
                 } else {
                     selections.push(Selection {
                         start: self.anchor_after(range.start)?,
                         end: self.anchor_before(range.end)?,
                         reversed: false,
-                        goal_column: None,
+                        goal: SelectionGoal::None,
                     });
                 }
             }

zed/src/editor/buffer/selection.rs 🔗

@@ -12,12 +12,19 @@ use std::{cmp::Ordering, mem, ops::Range};
 pub type SelectionSetId = time::Lamport;
 pub type SelectionsVersion = usize;
 
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+pub enum SelectionGoal {
+    None,
+    Column(u32),
+    ColumnRange { start: u32, end: u32 },
+}
+
 #[derive(Clone, Debug, Eq, PartialEq)]
 pub struct Selection {
     pub start: Anchor,
     pub end: Anchor,
     pub reversed: bool,
-    pub goal_column: Option<u32>,
+    pub goal: SelectionGoal,
 }
 
 impl Selection {

zed/src/editor/buffer_view.rs 🔗

@@ -1,6 +1,6 @@
 use super::{
     buffer, movement, Anchor, Bias, Buffer, BufferElement, DisplayMap, DisplayPoint, Point,
-    Selection, SelectionSetId, ToOffset, ToPoint,
+    Selection, SelectionGoal, SelectionSetId, ToOffset, ToPoint,
 };
 use crate::{settings::Settings, workspace, worktree::FileHandle};
 use anyhow::Result;
@@ -149,6 +149,16 @@ pub fn init(app: &mut MutableAppContext) {
             "buffer:split_selection_into_lines",
             Some("BufferView"),
         ),
+        Binding::new(
+            "ctrl-shift-up",
+            "buffer:add_cursor_above",
+            Some("BufferView"),
+        ),
+        Binding::new(
+            "ctrl-shift-down",
+            "buffer:add_cursor_below",
+            Some("BufferView"),
+        ),
         Binding::new("pageup", "buffer:page_up", Some("BufferView")),
         Binding::new("pagedown", "buffer:page_down", Some("BufferView")),
         Binding::new("alt-cmd-[", "buffer:fold", Some("BufferView")),
@@ -244,6 +254,8 @@ pub fn init(app: &mut MutableAppContext) {
         "buffer:split_selection_into_lines",
         BufferView::split_selection_into_lines,
     );
+    app.add_action("buffer:add_cursor_above", BufferView::add_cursor_above);
+    app.add_action("buffer:add_cursor_below", BufferView::add_cursor_below);
     app.add_action("buffer:page_up", BufferView::page_up);
     app.add_action("buffer:page_down", BufferView::page_down);
     app.add_action("buffer:fold", BufferView::fold);
@@ -311,7 +323,7 @@ impl BufferView {
                     start: buffer.anchor_before(0).unwrap(),
                     end: buffer.anchor_before(0).unwrap(),
                     reversed: false,
-                    goal_column: None,
+                    goal: SelectionGoal::None,
                 }],
                 Some(ctx),
             )
@@ -483,7 +495,7 @@ impl BufferView {
             start: cursor.clone(),
             end: cursor,
             reversed: false,
-            goal_column: None,
+            goal: SelectionGoal::None,
         };
 
         if !add {
@@ -552,7 +564,7 @@ impl BufferView {
                 start: buffer.anchor_before(start).unwrap(),
                 end: buffer.anchor_before(end).unwrap(),
                 reversed,
-                goal_column: None,
+                goal: SelectionGoal::None,
             });
         }
         self.update_selections(selections, autoscroll, ctx);
@@ -582,7 +594,7 @@ impl BufferView {
                     .display_map
                     .anchor_before(end, Bias::Left, ctx.as_ref())?,
                 reversed,
-                goal_column: None,
+                goal: SelectionGoal::None,
             });
         }
         self.update_selections(selections, false, ctx);
@@ -623,7 +635,7 @@ impl BufferView {
                         start: anchor.clone(),
                         end: anchor,
                         reversed: false,
-                        goal_column: None,
+                        goal: SelectionGoal::None,
                     }
                 })
                 .collect();
@@ -662,7 +674,7 @@ impl BufferView {
                         )
                         .unwrap();
                     selection.set_head(&buffer, cursor);
-                    selection.goal_column = None;
+                    selection.goal = SelectionGoal::None;
                 }
             }
         }
@@ -693,7 +705,7 @@ impl BufferView {
                         )
                         .unwrap();
                     selection.set_head(&buffer, cursor);
-                    selection.goal_column = None;
+                    selection.goal = SelectionGoal::None;
                 }
             }
         }
@@ -774,7 +786,7 @@ impl BufferView {
                 start: anchor.clone(),
                 end: anchor,
                 reversed: false,
-                goal_column: None,
+                goal: SelectionGoal::None,
             })
             .collect();
         self.buffer
@@ -1145,7 +1157,7 @@ impl BufferView {
                             start: new_selection_start.clone(),
                             end: new_selection_start,
                             reversed: false,
-                            goal_column: None,
+                            goal: SelectionGoal::None,
                         });
                     });
                 }
@@ -1196,7 +1208,7 @@ impl BufferView {
                     selection.end = cursor;
                 }
                 selection.reversed = false;
-                selection.goal_column = None;
+                selection.goal = SelectionGoal::None;
             }
         }
         self.update_selections(selections, true, ctx);
@@ -1220,7 +1232,7 @@ impl BufferView {
                     )
                     .unwrap();
                 selection.set_head(&buffer, cursor);
-                selection.goal_column = None;
+                selection.goal = SelectionGoal::None;
             }
         }
         self.update_selections(selections, true, ctx);
@@ -1255,7 +1267,7 @@ impl BufferView {
                     selection.end = cursor;
                 }
                 selection.reversed = false;
-                selection.goal_column = None;
+                selection.goal = SelectionGoal::None;
             }
         }
         self.update_selections(selections, true, ctx);
@@ -1280,7 +1292,7 @@ impl BufferView {
                     )
                     .unwrap();
                 selection.set_head(&buffer, cursor);
-                selection.goal_column = None;
+                selection.goal = SelectionGoal::None;
             }
         }
         self.update_selections(selections, true, ctx);
@@ -1303,18 +1315,18 @@ impl BufferView {
                         .to_display_point(&self.display_map, app)
                         .unwrap();
                     if start != end {
-                        selection.goal_column = None;
+                        selection.goal = SelectionGoal::None;
                     }
 
-                    let (start, goal_column) =
-                        movement::up(&self.display_map, start, selection.goal_column, app).unwrap();
+                    let (start, goal) =
+                        movement::up(&self.display_map, start, selection.goal, app).unwrap();
                     let cursor = self
                         .display_map
                         .anchor_before(start, Bias::Left, app)
                         .unwrap();
                     selection.start = cursor.clone();
                     selection.end = cursor;
-                    selection.goal_column = goal_column;
+                    selection.goal = goal;
                     selection.reversed = false;
                 }
             }
@@ -1332,15 +1344,15 @@ impl BufferView {
                     .head()
                     .to_display_point(&self.display_map, app)
                     .unwrap();
-                let (head, goal_column) =
-                    movement::up(&self.display_map, head, selection.goal_column, app).unwrap();
+                let (head, goal) =
+                    movement::up(&self.display_map, head, selection.goal, app).unwrap();
                 selection.set_head(
                     &buffer,
                     self.display_map
                         .anchor_before(head, Bias::Left, app)
                         .unwrap(),
                 );
-                selection.goal_column = goal_column;
+                selection.goal = goal;
             }
         }
         self.update_selections(selections, true, ctx);
@@ -1363,18 +1375,18 @@ impl BufferView {
                         .to_display_point(&self.display_map, app)
                         .unwrap();
                     if start != end {
-                        selection.goal_column = None;
+                        selection.goal = SelectionGoal::None;
                     }
 
-                    let (start, goal_column) =
-                        movement::down(&self.display_map, end, selection.goal_column, app).unwrap();
+                    let (start, goal) =
+                        movement::down(&self.display_map, end, selection.goal, app).unwrap();
                     let cursor = self
                         .display_map
                         .anchor_before(start, Bias::Right, app)
                         .unwrap();
                     selection.start = cursor.clone();
                     selection.end = cursor;
-                    selection.goal_column = goal_column;
+                    selection.goal = goal;
                     selection.reversed = false;
                 }
             }
@@ -1392,15 +1404,15 @@ impl BufferView {
                     .head()
                     .to_display_point(&self.display_map, app)
                     .unwrap();
-                let (head, goal_column) =
-                    movement::down(&self.display_map, head, selection.goal_column, app).unwrap();
+                let (head, goal) =
+                    movement::down(&self.display_map, head, selection.goal, app).unwrap();
                 selection.set_head(
                     &buffer,
                     self.display_map
                         .anchor_before(head, Bias::Right, app)
                         .unwrap(),
                 );
-                selection.goal_column = goal_column;
+                selection.goal = goal;
             }
         }
         self.update_selections(selections, true, ctx);
@@ -1423,7 +1435,7 @@ impl BufferView {
                 selection.start = anchor.clone();
                 selection.end = anchor;
                 selection.reversed = false;
-                selection.goal_column = None;
+                selection.goal = SelectionGoal::None;
             }
         }
         self.update_selections(selections, true, ctx);
@@ -1445,7 +1457,7 @@ impl BufferView {
                     .anchor_before(new_head, Bias::Left, app)
                     .unwrap();
                 selection.set_head(buffer, anchor);
-                selection.goal_column = None;
+                selection.goal = SelectionGoal::None;
             }
         }
         self.update_selections(selections, true, ctx);
@@ -1475,7 +1487,7 @@ impl BufferView {
                 selection.start = anchor.clone();
                 selection.end = anchor;
                 selection.reversed = false;
-                selection.goal_column = None;
+                selection.goal = SelectionGoal::None;
             }
         }
         self.update_selections(selections, true, ctx);
@@ -1497,7 +1509,7 @@ impl BufferView {
                     .anchor_before(new_head, Bias::Left, app)
                     .unwrap();
                 selection.set_head(buffer, anchor);
-                selection.goal_column = None;
+                selection.goal = SelectionGoal::None;
             }
         }
         self.update_selections(selections, true, ctx);
@@ -1528,7 +1540,7 @@ impl BufferView {
                 selection.start = anchor.clone();
                 selection.end = anchor;
                 selection.reversed = false;
-                selection.goal_column = None;
+                selection.goal = SelectionGoal::None;
             }
         }
         self.update_selections(selections, true, ctx);
@@ -1555,7 +1567,7 @@ impl BufferView {
                     .anchor_before(new_head, Bias::Left, app)
                     .unwrap();
                 selection.set_head(buffer, anchor);
-                selection.goal_column = None;
+                selection.goal = SelectionGoal::None;
             }
         }
         self.update_selections(selections, true, ctx);
@@ -1585,7 +1597,7 @@ impl BufferView {
                 selection.start = anchor.clone();
                 selection.end = anchor;
                 selection.reversed = false;
-                selection.goal_column = None;
+                selection.goal = SelectionGoal::None;
             }
         }
         self.update_selections(selections, true, ctx);
@@ -1607,7 +1619,7 @@ impl BufferView {
                     .anchor_before(new_head, Bias::Left, app)
                     .unwrap();
                 selection.set_head(buffer, anchor);
-                selection.goal_column = None;
+                selection.goal = SelectionGoal::None;
             }
         }
         self.update_selections(selections, true, ctx);
@@ -1627,7 +1639,7 @@ impl BufferView {
             start: cursor.clone(),
             end: cursor,
             reversed: false,
-            goal_column: None,
+            goal: SelectionGoal::None,
         };
         self.update_selections(vec![selection], true, ctx);
     }
@@ -1645,7 +1657,7 @@ impl BufferView {
             start: cursor.clone(),
             end: cursor,
             reversed: false,
-            goal_column: None,
+            goal: SelectionGoal::None,
         };
         self.update_selections(vec![selection], true, ctx);
     }
@@ -1661,7 +1673,7 @@ impl BufferView {
             start: Anchor::Start,
             end: Anchor::End,
             reversed: false,
-            goal_column: None,
+            goal: SelectionGoal::None,
         };
         self.update_selections(vec![selection], false, ctx);
     }
@@ -1697,7 +1709,7 @@ impl BufferView {
                     start: selection.start.clone(),
                     end: selection.start.clone(),
                     reversed: false,
-                    goal_column: None,
+                    goal: SelectionGoal::None,
                 });
             }
             for row in range.start.row + 1..range.end.row {
@@ -1708,14 +1720,14 @@ impl BufferView {
                     start: cursor.clone(),
                     end: cursor,
                     reversed: false,
-                    goal_column: None,
+                    goal: SelectionGoal::None,
                 });
             }
             new_selections.push(Selection {
                 start: selection.end.clone(),
                 end: selection.end.clone(),
                 reversed: false,
-                goal_column: None,
+                goal: SelectionGoal::None,
             });
             to_unfold.push(range);
         }
@@ -1723,6 +1735,111 @@ impl BufferView {
         self.update_selections(new_selections, true, ctx);
     }
 
+    pub fn add_cursor_above(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
+        use super::RangeExt;
+
+        let app = ctx.as_ref();
+        let buffer = self.buffer.read(app);
+
+        let mut new_selections = Vec::new();
+        for selection in self.selections(app) {
+            let range = selection.display_range(&self.display_map, app).sorted();
+
+            let start_column;
+            let end_column;
+            if let SelectionGoal::ColumnRange { start, end } = selection.goal {
+                start_column = start;
+                end_column = end;
+            } else {
+                start_column = cmp::min(range.start.column(), range.end.column());
+                end_column = cmp::max(range.start.column(), range.end.column());
+            }
+            let is_empty = start_column == end_column;
+
+            for row in (0..range.start.row()).rev() {
+                let line_len = self.display_map.line_len(row, app).unwrap();
+                if start_column < line_len || (is_empty && start_column == line_len) {
+                    let start = DisplayPoint::new(row, start_column);
+                    let end = DisplayPoint::new(row, cmp::min(end_column, line_len));
+                    new_selections.push(Selection {
+                        start: self
+                            .display_map
+                            .anchor_before(start, Bias::Left, app)
+                            .unwrap(),
+                        end: self
+                            .display_map
+                            .anchor_before(end, Bias::Left, app)
+                            .unwrap(),
+                        reversed: selection.reversed && range.start.row() == range.end.row(),
+                        goal: SelectionGoal::ColumnRange {
+                            start: start_column,
+                            end: end_column,
+                        },
+                    });
+                    break;
+                }
+            }
+
+            new_selections.push(selection.clone());
+        }
+
+        new_selections.sort_unstable_by(|a, b| a.start.cmp(&b.start, buffer).unwrap());
+        self.update_selections(new_selections, true, ctx);
+    }
+
+    pub fn add_cursor_below(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
+        use super::RangeExt;
+
+        let app = ctx.as_ref();
+        let buffer = self.buffer.read(app);
+        let max_point = self.display_map.max_point(app);
+
+        let mut new_selections = Vec::new();
+        for selection in self.selections(app) {
+            let range = selection.display_range(&self.display_map, app).sorted();
+
+            let start_column;
+            let end_column;
+            if let SelectionGoal::ColumnRange { start, end } = selection.goal {
+                start_column = start;
+                end_column = end;
+            } else {
+                start_column = cmp::min(range.start.column(), range.end.column());
+                end_column = cmp::max(range.start.column(), range.end.column());
+            }
+            let is_empty = start_column == end_column;
+
+            for row in range.end.row() + 1..=max_point.row() {
+                let line_len = self.display_map.line_len(row, app).unwrap();
+                if start_column < line_len || (is_empty && start_column == line_len) {
+                    let start = DisplayPoint::new(row, start_column);
+                    let end = DisplayPoint::new(row, cmp::min(end_column, line_len));
+                    new_selections.push(Selection {
+                        start: self
+                            .display_map
+                            .anchor_before(start, Bias::Left, app)
+                            .unwrap(),
+                        end: self
+                            .display_map
+                            .anchor_before(end, Bias::Left, app)
+                            .unwrap(),
+                        reversed: selection.reversed && range.start.row() == range.end.row(),
+                        goal: SelectionGoal::ColumnRange {
+                            start: start_column,
+                            end: end_column,
+                        },
+                    });
+                    break;
+                }
+            }
+
+            new_selections.push(selection.clone());
+        }
+
+        new_selections.sort_unstable_by(|a, b| a.start.cmp(&b.start, buffer).unwrap());
+        self.update_selections(new_selections, true, ctx);
+    }
+
     pub fn selections_in_range<'a>(
         &'a self,
         range: Range<DisplayPoint>,

zed/src/editor/movement.rs 🔗

@@ -1,4 +1,4 @@
-use super::{DisplayMap, DisplayPoint};
+use super::{DisplayMap, DisplayPoint, SelectionGoal};
 use anyhow::Result;
 use gpui::AppContext;
 use std::cmp;
@@ -27,36 +27,44 @@ pub fn right(map: &DisplayMap, mut point: DisplayPoint, app: &AppContext) -> Res
 pub fn up(
     map: &DisplayMap,
     mut point: DisplayPoint,
-    goal_column: Option<u32>,
+    goal: SelectionGoal,
     app: &AppContext,
-) -> Result<(DisplayPoint, Option<u32>)> {
-    let goal_column = goal_column.or(Some(point.column()));
+) -> Result<(DisplayPoint, SelectionGoal)> {
+    let goal_column = if let SelectionGoal::Column(column) = goal {
+        column
+    } else {
+        point.column()
+    };
     if point.row() > 0 {
         *point.row_mut() -= 1;
-        *point.column_mut() = cmp::min(goal_column.unwrap(), map.line_len(point.row(), app)?);
+        *point.column_mut() = cmp::min(goal_column, map.line_len(point.row(), app)?);
     } else {
         point = DisplayPoint::new(0, 0);
     }
 
-    Ok((point, goal_column))
+    Ok((point, SelectionGoal::Column(goal_column)))
 }
 
 pub fn down(
     map: &DisplayMap,
     mut point: DisplayPoint,
-    goal_column: Option<u32>,
+    goal: SelectionGoal,
     app: &AppContext,
-) -> Result<(DisplayPoint, Option<u32>)> {
-    let goal_column = goal_column.or(Some(point.column()));
+) -> Result<(DisplayPoint, SelectionGoal)> {
+    let goal_column = if let SelectionGoal::Column(column) = goal {
+        column
+    } else {
+        point.column()
+    };
     let max_point = map.max_point(app);
     if point.row() < max_point.row() {
         *point.row_mut() += 1;
-        *point.column_mut() = cmp::min(goal_column.unwrap(), map.line_len(point.row(), app)?)
+        *point.column_mut() = cmp::min(goal_column, map.line_len(point.row(), app)?)
     } else {
         point = max_point;
     }
 
-    Ok((point, goal_column))
+    Ok((point, SelectionGoal::Column(goal_column)))
 }
 
 pub fn line_beginning(