context_strip.rs

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