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