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