Skip to repository content1520 lines · 57.5 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T01:34:24.786Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
completions.rs
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