Overhaul assistant panel (#2610)

Antonio Scandurra created

Closes
https://linear.app/zed-industries/issue/Z-2368/use-a-different-icon-for-the-assistant-panel
Closes
https://linear.app/zed-industries/issue/Z-2363/ship-the-assistant-only-on-preview
Closes
https://linear.app/zed-industries/issue/Z-2331/scrolling-makes-it-hard-to-read
Closes
https://linear.app/zed-industries/issue/Z-2306/allow-undo-and-collaboration-in-assistant

This pull request is a significant overhaul of the assistant panel,
which now uses a simple `Buffer` as opposed to a `MultiBuffer` to show
messages. Specifically, we track the start of each message with an
anchor located right after the newline (or `Anchor::MIN` for the first
message). When the anchor becomes invalid (that is, the newline is
deleted), we merge the message with the preceding ones. Crucially,
messages don't actually get deleted so that, if the newline anchor
becomes valid again (such as when undoing/redoing), we can restore the
messages as well.

As part of this overhaul, we are also improving the scrolling behavior
to maintain the viewport stable only when editing or moving the cursor,
but otherwise leave the scroll position unchanged when manually
scrolling up or down.

Note that with these changes, we are limiting access to the assistant to
users on preview (and dev), as we want to polish the behavior a little
more before shipping to the general public. Users on stable will still
be able to see the default settings/keybindings of the assistant, but I
think that's okay, as they won't be able to do anything with them.

Release Notes:

- Added support for undo/redo in the assistant (preview-only)
- Improved the scrolling behavior of the assistant when it was
generating responses. Now Zed will keep the viewport stable only when
editing or moving the cursor, but otherwise leave the scroll position
unchanged when manually scrolling up or down (preview-only)
- Changed the icon of the assistant panel (preview-only)

**Note for @JosephTLyons: given that we're feature flagging this, let's
make sure things on stable look reasonable and work correctly. Things to
look out for: ensure a stock installation works, changing the settings
on stable works, changing the keybinding on stable works.**

Change summary

assets/icons/robot_14.svg              |   4 
crates/ai/src/assistant.rs             | 836 ++++++++++++++++-----------
crates/diagnostics/src/diagnostics.rs  |   3 
crates/editor/src/editor.rs            |  52 
crates/editor/src/editor_tests.rs      |   1 
crates/editor/src/element.rs           | 263 +++-----
crates/editor/src/items.rs             |   2 
crates/editor/src/scroll.rs            |  23 
crates/editor/src/scroll/autoscroll.rs |  10 
crates/zed/src/zed.rs                  |  20 
10 files changed, 664 insertions(+), 550 deletions(-)

Detailed changes

assets/icons/robot_14.svg 🔗

@@ -0,0 +1,4 @@
+<svg width="14" height="16" viewBox="0 0 14 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2.5 4C2.5 2.89531 3.39688 2 4.5 2H9.5C10.6031 2 11.5 2.89531 11.5 4V8C11.5 9.10312 10.6031 10 9.5 10H4.5C3.39688 10 2.5 9.10312 2.5 8V4ZM5 4C4.44687 4 4 4.44687 4 5C4 5.55313 4.44687 6 5 6C5.55313 6 6 5.55313 6 5C6 4.44687 5.55313 4 5 4ZM9 6C9.55313 6 10 5.55313 10 5C10 4.44687 9.55313 4 9 4C8.44687 4 8 4.44687 8 5C8 5.55313 8.44687 6 9 6ZM5 8.5C5.275 8.5 5.5 8.275 5.5 8C5.5 7.725 5.275 7.5 5 7.5C4.725 7.5 4.5 7.725 4.5 8C4.5 8.275 4.725 8.5 5 8.5ZM7 7.5C6.725 7.5 6.5 7.725 6.5 8C6.5 8.275 6.725 8.5 7 8.5C7.275 8.5 7.5 8.275 7.5 8C7.5 7.725 7.275 7.5 7 7.5ZM9 8.5C9.275 8.5 9.5 8.275 9.5 8C9.5 7.725 9.275 7.5 9 7.5C8.725 7.5 8.5 7.725 8.5 8C8.5 8.275 8.725 8.5 9 8.5ZM0 14C0 12.3156 1.34312 11 3 11H11C12.6562 11 14 12.3156 14 14V15C14 15.5531 13.5531 16 13 16H11V14C11 13.4469 10.5531 13 10 13H4C3.44687 13 3 13.4469 3 14V16H1C0.447812 16 0 15.5531 0 15V14Z" fill="#808080"/>
+<path d="M7.5 2H6.5V0.5C6.5 0.22375 6.725 0 7 0C7.275 0 7.5 0.22375 7.5 0.5V2ZM1.5 4.5V7.5C1.5 7.775 1.27625 8 1 8C0.72375 8 0.5 7.775 0.5 7.5V4.5C0.5 4.225 0.72375 4 1 4C1.27625 4 1.5 4.225 1.5 4.5ZM5.5 16H4.5V14.5C4.5 14.225 4.725 14 5 14C5.275 14 5.5 14.225 5.5 14.5V16ZM7.5 16H6.5V14.5C6.5 14.225 6.725 14 7 14C7.275 14 7.5 14.225 7.5 14.5V16ZM9 14C9.275 14 9.5 14.225 9.5 14.5V16H8.5V14.5C8.5 14.225 8.725 14 9 14ZM13.5 7.5C13.5 7.775 13.275 8 13 8C12.725 8 12.5 7.775 12.5 7.5V4.5C12.5 4.225 12.725 4 13 4C13.275 4 13.5 4.225 13.5 4.5V7.5Z" fill="#808080"/>
+</svg>

crates/ai/src/assistant.rs 🔗

@@ -6,12 +6,9 @@ use anyhow::{anyhow, Result};
 use chrono::{DateTime, Local};
 use collections::{HashMap, HashSet};
 use editor::{
-    display_map::ToDisplayPoint,
-    scroll::{
-        autoscroll::{Autoscroll, AutoscrollStrategy},
-        ScrollAnchor,
-    },
-    Anchor, DisplayPoint, Editor, ExcerptId, ExcerptRange, MultiBuffer,
+    display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint},
+    scroll::autoscroll::{Autoscroll, AutoscrollStrategy},
+    Anchor, Editor, ToOffset as _,
 };
 use fs::Fs;
 use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt};
@@ -19,17 +16,20 @@ use gpui::{
     actions,
     elements::*,
     executor::Background,
-    geometry::vector::vec2f,
+    geometry::vector::{vec2f, Vector2F},
     platform::{CursorStyle, MouseButton},
     Action, AppContext, AsyncAppContext, ClipboardItem, Entity, ModelContext, ModelHandle,
     Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
 };
 use isahc::{http::StatusCode, Request, RequestExt};
-use language::{language_settings::SoftWrap, Buffer, LanguageRegistry};
+use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _};
 use serde::Deserialize;
 use settings::SettingsStore;
-use std::{borrow::Cow, cell::RefCell, cmp, fmt::Write, io, rc::Rc, sync::Arc, time::Duration};
-use util::{post_inc, truncate_and_trailoff, ResultExt, TryFutureExt};
+use std::{
+    borrow::Cow, cell::RefCell, cmp, fmt::Write, io, iter, ops::Range, rc::Rc, sync::Arc,
+    time::Duration,
+};
+use util::{channel::ReleaseChannel, post_inc, truncate_and_trailoff, ResultExt, TryFutureExt};
 use workspace::{
     dock::{DockPosition, Panel},
     item::Item,
@@ -44,6 +44,12 @@ actions!(
 );
 
 pub fn init(cx: &mut AppContext) {
+    if *util::channel::RELEASE_CHANNEL == ReleaseChannel::Stable {
+        cx.update_default_global::<collections::CommandPaletteFilter, _, _>(move |filter, _cx| {
+            filter.filtered_namespaces.insert("assistant");
+        });
+    }
+
     settings::register::<AssistantSettings>(cx);
     cx.add_action(
         |workspace: &mut Workspace, _: &NewContext, cx: &mut ViewContext<Workspace>| {
@@ -60,6 +66,11 @@ pub fn init(cx: &mut AppContext) {
     cx.capture_action(AssistantEditor::copy);
     cx.add_action(AssistantPanel::save_api_key);
     cx.add_action(AssistantPanel::reset_api_key);
+    cx.add_action(
+        |workspace: &mut Workspace, _: &ToggleFocus, cx: &mut ViewContext<Workspace>| {
+            workspace.toggle_panel_focus::<AssistantPanel>(cx);
+        },
+    );
 }
 
 pub enum AssistantPanelEvent {
@@ -387,7 +398,7 @@ impl Panel for AssistantPanel {
     }
 
     fn icon_path(&self) -> &'static str {
-        "icons/speech_bubble_12.svg"
+        "icons/robot_14.svg"
     }
 
     fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
@@ -420,20 +431,20 @@ impl Panel for AssistantPanel {
 }
 
 enum AssistantEvent {
-    MessagesEdited { ids: Vec<ExcerptId> },
+    MessagesEdited,
     SummaryChanged,
     StreamedCompletion,
 }
 
 struct Assistant {
-    buffer: ModelHandle<MultiBuffer>,
+    buffer: ModelHandle<Buffer>,
     messages: Vec<Message>,
-    messages_metadata: HashMap<ExcerptId, MessageMetadata>,
+    messages_metadata: HashMap<MessageId, MessageMetadata>,
+    next_message_id: MessageId,
     summary: Option<String>,
     pending_summary: Task<Option<()>>,
     completion_count: usize,
     pending_completions: Vec<PendingCompletion>,
-    languages: Arc<LanguageRegistry>,
     model: String,
     token_count: Option<usize>,
     max_token_count: usize,
@@ -453,15 +464,32 @@ impl Assistant {
         cx: &mut ModelContext<Self>,
     ) -> Self {
         let model = "gpt-3.5-turbo";
-        let buffer = cx.add_model(|_| MultiBuffer::new(0));
+        let markdown = language_registry.language_for_name("Markdown");
+        let buffer = cx.add_model(|cx| {
+            let mut buffer = Buffer::new(0, "", cx);
+            buffer.set_language_registry(language_registry);
+            cx.spawn_weak(|buffer, mut cx| async move {
+                let markdown = markdown.await?;
+                let buffer = buffer
+                    .upgrade(&cx)
+                    .ok_or_else(|| anyhow!("buffer was dropped"))?;
+                buffer.update(&mut cx, |buffer, cx| {
+                    buffer.set_language(Some(markdown), cx)
+                });
+                anyhow::Ok(())
+            })
+            .detach_and_log_err(cx);
+            buffer
+        });
+
         let mut this = Self {
             messages: Default::default(),
             messages_metadata: Default::default(),
+            next_message_id: Default::default(),
             summary: None,
             pending_summary: Task::ready(None),
             completion_count: Default::default(),
             pending_completions: Default::default(),
-            languages: language_registry,
             token_count: None,
             max_token_count: tiktoken_rs::model::get_context_size(model),
             pending_token_count: Task::ready(None),
@@ -470,23 +498,34 @@ impl Assistant {
             api_key,
             buffer,
         };
-        this.insert_message_after(ExcerptId::max(), Role::User, cx);
+        let message = Message {
+            id: MessageId(post_inc(&mut this.next_message_id.0)),
+            start: language::Anchor::MIN,
+        };
+        this.messages.push(message.clone());
+        this.messages_metadata.insert(
+            message.id,
+            MessageMetadata {
+                role: Role::User,
+                sent_at: Local::now(),
+                error: None,
+            },
+        );
+
         this.count_remaining_tokens(cx);
         this
     }
 
     fn handle_buffer_event(
         &mut self,
-        _: ModelHandle<MultiBuffer>,
-        event: &editor::multi_buffer::Event,
+        _: ModelHandle<Buffer>,
+        event: &language::Event,
         cx: &mut ModelContext<Self>,
     ) {
         match event {
-            editor::multi_buffer::Event::ExcerptsAdded { .. }
-            | editor::multi_buffer::Event::ExcerptsRemoved { .. }
-            | editor::multi_buffer::Event::Edited => self.count_remaining_tokens(cx),
-            editor::multi_buffer::Event::ExcerptsEdited { ids } => {
-                cx.emit(AssistantEvent::MessagesEdited { ids: ids.clone() });
+            language::Event::Edited => {
+                self.count_remaining_tokens(cx);
+                cx.emit(AssistantEvent::MessagesEdited);
             }
             _ => {}
         }
@@ -494,16 +533,16 @@ impl Assistant {
 
     fn count_remaining_tokens(&mut self, cx: &mut ModelContext<Self>) {
         let messages = self
-            .messages
-            .iter()
+            .open_ai_request_messages(cx)
+            .into_iter()
             .filter_map(|message| {
                 Some(tiktoken_rs::ChatCompletionRequestMessage {
-                    role: match self.messages_metadata.get(&message.excerpt_id)?.role {
+                    role: match message.role {
                         Role::User => "user".into(),
                         Role::Assistant => "assistant".into(),
                         Role::System => "system".into(),
                     },
-                    content: message.content.read(cx).text(),
+                    content: message.content,
                     name: None,
                 })
             })
@@ -541,45 +580,48 @@ impl Assistant {
     }
 
     fn assist(&mut self, cx: &mut ModelContext<Self>) -> Option<(Message, Message)> {
-        let messages = self
-            .messages
-            .iter()
-            .filter_map(|message| {
-                Some(RequestMessage {
-                    role: self.messages_metadata.get(&message.excerpt_id)?.role,
-                    content: message.content.read(cx).text(),
-                })
-            })
-            .collect();
         let request = OpenAIRequest {
             model: self.model.clone(),
-            messages,
+            messages: self.open_ai_request_messages(cx),
             stream: true,
         };
 
         let api_key = self.api_key.borrow().clone()?;
         let stream = stream_completion(api_key, cx.background().clone(), request);
-        let assistant_message = self.insert_message_after(ExcerptId::max(), Role::Assistant, cx);
-        let user_message = self.insert_message_after(ExcerptId::max(), Role::User, cx);
+        let assistant_message =
+            self.insert_message_after(self.messages.last()?.id, Role::Assistant, cx)?;
+        let user_message = self.insert_message_after(assistant_message.id, Role::User, cx)?;
         let task = cx.spawn_weak({
-            let assistant_message = assistant_message.clone();
             |this, mut cx| async move {
-                let assistant_message = assistant_message;
+                let assistant_message_id = assistant_message.id;
                 let stream_completion = async {
                     let mut messages = stream.await?;
 
                     while let Some(message) = messages.next().await {
                         let mut message = message?;
                         if let Some(choice) = message.choices.pop() {
-                            assistant_message.content.update(&mut cx, |content, cx| {
-                                let text: Arc<str> = choice.delta.content?.into();
-                                content.edit([(content.len()..content.len(), text)], None, cx);
-                                Some(())
-                            });
                             this.upgrade(&cx)
                                 .ok_or_else(|| anyhow!("assistant was dropped"))?
-                                .update(&mut cx, |_, cx| {
+                                .update(&mut cx, |this, cx| {
+                                    let text: Arc<str> = choice.delta.content?.into();
+                                    let message_ix = this
+                                        .messages
+                                        .iter()
+                                        .position(|message| message.id == assistant_message_id)?;
+                                    this.buffer.update(cx, |buffer, cx| {
+                                        let offset = if message_ix + 1 == this.messages.len() {
+                                            buffer.len()
+                                        } else {
+                                            this.messages[message_ix + 1]
+                                                .start
+                                                .to_offset(buffer)
+                                                .saturating_sub(1)
+                                        };
+                                        buffer.edit([(offset..offset, text)], None, cx);
+                                    });
                                     cx.emit(AssistantEvent::StreamedCompletion);
+
+                                    Some(())
                                 });
                         }
                     }
@@ -599,9 +641,8 @@ impl Assistant {
                 if let Some(this) = this.upgrade(&cx) {
                     this.update(&mut cx, |this, cx| {
                         if let Err(error) = result {
-                            if let Some(metadata) = this
-                                .messages_metadata
-                                .get_mut(&assistant_message.excerpt_id)
+                            if let Some(metadata) =
+                                this.messages_metadata.get_mut(&assistant_message.id)
                             {
                                 metadata.error = Some(error.to_string().trim().into());
                                 cx.notify();
@@ -623,124 +664,64 @@ impl Assistant {
         self.pending_completions.pop().is_some()
     }
 
-    fn remove_empty_messages<'a>(
-        &mut self,
-        excerpts: HashSet<ExcerptId>,
-        protected_offsets: HashSet<usize>,
-        cx: &mut ModelContext<Self>,
-    ) {
-        let mut offset = 0;
-        let mut excerpts_to_remove = Vec::new();
-        self.messages.retain(|message| {
-            let range = offset..offset + message.content.read(cx).len();
-            offset = range.end + 1;
-            if range.is_empty()
-                && !protected_offsets.contains(&range.start)
-                && excerpts.contains(&message.excerpt_id)
-            {
-                excerpts_to_remove.push(message.excerpt_id);
-                self.messages_metadata.remove(&message.excerpt_id);
-                false
-            } else {
-                true
-            }
-        });
-
-        if !excerpts_to_remove.is_empty() {
-            self.buffer.update(cx, |buffer, cx| {
-                buffer.remove_excerpts(excerpts_to_remove, cx)
-            });
-            cx.notify();
-        }
-    }
-
-    fn cycle_message_role(&mut self, excerpt_id: ExcerptId, cx: &mut ModelContext<Self>) {
-        if let Some(metadata) = self.messages_metadata.get_mut(&excerpt_id) {
+    fn cycle_message_role(&mut self, id: MessageId, cx: &mut ModelContext<Self>) {
+        if let Some(metadata) = self.messages_metadata.get_mut(&id) {
             metadata.role.cycle();
+            cx.emit(AssistantEvent::MessagesEdited);
             cx.notify();
         }
     }
 
     fn insert_message_after(
         &mut self,
-        excerpt_id: ExcerptId,
+        message_id: MessageId,
         role: Role,
         cx: &mut ModelContext<Self>,
-    ) -> Message {
-        let content = cx.add_model(|cx| {
-            let mut buffer = Buffer::new(0, "", cx);
-            let markdown = self.languages.language_for_name("Markdown");
-            cx.spawn_weak(|buffer, mut cx| async move {
-                let markdown = markdown.await?;
-                let buffer = buffer
-                    .upgrade(&cx)
-                    .ok_or_else(|| anyhow!("buffer was dropped"))?;
-                buffer.update(&mut cx, |buffer, cx| {
-                    buffer.set_language(Some(markdown), cx)
-                });
-                anyhow::Ok(())
-            })
-            .detach_and_log_err(cx);
-            buffer.set_language_registry(self.languages.clone());
-            buffer
-        });
-        let new_excerpt_id = self.buffer.update(cx, |buffer, cx| {
-            buffer
-                .insert_excerpts_after(
-                    excerpt_id,
-                    content.clone(),
-                    vec![ExcerptRange {
-                        context: 0..0,
-                        primary: None,
-                    }],
-                    cx,
-                )
-                .pop()
-                .unwrap()
-        });
-
-        let ix = self
+    ) -> Option<Message> {
+        if let Some(prev_message_ix) = self
             .messages
             .iter()
-            .position(|message| message.excerpt_id == excerpt_id)
-            .map_or(self.messages.len(), |ix| ix + 1);
-        let message = Message {
-            excerpt_id: new_excerpt_id,
-            content: content.clone(),
-        };
-        self.messages.insert(ix, message.clone());
-        self.messages_metadata.insert(
-            new_excerpt_id,
-            MessageMetadata {
-                role,
-                sent_at: Local::now(),
-                error: None,
-            },
-        );
-        message
+            .position(|message| message.id == message_id)
+        {
+            let start = self.buffer.update(cx, |buffer, cx| {
+                let offset = self.messages[prev_message_ix + 1..]
+                    .iter()
+                    .find(|message| message.start.is_valid(buffer))
+                    .map_or(buffer.len(), |message| message.start.to_offset(buffer) - 1);
+                buffer.edit([(offset..offset, "\n")], None, cx);
+                buffer.anchor_before(offset + 1)
+            });
+            let message = Message {
+                id: MessageId(post_inc(&mut self.next_message_id.0)),
+                start,
+            };
+            self.messages.insert(prev_message_ix + 1, message.clone());
+            self.messages_metadata.insert(
+                message.id,
+                MessageMetadata {
+                    role,
+                    sent_at: Local::now(),
+                    error: None,
+                },
+            );
+            cx.emit(AssistantEvent::MessagesEdited);
+            Some(message)
+        } else {
+            None
+        }
     }
 
     fn summarize(&mut self, cx: &mut ModelContext<Self>) {
         if self.messages.len() >= 2 && self.summary.is_none() {
             let api_key = self.api_key.borrow().clone();
             if let Some(api_key) = api_key {
-                let messages = self
-                    .messages
-                    .iter()
-                    .take(2)
-                    .filter_map(|message| {
-                        Some(RequestMessage {
-                            role: self.messages_metadata.get(&message.excerpt_id)?.role,
-                            content: message.content.read(cx).text(),
-                        })
-                    })
-                    .chain(Some(RequestMessage {
-                        role: Role::User,
-                        content:
-                            "Summarize the conversation into a short title without punctuation"
-                                .into(),
-                    }))
-                    .collect();
+                let mut messages = self.open_ai_request_messages(cx);
+                messages.truncate(2);
+                messages.push(RequestMessage {
+                    role: Role::User,
+                    content: "Summarize the conversation into a short title without punctuation"
+                        .into(),
+                });
                 let request = OpenAIRequest {
                     model: self.model.clone(),
                     messages,
@@ -770,6 +751,54 @@ impl Assistant {
             }
         }
     }
+
+    fn open_ai_request_messages(&self, cx: &AppContext) -> Vec<RequestMessage> {
+        let buffer = self.buffer.read(cx);
+        self.messages(cx)
+            .map(|(_message, metadata, range)| RequestMessage {
+                role: metadata.role,
+                content: buffer.text_for_range(range).collect(),
+            })
+            .collect()
+    }
+
+    fn message_id_for_offset(&self, offset: usize, cx: &AppContext) -> Option<MessageId> {
+        Some(
+            self.messages(cx)
+                .find(|(_, _, range)| range.contains(&offset))
+                .map(|(message, _, _)| message)
+                .or(self.messages.last())?
+                .id,
+        )
+    }
+
+    fn messages<'a>(
+        &'a self,
+        cx: &'a AppContext,
+    ) -> impl 'a + Iterator<Item = (&Message, &MessageMetadata, Range<usize>)> {
+        let buffer = self.buffer.read(cx);
+        let mut messages = self.messages.iter().peekable();
+        iter::from_fn(move || {
+            while let Some(message) = messages.next() {
+                let metadata = self.messages_metadata.get(&message.id)?;
+                let message_start = message.start.to_offset(buffer);
+                let mut message_end = None;
+                while let Some(next_message) = messages.peek() {
+                    if next_message.start.is_valid(buffer) {
+                        message_end = Some(next_message.start);
+                        break;
+                    } else {
+                        messages.next();
+                    }
+                }
+                let message_end = message_end
+                    .unwrap_or(language::Anchor::MAX)
+                    .to_offset(buffer);
+                return Some((message, metadata, message_start..message_end));
+            }
+            None
+        })
+    }
 }
 
 struct PendingCompletion {
@@ -781,10 +810,17 @@ enum AssistantEditorEvent {
     TabContentChanged,
 }
 
+#[derive(Copy, Clone, Debug, PartialEq)]
+struct ScrollPosition {
+    offset_before_cursor: Vector2F,
+    cursor: Anchor,
+}
+
 struct AssistantEditor {
     assistant: ModelHandle<Assistant>,
     editor: ViewHandle<Editor>,
-    scroll_bottom: ScrollAnchor,
+    blocks: HashSet<BlockId>,
+    scroll_position: Option<ScrollPosition>,
     _subscriptions: Vec<Subscription>,
 }
 
@@ -796,98 +832,9 @@ impl AssistantEditor {
     ) -> Self {
         let assistant = cx.add_model(|cx| Assistant::new(api_key, language_registry, cx));
         let editor = cx.add_view(|cx| {
-            let mut editor = Editor::for_multibuffer(assistant.read(cx).buffer.clone(), None, cx);
+            let mut editor = Editor::for_buffer(assistant.read(cx).buffer.clone(), None, cx);
             editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
             editor.set_show_gutter(false, cx);
-            editor.set_render_excerpt_header(
-                {
-                    let assistant = assistant.clone();
-                    move |_editor, params: editor::RenderExcerptHeaderParams, cx| {
-                        enum Sender {}
-                        enum ErrorTooltip {}
-
-                        let theme = theme::current(cx);
-                        let style = &theme.assistant;
-                        let excerpt_id = params.id;
-                        if let Some(metadata) = assistant
-                            .read(cx)
-                            .messages_metadata
-                            .get(&excerpt_id)
-                            .cloned()
-                        {
-                            let sender = MouseEventHandler::<Sender, _>::new(
-                                params.id.into(),
-                                cx,
-                                |state, _| match metadata.role {
-                                    Role::User => {
-                                        let style = style.user_sender.style_for(state, false);
-                                        Label::new("You", style.text.clone())
-                                            .contained()
-                                            .with_style(style.container)
-                                    }
-                                    Role::Assistant => {
-                                        let style = style.assistant_sender.style_for(state, false);
-                                        Label::new("Assistant", style.text.clone())
-                                            .contained()
-                                            .with_style(style.container)
-                                    }
-                                    Role::System => {
-                                        let style = style.system_sender.style_for(state, false);
-                                        Label::new("System", style.text.clone())
-                                            .contained()
-                                            .with_style(style.container)
-                                    }
-                                },
-                            )
-                            .with_cursor_style(CursorStyle::PointingHand)
-                            .on_down(MouseButton::Left, {
-                                let assistant = assistant.clone();
-                                move |_, _, cx| {
-                                    assistant.update(cx, |assistant, cx| {
-                                        assistant.cycle_message_role(excerpt_id, cx)
-                                    })
-                                }
-                            });
-
-                            Flex::row()
-                                .with_child(sender.aligned())
-                                .with_child(
-                                    Label::new(
-                                        metadata.sent_at.format("%I:%M%P").to_string(),
-                                        style.sent_at.text.clone(),
-                                    )
-                                    .contained()
-                                    .with_style(style.sent_at.container)
-                                    .aligned(),
-                                )
-                                .with_children(metadata.error.map(|error| {
-                                    Svg::new("icons/circle_x_mark_12.svg")
-                                        .with_color(style.error_icon.color)
-                                        .constrained()
-                                        .with_width(style.error_icon.width)
-                                        .contained()
-                                        .with_style(style.error_icon.container)
-                                        .with_tooltip::<ErrorTooltip>(
-                                            params.id.into(),
-                                            error,
-                                            None,
-                                            theme.tooltip.clone(),
-                                            cx,
-                                        )
-                                        .aligned()
-                                }))
-                                .aligned()
-                                .left()
-                                .contained()
-                                .with_style(style.header)
-                                .into_any()
-                        } else {
-                            Empty::new().into_any()
-                        }
-                    }
-                },
-                cx,
-            );
             editor
         });
 
@@ -897,60 +844,48 @@ impl AssistantEditor {
             cx.subscribe(&editor, Self::handle_editor_event),
         ];
 
-        Self {
+        let mut this = Self {
             assistant,
             editor,
-            scroll_bottom: ScrollAnchor {
-                offset: Default::default(),
-                anchor: Anchor::max(),
-            },
+            blocks: Default::default(),
+            scroll_position: None,
             _subscriptions,
-        }
+        };
+        this.update_message_headers(cx);
+        this
     }
 
     fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
         let user_message = self.assistant.update(cx, |assistant, cx| {
             let editor = self.editor.read(cx);
-            let newest_selection = editor.selections.newest_anchor();
-            let excerpt_id = if newest_selection.head() == Anchor::min() {
-                assistant
-                    .messages
-                    .first()
-                    .map(|message| message.excerpt_id)?
-            } else if newest_selection.head() == Anchor::max() {
-                assistant
-                    .messages
-                    .last()
-                    .map(|message| message.excerpt_id)?
-            } else {
-                newest_selection.head().excerpt_id()
-            };
-
-            let metadata = assistant.messages_metadata.get(&excerpt_id)?;
+            let newest_selection = editor
+                .selections
+                .newest_anchor()
+                .head()
+                .to_offset(&editor.buffer().read(cx).snapshot(cx));
+            let message_id = assistant.message_id_for_offset(newest_selection, cx)?;
+            let metadata = assistant.messages_metadata.get(&message_id)?;
             let user_message = if metadata.role == Role::User {
                 let (_, user_message) = assistant.assist(cx)?;
                 user_message
             } else {
-                let user_message = assistant.insert_message_after(excerpt_id, Role::User, cx);
+                let user_message = assistant.insert_message_after(message_id, Role::User, cx)?;
                 user_message
             };
             Some(user_message)
         });
 
         if let Some(user_message) = user_message {
+            let cursor = user_message
+                .start
+                .to_offset(&self.assistant.read(cx).buffer.read(cx));
             self.editor.update(cx, |editor, cx| {
-                let cursor = editor
-                    .buffer()
-                    .read(cx)
-                    .snapshot(cx)
-                    .anchor_in_excerpt(user_message.excerpt_id, language::Anchor::MIN);
                 editor.change_selections(
                     Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)),
                     cx,
-                    |selections| selections.select_anchor_ranges([cursor..cursor]),
+                    |selections| selections.select_ranges([cursor..cursor]),
                 );
             });
-            self.update_scroll_bottom(cx);
         }
     }
 
@@ -970,34 +905,22 @@ impl AssistantEditor {
         cx: &mut ViewContext<Self>,
     ) {
         match event {
-            AssistantEvent::MessagesEdited { ids } => {
-                let selections = self.editor.read(cx).selections.all::<usize>(cx);
-                let selection_heads = selections
-                    .iter()
-                    .map(|selection| selection.head())
-                    .collect::<HashSet<usize>>();
-                let ids = ids.iter().copied().collect::<HashSet<_>>();
-                self.assistant.update(cx, |assistant, cx| {
-                    assistant.remove_empty_messages(ids, selection_heads, cx)
-                });
-            }
+            AssistantEvent::MessagesEdited => self.update_message_headers(cx),
             AssistantEvent::SummaryChanged => {
                 cx.emit(AssistantEditorEvent::TabContentChanged);
             }
             AssistantEvent::StreamedCompletion => {
                 self.editor.update(cx, |editor, cx| {
-                    let snapshot = editor.snapshot(cx);
-                    let scroll_bottom_row = self
-                        .scroll_bottom
-                        .anchor
-                        .to_display_point(&snapshot.display_snapshot)
-                        .row();
-
-                    let scroll_bottom = scroll_bottom_row as f32 + self.scroll_bottom.offset.y();
-                    let visible_line_count = editor.visible_line_count().unwrap_or(0.);
-                    let scroll_top = scroll_bottom - visible_line_count;
-                    editor
-                        .set_scroll_position(vec2f(self.scroll_bottom.offset.x(), scroll_top), cx);
+                    if let Some(scroll_position) = self.scroll_position {
+                        let snapshot = editor.snapshot(cx);
+                        let cursor_point = scroll_position.cursor.to_display_point(&snapshot);
+                        let scroll_top =
+                            cursor_point.row() as f32 - scroll_position.offset_before_cursor.y();
+                        editor.set_scroll_position(
+                            vec2f(scroll_position.offset_before_cursor.x(), scroll_top),
+                            cx,
+                        );
+                    }
                 });
             }
         }
@@ -1010,34 +933,145 @@ impl AssistantEditor {
         cx: &mut ViewContext<Self>,
     ) {
         match event {
-            editor::Event::ScrollPositionChanged { .. } => self.update_scroll_bottom(cx),
+            editor::Event::ScrollPositionChanged { autoscroll, .. } => {
+                let cursor_scroll_position = self.cursor_scroll_position(cx);
+                if *autoscroll {
+                    self.scroll_position = cursor_scroll_position;
+                } else if self.scroll_position != cursor_scroll_position {
+                    self.scroll_position = None;
+                }
+            }
+            editor::Event::SelectionsChanged { .. } => {
+                self.scroll_position = self.cursor_scroll_position(cx);
+            }
             _ => {}
         }
     }
 
-    fn update_scroll_bottom(&mut self, cx: &mut ViewContext<Self>) {
+    fn cursor_scroll_position(&self, cx: &mut ViewContext<Self>) -> Option<ScrollPosition> {
         self.editor.update(cx, |editor, cx| {
             let snapshot = editor.snapshot(cx);
+            let cursor = editor.selections.newest_anchor().head();
+            let cursor_row = cursor.to_display_point(&snapshot.display_snapshot).row() as f32;
             let scroll_position = editor
                 .scroll_manager
                 .anchor()
                 .scroll_position(&snapshot.display_snapshot);
+
             let scroll_bottom = scroll_position.y() + editor.visible_line_count().unwrap_or(0.);
-            let scroll_bottom_point = cmp::min(
-                DisplayPoint::new(scroll_bottom.floor() as u32, 0),
-                snapshot.display_snapshot.max_point(),
-            );
-            let scroll_bottom_anchor = snapshot
-                .buffer_snapshot
-                .anchor_after(scroll_bottom_point.to_point(&snapshot.display_snapshot));
-            let scroll_bottom_offset = vec2f(
-                scroll_position.x(),
-                scroll_bottom - scroll_bottom_point.row() as f32,
-            );
-            self.scroll_bottom = ScrollAnchor {
-                anchor: scroll_bottom_anchor,
-                offset: scroll_bottom_offset,
-            };
+            if (scroll_position.y()..scroll_bottom).contains(&cursor_row) {
+                Some(ScrollPosition {
+                    cursor,
+                    offset_before_cursor: vec2f(
+                        scroll_position.x(),
+                        cursor_row - scroll_position.y(),
+                    ),
+                })
+            } else {
+                None
+            }
+        })
+    }
+
+    fn update_message_headers(&mut self, cx: &mut ViewContext<Self>) {
+        self.editor.update(cx, |editor, cx| {
+            let buffer = editor.buffer().read(cx).snapshot(cx);
+            let excerpt_id = *buffer.as_singleton().unwrap().0;
+            let old_blocks = std::mem::take(&mut self.blocks);
+            let new_blocks = self
+                .assistant
+                .read(cx)
+                .messages(cx)
+                .map(|(message, metadata, _)| BlockProperties {
+                    position: buffer.anchor_in_excerpt(excerpt_id, message.start),
+                    height: 2,
+                    style: BlockStyle::Sticky,
+                    render: Arc::new({
+                        let assistant = self.assistant.clone();
+                        let metadata = metadata.clone();
+                        let message = message.clone();
+                        move |cx| {
+                            enum Sender {}
+                            enum ErrorTooltip {}
+
+                            let theme = theme::current(cx);
+                            let style = &theme.assistant;
+                            let message_id = message.id;
+                            let sender = MouseEventHandler::<Sender, _>::new(
+                                message_id.0,
+                                cx,
+                                |state, _| match metadata.role {
+                                    Role::User => {
+                                        let style = style.user_sender.style_for(state, false);
+                                        Label::new("You", style.text.clone())
+                                            .contained()
+                                            .with_style(style.container)
+                                    }
+                                    Role::Assistant => {
+                                        let style = style.assistant_sender.style_for(state, false);
+                                        Label::new("Assistant", style.text.clone())
+                                            .contained()
+                                            .with_style(style.container)
+                                    }
+                                    Role::System => {
+                                        let style = style.system_sender.style_for(state, false);
+                                        Label::new("System", style.text.clone())
+                                            .contained()
+                                            .with_style(style.container)
+                                    }
+                                },
+                            )
+                            .with_cursor_style(CursorStyle::PointingHand)
+                            .on_down(MouseButton::Left, {
+                                let assistant = assistant.clone();
+                                move |_, _, cx| {
+                                    assistant.update(cx, |assistant, cx| {
+                                        assistant.cycle_message_role(message_id, cx)
+                                    })
+                                }
+                            });
+
+                            Flex::row()
+                                .with_child(sender.aligned())
+                                .with_child(
+                                    Label::new(
+                                        metadata.sent_at.format("%I:%M%P").to_string(),
+                                        style.sent_at.text.clone(),
+                                    )
+                                    .contained()
+                                    .with_style(style.sent_at.container)
+                                    .aligned(),
+                                )
+                                .with_children(metadata.error.clone().map(|error| {
+                                    Svg::new("icons/circle_x_mark_12.svg")
+                                        .with_color(style.error_icon.color)
+                                        .constrained()
+                                        .with_width(style.error_icon.width)
+                                        .contained()
+                                        .with_style(style.error_icon.container)
+                                        .with_tooltip::<ErrorTooltip>(
+                                            message_id.0,
+                                            error,
+                                            None,
+                                            theme.tooltip.clone(),
+                                            cx,
+                                        )
+                                        .aligned()
+                                }))
+                                .aligned()
+                                .left()
+                                .contained()
+                                .with_style(style.header)
+                                .into_any()
+                        }
+                    }),
+                    disposition: BlockDisposition::Above,
+                })
+                .collect::<Vec<_>>();
+
+            editor.remove_blocks(old_blocks, None, cx);
+            let ids = editor.insert_blocks(new_blocks, None, cx);
+            self.blocks = HashSet::from_iter(ids);
         });
     }
 
@@ -1111,33 +1145,23 @@ impl AssistantEditor {
         let assistant = self.assistant.read(cx);
         if editor.selections.count() == 1 {
             let selection = editor.selections.newest::<usize>(cx);
-            let mut offset = 0;
             let mut copied_text = String::new();
             let mut spanned_messages = 0;
-            for message in &assistant.messages {
-                let message_range = offset..offset + message.content.read(cx).len() + 1;
-
+            for (_message, metadata, message_range) in assistant.messages(cx) {
                 if message_range.start >= selection.range().end {
                     break;
                 } else if message_range.end >= selection.range().start {
                     let range = cmp::max(message_range.start, selection.range().start)
                         ..cmp::min(message_range.end, selection.range().end);
                     if !range.is_empty() {
-                        if let Some(metadata) = assistant.messages_metadata.get(&message.excerpt_id)
-                        {
-                            spanned_messages += 1;
-                            write!(&mut copied_text, "## {}\n\n", metadata.role).unwrap();
-                            for chunk in
-                                assistant.buffer.read(cx).snapshot(cx).text_for_range(range)
-                            {
-                                copied_text.push_str(&chunk);
-                            }
-                            copied_text.push('\n');
+                        spanned_messages += 1;
+                        write!(&mut copied_text, "## {}\n\n", metadata.role).unwrap();
+                        for chunk in assistant.buffer.read(cx).text_for_range(range) {
+                            copied_text.push_str(&chunk);
                         }
+                        copied_text.push('\n');
                     }
                 }
-
-                offset = message_range.end;
             }
 
             if spanned_messages > 1 {
@@ -1255,10 +1279,13 @@ impl Item for AssistantEditor {
     }
 }
 
+#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]
+struct MessageId(usize);
+
 #[derive(Clone, Debug)]
 struct Message {
-    excerpt_id: ExcerptId,
-    content: ModelHandle<Buffer>,
+    id: MessageId,
+    start: language::Anchor,
 }
 
 #[derive(Clone, Debug)]
@@ -1362,22 +1389,137 @@ mod tests {
     #[gpui::test]
     fn test_inserting_and_removing_messages(cx: &mut AppContext) {
         let registry = Arc::new(LanguageRegistry::test());
+        let assistant = cx.add_model(|cx| Assistant::new(Default::default(), registry, cx));
+        let buffer = assistant.read(cx).buffer.clone();
 
-        cx.add_model(|cx| {
-            let mut assistant = Assistant::new(Default::default(), registry, cx);
-            let message_1 = assistant.messages[0].clone();
-            let message_2 = assistant.insert_message_after(ExcerptId::max(), Role::Assistant, cx);
-            let message_3 = assistant.insert_message_after(message_2.excerpt_id, Role::User, cx);
-            let message_4 = assistant.insert_message_after(message_2.excerpt_id, Role::User, cx);
-            assistant.remove_empty_messages(
-                HashSet::from_iter([message_3.excerpt_id, message_4.excerpt_id]),
-                Default::default(),
-                cx,
-            );
-            assert_eq!(assistant.messages.len(), 2);
-            assert_eq!(assistant.messages[0].excerpt_id, message_1.excerpt_id);
-            assert_eq!(assistant.messages[1].excerpt_id, message_2.excerpt_id);
+        let message_1 = assistant.read(cx).messages[0].clone();
+        assert_eq!(
+            messages(&assistant, cx),
+            vec![(message_1.id, Role::User, 0..0)]
+        );
+
+        let message_2 = assistant.update(cx, |assistant, cx| {
+            assistant
+                .insert_message_after(message_1.id, Role::Assistant, cx)
+                .unwrap()
+        });
+        assert_eq!(
+            messages(&assistant, cx),
+            vec![
+                (message_1.id, Role::User, 0..1),
+                (message_2.id, Role::Assistant, 1..1)
+            ]
+        );
+
+        buffer.update(cx, |buffer, cx| {
+            buffer.edit([(0..0, "1"), (1..1, "2")], None, cx)
+        });
+        assert_eq!(
+            messages(&assistant, cx),
+            vec![
+                (message_1.id, Role::User, 0..2),
+                (message_2.id, Role::Assistant, 2..3)
+            ]
+        );
+
+        let message_3 = assistant.update(cx, |assistant, cx| {
+            assistant
+                .insert_message_after(message_2.id, Role::User, cx)
+                .unwrap()
+        });
+        assert_eq!(
+            messages(&assistant, cx),
+            vec![
+                (message_1.id, Role::User, 0..2),
+                (message_2.id, Role::Assistant, 2..4),
+                (message_3.id, Role::User, 4..4)
+            ]
+        );
+
+        let message_4 = assistant.update(cx, |assistant, cx| {
             assistant
+                .insert_message_after(message_2.id, Role::User, cx)
+                .unwrap()
         });
+        assert_eq!(
+            messages(&assistant, cx),
+            vec![
+                (message_1.id, Role::User, 0..2),
+                (message_2.id, Role::Assistant, 2..4),
+                (message_4.id, Role::User, 4..5),
+                (message_3.id, Role::User, 5..5),
+            ]
+        );
+
+        buffer.update(cx, |buffer, cx| {
+            buffer.edit([(4..4, "C"), (5..5, "D")], None, cx)
+        });
+        assert_eq!(
+            messages(&assistant, cx),
+            vec![
+                (message_1.id, Role::User, 0..2),
+                (message_2.id, Role::Assistant, 2..4),
+                (message_4.id, Role::User, 4..6),
+                (message_3.id, Role::User, 6..7),
+            ]
+        );
+
+        // Deleting across message boundaries merges the messages.
+        buffer.update(cx, |buffer, cx| buffer.edit([(1..4, "")], None, cx));
+        assert_eq!(
+            messages(&assistant, cx),
+            vec![
+                (message_1.id, Role::User, 0..3),
+                (message_3.id, Role::User, 3..4),
+            ]
+        );
+
+        // Undoing the deletion should also undo the merge.
+        buffer.update(cx, |buffer, cx| buffer.undo(cx));
+        assert_eq!(
+            messages(&assistant, cx),
+            vec![
+                (message_1.id, Role::User, 0..2),
+                (message_2.id, Role::Assistant, 2..4),
+                (message_4.id, Role::User, 4..6),
+                (message_3.id, Role::User, 6..7),
+            ]
+        );
+
+        // Redoing the deletion should also redo the merge.
+        buffer.update(cx, |buffer, cx| buffer.redo(cx));
+        assert_eq!(
+            messages(&assistant, cx),
+            vec![
+                (message_1.id, Role::User, 0..3),
+                (message_3.id, Role::User, 3..4),
+            ]
+        );
+
+        // Ensure we can still insert after a merged message.
+        let message_5 = assistant.update(cx, |assistant, cx| {
+            assistant
+                .insert_message_after(message_1.id, Role::System, cx)
+                .unwrap()
+        });
+        assert_eq!(
+            messages(&assistant, cx),
+            vec![
+                (message_1.id, Role::User, 0..3),
+                (message_5.id, Role::System, 3..4),
+                (message_3.id, Role::User, 4..5)
+            ]
+        );
+    }
+
+    fn messages(
+        assistant: &ModelHandle<Assistant>,
+        cx: &AppContext,
+    ) -> Vec<(MessageId, Role, Range<usize>)> {
+        assistant
+            .read(cx)
+            .messages(cx)
+            .map(|(message, metadata, range)| (message.id, metadata.role, range))
+            .collect()
     }
 }

crates/diagnostics/src/diagnostics.rs 🔗

@@ -430,7 +430,7 @@ impl ProjectDiagnosticsEditor {
         });
 
         self.editor.update(cx, |editor, cx| {
-            editor.remove_blocks(blocks_to_remove, cx);
+            editor.remove_blocks(blocks_to_remove, None, cx);
             let block_ids = editor.insert_blocks(
                 blocks_to_add.into_iter().map(|block| {
                     let (excerpt_id, text_anchor) = block.position;
@@ -442,6 +442,7 @@ impl ProjectDiagnosticsEditor {
                         disposition: block.disposition,
                     }
                 }),
+                Some(Autoscroll::fit()),
                 cx,
             );
 

crates/editor/src/editor.rs 🔗

@@ -31,13 +31,11 @@ use copilot::Copilot;
 pub use display_map::DisplayPoint;
 use display_map::*;
 pub use editor_settings::EditorSettings;
-pub use element::RenderExcerptHeaderParams;
 pub use element::{
     Cursor, EditorElement, HighlightedRange, HighlightedRangeLine, LineWithInvisibles,
 };
 use futures::FutureExt;
 use fuzzy::{StringMatch, StringMatchCandidate};
-use gpui::LayoutContext;
 use gpui::{
     actions,
     color::Color,
@@ -511,7 +509,6 @@ pub struct Editor {
     mode: EditorMode,
     show_gutter: bool,
     placeholder_text: Option<Arc<str>>,
-    render_excerpt_header: Option<element::RenderExcerptHeader>,
     highlighted_rows: Option<Range<u32>>,
     #[allow(clippy::type_complexity)]
     background_highlights: BTreeMap<TypeId, (fn(&Theme) -> Color, Vec<Range<Anchor>>)>,
@@ -1317,7 +1314,6 @@ impl Editor {
             mode,
             show_gutter: mode == EditorMode::Full,
             placeholder_text: None,
-            render_excerpt_header: None,
             highlighted_rows: None,
             background_highlights: Default::default(),
             nav_history: None,
@@ -6272,6 +6268,7 @@ impl Editor {
                             }),
                             disposition: BlockDisposition::Below,
                         }],
+                        Some(Autoscroll::fit()),
                         cx,
                     )[0];
                     this.pending_rename = Some(RenameState {
@@ -6338,7 +6335,11 @@ impl Editor {
         cx: &mut ViewContext<Self>,
     ) -> Option<RenameState> {
         let rename = self.pending_rename.take()?;
-        self.remove_blocks([rename.block_id].into_iter().collect(), cx);
+        self.remove_blocks(
+            [rename.block_id].into_iter().collect(),
+            Some(Autoscroll::fit()),
+            cx,
+        );
         self.clear_text_highlights::<Rename>(cx);
         self.show_local_selections = true;
 
@@ -6724,29 +6725,43 @@ impl Editor {
     pub fn insert_blocks(
         &mut self,
         blocks: impl IntoIterator<Item = BlockProperties<Anchor>>,
+        autoscroll: Option<Autoscroll>,
         cx: &mut ViewContext<Self>,
     ) -> Vec<BlockId> {
         let blocks = self
             .display_map
             .update(cx, |display_map, cx| display_map.insert_blocks(blocks, cx));
-        self.request_autoscroll(Autoscroll::fit(), cx);
+        if let Some(autoscroll) = autoscroll {
+            self.request_autoscroll(autoscroll, cx);
+        }
         blocks
     }
 
     pub fn replace_blocks(
         &mut self,
         blocks: HashMap<BlockId, RenderBlock>,
+        autoscroll: Option<Autoscroll>,
         cx: &mut ViewContext<Self>,
     ) {
         self.display_map
             .update(cx, |display_map, _| display_map.replace_blocks(blocks));
-        self.request_autoscroll(Autoscroll::fit(), cx);
+        if let Some(autoscroll) = autoscroll {
+            self.request_autoscroll(autoscroll, cx);
+        }
     }
 
-    pub fn remove_blocks(&mut self, block_ids: HashSet<BlockId>, cx: &mut ViewContext<Self>) {
+    pub fn remove_blocks(
+        &mut self,
+        block_ids: HashSet<BlockId>,
+        autoscroll: Option<Autoscroll>,
+        cx: &mut ViewContext<Self>,
+    ) {
         self.display_map.update(cx, |display_map, cx| {
             display_map.remove_blocks(block_ids, cx)
         });
+        if let Some(autoscroll) = autoscroll {
+            self.request_autoscroll(autoscroll, cx);
+        }
     }
 
     pub fn longest_row(&self, cx: &mut AppContext) -> u32 {
@@ -6827,20 +6842,6 @@ impl Editor {
         cx.notify();
     }
 
-    pub fn set_render_excerpt_header(
-        &mut self,
-        render_excerpt_header: impl 'static
-            + Fn(
-                &mut Editor,
-                RenderExcerptHeaderParams,
-                &mut LayoutContext<Editor>,
-            ) -> AnyElement<Editor>,
-        cx: &mut ViewContext<Self>,
-    ) {
-        self.render_excerpt_header = Some(Arc::new(render_excerpt_header));
-        cx.notify();
-    }
-
     pub fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext<Self>) {
         if let Some(buffer) = self.buffer().read(cx).as_singleton() {
             if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
@@ -7448,6 +7449,7 @@ pub enum Event {
     },
     ScrollPositionChanged {
         local: bool,
+        autoscroll: bool,
     },
     Closed,
 }
@@ -7479,12 +7481,8 @@ impl View for Editor {
             });
         }
 
-        let mut editor = EditorElement::new(style.clone());
-        if let Some(render_excerpt_header) = self.render_excerpt_header.clone() {
-            editor = editor.with_render_excerpt_header(render_excerpt_header);
-        }
         Stack::new()
-            .with_child(editor)
+            .with_child(EditorElement::new(style.clone()))
             .with_child(ChildView::new(&self.mouse_context_menu, cx))
             .into_any()
     }

crates/editor/src/editor_tests.rs 🔗

@@ -2495,6 +2495,7 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
                 height: 1,
                 render: Arc::new(|_| Empty::new().into_any()),
             }],
+            Some(Autoscroll::fit()),
             cx,
         );
         editor.change_selections(None, cx, |s| {

crates/editor/src/element.rs 🔗

@@ -91,41 +91,17 @@ impl SelectionLayout {
     }
 }
 
-pub struct RenderExcerptHeaderParams<'a> {
-    pub id: crate::ExcerptId,
-    pub buffer: &'a language::BufferSnapshot,
-    pub range: &'a crate::ExcerptRange<text::Anchor>,
-    pub starts_new_buffer: bool,
-    pub gutter_padding: f32,
-    pub editor_style: &'a EditorStyle,
-}
-
-pub type RenderExcerptHeader = Arc<
-    dyn Fn(
-        &mut Editor,
-        RenderExcerptHeaderParams,
-        &mut LayoutContext<Editor>,
-    ) -> AnyElement<Editor>,
->;
-
 pub struct EditorElement {
     style: Arc<EditorStyle>,
-    render_excerpt_header: RenderExcerptHeader,
 }
 
 impl EditorElement {
     pub fn new(style: EditorStyle) -> Self {
         Self {
             style: Arc::new(style),
-            render_excerpt_header: Arc::new(render_excerpt_header),
         }
     }
 
-    pub fn with_render_excerpt_header(mut self, render: RenderExcerptHeader) -> Self {
-        self.render_excerpt_header = render;
-        self
-    }
-
     fn attach_mouse_handlers(
         scene: &mut SceneBuilder,
         position_map: &Arc<PositionMap>,
@@ -1531,18 +1507,117 @@ impl EditorElement {
                     range,
                     starts_new_buffer,
                     ..
-                } => (self.render_excerpt_header)(
-                    editor,
-                    RenderExcerptHeaderParams {
-                        id: *id,
-                        buffer,
-                        range,
-                        starts_new_buffer: *starts_new_buffer,
-                        gutter_padding,
-                        editor_style: style,
-                    },
-                    cx,
-                ),
+                } => {
+                    let tooltip_style = theme::current(cx).tooltip.clone();
+                    let include_root = editor
+                        .project
+                        .as_ref()
+                        .map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
+                        .unwrap_or_default();
+                    let jump_icon = project::File::from_dyn(buffer.file()).map(|file| {
+                        let jump_path = ProjectPath {
+                            worktree_id: file.worktree_id(cx),
+                            path: file.path.clone(),
+                        };
+                        let jump_anchor = range
+                            .primary
+                            .as_ref()
+                            .map_or(range.context.start, |primary| primary.start);
+                        let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
+
+                        enum JumpIcon {}
+                        MouseEventHandler::<JumpIcon, _>::new((*id).into(), cx, |state, _| {
+                            let style = style.jump_icon.style_for(state, false);
+                            Svg::new("icons/arrow_up_right_8.svg")
+                                .with_color(style.color)
+                                .constrained()
+                                .with_width(style.icon_width)
+                                .aligned()
+                                .contained()
+                                .with_style(style.container)
+                                .constrained()
+                                .with_width(style.button_width)
+                                .with_height(style.button_width)
+                        })
+                        .with_cursor_style(CursorStyle::PointingHand)
+                        .on_click(MouseButton::Left, move |_, editor, cx| {
+                            if let Some(workspace) = editor
+                                .workspace
+                                .as_ref()
+                                .and_then(|(workspace, _)| workspace.upgrade(cx))
+                            {
+                                workspace.update(cx, |workspace, cx| {
+                                    Editor::jump(
+                                        workspace,
+                                        jump_path.clone(),
+                                        jump_position,
+                                        jump_anchor,
+                                        cx,
+                                    );
+                                });
+                            }
+                        })
+                        .with_tooltip::<JumpIcon>(
+                            (*id).into(),
+                            "Jump to Buffer".to_string(),
+                            Some(Box::new(crate::OpenExcerpts)),
+                            tooltip_style.clone(),
+                            cx,
+                        )
+                        .aligned()
+                        .flex_float()
+                    });
+
+                    if *starts_new_buffer {
+                        let editor_font_size = style.text.font_size;
+                        let style = &style.diagnostic_path_header;
+                        let font_size = (style.text_scale_factor * editor_font_size).round();
+
+                        let path = buffer.resolve_file_path(cx, include_root);
+                        let mut filename = None;
+                        let mut parent_path = None;
+                        // Can't use .and_then() because `.file_name()` and `.parent()` return references :(
+                        if let Some(path) = path {
+                            filename = path.file_name().map(|f| f.to_string_lossy().to_string());
+                            parent_path =
+                                path.parent().map(|p| p.to_string_lossy().to_string() + "/");
+                        }
+
+                        Flex::row()
+                            .with_child(
+                                Label::new(
+                                    filename.unwrap_or_else(|| "untitled".to_string()),
+                                    style.filename.text.clone().with_font_size(font_size),
+                                )
+                                .contained()
+                                .with_style(style.filename.container)
+                                .aligned(),
+                            )
+                            .with_children(parent_path.map(|path| {
+                                Label::new(path, style.path.text.clone().with_font_size(font_size))
+                                    .contained()
+                                    .with_style(style.path.container)
+                                    .aligned()
+                            }))
+                            .with_children(jump_icon)
+                            .contained()
+                            .with_style(style.container)
+                            .with_padding_left(gutter_padding)
+                            .with_padding_right(gutter_padding)
+                            .expanded()
+                            .into_any_named("path header block")
+                    } else {
+                        let text_style = style.text.clone();
+                        Flex::row()
+                            .with_child(Label::new("⋯", text_style))
+                            .with_children(jump_icon)
+                            .contained()
+                            .with_padding_left(gutter_padding)
+                            .with_padding_right(gutter_padding)
+                            .expanded()
+                            .into_any_named("collapsed context")
+                    }
+                }
             };
 
             element.layout(
@@ -2679,121 +2754,6 @@ impl HighlightedRange {
     }
 }
 
-fn render_excerpt_header(
-    editor: &mut Editor,
-    RenderExcerptHeaderParams {
-        id,
-        buffer,
-        range,
-        starts_new_buffer,
-        gutter_padding,
-        editor_style,
-    }: RenderExcerptHeaderParams,
-    cx: &mut LayoutContext<Editor>,
-) -> AnyElement<Editor> {
-    let tooltip_style = theme::current(cx).tooltip.clone();
-    let include_root = editor
-        .project
-        .as_ref()
-        .map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
-        .unwrap_or_default();
-    let jump_icon = project::File::from_dyn(buffer.file()).map(|file| {
-        let jump_path = ProjectPath {
-            worktree_id: file.worktree_id(cx),
-            path: file.path.clone(),
-        };
-        let jump_anchor = range
-            .primary
-            .as_ref()
-            .map_or(range.context.start, |primary| primary.start);
-        let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
-
-        enum JumpIcon {}
-        MouseEventHandler::<JumpIcon, _>::new(id.into(), cx, |state, _| {
-            let style = editor_style.jump_icon.style_for(state, false);
-            Svg::new("icons/arrow_up_right_8.svg")
-                .with_color(style.color)
-                .constrained()
-                .with_width(style.icon_width)
-                .aligned()
-                .contained()
-                .with_style(style.container)
-                .constrained()
-                .with_width(style.button_width)
-                .with_height(style.button_width)
-        })
-        .with_cursor_style(CursorStyle::PointingHand)
-        .on_click(MouseButton::Left, move |_, editor, cx| {
-            if let Some(workspace) = editor
-                .workspace
-                .as_ref()
-                .and_then(|(workspace, _)| workspace.upgrade(cx))
-            {
-                workspace.update(cx, |workspace, cx| {
-                    Editor::jump(workspace, jump_path.clone(), jump_position, jump_anchor, cx);
-                });
-            }
-        })
-        .with_tooltip::<JumpIcon>(
-            id.into(),
-            "Jump to Buffer".to_string(),
-            Some(Box::new(crate::OpenExcerpts)),
-            tooltip_style.clone(),
-            cx,
-        )
-        .aligned()
-        .flex_float()
-    });
-
-    if starts_new_buffer {
-        let style = &editor_style.diagnostic_path_header;
-        let font_size = (style.text_scale_factor * editor_style.text.font_size).round();
-
-        let path = buffer.resolve_file_path(cx, include_root);
-        let mut filename = None;
-        let mut parent_path = None;
-        // Can't use .and_then() because `.file_name()` and `.parent()` return references :(
-        if let Some(path) = path {
-            filename = path.file_name().map(|f| f.to_string_lossy().to_string());
-            parent_path = path.parent().map(|p| p.to_string_lossy().to_string() + "/");
-        }
-
-        Flex::row()
-            .with_child(
-                Label::new(
-                    filename.unwrap_or_else(|| "untitled".to_string()),
-                    style.filename.text.clone().with_font_size(font_size),
-                )
-                .contained()
-                .with_style(style.filename.container)
-                .aligned(),
-            )
-            .with_children(parent_path.map(|path| {
-                Label::new(path, style.path.text.clone().with_font_size(font_size))
-                    .contained()
-                    .with_style(style.path.container)
-                    .aligned()
-            }))
-            .with_children(jump_icon)
-            .contained()
-            .with_style(style.container)
-            .with_padding_left(gutter_padding)
-            .with_padding_right(gutter_padding)
-            .expanded()
-            .into_any_named("path header block")
-    } else {
-        let text_style = editor_style.text.clone();
-        Flex::row()
-            .with_child(Label::new("⋯", text_style))
-            .with_children(jump_icon)
-            .contained()
-            .with_padding_left(gutter_padding)
-            .with_padding_right(gutter_padding)
-            .expanded()
-            .into_any_named("collapsed context")
-    }
-}
-
 fn position_to_display_point(
     position: Vector2F,
     text_bounds: RectF,
@@ -2923,6 +2883,7 @@ mod tests {
                     position: Anchor::min(),
                     render: Arc::new(|_| Empty::new().into_any()),
                 }],
+                None,
                 cx,
             );
 

crates/editor/src/items.rs 🔗

@@ -294,7 +294,7 @@ impl FollowableItem for Editor {
         match event {
             Event::Edited => true,
             Event::SelectionsChanged { local } => *local,
-            Event::ScrollPositionChanged { local } => *local,
+            Event::ScrollPositionChanged { local, .. } => *local,
             _ => false,
         }
     }

crates/editor/src/scroll.rs 🔗

@@ -173,6 +173,7 @@ impl ScrollManager {
         scroll_position: Vector2F,
         map: &DisplaySnapshot,
         local: bool,
+        autoscroll: bool,
         workspace_id: Option<i64>,
         cx: &mut ViewContext<Editor>,
     ) {
@@ -203,7 +204,7 @@ impl ScrollManager {
             )
         };
 
-        self.set_anchor(new_anchor, top_row, local, workspace_id, cx);
+        self.set_anchor(new_anchor, top_row, local, autoscroll, workspace_id, cx);
     }
 
     fn set_anchor(
@@ -211,11 +212,12 @@ impl ScrollManager {
         anchor: ScrollAnchor,
         top_row: u32,
         local: bool,
+        autoscroll: bool,
         workspace_id: Option<i64>,
         cx: &mut ViewContext<Editor>,
     ) {
         self.anchor = anchor;
-        cx.emit(Event::ScrollPositionChanged { local });
+        cx.emit(Event::ScrollPositionChanged { local, autoscroll });
         self.show_scrollbar(cx);
         self.autoscroll_request.take();
         if let Some(workspace_id) = workspace_id {
@@ -296,21 +298,28 @@ impl Editor {
     }
 
     pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext<Self>) {
-        self.set_scroll_position_internal(scroll_position, true, cx);
+        self.set_scroll_position_internal(scroll_position, true, false, cx);
     }
 
     pub(crate) fn set_scroll_position_internal(
         &mut self,
         scroll_position: Vector2F,
         local: bool,
+        autoscroll: bool,
         cx: &mut ViewContext<Self>,
     ) {
         let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
 
         hide_hover(self, cx);
         let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
-        self.scroll_manager
-            .set_scroll_position(scroll_position, &map, local, workspace_id, cx);
+        self.scroll_manager.set_scroll_position(
+            scroll_position,
+            &map,
+            local,
+            autoscroll,
+            workspace_id,
+            cx,
+        );
     }
 
     pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {
@@ -326,7 +335,7 @@ impl Editor {
             .to_point(&self.buffer().read(cx).snapshot(cx))
             .row;
         self.scroll_manager
-            .set_anchor(scroll_anchor, top_row, true, workspace_id, cx);
+            .set_anchor(scroll_anchor, top_row, true, false, workspace_id, cx);
     }
 
     pub(crate) fn set_scroll_anchor_remote(
@@ -341,7 +350,7 @@ impl Editor {
             .to_point(&self.buffer().read(cx).snapshot(cx))
             .row;
         self.scroll_manager
-            .set_anchor(scroll_anchor, top_row, false, workspace_id, cx);
+            .set_anchor(scroll_anchor, top_row, false, false, workspace_id, cx);
     }
 
     pub fn scroll_screen(&mut self, amount: &ScrollAmount, cx: &mut ViewContext<Self>) {

crates/editor/src/scroll/autoscroll.rs 🔗

@@ -136,23 +136,23 @@ impl Editor {
 
                 if target_top < start_row {
                     scroll_position.set_y(target_top);
-                    self.set_scroll_position_internal(scroll_position, local, cx);
+                    self.set_scroll_position_internal(scroll_position, local, true, cx);
                 } else if target_bottom >= end_row {
                     scroll_position.set_y(target_bottom - visible_lines);
-                    self.set_scroll_position_internal(scroll_position, local, cx);
+                    self.set_scroll_position_internal(scroll_position, local, true, cx);
                 }
             }
             AutoscrollStrategy::Center => {
                 scroll_position.set_y((first_cursor_top - margin).max(0.0));
-                self.set_scroll_position_internal(scroll_position, local, cx);
+                self.set_scroll_position_internal(scroll_position, local, true, cx);
             }
             AutoscrollStrategy::Top => {
                 scroll_position.set_y((first_cursor_top).max(0.0));
-                self.set_scroll_position_internal(scroll_position, local, cx);
+                self.set_scroll_position_internal(scroll_position, local, true, cx);
             }
             AutoscrollStrategy::Bottom => {
                 scroll_position.set_y((last_cursor_bottom - visible_lines).max(0.0));
-                self.set_scroll_position_internal(scroll_position, local, cx);
+                self.set_scroll_position_internal(scroll_position, local, true, cx);
             }
         }
 

crates/zed/src/zed.rs 🔗

@@ -254,13 +254,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
             workspace.toggle_panel_focus::<TerminalPanel>(cx);
         },
     );
-    cx.add_action(
-        |workspace: &mut Workspace,
-         _: &ai::assistant::ToggleFocus,
-         cx: &mut ViewContext<Workspace>| {
-            workspace.toggle_panel_focus::<AssistantPanel>(cx);
-        },
-    );
     cx.add_global_action({
         let app_state = Arc::downgrade(&app_state);
         move |_: &NewWindow, cx: &mut AppContext| {
@@ -368,9 +361,12 @@ pub fn initialize_workspace(
 
         let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
         let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
-        let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
-        let (project_panel, terminal_panel, assistant_panel) =
-            futures::try_join!(project_panel, terminal_panel, assistant_panel)?;
+        let assistant_panel = if *util::channel::RELEASE_CHANNEL == ReleaseChannel::Stable {
+            None
+        } else {
+            Some(AssistantPanel::load(workspace_handle.clone(), cx.clone()).await?)
+        };
+        let (project_panel, terminal_panel) = futures::try_join!(project_panel, terminal_panel)?;
         workspace_handle.update(&mut cx, |workspace, cx| {
             let project_panel_position = project_panel.position(cx);
             workspace.add_panel(project_panel, cx);
@@ -389,7 +385,9 @@ pub fn initialize_workspace(
             }
 
             workspace.add_panel(terminal_panel, cx);
-            workspace.add_panel(assistant_panel, cx);
+            if let Some(assistant_panel) = assistant_panel {
+                workspace.add_panel(assistant_panel, cx);
+            }
         })?;
         Ok(())
     })