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 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 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 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 fn keyword(&self) -> &'static str {
88 match self {
89 Self::AddSelections => "selection",
90 }
91 }
92
93 pub fn label(&self) -> &'static str {
94 match self {
95 Self::AddSelections => "Selection",
96 }
97 }
98
99 pub 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 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 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 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 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| editor.update(cx, |editor, cx| editor.has_non_empty_selection(cx)));
610 if has_selection {
611 entries.push(ContextPickerEntry::Action(
612 ContextPickerAction::AddSelections,
613 ));
614 }
615
616 if thread_store.is_some() {
617 entries.push(ContextPickerEntry::Mode(ContextPickerMode::Thread));
618 }
619
620 if prompt_store.is_some() {
621 entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules));
622 }
623
624 entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch));
625
626 entries
627}
628
629fn recent_context_picker_entries_with_store(
630 context_store: Entity<ContextStore>,
631 thread_store: Option<WeakEntity<HistoryStore>>,
632 workspace: Entity<Workspace>,
633 exclude_path: Option<ProjectPath>,
634 cx: &App,
635) -> Vec<RecentEntry> {
636 let project = workspace.read(cx).project();
637
638 let mut exclude_paths = context_store.read(cx).file_paths(cx);
639 exclude_paths.extend(exclude_path);
640
641 let exclude_paths = exclude_paths
642 .into_iter()
643 .filter_map(|project_path| project.read(cx).absolute_path(&project_path, cx))
644 .collect();
645
646 let exclude_threads = context_store.read(cx).thread_ids();
647
648 recent_context_picker_entries(thread_store, workspace, &exclude_paths, exclude_threads, cx)
649}
650
651pub(crate) fn recent_context_picker_entries(
652 thread_store: Option<WeakEntity<HistoryStore>>,
653 workspace: Entity<Workspace>,
654 exclude_paths: &HashSet<PathBuf>,
655 exclude_threads: &HashSet<acp::SessionId>,
656 cx: &App,
657) -> Vec<RecentEntry> {
658 let mut recent = Vec::with_capacity(6);
659 let workspace = workspace.read(cx);
660 let project = workspace.project().read(cx);
661
662 recent.extend(
663 workspace
664 .recent_navigation_history_iter(cx)
665 .filter(|(_, abs_path)| {
666 abs_path
667 .as_ref()
668 .is_none_or(|path| !exclude_paths.contains(path.as_path()))
669 })
670 .take(4)
671 .filter_map(|(project_path, _)| {
672 project
673 .worktree_for_id(project_path.worktree_id, cx)
674 .map(|worktree| RecentEntry::File {
675 project_path,
676 path_prefix: worktree.read(cx).root_name().into(),
677 })
678 }),
679 );
680
681 if let Some(thread_store) = thread_store.and_then(|store| store.upgrade()) {
682 const RECENT_THREADS_COUNT: usize = 2;
683 recent.extend(
684 thread_store
685 .read(cx)
686 .recently_opened_entries(cx)
687 .iter()
688 .filter(|e| match e.id() {
689 HistoryEntryId::AcpThread(session_id) => !exclude_threads.contains(&session_id),
690 HistoryEntryId::TextThread(path) => {
691 !exclude_paths.contains(&path.to_path_buf())
692 }
693 })
694 .take(RECENT_THREADS_COUNT)
695 .map(|thread| RecentEntry::Thread(thread.clone())),
696 );
697 }
698
699 recent
700}
701
702fn add_selections_as_context(
703 context_store: &Entity<ContextStore>,
704 workspace: &Entity<Workspace>,
705 cx: &mut App,
706) {
707 let selection_ranges = selection_ranges(workspace, cx);
708 context_store.update(cx, |context_store, cx| {
709 for (buffer, range) in selection_ranges {
710 context_store.add_selection(buffer, range, cx);
711 }
712 })
713}
714
715pub(crate) fn selection_ranges(
716 workspace: &Entity<Workspace>,
717 cx: &mut App,
718) -> Vec<(Entity<Buffer>, Range<text::Anchor>)> {
719 let Some(editor) = workspace
720 .read(cx)
721 .active_item(cx)
722 .and_then(|item| item.act_as::<Editor>(cx))
723 else {
724 return Vec::new();
725 };
726
727 editor.update(cx, |editor, cx| {
728 let selections = editor.selections.all_adjusted(cx);
729
730 let buffer = editor.buffer().clone().read(cx);
731 let snapshot = buffer.snapshot(cx);
732
733 selections
734 .into_iter()
735 .map(|s| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end))
736 .flat_map(|range| {
737 let (start_buffer, start) = buffer.text_anchor_for_position(range.start, cx)?;
738 let (end_buffer, end) = buffer.text_anchor_for_position(range.end, cx)?;
739 if start_buffer != end_buffer {
740 return None;
741 }
742 Some((start_buffer, start..end))
743 })
744 .collect::<Vec<_>>()
745 })
746}
747
748pub(crate) fn insert_crease_for_mention(
749 excerpt_id: ExcerptId,
750 crease_start: text::Anchor,
751 content_len: usize,
752 crease_label: SharedString,
753 crease_icon_path: SharedString,
754 editor_entity: Entity<Editor>,
755 window: &mut Window,
756 cx: &mut App,
757) -> Option<CreaseId> {
758 editor_entity.update(cx, |editor, cx| {
759 let snapshot = editor.buffer().read(cx).snapshot(cx);
760
761 let start = snapshot.anchor_in_excerpt(excerpt_id, crease_start)?;
762
763 let start = start.bias_right(&snapshot);
764 let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
765
766 let crease = crease_for_mention(
767 crease_label,
768 crease_icon_path,
769 start..end,
770 editor_entity.downgrade(),
771 );
772
773 let ids = editor.insert_creases(vec![crease.clone()], cx);
774 editor.fold_creases(vec![crease], false, window, cx);
775
776 Some(ids[0])
777 })
778}
779
780pub fn crease_for_mention(
781 label: SharedString,
782 icon_path: SharedString,
783 range: Range<Anchor>,
784 editor_entity: WeakEntity<Editor>,
785) -> Crease<Anchor> {
786 let placeholder = FoldPlaceholder {
787 render: render_fold_icon_button(icon_path.clone(), label.clone(), editor_entity),
788 merge_adjacent: false,
789 ..Default::default()
790 };
791
792 let render_trailer = move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
793
794 Crease::inline(range, placeholder, fold_toggle("mention"), render_trailer)
795 .with_metadata(CreaseMetadata { icon_path, label })
796}
797
798fn render_fold_icon_button(
799 icon_path: SharedString,
800 label: SharedString,
801 editor: WeakEntity<Editor>,
802) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
803 Arc::new({
804 move |fold_id, fold_range, cx| {
805 let is_in_text_selection = editor
806 .update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx))
807 .unwrap_or_default();
808
809 ButtonLike::new(fold_id)
810 .style(ButtonStyle::Filled)
811 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
812 .toggle_state(is_in_text_selection)
813 .child(
814 h_flex()
815 .gap_1()
816 .child(
817 Icon::from_path(icon_path.clone())
818 .size(IconSize::XSmall)
819 .color(Color::Muted),
820 )
821 .child(
822 Label::new(label.clone())
823 .size(LabelSize::Small)
824 .buffer_font(cx)
825 .single_line(),
826 ),
827 )
828 .into_any_element()
829 }
830 })
831}
832
833fn fold_toggle(
834 name: &'static str,
835) -> impl Fn(
836 MultiBufferRow,
837 bool,
838 Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
839 &mut Window,
840 &mut App,
841) -> AnyElement {
842 move |row, is_folded, fold, _window, _cx| {
843 Disclosure::new((name, row.0 as u64), !is_folded)
844 .toggle_state(is_folded)
845 .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
846 .into_any_element()
847 }
848}
849
850pub struct MentionLink;
851
852impl MentionLink {
853 const FILE: &str = "@file";
854 const SYMBOL: &str = "@symbol";
855 const SELECTION: &str = "@selection";
856 const THREAD: &str = "@thread";
857 const FETCH: &str = "@fetch";
858 const RULE: &str = "@rule";
859
860 const TEXT_THREAD_URL_PREFIX: &str = "text-thread://";
861
862 pub fn for_file(file_name: &str, full_path: &str) -> String {
863 format!("[@{}]({}:{})", file_name, Self::FILE, full_path)
864 }
865
866 pub fn for_symbol(symbol_name: &str, full_path: &str) -> String {
867 format!(
868 "[@{}]({}:{}:{})",
869 symbol_name,
870 Self::SYMBOL,
871 full_path,
872 symbol_name
873 )
874 }
875
876 pub fn for_selection(file_name: &str, full_path: &str, line_range: Range<usize>) -> String {
877 format!(
878 "[@{} ({}-{})]({}:{}:{}-{})",
879 file_name,
880 line_range.start + 1,
881 line_range.end + 1,
882 Self::SELECTION,
883 full_path,
884 line_range.start,
885 line_range.end
886 )
887 }
888
889 pub fn for_thread(thread: &HistoryEntry) -> String {
890 match thread {
891 HistoryEntry::AcpThread(thread) => {
892 format!("[@{}]({}:{})", thread.title, Self::THREAD, thread.id)
893 }
894 HistoryEntry::TextThread(thread) => {
895 let filename = thread
896 .path
897 .file_name()
898 .unwrap_or_default()
899 .to_string_lossy();
900 let escaped_filename = urlencoding::encode(&filename);
901 format!(
902 "[@{}]({}:{}{})",
903 thread.title,
904 Self::THREAD,
905 Self::TEXT_THREAD_URL_PREFIX,
906 escaped_filename
907 )
908 }
909 }
910 }
911
912 pub fn for_fetch(url: &str) -> String {
913 format!("[@{}]({}:{})", url, Self::FETCH, url)
914 }
915
916 pub fn for_rule(rule: &RulesContextEntry) -> String {
917 format!("[@{}]({}:{})", rule.title, Self::RULE, rule.prompt_id.0)
918 }
919}