1mod completion_provider;
2mod fetch_context_picker;
3mod file_context_picker;
4mod rules_context_picker;
5mod symbol_context_picker;
6mod thread_context_picker;
7
8use std::ops::Range;
9use std::path::PathBuf;
10use std::sync::Arc;
11
12use anyhow::{Result, anyhow};
13pub use completion_provider::ContextPickerCompletionProvider;
14use editor::display_map::{Crease, FoldId};
15use editor::{Anchor, AnchorRangeExt as _, Editor, ExcerptId, FoldPlaceholder, ToOffset};
16use fetch_context_picker::FetchContextPicker;
17use file_context_picker::FileContextPicker;
18use file_context_picker::render_file_context_entry;
19use gpui::{
20 App, DismissEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task,
21 WeakEntity,
22};
23use language::Buffer;
24use multi_buffer::MultiBufferRow;
25use project::{Entry, ProjectPath};
26use prompt_store::{PromptStore, UserPromptId};
27use rules_context_picker::{RulesContextEntry, RulesContextPicker};
28use symbol_context_picker::SymbolContextPicker;
29use thread_context_picker::{ThreadContextEntry, ThreadContextPicker, render_thread_context_entry};
30use ui::{
31 ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*,
32};
33use uuid::Uuid;
34use workspace::{Workspace, notifications::NotifyResultExt};
35
36use crate::AssistantPanel;
37use crate::context::RULES_ICON;
38use crate::context_store::ContextStore;
39use crate::thread::ThreadId;
40use crate::thread_store::ThreadStore;
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43enum ContextPickerEntry {
44 Mode(ContextPickerMode),
45 Action(ContextPickerAction),
46}
47
48impl ContextPickerEntry {
49 pub fn keyword(&self) -> &'static str {
50 match self {
51 Self::Mode(mode) => mode.keyword(),
52 Self::Action(action) => action.keyword(),
53 }
54 }
55
56 pub fn label(&self) -> &'static str {
57 match self {
58 Self::Mode(mode) => mode.label(),
59 Self::Action(action) => action.label(),
60 }
61 }
62
63 pub fn icon(&self) -> IconName {
64 match self {
65 Self::Mode(mode) => mode.icon(),
66 Self::Action(action) => action.icon(),
67 }
68 }
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72enum ContextPickerMode {
73 File,
74 Symbol,
75 Fetch,
76 Thread,
77 Rules,
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81enum ContextPickerAction {
82 AddSelections,
83}
84
85impl ContextPickerAction {
86 pub fn keyword(&self) -> &'static str {
87 match self {
88 Self::AddSelections => "selection",
89 }
90 }
91
92 pub fn label(&self) -> &'static str {
93 match self {
94 Self::AddSelections => "Selection",
95 }
96 }
97
98 pub fn icon(&self) -> IconName {
99 match self {
100 Self::AddSelections => IconName::Context,
101 }
102 }
103}
104
105impl TryFrom<&str> for ContextPickerMode {
106 type Error = String;
107
108 fn try_from(value: &str) -> Result<Self, Self::Error> {
109 match value {
110 "file" => Ok(Self::File),
111 "symbol" => Ok(Self::Symbol),
112 "fetch" => Ok(Self::Fetch),
113 "thread" => Ok(Self::Thread),
114 "rules" => Ok(Self::Rules),
115 _ => Err(format!("Invalid context picker mode: {}", value)),
116 }
117 }
118}
119
120impl ContextPickerMode {
121 pub fn keyword(&self) -> &'static str {
122 match self {
123 Self::File => "file",
124 Self::Symbol => "symbol",
125 Self::Fetch => "fetch",
126 Self::Thread => "thread",
127 Self::Rules => "rules",
128 }
129 }
130
131 pub fn label(&self) -> &'static str {
132 match self {
133 Self::File => "Files & Directories",
134 Self::Symbol => "Symbols",
135 Self::Fetch => "Fetch",
136 Self::Thread => "Threads",
137 Self::Rules => "Rules",
138 }
139 }
140
141 pub fn icon(&self) -> IconName {
142 match self {
143 Self::File => IconName::File,
144 Self::Symbol => IconName::Code,
145 Self::Fetch => IconName::Globe,
146 Self::Thread => IconName::MessageBubbles,
147 Self::Rules => RULES_ICON,
148 }
149 }
150}
151
152#[derive(Debug, Clone)]
153enum ContextPickerState {
154 Default(Entity<ContextMenu>),
155 File(Entity<FileContextPicker>),
156 Symbol(Entity<SymbolContextPicker>),
157 Fetch(Entity<FetchContextPicker>),
158 Thread(Entity<ThreadContextPicker>),
159 Rules(Entity<RulesContextPicker>),
160}
161
162pub(super) struct ContextPicker {
163 mode: ContextPickerState,
164 workspace: WeakEntity<Workspace>,
165 context_store: WeakEntity<ContextStore>,
166 thread_store: Option<WeakEntity<ThreadStore>>,
167 prompt_store: Option<Entity<PromptStore>>,
168 _subscriptions: Vec<Subscription>,
169}
170
171impl ContextPicker {
172 pub fn new(
173 workspace: WeakEntity<Workspace>,
174 thread_store: Option<WeakEntity<ThreadStore>>,
175 context_store: WeakEntity<ContextStore>,
176 window: &mut Window,
177 cx: &mut Context<Self>,
178 ) -> Self {
179 let subscriptions = context_store
180 .upgrade()
181 .map(|context_store| {
182 cx.observe(&context_store, |this, _, cx| this.notify_current_picker(cx))
183 })
184 .into_iter()
185 .chain(
186 thread_store
187 .as_ref()
188 .and_then(|thread_store| thread_store.upgrade())
189 .map(|thread_store| {
190 cx.observe(&thread_store, |this, _, cx| this.notify_current_picker(cx))
191 }),
192 )
193 .collect::<Vec<Subscription>>();
194
195 let prompt_store = thread_store.as_ref().and_then(|thread_store| {
196 thread_store
197 .read_with(cx, |thread_store, _cx| thread_store.prompt_store().clone())
198 .ok()
199 .flatten()
200 });
201
202 ContextPicker {
203 mode: ContextPickerState::Default(ContextMenu::build(
204 window,
205 cx,
206 |menu, _window, _cx| menu,
207 )),
208 workspace,
209 context_store,
210 thread_store,
211 prompt_store,
212 _subscriptions: subscriptions,
213 }
214 }
215
216 pub fn init(&mut self, window: &mut Window, cx: &mut Context<Self>) {
217 self.mode = ContextPickerState::Default(self.build_menu(window, cx));
218 cx.notify();
219 }
220
221 fn build_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<ContextMenu> {
222 let context_picker = cx.entity().clone();
223
224 let menu = ContextMenu::build(window, cx, move |menu, _window, cx| {
225 let recent = self.recent_entries(cx);
226 let has_recent = !recent.is_empty();
227 let recent_entries = recent
228 .into_iter()
229 .enumerate()
230 .map(|(ix, entry)| self.recent_menu_item(context_picker.clone(), ix, entry));
231
232 let entries = self
233 .workspace
234 .upgrade()
235 .map(|workspace| {
236 available_context_picker_entries(
237 &self.prompt_store,
238 &self.thread_store,
239 &workspace,
240 cx,
241 )
242 })
243 .unwrap_or_default();
244
245 menu.when(has_recent, |menu| {
246 menu.custom_row(|_, _| {
247 div()
248 .mb_1()
249 .child(
250 Label::new("Recent")
251 .color(Color::Muted)
252 .size(LabelSize::Small),
253 )
254 .into_any_element()
255 })
256 })
257 .extend(recent_entries)
258 .when(has_recent, |menu| menu.separator())
259 .extend(entries.into_iter().map(|entry| {
260 let context_picker = context_picker.clone();
261
262 ContextMenuEntry::new(entry.label())
263 .icon(entry.icon())
264 .icon_size(IconSize::XSmall)
265 .icon_color(Color::Muted)
266 .handler(move |window, cx| {
267 context_picker.update(cx, |this, cx| this.select_entry(entry, window, cx))
268 })
269 }))
270 .keep_open_on_confirm()
271 });
272
273 cx.subscribe(&menu, move |_, _, _: &DismissEvent, cx| {
274 cx.emit(DismissEvent);
275 })
276 .detach();
277
278 menu
279 }
280
281 /// Whether threads are allowed as context.
282 pub fn allow_threads(&self) -> bool {
283 self.thread_store.is_some()
284 }
285
286 fn select_entry(
287 &mut self,
288 entry: ContextPickerEntry,
289 window: &mut Window,
290 cx: &mut Context<Self>,
291 ) {
292 let context_picker = cx.entity().downgrade();
293
294 match entry {
295 ContextPickerEntry::Mode(mode) => match mode {
296 ContextPickerMode::File => {
297 self.mode = ContextPickerState::File(cx.new(|cx| {
298 FileContextPicker::new(
299 context_picker.clone(),
300 self.workspace.clone(),
301 self.context_store.clone(),
302 window,
303 cx,
304 )
305 }));
306 }
307 ContextPickerMode::Symbol => {
308 self.mode = ContextPickerState::Symbol(cx.new(|cx| {
309 SymbolContextPicker::new(
310 context_picker.clone(),
311 self.workspace.clone(),
312 self.context_store.clone(),
313 window,
314 cx,
315 )
316 }));
317 }
318 ContextPickerMode::Rules => {
319 if let Some(prompt_store) = self.prompt_store.as_ref() {
320 self.mode = ContextPickerState::Rules(cx.new(|cx| {
321 RulesContextPicker::new(
322 prompt_store.clone(),
323 context_picker.clone(),
324 self.context_store.clone(),
325 window,
326 cx,
327 )
328 }));
329 }
330 }
331 ContextPickerMode::Fetch => {
332 self.mode = ContextPickerState::Fetch(cx.new(|cx| {
333 FetchContextPicker::new(
334 context_picker.clone(),
335 self.workspace.clone(),
336 self.context_store.clone(),
337 window,
338 cx,
339 )
340 }));
341 }
342 ContextPickerMode::Thread => {
343 if let Some(thread_store) = self.thread_store.as_ref() {
344 self.mode = ContextPickerState::Thread(cx.new(|cx| {
345 ThreadContextPicker::new(
346 thread_store.clone(),
347 context_picker.clone(),
348 self.context_store.clone(),
349 window,
350 cx,
351 )
352 }));
353 }
354 }
355 },
356 ContextPickerEntry::Action(action) => match action {
357 ContextPickerAction::AddSelections => {
358 if let Some((context_store, workspace)) =
359 self.context_store.upgrade().zip(self.workspace.upgrade())
360 {
361 add_selections_as_context(&context_store, &workspace, cx);
362 }
363
364 cx.emit(DismissEvent);
365 }
366 },
367 }
368
369 cx.notify();
370 cx.focus_self(window);
371 }
372
373 fn recent_menu_item(
374 &self,
375 context_picker: Entity<ContextPicker>,
376 ix: usize,
377 entry: RecentEntry,
378 ) -> ContextMenuItem {
379 match entry {
380 RecentEntry::File {
381 project_path,
382 path_prefix,
383 } => {
384 let context_store = self.context_store.clone();
385 let worktree_id = project_path.worktree_id;
386 let path = project_path.path.clone();
387
388 ContextMenuItem::custom_entry(
389 move |_window, cx| {
390 render_file_context_entry(
391 ElementId::named_usize("ctx-recent", ix),
392 worktree_id,
393 &path,
394 &path_prefix,
395 false,
396 context_store.clone(),
397 cx,
398 )
399 .into_any()
400 },
401 move |window, cx| {
402 context_picker.update(cx, |this, cx| {
403 this.add_recent_file(project_path.clone(), window, cx);
404 })
405 },
406 )
407 }
408 RecentEntry::Thread(thread) => {
409 let context_store = self.context_store.clone();
410 let view_thread = thread.clone();
411
412 ContextMenuItem::custom_entry(
413 move |_window, cx| {
414 render_thread_context_entry(&view_thread, context_store.clone(), cx)
415 .into_any()
416 },
417 move |_window, cx| {
418 context_picker.update(cx, |this, cx| {
419 this.add_recent_thread(thread.clone(), cx)
420 .detach_and_log_err(cx);
421 })
422 },
423 )
424 }
425 }
426 }
427
428 fn add_recent_file(
429 &self,
430 project_path: ProjectPath,
431 window: &mut Window,
432 cx: &mut Context<Self>,
433 ) {
434 let Some(context_store) = self.context_store.upgrade() else {
435 return;
436 };
437
438 let task = context_store.update(cx, |context_store, cx| {
439 context_store.add_file_from_path(project_path.clone(), true, cx)
440 });
441
442 cx.spawn_in(window, async move |_, cx| task.await.notify_async_err(cx))
443 .detach();
444
445 cx.notify();
446 }
447
448 fn add_recent_thread(
449 &self,
450 thread: ThreadContextEntry,
451 cx: &mut Context<Self>,
452 ) -> Task<Result<()>> {
453 let Some(context_store) = self.context_store.upgrade() else {
454 return Task::ready(Err(anyhow!("context store not available")));
455 };
456
457 let Some(thread_store) = self
458 .thread_store
459 .as_ref()
460 .and_then(|thread_store| thread_store.upgrade())
461 else {
462 return Task::ready(Err(anyhow!("thread store not available")));
463 };
464
465 let open_thread_task = thread_store.update(cx, |this, cx| this.open_thread(&thread.id, cx));
466 cx.spawn(async move |this, cx| {
467 let thread = open_thread_task.await?;
468 context_store.update(cx, |context_store, cx| {
469 context_store.add_thread(thread, true, cx);
470 })?;
471
472 this.update(cx, |_this, cx| cx.notify())
473 })
474 }
475
476 fn recent_entries(&self, cx: &mut App) -> Vec<RecentEntry> {
477 let Some(workspace) = self.workspace.upgrade() else {
478 return vec![];
479 };
480
481 let Some(context_store) = self.context_store.upgrade() else {
482 return vec![];
483 };
484
485 recent_context_picker_entries(context_store, self.thread_store.clone(), workspace, cx)
486 }
487
488 fn notify_current_picker(&mut self, cx: &mut Context<Self>) {
489 match &self.mode {
490 ContextPickerState::Default(entity) => entity.update(cx, |_, cx| cx.notify()),
491 ContextPickerState::File(entity) => entity.update(cx, |_, cx| cx.notify()),
492 ContextPickerState::Symbol(entity) => entity.update(cx, |_, cx| cx.notify()),
493 ContextPickerState::Fetch(entity) => entity.update(cx, |_, cx| cx.notify()),
494 ContextPickerState::Thread(entity) => entity.update(cx, |_, cx| cx.notify()),
495 ContextPickerState::Rules(entity) => entity.update(cx, |_, cx| cx.notify()),
496 }
497 }
498}
499
500impl EventEmitter<DismissEvent> for ContextPicker {}
501
502impl Focusable for ContextPicker {
503 fn focus_handle(&self, cx: &App) -> FocusHandle {
504 match &self.mode {
505 ContextPickerState::Default(menu) => menu.focus_handle(cx),
506 ContextPickerState::File(file_picker) => file_picker.focus_handle(cx),
507 ContextPickerState::Symbol(symbol_picker) => symbol_picker.focus_handle(cx),
508 ContextPickerState::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
509 ContextPickerState::Thread(thread_picker) => thread_picker.focus_handle(cx),
510 ContextPickerState::Rules(user_rules_picker) => user_rules_picker.focus_handle(cx),
511 }
512 }
513}
514
515impl Render for ContextPicker {
516 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
517 v_flex()
518 .w(px(400.))
519 .min_w(px(400.))
520 .map(|parent| match &self.mode {
521 ContextPickerState::Default(menu) => parent.child(menu.clone()),
522 ContextPickerState::File(file_picker) => parent.child(file_picker.clone()),
523 ContextPickerState::Symbol(symbol_picker) => parent.child(symbol_picker.clone()),
524 ContextPickerState::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
525 ContextPickerState::Thread(thread_picker) => parent.child(thread_picker.clone()),
526 ContextPickerState::Rules(user_rules_picker) => {
527 parent.child(user_rules_picker.clone())
528 }
529 })
530 }
531}
532enum RecentEntry {
533 File {
534 project_path: ProjectPath,
535 path_prefix: Arc<str>,
536 },
537 Thread(ThreadContextEntry),
538}
539
540fn available_context_picker_entries(
541 prompt_store: &Option<Entity<PromptStore>>,
542 thread_store: &Option<WeakEntity<ThreadStore>>,
543 workspace: &Entity<Workspace>,
544 cx: &mut App,
545) -> Vec<ContextPickerEntry> {
546 let mut entries = vec![
547 ContextPickerEntry::Mode(ContextPickerMode::File),
548 ContextPickerEntry::Mode(ContextPickerMode::Symbol),
549 ];
550
551 let has_selection = workspace
552 .read(cx)
553 .active_item(cx)
554 .and_then(|item| item.downcast::<Editor>())
555 .map_or(false, |editor| {
556 editor.update(cx, |editor, cx| editor.has_non_empty_selection(cx))
557 });
558 if has_selection {
559 entries.push(ContextPickerEntry::Action(
560 ContextPickerAction::AddSelections,
561 ));
562 }
563
564 if thread_store.is_some() {
565 entries.push(ContextPickerEntry::Mode(ContextPickerMode::Thread));
566 }
567
568 if prompt_store.is_some() {
569 entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules));
570 }
571
572 entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch));
573
574 entries
575}
576
577fn recent_context_picker_entries(
578 context_store: Entity<ContextStore>,
579 thread_store: Option<WeakEntity<ThreadStore>>,
580 workspace: Entity<Workspace>,
581 cx: &App,
582) -> Vec<RecentEntry> {
583 let mut recent = Vec::with_capacity(6);
584
585 let current_files = context_store.read(cx).file_paths(cx);
586 let workspace = workspace.read(cx);
587 let project = workspace.project().read(cx);
588
589 recent.extend(
590 workspace
591 .recent_navigation_history_iter(cx)
592 .filter(|(path, _)| !current_files.contains(path))
593 .take(4)
594 .filter_map(|(project_path, _)| {
595 project
596 .worktree_for_id(project_path.worktree_id, cx)
597 .map(|worktree| RecentEntry::File {
598 project_path,
599 path_prefix: worktree.read(cx).root_name().into(),
600 })
601 }),
602 );
603
604 let current_threads = context_store.read(cx).thread_ids();
605
606 let active_thread_id = workspace
607 .panel::<AssistantPanel>(cx)
608 .map(|panel| panel.read(cx).active_thread(cx).read(cx).id());
609
610 if let Some(thread_store) = thread_store.and_then(|thread_store| thread_store.upgrade()) {
611 recent.extend(
612 thread_store
613 .read(cx)
614 .reverse_chronological_threads()
615 .into_iter()
616 .filter(|thread| {
617 Some(&thread.id) != active_thread_id && !current_threads.contains(&thread.id)
618 })
619 .take(2)
620 .map(|thread| {
621 RecentEntry::Thread(ThreadContextEntry {
622 id: thread.id,
623 summary: thread.summary,
624 })
625 }),
626 );
627 }
628
629 recent
630}
631
632fn add_selections_as_context(
633 context_store: &Entity<ContextStore>,
634 workspace: &Entity<Workspace>,
635 cx: &mut App,
636) {
637 let selection_ranges = selection_ranges(workspace, cx);
638 context_store.update(cx, |context_store, cx| {
639 for (buffer, range) in selection_ranges {
640 context_store.add_selection(buffer, range, cx);
641 }
642 })
643}
644
645fn selection_ranges(
646 workspace: &Entity<Workspace>,
647 cx: &mut App,
648) -> Vec<(Entity<Buffer>, Range<text::Anchor>)> {
649 let Some(editor) = workspace
650 .read(cx)
651 .active_item(cx)
652 .and_then(|item| item.act_as::<Editor>(cx))
653 else {
654 return Vec::new();
655 };
656
657 editor.update(cx, |editor, cx| {
658 let selections = editor.selections.all_adjusted(cx);
659
660 let buffer = editor.buffer().clone().read(cx);
661 let snapshot = buffer.snapshot(cx);
662
663 selections
664 .into_iter()
665 .map(|s| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end))
666 .flat_map(|range| {
667 let (start_buffer, start) = buffer.text_anchor_for_position(range.start, cx)?;
668 let (end_buffer, end) = buffer.text_anchor_for_position(range.end, cx)?;
669 if start_buffer != end_buffer {
670 return None;
671 }
672 Some((start_buffer, start..end))
673 })
674 .collect::<Vec<_>>()
675 })
676}
677
678pub(crate) fn insert_fold_for_mention(
679 excerpt_id: ExcerptId,
680 crease_start: text::Anchor,
681 content_len: usize,
682 crease_label: SharedString,
683 crease_icon_path: SharedString,
684 editor_entity: Entity<Editor>,
685 cx: &mut App,
686) {
687 editor_entity.update(cx, |editor, cx| {
688 let snapshot = editor.buffer().read(cx).snapshot(cx);
689
690 let Some(start) = snapshot.anchor_in_excerpt(excerpt_id, crease_start) else {
691 return;
692 };
693
694 let start = start.bias_right(&snapshot);
695 let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
696
697 let crease = crease_for_mention(
698 crease_label,
699 crease_icon_path,
700 start..end,
701 editor_entity.downgrade(),
702 );
703
704 editor.display_map.update(cx, |display_map, cx| {
705 display_map.fold(vec![crease], cx);
706 });
707 });
708}
709
710pub fn crease_for_mention(
711 label: SharedString,
712 icon_path: SharedString,
713 range: Range<Anchor>,
714 editor_entity: WeakEntity<Editor>,
715) -> Crease<Anchor> {
716 let placeholder = FoldPlaceholder {
717 render: render_fold_icon_button(icon_path, label, editor_entity),
718 merge_adjacent: false,
719 ..Default::default()
720 };
721
722 let render_trailer = move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
723
724 let crease = Crease::inline(
725 range,
726 placeholder.clone(),
727 fold_toggle("mention"),
728 render_trailer,
729 );
730 crease
731}
732
733fn render_fold_icon_button(
734 icon_path: SharedString,
735 label: SharedString,
736 editor: WeakEntity<Editor>,
737) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
738 Arc::new({
739 move |fold_id, fold_range, cx| {
740 let is_in_text_selection = editor.upgrade().is_some_and(|editor| {
741 editor.update(cx, |editor, cx| {
742 let snapshot = editor
743 .buffer()
744 .update(cx, |multi_buffer, cx| multi_buffer.snapshot(cx));
745
746 let is_in_pending_selection = || {
747 editor
748 .selections
749 .pending
750 .as_ref()
751 .is_some_and(|pending_selection| {
752 pending_selection
753 .selection
754 .range()
755 .includes(&fold_range, &snapshot)
756 })
757 };
758
759 let mut is_in_complete_selection = || {
760 editor
761 .selections
762 .disjoint_in_range::<usize>(fold_range.clone(), cx)
763 .into_iter()
764 .any(|selection| {
765 // This is needed to cover a corner case, if we just check for an existing
766 // selection in the fold range, having a cursor at the start of the fold
767 // marks it as selected. Non-empty selections don't cause this.
768 let length = selection.end - selection.start;
769 length > 0
770 })
771 };
772
773 is_in_pending_selection() || is_in_complete_selection()
774 })
775 });
776
777 ButtonLike::new(fold_id)
778 .style(ButtonStyle::Filled)
779 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
780 .toggle_state(is_in_text_selection)
781 .child(
782 h_flex()
783 .gap_1()
784 .child(
785 Icon::from_path(icon_path.clone())
786 .size(IconSize::XSmall)
787 .color(Color::Muted),
788 )
789 .child(
790 Label::new(label.clone())
791 .size(LabelSize::Small)
792 .buffer_font(cx)
793 .single_line(),
794 ),
795 )
796 .into_any_element()
797 }
798 })
799}
800
801fn fold_toggle(
802 name: &'static str,
803) -> impl Fn(
804 MultiBufferRow,
805 bool,
806 Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
807 &mut Window,
808 &mut App,
809) -> AnyElement {
810 move |row, is_folded, fold, _window, _cx| {
811 Disclosure::new((name, row.0 as u64), !is_folded)
812 .toggle_state(is_folded)
813 .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
814 .into_any_element()
815 }
816}
817
818pub enum MentionLink {
819 File(ProjectPath, Entry),
820 Symbol(ProjectPath, String),
821 Selection(ProjectPath, Range<usize>),
822 Fetch(String),
823 Thread(ThreadId),
824 Rules(UserPromptId),
825}
826
827impl MentionLink {
828 const FILE: &str = "@file";
829 const SYMBOL: &str = "@symbol";
830 const SELECTION: &str = "@selection";
831 const THREAD: &str = "@thread";
832 const FETCH: &str = "@fetch";
833 const RULES: &str = "@rules";
834
835 const SEPARATOR: &str = ":";
836
837 pub fn is_valid(url: &str) -> bool {
838 url.starts_with(Self::FILE)
839 || url.starts_with(Self::SYMBOL)
840 || url.starts_with(Self::FETCH)
841 || url.starts_with(Self::SELECTION)
842 || url.starts_with(Self::THREAD)
843 || url.starts_with(Self::RULES)
844 }
845
846 pub fn for_file(file_name: &str, full_path: &str) -> String {
847 format!("[@{}]({}:{})", file_name, Self::FILE, full_path)
848 }
849
850 pub fn for_symbol(symbol_name: &str, full_path: &str) -> String {
851 format!(
852 "[@{}]({}:{}:{})",
853 symbol_name,
854 Self::SYMBOL,
855 full_path,
856 symbol_name
857 )
858 }
859
860 pub fn for_selection(file_name: &str, full_path: &str, line_range: Range<usize>) -> String {
861 format!(
862 "[@{} ({}-{})]({}:{}:{}-{})",
863 file_name,
864 line_range.start,
865 line_range.end,
866 Self::SELECTION,
867 full_path,
868 line_range.start,
869 line_range.end
870 )
871 }
872
873 pub fn for_thread(thread: &ThreadContextEntry) -> String {
874 format!("[@{}]({}:{})", thread.summary, Self::THREAD, thread.id)
875 }
876
877 pub fn for_fetch(url: &str) -> String {
878 format!("[@{}]({}:{})", url, Self::FETCH, url)
879 }
880
881 pub fn for_rules(rules: &RulesContextEntry) -> String {
882 format!("[@{}]({}:{})", rules.title, Self::RULES, rules.prompt_id.0)
883 }
884
885 pub fn try_parse(link: &str, workspace: &Entity<Workspace>, cx: &App) -> Option<Self> {
886 fn extract_project_path_from_link(
887 path: &str,
888 workspace: &Entity<Workspace>,
889 cx: &App,
890 ) -> Option<ProjectPath> {
891 let path = PathBuf::from(path);
892 let worktree_name = path.iter().next()?;
893 let path: PathBuf = path.iter().skip(1).collect();
894 let worktree_id = workspace
895 .read(cx)
896 .visible_worktrees(cx)
897 .find(|worktree| worktree.read(cx).root_name() == worktree_name)
898 .map(|worktree| worktree.read(cx).id())?;
899 Some(ProjectPath {
900 worktree_id,
901 path: path.into(),
902 })
903 }
904
905 let (prefix, argument) = link.split_once(Self::SEPARATOR)?;
906 match prefix {
907 Self::FILE => {
908 let project_path = extract_project_path_from_link(argument, workspace, cx)?;
909 let entry = workspace
910 .read(cx)
911 .project()
912 .read(cx)
913 .entry_for_path(&project_path, cx)?;
914 Some(MentionLink::File(project_path, entry))
915 }
916 Self::SYMBOL => {
917 let (path, symbol) = argument.split_once(Self::SEPARATOR)?;
918 let project_path = extract_project_path_from_link(path, workspace, cx)?;
919 Some(MentionLink::Symbol(project_path, symbol.to_string()))
920 }
921 Self::SELECTION => {
922 let (path, line_args) = argument.split_once(Self::SEPARATOR)?;
923 let project_path = extract_project_path_from_link(path, workspace, cx)?;
924
925 let line_range = {
926 let (start, end) = line_args
927 .trim_start_matches('(')
928 .trim_end_matches(')')
929 .split_once('-')?;
930 start.parse::<usize>().ok()?..end.parse::<usize>().ok()?
931 };
932
933 Some(MentionLink::Selection(project_path, line_range))
934 }
935 Self::THREAD => {
936 let thread_id = ThreadId::from(argument);
937 Some(MentionLink::Thread(thread_id))
938 }
939 Self::FETCH => Some(MentionLink::Fetch(argument.to_string())),
940 Self::RULES => {
941 let prompt_id = UserPromptId(Uuid::try_parse(argument).ok()?);
942 Some(MentionLink::Rules(prompt_id))
943 }
944 _ => None,
945 }
946 }
947}