context_strip.rs

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