context_strip.rs

  1use std::rc::Rc;
  2
  3use editor::Editor;
  4use gpui::{AppContext, FocusHandle, Model, View, WeakModel, WeakView};
  5use language::Buffer;
  6use project::ProjectEntryId;
  7use ui::{prelude::*, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip};
  8use workspace::Workspace;
  9
 10use crate::context::ContextKind;
 11use crate::context_picker::{ConfirmBehavior, ContextPicker};
 12use crate::context_store::ContextStore;
 13use crate::thread::{Thread, ThreadId};
 14use crate::thread_store::ThreadStore;
 15use crate::ui::ContextPill;
 16use crate::{AssistantPanel, ToggleContextPicker};
 17
 18pub struct ContextStrip {
 19    context_store: Model<ContextStore>,
 20    context_picker: View<ContextPicker>,
 21    context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
 22    focus_handle: FocusHandle,
 23    suggest_context_kind: SuggestContextKind,
 24    workspace: WeakView<Workspace>,
 25}
 26
 27impl ContextStrip {
 28    pub fn new(
 29        context_store: Model<ContextStore>,
 30        workspace: WeakView<Workspace>,
 31        thread_store: Option<WeakModel<ThreadStore>>,
 32        focus_handle: FocusHandle,
 33        context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
 34        suggest_context_kind: SuggestContextKind,
 35        cx: &mut ViewContext<Self>,
 36    ) -> Self {
 37        Self {
 38            context_store: context_store.clone(),
 39            context_picker: cx.new_view(|cx| {
 40                ContextPicker::new(
 41                    workspace.clone(),
 42                    thread_store.clone(),
 43                    context_store.downgrade(),
 44                    ConfirmBehavior::KeepOpen,
 45                    cx,
 46                )
 47            }),
 48            context_picker_menu_handle,
 49            focus_handle,
 50            suggest_context_kind,
 51            workspace,
 52        }
 53    }
 54
 55    fn suggested_context(&self, cx: &ViewContext<Self>) -> Option<SuggestedContext> {
 56        match self.suggest_context_kind {
 57            SuggestContextKind::File => self.suggested_file(cx),
 58            SuggestContextKind::Thread => self.suggested_thread(cx),
 59        }
 60    }
 61
 62    fn suggested_file(&self, cx: &ViewContext<Self>) -> Option<SuggestedContext> {
 63        let workspace = self.workspace.upgrade()?;
 64        let active_item = workspace.read(cx).active_item(cx)?;
 65        let entry_id = *active_item.project_entry_ids(cx).first()?;
 66
 67        if self.context_store.read(cx).contains_project_entry(entry_id) {
 68            return None;
 69        }
 70
 71        let editor = active_item.to_any().downcast::<Editor>().ok()?.read(cx);
 72        let active_buffer = editor.buffer().read(cx).as_singleton()?;
 73
 74        let file = active_buffer.read(cx).file()?;
 75        let title = file.path().to_string_lossy().into_owned().into();
 76
 77        Some(SuggestedContext::File {
 78            entry_id,
 79            title,
 80            buffer: active_buffer.downgrade(),
 81        })
 82    }
 83
 84    fn suggested_thread(&self, cx: &ViewContext<Self>) -> Option<SuggestedContext> {
 85        let workspace = self.workspace.upgrade()?;
 86        let active_thread = workspace
 87            .read(cx)
 88            .panel::<AssistantPanel>(cx)?
 89            .read(cx)
 90            .active_thread(cx);
 91        let weak_active_thread = active_thread.downgrade();
 92
 93        let active_thread = active_thread.read(cx);
 94
 95        if self
 96            .context_store
 97            .read(cx)
 98            .contains_thread(active_thread.id())
 99        {
100            return None;
101        }
102
103        Some(SuggestedContext::Thread {
104            id: active_thread.id().clone(),
105            title: active_thread.summary().unwrap_or("Active Thread".into()),
106            thread: weak_active_thread,
107        })
108    }
109}
110
111impl Render for ContextStrip {
112    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
113        let context_store = self.context_store.read(cx);
114        let context = context_store.context().clone();
115        let context_picker = self.context_picker.clone();
116        let focus_handle = self.focus_handle.clone();
117
118        let suggested_context = self.suggested_context(cx);
119
120        h_flex()
121            .flex_wrap()
122            .gap_1()
123            .child(
124                PopoverMenu::new("context-picker")
125                    .menu(move |_cx| Some(context_picker.clone()))
126                    .trigger(
127                        IconButton::new("add-context", IconName::Plus)
128                            .icon_size(IconSize::Small)
129                            .style(ui::ButtonStyle::Filled)
130                            .tooltip({
131                                let focus_handle = focus_handle.clone();
132
133                                move |cx| {
134                                    Tooltip::for_action_in(
135                                        "Add Context",
136                                        &ToggleContextPicker,
137                                        &focus_handle,
138                                        cx,
139                                    )
140                                }
141                            }),
142                    )
143                    .attach(gpui::Corner::TopLeft)
144                    .anchor(gpui::Corner::BottomLeft)
145                    .offset(gpui::Point {
146                        x: px(0.0),
147                        y: px(-16.0),
148                    })
149                    .with_handle(self.context_picker_menu_handle.clone()),
150            )
151            .when(context.is_empty() && suggested_context.is_none(), {
152                |parent| {
153                    parent.child(
154                        h_flex()
155                            .ml_1p5()
156                            .gap_2()
157                            .child(
158                                Label::new("Add Context")
159                                    .size(LabelSize::Small)
160                                    .color(Color::Muted),
161                            )
162                            .opacity(0.5)
163                            .children(
164                                KeyBinding::for_action_in(&ToggleContextPicker, &focus_handle, cx)
165                                    .map(|binding| binding.into_any_element()),
166                            ),
167                    )
168                }
169            })
170            .children(context.iter().map(|context| {
171                ContextPill::new(context.clone()).on_remove({
172                    let context = context.clone();
173                    let context_store = self.context_store.clone();
174                    Rc::new(cx.listener(move |_this, _event, cx| {
175                        context_store.update(cx, |this, _cx| {
176                            this.remove_context(&context.id);
177                        });
178                        cx.notify();
179                    }))
180                })
181            }))
182            .when_some(suggested_context, |el, suggested| {
183                el.child(
184                    Button::new("add-suggested-context", suggested.title().clone())
185                        .on_click({
186                            let context_store = self.context_store.clone();
187
188                            cx.listener(move |_this, _event, cx| {
189                                context_store.update(cx, |context_store, cx| {
190                                    suggested.accept(context_store, cx);
191                                });
192                                cx.notify();
193                            })
194                        })
195                        .icon(IconName::Plus)
196                        .icon_position(IconPosition::Start)
197                        .icon_size(IconSize::XSmall)
198                        .icon_color(Color::Muted)
199                        .label_size(LabelSize::Small)
200                        .style(ButtonStyle::Filled)
201                        .tooltip(|cx| {
202                            Tooltip::with_meta("Suggested Context", None, "Click to add it", cx)
203                        }),
204                )
205            })
206            .when(!context.is_empty(), {
207                move |parent| {
208                    parent.child(
209                        IconButton::new("remove-all-context", IconName::Eraser)
210                            .icon_size(IconSize::Small)
211                            .tooltip(move |cx| Tooltip::text("Remove All Context", cx))
212                            .on_click({
213                                let context_store = self.context_store.clone();
214                                cx.listener(move |_this, _event, cx| {
215                                    context_store.update(cx, |this, _cx| this.clear());
216                                    cx.notify();
217                                })
218                            }),
219                    )
220                }
221            })
222    }
223}
224
225pub enum SuggestContextKind {
226    File,
227    Thread,
228}
229
230#[derive(Clone)]
231pub enum SuggestedContext {
232    File {
233        entry_id: ProjectEntryId,
234        title: SharedString,
235        buffer: WeakModel<Buffer>,
236    },
237    Thread {
238        id: ThreadId,
239        title: SharedString,
240        thread: WeakModel<Thread>,
241    },
242}
243
244impl SuggestedContext {
245    pub fn title(&self) -> &SharedString {
246        match self {
247            Self::File { title, .. } => title,
248            Self::Thread { title, .. } => title,
249        }
250    }
251
252    pub fn accept(&self, context_store: &mut ContextStore, cx: &mut AppContext) {
253        match self {
254            Self::File {
255                entry_id,
256                title,
257                buffer,
258            } => {
259                let Some(buffer) = buffer.upgrade() else {
260                    return;
261                };
262                let text = buffer.read(cx).text();
263
264                context_store.insert_context(
265                    ContextKind::File(*entry_id),
266                    title.clone(),
267                    text.clone(),
268                );
269            }
270            Self::Thread { id, title, thread } => {
271                let Some(thread) = thread.upgrade() else {
272                    return;
273                };
274
275                context_store.insert_context(
276                    ContextKind::Thread(id.clone()),
277                    title.clone(),
278                    thread.read(cx).text(),
279                );
280            }
281        }
282    }
283}