From d238e1867ed0cda7334e08be3962af7df314530b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 14 Jun 2023 14:09:09 +0200 Subject: [PATCH] Overhaul assistant panel (#2610) 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.** --- 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(-) create mode 100644 assets/icons/robot_14.svg diff --git a/assets/icons/robot_14.svg b/assets/icons/robot_14.svg new file mode 100644 index 0000000000000000000000000000000000000000..7b6dc3f752a23d6a9ff5804cac8ec7d938218663 --- /dev/null +++ b/assets/icons/robot_14.svg @@ -0,0 +1,4 @@ + + + + diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 77353e1ee497190277218ef747c48a2b2afe3eb4..e5702cb677b62398c1bb75ae2054980d10c028f9 100644 --- a/crates/ai/src/assistant.rs +++ b/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::(move |filter, _cx| { + filter.filtered_namespaces.insert("assistant"); + }); + } + settings::register::(cx); cx.add_action( |workspace: &mut Workspace, _: &NewContext, cx: &mut ViewContext| { @@ -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.toggle_panel_focus::(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>) { @@ -420,20 +431,20 @@ impl Panel for AssistantPanel { } enum AssistantEvent { - MessagesEdited { ids: Vec }, + MessagesEdited, SummaryChanged, StreamedCompletion, } struct Assistant { - buffer: ModelHandle, + buffer: ModelHandle, messages: Vec, - messages_metadata: HashMap, + messages_metadata: HashMap, + next_message_id: MessageId, summary: Option, pending_summary: Task>, completion_count: usize, pending_completions: Vec, - languages: Arc, model: String, token_count: Option, max_token_count: usize, @@ -453,15 +464,32 @@ impl Assistant { cx: &mut ModelContext, ) -> 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, - event: &editor::multi_buffer::Event, + _: ModelHandle, + event: &language::Event, cx: &mut ModelContext, ) { 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) { 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) -> 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 = 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 = 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, - protected_offsets: HashSet, - cx: &mut ModelContext, - ) { - 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) { - if let Some(metadata) = self.messages_metadata.get_mut(&excerpt_id) { + fn cycle_message_role(&mut self, id: MessageId, cx: &mut ModelContext) { + 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, - ) -> 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 { + 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) { 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 { + 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 { + 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)> { + 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, editor: ViewHandle, - scroll_bottom: ScrollAnchor, + blocks: HashSet, + scroll_position: Option, _subscriptions: Vec, } @@ -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::::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::( - 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) { 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, ) { match event { - AssistantEvent::MessagesEdited { ids } => { - let selections = self.editor.read(cx).selections.all::(cx); - let selection_heads = selections - .iter() - .map(|selection| selection.head()) - .collect::>(); - let ids = ids.iter().copied().collect::>(); - 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, ) { 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) { + fn cursor_scroll_position(&self, cx: &mut ViewContext) -> Option { 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.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::::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::( + message_id.0, + error, + None, + theme.tooltip.clone(), + cx, + ) + .aligned() + })) + .aligned() + .left() + .contained() + .with_style(style.header) + .into_any() + } + }), + disposition: BlockDisposition::Above, + }) + .collect::>(); + + 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::(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, + 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, + cx: &AppContext, + ) -> Vec<(MessageId, Role, Range)> { + assistant + .read(cx) + .messages(cx) + .map(|(message, metadata, range)| (message.id, metadata.role, range)) + .collect() } } diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 12fcc1395bf5a430452f1647cc197aa07df91f4b..5350e53d6aef0cff11b8d7d96b148bcb3a701af3 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/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, ); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index cecefc7061eeb22791497db76af0ccad7af6cded..3c1a5e67762b766d80ab81c2e5caee1cd63c284b 100644 --- a/crates/editor/src/editor.rs +++ b/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>, - render_excerpt_header: Option, highlighted_rows: Option>, #[allow(clippy::type_complexity)] background_highlights: BTreeMap Color, Vec>)>, @@ -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, @@ -6268,6 +6264,7 @@ impl Editor { }), disposition: BlockDisposition::Below, }], + Some(Autoscroll::fit()), cx, )[0]; this.pending_rename = Some(RenameState { @@ -6334,7 +6331,11 @@ impl Editor { cx: &mut ViewContext, ) -> Option { 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::(cx); self.show_local_selections = true; @@ -6720,29 +6721,43 @@ impl Editor { pub fn insert_blocks( &mut self, blocks: impl IntoIterator>, + autoscroll: Option, cx: &mut ViewContext, ) -> Vec { 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, + autoscroll: Option, cx: &mut ViewContext, ) { 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, cx: &mut ViewContext) { + pub fn remove_blocks( + &mut self, + block_ids: HashSet, + autoscroll: Option, + cx: &mut ViewContext, + ) { 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 { @@ -6823,20 +6838,6 @@ impl Editor { cx.notify(); } - pub fn set_render_excerpt_header( - &mut self, - render_excerpt_header: impl 'static - + Fn( - &mut Editor, - RenderExcerptHeaderParams, - &mut LayoutContext, - ) -> AnyElement, - cx: &mut ViewContext, - ) { - self.render_excerpt_header = Some(Arc::new(render_excerpt_header)); - cx.notify(); - } - pub fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext) { if let Some(buffer) = self.buffer().read(cx).as_singleton() { if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) { @@ -7444,6 +7445,7 @@ pub enum Event { }, ScrollPositionChanged { local: bool, + autoscroll: bool, }, Closed, } @@ -7475,12 +7477,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() } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 5cf79f9163ee3b787ba7685fb909ed63e34f550c..bb67c2044dcd09fc271ee255c8d8d93fb9c2f1e0 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -2481,6 +2481,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| { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 9aec670659df0b9ecba06ddbd4bffb1e26119ab3..d6f9a2e90682f72d33d06ee1b37d813e35764094 100644 --- a/crates/editor/src/element.rs +++ b/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, - 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, - ) -> AnyElement, ->; - pub struct EditorElement { style: Arc, - 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, @@ -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::::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::( + (*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, -) -> AnyElement { - 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::::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::( - 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, ); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 9d639f9b7bd6aeebe619b9382862c75f0347c7ad..74b8e0ddb68d81f3d00fafa518cfbff4bc4c71b6 100644 --- a/crates/editor/src/items.rs +++ b/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, } } diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index 17e8d18a625434b2fd81f8ea6938c72729aed423..a13619a82a0abfc82bdc3172b42a61bb5030d252 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -173,6 +173,7 @@ impl ScrollManager { scroll_position: Vector2F, map: &DisplaySnapshot, local: bool, + autoscroll: bool, workspace_id: Option, cx: &mut ViewContext, ) { @@ -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, cx: &mut ViewContext, ) { 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.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, ) { 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) -> 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) { diff --git a/crates/editor/src/scroll/autoscroll.rs b/crates/editor/src/scroll/autoscroll.rs index ed098293f6dd97dd150865fa4aa8cc3be3a3b3f6..e83e2286b1f4809d777c72257eda0e7471508ccf 100644 --- a/crates/editor/src/scroll/autoscroll.rs +++ b/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); } } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index ecdd1b7a180cee33fa938a39d901022855fe538a..f8961d8baeb50cfd2ccc160a3bd04d119ca5a079 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -254,13 +254,6 @@ pub fn init(app_state: &Arc, cx: &mut gpui::AppContext) { workspace.toggle_panel_focus::(cx); }, ); - cx.add_action( - |workspace: &mut Workspace, - _: &ai::assistant::ToggleFocus, - cx: &mut ViewContext| { - workspace.toggle_panel_focus::(cx); - }, - ); cx.add_global_action({ let app_state = Arc::downgrade(&app_state); move |_: &NewWindow, cx: &mut AppContext| { @@ -366,9 +359,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); @@ -387,7 +383,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(()) })