context_strip.rs

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