WIP

Antonio Scandurra created

Change summary

crates/ai2/src/providers/open_ai/completion.rs |   6 
crates/assistant2/src/assistant_panel.rs       | 517 +++++++++----------
crates/editor2/src/editor.rs                   |  93 +-
crates/gpui2/src/window.rs                     |   6 
crates/util/src/arc_cow.rs                     |   8 
5 files changed, 305 insertions(+), 325 deletions(-)

Detailed changes

crates/ai2/src/providers/open_ai/completion.rs 🔗

@@ -104,7 +104,7 @@ pub struct OpenAIResponseStreamEvent {
 
 pub async fn stream_completion(
     credential: ProviderCredential,
-    executor: Arc<BackgroundExecutor>,
+    executor: BackgroundExecutor,
     request: Box<dyn CompletionRequest>,
 ) -> Result<impl Stream<Item = Result<OpenAIResponseStreamEvent>>> {
     let api_key = match credential {
@@ -197,11 +197,11 @@ pub async fn stream_completion(
 pub struct OpenAICompletionProvider {
     model: OpenAILanguageModel,
     credential: Arc<RwLock<ProviderCredential>>,
-    executor: Arc<BackgroundExecutor>,
+    executor: BackgroundExecutor,
 }
 
 impl OpenAICompletionProvider {
-    pub fn new(model_name: &str, executor: Arc<BackgroundExecutor>) -> Self {
+    pub fn new(model_name: &str, executor: BackgroundExecutor) -> Self {
         let model = OpenAILanguageModel::load(model_name);
         let credential = Arc::new(RwLock::new(ProviderCredential::NoCredentials));
         Self {

crates/assistant2/src/assistant_panel.rs 🔗

@@ -27,8 +27,8 @@ use editor::{
 use fs::Fs;
 use futures::StreamExt;
 use gpui::{
-    actions, div, point, uniform_list, Action, AnyElement, AppContext, AsyncAppContext,
-    ClipboardItem, Div, Element, Entity, EventEmitter, FocusHandle, Focusable, FocusableView,
+    actions, div, point, uniform_list, Action, AnyElement, AppContext, AsyncWindowContext,
+    ClipboardItem, Context, Div, EventEmitter, FocusHandle, Focusable, FocusableView,
     HighlightStyle, InteractiveElement, IntoElement, Model, ModelContext, ParentElement, Pixels,
     PromptLevel, Render, StatefulInteractiveElement, Styled, Subscription, Task,
     UniformListScrollHandle, View, ViewContext, VisualContext, WeakModel, WeakView, WindowContext,
@@ -51,7 +51,7 @@ use std::{
 };
 use ui::{
     h_stack, v_stack, Button, ButtonCommon, ButtonLike, Clickable, Color, Icon, IconButton,
-    IconElement, Label, Selectable, StyledExt, Tooltip,
+    IconElement, Label, Selectable, Tooltip,
 };
 use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt};
 use uuid::Uuid;
@@ -76,49 +76,18 @@ actions!(
 
 pub fn init(cx: &mut AppContext) {
     AssistantSettings::register(cx);
-    cx.add_action(
-        |this: &mut AssistantPanel,
-         _: &workspace::NewFile,
-         cx: &mut ViewContext<AssistantPanel>| {
-            this.new_conversation(cx);
-        },
-    );
-    cx.add_action(ConversationEditor::assist);
-    cx.capture_action(ConversationEditor::cancel_last_assist);
-    cx.capture_action(ConversationEditor::save);
-    cx.add_action(ConversationEditor::quote_selection);
-    cx.capture_action(ConversationEditor::copy);
-    cx.add_action(ConversationEditor::split);
-    cx.capture_action(ConversationEditor::cycle_message_role);
-    cx.add_action(AssistantPanel::save_credentials);
-    cx.add_action(AssistantPanel::reset_credentials);
-    cx.add_action(AssistantPanel::toggle_zoom);
-    cx.add_action(AssistantPanel::deploy);
-    cx.add_action(AssistantPanel::select_next_match);
-    cx.add_action(AssistantPanel::select_prev_match);
-    cx.add_action(AssistantPanel::handle_editor_cancel);
-    cx.add_action(
-        |workspace: &mut Workspace, _: &ToggleFocus, cx: &mut ViewContext<Workspace>| {
-            workspace.toggle_panel_focus::<AssistantPanel>(cx);
+    cx.observe_new_views(
+        |workspace: &mut Workspace, cx: &mut ViewContext<Workspace>| {
+            workspace
+                .register_action(|workspace, _: &ToggleFocus, cx| {
+                    workspace.toggle_panel_focus::<AssistantPanel>(cx);
+                })
+                .register_action(AssistantPanel::inline_assist)
+                .register_action(AssistantPanel::cancel_last_inline_assist)
+                .register_action(ConversationEditor::quote_selection);
         },
-    );
-    cx.add_action(AssistantPanel::inline_assist);
-    cx.add_action(AssistantPanel::cancel_last_inline_assist);
-    cx.add_action(InlineAssistant::confirm);
-    cx.add_action(InlineAssistant::cancel);
-    cx.add_action(InlineAssistant::toggle_include_conversation);
-    cx.add_action(InlineAssistant::toggle_retrieve_context);
-    cx.add_action(InlineAssistant::move_up);
-    cx.add_action(InlineAssistant::move_down);
-}
-
-#[derive(Debug)]
-pub enum AssistantPanelEvent {
-    ZoomIn,
-    ZoomOut,
-    Focus,
-    Close,
-    DockPositionChanged,
+    )
+    .detach();
 }
 
 pub struct AssistantPanel {
@@ -131,7 +100,6 @@ pub struct AssistantPanel {
     saved_conversations: Vec<SavedConversationMetadata>,
     saved_conversations_scroll_handle: UniformListScrollHandle,
     zoomed: bool,
-    // todo!("remove has_focus field")
     focus_handle: FocusHandle,
     toolbar: View<Toolbar>,
     completion_provider: Arc<dyn CompletionProvider>,
@@ -152,9 +120,12 @@ pub struct AssistantPanel {
 impl AssistantPanel {
     const INLINE_PROMPT_HISTORY_MAX_LEN: usize = 20;
 
-    pub fn load(workspace: WeakView<Workspace>, cx: AsyncAppContext) -> Task<Result<View<Self>>> {
+    pub fn load(
+        workspace: WeakView<Workspace>,
+        mut cx: AsyncWindowContext,
+    ) -> Task<Result<View<Self>>> {
         cx.spawn(|mut cx| async move {
-            let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?;
+            let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
             let saved_conversations = SavedConversationMetadata::list(fs.clone())
                 .await
                 .log_err()
@@ -163,7 +134,7 @@ impl AssistantPanel {
             // TODO: deserialize state.
             let workspace_handle = workspace.clone();
             workspace.update(&mut cx, |workspace, cx| {
-                cx.add_view::<Self, _>(|cx| {
+                cx.build_view::<Self>(|cx| {
                     const CONVERSATION_WATCH_DURATION: Duration = Duration::from_millis(100);
                     let _watch_saved_conversations = cx.spawn(move |this, mut cx| async move {
                         let mut events = fs
@@ -184,10 +155,10 @@ impl AssistantPanel {
                         anyhow::Ok(())
                     });
 
-                    let toolbar = cx.add_view(|cx| {
+                    let toolbar = cx.build_view(|cx| {
                         let mut toolbar = Toolbar::new();
                         toolbar.set_can_navigate(false, cx);
-                        toolbar.add_item(cx.add_view(|cx| BufferSearchBar::new(cx)), cx);
+                        toolbar.add_item(cx.build_view(|cx| BufferSearchBar::new(cx)), cx);
                         toolbar
                     });
 
@@ -199,8 +170,8 @@ impl AssistantPanel {
                     ));
 
                     let focus_handle = cx.focus_handle();
-                    cx.on_focus_in(Self::focus_in).detach();
-                    cx.on_focus_out(Self::focus_out).detach();
+                    cx.on_focus_in(&focus_handle, Self::focus_in).detach();
+                    cx.on_focus_out(&focus_handle, Self::focus_out).detach();
 
                     let mut this = Self {
                         workspace: workspace_handle,
@@ -231,11 +202,11 @@ impl AssistantPanel {
 
                     let mut old_dock_position = this.position(cx);
                     this.subscriptions =
-                        vec![cx.observe_global::<SettingsStore, _>(move |this, cx| {
+                        vec![cx.observe_global::<SettingsStore>(move |this, cx| {
                             let new_dock_position = this.position(cx);
                             if new_dock_position != old_dock_position {
                                 old_dock_position = new_dock_position;
-                                cx.emit(AssistantPanelEvent::DockPositionChanged);
+                                cx.emit(PanelEvent::ChangePosition);
                             }
                             cx.notify();
                         })];
@@ -343,7 +314,7 @@ impl AssistantPanel {
         // Retrieve Credentials Authenticates the Provider
         provider.retrieve_credentials(cx);
 
-        let codegen = cx.add_model(|cx| {
+        let codegen = cx.build_model(|cx| {
             Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx)
         });
 
@@ -353,14 +324,14 @@ impl AssistantPanel {
                 let previously_indexed = semantic_index
                     .update(&mut cx, |index, cx| {
                         index.project_previously_indexed(&project, cx)
-                    })
+                    })?
                     .await
                     .unwrap_or(false);
                 if previously_indexed {
                     let _ = semantic_index
                         .update(&mut cx, |index, cx| {
                             index.index_project(project.clone(), cx)
-                        })
+                        })?
                         .await;
                 }
                 anyhow::Ok(())
@@ -369,7 +340,7 @@ impl AssistantPanel {
         }
 
         let measurements = Rc::new(Cell::new(BlockMeasurements::default()));
-        let inline_assistant = cx.add_view(|cx| {
+        let inline_assistant = cx.build_view(|cx| {
             let assistant = InlineAssistant::new(
                 inline_assist_id,
                 measurements.clone(),
@@ -382,7 +353,7 @@ impl AssistantPanel {
                 self.semantic_index.clone(),
                 project.clone(),
             );
-            cx.focus_self();
+            assistant.focus_handle.focus(cx);
             assistant
         });
         let block_id = editor.update(cx, |editor, cx| {
@@ -429,8 +400,13 @@ impl AssistantPanel {
                         move |_, editor, event, cx| {
                             if let Some(inline_assistant) = inline_assistant.upgrade() {
                                 if let EditorEvent::SelectionsChanged { local } = event {
-                                    if *local && inline_assistant.read(cx).has_focus {
-                                        cx.focus(&editor);
+                                    if *local
+                                        && inline_assistant
+                                            .read(cx)
+                                            .focus_handle
+                                            .contains_focused(cx)
+                                    {
+                                        cx.focus_view(&editor);
                                     }
                                 }
                             }
@@ -555,7 +531,7 @@ impl AssistantPanel {
             }
         }
 
-        cx.propagate_action();
+        cx.propagate();
     }
 
     fn finish_inline_assist(&mut self, assist_id: usize, undo: bool, cx: &mut ViewContext<Self>) {
@@ -709,13 +685,17 @@ impl AssistantPanel {
             let snippets = cx.spawn(|_, mut cx| async move {
                 let mut snippets = Vec::new();
                 for result in search_results.await {
-                    snippets.push(PromptCodeSnippet::new(result.buffer, result.range, &mut cx));
+                    snippets.push(PromptCodeSnippet::new(
+                        result.buffer,
+                        result.range,
+                        &mut cx,
+                    )?);
                 }
-                snippets
+                anyhow::Ok(snippets)
             });
             snippets
         } else {
-            Task::ready(Vec::new())
+            Task::ready(Ok(Vec::new()))
         };
 
         let mut model = AssistantSettings::get_global(cx)
@@ -724,7 +704,7 @@ impl AssistantPanel {
         let model_name = model.full_name();
 
         let prompt = cx.background_executor().spawn(async move {
-            let snippets = snippets.await;
+            let snippets = snippets.await?;
 
             let language_name = language_name.as_deref();
             generate_content_prompt(
@@ -799,7 +779,7 @@ impl AssistantPanel {
             } else {
                 editor.highlight_background::<PendingInlineAssist>(
                     background_ranges,
-                    |theme| theme.assistant.inline.pending_edit_background,
+                    |theme| gpui::red(), // todo!("use the appropriate color")
                     cx,
                 );
             }
@@ -820,7 +800,7 @@ impl AssistantPanel {
     }
 
     fn new_conversation(&mut self, cx: &mut ViewContext<Self>) -> View<ConversationEditor> {
-        let editor = cx.add_view(|cx| {
+        let editor = cx.build_view(|cx| {
             ConversationEditor::new(
                 self.completion_provider.clone(),
                 self.languages.clone(),
@@ -854,8 +834,8 @@ impl AssistantPanel {
             self.toolbar.update(cx, |toolbar, cx| {
                 toolbar.set_active_item(Some(&editor), cx);
             });
-            if self.has_focus(cx) {
-                cx.focus(&editor);
+            if self.focus_handle.contains_focused(cx) {
+                cx.focus_view(&editor);
             }
         } else {
             self.toolbar.update(cx, |toolbar, cx| {
@@ -891,31 +871,31 @@ impl AssistantPanel {
                 self.completion_provider.save_credentials(cx, credential);
 
                 self.api_key_editor.take();
-                cx.focus_self();
+                self.focus_handle.focus(cx);
                 cx.notify();
             }
         } else {
-            cx.propagate_action();
+            cx.propagate();
         }
     }
 
     fn reset_credentials(&mut self, _: &ResetKey, cx: &mut ViewContext<Self>) {
         self.completion_provider.delete_credentials(cx);
         self.api_key_editor = Some(build_api_key_editor(cx));
-        cx.focus_self();
+        self.focus_handle.focus(cx);
         cx.notify();
     }
 
     fn toggle_zoom(&mut self, _: &workspace::ToggleZoom, cx: &mut ViewContext<Self>) {
         if self.zoomed {
-            cx.emit(AssistantPanelEvent::ZoomOut)
+            cx.emit(PanelEvent::ZoomOut)
         } else {
-            cx.emit(AssistantPanelEvent::ZoomIn)
+            cx.emit(PanelEvent::ZoomIn)
         }
     }
 
     fn deploy(&mut self, action: &search::buffer_search::Deploy, cx: &mut ViewContext<Self>) {
-        let mut propagate_action = true;
+        let mut propagate = true;
         if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
             search_bar.update(cx, |search_bar, cx| {
                 if search_bar.show(cx) {
@@ -924,12 +904,12 @@ impl AssistantPanel {
                         search_bar.select_query(cx);
                         cx.focus_self();
                     }
-                    propagate_action = false
+                    propagate = false
                 }
             });
         }
-        if propagate_action {
-            cx.propagate_action();
+        if propagate {
+            cx.propagate();
         }
     }
 
@@ -942,7 +922,7 @@ impl AssistantPanel {
                 return;
             }
         }
-        cx.propagate_action();
+        cx.propagate();
     }
 
     fn select_next_match(&mut self, _: &search::SelectNextMatch, cx: &mut ViewContext<Self>) {
@@ -976,9 +956,9 @@ impl AssistantPanel {
     fn render_editor_tools(&self, cx: &mut ViewContext<Self>) -> Vec<AnyElement> {
         if self.active_editor().is_some() {
             vec![
-                Self::render_split_button(cx).into_any(),
-                Self::render_quote_button(cx).into_any(),
-                Self::render_assist_button(cx).into_any(),
+                Self::render_split_button(cx).into_any_element(),
+                Self::render_quote_button(cx).into_any_element(),
+                Self::render_assist_button(cx).into_any_element(),
             ]
         } else {
             Default::default()
@@ -1028,16 +1008,13 @@ impl AssistantPanel {
     }
 
     fn render_zoom_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        let zoomed = self.zoomed;
         IconButton::new("zoom_button", Icon::Menu)
             .on_click(cx.listener(|this, _event, cx| {
                 this.toggle_zoom(&ToggleZoom, cx);
             }))
-            .tooltip(|cx| {
-                Tooltip::for_action(
-                    if self.zoomed { "Zoom Out" } else { "Zoom In" },
-                    &ToggleZoom,
-                    cx,
-                )
+            .tooltip(move |cx| {
+                Tooltip::for_action(if zoomed { "Zoom Out" } else { "Zoom In" }, &ToggleZoom, cx)
             })
     }
 
@@ -1072,16 +1049,16 @@ impl AssistantPanel {
         cx.spawn(|this, mut cx| async move {
             let saved_conversation = fs.load(&path).await?;
             let saved_conversation = serde_json::from_str(&saved_conversation)?;
-            let conversation = cx.add_model(|cx| {
+            let conversation = cx.build_model(|cx| {
                 Conversation::deserialize(saved_conversation, path.clone(), languages, cx)
-            });
+            })?;
             this.update(&mut cx, |this, cx| {
                 // If, by the time we've loaded the conversation, the user has already opened
                 // the same conversation, we don't want to open it again.
                 if let Some(ix) = this.editor_index_for_path(&path, cx) {
                     this.set_active_editor_index(Some(ix), cx);
                 } else {
-                    let editor = cx.add_view(|cx| {
+                    let editor = cx.build_view(|cx| {
                         ConversationEditor::for_conversation(conversation, fs, workspace, cx)
                     });
                     this.add_conversation(editor, cx);
@@ -1120,6 +1097,7 @@ impl Render for AssistantPanel {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
         if let Some(api_key_editor) = self.api_key_editor.clone() {
             v_stack()
+                .on_action(cx.listener(AssistantPanel::save_credentials))
                 .track_focus(&self.focus_handle)
                 .child(Label::new(
                     "To use the assistant panel or inline assistant, you need to add your OpenAI api key.",
@@ -1159,6 +1137,15 @@ impl Render for AssistantPanel {
             }
 
             v_stack()
+                .on_action(cx.listener(|this, _: &workspace::NewFile, cx| {
+                    this.new_conversation(cx);
+                }))
+                .on_action(cx.listener(AssistantPanel::reset_credentials))
+                .on_action(cx.listener(AssistantPanel::toggle_zoom))
+                .on_action(cx.listener(AssistantPanel::deploy))
+                .on_action(cx.listener(AssistantPanel::select_next_match))
+                .on_action(cx.listener(AssistantPanel::select_prev_match))
+                .on_action(cx.listener(AssistantPanel::handle_editor_cancel))
                 .track_focus(&self.focus_handle)
                 .child(header)
                 .children(if self.toolbar.read(cx).hidden() {
@@ -1175,7 +1162,7 @@ impl Render for AssistantPanel {
                         self.saved_conversations.len(),
                         |this, range, cx| {
                             range
-                                .map(|ix| this.render_saved_conversation(ix, cx).into_any())
+                                .map(|ix| this.render_saved_conversation(ix, cx))
                                 .collect()
                         },
                     )
@@ -1311,17 +1298,14 @@ impl Conversation {
         completion_provider: Arc<dyn CompletionProvider>,
     ) -> Self {
         let markdown = language_registry.language_for_name("Markdown");
-        let buffer = cx.add_model(|cx| {
-            let mut buffer = Buffer::new(0, cx.model_id() as u64, "");
+        let buffer = cx.build_model(|cx| {
+            let mut buffer = Buffer::new(0, cx.entity_id().as_u64(), "");
             buffer.set_language_registry(language_registry);
-            cx.spawn_weak(|buffer, mut cx| async move {
+            cx.spawn(|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: &mut Buffer, cx| {
                     buffer.set_language(Some(markdown), cx)
-                });
+                })?;
                 anyhow::Ok(())
             })
             .detach_and_log_err(cx);
@@ -1409,8 +1393,8 @@ impl Conversation {
         let markdown = language_registry.language_for_name("Markdown");
         let mut message_anchors = Vec::new();
         let mut next_message_id = MessageId(0);
-        let buffer = cx.add_model(|cx| {
-            let mut buffer = Buffer::new(0, cx.model_id() as u64, saved_conversation.text);
+        let buffer = cx.build_model(|cx| {
+            let mut buffer = Buffer::new(0, cx.entity_id().as_u64(), saved_conversation.text);
             for message in saved_conversation.messages {
                 message_anchors.push(MessageAnchor {
                     id: message.id,
@@ -1419,14 +1403,11 @@ impl Conversation {
                 next_message_id = cmp::max(next_message_id, MessageId(message.id.0 + 1));
             }
             buffer.set_language_registry(language_registry);
-            cx.spawn_weak(|buffer, mut cx| async move {
+            cx.spawn(|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: &mut Buffer, cx| {
                     buffer.set_language(Some(markdown), cx)
-                });
+                })?;
                 anyhow::Ok(())
             })
             .detach_and_log_err(cx);
@@ -1497,26 +1478,24 @@ impl Conversation {
             })
             .collect::<Vec<_>>();
         let model = self.model.clone();
-        self.pending_token_count = cx.spawn_weak(|this, mut cx| {
+        self.pending_token_count = cx.spawn(|this, mut cx| {
             async move {
                 cx.background_executor()
                     .timer(Duration::from_millis(200))
                     .await;
                 let token_count = cx
-                    .background()
+                    .background_executor()
                     .spawn(async move {
                         tiktoken_rs::num_tokens_from_messages(&model.full_name(), &messages)
                     })
                     .await?;
 
-                this.upgrade(&cx)
-                    .ok_or_else(|| anyhow!("conversation was dropped"))?
-                    .update(&mut cx, |this, cx| {
-                        this.max_token_count =
-                            tiktoken_rs::model::get_context_size(&this.model.full_name());
-                        this.token_count = Some(token_count);
-                        cx.notify()
-                    });
+                this.update(&mut cx, |this, cx| {
+                    this.max_token_count =
+                        tiktoken_rs::model::get_context_size(&this.model.full_name());
+                    this.token_count = Some(token_count);
+                    cx.notify()
+                })?;
                 anyhow::Ok(())
             }
             .log_err()
@@ -1603,7 +1582,7 @@ impl Conversation {
                 .unwrap();
             user_messages.push(user_message);
 
-            let task = cx.spawn_weak({
+            let task = cx.spawn({
                 |this, mut cx| async move {
                     let assistant_message_id = assistant_message.id;
                     let stream_completion = async {
@@ -1612,59 +1591,55 @@ impl Conversation {
                         while let Some(message) = messages.next().await {
                             let text = message?;
 
-                            this.upgrade(&cx)
-                                .ok_or_else(|| anyhow!("conversation was dropped"))?
-                                .update(&mut cx, |this, cx| {
-                                    let message_ix = this
-                                        .message_anchors
+                            this.update(&mut cx, |this, cx| {
+                                let message_ix = this
+                                    .message_anchors
+                                    .iter()
+                                    .position(|message| message.id == assistant_message_id)?;
+                                this.buffer.update(cx, |buffer, cx| {
+                                    let offset = this.message_anchors[message_ix + 1..]
                                         .iter()
-                                        .position(|message| message.id == assistant_message_id)?;
-                                    this.buffer.update(cx, |buffer, cx| {
-                                        let offset = this.message_anchors[message_ix + 1..]
-                                            .iter()
-                                            .find(|message| message.start.is_valid(buffer))
-                                            .map_or(buffer.len(), |message| {
-                                                message.start.to_offset(buffer).saturating_sub(1)
-                                            });
-                                        buffer.edit([(offset..offset, text)], None, cx);
-                                    });
-                                    cx.emit(ConversationEvent::StreamedCompletion);
-
-                                    Some(())
+                                        .find(|message| message.start.is_valid(buffer))
+                                        .map_or(buffer.len(), |message| {
+                                            message.start.to_offset(buffer).saturating_sub(1)
+                                        });
+                                    buffer.edit([(offset..offset, text)], None, cx);
                                 });
+                                cx.emit(ConversationEvent::StreamedCompletion);
+
+                                Some(())
+                            })?;
                             smol::future::yield_now().await;
                         }
 
-                        this.upgrade(&cx)
-                            .ok_or_else(|| anyhow!("conversation was dropped"))?
-                            .update(&mut cx, |this, cx| {
-                                this.pending_completions
-                                    .retain(|completion| completion.id != this.completion_count);
-                                this.summarize(cx);
-                            });
+                        this.update(&mut cx, |this, cx| {
+                            this.pending_completions
+                                .retain(|completion| completion.id != this.completion_count);
+                            this.summarize(cx);
+                        })?;
 
                         anyhow::Ok(())
                     };
 
                     let result = stream_completion.await;
-                    if let Some(this) = this.upgrade(&cx) {
-                        this.update(&mut cx, |this, cx| {
-                            if let Some(metadata) =
-                                this.messages_metadata.get_mut(&assistant_message.id)
-                            {
-                                match result {
-                                    Ok(_) => {
-                                        metadata.status = MessageStatus::Done;
-                                    }
-                                    Err(error) => {
-                                        metadata.status =
-                                            MessageStatus::Error(error.to_string().trim().into());
-                                    }
+
+                    this.update(&mut cx, |this, cx| {
+                        if let Some(metadata) =
+                            this.messages_metadata.get_mut(&assistant_message.id)
+                        {
+                            match result {
+                                Ok(_) => {
+                                    metadata.status = MessageStatus::Done;
+                                }
+                                Err(error) => {
+                                    metadata.status =
+                                        MessageStatus::Error(error.to_string().trim().into());
                                 }
-                                cx.notify();
                             }
-                        });
-                    }
+                            cx.notify();
+                        }
+                    })
+                    .ok();
                 }
             });
 
@@ -1999,10 +1974,10 @@ impl Conversation {
                     None
                 };
                 (path, summary)
-            });
+            })?;
 
             if let Some(summary) = summary {
-                let conversation = this.read_with(&cx, |this, cx| this.serialize(cx));
+                let conversation = this.read_with(&cx, |this, cx| this.serialize(cx))?;
                 let path = if let Some(old_path) = old_path {
                     old_path
                 } else {
@@ -2026,7 +2001,7 @@ impl Conversation {
                 fs.create_dir(CONVERSATIONS_DIR.as_ref()).await?;
                 fs.atomic_write(path.clone(), serde_json::to_string(&conversation).unwrap())
                     .await?;
-                this.update(&mut cx, |this, _| this.path = Some(path));
+                this.update(&mut cx, |this, _| this.path = Some(path))?;
             }
 
             Ok(())
@@ -2069,7 +2044,7 @@ impl ConversationEditor {
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let conversation =
-            cx.add_model(|cx| Conversation::new(language_registry, cx, completion_provider));
+            cx.build_model(|cx| Conversation::new(language_registry, cx, completion_provider));
         Self::for_conversation(conversation, fs, workspace, cx)
     }
 
@@ -2079,7 +2054,7 @@ impl ConversationEditor {
         workspace: WeakView<Workspace>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
-        let editor = cx.add_view(|cx| {
+        let editor = cx.build_view(|cx| {
             let mut editor = Editor::for_buffer(conversation.read(cx).buffer.clone(), None, cx);
             editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
             editor.set_show_gutter(false, cx);
@@ -2093,7 +2068,7 @@ impl ConversationEditor {
             cx.observe(&conversation, |_, _, cx| cx.notify()),
             cx.subscribe(&conversation, Self::handle_conversation_event),
             cx.subscribe(&editor, Self::handle_editor_event),
-            cx.on_focus(&focus_handle, |this, _, cx| cx.focus(&this.editor)),
+            cx.on_focus(&focus_handle, |this, cx| cx.focus_view(&this.editor)),
         ];
 
         let mut this = Self {
@@ -2155,7 +2130,7 @@ impl ConversationEditor {
             .conversation
             .update(cx, |conversation, _| conversation.cancel_last_assist())
         {
-            cx.propagate_action();
+            cx.propagate();
         }
     }
 
@@ -2247,8 +2222,8 @@ impl ConversationEditor {
                 .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) {
+            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: point(scroll_position.x, cursor_row - scroll_position.y),
@@ -2286,7 +2261,7 @@ impl ConversationEditor {
                                 })
                                 .on_click({
                                     let conversation = conversation.clone();
-                                    move |_, _, cx| {
+                                    move |_, cx| {
                                         conversation.update(cx, |conversation, cx| {
                                             conversation.cycle_message_roles(
                                                 HashSet::from_iter(Some(message_id)),
@@ -2302,18 +2277,16 @@ impl ConversationEditor {
                                 .border_color(gpui::red())
                                 .child(sender)
                                 .child(Label::new(message.sent_at.format("%I:%M%P").to_string()))
-                                .with_children(
-                                    if let MessageStatus::Error(error) = &message.status {
-                                        Some(
-                                            div()
-                                                .id("error")
-                                                .tooltip(|cx| Tooltip::text(error, cx))
-                                                .child(IconElement::new(Icon::XCircle)),
-                                        )
-                                    } else {
-                                        None
-                                    },
-                                )
+                                .children(if let MessageStatus::Error(error) = &message.status {
+                                    Some(
+                                        div()
+                                            .id("error")
+                                            .tooltip(|cx| Tooltip::text(error, cx))
+                                            .child(IconElement::new(Icon::XCircle)),
+                                    )
+                                } else {
+                                    None
+                                })
                                 .into_any_element()
                         }
                     }),
@@ -2342,36 +2315,35 @@ impl ConversationEditor {
             return;
         };
 
-        let text = editor.read_with(cx, |editor, cx| {
-            let range = editor.selections.newest::<usize>(cx).range();
-            let buffer = editor.buffer().read(cx).snapshot(cx);
-            let start_language = buffer.language_at(range.start);
-            let end_language = buffer.language_at(range.end);
-            let language_name = if start_language == end_language {
-                start_language.map(|language| language.name())
-            } else {
-                None
-            };
-            let language_name = language_name.as_deref().unwrap_or("").to_lowercase();
+        let editor = editor.read(cx);
+        let range = editor.selections.newest::<usize>(cx).range();
+        let buffer = editor.buffer().read(cx).snapshot(cx);
+        let start_language = buffer.language_at(range.start);
+        let end_language = buffer.language_at(range.end);
+        let language_name = if start_language == end_language {
+            start_language.map(|language| language.name())
+        } else {
+            None
+        };
+        let language_name = language_name.as_deref().unwrap_or("").to_lowercase();
 
-            let selected_text = buffer.text_for_range(range).collect::<String>();
-            if selected_text.is_empty() {
-                None
+        let selected_text = buffer.text_for_range(range).collect::<String>();
+        let text = if selected_text.is_empty() {
+            None
+        } else {
+            Some(if language_name == "markdown" {
+                selected_text
+                    .lines()
+                    .map(|line| format!("> {}", line))
+                    .collect::<Vec<_>>()
+                    .join("\n")
             } else {
-                Some(if language_name == "markdown" {
-                    selected_text
-                        .lines()
-                        .map(|line| format!("> {}", line))
-                        .collect::<Vec<_>>()
-                        .join("\n")
-                } else {
-                    format!("```{language_name}\n{selected_text}\n```")
-                })
-            }
-        });
+                format!("```{language_name}\n{selected_text}\n```")
+            })
+        };
 
         // Activate the panel
-        if !panel.read(cx).has_focus(cx) {
+        if !panel.focus_handle(cx).contains_focused(cx) {
             workspace.toggle_panel_focus::<AssistantPanel>(cx);
         }
 
@@ -2415,13 +2387,12 @@ impl ConversationEditor {
             }
 
             if spanned_messages > 1 {
-                cx.platform()
-                    .write_to_clipboard(ClipboardItem::new(copied_text));
+                cx.write_to_clipboard(ClipboardItem::new(copied_text));
                 return;
             }
         }
 
-        cx.propagate_action();
+        cx.propagate();
     }
 
     fn split(&mut self, _: &Split, cx: &mut ViewContext<Self>) {
@@ -2492,15 +2463,30 @@ impl Render for ConversationEditor {
     type Element = Div;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-        div().relative().child(self.editor.clone()).child(
-            h_stack()
-                .absolute()
-                .gap_1()
-                .top_3()
-                .right_5()
-                .child(self.render_current_model(cx))
-                .children(self.render_remaining_tokens(cx)),
-        )
+        div()
+            .relative()
+            .capture_action(cx.listener(ConversationEditor::cancel_last_assist))
+            .capture_action(cx.listener(ConversationEditor::save))
+            .capture_action(cx.listener(ConversationEditor::copy))
+            .capture_action(cx.listener(ConversationEditor::cycle_message_role))
+            .on_action(cx.listener(ConversationEditor::assist))
+            .on_action(cx.listener(ConversationEditor::split))
+            .child(self.editor.clone())
+            .child(
+                h_stack()
+                    .absolute()
+                    .gap_1()
+                    .top_3()
+                    .right_5()
+                    .child(self.render_current_model(cx))
+                    .children(self.render_remaining_tokens(cx)),
+            )
+    }
+}
+
+impl FocusableView for ConversationEditor {
+    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
+        self.focus_handle.clone()
     }
 }
 
@@ -2577,30 +2563,40 @@ impl Render for InlineAssistant {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
         let measurements = self.measurements.get();
         h_stack()
+            .on_action(cx.listener(Self::confirm))
+            .on_action(cx.listener(Self::cancel))
+            .on_action(cx.listener(Self::toggle_include_conversation))
+            .on_action(cx.listener(Self::toggle_retrieve_context))
+            .on_action(cx.listener(Self::move_up))
+            .on_action(cx.listener(Self::move_down))
             .child(
                 h_stack()
                     .justify_center()
                     .w(measurements.gutter_width)
                     .child(
                         IconButton::new("include_conversation", Icon::Ai)
-                            .action(ToggleIncludeConversation)
+                            .action(Box::new(ToggleIncludeConversation))
                             .selected(self.include_conversation)
-                            .tooltip(Tooltip::for_action(
-                                "Include Conversation",
-                                &ToggleIncludeConversation,
-                                cx,
-                            )),
+                            .tooltip(|cx| {
+                                Tooltip::for_action(
+                                    "Include Conversation",
+                                    &ToggleIncludeConversation,
+                                    cx,
+                                )
+                            }),
                     )
                     .children(if SemanticIndex::enabled(cx) {
                         Some(
                             IconButton::new("retrieve_context", Icon::MagnifyingGlass)
-                                .action(ToggleRetrieveContext)
+                                .action(Box::new(ToggleRetrieveContext))
                                 .selected(self.retrieve_context)
-                                .tooltip(Tooltip::for_action(
-                                    "Retrieve Context",
-                                    &ToggleRetrieveContext,
-                                    cx,
-                                )),
+                                .tooltip(|cx| {
+                                    Tooltip::for_action(
+                                        "Retrieve Context",
+                                        &ToggleRetrieveContext,
+                                        cx,
+                                    )
+                                }),
                         )
                     } else {
                         None
@@ -2629,6 +2625,12 @@ impl Render for InlineAssistant {
     }
 }
 
+impl FocusableView for InlineAssistant {
+    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
+        self.focus_handle.clone()
+    }
+}
+
 impl InlineAssistant {
     fn new(
         id: usize,
@@ -2656,10 +2658,7 @@ impl InlineAssistant {
         let mut subscriptions = vec![
             cx.observe(&codegen, Self::handle_codegen_changed),
             cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events),
-            cx.on_focus(
-                &focus_handle,
-                cx.listener(|this, _, cx| cx.focus(&this.prompt_editor)),
-            ),
+            cx.on_focus(&focus_handle, |this, cx| cx.focus_view(&this.prompt_editor)),
         ];
 
         if let Some(semantic_index) = semantic_index.clone() {
@@ -2939,42 +2938,17 @@ impl InlineAssistant {
                         div()
                             .id("update")
                             .tooltip(|cx| Tooltip::text(status_text, cx))
-                            .child(IconElement::new(Icon::Update).color(color))
+                            .child(IconElement::new(Icon::Update).color(Color::Info))
                             .into_any_element()
-                        Svg::new("icons/update.svg")
-                            .with_color(theme.assistant.inline.context_status.in_progress_icon.color)
-                            .constrained()
-                            .with_width(theme.assistant.inline.context_status.in_progress_icon.width)
-                            .contained()
-                            .with_style(theme.assistant.inline.context_status.in_progress_icon.container)
-                            .with_tooltip::<ContextStatusIcon>(
-                                self.id,
-                                status_text,
-                                None,
-                                theme.tooltip.clone(),
-                                cx,
-                            )
-                            .aligned()
-                            .into_any(),
                     )
                 }
 
                 SemanticIndexStatus::Indexed {} => Some(
-                    Svg::new("icons/check.svg")
-                        .with_color(theme.assistant.inline.context_status.complete_icon.color)
-                        .constrained()
-                        .with_width(theme.assistant.inline.context_status.complete_icon.width)
-                        .contained()
-                        .with_style(theme.assistant.inline.context_status.complete_icon.container)
-                        .with_tooltip::<ContextStatusIcon>(
-                            self.id,
-                            "Index up to date",
-                            None,
-                            theme.tooltip.clone(),
-                            cx,
-                        )
-                        .aligned()
-                        .into_any(),
+                    div()
+                        .id("check")
+                        .tooltip(|cx| Tooltip::text("Index up to date", cx))
+                        .child(IconElement::new(Icon::Check).color(Color::Success))
+                        .into_any_element()
                 ),
             }
         } else {
@@ -3083,7 +3057,8 @@ mod tests {
         let registry = Arc::new(LanguageRegistry::test());
 
         let completion_provider = Arc::new(FakeCompletionProvider::new());
-        let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider));
+        let conversation =
+            cx.build_model(|cx| Conversation::new(registry, cx, completion_provider));
         let buffer = conversation.read(cx).buffer.clone();
 
         let message_1 = conversation.read(cx).message_anchors[0].clone();
@@ -3213,7 +3188,8 @@ mod tests {
         let registry = Arc::new(LanguageRegistry::test());
         let completion_provider = Arc::new(FakeCompletionProvider::new());
 
-        let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider));
+        let conversation =
+            cx.build_model(|cx| Conversation::new(registry, cx, completion_provider));
         let buffer = conversation.read(cx).buffer.clone();
 
         let message_1 = conversation.read(cx).message_anchors[0].clone();
@@ -3310,7 +3286,8 @@ mod tests {
         init(cx);
         let registry = Arc::new(LanguageRegistry::test());
         let completion_provider = Arc::new(FakeCompletionProvider::new());
-        let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider));
+        let conversation =
+            cx.build_model(|cx| Conversation::new(registry, cx, completion_provider));
         let buffer = conversation.read(cx).buffer.clone();
 
         let message_1 = conversation.read(cx).message_anchors[0].clone();

crates/editor2/src/editor.rs 🔗

@@ -1961,14 +1961,14 @@ impl Editor {
         cx.notify();
     }
 
-    //     pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape, cx: &mut ViewContext<Self>) {
-    //         self.cursor_shape = cursor_shape;
-    //         cx.notify();
-    //     }
+    pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape, cx: &mut ViewContext<Self>) {
+        self.cursor_shape = cursor_shape;
+        cx.notify();
+    }
 
-    //     pub fn set_collapse_matches(&mut self, collapse_matches: bool) {
-    //         self.collapse_matches = collapse_matches;
-    //     }
+    pub fn set_collapse_matches(&mut self, collapse_matches: bool) {
+        self.collapse_matches = collapse_matches;
+    }
 
     pub fn range_for_match<T: std::marker::Copy>(&self, range: &Range<T>) -> Range<T> {
         if self.collapse_matches {
@@ -1977,56 +1977,47 @@ impl Editor {
         range.clone()
     }
 
-    //     pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut ViewContext<Self>) {
-    //         if self.display_map.read(cx).clip_at_line_ends != clip {
-    //             self.display_map
-    //                 .update(cx, |map, _| map.clip_at_line_ends = clip);
-    //         }
-    //     }
-
-    //     pub fn set_keymap_context_layer<Tag: 'static>(
-    //         &mut self,
-    //         context: KeymapContext,
-    //         cx: &mut ViewContext<Self>,
-    //     ) {
-    //         self.keymap_context_layers
-    //             .insert(TypeId::of::<Tag>(), context);
-    //         cx.notify();
-    //     }
+    pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut ViewContext<Self>) {
+        if self.display_map.read(cx).clip_at_line_ends != clip {
+            self.display_map
+                .update(cx, |map, _| map.clip_at_line_ends = clip);
+        }
+    }
 
-    //     pub fn remove_keymap_context_layer<Tag: 'static>(&mut self, cx: &mut ViewContext<Self>) {
-    //         self.keymap_context_layers.remove(&TypeId::of::<Tag>());
-    //         cx.notify();
-    //     }
+    pub fn set_keymap_context_layer<Tag: 'static>(
+        &mut self,
+        context: KeyContext,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.keymap_context_layers
+            .insert(TypeId::of::<Tag>(), context);
+        cx.notify();
+    }
 
-    //     pub fn set_input_enabled(&mut self, input_enabled: bool) {
-    //         self.input_enabled = input_enabled;
-    //     }
+    pub fn remove_keymap_context_layer<Tag: 'static>(&mut self, cx: &mut ViewContext<Self>) {
+        self.keymap_context_layers.remove(&TypeId::of::<Tag>());
+        cx.notify();
+    }
 
-    //     pub fn set_autoindent(&mut self, autoindent: bool) {
-    //         if autoindent {
-    //             self.autoindent_mode = Some(AutoindentMode::EachLine);
-    //         } else {
-    //             self.autoindent_mode = None;
-    //         }
-    //     }
+    pub fn set_input_enabled(&mut self, input_enabled: bool) {
+        self.input_enabled = input_enabled;
+    }
 
-    //     pub fn read_only(&self) -> bool {
-    //         self.read_only
-    //     }
+    pub fn set_autoindent(&mut self, autoindent: bool) {
+        if autoindent {
+            self.autoindent_mode = Some(AutoindentMode::EachLine);
+        } else {
+            self.autoindent_mode = None;
+        }
+    }
 
-    //     pub fn set_read_only(&mut self, read_only: bool) {
-    //         self.read_only = read_only;
-    //     }
+    pub fn read_only(&self) -> bool {
+        self.read_only
+    }
 
-    //     pub fn set_field_editor_style(
-    //         &mut self,
-    //         style: Option<Arc<GetFieldEditorTheme>>,
-    //         cx: &mut ViewContext<Self>,
-    //     ) {
-    //         self.get_field_editor_theme = style;
-    //         cx.notify();
-    //     }
+    pub fn set_read_only(&mut self, read_only: bool) {
+        self.read_only = read_only;
+    }
 
     fn selections_did_change(
         &mut self,

crates/gpui2/src/window.rs 🔗

@@ -2816,3 +2816,9 @@ impl From<(&'static str, EntityId)> for ElementId {
         ElementId::NamedInteger(name.into(), id.as_u64() as usize)
     }
 }
+
+impl From<(&'static str, usize)> for ElementId {
+    fn from((name, id): (&'static str, usize)) -> Self {
+        ElementId::NamedInteger(name.into(), id)
+    }
+}

crates/util/src/arc_cow.rs 🔗

@@ -44,12 +44,18 @@ impl<'a, T: ?Sized> From<&'a T> for ArcCow<'a, T> {
     }
 }
 
-impl<T> From<Arc<T>> for ArcCow<'_, T> {
+impl<T: ?Sized> From<Arc<T>> for ArcCow<'_, T> {
     fn from(s: Arc<T>) -> Self {
         Self::Owned(s)
     }
 }
 
+impl<T: ?Sized> From<&'_ Arc<T>> for ArcCow<'_, T> {
+    fn from(s: &'_ Arc<T>) -> Self {
+        Self::Owned(s.clone())
+    }
+}
+
 impl From<String> for ArcCow<'_, str> {
     fn from(value: String) -> Self {
         Self::Owned(value.into())