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, CreaseId, CreaseMetadata, 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 "rule" => 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 => "rule",
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(true)
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(
486 context_store,
487 self.thread_store.clone(),
488 workspace,
489 None,
490 cx,
491 )
492 }
493
494 fn notify_current_picker(&mut self, cx: &mut Context<Self>) {
495 match &self.mode {
496 ContextPickerState::Default(entity) => entity.update(cx, |_, cx| cx.notify()),
497 ContextPickerState::File(entity) => entity.update(cx, |_, cx| cx.notify()),
498 ContextPickerState::Symbol(entity) => entity.update(cx, |_, cx| cx.notify()),
499 ContextPickerState::Fetch(entity) => entity.update(cx, |_, cx| cx.notify()),
500 ContextPickerState::Thread(entity) => entity.update(cx, |_, cx| cx.notify()),
501 ContextPickerState::Rules(entity) => entity.update(cx, |_, cx| cx.notify()),
502 }
503 }
504}
505
506impl EventEmitter<DismissEvent> for ContextPicker {}
507
508impl Focusable for ContextPicker {
509 fn focus_handle(&self, cx: &App) -> FocusHandle {
510 match &self.mode {
511 ContextPickerState::Default(menu) => menu.focus_handle(cx),
512 ContextPickerState::File(file_picker) => file_picker.focus_handle(cx),
513 ContextPickerState::Symbol(symbol_picker) => symbol_picker.focus_handle(cx),
514 ContextPickerState::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
515 ContextPickerState::Thread(thread_picker) => thread_picker.focus_handle(cx),
516 ContextPickerState::Rules(user_rules_picker) => user_rules_picker.focus_handle(cx),
517 }
518 }
519}
520
521impl Render for ContextPicker {
522 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
523 v_flex()
524 .w(px(400.))
525 .min_w(px(400.))
526 .map(|parent| match &self.mode {
527 ContextPickerState::Default(menu) => parent.child(menu.clone()),
528 ContextPickerState::File(file_picker) => parent.child(file_picker.clone()),
529 ContextPickerState::Symbol(symbol_picker) => parent.child(symbol_picker.clone()),
530 ContextPickerState::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
531 ContextPickerState::Thread(thread_picker) => parent.child(thread_picker.clone()),
532 ContextPickerState::Rules(user_rules_picker) => {
533 parent.child(user_rules_picker.clone())
534 }
535 })
536 }
537}
538enum RecentEntry {
539 File {
540 project_path: ProjectPath,
541 path_prefix: Arc<str>,
542 },
543 Thread(ThreadContextEntry),
544}
545
546fn available_context_picker_entries(
547 prompt_store: &Option<Entity<PromptStore>>,
548 thread_store: &Option<WeakEntity<ThreadStore>>,
549 workspace: &Entity<Workspace>,
550 cx: &mut App,
551) -> Vec<ContextPickerEntry> {
552 let mut entries = vec![
553 ContextPickerEntry::Mode(ContextPickerMode::File),
554 ContextPickerEntry::Mode(ContextPickerMode::Symbol),
555 ];
556
557 let has_selection = workspace
558 .read(cx)
559 .active_item(cx)
560 .and_then(|item| item.downcast::<Editor>())
561 .map_or(false, |editor| {
562 editor.update(cx, |editor, cx| editor.has_non_empty_selection(cx))
563 });
564 if has_selection {
565 entries.push(ContextPickerEntry::Action(
566 ContextPickerAction::AddSelections,
567 ));
568 }
569
570 if thread_store.is_some() {
571 entries.push(ContextPickerEntry::Mode(ContextPickerMode::Thread));
572 }
573
574 if prompt_store.is_some() {
575 entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules));
576 }
577
578 entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch));
579
580 entries
581}
582
583fn recent_context_picker_entries(
584 context_store: Entity<ContextStore>,
585 thread_store: Option<WeakEntity<ThreadStore>>,
586 workspace: Entity<Workspace>,
587 exclude_path: Option<ProjectPath>,
588 cx: &App,
589) -> Vec<RecentEntry> {
590 let mut recent = Vec::with_capacity(6);
591 let mut current_files = context_store.read(cx).file_paths(cx);
592 current_files.extend(exclude_path);
593 let workspace = workspace.read(cx);
594 let project = workspace.project().read(cx);
595
596 recent.extend(
597 workspace
598 .recent_navigation_history_iter(cx)
599 .filter(|(path, _)| !current_files.contains(path))
600 .take(4)
601 .filter_map(|(project_path, _)| {
602 project
603 .worktree_for_id(project_path.worktree_id, cx)
604 .map(|worktree| RecentEntry::File {
605 project_path,
606 path_prefix: worktree.read(cx).root_name().into(),
607 })
608 }),
609 );
610
611 let current_threads = context_store.read(cx).thread_ids();
612
613 let active_thread_id = workspace
614 .panel::<AssistantPanel>(cx)
615 .map(|panel| panel.read(cx).active_thread(cx).read(cx).id());
616
617 if let Some(thread_store) = thread_store.and_then(|thread_store| thread_store.upgrade()) {
618 recent.extend(
619 thread_store
620 .read(cx)
621 .reverse_chronological_threads()
622 .into_iter()
623 .filter(|thread| {
624 Some(&thread.id) != active_thread_id && !current_threads.contains(&thread.id)
625 })
626 .take(2)
627 .map(|thread| {
628 RecentEntry::Thread(ThreadContextEntry {
629 id: thread.id,
630 summary: thread.summary,
631 })
632 }),
633 );
634 }
635
636 recent
637}
638
639fn add_selections_as_context(
640 context_store: &Entity<ContextStore>,
641 workspace: &Entity<Workspace>,
642 cx: &mut App,
643) {
644 let selection_ranges = selection_ranges(workspace, cx);
645 context_store.update(cx, |context_store, cx| {
646 for (buffer, range) in selection_ranges {
647 context_store.add_selection(buffer, range, cx);
648 }
649 })
650}
651
652fn selection_ranges(
653 workspace: &Entity<Workspace>,
654 cx: &mut App,
655) -> Vec<(Entity<Buffer>, Range<text::Anchor>)> {
656 let Some(editor) = workspace
657 .read(cx)
658 .active_item(cx)
659 .and_then(|item| item.act_as::<Editor>(cx))
660 else {
661 return Vec::new();
662 };
663
664 editor.update(cx, |editor, cx| {
665 let selections = editor.selections.all_adjusted(cx);
666
667 let buffer = editor.buffer().clone().read(cx);
668 let snapshot = buffer.snapshot(cx);
669
670 selections
671 .into_iter()
672 .map(|s| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end))
673 .flat_map(|range| {
674 let (start_buffer, start) = buffer.text_anchor_for_position(range.start, cx)?;
675 let (end_buffer, end) = buffer.text_anchor_for_position(range.end, cx)?;
676 if start_buffer != end_buffer {
677 return None;
678 }
679 Some((start_buffer, start..end))
680 })
681 .collect::<Vec<_>>()
682 })
683}
684
685pub(crate) fn insert_crease_for_mention(
686 excerpt_id: ExcerptId,
687 crease_start: text::Anchor,
688 content_len: usize,
689 crease_label: SharedString,
690 crease_icon_path: SharedString,
691 editor_entity: Entity<Editor>,
692 window: &mut Window,
693 cx: &mut App,
694) -> Option<CreaseId> {
695 editor_entity.update(cx, |editor, cx| {
696 let snapshot = editor.buffer().read(cx).snapshot(cx);
697
698 let start = snapshot.anchor_in_excerpt(excerpt_id, crease_start)?;
699
700 let start = start.bias_right(&snapshot);
701 let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
702
703 let crease = crease_for_mention(
704 crease_label,
705 crease_icon_path,
706 start..end,
707 editor_entity.downgrade(),
708 );
709
710 let ids = editor.insert_creases(vec![crease.clone()], cx);
711 editor.fold_creases(vec![crease], false, window, cx);
712 Some(ids[0])
713 })
714}
715
716pub fn crease_for_mention(
717 label: SharedString,
718 icon_path: SharedString,
719 range: Range<Anchor>,
720 editor_entity: WeakEntity<Editor>,
721) -> Crease<Anchor> {
722 let placeholder = FoldPlaceholder {
723 render: render_fold_icon_button(icon_path.clone(), label.clone(), editor_entity),
724 merge_adjacent: false,
725 ..Default::default()
726 };
727
728 let render_trailer = move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
729
730 Crease::inline(
731 range,
732 placeholder.clone(),
733 fold_toggle("mention"),
734 render_trailer,
735 )
736 .with_metadata(CreaseMetadata { icon_path, label })
737}
738
739fn render_fold_icon_button(
740 icon_path: SharedString,
741 label: SharedString,
742 editor: WeakEntity<Editor>,
743) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
744 Arc::new({
745 move |fold_id, fold_range, cx| {
746 let is_in_text_selection = editor.upgrade().is_some_and(|editor| {
747 editor.update(cx, |editor, cx| {
748 let snapshot = editor
749 .buffer()
750 .update(cx, |multi_buffer, cx| multi_buffer.snapshot(cx));
751
752 let is_in_pending_selection = || {
753 editor
754 .selections
755 .pending
756 .as_ref()
757 .is_some_and(|pending_selection| {
758 pending_selection
759 .selection
760 .range()
761 .includes(&fold_range, &snapshot)
762 })
763 };
764
765 let mut is_in_complete_selection = || {
766 editor
767 .selections
768 .disjoint_in_range::<usize>(fold_range.clone(), cx)
769 .into_iter()
770 .any(|selection| {
771 // This is needed to cover a corner case, if we just check for an existing
772 // selection in the fold range, having a cursor at the start of the fold
773 // marks it as selected. Non-empty selections don't cause this.
774 let length = selection.end - selection.start;
775 length > 0
776 })
777 };
778
779 is_in_pending_selection() || is_in_complete_selection()
780 })
781 });
782
783 ButtonLike::new(fold_id)
784 .style(ButtonStyle::Filled)
785 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
786 .toggle_state(is_in_text_selection)
787 .child(
788 h_flex()
789 .gap_1()
790 .child(
791 Icon::from_path(icon_path.clone())
792 .size(IconSize::XSmall)
793 .color(Color::Muted),
794 )
795 .child(
796 Label::new(label.clone())
797 .size(LabelSize::Small)
798 .buffer_font(cx)
799 .single_line(),
800 ),
801 )
802 .into_any_element()
803 }
804 })
805}
806
807fn fold_toggle(
808 name: &'static str,
809) -> impl Fn(
810 MultiBufferRow,
811 bool,
812 Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
813 &mut Window,
814 &mut App,
815) -> AnyElement {
816 move |row, is_folded, fold, _window, _cx| {
817 Disclosure::new((name, row.0 as u64), !is_folded)
818 .toggle_state(is_folded)
819 .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
820 .into_any_element()
821 }
822}
823
824pub enum MentionLink {
825 File(ProjectPath, Entry),
826 Symbol(ProjectPath, String),
827 Selection(ProjectPath, Range<usize>),
828 Fetch(String),
829 Thread(ThreadId),
830 Rule(UserPromptId),
831}
832
833impl MentionLink {
834 const FILE: &str = "@file";
835 const SYMBOL: &str = "@symbol";
836 const SELECTION: &str = "@selection";
837 const THREAD: &str = "@thread";
838 const FETCH: &str = "@fetch";
839 const RULE: &str = "@rule";
840
841 const SEPARATOR: &str = ":";
842
843 pub fn is_valid(url: &str) -> bool {
844 url.starts_with(Self::FILE)
845 || url.starts_with(Self::SYMBOL)
846 || url.starts_with(Self::FETCH)
847 || url.starts_with(Self::SELECTION)
848 || url.starts_with(Self::THREAD)
849 || url.starts_with(Self::RULE)
850 }
851
852 pub fn for_file(file_name: &str, full_path: &str) -> String {
853 format!("[@{}]({}:{})", file_name, Self::FILE, full_path)
854 }
855
856 pub fn for_symbol(symbol_name: &str, full_path: &str) -> String {
857 format!(
858 "[@{}]({}:{}:{})",
859 symbol_name,
860 Self::SYMBOL,
861 full_path,
862 symbol_name
863 )
864 }
865
866 pub fn for_selection(file_name: &str, full_path: &str, line_range: Range<usize>) -> String {
867 format!(
868 "[@{} ({}-{})]({}:{}:{}-{})",
869 file_name,
870 line_range.start,
871 line_range.end,
872 Self::SELECTION,
873 full_path,
874 line_range.start,
875 line_range.end
876 )
877 }
878
879 pub fn for_thread(thread: &ThreadContextEntry) -> String {
880 format!("[@{}]({}:{})", thread.summary, Self::THREAD, thread.id)
881 }
882
883 pub fn for_fetch(url: &str) -> String {
884 format!("[@{}]({}:{})", url, Self::FETCH, url)
885 }
886
887 pub fn for_rule(rule: &RulesContextEntry) -> String {
888 format!("[@{}]({}:{})", rule.title, Self::RULE, rule.prompt_id.0)
889 }
890
891 pub fn try_parse(link: &str, workspace: &Entity<Workspace>, cx: &App) -> Option<Self> {
892 fn extract_project_path_from_link(
893 path: &str,
894 workspace: &Entity<Workspace>,
895 cx: &App,
896 ) -> Option<ProjectPath> {
897 let path = PathBuf::from(path);
898 let worktree_name = path.iter().next()?;
899 let path: PathBuf = path.iter().skip(1).collect();
900 let worktree_id = workspace
901 .read(cx)
902 .visible_worktrees(cx)
903 .find(|worktree| worktree.read(cx).root_name() == worktree_name)
904 .map(|worktree| worktree.read(cx).id())?;
905 Some(ProjectPath {
906 worktree_id,
907 path: path.into(),
908 })
909 }
910
911 let (prefix, argument) = link.split_once(Self::SEPARATOR)?;
912 match prefix {
913 Self::FILE => {
914 let project_path = extract_project_path_from_link(argument, workspace, cx)?;
915 let entry = workspace
916 .read(cx)
917 .project()
918 .read(cx)
919 .entry_for_path(&project_path, cx)?;
920 Some(MentionLink::File(project_path, entry))
921 }
922 Self::SYMBOL => {
923 let (path, symbol) = argument.split_once(Self::SEPARATOR)?;
924 let project_path = extract_project_path_from_link(path, workspace, cx)?;
925 Some(MentionLink::Symbol(project_path, symbol.to_string()))
926 }
927 Self::SELECTION => {
928 let (path, line_args) = argument.split_once(Self::SEPARATOR)?;
929 let project_path = extract_project_path_from_link(path, workspace, cx)?;
930
931 let line_range = {
932 let (start, end) = line_args
933 .trim_start_matches('(')
934 .trim_end_matches(')')
935 .split_once('-')?;
936 start.parse::<usize>().ok()?..end.parse::<usize>().ok()?
937 };
938
939 Some(MentionLink::Selection(project_path, line_range))
940 }
941 Self::THREAD => {
942 let thread_id = ThreadId::from(argument);
943 Some(MentionLink::Thread(thread_id))
944 }
945 Self::FETCH => Some(MentionLink::Fetch(argument.to_string())),
946 Self::RULE => {
947 let prompt_id = UserPromptId(Uuid::try_parse(argument).ok()?);
948 Some(MentionLink::Rule(prompt_id))
949 }
950 _ => None,
951 }
952 }
953}