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