completion_provider.rs

  1use std::ops::Range;
  2use std::sync::Arc;
  3use std::sync::atomic::AtomicBool;
  4
  5use acp_thread::MentionUri;
  6use agent2::{HistoryEntry, HistoryStore};
  7use anyhow::Result;
  8use editor::{CompletionProvider, Editor, ExcerptId};
  9use fuzzy::{StringMatch, StringMatchCandidate};
 10use gpui::{App, Entity, Task, WeakEntity};
 11use language::{Buffer, CodeLabel, HighlightId};
 12use lsp::CompletionContext;
 13use project::{
 14    Completion, CompletionIntent, CompletionResponse, Project, ProjectPath, Symbol, WorktreeId,
 15};
 16use prompt_store::PromptStore;
 17use rope::Point;
 18use text::{Anchor, ToPoint as _};
 19use ui::prelude::*;
 20use workspace::Workspace;
 21
 22use crate::AgentPanel;
 23use crate::acp::message_editor::MessageEditor;
 24use crate::context_picker::file_context_picker::{FileMatch, search_files};
 25use crate::context_picker::rules_context_picker::{RulesContextEntry, search_rules};
 26use crate::context_picker::symbol_context_picker::SymbolMatch;
 27use crate::context_picker::symbol_context_picker::search_symbols;
 28use crate::context_picker::{
 29    ContextPickerAction, ContextPickerEntry, ContextPickerMode, selection_ranges,
 30};
 31
 32pub(crate) enum Match {
 33    File(FileMatch),
 34    Symbol(SymbolMatch),
 35    Thread(HistoryEntry),
 36    RecentThread(HistoryEntry),
 37    Fetch(SharedString),
 38    Rules(RulesContextEntry),
 39    Entry(EntryMatch),
 40}
 41
 42pub struct EntryMatch {
 43    mat: Option<StringMatch>,
 44    entry: ContextPickerEntry,
 45}
 46
 47impl Match {
 48    pub fn score(&self) -> f64 {
 49        match self {
 50            Match::File(file) => file.mat.score,
 51            Match::Entry(mode) => mode.mat.as_ref().map(|mat| mat.score).unwrap_or(1.),
 52            Match::Thread(_) => 1.,
 53            Match::RecentThread(_) => 1.,
 54            Match::Symbol(_) => 1.,
 55            Match::Rules(_) => 1.,
 56            Match::Fetch(_) => 1.,
 57        }
 58    }
 59}
 60
 61pub struct ContextPickerCompletionProvider {
 62    message_editor: WeakEntity<MessageEditor>,
 63    workspace: WeakEntity<Workspace>,
 64    history_store: Entity<HistoryStore>,
 65    prompt_store: Option<Entity<PromptStore>>,
 66}
 67
 68impl ContextPickerCompletionProvider {
 69    pub fn new(
 70        message_editor: WeakEntity<MessageEditor>,
 71        workspace: WeakEntity<Workspace>,
 72        history_store: Entity<HistoryStore>,
 73        prompt_store: Option<Entity<PromptStore>>,
 74    ) -> Self {
 75        Self {
 76            message_editor,
 77            workspace,
 78            history_store,
 79            prompt_store,
 80        }
 81    }
 82
 83    fn completion_for_entry(
 84        entry: ContextPickerEntry,
 85        source_range: Range<Anchor>,
 86        message_editor: WeakEntity<MessageEditor>,
 87        workspace: &Entity<Workspace>,
 88        cx: &mut App,
 89    ) -> Option<Completion> {
 90        match entry {
 91            ContextPickerEntry::Mode(mode) => Some(Completion {
 92                replace_range: source_range.clone(),
 93                new_text: format!("@{} ", mode.keyword()),
 94                label: CodeLabel::plain(mode.label().to_string(), None),
 95                icon_path: Some(mode.icon().path().into()),
 96                documentation: None,
 97                source: project::CompletionSource::Custom,
 98                insert_text_mode: None,
 99                // This ensures that when a user accepts this completion, the
100                // completion menu will still be shown after "@category " is
101                // inserted
102                confirm: Some(Arc::new(|_, _, _| true)),
103            }),
104            ContextPickerEntry::Action(action) => {
105                let (new_text, on_action) = match action {
106                    ContextPickerAction::AddSelections => {
107                        const PLACEHOLDER: &str = "selection ";
108                        let selections = selection_ranges(workspace, cx)
109                            .into_iter()
110                            .enumerate()
111                            .map(|(ix, (buffer, range))| {
112                                (
113                                    buffer,
114                                    range,
115                                    (PLACEHOLDER.len() * ix)..(PLACEHOLDER.len() * (ix + 1) - 1),
116                                )
117                            })
118                            .collect::<Vec<_>>();
119
120                        let new_text: String = PLACEHOLDER.repeat(selections.len());
121
122                        let callback = Arc::new({
123                            let source_range = source_range.clone();
124                            move |_, window: &mut Window, cx: &mut App| {
125                                let selections = selections.clone();
126                                let message_editor = message_editor.clone();
127                                let source_range = source_range.clone();
128                                window.defer(cx, move |window, cx| {
129                                    message_editor
130                                        .update(cx, |message_editor, cx| {
131                                            message_editor.confirm_mention_for_selection(
132                                                source_range,
133                                                selections,
134                                                window,
135                                                cx,
136                                            )
137                                        })
138                                        .ok();
139                                });
140                                false
141                            }
142                        });
143
144                        (new_text, callback)
145                    }
146                };
147
148                Some(Completion {
149                    replace_range: source_range.clone(),
150                    new_text,
151                    label: CodeLabel::plain(action.label().to_string(), None),
152                    icon_path: Some(action.icon().path().into()),
153                    documentation: None,
154                    source: project::CompletionSource::Custom,
155                    insert_text_mode: None,
156                    // This ensures that when a user accepts this completion, the
157                    // completion menu will still be shown after "@category " is
158                    // inserted
159                    confirm: Some(on_action),
160                })
161            }
162        }
163    }
164
165    fn completion_for_thread(
166        thread_entry: HistoryEntry,
167        source_range: Range<Anchor>,
168        recent: bool,
169        editor: WeakEntity<MessageEditor>,
170        cx: &mut App,
171    ) -> Completion {
172        let uri = thread_entry.mention_uri();
173
174        let icon_for_completion = if recent {
175            IconName::HistoryRerun.path().into()
176        } else {
177            uri.icon_path(cx)
178        };
179
180        let new_text = format!("{} ", uri.as_link());
181
182        let new_text_len = new_text.len();
183        Completion {
184            replace_range: source_range.clone(),
185            new_text,
186            label: CodeLabel::plain(thread_entry.title().to_string(), None),
187            documentation: None,
188            insert_text_mode: None,
189            source: project::CompletionSource::Custom,
190            icon_path: Some(icon_for_completion.clone()),
191            confirm: Some(confirm_completion_callback(
192                thread_entry.title().clone(),
193                source_range.start,
194                new_text_len - 1,
195                editor,
196                uri,
197            )),
198        }
199    }
200
201    fn completion_for_rules(
202        rule: RulesContextEntry,
203        source_range: Range<Anchor>,
204        editor: WeakEntity<MessageEditor>,
205        cx: &mut App,
206    ) -> Completion {
207        let uri = MentionUri::Rule {
208            id: rule.prompt_id.into(),
209            name: rule.title.to_string(),
210        };
211        let new_text = format!("{} ", uri.as_link());
212        let new_text_len = new_text.len();
213        let icon_path = uri.icon_path(cx);
214        Completion {
215            replace_range: source_range.clone(),
216            new_text,
217            label: CodeLabel::plain(rule.title.to_string(), None),
218            documentation: None,
219            insert_text_mode: None,
220            source: project::CompletionSource::Custom,
221            icon_path: Some(icon_path.clone()),
222            confirm: Some(confirm_completion_callback(
223                rule.title.clone(),
224                source_range.start,
225                new_text_len - 1,
226                editor,
227                uri,
228            )),
229        }
230    }
231
232    pub(crate) fn completion_for_path(
233        project_path: ProjectPath,
234        path_prefix: &str,
235        is_recent: bool,
236        is_directory: bool,
237        source_range: Range<Anchor>,
238        message_editor: WeakEntity<MessageEditor>,
239        project: Entity<Project>,
240        cx: &mut App,
241    ) -> Option<Completion> {
242        let (file_name, directory) =
243            crate::context_picker::file_context_picker::extract_file_name_and_directory(
244                &project_path.path,
245                path_prefix,
246            );
247
248        let label =
249            build_code_label_for_full_path(&file_name, directory.as_ref().map(|s| s.as_ref()), cx);
250
251        let abs_path = project.read(cx).absolute_path(&project_path, cx)?;
252
253        let uri = if is_directory {
254            MentionUri::Directory { abs_path }
255        } else {
256            MentionUri::File { abs_path }
257        };
258
259        let crease_icon_path = uri.icon_path(cx);
260        let completion_icon_path = if is_recent {
261            IconName::HistoryRerun.path().into()
262        } else {
263            crease_icon_path.clone()
264        };
265
266        let new_text = format!("{} ", uri.as_link());
267        let new_text_len = new_text.len();
268        Some(Completion {
269            replace_range: source_range.clone(),
270            new_text,
271            label,
272            documentation: None,
273            source: project::CompletionSource::Custom,
274            icon_path: Some(completion_icon_path),
275            insert_text_mode: None,
276            confirm: Some(confirm_completion_callback(
277                file_name,
278                source_range.start,
279                new_text_len - 1,
280                message_editor,
281                uri,
282            )),
283        })
284    }
285
286    fn completion_for_symbol(
287        symbol: Symbol,
288        source_range: Range<Anchor>,
289        message_editor: WeakEntity<MessageEditor>,
290        workspace: Entity<Workspace>,
291        cx: &mut App,
292    ) -> Option<Completion> {
293        let project = workspace.read(cx).project().clone();
294
295        let label = CodeLabel::plain(symbol.name.clone(), None);
296
297        let abs_path = project.read(cx).absolute_path(&symbol.path, cx)?;
298        let uri = MentionUri::Symbol {
299            path: abs_path,
300            name: symbol.name.clone(),
301            line_range: symbol.range.start.0.row..symbol.range.end.0.row,
302        };
303        let new_text = format!("{} ", uri.as_link());
304        let new_text_len = new_text.len();
305        let icon_path = uri.icon_path(cx);
306        Some(Completion {
307            replace_range: source_range.clone(),
308            new_text,
309            label,
310            documentation: None,
311            source: project::CompletionSource::Custom,
312            icon_path: Some(icon_path.clone()),
313            insert_text_mode: None,
314            confirm: Some(confirm_completion_callback(
315                symbol.name.clone().into(),
316                source_range.start,
317                new_text_len - 1,
318                message_editor,
319                uri,
320            )),
321        })
322    }
323
324    fn completion_for_fetch(
325        source_range: Range<Anchor>,
326        url_to_fetch: SharedString,
327        message_editor: WeakEntity<MessageEditor>,
328        cx: &mut App,
329    ) -> Option<Completion> {
330        let new_text = format!("@fetch {} ", url_to_fetch.clone());
331        let url_to_fetch = url::Url::parse(url_to_fetch.as_ref())
332            .or_else(|_| url::Url::parse(&format!("https://{url_to_fetch}")))
333            .ok()?;
334        let mention_uri = MentionUri::Fetch {
335            url: url_to_fetch.clone(),
336        };
337        let icon_path = mention_uri.icon_path(cx);
338        Some(Completion {
339            replace_range: source_range.clone(),
340            new_text: new_text.clone(),
341            label: CodeLabel::plain(url_to_fetch.to_string(), None),
342            documentation: None,
343            source: project::CompletionSource::Custom,
344            icon_path: Some(icon_path.clone()),
345            insert_text_mode: None,
346            confirm: Some(confirm_completion_callback(
347                url_to_fetch.to_string().into(),
348                source_range.start,
349                new_text.len() - 1,
350                message_editor,
351                mention_uri,
352            )),
353        })
354    }
355
356    fn search(
357        &self,
358        mode: Option<ContextPickerMode>,
359        query: String,
360        cancellation_flag: Arc<AtomicBool>,
361        cx: &mut App,
362    ) -> Task<Vec<Match>> {
363        let Some(workspace) = self.workspace.upgrade() else {
364            return Task::ready(Vec::default());
365        };
366        match mode {
367            Some(ContextPickerMode::File) => {
368                let search_files_task =
369                    search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
370                cx.background_spawn(async move {
371                    search_files_task
372                        .await
373                        .into_iter()
374                        .map(Match::File)
375                        .collect()
376                })
377            }
378
379            Some(ContextPickerMode::Symbol) => {
380                let search_symbols_task =
381                    search_symbols(query.clone(), cancellation_flag.clone(), &workspace, cx);
382                cx.background_spawn(async move {
383                    search_symbols_task
384                        .await
385                        .into_iter()
386                        .map(Match::Symbol)
387                        .collect()
388                })
389            }
390
391            Some(ContextPickerMode::Thread) => {
392                let search_threads_task = search_threads(
393                    query.clone(),
394                    cancellation_flag.clone(),
395                    &self.history_store,
396                    cx,
397                );
398                cx.background_spawn(async move {
399                    search_threads_task
400                        .await
401                        .into_iter()
402                        .map(Match::Thread)
403                        .collect()
404                })
405            }
406
407            Some(ContextPickerMode::Fetch) => {
408                if !query.is_empty() {
409                    Task::ready(vec![Match::Fetch(query.into())])
410                } else {
411                    Task::ready(Vec::new())
412                }
413            }
414
415            Some(ContextPickerMode::Rules) => {
416                if let Some(prompt_store) = self.prompt_store.as_ref() {
417                    let search_rules_task =
418                        search_rules(query.clone(), cancellation_flag.clone(), prompt_store, cx);
419                    cx.background_spawn(async move {
420                        search_rules_task
421                            .await
422                            .into_iter()
423                            .map(Match::Rules)
424                            .collect::<Vec<_>>()
425                    })
426                } else {
427                    Task::ready(Vec::new())
428                }
429            }
430
431            None if query.is_empty() => {
432                let mut matches = self.recent_context_picker_entries(&workspace, cx);
433
434                matches.extend(
435                    self.available_context_picker_entries(&workspace, cx)
436                        .into_iter()
437                        .map(|mode| {
438                            Match::Entry(EntryMatch {
439                                entry: mode,
440                                mat: None,
441                            })
442                        }),
443                );
444
445                Task::ready(matches)
446            }
447            None => {
448                let executor = cx.background_executor().clone();
449
450                let search_files_task =
451                    search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
452
453                let entries = self.available_context_picker_entries(&workspace, cx);
454                let entry_candidates = entries
455                    .iter()
456                    .enumerate()
457                    .map(|(ix, entry)| StringMatchCandidate::new(ix, entry.keyword()))
458                    .collect::<Vec<_>>();
459
460                cx.background_spawn(async move {
461                    let mut matches = search_files_task
462                        .await
463                        .into_iter()
464                        .map(Match::File)
465                        .collect::<Vec<_>>();
466
467                    let entry_matches = fuzzy::match_strings(
468                        &entry_candidates,
469                        &query,
470                        false,
471                        true,
472                        100,
473                        &Arc::new(AtomicBool::default()),
474                        executor,
475                    )
476                    .await;
477
478                    matches.extend(entry_matches.into_iter().map(|mat| {
479                        Match::Entry(EntryMatch {
480                            entry: entries[mat.candidate_id],
481                            mat: Some(mat),
482                        })
483                    }));
484
485                    matches.sort_by(|a, b| {
486                        b.score()
487                            .partial_cmp(&a.score())
488                            .unwrap_or(std::cmp::Ordering::Equal)
489                    });
490
491                    matches
492                })
493            }
494        }
495    }
496
497    fn recent_context_picker_entries(
498        &self,
499        workspace: &Entity<Workspace>,
500        cx: &mut App,
501    ) -> Vec<Match> {
502        let mut recent = Vec::with_capacity(6);
503
504        let mut mentions = self
505            .message_editor
506            .read_with(cx, |message_editor, _cx| message_editor.mentions())
507            .unwrap_or_default();
508        let workspace = workspace.read(cx);
509        let project = workspace.project().read(cx);
510
511        if let Some(agent_panel) = workspace.panel::<AgentPanel>(cx)
512            && let Some(thread) = agent_panel.read(cx).active_agent_thread(cx)
513        {
514            let thread = thread.read(cx);
515            mentions.insert(MentionUri::Thread {
516                id: thread.session_id().clone(),
517                name: thread.title().into(),
518            });
519        }
520
521        recent.extend(
522            workspace
523                .recent_navigation_history_iter(cx)
524                .filter(|(_, abs_path)| {
525                    abs_path.as_ref().is_none_or(|path| {
526                        !mentions.contains(&MentionUri::File {
527                            abs_path: path.clone(),
528                        })
529                    })
530                })
531                .take(4)
532                .filter_map(|(project_path, _)| {
533                    project
534                        .worktree_for_id(project_path.worktree_id, cx)
535                        .map(|worktree| {
536                            let path_prefix = worktree.read(cx).root_name().into();
537                            Match::File(FileMatch {
538                                mat: fuzzy::PathMatch {
539                                    score: 1.,
540                                    positions: Vec::new(),
541                                    worktree_id: project_path.worktree_id.to_usize(),
542                                    path: project_path.path,
543                                    path_prefix,
544                                    is_dir: false,
545                                    distance_to_relative_ancestor: 0,
546                                },
547                                is_recent: true,
548                            })
549                        })
550                }),
551        );
552
553        const RECENT_COUNT: usize = 2;
554        let threads = self
555            .history_store
556            .read(cx)
557            .recently_opened_entries(cx)
558            .into_iter()
559            .filter(|thread| !mentions.contains(&thread.mention_uri()))
560            .take(RECENT_COUNT)
561            .collect::<Vec<_>>();
562
563        recent.extend(threads.into_iter().map(Match::RecentThread));
564
565        recent
566    }
567
568    fn available_context_picker_entries(
569        &self,
570        workspace: &Entity<Workspace>,
571        cx: &mut App,
572    ) -> Vec<ContextPickerEntry> {
573        let mut entries = vec![
574            ContextPickerEntry::Mode(ContextPickerMode::File),
575            ContextPickerEntry::Mode(ContextPickerMode::Symbol),
576            ContextPickerEntry::Mode(ContextPickerMode::Thread),
577        ];
578
579        let has_selection = workspace
580            .read(cx)
581            .active_item(cx)
582            .and_then(|item| item.downcast::<Editor>())
583            .is_some_and(|editor| {
584                editor.update(cx, |editor, cx| editor.has_non_empty_selection(cx))
585            });
586        if has_selection {
587            entries.push(ContextPickerEntry::Action(
588                ContextPickerAction::AddSelections,
589            ));
590        }
591
592        if self.prompt_store.is_some() {
593            entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules));
594        }
595
596        entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch));
597
598        entries
599    }
600}
601
602fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
603    let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
604    let mut label = CodeLabel::default();
605
606    label.push_str(file_name, None);
607    label.push_str(" ", None);
608
609    if let Some(directory) = directory {
610        label.push_str(directory, comment_id);
611    }
612
613    label.filter_range = 0..label.text().len();
614
615    label
616}
617
618impl CompletionProvider for ContextPickerCompletionProvider {
619    fn completions(
620        &self,
621        _excerpt_id: ExcerptId,
622        buffer: &Entity<Buffer>,
623        buffer_position: Anchor,
624        _trigger: CompletionContext,
625        _window: &mut Window,
626        cx: &mut Context<Editor>,
627    ) -> Task<Result<Vec<CompletionResponse>>> {
628        let state = buffer.update(cx, |buffer, _cx| {
629            let position = buffer_position.to_point(buffer);
630            let line_start = Point::new(position.row, 0);
631            let offset_to_line = buffer.point_to_offset(line_start);
632            let mut lines = buffer.text_for_range(line_start..position).lines();
633            let line = lines.next()?;
634            MentionCompletion::try_parse(line, offset_to_line)
635        });
636        let Some(state) = state else {
637            return Task::ready(Ok(Vec::new()));
638        };
639
640        let Some(workspace) = self.workspace.upgrade() else {
641            return Task::ready(Ok(Vec::new()));
642        };
643
644        let project = workspace.read(cx).project().clone();
645        let snapshot = buffer.read(cx).snapshot();
646        let source_range = snapshot.anchor_before(state.source_range.start)
647            ..snapshot.anchor_after(state.source_range.end);
648
649        let editor = self.message_editor.clone();
650
651        let MentionCompletion { mode, argument, .. } = state;
652        let query = argument.unwrap_or_else(|| "".to_string());
653
654        let search_task = self.search(mode, query, Arc::<AtomicBool>::default(), cx);
655
656        cx.spawn(async move |_, cx| {
657            let matches = search_task.await;
658
659            let completions = cx.update(|cx| {
660                matches
661                    .into_iter()
662                    .filter_map(|mat| match mat {
663                        Match::File(FileMatch { mat, is_recent }) => {
664                            let project_path = ProjectPath {
665                                worktree_id: WorktreeId::from_usize(mat.worktree_id),
666                                path: mat.path.clone(),
667                            };
668
669                            Self::completion_for_path(
670                                project_path,
671                                &mat.path_prefix,
672                                is_recent,
673                                mat.is_dir,
674                                source_range.clone(),
675                                editor.clone(),
676                                project.clone(),
677                                cx,
678                            )
679                        }
680
681                        Match::Symbol(SymbolMatch { symbol, .. }) => Self::completion_for_symbol(
682                            symbol,
683                            source_range.clone(),
684                            editor.clone(),
685                            workspace.clone(),
686                            cx,
687                        ),
688
689                        Match::Thread(thread) => Some(Self::completion_for_thread(
690                            thread,
691                            source_range.clone(),
692                            false,
693                            editor.clone(),
694                            cx,
695                        )),
696
697                        Match::RecentThread(thread) => Some(Self::completion_for_thread(
698                            thread,
699                            source_range.clone(),
700                            true,
701                            editor.clone(),
702                            cx,
703                        )),
704
705                        Match::Rules(user_rules) => Some(Self::completion_for_rules(
706                            user_rules,
707                            source_range.clone(),
708                            editor.clone(),
709                            cx,
710                        )),
711
712                        Match::Fetch(url) => Self::completion_for_fetch(
713                            source_range.clone(),
714                            url,
715                            editor.clone(),
716                            cx,
717                        ),
718
719                        Match::Entry(EntryMatch { entry, .. }) => Self::completion_for_entry(
720                            entry,
721                            source_range.clone(),
722                            editor.clone(),
723                            &workspace,
724                            cx,
725                        ),
726                    })
727                    .collect()
728            })?;
729
730            Ok(vec![CompletionResponse {
731                completions,
732                // Since this does its own filtering (see `filter_completions()` returns false),
733                // there is no benefit to computing whether this set of completions is incomplete.
734                is_incomplete: true,
735            }])
736        })
737    }
738
739    fn is_completion_trigger(
740        &self,
741        buffer: &Entity<language::Buffer>,
742        position: language::Anchor,
743        _text: &str,
744        _trigger_in_words: bool,
745        _menu_is_open: bool,
746        cx: &mut Context<Editor>,
747    ) -> bool {
748        let buffer = buffer.read(cx);
749        let position = position.to_point(buffer);
750        let line_start = Point::new(position.row, 0);
751        let offset_to_line = buffer.point_to_offset(line_start);
752        let mut lines = buffer.text_for_range(line_start..position).lines();
753        if let Some(line) = lines.next() {
754            MentionCompletion::try_parse(line, offset_to_line)
755                .map(|completion| {
756                    completion.source_range.start <= offset_to_line + position.column as usize
757                        && completion.source_range.end >= offset_to_line + position.column as usize
758                })
759                .unwrap_or(false)
760        } else {
761            false
762        }
763    }
764
765    fn sort_completions(&self) -> bool {
766        false
767    }
768
769    fn filter_completions(&self) -> bool {
770        false
771    }
772}
773
774pub(crate) fn search_threads(
775    query: String,
776    cancellation_flag: Arc<AtomicBool>,
777    history_store: &Entity<HistoryStore>,
778    cx: &mut App,
779) -> Task<Vec<HistoryEntry>> {
780    let threads = history_store.read(cx).entries(cx);
781    if query.is_empty() {
782        return Task::ready(threads);
783    }
784
785    let executor = cx.background_executor().clone();
786    cx.background_spawn(async move {
787        let candidates = threads
788            .iter()
789            .enumerate()
790            .map(|(id, thread)| StringMatchCandidate::new(id, thread.title()))
791            .collect::<Vec<_>>();
792        let matches = fuzzy::match_strings(
793            &candidates,
794            &query,
795            false,
796            true,
797            100,
798            &cancellation_flag,
799            executor,
800        )
801        .await;
802
803        matches
804            .into_iter()
805            .map(|mat| threads[mat.candidate_id].clone())
806            .collect()
807    })
808}
809
810fn confirm_completion_callback(
811    crease_text: SharedString,
812    start: Anchor,
813    content_len: usize,
814    message_editor: WeakEntity<MessageEditor>,
815    mention_uri: MentionUri,
816) -> Arc<dyn Fn(CompletionIntent, &mut Window, &mut App) -> bool + Send + Sync> {
817    Arc::new(move |_, window, cx| {
818        let message_editor = message_editor.clone();
819        let crease_text = crease_text.clone();
820        let mention_uri = mention_uri.clone();
821        window.defer(cx, move |window, cx| {
822            message_editor
823                .clone()
824                .update(cx, |message_editor, cx| {
825                    message_editor
826                        .confirm_completion(
827                            crease_text,
828                            start,
829                            content_len,
830                            mention_uri,
831                            window,
832                            cx,
833                        )
834                        .detach();
835                })
836                .ok();
837        });
838        false
839    })
840}
841
842#[derive(Debug, Default, PartialEq)]
843struct MentionCompletion {
844    source_range: Range<usize>,
845    mode: Option<ContextPickerMode>,
846    argument: Option<String>,
847}
848
849impl MentionCompletion {
850    fn try_parse(line: &str, offset_to_line: usize) -> Option<Self> {
851        let last_mention_start = line.rfind('@')?;
852        if last_mention_start >= line.len() {
853            return Some(Self::default());
854        }
855        if last_mention_start > 0
856            && line
857                .chars()
858                .nth(last_mention_start - 1)
859                .is_some_and(|c| !c.is_whitespace())
860        {
861            return None;
862        }
863
864        let rest_of_line = &line[last_mention_start + 1..];
865
866        let mut mode = None;
867        let mut argument = None;
868
869        let mut parts = rest_of_line.split_whitespace();
870        let mut end = last_mention_start + 1;
871        if let Some(mode_text) = parts.next() {
872            end += mode_text.len();
873
874            if let Some(parsed_mode) = ContextPickerMode::try_from(mode_text).ok() {
875                mode = Some(parsed_mode);
876            } else {
877                argument = Some(mode_text.to_string());
878            }
879            match rest_of_line[mode_text.len()..].find(|c: char| !c.is_whitespace()) {
880                Some(whitespace_count) => {
881                    if let Some(argument_text) = parts.next() {
882                        argument = Some(argument_text.to_string());
883                        end += whitespace_count + argument_text.len();
884                    }
885                }
886                None => {
887                    // Rest of line is entirely whitespace
888                    end += rest_of_line.len() - mode_text.len();
889                }
890            }
891        }
892
893        Some(Self {
894            source_range: last_mention_start + offset_to_line..end + offset_to_line,
895            mode,
896            argument,
897        })
898    }
899}
900
901#[cfg(test)]
902mod tests {
903    use super::*;
904
905    #[test]
906    fn test_mention_completion_parse() {
907        assert_eq!(MentionCompletion::try_parse("Lorem Ipsum", 0), None);
908
909        assert_eq!(
910            MentionCompletion::try_parse("Lorem @", 0),
911            Some(MentionCompletion {
912                source_range: 6..7,
913                mode: None,
914                argument: None,
915            })
916        );
917
918        assert_eq!(
919            MentionCompletion::try_parse("Lorem @file", 0),
920            Some(MentionCompletion {
921                source_range: 6..11,
922                mode: Some(ContextPickerMode::File),
923                argument: None,
924            })
925        );
926
927        assert_eq!(
928            MentionCompletion::try_parse("Lorem @file ", 0),
929            Some(MentionCompletion {
930                source_range: 6..12,
931                mode: Some(ContextPickerMode::File),
932                argument: None,
933            })
934        );
935
936        assert_eq!(
937            MentionCompletion::try_parse("Lorem @file main.rs", 0),
938            Some(MentionCompletion {
939                source_range: 6..19,
940                mode: Some(ContextPickerMode::File),
941                argument: Some("main.rs".to_string()),
942            })
943        );
944
945        assert_eq!(
946            MentionCompletion::try_parse("Lorem @file main.rs ", 0),
947            Some(MentionCompletion {
948                source_range: 6..19,
949                mode: Some(ContextPickerMode::File),
950                argument: Some("main.rs".to_string()),
951            })
952        );
953
954        assert_eq!(
955            MentionCompletion::try_parse("Lorem @file main.rs Ipsum", 0),
956            Some(MentionCompletion {
957                source_range: 6..19,
958                mode: Some(ContextPickerMode::File),
959                argument: Some("main.rs".to_string()),
960            })
961        );
962
963        assert_eq!(
964            MentionCompletion::try_parse("Lorem @main", 0),
965            Some(MentionCompletion {
966                source_range: 6..11,
967                mode: None,
968                argument: Some("main".to_string()),
969            })
970        );
971
972        assert_eq!(MentionCompletion::try_parse("test@", 0), None);
973    }
974}