Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:40:52.459Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

completions.rs

1520 lines · 57.5 KB · rust
1use super::*;
2
3impl Editor {
4    pub fn set_completion_provider(&mut self, provider: Option<Rc<dyn CompletionProvider>>) {
5        self.completion_provider = provider;
6    }
7
8    pub fn set_show_completions_on_input(&mut self, show_completions_on_input: Option<bool>) {
9        self.show_completions_on_input_override = show_completions_on_input;
10    }
11
12    pub fn text_layout_details(&self, window: &mut Window, cx: &mut App) -> TextLayoutDetails {
13        TextLayoutDetails {
14            text_system: window.text_system().clone(),
15            editor_style: self.style.clone().unwrap_or_else(|| self.create_style(cx)),
16            rem_size: window.rem_size(),
17            scroll_anchor: self.scroll_manager.shared_scroll_anchor(cx),
18            visible_rows: self.visible_line_count(),
19            vertical_scroll_margin: self.scroll_manager.vertical_scroll_margin,
20        }
21    }
22
23    pub fn show_word_completions(
24        &mut self,
25        _: &ShowWordCompletions,
26        window: &mut Window,
27        cx: &mut Context<Self>,
28    ) {
29        self.open_or_update_completions_menu(
30            Some(CompletionsMenuSource::Words {
31                ignore_threshold: true,
32            }),
33            None,
34            false,
35            window,
36            cx,
37        );
38    }
39
40    pub fn show_completions(
41        &mut self,
42        _: &ShowCompletions,
43        window: &mut Window,
44        cx: &mut Context<Self>,
45    ) {
46        self.open_or_update_completions_menu(None, None, false, window, cx);
47    }
48
49    pub fn confirm_completion(
50        &mut self,
51        action: &ConfirmCompletion,
52        window: &mut Window,
53        cx: &mut Context<Self>,
54    ) -> Option<Task<Result<()>>> {
55        if self.read_only(cx) {
56            return None;
57        }
58        self.do_completion(action.item_ix, CompletionIntent::Complete, window, cx)
59    }
60
61    pub fn confirm_completion_insert(
62        &mut self,
63        _: &ConfirmCompletionInsert,
64        window: &mut Window,
65        cx: &mut Context<Self>,
66    ) -> Option<Task<Result<()>>> {
67        if self.read_only(cx) {
68            return None;
69        }
70        self.do_completion(None, CompletionIntent::CompleteWithInsert, window, cx)
71    }
72
73    pub fn confirm_completion_replace(
74        &mut self,
75        _: &ConfirmCompletionReplace,
76        window: &mut Window,
77        cx: &mut Context<Self>,
78    ) -> Option<Task<Result<()>>> {
79        if self.read_only(cx) {
80            return None;
81        }
82        self.do_completion(None, CompletionIntent::CompleteWithReplace, window, cx)
83    }
84
85    pub fn compose_completion(
86        &mut self,
87        action: &ComposeCompletion,
88        window: &mut Window,
89        cx: &mut Context<Self>,
90    ) -> Option<Task<Result<()>>> {
91        self.do_completion(action.item_ix, CompletionIntent::Compose, window, cx)
92    }
93
94    pub fn has_visible_completions_menu(&self) -> bool {
95        !self.edit_prediction_preview_is_active()
96            && self.context_menu.borrow().as_ref().is_some_and(|menu| {
97                menu.visible() && matches!(menu, CodeContextMenu::Completions(_))
98            })
99    }
100
101    pub(super) fn trigger_completion_on_input(
102        &mut self,
103        text: &str,
104        trigger_in_words: bool,
105        window: &mut Window,
106        cx: &mut Context<Self>,
107    ) {
108        let completions_source = self
109            .context_menu
110            .borrow()
111            .as_ref()
112            .and_then(|menu| match menu {
113                CodeContextMenu::Completions(completions_menu) => Some(completions_menu.source),
114                CodeContextMenu::CodeActions(_) => None,
115            });
116
117        match completions_source {
118            Some(CompletionsMenuSource::Words { .. }) => {
119                self.open_or_update_completions_menu(
120                    Some(CompletionsMenuSource::Words {
121                        ignore_threshold: false,
122                    }),
123                    None,
124                    trigger_in_words,
125                    window,
126                    cx,
127                );
128            }
129            _ => self.open_or_update_completions_menu(
130                None,
131                Some(text.to_owned()).filter(|x| !x.is_empty()),
132                trigger_in_words,
133                window,
134                cx,
135            ),
136        }
137    }
138
139    pub(super) fn is_lsp_relevant(&self, file: Option<&Arc<dyn language::File>>, cx: &App) -> bool {
140        let Some(project) = self.project() else {
141            return false;
142        };
143        let Some(buffer_file) = project::File::from_dyn(file) else {
144            return false;
145        };
146        let Some(entry_id) = buffer_file.project_entry_id() else {
147            return false;
148        };
149        let project = project.read(cx);
150        let Some(buffer_worktree) = project.worktree_for_id(buffer_file.worktree_id(cx), cx) else {
151            return false;
152        };
153        let Some(worktree_entry) = buffer_worktree.read(cx).entry_for_id(entry_id) else {
154            return false;
155        };
156        !worktree_entry.is_ignored
157    }
158
159    pub(super) fn visible_buffers(&self, cx: &mut Context<Editor>) -> Vec<Entity<Buffer>> {
160        let display_snapshot = self.display_snapshot(cx);
161        let visible_range = self.multi_buffer_visible_range(&display_snapshot, cx);
162        let multi_buffer = self.buffer().read(cx);
163        display_snapshot
164            .buffer_snapshot()
165            .range_to_buffer_ranges(visible_range)
166            .into_iter()
167            .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty())
168            .filter_map(|(buffer_snapshot, _, _)| multi_buffer.buffer(buffer_snapshot.remote_id()))
169            .collect()
170    }
171
172    pub(super) fn visible_buffer_ranges(
173        &self,
174        cx: &mut Context<Editor>,
175    ) -> Vec<(
176        BufferSnapshot,
177        Range<BufferOffset>,
178        ExcerptRange<text::Anchor>,
179    )> {
180        let display_snapshot = self.display_snapshot(cx);
181        let visible_range = self.multi_buffer_visible_range(&display_snapshot, cx);
182        display_snapshot
183            .buffer_snapshot()
184            .range_to_buffer_ranges(visible_range)
185            .into_iter()
186            .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty())
187            .map(|(buffer_snapshot, buffer_offset_range, excerpt_range)| {
188                (buffer_snapshot.clone(), buffer_offset_range, excerpt_range)
189            })
190            .collect()
191    }
192
193    pub(super) fn trigger_on_type_formatting(
194        &self,
195        input: String,
196        window: &mut Window,
197        cx: &mut Context<Self>,
198    ) -> Option<Task<Result<()>>> {
199        if input.chars().count() != 1 {
200            return None;
201        }
202
203        let project = self.project()?;
204        let position = self.selections.newest_anchor().head();
205        let (buffer, buffer_position) = self
206            .buffer
207            .read(cx)
208            .text_anchor_for_position(position, cx)?;
209
210        let settings = LanguageSettings::for_buffer_at(&buffer.read(cx), buffer_position, cx);
211        if !settings.use_on_type_format {
212            return None;
213        }
214
215        // OnTypeFormatting returns a list of edits, no need to pass them between Zed instances,
216        // hence we do LSP request & edit on host side only — add formats to host's history.
217        let push_to_lsp_host_history = true;
218        // If this is not the host, append its history with new edits.
219        let push_to_client_history = project.read(cx).is_via_collab();
220
221        let on_type_formatting = project.update(cx, |project, cx| {
222            project.on_type_format(
223                buffer.clone(),
224                buffer_position,
225                input,
226                push_to_lsp_host_history,
227                cx,
228            )
229        });
230        Some(cx.spawn_in(window, async move |editor, cx| {
231            if let Some(transaction) = on_type_formatting.await? {
232                if push_to_client_history {
233                    buffer.update(cx, |buffer, _| {
234                        buffer.push_transaction(transaction, Instant::now());
235                        buffer.finalize_last_transaction();
236                    });
237                }
238                editor.update(cx, |editor, cx| {
239                    editor.refresh_document_highlights(cx);
240                })?;
241            }
242            Ok(())
243        }))
244    }
245
246    pub(super) fn open_or_update_completions_menu(
247        &mut self,
248        requested_source: Option<CompletionsMenuSource>,
249        trigger: Option<String>,
250        trigger_in_words: bool,
251        window: &mut Window,
252        cx: &mut Context<Self>,
253    ) {
254        if self.pending_rename.is_some() {
255            return;
256        }
257
258        let completions_source = self
259            .context_menu
260            .borrow()
261            .as_ref()
262            .and_then(|menu| match menu {
263                CodeContextMenu::Completions(completions_menu) => Some(completions_menu.source),
264                CodeContextMenu::CodeActions(_) => None,
265            });
266
267        let multibuffer_snapshot = self.buffer.read(cx).read(cx);
268
269        let is_showing_snippet_choices = matches!(
270            completions_source,
271            Some(CompletionsMenuSource::SnippetChoices)
272        );
273
274        let anchor = self.selections.newest_anchor();
275        let position = if is_showing_snippet_choices {
276            // Typically `start` == `end`, but with snippet tabstop choices the default choice is
277            // inserted and selected. To handle that case, the start of the selection is used so that
278            // the menu starts with all choices.
279            anchor.start.bias_right(&multibuffer_snapshot)
280        } else {
281            anchor.head().bias_right(&multibuffer_snapshot)
282        };
283
284        if position.diff_base_anchor().is_some() {
285            return;
286        }
287        let multibuffer_position = multibuffer_snapshot.anchor_before(position);
288        let Some((buffer_position, _)) =
289            multibuffer_snapshot.anchor_to_buffer_anchor(multibuffer_position)
290        else {
291            return;
292        };
293        let Some(buffer) = self.buffer.read(cx).buffer(buffer_position.buffer_id) else {
294            return;
295        };
296        let buffer_snapshot = buffer.read(cx).snapshot();
297
298        let menu_is_open = matches!(
299            self.context_menu.borrow().as_ref(),
300            Some(CodeContextMenu::Completions(_))
301        );
302
303        let language = buffer_snapshot
304            .language_at(buffer_position)
305            .map(|language| language.name());
306        let language_settings = multibuffer_snapshot.language_settings_at(multibuffer_position, cx);
307        let completion_settings = language_settings.completions.clone();
308
309        let show_completions_on_input = self
310            .show_completions_on_input_override
311            .unwrap_or(language_settings.show_completions_on_input);
312        if !menu_is_open && trigger.is_some() && !show_completions_on_input {
313            return;
314        }
315
316        let query: Option<Arc<String>> =
317            Self::completion_query(&multibuffer_snapshot, multibuffer_position)
318                .map(|query| query.into());
319
320        drop(multibuffer_snapshot);
321
322        // Hide the current completions menu when query is empty. Without this, cached
323        // completions from before the trigger char may be reused (#32774).
324        if query.is_none() && menu_is_open && !is_showing_snippet_choices {
325            self.hide_context_menu(window, cx);
326        }
327
328        let mut ignore_word_threshold = false;
329        let provider = match requested_source {
330            Some(CompletionsMenuSource::Normal) | None => self.completion_provider.clone(),
331            Some(CompletionsMenuSource::Words { ignore_threshold }) => {
332                ignore_word_threshold = ignore_threshold;
333                None
334            }
335            Some(CompletionsMenuSource::SnippetChoices)
336            | Some(CompletionsMenuSource::SnippetsOnly) => {
337                log::error!("bug: SnippetChoices requested_source is not handled");
338                None
339            }
340        };
341
342        let sort_completions = provider
343            .as_ref()
344            .is_some_and(|provider| provider.sort_completions());
345
346        let filter_completions = provider
347            .as_ref()
348            .is_none_or(|provider| provider.filter_completions());
349
350        let was_snippets_only = matches!(
351            completions_source,
352            Some(CompletionsMenuSource::SnippetsOnly)
353        );
354
355        if let Some(CodeContextMenu::Completions(menu)) = self.context_menu.borrow_mut().as_mut() {
356            if filter_completions {
357                menu.filter(
358                    query.clone().unwrap_or_default(),
359                    buffer_position,
360                    &buffer,
361                    provider.clone(),
362                    window,
363                    cx,
364                );
365            }
366            // When `is_incomplete` is false, no need to re-query completions when the current query
367            // is a suffix of the initial query.
368            let was_complete = !menu.is_incomplete;
369            if was_complete && !was_snippets_only {
370                // If the new query is a suffix of the old query (typing more characters) and
371                // the previous result was complete, the existing completions can be filtered.
372                //
373                // Note that snippet completions are always complete.
374                let query_matches = match (&menu.initial_query, &query) {
375                    (Some(initial_query), Some(query)) => query.starts_with(initial_query.as_ref()),
376                    (None, _) => true,
377                    _ => false,
378                };
379                if query_matches {
380                    let position_matches = if menu.initial_position == position {
381                        true
382                    } else {
383                        let snapshot = self.buffer.read(cx).read(cx);
384                        menu.initial_position.to_offset(&snapshot) == position.to_offset(&snapshot)
385                    };
386                    if position_matches {
387                        return;
388                    }
389                }
390            }
391        };
392
393        let (word_replace_range, word_to_exclude) = if let (word_range, Some(CharKind::Word)) =
394            buffer_snapshot.surrounding_word(buffer_position, None)
395        {
396            let word_to_exclude = buffer_snapshot
397                .text_for_range(word_range.clone())
398                .collect::<String>();
399            (
400                buffer_snapshot.anchor_before(word_range.start)
401                    ..buffer_snapshot.anchor_after(buffer_position),
402                Some(word_to_exclude),
403            )
404        } else {
405            (buffer_position..buffer_position, None)
406        };
407
408        let show_completion_documentation = buffer_snapshot
409            .settings_at(buffer_position, cx)
410            .show_completion_documentation;
411
412        // The document can be large, so stay in reasonable bounds when searching for words,
413        // otherwise completion pop-up might be slow to appear.
414        const WORD_LOOKUP_ROWS: u32 = 5_000;
415        let buffer_row = text::ToPoint::to_point(&buffer_position, &buffer_snapshot).row;
416        let min_word_search = buffer_snapshot.clip_point(
417            Point::new(buffer_row.saturating_sub(WORD_LOOKUP_ROWS), 0),
418            Bias::Left,
419        );
420        let max_word_search = buffer_snapshot.clip_point(
421            Point::new(buffer_row + WORD_LOOKUP_ROWS, 0).min(buffer_snapshot.max_point()),
422            Bias::Right,
423        );
424        let word_search_range = buffer_snapshot.point_to_offset(min_word_search)
425            ..buffer_snapshot.point_to_offset(max_word_search);
426
427        let skip_digits = query
428            .as_ref()
429            .is_none_or(|query| !query.chars().any(|c| c.is_digit(10)));
430
431        let load_provider_completions = provider.as_ref().is_some_and(|provider| {
432            trigger.as_ref().is_none_or(|trigger| {
433                provider.is_completion_trigger(
434                    &buffer,
435                    buffer_position,
436                    trigger,
437                    trigger_in_words,
438                    cx,
439                )
440            })
441        });
442
443        let provider_responses = if let Some(provider) = &provider
444            && load_provider_completions
445        {
446            let trigger_character = trigger
447                .as_ref()
448                .filter(|trigger| {
449                    buffer
450                        .read(cx)
451                        .completion_triggers()
452                        .contains(trigger.as_str())
453                })
454                .cloned();
455            let completion_context = CompletionContext {
456                trigger_kind: match &trigger_character {
457                    Some(_) => CompletionTriggerKind::TRIGGER_CHARACTER,
458                    None => CompletionTriggerKind::INVOKED,
459                },
460                trigger_character,
461            };
462
463            provider.completions(&buffer, buffer_position, completion_context, window, cx)
464        } else {
465            Task::ready(Ok(Vec::new()))
466        };
467
468        let load_word_completions = if !self.word_completions_enabled {
469            false
470        } else if requested_source
471            == Some(CompletionsMenuSource::Words {
472                ignore_threshold: true,
473            })
474        {
475            true
476        } else {
477            load_provider_completions
478                && completion_settings.words != WordsCompletionMode::Disabled
479                && (ignore_word_threshold || {
480                    let words_min_length = completion_settings.words_min_length;
481                    // check whether word has at least `words_min_length` characters
482                    let query_chars = query.iter().flat_map(|q| q.chars());
483                    query_chars.take(words_min_length).count() == words_min_length
484                })
485        };
486
487        let mut words = if load_word_completions {
488            cx.background_spawn({
489                let buffer_snapshot = buffer_snapshot.clone();
490                async move {
491                    buffer_snapshot.words_in_range(WordsQuery {
492                        fuzzy_contents: None,
493                        range: word_search_range,
494                        skip_digits,
495                    })
496                }
497            })
498        } else {
499            Task::ready(BTreeMap::default())
500        };
501
502        let snippet_char_classifier = buffer_snapshot
503            .char_classifier_at(buffer_position)
504            .scope_context(Some(CharScopeContext::Completion));
505
506        let snippets = if let Some(provider) = &provider
507            && provider.show_snippets()
508            && let Some(project) = self.project()
509        {
510            let word_trigger = trigger.as_ref().is_some_and(|trigger| {
511                !trigger.is_empty()
512                    && trigger
513                        .chars()
514                        .all(|character| snippet_char_classifier.is_word(character))
515            });
516            let requires_strong_snippet_match = !menu_is_open && !trigger_in_words && word_trigger;
517            let load_snippet_completions = !requires_strong_snippet_match
518                || query.as_ref().is_some_and(|query| {
519                    let project = project.read(cx);
520                    has_strong_snippet_prefix_match(
521                        &project,
522                        &buffer,
523                        buffer_position,
524                        &snippet_char_classifier,
525                        query,
526                        cx,
527                    )
528                });
529
530            if load_snippet_completions {
531                project.update(cx, |project, cx| {
532                    snippet_completions(
533                        project,
534                        &buffer,
535                        buffer_position,
536                        snippet_char_classifier,
537                        cx,
538                    )
539                })
540            } else {
541                Task::ready(Ok(CompletionResponse {
542                    completions: Vec::new(),
543                    display_options: Default::default(),
544                    is_incomplete: false,
545                }))
546            }
547        } else {
548            Task::ready(Ok(CompletionResponse {
549                completions: Vec::new(),
550                display_options: Default::default(),
551                is_incomplete: false,
552            }))
553        };
554
555        let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order;
556
557        let id = post_inc(&mut self.next_completion_id);
558        let task = cx.spawn_in(window, async move |editor, cx| {
559            let Ok(()) = editor.update(cx, |this, _| {
560                this.completion_tasks.retain(|(task_id, _)| *task_id >= id);
561            }) else {
562                return;
563            };
564
565            // TODO: Ideally completions from different sources would be selectively re-queried, so
566            // that having one source with `is_incomplete: true` doesn't cause all to be re-queried.
567            let mut completions = Vec::new();
568            let mut is_incomplete = false;
569            let mut display_options: Option<CompletionDisplayOptions> = None;
570            if let Some(provider_responses) = provider_responses.await.log_err()
571                && !provider_responses.is_empty()
572            {
573                for response in provider_responses {
574                    completions.extend(response.completions);
575                    is_incomplete = is_incomplete || response.is_incomplete;
576                    match display_options.as_mut() {
577                        None => {
578                            display_options = Some(response.display_options);
579                        }
580                        Some(options) => options.merge(&response.display_options),
581                    }
582                }
583                if completion_settings.words == WordsCompletionMode::Fallback {
584                    words = Task::ready(BTreeMap::default());
585                }
586            }
587            let display_options = display_options.unwrap_or_default();
588
589            let mut words = words.await;
590            if let Some(word_to_exclude) = &word_to_exclude {
591                words.remove(word_to_exclude);
592            }
593            for lsp_completion in &completions {
594                words.remove(&lsp_completion.new_text);
595            }
596            completions.extend(words.into_iter().map(|(word, word_range)| Completion {
597                replace_range: word_replace_range.clone(),
598                new_text: word.clone(),
599                label: CodeLabel::plain(word, None),
600                match_start: None,
601                snippet_deduplication_key: None,
602                icon_path: None,
603                icon_color: None,
604                documentation: None,
605                source: CompletionSource::BufferWord {
606                    word_range,
607                    resolved: false,
608                },
609                insert_text_mode: Some(InsertTextMode::AS_IS),
610                confirm: None,
611                group: None,
612            }));
613
614            completions.extend(
615                snippets
616                    .await
617                    .into_iter()
618                    .flat_map(|response| response.completions),
619            );
620
621            let menu = if completions.is_empty() {
622                None
623            } else {
624                let Ok((mut menu, matches_task)) = editor.update(cx, |editor, cx| {
625                    let languages = editor
626                        .workspace
627                        .as_ref()
628                        .and_then(|(workspace, _)| workspace.upgrade())
629                        .map(|workspace| workspace.read(cx).app_state().languages.clone());
630                    let menu = CompletionsMenu::new(
631                        id,
632                        requested_source.unwrap_or(if load_provider_completions {
633                            CompletionsMenuSource::Normal
634                        } else {
635                            CompletionsMenuSource::SnippetsOnly
636                        }),
637                        sort_completions,
638                        show_completion_documentation,
639                        position,
640                        query.clone(),
641                        is_incomplete,
642                        buffer.clone(),
643                        completions.into(),
644                        editor
645                            .context_menu()
646                            .borrow_mut()
647                            .as_ref()
648                            .map(|menu| menu.primary_scroll_handle()),
649                        display_options,
650                        snippet_sort_order,
651                        languages,
652                        language,
653                        cx,
654                    );
655
656                    let query = if filter_completions { query } else { None };
657                    let matches_task = menu.do_async_filtering(
658                        query.unwrap_or_default(),
659                        buffer_position,
660                        &buffer,
661                        cx,
662                    );
663                    (menu, matches_task)
664                }) else {
665                    return;
666                };
667
668                let matches = matches_task.await;
669
670                let Ok(()) = editor.update_in(cx, |editor, window, cx| {
671                    // Newer menu already set, so exit.
672                    if let Some(CodeContextMenu::Completions(prev_menu)) =
673                        editor.context_menu.borrow().as_ref()
674                        && prev_menu.id > id
675                    {
676                        return;
677                    };
678
679                    // Only valid to take prev_menu because either the new menu is immediately set
680                    // below, or the menu is hidden.
681                    if let Some(CodeContextMenu::Completions(prev_menu)) =
682                        editor.context_menu.borrow_mut().take()
683                    {
684                        let position_matches =
685                            if prev_menu.initial_position == menu.initial_position {
686                                true
687                            } else {
688                                let snapshot = editor.buffer.read(cx).read(cx);
689                                prev_menu.initial_position.to_offset(&snapshot)
690                                    == menu.initial_position.to_offset(&snapshot)
691                            };
692                        if position_matches {
693                            // Preserve markdown cache before `set_filter_results` because it will
694                            // try to populate the documentation cache.
695                            menu.preserve_markdown_cache(prev_menu);
696                        }
697                    };
698
699                    menu.set_filter_results(matches, provider, window, cx);
700                }) else {
701                    return;
702                };
703
704                menu.visible().then_some(menu)
705            };
706
707            editor
708                .update_in(cx, |editor, window, cx| {
709                    if editor.focus_handle.is_focused(window)
710                        && let Some(menu) = menu
711                    {
712                        *editor.context_menu.borrow_mut() =
713                            Some(CodeContextMenu::Completions(menu));
714
715                        crate::hover_popover::hide_hover(editor, cx);
716                        if editor.show_edit_predictions_in_menu() {
717                            editor.update_visible_edit_prediction(window, cx);
718                        } else {
719                            editor
720                                .discard_edit_prediction(EditPredictionDiscardReason::Ignored, cx);
721                        }
722
723                        cx.notify();
724                        return;
725                    }
726
727                    if editor.completion_tasks.len() <= 1 {
728                        // If there are no more completion tasks and the last menu was empty, we should hide it.
729                        let was_hidden = editor.hide_context_menu(window, cx).is_none();
730                        // If it was already hidden and we don't show edit predictions in the menu,
731                        // we should also show the edit prediction when available.
732                        if was_hidden && editor.show_edit_predictions_in_menu() {
733                            editor.update_visible_edit_prediction(window, cx);
734                        }
735                    }
736                })
737                .ok();
738        });
739
740        self.completion_tasks.push((id, task));
741    }
742
743    pub(super) fn with_completions_menu_matching_id<R>(
744        &self,
745        id: CompletionId,
746        f: impl FnOnce(Option<&mut CompletionsMenu>) -> R,
747    ) -> R {
748        let mut context_menu = self.context_menu.borrow_mut();
749        let Some(CodeContextMenu::Completions(completions_menu)) = &mut *context_menu else {
750            return f(None);
751        };
752        if completions_menu.id != id {
753            return f(None);
754        }
755        f(Some(completions_menu))
756    }
757
758    fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option<String> {
759        let offset = position.to_offset(buffer);
760        let (word_range, kind) =
761            buffer.surrounding_word(offset, Some(CharScopeContext::Completion));
762        if offset > word_range.start && kind == Some(CharKind::Word) {
763            Some(
764                buffer
765                    .text_for_range(word_range.start..offset)
766                    .collect::<String>(),
767            )
768        } else {
769            None
770        }
771    }
772
773    fn do_completion(
774        &mut self,
775        item_ix: Option<usize>,
776        intent: CompletionIntent,
777        window: &mut Window,
778        cx: &mut Context<Editor>,
779    ) -> Option<Task<Result<()>>> {
780        use language::ToOffset as _;
781
782        let CodeContextMenu::Completions(completions_menu) = self.hide_context_menu(window, cx)?
783        else {
784            return None;
785        };
786
787        let candidate_id = {
788            let entries = completions_menu.entries.borrow();
789            let entry = entries.get(item_ix.unwrap_or(completions_menu.selected_item))?;
790            let mat = entry.as_match()?;
791            if self.show_edit_predictions_in_menu() {
792                self.discard_edit_prediction(EditPredictionDiscardReason::Rejected, cx);
793            }
794            mat.candidate_id
795        };
796
797        let completion = completions_menu
798            .completions
799            .borrow()
800            .get(candidate_id)?
801            .clone();
802        cx.stop_propagation();
803
804        let buffer_handle = completions_menu.buffer.clone();
805        let multibuffer_snapshot = self.buffer.read(cx).snapshot(cx);
806        let (initial_position, _) =
807            multibuffer_snapshot.anchor_to_buffer_anchor(completions_menu.initial_position)?;
808
809        let CompletionEdit {
810            new_text,
811            snippet,
812            replace_range,
813        } = process_completion_for_edit(&completion, intent, &buffer_handle, &initial_position, cx);
814
815        let buffer = buffer_handle.read(cx).snapshot();
816        let newest_selection = self.selections.newest_anchor();
817
818        let Some(replace_range_multibuffer) =
819            multibuffer_snapshot.buffer_anchor_range_to_anchor_range(replace_range.clone())
820        else {
821            return None;
822        };
823
824        let Some((buffer_snapshot, newest_range_buffer)) =
825            multibuffer_snapshot.anchor_range_to_buffer_anchor_range(newest_selection.range())
826        else {
827            return None;
828        };
829
830        let old_text = buffer
831            .text_for_range(replace_range.clone())
832            .collect::<String>();
833        let (lookbehind, lookahead) = if buffer.remote_id() == buffer_snapshot.remote_id() {
834            let lookbehind = newest_range_buffer
835                .start
836                .to_offset(&buffer)
837                .saturating_sub(replace_range.start.to_offset(&buffer));
838            let lookahead = replace_range
839                .end
840                .to_offset(&buffer)
841                .saturating_sub(newest_range_buffer.end.to_offset(&buffer));
842            (lookbehind, lookahead)
843        } else {
844            (0, 0)
845        };
846        let prefix = &old_text[..old_text.len().saturating_sub(lookahead)];
847        let suffix = &old_text[lookbehind.min(old_text.len())..];
848
849        let selections = self
850            .selections
851            .all::<MultiBufferOffset>(&self.display_snapshot(cx));
852        let mut ranges = Vec::new();
853        let mut all_commit_ranges = Vec::new();
854        let mut linked_edits = LinkedEdits::new();
855
856        let text: Arc<str> = new_text.clone().into();
857        for selection in &selections {
858            let range = if selection.id == newest_selection.id {
859                replace_range_multibuffer.clone()
860            } else {
861                let mut range = selection.range();
862
863                // if prefix is present, don't duplicate it
864                if multibuffer_snapshot
865                    .contains_str_at(range.start.saturating_sub_usize(lookbehind), prefix)
866                {
867                    range.start = range.start.saturating_sub_usize(lookbehind);
868
869                    // if suffix is also present, mimic the newest cursor and replace it
870                    if selection.id != newest_selection.id
871                        && multibuffer_snapshot.contains_str_at(range.end, suffix)
872                    {
873                        range.end += lookahead;
874                    }
875                }
876                range.to_anchors(&multibuffer_snapshot)
877            };
878
879            ranges.push(range.clone());
880
881            let start_anchor = multibuffer_snapshot.anchor_before(range.start);
882            let end_anchor = multibuffer_snapshot.anchor_after(range.end);
883
884            if let Some((buffer_snapshot_2, anchor_range)) =
885                multibuffer_snapshot.anchor_range_to_buffer_anchor_range(start_anchor..end_anchor)
886                && buffer_snapshot_2.remote_id() == buffer_snapshot.remote_id()
887            {
888                all_commit_ranges.push(anchor_range.clone());
889                if !self.linked_edit_ranges.is_empty() {
890                    linked_edits.push(&self, anchor_range, text.clone(), cx);
891                }
892            }
893        }
894
895        let common_prefix_len = old_text
896            .chars()
897            .zip(new_text.chars())
898            .take_while(|(a, b)| a == b)
899            .map(|(a, _)| a.len_utf8())
900            .sum::<usize>();
901
902        cx.emit(EditorEvent::InputHandled {
903            utf16_range_to_replace: None,
904            text: new_text[common_prefix_len..].into(),
905        });
906
907        let tx_id = self.transact(window, cx, |editor, window, cx| {
908            if let Some(mut snippet) = snippet {
909                snippet.text = new_text.to_string();
910                let offset_ranges = ranges
911                    .iter()
912                    .map(|range| range.to_offset(&multibuffer_snapshot))
913                    .collect::<Vec<_>>();
914                editor
915                    .insert_snippet(&offset_ranges, snippet, window, cx)
916                    .log_err();
917            } else {
918                editor.buffer.update(cx, |multi_buffer, cx| {
919                    let auto_indent = match completion.insert_text_mode {
920                        Some(InsertTextMode::AS_IS) => None,
921                        _ => editor.autoindent_mode.clone(),
922                    };
923                    let edits = ranges.into_iter().map(|range| (range, new_text.as_str()));
924                    multi_buffer.edit(edits, auto_indent, cx);
925                });
926            }
927            linked_edits.apply(cx);
928            editor.refresh_edit_prediction(
929                true,
930                false,
931                EditPredictionRequestTrigger::LSPCompletionAccepted,
932                window,
933                cx,
934            );
935        });
936        self.invalidate_autoclose_regions(
937            &self.selections.disjoint_anchors_arc(),
938            &multibuffer_snapshot,
939        );
940
941        let show_new_completions_on_confirm = completion
942            .confirm
943            .as_ref()
944            .is_some_and(|confirm| confirm(intent, window, cx));
945        if show_new_completions_on_confirm {
946            self.open_or_update_completions_menu(None, None, false, window, cx);
947        }
948
949        let provider = self.completion_provider.as_ref()?;
950
951        let lsp_store = self.project().map(|project| project.read(cx).lsp_store());
952        let command = lsp_store.as_ref().and_then(|lsp_store| {
953            let CompletionSource::Lsp {
954                lsp_completion,
955                server_id,
956                ..
957            } = &completion.source
958            else {
959                return None;
960            };
961            let lsp_command = lsp_completion.command.as_ref()?;
962            let available_commands = lsp_store
963                .read(cx)
964                .lsp_server_capabilities
965                .get(server_id)
966                .and_then(|server_capabilities| {
967                    server_capabilities
968                        .execute_command_provider
969                        .as_ref()
970                        .map(|options| options.commands.as_slice())
971                })?;
972            if available_commands.contains(&lsp_command.command) {
973                Some(CodeAction {
974                    server_id: *server_id,
975                    range: language::Anchor::min_min_range_for_buffer(buffer.remote_id()),
976                    lsp_action: LspAction::Command(lsp_command.clone()),
977                    resolved: false,
978                })
979            } else {
980                None
981            }
982        });
983
984        drop(completion);
985        let apply_edits = provider.apply_additional_edits_for_completion(
986            buffer_handle.clone(),
987            completions_menu.completions.clone(),
988            candidate_id,
989            true,
990            all_commit_ranges,
991            cx,
992        );
993
994        let editor_settings = EditorSettings::get_global(cx);
995        if editor_settings.show_signature_help_after_edits || editor_settings.auto_signature_help {
996            // After the code completion is finished, users often want to know what signatures are needed.
997            // so we should automatically call signature_help
998            self.show_signature_help(&ShowSignatureHelp, window, cx);
999        }
1000
1001        // After the code completion is finished, we should finalize the last transaction.
1002        // This ensure vim/helix not group the edits together.
1003        self.buffer
1004            .update(cx, |buffer, cx| buffer.finalize_last_transaction(cx));
1005
1006        Some(cx.spawn_in(window, async move |editor, cx| {
1007            let additional_edits_tx = apply_edits.await?;
1008
1009            if let Some((lsp_store, command)) = lsp_store.zip(command) {
1010                let title = command.lsp_action.title().to_owned();
1011                let project_transaction = lsp_store
1012                    .update(cx, |lsp_store, cx| {
1013                        lsp_store.apply_code_action(buffer_handle, command, false, cx)
1014                    })
1015                    .await
1016                    .context("applying post-completion command")?;
1017                if let Some(workspace) = editor.read_with(cx, |editor, _| editor.workspace())? {
1018                    Self::open_project_transaction(
1019                        &editor,
1020                        workspace.downgrade(),
1021                        project_transaction,
1022                        title,
1023                        cx,
1024                    )
1025                    .await?;
1026                }
1027            }
1028
1029            if let Some(tx_id) = tx_id
1030                && let Some(additional_edits_tx) = additional_edits_tx
1031            {
1032                editor
1033                    .update(cx, |editor, cx| {
1034                        editor.buffer.update(cx, |buffer, cx| {
1035                            buffer.merge_transactions(additional_edits_tx.id, tx_id, cx)
1036                        });
1037                    })
1038                    .context("merge transactions")?;
1039            }
1040
1041            Ok(())
1042        }))
1043    }
1044}
1045
1046#[cfg(any(test, feature = "test-support"))]
1047impl Editor {
1048    pub fn completion_provider(&self) -> Option<Rc<dyn CompletionProvider>> {
1049        self.completion_provider.clone()
1050    }
1051
1052    pub fn current_completions(&self) -> Option<Vec<project::Completion>> {
1053        let menu = self.context_menu.borrow();
1054        if let CodeContextMenu::Completions(menu) = menu.as_ref()? {
1055            let completions = menu.completions.borrow();
1056            Some(completions.to_vec())
1057        } else {
1058            None
1059        }
1060    }
1061
1062    #[cfg(test)]
1063    pub(super) fn disable_word_completions(&mut self) {
1064        self.word_completions_enabled = false;
1065    }
1066}
1067
1068pub trait CompletionProvider {
1069    fn completions(
1070        &self,
1071        buffer: &Entity<Buffer>,
1072        buffer_position: text::Anchor,
1073        trigger: CompletionContext,
1074        window: &mut Window,
1075        cx: &mut Context<Editor>,
1076    ) -> Task<Result<Vec<CompletionResponse>>>;
1077
1078    fn resolve_completions(
1079        &self,
1080        _buffer: Entity<Buffer>,
1081        _completion_indices: Vec<usize>,
1082        _completions: Rc<RefCell<Box<[Completion]>>>,
1083        _cx: &mut Context<Editor>,
1084    ) -> Task<Result<bool>> {
1085        Task::ready(Ok(false))
1086    }
1087
1088    fn apply_additional_edits_for_completion(
1089        &self,
1090        _buffer: Entity<Buffer>,
1091        _completions: Rc<RefCell<Box<[Completion]>>>,
1092        _completion_index: usize,
1093        _push_to_history: bool,
1094        _all_commit_ranges: Vec<Range<language::Anchor>>,
1095        _cx: &mut Context<Editor>,
1096    ) -> Task<Result<Option<language::Transaction>>> {
1097        Task::ready(Ok(None))
1098    }
1099
1100    fn is_completion_trigger(
1101        &self,
1102        buffer: &Entity<Buffer>,
1103        position: language::Anchor,
1104        text: &str,
1105        trigger_in_words: bool,
1106        cx: &mut Context<Editor>,
1107    ) -> bool;
1108
1109    fn selection_changed(&self, _mat: Option<&StringMatch>, _window: &mut Window, _cx: &mut App) {}
1110
1111    fn sort_completions(&self) -> bool {
1112        true
1113    }
1114
1115    fn filter_completions(&self) -> bool {
1116        true
1117    }
1118
1119    fn show_snippets(&self) -> bool {
1120        false
1121    }
1122}
1123
1124fn has_strong_snippet_prefix_match(
1125    project: &Project,
1126    buffer: &Entity<Buffer>,
1127    buffer_anchor: text::Anchor,
1128    classifier: &CharClassifier,
1129    query: &str,
1130    cx: &App,
1131) -> bool {
1132    if query.chars().take(2).count() < 2 {
1133        return false;
1134    }
1135
1136    let query = query.to_lowercase();
1137    let is_word_char = |character| classifier.is_word(character);
1138    let languages = buffer.read(cx).languages_at(buffer_anchor);
1139    let snippet_store = project.snippets().read(cx);
1140
1141    languages.iter().any(|language| {
1142        snippet_store
1143            .snippets_for(Some(language.snippet_scope_id()), cx)
1144            .iter()
1145            .flat_map(|snippet| snippet.prefix.iter())
1146            .flat_map(|prefix| snippet_candidate_suffixes(prefix, &is_word_char))
1147            .any(|candidate| candidate.to_lowercase().starts_with(&query))
1148    })
1149}
1150
1151fn snippet_completions(
1152    project: &Project,
1153    buffer: &Entity<Buffer>,
1154    buffer_anchor: text::Anchor,
1155    classifier: CharClassifier,
1156    cx: &mut App,
1157) -> Task<Result<CompletionResponse>> {
1158    let languages = buffer.read(cx).languages_at(buffer_anchor);
1159    let snippet_store = project.snippets().read(cx);
1160
1161    let scopes: Vec<_> = languages
1162        .iter()
1163        .filter_map(|language| {
1164            let language_name = language.snippet_scope_id();
1165            let snippets = snippet_store.snippets_for(Some(language_name), cx);
1166
1167            if snippets.is_empty() {
1168                None
1169            } else {
1170                Some((language.default_scope(), snippets))
1171            }
1172        })
1173        .collect();
1174
1175    if scopes.is_empty() {
1176        return Task::ready(Ok(CompletionResponse {
1177            completions: vec![],
1178            display_options: CompletionDisplayOptions::default(),
1179            is_incomplete: false,
1180        }));
1181    }
1182
1183    let snapshot = buffer.read(cx).text_snapshot();
1184    let executor = cx.background_executor().clone();
1185
1186    cx.background_spawn(async move {
1187        let is_word_char = |c| classifier.is_word(c);
1188
1189        let mut is_incomplete = false;
1190        let mut completions: Vec<Completion> = Vec::new();
1191
1192        const MAX_PREFIX_LEN: usize = 128;
1193        let buffer_offset = text::ToOffset::to_offset(&buffer_anchor, &snapshot);
1194        let window_start = buffer_offset.saturating_sub(MAX_PREFIX_LEN);
1195        let window_start = snapshot.clip_offset(window_start, Bias::Left);
1196
1197        let max_buffer_window: String = snapshot
1198            .text_for_range(window_start..buffer_offset)
1199            .collect();
1200
1201        if max_buffer_window.is_empty() {
1202            return Ok(CompletionResponse {
1203                completions: vec![],
1204                display_options: CompletionDisplayOptions::default(),
1205                is_incomplete: true,
1206            });
1207        }
1208
1209        for (_scope, snippets) in scopes.into_iter() {
1210            // Sort snippets by word count to match longer snippet prefixes first.
1211            let mut sorted_snippet_candidates = snippets
1212                .iter()
1213                .enumerate()
1214                .flat_map(|(snippet_ix, snippet)| {
1215                    snippet
1216                        .prefix
1217                        .iter()
1218                        .enumerate()
1219                        .map(move |(prefix_ix, prefix)| {
1220                            let word_count =
1221                                snippet_candidate_suffixes(prefix, &is_word_char).count();
1222                            ((snippet_ix, prefix_ix), prefix, word_count)
1223                        })
1224                })
1225                .collect_vec();
1226            sorted_snippet_candidates
1227                .sort_unstable_by_key(|(_, _, word_count)| Reverse(*word_count));
1228
1229            // Each prefix may be matched multiple times; the completion menu must filter out duplicates.
1230
1231            let buffer_windows = snippet_candidate_suffixes(&max_buffer_window, &is_word_char)
1232                .take(
1233                    sorted_snippet_candidates
1234                        .first()
1235                        .map(|(_, _, word_count)| *word_count)
1236                        .unwrap_or_default(),
1237                )
1238                .collect_vec();
1239
1240            const MAX_RESULTS: usize = 100;
1241            // Each match also remembers how many characters from the buffer it consumed
1242            let mut matches: Vec<(StringMatch, usize)> = vec![];
1243
1244            let mut snippet_list_cutoff_index = 0;
1245            for (buffer_index, buffer_window) in buffer_windows.iter().enumerate().rev() {
1246                let word_count = buffer_index + 1;
1247                // Increase `snippet_list_cutoff_index` until we have all of the
1248                // snippets with sufficiently many words.
1249                while sorted_snippet_candidates
1250                    .get(snippet_list_cutoff_index)
1251                    .is_some_and(|(_ix, _prefix, snippet_word_count)| {
1252                        *snippet_word_count >= word_count
1253                    })
1254                {
1255                    snippet_list_cutoff_index += 1;
1256                }
1257
1258                // Take only the candidates with at least `word_count` many words
1259                let snippet_candidates_at_word_len =
1260                    &sorted_snippet_candidates[..snippet_list_cutoff_index];
1261
1262                let candidates = snippet_candidates_at_word_len
1263                    .iter()
1264                    .map(|(_snippet_ix, prefix, _snippet_word_count)| prefix)
1265                    .enumerate() // index in `sorted_snippet_candidates`
1266                    // First char must match
1267                    .filter(|(_ix, prefix)| {
1268                        itertools::equal(
1269                            prefix
1270                                .chars()
1271                                .next()
1272                                .into_iter()
1273                                .flat_map(|c| c.to_lowercase()),
1274                            buffer_window
1275                                .chars()
1276                                .next()
1277                                .into_iter()
1278                                .flat_map(|c| c.to_lowercase()),
1279                        )
1280                    })
1281                    .map(|(ix, prefix)| StringMatchCandidate::new(ix, prefix))
1282                    .collect::<Vec<StringMatchCandidate>>();
1283
1284                matches.extend(
1285                    fuzzy::match_strings(
1286                        &candidates,
1287                        &buffer_window,
1288                        buffer_window.chars().any(|c| c.is_uppercase()),
1289                        true,
1290                        MAX_RESULTS - matches.len(), // always prioritize longer snippets
1291                        &Default::default(),
1292                        executor.clone(),
1293                    )
1294                    .await
1295                    .into_iter()
1296                    .map(|string_match| (string_match, buffer_window.len())),
1297                );
1298
1299                if matches.len() >= MAX_RESULTS {
1300                    break;
1301                }
1302            }
1303
1304            let to_lsp = |point: &text::Anchor| {
1305                let end = text::ToPointUtf16::to_point_utf16(point, &snapshot);
1306                point_to_lsp(end)
1307            };
1308            let lsp_end = to_lsp(&buffer_anchor);
1309
1310            if matches.len() >= MAX_RESULTS {
1311                is_incomplete = true;
1312            }
1313
1314            completions.extend(matches.iter().map(|(string_match, buffer_window_len)| {
1315                let ((snippet_index, prefix_index), matching_prefix, _snippet_word_count) =
1316                    sorted_snippet_candidates[string_match.candidate_id];
1317                let snippet = &snippets[snippet_index];
1318                let start = buffer_offset - buffer_window_len;
1319                let start = snapshot.anchor_before(start);
1320                let range = start..buffer_anchor;
1321                let lsp_start = to_lsp(&start);
1322                let lsp_range = lsp::Range {
1323                    start: lsp_start,
1324                    end: lsp_end,
1325                };
1326                Completion {
1327                    replace_range: range,
1328                    new_text: snippet.body.clone(),
1329                    source: CompletionSource::Lsp {
1330                        insert_range: None,
1331                        server_id: LanguageServerId(usize::MAX),
1332                        resolved: true,
1333                        lsp_completion: Box::new(lsp::CompletionItem {
1334                            label: matching_prefix.clone(),
1335                            kind: Some(CompletionItemKind::SNIPPET),
1336                            label_details: snippet.description.as_ref().map(|description| {
1337                                lsp::CompletionItemLabelDetails {
1338                                    detail: Some(description.clone()),
1339                                    description: None,
1340                                }
1341                            }),
1342                            insert_text_format: Some(InsertTextFormat::SNIPPET),
1343                            text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
1344                                lsp::InsertReplaceEdit {
1345                                    new_text: snippet.body.clone(),
1346                                    insert: lsp_range,
1347                                    replace: lsp_range,
1348                                },
1349                            )),
1350                            filter_text: Some(snippet.body.clone()),
1351                            sort_text: Some(char::MAX.to_string()),
1352                            ..lsp::CompletionItem::default()
1353                        }),
1354                        lsp_defaults: None,
1355                    },
1356                    label: CodeLabel {
1357                        text: matching_prefix.clone(),
1358                        runs: Vec::new(),
1359                        filter_range: 0..matching_prefix.len(),
1360                    },
1361                    icon_path: None,
1362                    icon_color: None,
1363                    documentation: Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
1364                        single_line: snippet.name.clone().into(),
1365                        plain_text: snippet
1366                            .description
1367                            .clone()
1368                            .map(|description| description.into()),
1369                    }),
1370                    insert_text_mode: None,
1371                    confirm: None,
1372                    match_start: Some(start),
1373                    snippet_deduplication_key: Some((snippet_index, prefix_index)),
1374                    group: None,
1375                }
1376            }));
1377        }
1378
1379        Ok(CompletionResponse {
1380            completions,
1381            display_options: CompletionDisplayOptions::default(),
1382            is_incomplete,
1383        })
1384    })
1385}
1386
1387impl CompletionProvider for Entity<Project> {
1388    fn completions(
1389        &self,
1390        buffer: &Entity<Buffer>,
1391        buffer_position: text::Anchor,
1392        options: CompletionContext,
1393        _window: &mut Window,
1394        cx: &mut Context<Editor>,
1395    ) -> Task<Result<Vec<CompletionResponse>>> {
1396        self.update(cx, |project, cx| {
1397            let task = project.completions(buffer, buffer_position, options, cx);
1398            cx.background_spawn(task)
1399        })
1400    }
1401
1402    fn resolve_completions(
1403        &self,
1404        buffer: Entity<Buffer>,
1405        completion_indices: Vec<usize>,
1406        completions: Rc<RefCell<Box<[Completion]>>>,
1407        cx: &mut Context<Editor>,
1408    ) -> Task<Result<bool>> {
1409        self.update(cx, |project, cx| {
1410            project.lsp_store().update(cx, |lsp_store, cx| {
1411                lsp_store.resolve_completions(buffer, completion_indices, completions, cx)
1412            })
1413        })
1414    }
1415
1416    fn apply_additional_edits_for_completion(
1417        &self,
1418        buffer: Entity<Buffer>,
1419        completions: Rc<RefCell<Box<[Completion]>>>,
1420        completion_index: usize,
1421        push_to_history: bool,
1422        all_commit_ranges: Vec<Range<language::Anchor>>,
1423        cx: &mut Context<Editor>,
1424    ) -> Task<Result<Option<language::Transaction>>> {
1425        self.update(cx, |project, cx| {
1426            project.lsp_store().update(cx, |lsp_store, cx| {
1427                lsp_store.apply_additional_edits_for_completion(
1428                    buffer,
1429                    completions,
1430                    completion_index,
1431                    push_to_history,
1432                    all_commit_ranges,
1433                    cx,
1434                )
1435            })
1436        })
1437    }
1438
1439    fn is_completion_trigger(
1440        &self,
1441        buffer: &Entity<Buffer>,
1442        position: language::Anchor,
1443        text: &str,
1444        trigger_in_words: bool,
1445        cx: &mut Context<Editor>,
1446    ) -> bool {
1447        let mut chars = text.chars();
1448        let char = if let Some(char) = chars.next() {
1449            char
1450        } else {
1451            return false;
1452        };
1453        if chars.next().is_some() {
1454            return false;
1455        }
1456
1457        let buffer = buffer.read(cx);
1458        let snapshot = buffer.snapshot();
1459        let classifier = snapshot
1460            .char_classifier_at(position)
1461            .scope_context(Some(CharScopeContext::Completion));
1462        if trigger_in_words && classifier.is_word(char) {
1463            return true;
1464        }
1465
1466        buffer.completion_triggers().contains(text)
1467    }
1468
1469    fn show_snippets(&self) -> bool {
1470        true
1471    }
1472}
1473
1474pub(crate) fn split_words(text: &str) -> impl std::iter::Iterator<Item = &str> + '_ {
1475    let mut prev_index = 0;
1476    let mut prev_codepoint: Option<char> = None;
1477    text.char_indices()
1478        .chain([(text.len(), '\0')])
1479        .filter_map(move |(index, codepoint)| {
1480            let prev_codepoint = prev_codepoint.replace(codepoint)?;
1481            let is_boundary = index == text.len()
1482                || !prev_codepoint.is_uppercase() && codepoint.is_uppercase()
1483                || !prev_codepoint.is_alphanumeric() && codepoint.is_alphanumeric();
1484            if is_boundary {
1485                let chunk = &text[prev_index..index];
1486                prev_index = index;
1487                Some(chunk)
1488            } else {
1489                None
1490            }
1491        })
1492}
1493
1494/// Given a string of text immediately before the cursor, iterates over possible
1495/// strings a snippet could match to. More precisely: returns an iterator over
1496/// suffixes of `text` created by splitting at word boundaries (before & after
1497/// every non-word character).
1498///
1499/// Shorter suffixes are returned first.
1500pub(crate) fn snippet_candidate_suffixes<'a>(
1501    text: &'a str,
1502    is_word_char: &'a dyn Fn(char) -> bool,
1503) -> impl std::iter::Iterator<Item = &'a str> + 'a {
1504    let mut prev_index = text.len();
1505    let mut prev_codepoint = None;
1506    text.char_indices()
1507        .rev()
1508        .chain([(0, '\0')])
1509        .filter_map(move |(index, codepoint)| {
1510            let prev_index = std::mem::replace(&mut prev_index, index);
1511            let prev_codepoint = prev_codepoint.replace(codepoint)?;
1512            if is_word_char(prev_codepoint) && is_word_char(codepoint) {
1513                None
1514            } else {
1515                let chunk = &text[prev_index..]; // go to end of string
1516                Some(chunk)
1517            }
1518        })
1519}
1520
Served at tenant.openagents/omega Member data and write actions are omitted.