Use a block decoration for entering rename text

Max Brunsfeld and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

crates/editor/src/editor.rs         | 138 ++++++++++++++++++------------
crates/editor/src/element.rs        |  24 +++-
crates/theme/src/theme.rs           |   4 
crates/zed/assets/themes/_base.toml |   4 
crates/zed/assets/themes/black.toml |   2 
crates/zed/assets/themes/dark.toml  |   2 
crates/zed/assets/themes/light.toml |   2 
7 files changed, 113 insertions(+), 63 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -480,7 +480,9 @@ struct SnippetState {
 
 struct RenameState {
     range: Range<Anchor>,
-    first_transaction: Option<TransactionId>,
+    old_name: String,
+    editor: ViewHandle<Editor>,
+    block_id: BlockId,
 }
 
 struct InvalidationStack<T>(Vec<T>);
@@ -3161,6 +3163,26 @@ impl Editor {
     }
 
     pub fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
+        if let Some((range, column, _, _)) = self.take_rename(cx) {
+            let snapshot = self.buffer.read(cx).snapshot(cx);
+            let position = snapshot.clip_point(
+                range.start.to_point(&snapshot) + Point::new(0, column),
+                Bias::Left,
+            );
+            self.update_selections(
+                vec![Selection {
+                    id: self.newest_anchor_selection().id,
+                    start: position,
+                    end: position,
+                    reversed: false,
+                    goal: SelectionGoal::None,
+                }],
+                None,
+                cx,
+            );
+            return;
+        }
+
         if let Some(context_menu) = self.context_menu.as_mut() {
             if context_menu.select_prev(cx) {
                 return;
@@ -4118,20 +4140,51 @@ impl Editor {
                     let start = offset - lookbehind;
                     let end = offset + lookahead;
                     let rename_range = buffer.anchor_before(start)..buffer.anchor_after(end);
+                    let old_name = buffer.text_for_range(start..end).collect::<String>();
                     drop(buffer);
 
-                    this.buffer
-                        .update(cx, |buffer, cx| buffer.finalize_last_transaction(cx));
-                    this.pending_rename = Some(RenameState {
-                        range: rename_range.clone(),
-                        first_transaction: None,
+                    let editor = cx.add_view(|cx| {
+                        let mut editor = Editor::single_line(this.build_settings.clone(), cx);
+                        editor
+                            .buffer
+                            .update(cx, |buffer, cx| buffer.edit([0..0], &old_name, cx));
+                        editor.select_ranges([0..old_name.len()], None, cx);
+                        editor.highlight_ranges::<Rename>(
+                            vec![Anchor::min()..Anchor::max()],
+                            settings.style.diff_background_inserted,
+                            cx,
+                        );
+                        editor
                     });
-                    this.select_ranges([start..end], None, cx);
                     this.highlight_ranges::<Rename>(
-                        vec![rename_range],
-                        settings.style.highlighted_line_background,
+                        vec![rename_range.clone()],
+                        settings.style.diff_background_deleted,
                         cx,
                     );
+                    cx.focus(&editor);
+                    let block_id = this.insert_blocks(
+                        [BlockProperties {
+                            position: rename_range.start.clone(),
+                            height: 1,
+                            render: Arc::new({
+                                let editor = editor.clone();
+                                move |cx: &BlockContext| {
+                                    ChildView::new(editor.clone())
+                                        .contained()
+                                        .with_padding_left(cx.anchor_x)
+                                        .boxed()
+                                }
+                            }),
+                            disposition: BlockDisposition::Below,
+                        }],
+                        cx,
+                    )[0];
+                    this.pending_rename = Some(RenameState {
+                        range: rename_range,
+                        old_name,
+                        editor,
+                        block_id,
+                    });
                 });
             }
 
@@ -4146,13 +4199,13 @@ impl Editor {
     ) -> Option<Task<Result<()>>> {
         let editor = workspace.active_item(cx)?.act_as::<Editor>(cx)?;
 
-        let (buffer, range, new_name) = editor.update(cx, |editor, cx| {
-            let (range, new_name) = editor.take_rename(cx)?;
+        let (buffer, range, old_name, new_name) = editor.update(cx, |editor, cx| {
+            let (range, _, old_name, new_name) = editor.take_rename(cx)?;
             let buffer = editor.buffer.read(cx);
             let (start_buffer, start) = buffer.text_anchor_for_position(range.start.clone(), cx)?;
             let (end_buffer, end) = buffer.text_anchor_for_position(range.end.clone(), cx)?;
             if start_buffer == end_buffer {
-                Some((start_buffer, start..end, new_name))
+                Some((start_buffer, start..end, old_name, new_name))
             } else {
                 None
             }
@@ -4168,55 +4221,36 @@ impl Editor {
             )
         });
 
-        let transaction = buffer.update(cx, |buffer, cx| {
-            buffer.finalize_last_transaction();
-            buffer.start_transaction();
-            buffer.edit([range], &new_name, cx);
-            if buffer.end_transaction(cx).is_some() {
-                let transaction = buffer.finalize_last_transaction().unwrap().clone();
-                buffer.forget_transaction(transaction.id);
-                Some(transaction)
-            } else {
-                None
-            }
-        });
-
-        Some(cx.spawn(|workspace, mut cx| async move {
+        Some(cx.spawn(|workspace, cx| async move {
             let project_transaction = rename.await?;
-            if let Some(transaction) = transaction {
-                buffer.update(&mut cx, |buffer, cx| {
-                    buffer.push_transaction(transaction, Instant::now());
-                    buffer.undo(cx);
-                });
-            }
             Self::open_project_transaction(
                 editor,
                 workspace,
                 project_transaction,
-                format!("Rename: {}", new_name),
+                format!("Rename: {} → {}", old_name, new_name),
                 cx,
             )
             .await
         }))
     }
 
-    fn take_rename(&mut self, cx: &mut ViewContext<Self>) -> Option<(Range<Anchor>, String)> {
+    fn take_rename(
+        &mut self,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<(Range<Anchor>, u32, String, String)> {
         let rename = self.pending_rename.take()?;
-        let new_name = self
-            .buffer
-            .read(cx)
-            .read(cx)
-            .text_for_range(rename.range.clone())
-            .collect::<String>();
-
+        let editor = rename.editor.read(cx);
+        let new_name = editor.text(cx);
+        let buffer = editor.buffer.read(cx).snapshot(cx);
+        let rename_position = editor.newest_selection::<Point>(&buffer);
+        self.remove_blocks([rename.block_id].into_iter().collect(), cx);
         self.clear_highlighted_ranges::<Rename>(cx);
-        if let Some(transaction_id) = rename.first_transaction {
-            self.buffer.update(cx, |buffer, cx| {
-                buffer.undo_to_transaction(transaction_id, false, cx)
-            });
-        }
-
-        Some((rename.range, new_name))
+        Some((
+            rename.range,
+            rename_position.head().column,
+            rename.old_name,
+            new_name,
+        ))
     }
 
     fn invalidate_rename_range(
@@ -4722,12 +4756,6 @@ impl Editor {
             .buffer
             .update(cx, |buffer, cx| buffer.end_transaction_at(now, cx))
         {
-            if let Some(rename) = self.pending_rename.as_mut() {
-                if rename.first_transaction.is_none() {
-                    rename.first_transaction = Some(tx_id);
-                }
-            }
-
             if let Some((_, end_selections)) = self.selection_history.get_mut(&tx_id) {
                 *end_selections = Some(self.selections.clone());
             } else {
@@ -5146,6 +5174,8 @@ impl EditorSettings {
                     gutter_padding_factor: 2.,
                     active_line_background: Default::default(),
                     highlighted_line_background: Default::default(),
+                    diff_background_deleted: Default::default(),
+                    diff_background_inserted: Default::default(),
                     line_number: Default::default(),
                     line_number_active: Default::default(),
                     selection: Default::default(),

crates/editor/src/element.rs 🔗

@@ -299,7 +299,7 @@ impl EditorElement {
         if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() {
             let mut x = bounds.width() - layout.gutter_padding;
             let mut y = *row as f32 * layout.line_height - scroll_top;
-            x += ((layout.gutter_padding + layout.text_offset.x()) - indicator.size().x()) / 2.;
+            x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x()) / 2.;
             y += (layout.line_height - indicator.size().y()) / 2.;
             indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx);
         }
@@ -321,7 +321,7 @@ impl EditorElement {
         let end_row = ((scroll_top + bounds.height()) / layout.line_height).ceil() as u32 + 1; // Add 1 to ensure selections bleed off screen
         let max_glyph_width = layout.em_width;
         let scroll_left = scroll_position.x() * max_glyph_width;
-        let content_origin = bounds.origin() + layout.text_offset;
+        let content_origin = bounds.origin() + layout.gutter_margin;
 
         cx.scene.push_layer(Some(bounds));
 
@@ -776,22 +776,24 @@ impl Element for EditorElement {
 
         let gutter_padding;
         let gutter_width;
+        let gutter_margin;
         if snapshot.mode == EditorMode::Full {
             gutter_padding = style.text.em_width(cx.font_cache) * style.gutter_padding_factor;
             gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0;
+            gutter_margin = -style.text.descent(cx.font_cache);
         } else {
             gutter_padding = 0.0;
-            gutter_width = 0.0
+            gutter_width = 0.0;
+            gutter_margin = 0.0;
         };
 
         let text_width = size.x() - gutter_width;
-        let text_offset = vec2f(-style.text.descent(cx.font_cache), 0.);
         let em_width = style.text.em_width(cx.font_cache);
         let em_advance = style.text.em_advance(cx.font_cache);
         let overscroll = vec2f(em_width, 0.);
         let wrap_width = match self.settings.soft_wrap {
             SoftWrap::None => None,
-            SoftWrap::EditorWidth => Some(text_width - text_offset.x() - overscroll.x() - em_width),
+            SoftWrap::EditorWidth => Some(text_width - gutter_margin - overscroll.x() - em_width),
             SoftWrap::Column(column) => Some(column as f32 * em_advance),
         };
         let snapshot = self.update_view(cx.app, |view, cx| {
@@ -991,7 +993,7 @@ impl Element for EditorElement {
             gutter_padding,
             gutter_width,
             em_width,
-            gutter_width + text_offset.x(),
+            gutter_width + gutter_margin,
             line_height,
             &style,
             &line_layouts,
@@ -1006,7 +1008,7 @@ impl Element for EditorElement {
                 gutter_size,
                 gutter_padding,
                 text_size,
-                text_offset,
+                gutter_margin,
                 snapshot,
                 active_rows,
                 highlighted_rows,
@@ -1080,6 +1082,12 @@ impl Element for EditorElement {
             }
         }
 
+        for (_, block) in &mut layout.blocks {
+            if block.dispatch_event(event, cx) {
+                return true;
+            }
+        }
+
         match event {
             Event::LeftMouseDown {
                 position,
@@ -1123,6 +1131,7 @@ pub struct LayoutState {
     scroll_max: Vector2F,
     gutter_size: Vector2F,
     gutter_padding: f32,
+    gutter_margin: f32,
     text_size: Vector2F,
     snapshot: EditorSnapshot,
     active_rows: BTreeMap<u32, bool>,
@@ -1135,7 +1144,6 @@ pub struct LayoutState {
     em_advance: f32,
     highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
     selections: HashMap<ReplicaId, Vec<text::Selection<DisplayPoint>>>,
-    text_offset: Vector2F,
     context_menu: Option<(DisplayPoint, ElementBox)>,
     code_actions_indicator: Option<(u32, ElementBox)>,
 }

crates/theme/src/theme.rs 🔗

@@ -278,6 +278,8 @@ pub struct EditorStyle {
     pub gutter_padding_factor: f32,
     pub active_line_background: Color,
     pub highlighted_line_background: Color,
+    pub diff_background_deleted: Color,
+    pub diff_background_inserted: Color,
     pub line_number: Color,
     pub line_number_active: Color,
     pub guest_selections: Vec<SelectionStyle>,
@@ -383,6 +385,8 @@ impl InputEditorStyle {
             gutter_padding_factor: Default::default(),
             active_line_background: Default::default(),
             highlighted_line_background: Default::default(),
+            diff_background_deleted: Default::default(),
+            diff_background_inserted: Default::default(),
             line_number: Default::default(),
             line_number_active: Default::default(),
             guest_selections: Default::default(),

crates/zed/assets/themes/_base.toml 🔗

@@ -188,7 +188,7 @@ corner_radius = 6
 
 [project_panel]
 extends = "$panel"
-padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2
+padding.top = 6    # ($workspace.tab.height - $project_panel.entry.height) / 2
 
 [project_panel.entry]
 text = "$text.1"
@@ -248,6 +248,8 @@ gutter_background = "$surface.1"
 gutter_padding_factor = 2.5
 active_line_background = "$state.active_line"
 highlighted_line_background = "$state.highlighted_line"
+diff_background_deleted = "$state.deleted_line"
+diff_background_inserted = "$state.inserted_line"
 line_number = "$text.2.color"
 line_number_active = "$text.0.color"
 selection = "$selection.host"

crates/zed/assets/themes/black.toml 🔗

@@ -39,6 +39,8 @@ bad = "#b7372e"
 [state]
 active_line = "#161313"
 highlighted_line = "#faca5033"
+deleted_line = "#dd000022"
+inserted_line = "#00dd0022"
 hover = "#00000033"
 selected = "#00000088"
 

crates/zed/assets/themes/dark.toml 🔗

@@ -39,6 +39,8 @@ bad = "#b7372e"
 [state]
 active_line = "#00000022"
 highlighted_line = "#faca5033"
+deleted_line = "#dd000044"
+inserted_line = "#00dd0044"
 hover = "#00000033"
 selected = "#00000088"
 

crates/zed/assets/themes/light.toml 🔗

@@ -39,6 +39,8 @@ bad = "#b7372e"
 [state]
 active_line = "#00000008"
 highlighted_line = "#faca5033"
+deleted_line = "#dd000044"
+inserted_line = "#00dd0044"
 hover = "#0000000D"
 selected = "#0000001c"