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}