diff --git a/assets/icons/robot_14.svg b/assets/icons/robot_14.svg new file mode 100644 index 0000000000..7b6dc3f752 --- /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 77353e1ee4..e5702cb677 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,27 +832,173 @@ 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 {} + editor + }); - 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 _subscriptions = vec![ + cx.observe(&assistant, |_, _, cx| cx.notify()), + cx.subscribe(&assistant, Self::handle_assistant_event), + cx.subscribe(&editor, Self::handle_editor_event), + ]; + + let mut this = Self { + assistant, + editor, + 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() + .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(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| { + editor.change_selections( + Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)), + cx, + |selections| selections.select_ranges([cursor..cursor]), + ); + }); + } + } + + fn cancel_last_assist(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { + if !self + .assistant + .update(cx, |assistant, _| assistant.cancel_last_assist()) + { + cx.propagate_action(); + } + } + + fn handle_assistant_event( + &mut self, + _: ModelHandle, + event: &AssistantEvent, + cx: &mut ViewContext, + ) { + match event { + AssistantEvent::MessagesEdited => self.update_message_headers(cx), + AssistantEvent::SummaryChanged => { + cx.emit(AssistantEditorEvent::TabContentChanged); + } + AssistantEvent::StreamedCompletion => { + self.editor.update(cx, |editor, 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, + ); + } + }); + } + } + } + + fn handle_editor_event( + &mut self, + _: ViewHandle, + event: &editor::Event, + cx: &mut ViewContext, + ) { + match event { + 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 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.); + 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( - params.id.into(), + message_id.0, cx, |state, _| match metadata.role { Role::User => { @@ -844,7 +1026,7 @@ impl AssistantEditor { let assistant = assistant.clone(); move |_, _, cx| { assistant.update(cx, |assistant, cx| { - assistant.cycle_message_role(excerpt_id, cx) + assistant.cycle_message_role(message_id, cx) }) } }); @@ -860,7 +1042,7 @@ impl AssistantEditor { .with_style(style.sent_at.container) .aligned(), ) - .with_children(metadata.error.map(|error| { + .with_children(metadata.error.clone().map(|error| { Svg::new("icons/circle_x_mark_12.svg") .with_color(style.error_icon.color) .constrained() @@ -868,7 +1050,7 @@ impl AssistantEditor { .contained() .with_style(style.error_icon.container) .with_tooltip::( - params.id.into(), + message_id.0, error, None, theme.tooltip.clone(), @@ -881,163 +1063,15 @@ impl AssistantEditor { .contained() .with_style(style.header) .into_any() - } else { - Empty::new().into_any() } - } - }, - cx, - ); - editor - }); + }), + disposition: BlockDisposition::Above, + }) + .collect::>(); - let _subscriptions = vec![ - cx.observe(&assistant, |_, _, cx| cx.notify()), - cx.subscribe(&assistant, Self::handle_assistant_event), - cx.subscribe(&editor, Self::handle_editor_event), - ]; - - Self { - assistant, - editor, - scroll_bottom: ScrollAnchor { - offset: Default::default(), - anchor: Anchor::max(), - }, - _subscriptions, - } - } - - 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 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); - user_message - }; - Some(user_message) - }); - - if let Some(user_message) = user_message { - 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]), - ); - }); - self.update_scroll_bottom(cx); - } - } - - fn cancel_last_assist(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { - if !self - .assistant - .update(cx, |assistant, _| assistant.cancel_last_assist()) - { - cx.propagate_action(); - } - } - - fn handle_assistant_event( - &mut self, - _: ModelHandle, - event: &AssistantEvent, - 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::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); - }); - } - } - } - - fn handle_editor_event( - &mut self, - _: ViewHandle, - event: &editor::Event, - cx: &mut ViewContext, - ) { - match event { - editor::Event::ScrollPositionChanged { .. } => self.update_scroll_bottom(cx), - _ => {} - } - } - - fn update_scroll_bottom(&mut self, cx: &mut ViewContext) { - self.editor.update(cx, |editor, cx| { - let snapshot = editor.snapshot(cx); - 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, - }; + 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 12fcc1395b..5350e53d6a 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 cecefc7061..3c1a5e6776 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 5cf79f9163..bb67c2044d 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 9aec670659..d6f9a2e906 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 9d639f9b7b..74b8e0ddb6 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 17e8d18a62..a13619a82a 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 ed098293f6..e83e2286b1 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 ecdd1b7a18..f8961d8bae 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(()) })