Fix clicking in to agent message editor and tighten up vertical spacing (#32765)

Michael Sloan created

* Adds `min_lines` to `EditorMode::AutoHeight` and use `min_lines: 4` in
agent message editor. This makes it so that clicks in the blank space
below the first line of the editor also focus it, instead of needing to
click the very first line.

* Removes the div wrapping the editor, as it was only there to set
`min_h_16()`. This also tightens up the min space given to the editor -
before it was not evenly dividing the number of lines.

* Further tightens up vertical spacing by using `gap_1` instead of
`gap_4` between editor and controls below

At 4 line min height (after on the left, before on the right):


![image](https://github.com/user-attachments/assets/e8eefb1b-9ea3-4f98-ad55-25f95760d61f)

At 5 lines, one more than min height (after on the left, before on the
right):


![image](https://github.com/user-attachments/assets/a6ba737c-6a56-4343-a55a-d264f2a06377)

Release Notes:

- Agent: Fixed clicking to focus the message editor to also work for
clicks below the last line.

Change summary

crates/agent/src/active_thread.rs                                      |  5 
crates/agent/src/agent_configuration/configure_context_server_modal.rs |  2 
crates/agent/src/inline_prompt_editor.rs                               |  4 
crates/agent/src/message_editor.rs                                     | 68 
crates/collab_ui/src/chat_panel.rs                                     |  2 
crates/editor/src/editor.rs                                            | 16 
crates/editor/src/element.rs                                           | 14 
crates/git_ui/src/git_panel.rs                                         |  5 
crates/repl/src/notebook/cell.rs                                       |  5 
crates/storybook/src/stories/auto_height_editor.rs                     |  2 
10 files changed, 75 insertions(+), 48 deletions(-)

Detailed changes

crates/agent/src/active_thread.rs πŸ”—

@@ -1681,7 +1681,10 @@ impl ActiveThread {
 
         let editor = cx.new(|cx| {
             let mut editor = Editor::new(
-                editor::EditorMode::AutoHeight { max_lines: 4 },
+                editor::EditorMode::AutoHeight {
+                    min_lines: 1,
+                    max_lines: 4,
+                },
                 buffer,
                 None,
                 window,

crates/agent/src/agent_configuration/configure_context_server_modal.rs πŸ”—

@@ -89,7 +89,7 @@ impl ConfigureContextServerModal {
                         }),
                         settings_validator,
                         settings_editor: cx.new(|cx| {
-                            let mut editor = Editor::auto_height(16, window, cx);
+                            let mut editor = Editor::auto_height(1, 16, window, cx);
                             editor.set_text(config.default_settings.trim(), window, cx);
                             editor.set_show_gutter(false, cx);
                             editor.set_soft_wrap_mode(

crates/agent/src/inline_prompt_editor.rs πŸ”—

@@ -261,7 +261,7 @@ impl<T: 'static> PromptEditor<T> {
 
         let focus = self.editor.focus_handle(cx).contains_focused(window, cx);
         self.editor = cx.new(|cx| {
-            let mut editor = Editor::auto_height(Self::MAX_LINES as usize, window, cx);
+            let mut editor = Editor::auto_height(1, Self::MAX_LINES as usize, window, cx);
             editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
             editor.set_placeholder_text("Add a prompt…", cx);
             editor.set_text(prompt, window, cx);
@@ -872,6 +872,7 @@ impl PromptEditor<BufferCodegen> {
         let prompt_editor = cx.new(|cx| {
             let mut editor = Editor::new(
                 EditorMode::AutoHeight {
+                    min_lines: 1,
                     max_lines: Self::MAX_LINES as usize,
                 },
                 prompt_buffer,
@@ -1050,6 +1051,7 @@ impl PromptEditor<TerminalCodegen> {
         let prompt_editor = cx.new(|cx| {
             let mut editor = Editor::new(
                 EditorMode::AutoHeight {
+                    min_lines: 1,
                     max_lines: Self::MAX_LINES as usize,
                 },
                 prompt_buffer,

crates/agent/src/message_editor.rs πŸ”—

@@ -79,6 +79,7 @@ pub struct MessageEditor {
     _subscriptions: Vec<Subscription>,
 }
 
+const MIN_EDITOR_LINES: usize = 4;
 const MAX_EDITOR_LINES: usize = 8;
 
 pub(crate) fn create_editor(
@@ -102,6 +103,7 @@ pub(crate) fn create_editor(
         let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
         let mut editor = Editor::new(
             editor::EditorMode::AutoHeight {
+                min_lines: MIN_EDITOR_LINES,
                 max_lines: MAX_EDITOR_LINES,
             },
             buffer,
@@ -253,6 +255,7 @@ impl MessageEditor {
                 })
             } else {
                 editor.set_mode(EditorMode::AutoHeight {
+                    min_lines: MIN_EDITOR_LINES,
                     max_lines: MAX_EDITOR_LINES,
                 })
             }
@@ -671,44 +674,39 @@ impl MessageEditor {
             .child(
                 v_flex()
                     .size_full()
-                    .gap_4()
+                    .gap_1()
                     .when(is_editor_expanded, |this| {
                         this.h(vh(0.8, window)).justify_between()
                     })
-                    .child(
-                        v_flex()
-                            .min_h_16()
-                            .when(is_editor_expanded, |this| this.h_full())
-                            .child({
-                                let settings = ThemeSettings::get_global(cx);
-                                let font_size = TextSize::Small
-                                    .rems(cx)
-                                    .to_pixels(settings.agent_font_size(cx));
-                                let line_height = settings.buffer_line_height.value() * font_size;
-
-                                let text_style = TextStyle {
-                                    color: cx.theme().colors().text,
-                                    font_family: settings.buffer_font.family.clone(),
-                                    font_fallbacks: settings.buffer_font.fallbacks.clone(),
-                                    font_features: settings.buffer_font.features.clone(),
-                                    font_size: font_size.into(),
-                                    line_height: line_height.into(),
-                                    ..Default::default()
-                                };
-
-                                EditorElement::new(
-                                    &self.editor,
-                                    EditorStyle {
-                                        background: editor_bg_color,
-                                        local_player: cx.theme().players().local(),
-                                        text: text_style,
-                                        syntax: cx.theme().syntax().clone(),
-                                        ..Default::default()
-                                    },
-                                )
-                                .into_any()
-                            }),
-                    )
+                    .child({
+                        let settings = ThemeSettings::get_global(cx);
+                        let font_size = TextSize::Small
+                            .rems(cx)
+                            .to_pixels(settings.agent_font_size(cx));
+                        let line_height = settings.buffer_line_height.value() * font_size;
+
+                        let text_style = TextStyle {
+                            color: cx.theme().colors().text,
+                            font_family: settings.buffer_font.family.clone(),
+                            font_fallbacks: settings.buffer_font.fallbacks.clone(),
+                            font_features: settings.buffer_font.features.clone(),
+                            font_size: font_size.into(),
+                            line_height: line_height.into(),
+                            ..Default::default()
+                        };
+
+                        EditorElement::new(
+                            &self.editor,
+                            EditorStyle {
+                                background: editor_bg_color,
+                                local_player: cx.theme().players().local(),
+                                text: text_style,
+                                syntax: cx.theme().syntax().clone(),
+                                ..Default::default()
+                            },
+                        )
+                        .into_any()
+                    })
                     .child(
                         h_flex()
                             .flex_none()

crates/collab_ui/src/chat_panel.rs πŸ”—

@@ -90,7 +90,7 @@ impl ChatPanel {
                 languages.clone(),
                 user_store.clone(),
                 None,
-                cx.new(|cx| Editor::auto_height(4, window, cx)),
+                cx.new(|cx| Editor::auto_height(1, 4, window, cx)),
                 window,
                 cx,
             )

crates/editor/src/editor.rs πŸ”—

@@ -475,6 +475,7 @@ pub enum EditorMode {
         auto_width: bool,
     },
     AutoHeight {
+        min_lines: usize,
         max_lines: usize,
     },
     Full {
@@ -1609,11 +1610,19 @@ impl Editor {
         )
     }
 
-    pub fn auto_height(max_lines: usize, window: &mut Window, cx: &mut Context<Self>) -> Self {
+    pub fn auto_height(
+        min_lines: usize,
+        max_lines: usize,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Self {
         let buffer = cx.new(|cx| Buffer::local("", cx));
         let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
         Self::new(
-            EditorMode::AutoHeight { max_lines },
+            EditorMode::AutoHeight {
+                min_lines,
+                max_lines,
+            },
             buffer,
             None,
             window,
@@ -21884,7 +21893,7 @@ impl Render for Editor {
 
         let background = match self.mode {
             EditorMode::SingleLine { .. } => cx.theme().system().transparent,
-            EditorMode::AutoHeight { max_lines: _ } => cx.theme().system().transparent,
+            EditorMode::AutoHeight { .. } => cx.theme().system().transparent,
             EditorMode::Full { .. } => cx.theme().colors().editor_background,
             EditorMode::Minimap { .. } => cx.theme().colors().editor_background.opacity(0.7),
         };
@@ -22542,6 +22551,7 @@ impl BreakpointPromptEditor {
         let prompt = cx.new(|cx| {
             let mut prompt = Editor::new(
                 EditorMode::AutoHeight {
+                    min_lines: 1,
                     max_lines: Self::MAX_LINES as usize,
                 },
                 buffer,

crates/editor/src/element.rs πŸ”—

@@ -7681,7 +7681,10 @@ impl Element for EditorElement {
                             window.request_layout(style, None, cx)
                         }
                     }
-                    EditorMode::AutoHeight { max_lines } => {
+                    EditorMode::AutoHeight {
+                        min_lines,
+                        max_lines,
+                    } => {
                         let editor_handle = cx.entity().clone();
                         let max_line_number_width =
                             self.max_line_number_width(&editor.snapshot(window, cx), window, cx);
@@ -7692,6 +7695,7 @@ impl Element for EditorElement {
                                     .update(cx, |editor, cx| {
                                         compute_auto_height_layout(
                                             editor,
+                                            min_lines,
                                             max_lines,
                                             max_line_number_width,
                                             known_dimensions,
@@ -9864,6 +9868,7 @@ pub fn register_action<T: Action>(
 
 fn compute_auto_height_layout(
     editor: &mut Editor,
+    min_lines: usize,
     max_lines: usize,
     max_line_number_width: Pixels,
     known_dimensions: Size<Option<Pixels>>,
@@ -9911,7 +9916,7 @@ fn compute_auto_height_layout(
 
     let scroll_height = (snapshot.max_point().row().next_row().0 as f32) * line_height;
     let height = scroll_height
-        .max(line_height)
+        .max(line_height * min_lines as f32)
         .min(line_height * max_lines as f32);
 
     Some(size(width, height))
@@ -10214,7 +10219,10 @@ mod tests {
 
         for editor_mode_without_invisibles in [
             EditorMode::SingleLine { auto_width: false },
-            EditorMode::AutoHeight { max_lines: 100 },
+            EditorMode::AutoHeight {
+                min_lines: 1,
+                max_lines: 100,
+            },
         ] {
             for show_line_numbers in [true, false] {
                 let invisibles = collect_invisibles_from_new_editor(

crates/git_ui/src/git_panel.rs πŸ”—

@@ -377,7 +377,10 @@ pub(crate) fn commit_message_editor(
     let buffer = cx.new(|cx| MultiBuffer::singleton(commit_message_buffer, cx));
     let max_lines = if in_panel { MAX_PANEL_EDITOR_LINES } else { 18 };
     let mut commit_editor = Editor::new(
-        EditorMode::AutoHeight { max_lines },
+        EditorMode::AutoHeight {
+            min_lines: 1,
+            max_lines,
+        },
         buffer,
         None,
         window,

crates/repl/src/notebook/cell.rs πŸ”—

@@ -177,7 +177,10 @@ impl Cell {
 
                 let editor_view = cx.new(|cx| {
                     let mut editor = Editor::new(
-                        EditorMode::AutoHeight { max_lines: 1024 },
+                        EditorMode::AutoHeight {
+                            min_lines: 1,
+                            max_lines: 1024,
+                        },
                         multi_buffer,
                         None,
                         window,

crates/storybook/src/stories/auto_height_editor.rs πŸ”—

@@ -17,7 +17,7 @@ impl AutoHeightEditorStory {
         )]);
         cx.new(|cx| Self {
             editor: cx.new(|cx| {
-                let mut editor = Editor::auto_height(3, window, cx);
+                let mut editor = Editor::auto_height(1, 3, window, cx);
                 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
                 editor
             }),