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