Implement `move_line_up` and `move_line_down`

Antonio Scandurra created

This does not restore folds yet.

Change summary

zed/src/editor/buffer/selection.rs     |  11 +
zed/src/editor/buffer_view.rs          | 176 ++++++++++++++++++++++++++-
zed/src/editor/display_map/fold_map.rs |   7 +
zed/src/editor/display_map/mod.rs      |   5 
4 files changed, 188 insertions(+), 11 deletions(-)

Detailed changes

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

@@ -73,7 +73,11 @@ impl Selection {
         }
     }
 
-    pub fn buffer_rows_for_display_rows(&self, map: &DisplayMap, ctx: &AppContext) -> Range<u32> {
+    pub fn buffer_rows_for_display_rows(
+        &self,
+        map: &DisplayMap,
+        ctx: &AppContext,
+    ) -> (Range<u32>, Range<u32>) {
         let display_start = self.start.to_display_point(map, ctx).unwrap();
         let buffer_start = DisplayPoint::new(display_start.row(), 0)
             .to_buffer_point(map, Bias::Left, ctx)
@@ -93,6 +97,9 @@ impl Selection {
         .to_buffer_point(map, Bias::Left, ctx)
         .unwrap();
 
-        buffer_start.row..buffer_end.row + 1
+        (
+            buffer_start.row..buffer_end.row + 1,
+            display_start.row()..display_end.row() + 1,
+        )
     }
 }

zed/src/editor/buffer_view.rs 🔗

@@ -46,6 +46,8 @@ pub fn init(app: &mut MutableAppContext) {
             Some("BufferView"),
         ),
         Binding::new("cmd-shift-D", "buffer:duplicate_line", Some("BufferView")),
+        Binding::new("ctrl-cmd-up", "buffer:move_line_up", Some("BufferView")),
+        Binding::new("ctrl-cmd-down", "buffer:move_line_down", Some("BufferView")),
         Binding::new("cmd-x", "buffer:cut", Some("BufferView")),
         Binding::new("cmd-c", "buffer:copy", Some("BufferView")),
         Binding::new("cmd-v", "buffer:paste", Some("BufferView")),
@@ -133,6 +135,8 @@ pub fn init(app: &mut MutableAppContext) {
         BufferView::delete_to_end_of_line,
     );
     app.add_action("buffer:duplicate_line", BufferView::duplicate_line);
+    app.add_action("buffer:move_line_up", BufferView::move_line_up);
+    app.add_action("buffer:move_line_down", BufferView::move_line_down);
     app.add_action("buffer:cut", BufferView::cut);
     app.add_action("buffer:copy", BufferView::copy);
     app.add_action("buffer:paste", BufferView::paste);
@@ -639,7 +643,7 @@ impl BufferView {
 
         let mut selections = self.selections(app).iter().peekable();
         while let Some(selection) = selections.next() {
-            let mut rows = selection.buffer_rows_for_display_rows(map, app);
+            let (mut rows, _) = selection.buffer_rows_for_display_rows(map, app);
             let goal_display_column = selection
                 .head()
                 .to_display_point(map, app)
@@ -648,7 +652,7 @@ impl BufferView {
 
             // Accumulate contiguous regions of rows that we want to delete.
             while let Some(next_selection) = selections.peek() {
-                let next_rows = next_selection.buffer_rows_for_display_rows(map, app);
+                let (next_rows, _) = next_selection.buffer_rows_for_display_rows(map, app);
                 if next_rows.start <= rows.end {
                     rows.end = next_rows.end;
                     selections.next().unwrap();
@@ -696,10 +700,10 @@ impl BufferView {
                 goal_column: None,
             })
             .collect();
-        self.update_selections(new_selections, true, ctx);
         self.buffer
             .update(ctx, |buffer, ctx| buffer.edit(edit_ranges, "", Some(ctx)))
             .unwrap();
+        self.update_selections(new_selections, true, ctx);
         self.end_transaction(ctx);
     }
 
@@ -726,9 +730,9 @@ impl BufferView {
         let mut selections_iter = selections.iter_mut().peekable();
         while let Some(selection) = selections_iter.next() {
             // Avoid duplicating the same lines twice.
-            let mut rows = selection.buffer_rows_for_display_rows(map, app);
+            let (mut rows, _) = selection.buffer_rows_for_display_rows(map, app);
             while let Some(next_selection) = selections_iter.peek() {
-                let next_rows = next_selection.buffer_rows_for_display_rows(map, app);
+                let (next_rows, _) = next_selection.buffer_rows_for_display_rows(map, app);
                 if next_rows.start <= rows.end - 1 {
                     rows.end = next_rows.end;
                     selections_iter.next().unwrap();
@@ -765,6 +769,156 @@ impl BufferView {
         self.end_transaction(ctx);
     }
 
+    pub fn move_line_up(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
+        self.start_transaction(ctx);
+
+        let app = ctx.as_ref();
+        let buffer = self.buffer.read(ctx);
+        let map = self.display_map.read(ctx);
+
+        let mut edits = Vec::new();
+        let selections = self.selections(ctx.as_ref()).to_vec();
+        let mut selections_iter = selections.iter().peekable();
+        while let Some(selection) = selections_iter.next() {
+            // Accumulate contiguous regions of rows that we want to move.
+            let (mut buffer_rows, mut display_rows) =
+                selection.buffer_rows_for_display_rows(map, app);
+            while let Some(next_selection) = selections_iter.peek() {
+                let (next_buffer_rows, next_display_rows) =
+                    next_selection.buffer_rows_for_display_rows(map, app);
+                if next_buffer_rows.start <= buffer_rows.end {
+                    buffer_rows.end = next_buffer_rows.end;
+                    display_rows.end = next_display_rows.end;
+                    selections_iter.next().unwrap();
+                } else {
+                    break;
+                }
+            }
+
+            // Cut the text from the previous line and paste it at the end of the selected region.
+            if display_rows.start != 0 {
+                let selection_line_end = Point::new(
+                    buffer_rows.end - 1,
+                    buffer.line_len(buffer_rows.end - 1).unwrap(),
+                )
+                .to_offset(buffer)
+                .unwrap();
+                let prev_line_display_start = DisplayPoint::new(display_rows.start - 1, 0);
+                let prev_line_display_end = DisplayPoint::new(
+                    prev_line_display_start.row(),
+                    map.line_len(prev_line_display_start.row(), app).unwrap(),
+                );
+                let prev_line_start = prev_line_display_start
+                    .to_buffer_offset(map, Bias::Left, app)
+                    .unwrap();
+                let prev_line_end = prev_line_display_end
+                    .to_buffer_offset(map, Bias::Right, app)
+                    .unwrap();
+
+                let mut text = String::new();
+                text.push('\n');
+                text.extend(
+                    buffer
+                        .text_for_range(prev_line_start..prev_line_end)
+                        .unwrap(),
+                );
+                edits.push((prev_line_start..prev_line_end + 1, String::new()));
+                edits.push((selection_line_end..selection_line_end, text));
+            }
+        }
+
+        self.buffer.update(ctx, |buffer, ctx| {
+            for (range, text) in edits.into_iter().rev() {
+                buffer.edit(Some(range), text, Some(ctx)).unwrap();
+            }
+        });
+        self.update_selections(selections, true, ctx);
+        self.end_transaction(ctx);
+    }
+
+    pub fn move_line_down(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
+        self.start_transaction(ctx);
+
+        let mut selections = self.selections(ctx.as_ref()).to_vec();
+        {
+            // Temporarily bias selections right to allow moved lines to push them down when the
+            // selections are at the beginning of a line.
+            let buffer = self.buffer.read(ctx);
+            for selection in &mut selections {
+                selection.start = selection.start.bias_right(buffer).unwrap();
+                selection.end = selection.end.bias_right(buffer).unwrap();
+            }
+        }
+        self.update_selections(selections.clone(), false, ctx);
+
+        let app = ctx.as_ref();
+        let buffer = self.buffer.read(ctx);
+        let map = self.display_map.read(ctx);
+
+        let mut edits = Vec::new();
+        let selections = self.selections(ctx.as_ref()).to_vec();
+        let mut selections_iter = selections.iter().peekable();
+        while let Some(selection) = selections_iter.next() {
+            // Accumulate contiguous regions of rows that we want to move.
+            let (mut buffer_rows, mut display_rows) =
+                selection.buffer_rows_for_display_rows(map, app);
+            while let Some(next_selection) = selections_iter.peek() {
+                let (next_buffer_rows, next_display_rows) =
+                    next_selection.buffer_rows_for_display_rows(map, app);
+                if next_buffer_rows.start <= buffer_rows.end {
+                    buffer_rows.end = next_buffer_rows.end;
+                    display_rows.end = next_display_rows.end;
+                    selections_iter.next().unwrap();
+                } else {
+                    break;
+                }
+            }
+
+            // Cut the text from the following line and paste it at the start of the selected region.
+            if buffer_rows.end <= buffer.max_point().row {
+                let selection_line_start =
+                    Point::new(buffer_rows.start, 0).to_offset(buffer).unwrap();
+                let next_line_display_end = DisplayPoint::new(
+                    display_rows.end,
+                    map.line_len(display_rows.end, app).unwrap(),
+                );
+                let next_line_start = Point::new(buffer_rows.end, 0).to_offset(buffer).unwrap();
+                let next_line_end = next_line_display_end
+                    .to_buffer_offset(map, Bias::Right, app)
+                    .unwrap();
+
+                let mut text = String::new();
+                text.extend(
+                    buffer
+                        .text_for_range(next_line_start..next_line_end)
+                        .unwrap(),
+                );
+                text.push('\n');
+                edits.push((selection_line_start..selection_line_start, text));
+                edits.push((next_line_start - 1..next_line_end, String::new()));
+            }
+        }
+
+        self.buffer.update(ctx, |buffer, ctx| {
+            for (range, text) in edits.into_iter().rev() {
+                buffer.edit(Some(range), text, Some(ctx)).unwrap();
+            }
+        });
+
+        let mut selections = self.selections(ctx.as_ref()).to_vec();
+        {
+            // Restore bias on selections.
+            let buffer = self.buffer.read(ctx);
+            for selection in &mut selections {
+                selection.start = selection.start.bias_left(buffer).unwrap();
+                selection.end = selection.end.bias_left(buffer).unwrap();
+            }
+        }
+        self.update_selections(selections, true, ctx);
+
+        self.end_transaction(ctx);
+    }
+
     pub fn cut(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
         self.start_transaction(ctx);
         let mut text = String::new();
@@ -1175,9 +1329,11 @@ impl BufferView {
     }
 
     pub fn move_to_beginning(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
+        let buffer = self.buffer.read(ctx);
+        let cursor = buffer.anchor_before(Point::new(0, 0)).unwrap();
         let selection = Selection {
-            start: Anchor::Start,
-            end: Anchor::Start,
+            start: cursor.clone(),
+            end: cursor,
             reversed: false,
             goal_column: None,
         };
@@ -1191,9 +1347,11 @@ impl BufferView {
     }
 
     pub fn move_to_end(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
+        let buffer = self.buffer.read(ctx);
+        let cursor = buffer.anchor_before(buffer.max_point()).unwrap();
         let selection = Selection {
-            start: Anchor::End,
-            end: Anchor::End,
+            start: cursor.clone(),
+            end: cursor,
             reversed: false,
             goal_column: None,
         };

zed/src/editor/display_map/fold_map.rs 🔗

@@ -169,6 +169,13 @@ impl FoldMap {
         false
     }
 
+    pub fn to_buffer_offset(&self, point: DisplayPoint, app: &AppContext) -> Result<usize> {
+        let mut cursor = self.transforms.cursor::<DisplayPoint, TransformSummary>();
+        cursor.seek(&point, SeekBias::Right);
+        let overshoot = point.0 - cursor.start().display.lines;
+        (cursor.start().buffer.lines + overshoot).to_offset(self.buffer.read(app))
+    }
+
     pub fn to_display_offset(
         &self,
         point: DisplayPoint,

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

@@ -179,6 +179,11 @@ impl DisplayPoint {
             .to_buffer_point(self.collapse_tabs(map, bias, app)?.0))
     }
 
+    pub fn to_buffer_offset(self, map: &DisplayMap, bias: Bias, app: &AppContext) -> Result<usize> {
+        map.fold_map
+            .to_buffer_offset(self.collapse_tabs(map, bias, app)?.0, app)
+    }
+
     fn expand_tabs(mut self, map: &DisplayMap, app: &AppContext) -> Result<Self> {
         let chars = map
             .fold_map