From 7db690b713a547fbc1f2e7567a273df8916734c8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 12 Jun 2023 17:50:13 +0200 Subject: [PATCH 01/15] WIP --- Cargo.lock | 2 +- crates/ai/src/assistant.rs | 314 +++++++++++++------------------------ 2 files changed, 110 insertions(+), 206 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8ef48849acb94fe864b9af31d2999fc4c9e05271..fba46c59cfb57bd91825a2416ca93725699672b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7559,7 +7559,7 @@ dependencies = [ [[package]] name = "tree-sitter-yaml" version = "0.0.1" -source = "git+https://github.com/zed-industries/tree-sitter-yaml?rev=5694b7f290cd9ef998829a0a6d8391a666370886#5694b7f290cd9ef998829a0a6d8391a666370886" +source = "git+https://github.com/zed-industries/tree-sitter-yaml?rev=f545a41f57502e1b5ddf2a6668896c1b0620f930#f545a41f57502e1b5ddf2a6668896c1b0620f930" dependencies = [ "cc", "tree-sitter", diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 77353e1ee497190277218ef747c48a2b2afe3eb4..f7057b1e5bb7fa355d1734d2961f3bc991a06157 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -11,7 +11,7 @@ use editor::{ autoscroll::{Autoscroll, AutoscrollStrategy}, ScrollAnchor, }, - Anchor, DisplayPoint, Editor, ExcerptId, ExcerptRange, MultiBuffer, + Anchor, DisplayPoint, Editor, ExcerptId, }; use fs::Fs; use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt}; @@ -420,15 +420,16 @@ 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, @@ -453,10 +454,11 @@ impl Assistant { cx: &mut ModelContext, ) -> Self { let model = "gpt-3.5-turbo"; - let buffer = cx.add_model(|_| MultiBuffer::new(0)); + let buffer = cx.add_model(|cx| Buffer::new(0, "", cx)); 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(), @@ -470,23 +472,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); } _ => {} } @@ -625,7 +638,7 @@ impl Assistant { fn remove_empty_messages<'a>( &mut self, - excerpts: HashSet, + messages: HashSet, protected_offsets: HashSet, cx: &mut ModelContext, ) { @@ -636,7 +649,7 @@ impl Assistant { offset = range.end + 1; if range.is_empty() && !protected_offsets.contains(&range.start) - && excerpts.contains(&message.excerpt_id) + && messages.contains(&message.id) { excerpts_to_remove.push(message.excerpt_id); self.messages_metadata.remove(&message.excerpt_id); @@ -663,84 +676,61 @@ impl Assistant { 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 len = buffer.len(); + buffer.edit([(len..len, "\n")], None, cx); + buffer.anchor_before(len + 1) + }); + let message = Message { + id: MessageId(post_inc(&mut self.next_message_id.0)), + start, + }; + self.messages.insert(prev_message_ix, message.clone()); + self.messages_metadata.insert( + message.id, + MessageMetadata { + role, + sent_at: Local::now(), + error: None, + }, + ); + 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 messages = self + // .messages + // .iter() + // .take(2) + // .filter_map(|message| { + // Some(RequestMessage { + // role: self.messages_metadata.get(&message.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 messages = todo!(); let request = OpenAIRequest { model: self.model.clone(), messages, @@ -796,98 +786,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 }); @@ -912,26 +813,21 @@ impl AssistantEditor { 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)? + let message_id = if newest_selection.head() == Anchor::min() { + assistant.messages.first().map(|message| message.id)? } else if newest_selection.head() == Anchor::max() { - assistant - .messages - .last() - .map(|message| message.excerpt_id)? + assistant.messages.last().map(|message| message.id)? } else { - newest_selection.head().excerpt_id() + todo!() + // newest_selection.head().excerpt_id() }; - let metadata = assistant.messages_metadata.get(&excerpt_id)?; + 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) @@ -943,7 +839,7 @@ impl AssistantEditor { .buffer() .read(cx) .snapshot(cx) - .anchor_in_excerpt(user_message.excerpt_id, language::Anchor::MIN); + .anchor_in_excerpt(Default::default(), user_message.start); editor.change_selections( Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)), cx, @@ -970,16 +866,16 @@ impl AssistantEditor { cx: &mut ViewContext, ) { match event { - AssistantEvent::MessagesEdited { ids } => { + AssistantEvent::MessagesEdited => { 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) - }); + // 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); @@ -1115,7 +1011,9 @@ impl AssistantEditor { 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; + // TODO + // let message_range = offset..offset + message.content.read(cx).len() + 1; + let message_range = offset..offset + 1; if message_range.start >= selection.range().end { break; @@ -1123,13 +1021,10 @@ impl AssistantEditor { 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) - { + if let Some(metadata) = assistant.messages_metadata.get(&message.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) - { + for chunk in assistant.buffer.read(cx).text_for_range(range) { copied_text.push_str(&chunk); } copied_text.push('\n'); @@ -1255,10 +1150,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)] @@ -1366,17 +1264,23 @@ mod tests { 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); + let message_2 = assistant + .insert_message_after(message_1.id, Role::Assistant, cx) + .unwrap(); + let message_3 = assistant + .insert_message_after(message_2.id, Role::User, cx) + .unwrap(); + let message_4 = assistant + .insert_message_after(message_2.id, Role::User, cx) + .unwrap(); assistant.remove_empty_messages( - HashSet::from_iter([message_3.excerpt_id, message_4.excerpt_id]), + HashSet::from_iter([message_3.id, message_4.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); + assert_eq!(assistant.messages[0].id, message_1.id); + assert_eq!(assistant.messages[1].id, message_2.id); assistant }); } From 2ae8b558b991cf3beb5c998fb3168f7d4f70c369 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 13 Jun 2023 13:43:06 +0200 Subject: [PATCH 02/15] Get back to a compiling state with `Buffer` backing the assistant --- crates/ai/src/assistant.rs | 328 ++++++++++++++++++++++++------------- 1 file changed, 214 insertions(+), 114 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index f7057b1e5bb7fa355d1734d2961f3bc991a06157..31a5f66700a7d941b2c1804a20f88a8cec38c1d1 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -11,7 +11,7 @@ use editor::{ autoscroll::{Autoscroll, AutoscrollStrategy}, ScrollAnchor, }, - Anchor, DisplayPoint, Editor, ExcerptId, + Anchor, DisplayPoint, Editor, ToOffset as _, }; use fs::Fs; use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt}; @@ -25,10 +25,13 @@ use gpui::{ 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 std::{ + borrow::Cow, cell::RefCell, cmp, fmt::Write, io, iter, ops::Range, rc::Rc, sync::Arc, + time::Duration, +}; use util::{post_inc, truncate_and_trailoff, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, @@ -507,16 +510,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, }) }) @@ -554,45 +557,47 @@ 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| { - cx.emit(AssistantEvent::StreamedCompletion); + .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); + }); + + Some(()) }); } } @@ -612,9 +617,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(); @@ -642,33 +646,33 @@ impl Assistant { 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) - && messages.contains(&message.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(); - } + // 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) + // && messages.contains(&message.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.notify(); } @@ -686,15 +690,18 @@ impl Assistant { .position(|message| message.id == message_id) { let start = self.buffer.update(cx, |buffer, cx| { - let len = buffer.len(); - buffer.edit([(len..len, "\n")], None, cx); - buffer.anchor_before(len + 1) + let offset = self + .messages + .get(prev_message_ix + 1) + .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, message.clone()); + self.messages.insert(prev_message_ix + 1, message.clone()); self.messages_metadata.insert( message.id, MessageMetadata { @@ -713,24 +720,13 @@ impl Assistant { 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.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 messages = todo!(); + 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, @@ -760,6 +756,44 @@ 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 || { + let message = messages.next()?; + let metadata = self.messages_metadata.get(&message.id)?; + let message_start = message.start.to_offset(buffer); + let message_end = messages + .peek() + .map_or(language::Anchor::MAX, |message| message.start) + .to_offset(buffer); + Some((message, metadata, message_start..message_end)) + }) + } } struct PendingCompletion { @@ -812,16 +846,12 @@ impl AssistantEditor { 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 message_id = if newest_selection.head() == Anchor::min() { - assistant.messages.first().map(|message| message.id)? - } else if newest_selection.head() == Anchor::max() { - assistant.messages.last().map(|message| message.id)? - } else { - todo!() - // newest_selection.head().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)?; @@ -834,16 +864,14 @@ impl AssistantEditor { }); 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(Default::default(), user_message.start); 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); @@ -1011,7 +1039,7 @@ impl AssistantEditor { let mut copied_text = String::new(); let mut spanned_messages = 0; for message in &assistant.messages { - // TODO + todo!(); // let message_range = offset..offset + message.content.read(cx).len() + 1; let message_range = offset..offset + 1; @@ -1260,28 +1288,100 @@ 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(); + + let message_1 = assistant.read(cx).messages[0].clone(); + assert_eq!( + messages(&assistant, cx), + vec![(message_1.id, Role::User, 0..0)] + ); - cx.add_model(|cx| { - let mut assistant = Assistant::new(Default::default(), registry, cx); - let message_1 = assistant.messages[0].clone(); - let message_2 = assistant + let message_2 = assistant.update(cx, |assistant, cx| { + assistant .insert_message_after(message_1.id, Role::Assistant, cx) - .unwrap(); - let message_3 = assistant - .insert_message_after(message_2.id, Role::User, cx) - .unwrap(); - let message_4 = assistant + .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(); - assistant.remove_empty_messages( - HashSet::from_iter([message_3.id, message_4.id]), - Default::default(), - cx, - ); - assert_eq!(assistant.messages.len(), 2); - assert_eq!(assistant.messages[0].id, message_1.id); - assert_eq!(assistant.messages[1].id, message_2.id); + .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..6), + (message_3.id, Role::User, 6..7), + ] + ); + } + + 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() } } From 2842fc2b1dada15a4b409aee6f32565c75da30ad Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 13 Jun 2023 15:09:19 +0200 Subject: [PATCH 03/15] Merge messages whose header has been invalidated --- crates/ai/src/assistant.rs | 39 +++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 31a5f66700a7d941b2c1804a20f88a8cec38c1d1..30f3865415cdeabf793f222fd07c8ab7339c4af7 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -784,14 +784,24 @@ impl Assistant { let buffer = self.buffer.read(cx); let mut messages = self.messages.iter().peekable(); iter::from_fn(move || { - let message = messages.next()?; - let metadata = self.messages_metadata.get(&message.id)?; - let message_start = message.start.to_offset(buffer); - let message_end = messages - .peek() - .map_or(language::Anchor::MAX, |message| message.start) - .to_offset(buffer); - Some((message, metadata, message_start..message_end)) + 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 }) } } @@ -1368,7 +1378,18 @@ mod tests { assert_eq!( messages(&assistant, cx), vec![ - (message_1.id, Role::User, 0..6), + (message_1.id, Role::User, 0..3), + (message_3.id, Role::User, 3..4), + ] + ); + + 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), ] ); From 00cede63a8934ff737fe52850f5dc59fa68f6b77 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 13 Jun 2023 15:55:01 +0200 Subject: [PATCH 04/15] Show message headers again Co-Authored-By: Julia Risley --- crates/ai/src/assistant.rs | 118 +++++++++++++++++++++++++++++++++---- 1 file changed, 108 insertions(+), 10 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 30f3865415cdeabf793f222fd07c8ab7339c4af7..6d5b6040d1c6baef6072c74bbdfa847b7272fc89 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -6,7 +6,7 @@ use anyhow::{anyhow, Result}; use chrono::{DateTime, Local}; use collections::{HashMap, HashSet}; use editor::{ - display_map::ToDisplayPoint, + display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint}, scroll::{ autoscroll::{Autoscroll, AutoscrollStrategy}, ScrollAnchor, @@ -818,6 +818,7 @@ enum AssistantEditorEvent { struct AssistantEditor { assistant: ModelHandle, editor: ViewHandle, + blocks: HashSet, scroll_bottom: ScrollAnchor, _subscriptions: Vec, } @@ -845,6 +846,7 @@ impl AssistantEditor { Self { assistant, editor, + blocks: Default::default(), scroll_bottom: ScrollAnchor { offset: Default::default(), anchor: Anchor::max(), @@ -905,19 +907,115 @@ impl AssistantEditor { ) { match event { AssistantEvent::MessagesEdited => { - 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) - // }); + 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, cx); + let ids = editor.insert_blocks(new_blocks, cx); + self.blocks = HashSet::from_iter(ids); + }); } + AssistantEvent::SummaryChanged => { cx.emit(AssistantEditorEvent::TabContentChanged); } + AssistantEvent::StreamedCompletion => { self.editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); From 6e5de2fbbb4914f2786ce1ce6b52cbf0617dc63c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 13 Jun 2023 16:00:13 +0200 Subject: [PATCH 05/15] Update blocks when cycling the message role Co-Authored-By: Julia Risley --- crates/ai/src/assistant.rs | 32 +------------------------------- 1 file changed, 1 insertion(+), 31 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 6d5b6040d1c6baef6072c74bbdfa847b7272fc89..1777253f1331cc5f6f091156642f9e1736d18012 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -640,40 +640,10 @@ impl Assistant { self.pending_completions.pop().is_some() } - fn remove_empty_messages<'a>( - &mut self, - messages: 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) - // && messages.contains(&message.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, id: MessageId, cx: &mut ModelContext) { if let Some(metadata) = self.messages_metadata.get_mut(&id) { metadata.role.cycle(); + cx.emit(AssistantEvent::MessagesEdited); cx.notify(); } } From 0db0a1ccef443fe0ed943c25fec43267a073e5b6 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 13 Jun 2023 16:06:50 +0200 Subject: [PATCH 06/15] Skip merged messages when inserting new ones Co-Authored-By: Julia Risley --- crates/ai/src/assistant.rs | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 1777253f1331cc5f6f091156642f9e1736d18012..b650a6673f2098b1778ba1d7aeb80aa981a24c27 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -661,8 +661,9 @@ impl Assistant { { let start = self.buffer.update(cx, |buffer, cx| { let offset = self - .messages - .get(prev_message_ix + 1) + .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) @@ -1451,6 +1452,7 @@ mod tests { ] ); + // Undoing the deletion should also undo the merge. buffer.update(cx, |buffer, cx| buffer.undo(cx)); assert_eq!( messages(&assistant, cx), @@ -1461,6 +1463,31 @@ mod tests { (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( From 9b7617403d137bc90fb14404361b2dc6032062ce Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 14 Jun 2023 09:22:47 +0200 Subject: [PATCH 07/15] Parse buffer as Markdown --- crates/ai/src/assistant.rs | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index b650a6673f2098b1778ba1d7aeb80aa981a24c27..ea58504c53af5bdf569dd03cab884134d86917c2 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -437,7 +437,6 @@ struct Assistant { pending_summary: Task>, completion_count: usize, pending_completions: Vec, - languages: Arc, model: String, token_count: Option, max_token_count: usize, @@ -457,7 +456,24 @@ impl Assistant { cx: &mut ModelContext, ) -> Self { let model = "gpt-3.5-turbo"; - let buffer = cx.add_model(|cx| Buffer::new(0, "", cx)); + 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(), @@ -466,7 +482,6 @@ impl Assistant { 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), @@ -660,8 +675,7 @@ impl Assistant { .position(|message| message.id == message_id) { let start = self.buffer.update(cx, |buffer, cx| { - let offset = self - .messages[prev_message_ix + 1..] + 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); From 27c83ca3f7bc15eb5259d74cc3e46291608ae43e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 14 Jun 2023 09:38:49 +0200 Subject: [PATCH 08/15] Remove unnecessary `set_render_excerpt_header` method --- crates/editor/src/editor.rs | 24 +--- crates/editor/src/element.rs | 262 +++++++++++++++-------------------- 2 files changed, 112 insertions(+), 174 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1d6c4f9359b0deaf6af64de0b8816733c3482db3..4fef94a9bbe737c50f2346c97104de289035c3ab 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, @@ -6827,20 +6823,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()) { @@ -7479,12 +7461,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/element.rs b/crates/editor/src/element.rs index 9aec670659df0b9ecba06ddbd4bffb1e26119ab3..e1b925d4eabce68edc1cf6bcc9a50072287c8e0a 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, From 7dab17e233308d51cc1103eb701c5270f5b75e98 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 14 Jun 2023 09:40:43 +0200 Subject: [PATCH 09/15] Re-enable copy support in the assistant --- crates/ai/src/assistant.rs | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index ea58504c53af5bdf569dd03cab884134d86917c2..aea952019101d89a205bab0fd5df060312c4f120 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -1131,25 +1131,19 @@ impl AssistantEditor { let mut offset = 0; let mut copied_text = String::new(); let mut spanned_messages = 0; - for message in &assistant.messages { - todo!(); - // let message_range = offset..offset + message.content.read(cx).len() + 1; - let message_range = offset..offset + 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.id) { - 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'); + 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'); } } From 75ad76bfb2131779be0a129eeed98c06884520ae Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 14 Jun 2023 09:47:30 +0200 Subject: [PATCH 10/15] :lipstick: --- crates/ai/src/assistant.rs | 224 ++++++++++++++++++------------------- 1 file changed, 110 insertions(+), 114 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index aea952019101d89a205bab0fd5df060312c4f120..775e99c9888f3a9315dcfc6382cf19fc2c41193f 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -611,6 +611,7 @@ impl Assistant { }; buffer.edit([(offset..offset, text)], None, cx); }); + cx.emit(AssistantEvent::StreamedCompletion); Some(()) }); @@ -745,7 +746,7 @@ 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 { + .map(|(_message, metadata, range)| RequestMessage { role: metadata.role, content: buffer.text_for_range(range).collect(), }) @@ -828,7 +829,7 @@ impl AssistantEditor { cx.subscribe(&editor, Self::handle_editor_event), ]; - Self { + let mut this = Self { assistant, editor, blocks: Default::default(), @@ -837,7 +838,9 @@ impl AssistantEditor { anchor: Anchor::max(), }, _subscriptions, - } + }; + this.update_message_headers(cx); + this } fn assist(&mut self, _: &Assist, cx: &mut ViewContext) { @@ -891,116 +894,10 @@ impl AssistantEditor { cx: &mut ViewContext, ) { match event { - AssistantEvent::MessagesEdited => { - 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, cx); - let ids = editor.insert_blocks(new_blocks, cx); - self.blocks = HashSet::from_iter(ids); - }); - } - + 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); @@ -1032,6 +929,108 @@ impl AssistantEditor { } } + 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, cx); + let ids = editor.insert_blocks(new_blocks, cx); + self.blocks = HashSet::from_iter(ids); + }); + } + fn update_scroll_bottom(&mut self, cx: &mut ViewContext) { self.editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); @@ -1128,10 +1127,9 @@ 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, metadata, message_range) in assistant.messages(cx) { + 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 { @@ -1146,8 +1144,6 @@ impl AssistantEditor { copied_text.push('\n'); } } - - offset = message_range.end; } if spanned_messages > 1 { From f8b94174065dc5435fb1be20d30aeb4fcfaed76f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 14 Jun 2023 10:41:18 +0200 Subject: [PATCH 11/15] Keep cursor stable as autocompletions are being streamed --- crates/ai/src/assistant.rs | 106 +++++++++++++++++++------------------ 1 file changed, 55 insertions(+), 51 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 775e99c9888f3a9315dcfc6382cf19fc2c41193f..39309fd361003fbc234af01089d2bb98705dc98a 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -7,11 +7,8 @@ use chrono::{DateTime, Local}; use collections::{HashMap, HashSet}; use editor::{ display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint}, - scroll::{ - autoscroll::{Autoscroll, AutoscrollStrategy}, - ScrollAnchor, - }, - Anchor, DisplayPoint, Editor, ToOffset as _, + scroll::autoscroll::{Autoscroll, AutoscrollStrategy}, + Anchor, Editor, ToOffset as _, }; use fs::Fs; use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt}; @@ -19,7 +16,7 @@ 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, @@ -801,11 +798,17 @@ enum AssistantEditorEvent { TabContentChanged, } +#[derive(Copy, Clone, Debug, PartialEq)] +struct ScrollPosition { + offset_before_cursor: Vector2F, + cursor: Anchor, +} + struct AssistantEditor { assistant: ModelHandle, editor: ViewHandle, blocks: HashSet, - scroll_bottom: ScrollAnchor, + scroll_position: Option, _subscriptions: Vec, } @@ -833,10 +836,7 @@ impl AssistantEditor { assistant, editor, blocks: Default::default(), - scroll_bottom: ScrollAnchor { - offset: Default::default(), - anchor: Anchor::max(), - }, + scroll_position: None, _subscriptions, }; this.update_message_headers(cx); @@ -874,7 +874,7 @@ impl AssistantEditor { |selections| selections.select_ranges([cursor..cursor]), ); }); - self.update_scroll_bottom(cx); + self.scroll_position = self.cursor_scroll_position(cx); } } @@ -900,18 +900,16 @@ impl AssistantEditor { } 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, + ); + } }); } } @@ -924,11 +922,43 @@ impl AssistantEditor { cx: &mut ViewContext, ) { match event { - editor::Event::ScrollPositionChanged { .. } => self.update_scroll_bottom(cx), + editor::Event::ScrollPositionChanged { .. } => { + if self.cursor_scroll_position(cx) != self.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); @@ -1031,32 +1061,6 @@ impl AssistantEditor { }); } - 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, - }; - }); - } - fn quote_selection( workspace: &mut Workspace, _: &QuoteSelection, From 1aa1774688090b06bbc64ef4ff5c96703886af06 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 14 Jun 2023 10:49:23 +0200 Subject: [PATCH 12/15] Avoid auto-scrolling the editor when inserting/removing headers --- crates/ai/src/assistant.rs | 4 ++-- crates/diagnostics/src/diagnostics.rs | 3 ++- crates/editor/src/editor.rs | 27 +++++++++++++++++++++++---- crates/editor/src/editor_tests.rs | 1 + crates/editor/src/element.rs | 1 + 5 files changed, 29 insertions(+), 7 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 39309fd361003fbc234af01089d2bb98705dc98a..e733f4d477116e0e732ada0c5cd30567cda0e7c5 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -1055,8 +1055,8 @@ impl AssistantEditor { }) .collect::>(); - editor.remove_blocks(old_blocks, cx); - let ids = editor.insert_blocks(new_blocks, cx); + editor.remove_blocks(old_blocks, None, cx); + let ids = editor.insert_blocks(new_blocks, None, cx); self.blocks = HashSet::from_iter(ids); }); } 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 4fef94a9bbe737c50f2346c97104de289035c3ab..6e06c901f0a10dbd9a9d6edd281d8b49c6b1de19 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6268,6 +6268,7 @@ impl Editor { }), disposition: BlockDisposition::Below, }], + Some(Autoscroll::fit()), cx, )[0]; this.pending_rename = Some(RenameState { @@ -6334,7 +6335,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 +6725,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 { diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index d256a0424d4f0d1e34b3e51fc47a969eb4f0f908..e2b876f4b763916f855c6d501bec50109b9cc8c0 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -2495,6 +2495,7 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { height: 1, render: Arc::new(|_| Empty::new().into_any()), }], + Some(Autoscroll::fit()), cx, ); editor.change_selections(None, cx, |s| { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index e1b925d4eabce68edc1cf6bcc9a50072287c8e0a..d6f9a2e90682f72d33d06ee1b37d813e35764094 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2883,6 +2883,7 @@ mod tests { position: Anchor::min(), render: Arc::new(|_| Empty::new().into_any()), }], + None, cx, ); From 56b0bf8601b644a9ebb0b9499dd29e90250af3b2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 14 Jun 2023 11:43:44 +0200 Subject: [PATCH 13/15] Save cursor scroll position when the editor is auto-scrolled --- crates/ai/src/assistant.rs | 9 ++++++--- crates/editor/src/editor.rs | 1 + crates/editor/src/items.rs | 2 +- crates/editor/src/scroll.rs | 23 ++++++++++++++++------- crates/editor/src/scroll/autoscroll.rs | 10 +++++----- 5 files changed, 29 insertions(+), 16 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index e733f4d477116e0e732ada0c5cd30567cda0e7c5..a168de6c9b88a9fa84dcee9a4e724497b7dff375 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -693,6 +693,7 @@ impl Assistant { error: None, }, ); + cx.emit(AssistantEvent::MessagesEdited); Some(message) } else { None @@ -874,7 +875,6 @@ impl AssistantEditor { |selections| selections.select_ranges([cursor..cursor]), ); }); - self.scroll_position = self.cursor_scroll_position(cx); } } @@ -922,8 +922,11 @@ impl AssistantEditor { cx: &mut ViewContext, ) { match event { - editor::Event::ScrollPositionChanged { .. } => { - if self.cursor_scroll_position(cx) != self.scroll_position { + 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; } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6e06c901f0a10dbd9a9d6edd281d8b49c6b1de19..2e0d444e0473b4b4cfe2b79f712797c2e8e799f5 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7449,6 +7449,7 @@ pub enum Event { }, ScrollPositionChanged { local: bool, + autoscroll: bool, }, Closed, } 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); } } From 049c9873109c5370c93027765edd1a7d9a46637f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 14 Jun 2023 12:08:49 +0200 Subject: [PATCH 14/15] Avoid loading the assistant panel on stable --- crates/ai/src/assistant.rs | 13 ++++++++++++- crates/zed/src/zed.rs | 20 +++++++++----------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index a168de6c9b88a9fa84dcee9a4e724497b7dff375..c61ea750ec581c0d53a0428bfff874144b55eed3 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -29,7 +29,7 @@ use std::{ borrow::Cow, cell::RefCell, cmp, fmt::Write, io, iter, ops::Range, rc::Rc, sync::Arc, time::Duration, }; -use util::{post_inc, truncate_and_trailoff, ResultExt, TryFutureExt}; +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 { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index b0b88b37ace7da3d161d87fb1b4367dfae9e8a76..088d26be78ad3048fba5516ac3f67eb3f846d600 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| { @@ -368,9 +361,12 @@ pub fn initialize_workspace( let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone()); let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone()); - let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone()); - let (project_panel, terminal_panel, assistant_panel) = - futures::try_join!(project_panel, terminal_panel, assistant_panel)?; + let assistant_panel = if *util::channel::RELEASE_CHANNEL == ReleaseChannel::Stable { + None + } else { + Some(AssistantPanel::load(workspace_handle.clone(), cx.clone()).await?) + }; + let (project_panel, terminal_panel) = futures::try_join!(project_panel, terminal_panel)?; workspace_handle.update(&mut cx, |workspace, cx| { let project_panel_position = project_panel.position(cx); workspace.add_panel(project_panel, cx); @@ -389,7 +385,9 @@ pub fn initialize_workspace( } workspace.add_panel(terminal_panel, cx); - workspace.add_panel(assistant_panel, cx); + if let Some(assistant_panel) = assistant_panel { + workspace.add_panel(assistant_panel, cx); + } })?; Ok(()) }) From 4efe62b3e5e185f345d6211517f554e2c65c7afc Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 14 Jun 2023 12:14:43 +0200 Subject: [PATCH 15/15] Use robot icon for assistant to prevent confusion with conversations --- assets/icons/robot_14.svg | 4 ++++ crates/ai/src/assistant.rs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) 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 c61ea750ec581c0d53a0428bfff874144b55eed3..e5702cb677b62398c1bb75ae2054980d10c028f9 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -398,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>) {