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