context_strip.rs

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