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