Skip to repository content2094 lines · 77.3 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:30:17.504Z 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
code_context_menus.rs
1use crate::scroll::ScrollAmount;
2use fuzzy::{StringMatch, StringMatchCandidate};
3use gpui::{
4 AnyElement, Entity, Focusable, FontWeight, HighlightStyle, ListSizingBehavior, ScrollHandle,
5 ScrollStrategy, SharedString, Size, StrikethroughStyle, StyledText, Task, TaskExt,
6 UniformListScrollHandle, div, px, uniform_list,
7};
8use itertools::Itertools;
9use language::CodeLabel;
10use language::{Buffer, LanguageName, LanguageRegistry};
11use lsp::{CompletionItemKind, CompletionItemTag};
12use markdown::{CopyButtonVisibility, Markdown, MarkdownElement};
13use multi_buffer::Anchor;
14use ordered_float::OrderedFloat;
15use project::lsp_store::CompletionDocumentation;
16use project::{CodeAction, Completion, CompletionGroup, TaskSourceKind};
17use project::{CompletionDisplayOptions, CompletionSource};
18use task::DebugScenario;
19use task::TaskContext;
20
21use std::sync::Arc;
22use std::sync::atomic::{AtomicBool, Ordering};
23use std::{
24 cell::RefCell,
25 cmp::{Reverse, min},
26 iter,
27 ops::Range,
28 rc::Rc,
29};
30use task::ResolvedTask;
31use ui::{
32 Divider, ListItem, ListSubHeader, Popover, ScrollAxes, Scrollbars, Tooltip, WithScrollbar,
33 prelude::*,
34};
35use util::ResultExt;
36
37use crate::hover_popover::{hover_markdown_style, open_markdown_url};
38use crate::{
39 CodeActionProvider, CompletionId, CompletionProvider, DisplayRow, Editor, EditorStyle,
40 ResolvedTasks,
41 actions::{ConfirmCodeAction, ConfirmCompletion},
42 split_words, styled_runs_for_code_label,
43};
44use crate::{CodeActionSource, EditorSettings};
45use collections::{HashSet, VecDeque};
46use settings::{CompletionDetailAlignment, CompletionMenuItemKind, Settings, SnippetSortOrder};
47
48pub const MENU_GAP: Pixels = px(4.);
49pub const MENU_ASIDE_X_PADDING: Pixels = px(16.);
50pub const MENU_ASIDE_MIN_WIDTH: Pixels = px(260.);
51pub const MENU_ASIDE_MAX_WIDTH: Pixels = px(500.);
52pub const COMPLETION_MENU_MIN_WIDTH: Pixels = px(280.);
53pub const COMPLETION_MENU_MAX_WIDTH: Pixels = px(540.);
54pub const CODE_ACTION_MENU_MIN_WIDTH: Pixels = px(220.);
55pub const CODE_ACTION_MENU_MAX_WIDTH: Pixels = px(540.);
56
57// Constants for the markdown cache. The purpose of this cache is to reduce flickering due to
58// documentation not yet being parsed.
59//
60// The size of the cache is set to 16, which is roughly 3 times more than the number of items
61// fetched around the current selection. This way documentation is more often ready for render when
62// revisiting previous entries, such as when pressing backspace.
63const MARKDOWN_CACHE_MAX_SIZE: usize = 16;
64const MARKDOWN_CACHE_BEFORE_ITEMS: usize = 2;
65const MARKDOWN_CACHE_AFTER_ITEMS: usize = 2;
66
67// Number of items beyond the visible items to resolve documentation.
68const RESOLVE_BEFORE_ITEMS: usize = 4;
69const RESOLVE_AFTER_ITEMS: usize = 4;
70
71#[derive(Clone, Debug)]
72pub enum CompletionMenuEntry {
73 Match(StringMatch),
74 Divider,
75 GroupHeader(SharedString),
76}
77
78impl CompletionMenuEntry {
79 pub fn as_match(&self) -> Option<&StringMatch> {
80 match self {
81 CompletionMenuEntry::Match(m) => Some(m),
82 CompletionMenuEntry::Divider | CompletionMenuEntry::GroupHeader(_) => None,
83 }
84 }
85
86 pub fn is_selectable(&self) -> bool {
87 matches!(self, CompletionMenuEntry::Match(_))
88 }
89}
90
91pub enum CodeContextMenu {
92 Completions(CompletionsMenu),
93 CodeActions(CodeActionsMenu),
94}
95
96impl CodeContextMenu {
97 pub fn select_first(
98 &mut self,
99 provider: Option<&dyn CompletionProvider>,
100 window: &mut Window,
101 cx: &mut Context<Editor>,
102 ) -> bool {
103 if self.visible() {
104 match self {
105 CodeContextMenu::Completions(menu) => menu.select_first(provider, window, cx),
106 CodeContextMenu::CodeActions(menu) => menu.select_first(cx),
107 }
108 true
109 } else {
110 false
111 }
112 }
113
114 pub fn select_prev(
115 &mut self,
116 provider: Option<&dyn CompletionProvider>,
117 window: &mut Window,
118 cx: &mut Context<Editor>,
119 ) -> bool {
120 if self.visible() {
121 match self {
122 CodeContextMenu::Completions(menu) => menu.select_prev(provider, window, cx),
123 CodeContextMenu::CodeActions(menu) => menu.select_prev(cx),
124 }
125 true
126 } else {
127 false
128 }
129 }
130
131 pub fn select_next(
132 &mut self,
133 provider: Option<&dyn CompletionProvider>,
134 window: &mut Window,
135 cx: &mut Context<Editor>,
136 ) -> bool {
137 if self.visible() {
138 match self {
139 CodeContextMenu::Completions(menu) => menu.select_next(provider, window, cx),
140 CodeContextMenu::CodeActions(menu) => menu.select_next(cx),
141 }
142 true
143 } else {
144 false
145 }
146 }
147
148 pub fn select_last(
149 &mut self,
150 provider: Option<&dyn CompletionProvider>,
151 window: &mut Window,
152 cx: &mut Context<Editor>,
153 ) -> bool {
154 if self.visible() {
155 match self {
156 CodeContextMenu::Completions(menu) => menu.select_last(provider, window, cx),
157 CodeContextMenu::CodeActions(menu) => menu.select_last(cx),
158 }
159 true
160 } else {
161 false
162 }
163 }
164
165 pub fn visible(&self) -> bool {
166 match self {
167 CodeContextMenu::Completions(menu) => menu.visible(),
168 CodeContextMenu::CodeActions(menu) => menu.visible(),
169 }
170 }
171
172 pub fn origin(&self) -> ContextMenuOrigin {
173 match self {
174 CodeContextMenu::Completions(menu) => menu.origin(),
175 CodeContextMenu::CodeActions(menu) => menu.origin(),
176 }
177 }
178
179 pub fn render(
180 &self,
181 style: &EditorStyle,
182 max_height_in_lines: u32,
183 window: &mut Window,
184 cx: &mut Context<Editor>,
185 ) -> AnyElement {
186 match self {
187 CodeContextMenu::Completions(menu) => {
188 menu.render(style, max_height_in_lines, window, cx)
189 }
190 CodeContextMenu::CodeActions(menu) => {
191 menu.render(style, max_height_in_lines, window, cx)
192 }
193 }
194 }
195
196 pub fn render_aside(
197 &mut self,
198 max_size: Size<Pixels>,
199 window: &mut Window,
200 cx: &mut Context<Editor>,
201 ) -> Option<AnyElement> {
202 match self {
203 CodeContextMenu::Completions(menu) => menu.render_aside(max_size, window, cx),
204 CodeContextMenu::CodeActions(menu) => menu.render_aside(max_size, window, cx),
205 }
206 }
207
208 pub fn focused(&self, window: &mut Window, cx: &mut Context<Editor>) -> bool {
209 match self {
210 CodeContextMenu::Completions(completions_menu) => completions_menu
211 .get_or_create_entry_markdown(completions_menu.selected_item, cx)
212 .as_ref()
213 .is_some_and(|markdown| markdown.focus_handle(cx).contains_focused(window, cx)),
214 CodeContextMenu::CodeActions(_) => false,
215 }
216 }
217
218 pub fn scroll_aside(
219 &mut self,
220 scroll_amount: ScrollAmount,
221 window: &mut Window,
222 cx: &mut Context<Editor>,
223 ) {
224 match self {
225 CodeContextMenu::Completions(completions_menu) => {
226 completions_menu.scroll_aside(scroll_amount, window, cx)
227 }
228 CodeContextMenu::CodeActions(_) => (),
229 }
230 }
231
232 pub fn primary_scroll_handle(&self) -> UniformListScrollHandle {
233 match self {
234 CodeContextMenu::Completions(menu) => menu.scroll_handle.clone(),
235 CodeContextMenu::CodeActions(menu) => menu.scroll_handle.clone(),
236 }
237 }
238}
239
240pub enum ContextMenuOrigin {
241 Cursor,
242 GutterIndicator(DisplayRow),
243 QuickActionBar,
244}
245
246pub struct CompletionsMenu {
247 pub id: CompletionId,
248 pub source: CompletionsMenuSource,
249 sort_completions: bool,
250 pub initial_position: Anchor,
251 pub initial_query: Option<Arc<String>>,
252 pub is_incomplete: bool,
253 pub buffer: Entity<Buffer>,
254 pub completions: Rc<RefCell<Box<[Completion]>>>,
255 /// String match candidate for each completion, grouped by `match_start`.
256 match_candidates: Arc<[(Option<text::Anchor>, Vec<StringMatchCandidate>)]>,
257 /// Entries displayed in the menu, which is a filtered and sorted subset of `match_candidates`.
258 pub entries: Rc<RefCell<Box<[CompletionMenuEntry]>>>,
259 pub selected_item: usize,
260 filter_task: Task<()>,
261 cancel_filter: Arc<AtomicBool>,
262 scroll_handle: UniformListScrollHandle,
263 // The `ScrollHandle` used on the Markdown documentation rendered on the
264 // side of the completions menu.
265 pub scroll_handle_aside: ScrollHandle,
266 resolve_completions: bool,
267 show_completion_documentation: bool,
268 last_rendered_range: Rc<RefCell<Option<Range<usize>>>>,
269 markdown_cache: Rc<RefCell<VecDeque<(MarkdownCacheKey, Entity<Markdown>)>>>,
270 language_registry: Option<Arc<LanguageRegistry>>,
271 language: Option<LanguageName>,
272 display_options: CompletionDisplayOptions,
273 snippet_sort_order: SnippetSortOrder,
274}
275
276#[derive(Clone, Debug, PartialEq)]
277enum MarkdownCacheKey {
278 ForCandidate {
279 candidate_id: usize,
280 },
281 ForCompletionMatch {
282 new_text: String,
283 markdown_source: SharedString,
284 },
285}
286
287#[derive(Copy, Clone, Debug, PartialEq, Eq)]
288pub enum CompletionsMenuSource {
289 /// Show all completions (words, snippets, LSP)
290 Normal,
291 /// Show only snippets (not words or LSP)
292 ///
293 /// Used after typing a non-word character
294 SnippetsOnly,
295 /// Tab stops within a snippet that have a predefined finite set of choices
296 SnippetChoices,
297 /// Show only words (not snippets or LSP)
298 ///
299 /// Used when word completions are explicitly triggered
300 Words { ignore_threshold: bool },
301}
302
303// TODO: There should really be a wrapper around fuzzy match tasks that does this.
304impl Drop for CompletionsMenu {
305 fn drop(&mut self) {
306 self.cancel_filter.store(true, Ordering::Relaxed);
307 }
308}
309
310#[derive(Default)]
311struct CompletionMenuScrollBarSetting;
312
313impl ui::scrollbars::ScrollbarVisibility for CompletionMenuScrollBarSetting {
314 fn visibility(&self, cx: &App) -> ui::scrollbars::ShowScrollbar {
315 EditorSettings::get_global(cx).completion_menu_scrollbar
316 }
317}
318
319impl CompletionsMenu {
320 pub fn new(
321 id: CompletionId,
322 source: CompletionsMenuSource,
323 sort_completions: bool,
324 show_completion_documentation: bool,
325 initial_position: Anchor,
326 initial_query: Option<Arc<String>>,
327 is_incomplete: bool,
328 buffer: Entity<Buffer>,
329 completions: Box<[Completion]>,
330 scroll_handle: Option<UniformListScrollHandle>,
331 display_options: CompletionDisplayOptions,
332 snippet_sort_order: SnippetSortOrder,
333 language_registry: Option<Arc<LanguageRegistry>>,
334 language: Option<LanguageName>,
335 cx: &mut Context<Editor>,
336 ) -> Self {
337 let match_candidates = completions
338 .iter()
339 .enumerate()
340 .map(|(id, completion)| StringMatchCandidate::new(id, completion.label.filter_text()))
341 .into_group_map_by(|candidate| completions[candidate.id].match_start)
342 .into_iter()
343 .collect();
344
345 let completions_menu = Self {
346 id,
347 source,
348 sort_completions,
349 initial_position,
350 initial_query,
351 is_incomplete,
352 buffer,
353 show_completion_documentation,
354 completions: RefCell::new(completions).into(),
355 match_candidates,
356 entries: Rc::new(RefCell::new(Box::new([]))),
357 selected_item: 0,
358 filter_task: Task::ready(()),
359 cancel_filter: Arc::new(AtomicBool::new(false)),
360 scroll_handle: scroll_handle.unwrap_or_else(UniformListScrollHandle::new),
361 scroll_handle_aside: ScrollHandle::new(),
362 resolve_completions: true,
363 last_rendered_range: RefCell::new(None).into(),
364 markdown_cache: RefCell::new(VecDeque::new()).into(),
365 language_registry,
366 language,
367 display_options,
368 snippet_sort_order,
369 };
370
371 completions_menu.start_markdown_parse_for_nearby_entries(cx);
372
373 completions_menu
374 }
375
376 pub fn new_snippet_choices(
377 id: CompletionId,
378 sort_completions: bool,
379 choices: &Vec<String>,
380 initial_position: Anchor,
381 selection: Range<text::Anchor>,
382 buffer: Entity<Buffer>,
383 scroll_handle: Option<UniformListScrollHandle>,
384 snippet_sort_order: SnippetSortOrder,
385 ) -> Self {
386 let completions = choices
387 .iter()
388 .map(|choice| Completion {
389 replace_range: selection.clone(),
390 new_text: choice.to_string(),
391 label: CodeLabel::plain(choice.to_string(), None),
392 match_start: None,
393 snippet_deduplication_key: None,
394 icon_path: None,
395 icon_color: None,
396 documentation: None,
397 confirm: None,
398 insert_text_mode: None,
399 source: CompletionSource::Custom,
400 group: None,
401 })
402 .collect();
403
404 let match_candidates = Arc::new([(
405 None,
406 choices
407 .iter()
408 .enumerate()
409 .map(|(id, completion)| StringMatchCandidate::new(id, completion))
410 .collect(),
411 )]);
412 let entries = choices
413 .iter()
414 .enumerate()
415 .map(|(id, completion)| {
416 CompletionMenuEntry::Match(StringMatch {
417 candidate_id: id,
418 score: 1.,
419 positions: vec![],
420 string: completion.clone(),
421 })
422 })
423 .collect();
424 Self {
425 id,
426 source: CompletionsMenuSource::SnippetChoices,
427 sort_completions,
428 initial_position,
429 initial_query: None,
430 is_incomplete: false,
431 buffer,
432 completions: RefCell::new(completions).into(),
433 match_candidates,
434 entries: RefCell::new(entries).into(),
435 selected_item: 0,
436 filter_task: Task::ready(()),
437 cancel_filter: Arc::new(AtomicBool::new(false)),
438 scroll_handle: scroll_handle.unwrap_or_else(UniformListScrollHandle::new),
439 scroll_handle_aside: ScrollHandle::new(),
440 resolve_completions: false,
441 show_completion_documentation: false,
442 last_rendered_range: RefCell::new(None).into(),
443 markdown_cache: RefCell::new(VecDeque::new()).into(),
444 language_registry: None,
445 language: None,
446 display_options: CompletionDisplayOptions::default(),
447 snippet_sort_order,
448 }
449 }
450
451 fn select_first(
452 &mut self,
453 provider: Option<&dyn CompletionProvider>,
454 window: &mut Window,
455 cx: &mut Context<Editor>,
456 ) {
457 let entries = self.entries.borrow();
458 if entries.is_empty() {
459 return;
460 }
461 let start = if self.scroll_handle.y_flipped() {
462 entries.len() - 1
463 } else {
464 0
465 };
466 drop(entries);
467 let index = self.find_selectable_entry(start, !self.scroll_handle.y_flipped());
468 if let Some(index) = index {
469 self.update_selection_index(index, provider, window, cx);
470 }
471 }
472
473 fn select_last(
474 &mut self,
475 provider: Option<&dyn CompletionProvider>,
476 window: &mut Window,
477 cx: &mut Context<Editor>,
478 ) {
479 let entries = self.entries.borrow();
480 if entries.is_empty() {
481 return;
482 }
483 let start = if self.scroll_handle.y_flipped() {
484 0
485 } else {
486 entries.len() - 1
487 };
488 drop(entries);
489 let index = self.find_selectable_entry(start, self.scroll_handle.y_flipped());
490 if let Some(index) = index {
491 self.update_selection_index(index, provider, window, cx);
492 }
493 }
494
495 fn select_prev(
496 &mut self,
497 provider: Option<&dyn CompletionProvider>,
498 window: &mut Window,
499 cx: &mut Context<Editor>,
500 ) {
501 let index = if self.scroll_handle.y_flipped() {
502 self.next_match_index()
503 } else {
504 self.prev_match_index()
505 };
506 self.update_selection_index(index, provider, window, cx);
507 }
508
509 fn select_next(
510 &mut self,
511 provider: Option<&dyn CompletionProvider>,
512 window: &mut Window,
513 cx: &mut Context<Editor>,
514 ) {
515 let index = if self.scroll_handle.y_flipped() {
516 self.prev_match_index()
517 } else {
518 self.next_match_index()
519 };
520 self.update_selection_index(index, provider, window, cx);
521 }
522
523 fn update_selection_index(
524 &mut self,
525 match_index: usize,
526 provider: Option<&dyn CompletionProvider>,
527 window: &mut Window,
528 cx: &mut Context<Editor>,
529 ) {
530 if self.selected_item != match_index {
531 self.selected_item = match_index;
532 self.handle_selection_changed(provider, window, cx);
533 }
534 }
535
536 fn prev_match_index(&self) -> usize {
537 let entries = self.entries.borrow();
538 let len = entries.len();
539 if len == 0 {
540 return 0;
541 }
542 let mut index = if self.selected_item > 0 {
543 self.selected_item - 1
544 } else {
545 len - 1
546 };
547 let start = index;
548 loop {
549 if entries[index].is_selectable() {
550 return index;
551 }
552 index = if index > 0 { index - 1 } else { len - 1 };
553 if index == start {
554 return self.selected_item;
555 }
556 }
557 }
558
559 fn next_match_index(&self) -> usize {
560 let entries = self.entries.borrow();
561 let len = entries.len();
562 if len == 0 {
563 return 0;
564 }
565 let mut index = if self.selected_item + 1 < len {
566 self.selected_item + 1
567 } else {
568 0
569 };
570 let start = index;
571 loop {
572 if entries[index].is_selectable() {
573 return index;
574 }
575 index = if index + 1 < len { index + 1 } else { 0 };
576 if index == start {
577 return self.selected_item;
578 }
579 }
580 }
581
582 fn find_selectable_entry(&self, start: usize, forward: bool) -> Option<usize> {
583 let entries = self.entries.borrow();
584 let len = entries.len();
585 if len == 0 {
586 return None;
587 }
588 let mut index = start;
589 loop {
590 if entries[index].is_selectable() {
591 return Some(index);
592 }
593 if forward {
594 index = if index + 1 < len { index + 1 } else { 0 };
595 } else {
596 index = if index > 0 { index - 1 } else { len - 1 };
597 }
598 if index == start {
599 return None;
600 }
601 }
602 }
603
604 fn handle_selection_changed(
605 &mut self,
606 provider: Option<&dyn CompletionProvider>,
607 window: &mut Window,
608 cx: &mut Context<Editor>,
609 ) {
610 self.scroll_handle
611 .scroll_to_item(self.selected_item, ScrollStrategy::Nearest);
612 if let Some(provider) = provider {
613 let entries = self.entries.borrow();
614 let entry = if self.selected_item < entries.len() {
615 entries[self.selected_item].as_match()
616 } else {
617 None
618 };
619 provider.selection_changed(entry, window, cx);
620 }
621 self.resolve_visible_completions(provider, cx);
622 self.start_markdown_parse_for_nearby_entries(cx);
623 cx.notify();
624 }
625
626 pub fn resolve_visible_completions(
627 &mut self,
628 provider: Option<&dyn CompletionProvider>,
629 cx: &mut Context<Editor>,
630 ) {
631 if !self.resolve_completions {
632 return;
633 }
634 let Some(provider) = provider else {
635 return;
636 };
637
638 let entries = self.entries.borrow();
639 if entries.is_empty() {
640 return;
641 }
642 if self.selected_item >= entries.len() {
643 log::error!(
644 "bug: completion selected_item >= entries.len(): {} >= {}",
645 self.selected_item,
646 entries.len()
647 );
648 self.selected_item = entries.len() - 1;
649 }
650
651 // Attempt to resolve completions for every item that will be displayed. This matters
652 // because single line documentation may be displayed inline with the completion.
653 //
654 // When navigating to the very beginning or end of completions, `last_rendered_range` may
655 // have no overlap with the completions that will be displayed, so instead use a range based
656 // on the last rendered count.
657 const APPROXIMATE_VISIBLE_COUNT: usize = 12;
658 let last_rendered_range = self.last_rendered_range.borrow().clone();
659 let visible_count = last_rendered_range
660 .clone()
661 .map_or(APPROXIMATE_VISIBLE_COUNT, |range| range.count());
662 let entry_range = if self.selected_item == 0 {
663 0..min(visible_count, entries.len())
664 } else if self.selected_item == entries.len() - 1 {
665 entries.len().saturating_sub(visible_count)..entries.len()
666 } else {
667 last_rendered_range.map_or(0..0, |range| {
668 min(range.start, entries.len())..min(range.end, entries.len())
669 })
670 };
671
672 // Expand the range to resolve more completions than are predicted to be visible, to reduce
673 // jank on navigation.
674 let entry_indices = util::expanded_and_wrapped_usize_range(
675 entry_range,
676 RESOLVE_BEFORE_ITEMS,
677 RESOLVE_AFTER_ITEMS,
678 entries.len(),
679 );
680
681 // Avoid work by sometimes filtering out completions that already have documentation.
682 // This filtering doesn't happen if the completions are currently being updated.
683 let completions = self.completions.borrow();
684 let candidate_ids = entry_indices
685 .filter_map(|i| entries[i].as_match().map(|m| m.candidate_id))
686 .filter(|i| completions[*i].documentation.is_none());
687
688 // Current selection is always resolved even if it already has documentation, to handle
689 // out-of-spec language servers that return more results later.
690 let Some(selected_candidate_id) = entries[self.selected_item]
691 .as_match()
692 .map(|m| m.candidate_id)
693 else {
694 drop(entries);
695 drop(completions);
696 return;
697 };
698 let candidate_ids = iter::once(selected_candidate_id)
699 .chain(candidate_ids.filter(|id| *id != selected_candidate_id))
700 .collect::<Vec<usize>>();
701 drop(entries);
702
703 if candidate_ids.is_empty() {
704 return;
705 }
706
707 let resolve_task = provider.resolve_completions(
708 self.buffer.clone(),
709 candidate_ids,
710 self.completions.clone(),
711 cx,
712 );
713
714 let completion_id = self.id;
715 cx.spawn(async move |editor, cx| {
716 if let Some(true) = resolve_task.await.log_err() {
717 editor
718 .update(cx, |editor, cx| {
719 // `resolve_completions` modified state affecting display.
720 cx.notify();
721 editor.with_completions_menu_matching_id(completion_id, |menu| {
722 if let Some(menu) = menu {
723 menu.start_markdown_parse_for_nearby_entries(cx)
724 }
725 });
726 })
727 .ok();
728 }
729 })
730 .detach();
731 }
732
733 fn start_markdown_parse_for_nearby_entries(&self, cx: &mut Context<Editor>) {
734 // Enqueue parse tasks of nearer items first.
735 //
736 // TODO: This means that the nearer items will actually be further back in the cache, which
737 // is not ideal. In practice this is fine because `get_or_create_markdown` moves the current
738 // selection to the front (when `is_render = true`).
739 let entry_indices = util::wrapped_usize_outward_from(
740 self.selected_item,
741 MARKDOWN_CACHE_BEFORE_ITEMS,
742 MARKDOWN_CACHE_AFTER_ITEMS,
743 self.entries.borrow().len(),
744 );
745
746 for index in entry_indices {
747 self.get_or_create_entry_markdown(index, cx);
748 }
749 }
750
751 fn get_or_create_entry_markdown(
752 &self,
753 index: usize,
754 cx: &mut Context<Editor>,
755 ) -> Option<Entity<Markdown>> {
756 let entries = self.entries.borrow();
757 if index >= entries.len() {
758 return None;
759 }
760 let candidate_id = entries[index].as_match()?.candidate_id;
761 let completions = self.completions.borrow();
762 match &completions[candidate_id].documentation {
763 Some(CompletionDocumentation::MultiLineMarkdown(source)) if !source.is_empty() => self
764 .get_or_create_markdown(candidate_id, Some(source), false, &completions, cx)
765 .map(|(_, markdown)| markdown),
766 Some(_) => None,
767 _ => None,
768 }
769 }
770
771 fn get_or_create_markdown(
772 &self,
773 candidate_id: usize,
774 source: Option<&SharedString>,
775 is_render: bool,
776 completions: &[Completion],
777 cx: &mut Context<Editor>,
778 ) -> Option<(bool, Entity<Markdown>)> {
779 let mut markdown_cache = self.markdown_cache.borrow_mut();
780
781 let mut has_completion_match_cache_entry = false;
782 let mut matching_entry = markdown_cache.iter().find_position(|(key, _)| match key {
783 MarkdownCacheKey::ForCandidate { candidate_id: id } => *id == candidate_id,
784 MarkdownCacheKey::ForCompletionMatch { .. } => {
785 has_completion_match_cache_entry = true;
786 false
787 }
788 });
789
790 if has_completion_match_cache_entry && matching_entry.is_none() {
791 if let Some(source) = source {
792 matching_entry = markdown_cache.iter().find_position(|(key, _)| {
793 matches!(key, MarkdownCacheKey::ForCompletionMatch { markdown_source, .. }
794 if markdown_source == source)
795 });
796 } else {
797 // Heuristic guess that documentation can be reused when new_text matches. This is
798 // to mitigate documentation flicker while typing. If this is wrong, then resolution
799 // should cause the correct documentation to be displayed soon.
800 let completion = &completions[candidate_id];
801 matching_entry = markdown_cache.iter().find_position(|(key, _)| {
802 matches!(key, MarkdownCacheKey::ForCompletionMatch { new_text, .. }
803 if new_text == &completion.new_text)
804 });
805 }
806 }
807
808 if let Some((cache_index, (key, markdown))) = matching_entry {
809 let markdown = markdown.clone();
810
811 // Since the markdown source matches, the key can now be ForCandidate.
812 if source.is_some() && matches!(key, MarkdownCacheKey::ForCompletionMatch { .. }) {
813 markdown_cache[cache_index].0 = MarkdownCacheKey::ForCandidate { candidate_id };
814 }
815
816 if is_render && cache_index != 0 {
817 // Move the current selection's cache entry to the front.
818 markdown_cache.rotate_right(1);
819 let cache_len = markdown_cache.len();
820 markdown_cache.swap(0, (cache_index + 1) % cache_len);
821 }
822
823 let is_parsing = markdown.update(cx, |markdown, cx| {
824 if let Some(source) = source {
825 // `reset` is called as it's possible for documentation to change due to resolve
826 // requests. It does nothing if `source` is unchanged.
827 markdown.reset(source.clone(), cx);
828 }
829 markdown.is_parsing()
830 });
831 return Some((is_parsing, markdown));
832 }
833
834 let Some(source) = source else {
835 // Can't create markdown as there is no source.
836 return None;
837 };
838
839 if markdown_cache.len() < MARKDOWN_CACHE_MAX_SIZE {
840 let markdown = cx.new(|cx| {
841 Markdown::new(
842 source.clone(),
843 self.language_registry.clone(),
844 self.language.clone(),
845 cx,
846 )
847 });
848 // Handles redraw when the markdown is done parsing. The current render is for a
849 // deferred draw, and so without this did not redraw when `markdown` notified.
850 cx.observe(&markdown, |_, _, cx| cx.notify()).detach();
851 markdown_cache.push_front((
852 MarkdownCacheKey::ForCandidate { candidate_id },
853 markdown.clone(),
854 ));
855 Some((true, markdown))
856 } else {
857 debug_assert_eq!(markdown_cache.capacity(), MARKDOWN_CACHE_MAX_SIZE);
858 // Moves the last cache entry to the start. The ring buffer is full, so this does no
859 // copying and just shifts indexes.
860 markdown_cache.rotate_right(1);
861 markdown_cache[0].0 = MarkdownCacheKey::ForCandidate { candidate_id };
862 let markdown = &markdown_cache[0].1;
863 markdown.update(cx, |markdown, cx| markdown.reset(source.clone(), cx));
864 Some((true, markdown.clone()))
865 }
866 }
867
868 pub fn visible(&self) -> bool {
869 self.entries.borrow().iter().any(|e| e.as_match().is_some())
870 }
871
872 fn origin(&self) -> ContextMenuOrigin {
873 ContextMenuOrigin::Cursor
874 }
875
876 fn render(
877 &self,
878 style: &EditorStyle,
879 max_height_in_lines: u32,
880 window: &mut Window,
881 cx: &mut Context<Editor>,
882 ) -> AnyElement {
883 let show_completion_documentation = self.show_completion_documentation;
884 let editor_settings = EditorSettings::get_global(cx);
885 let completion_detail_alignment = editor_settings.completion_detail_alignment;
886 let completion_menu_item_kind = editor_settings.completion_menu_item_kind;
887 let widest_completion_ix = if self.display_options.dynamic_width {
888 let completions = self.completions.borrow();
889 let widest_completion_ix = self
890 .entries
891 .borrow()
892 .iter()
893 .enumerate()
894 .filter_map(|(ix, entry)| entry.as_match().map(|m| (ix, m)))
895 .max_by_key(|(_, mat)| {
896 let completion = &completions[mat.candidate_id];
897 let documentation = &completion.documentation;
898
899 let mut len = completion.label.text.chars().count();
900 if show_completion_documentation {
901 match documentation {
902 Some(CompletionDocumentation::SingleLine(text)) => {
903 len += text.chars().count();
904 }
905 Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
906 single_line,
907 ..
908 }) => {
909 len += single_line.chars().count();
910 }
911 _ => {}
912 }
913 }
914
915 len
916 })
917 .map(|(ix, _)| ix);
918 drop(completions);
919 widest_completion_ix
920 } else {
921 None
922 };
923
924 let selected_item = self.selected_item;
925 let completions = self.completions.clone();
926 let entries = self.entries.clone();
927 let last_rendered_range = self.last_rendered_range.clone();
928 let style = style.clone();
929 let list = uniform_list(
930 "completions",
931 self.entries.borrow().len(),
932 cx.processor(move |_editor, range: Range<usize>, _window, cx| {
933 last_rendered_range.borrow_mut().replace(range.clone());
934 let start_ix = range.start;
935 let completions_guard = completions.borrow_mut();
936
937 entries.borrow()[range]
938 .iter()
939 .enumerate()
940 .map(|(ix, entry)| {
941 let item_ix = start_ix + ix;
942
943 let Some(mat) = entry.as_match() else {
944 return match entry {
945 CompletionMenuEntry::GroupHeader(label) => div()
946 .child(ListSubHeader::new(label.clone()).inset(true))
947 .into_any_element(),
948 CompletionMenuEntry::Divider => h_flex()
949 .flex_1()
950 .size_full()
951 .child(Divider::horizontal())
952 .into_any_element(),
953 CompletionMenuEntry::Match(_) => unreachable!(),
954 };
955 };
956
957 let completion = &completions_guard[mat.candidate_id];
958 let documentation = if show_completion_documentation {
959 &completion.documentation
960 } else {
961 &None
962 };
963
964 let filter_start = completion.label.filter_range.start;
965
966 let highlights = gpui::combine_highlights(
967 mat.ranges().map(|range| {
968 (
969 filter_start + range.start..filter_start + range.end,
970 FontWeight::BOLD.into(),
971 )
972 }),
973 styled_runs_for_code_label(
974 &completion.label,
975 &style.syntax,
976 &style.local_player,
977 )
978 .map(|(range, mut highlight)| {
979 // Ignore font weight for syntax highlighting, as we'll use it
980 // for fuzzy matches.
981 highlight.font_weight = None;
982 if completion
983 .source
984 .lsp_completion(false)
985 .and_then(|lsp_completion| {
986 match (lsp_completion.deprecated, &lsp_completion.tags) {
987 (Some(true), _) => Some(true),
988 (_, Some(tags)) => {
989 Some(tags.contains(&CompletionItemTag::DEPRECATED))
990 }
991 _ => None,
992 }
993 })
994 .unwrap_or(false)
995 {
996 highlight.strikethrough = Some(StrikethroughStyle {
997 thickness: 1.0.into(),
998 ..Default::default()
999 });
1000 highlight.color = Some(cx.theme().colors().text_muted);
1001 }
1002
1003 (range, highlight)
1004 }),
1005 );
1006
1007 let highlights: Vec<_> = highlights.collect();
1008
1009 let ((main_text, main_highlights), (suffix_text, suffix_highlights)) =
1010 split_completion_label(
1011 &completion.label.text,
1012 &completion.label.filter_range,
1013 &highlights,
1014 );
1015 let main_label = StyledText::new(main_text.to_string())
1016 .with_default_highlights(&style.text, main_highlights);
1017
1018 let suffix_label = if !suffix_text.is_empty() {
1019 Some(
1020 StyledText::new(suffix_text.to_string())
1021 .with_default_highlights(&style.text, suffix_highlights),
1022 )
1023 } else {
1024 None
1025 };
1026
1027 let left_aligned_suffix =
1028 matches!(completion_detail_alignment, CompletionDetailAlignment::Left);
1029
1030 let right_aligned_suffix = matches!(
1031 completion_detail_alignment,
1032 CompletionDetailAlignment::Right,
1033 );
1034
1035 let documentation_label = match documentation {
1036 Some(CompletionDocumentation::SingleLine(text))
1037 | Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
1038 single_line: text,
1039 ..
1040 }) => {
1041 if text.trim().is_empty() {
1042 None
1043 } else {
1044 Some(
1045 Label::new(text.trim().to_string())
1046 .ml_4()
1047 .size(LabelSize::Small)
1048 .color(Color::Muted),
1049 )
1050 }
1051 }
1052 _ => None,
1053 };
1054
1055 let icon_or_color_slot = completion
1056 .color()
1057 .map(|color| {
1058 div()
1059 .flex_shrink_0()
1060 .size_3p5()
1061 .rounded_xs()
1062 .bg(color)
1063 .into_any_element()
1064 })
1065 .or_else(|| {
1066 completion.icon_path.as_ref().map(|path| {
1067 Icon::from_path(path)
1068 .size(IconSize::XSmall)
1069 .color(
1070 completion
1071 .icon_color
1072 .map_or(Color::Muted, Color::Custom),
1073 )
1074 .into_any_element()
1075 })
1076 });
1077
1078 let kind_letter_slot = match completion_menu_item_kind {
1079 CompletionMenuItemKind::Off => None,
1080 CompletionMenuItemKind::Symbol => Some(render_completion_kind_letter(
1081 completion.kind(),
1082 item_ix,
1083 &style,
1084 )),
1085 };
1086
1087 let start_slot = match (kind_letter_slot, icon_or_color_slot) {
1088 (Some(letter), Some(icon_or_color)) => Some(
1089 h_flex()
1090 .gap_0p5()
1091 .child(letter)
1092 .child(icon_or_color)
1093 .into_any_element(),
1094 ),
1095 (Some(letter), None) => Some(letter),
1096 (None, slot) => slot,
1097 };
1098
1099 div()
1100 .min_w(COMPLETION_MENU_MIN_WIDTH)
1101 .max_w(COMPLETION_MENU_MAX_WIDTH)
1102 .child(
1103 ListItem::new(mat.candidate_id)
1104 .inset(true)
1105 .toggle_state(item_ix == selected_item)
1106 .on_click(cx.listener(move |editor, _event, window, cx| {
1107 cx.stop_propagation();
1108 if let Some(task) = editor.confirm_completion(
1109 &ConfirmCompletion {
1110 item_ix: Some(item_ix),
1111 },
1112 window,
1113 cx,
1114 ) {
1115 task.detach_and_log_err(cx)
1116 }
1117 }))
1118 .start_slot::<AnyElement>(start_slot)
1119 .child(
1120 h_flex()
1121 .min_w_0()
1122 .w_full()
1123 .when(left_aligned_suffix, |this| this.justify_start())
1124 .when(right_aligned_suffix, |this| {
1125 this.justify_between()
1126 })
1127 .child(
1128 div()
1129 .flex_none()
1130 .whitespace_nowrap()
1131 .child(main_label),
1132 )
1133 .when_some(suffix_label, |this, suffix| {
1134 this.child(div().truncate().child(suffix))
1135 }),
1136 )
1137 .end_slot::<Label>(documentation_label),
1138 )
1139 .into_any_element()
1140 })
1141 .collect()
1142 }),
1143 )
1144 .occlude()
1145 .max_h(max_height_in_lines as f32 * window.line_height())
1146 .track_scroll(&self.scroll_handle)
1147 .with_sizing_behavior(ListSizingBehavior::Infer)
1148 .map(|this| {
1149 if self.display_options.dynamic_width {
1150 this.with_width_from_item(widest_completion_ix)
1151 } else {
1152 this.w(rems(34.))
1153 }
1154 });
1155
1156 Popover::new()
1157 .child(
1158 div().child(list).custom_scrollbars(
1159 Scrollbars::for_settings::<CompletionMenuScrollBarSetting>()
1160 .show_along(ScrollAxes::Vertical)
1161 .tracked_scroll_handle(&self.scroll_handle),
1162 window,
1163 cx,
1164 ),
1165 )
1166 .into_any_element()
1167 }
1168
1169 fn render_aside(
1170 &mut self,
1171 max_size: Size<Pixels>,
1172 window: &mut Window,
1173 cx: &mut Context<Editor>,
1174 ) -> Option<AnyElement> {
1175 if !self.show_completion_documentation {
1176 return None;
1177 }
1178
1179 let entries = self.entries.borrow();
1180 let Some(mat) = entries[self.selected_item].as_match() else {
1181 return None;
1182 };
1183 let completions = self.completions.borrow();
1184 let multiline_docs = match completions[mat.candidate_id].documentation.as_ref() {
1185 Some(CompletionDocumentation::MultiLinePlainText(text)) => div().child(text.clone()),
1186 Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
1187 plain_text: Some(text),
1188 ..
1189 }) => div().child(text.clone()),
1190 Some(CompletionDocumentation::MultiLineMarkdown(source)) if !source.is_empty() => {
1191 let Some((false, markdown)) = self.get_or_create_markdown(
1192 mat.candidate_id,
1193 Some(source),
1194 true,
1195 &completions,
1196 cx,
1197 ) else {
1198 return None;
1199 };
1200 Self::render_markdown(markdown, window, cx)
1201 }
1202 None => {
1203 // Handle the case where documentation hasn't yet been resolved but there's a
1204 // `new_text` match in the cache.
1205 //
1206 // TODO: It's inconsistent that documentation caching based on matching `new_text`
1207 // only works for markdown. Consider generally caching the results of resolving
1208 // completions.
1209 let Some((false, markdown)) =
1210 self.get_or_create_markdown(mat.candidate_id, None, true, &completions, cx)
1211 else {
1212 return None;
1213 };
1214 Self::render_markdown(markdown, window, cx)
1215 }
1216 Some(CompletionDocumentation::MultiLineMarkdown(_)) => return None,
1217 Some(CompletionDocumentation::SingleLine(_)) => return None,
1218 Some(CompletionDocumentation::Undocumented) => return None,
1219 Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
1220 plain_text: None,
1221 ..
1222 }) => {
1223 return None;
1224 }
1225 };
1226
1227 Some(
1228 Popover::new()
1229 .child(
1230 multiline_docs
1231 .id("multiline_docs")
1232 .px(MENU_ASIDE_X_PADDING / 2.)
1233 .max_w(max_size.width)
1234 .max_h(max_size.height)
1235 .overflow_y_scroll()
1236 .track_scroll(&self.scroll_handle_aside)
1237 .occlude(),
1238 )
1239 .into_any_element(),
1240 )
1241 }
1242
1243 fn render_markdown(
1244 markdown: Entity<Markdown>,
1245 window: &mut Window,
1246 cx: &mut Context<Editor>,
1247 ) -> Div {
1248 let editor = cx.weak_entity();
1249 div().child(
1250 MarkdownElement::new(markdown, hover_markdown_style(window, cx))
1251 .code_block_renderer(markdown::CodeBlockRenderer::Default {
1252 copy_button_visibility: CopyButtonVisibility::Hidden,
1253 wrap_button_visibility: markdown::WrapButtonVisibility::Hidden,
1254 border: false,
1255 })
1256 .on_url_click(move |link, window, cx| {
1257 open_markdown_url(
1258 editor
1259 .read_with(cx, |editor, _| editor.workspace())
1260 .ok()
1261 .flatten(),
1262 link,
1263 window,
1264 cx,
1265 )
1266 }),
1267 )
1268 }
1269
1270 pub fn filter(
1271 &mut self,
1272 query: Arc<String>,
1273 query_end: text::Anchor,
1274 buffer: &Entity<Buffer>,
1275 provider: Option<Rc<dyn CompletionProvider>>,
1276 window: &mut Window,
1277 cx: &mut Context<Editor>,
1278 ) {
1279 self.cancel_filter.store(true, Ordering::Relaxed);
1280 self.cancel_filter = Arc::new(AtomicBool::new(false));
1281 let matches = self.do_async_filtering(query, query_end, buffer, cx);
1282 let id = self.id;
1283 self.filter_task = cx.spawn_in(window, async move |editor, cx| {
1284 let matches = matches.await;
1285 editor
1286 .update_in(cx, |editor, window, cx| {
1287 editor.with_completions_menu_matching_id(id, |this| {
1288 if let Some(this) = this {
1289 this.set_filter_results(matches, provider, window, cx);
1290 }
1291 });
1292 })
1293 .ok();
1294 });
1295 }
1296
1297 pub fn do_async_filtering(
1298 &self,
1299 query: Arc<String>,
1300 query_end: text::Anchor,
1301 buffer: &Entity<Buffer>,
1302 cx: &Context<Editor>,
1303 ) -> Task<Vec<StringMatch>> {
1304 let buffer_snapshot = buffer.read(cx).snapshot();
1305 let background_executor = cx.background_executor().clone();
1306 let match_candidates = self.match_candidates.clone();
1307 let cancel_filter = self.cancel_filter.clone();
1308 let default_query = query.clone();
1309
1310 let matches_task = cx.background_spawn(async move {
1311 let queries_and_candidates = match_candidates
1312 .iter()
1313 .map(|(query_start, candidates)| {
1314 let query_for_batch = match query_start {
1315 Some(start) => {
1316 Arc::new(buffer_snapshot.text_for_range(*start..query_end).collect())
1317 }
1318 None => default_query.clone(),
1319 };
1320 (query_for_batch, candidates)
1321 })
1322 .collect_vec();
1323
1324 let mut results = vec![];
1325 for (query, match_candidates) in queries_and_candidates {
1326 results.extend(
1327 fuzzy::match_strings(
1328 &match_candidates,
1329 &query,
1330 query.chars().any(|c| c.is_uppercase()),
1331 false,
1332 1000,
1333 &cancel_filter,
1334 background_executor.clone(),
1335 )
1336 .await,
1337 );
1338 }
1339 results
1340 });
1341
1342 let completions = self.completions.clone();
1343 let sort_completions = self.sort_completions;
1344 let snippet_sort_order = self.snippet_sort_order;
1345 cx.foreground_executor().spawn(async move {
1346 let mut matches = matches_task.await;
1347
1348 let completions_ref = completions.borrow();
1349
1350 if sort_completions {
1351 matches = Self::sort_string_matches(
1352 matches,
1353 Some(&query), // used for non-snippets only
1354 snippet_sort_order,
1355 &completions_ref,
1356 );
1357 }
1358
1359 // Remove duplicate snippet prefixes (e.g., "cool code" will match
1360 // the text "c c" in two places; we should only show the longer one)
1361 let mut snippets_seen = HashSet::<(usize, usize)>::default();
1362 matches.retain(|result| {
1363 match completions_ref[result.candidate_id].snippet_deduplication_key {
1364 Some(key) => snippets_seen.insert(key),
1365 None => true,
1366 }
1367 });
1368
1369 matches
1370 })
1371 }
1372
1373 pub fn set_filter_results(
1374 &mut self,
1375 matches: Vec<StringMatch>,
1376 provider: Option<Rc<dyn CompletionProvider>>,
1377 window: &mut Window,
1378 cx: &mut Context<Editor>,
1379 ) {
1380 let completions = self.completions.borrow();
1381 let mut entries: Vec<CompletionMenuEntry> = Vec::with_capacity(matches.len());
1382 let mut last_group: Option<&CompletionGroup> = None;
1383 for mat in matches {
1384 let group = completions[mat.candidate_id].group.as_ref();
1385 if group != last_group {
1386 if group.is_some() || last_group.is_some() {
1387 if !entries.is_empty() {
1388 entries.push(CompletionMenuEntry::Divider);
1389 }
1390 if let Some(label) = group.and_then(|g| g.label.as_ref()) {
1391 entries.push(CompletionMenuEntry::GroupHeader(label.clone()));
1392 }
1393 }
1394 last_group = group;
1395 }
1396 entries.push(CompletionMenuEntry::Match(mat));
1397 }
1398 drop(completions);
1399 *self.entries.borrow_mut() = entries.into_boxed_slice();
1400 self.selected_item = self.find_selectable_entry(0, true).unwrap_or(0);
1401 self.handle_selection_changed(provider.as_deref(), window, cx);
1402 }
1403
1404 pub fn sort_string_matches(
1405 matches: Vec<StringMatch>,
1406 query: Option<&str>,
1407 snippet_sort_order: SnippetSortOrder,
1408 completions: &[Completion],
1409 ) -> Vec<StringMatch> {
1410 let mut matches = matches;
1411
1412 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
1413 enum MatchTier<'a> {
1414 WordStartMatch {
1415 sort_exact: Reverse<i32>,
1416 sort_snippet: Reverse<i32>,
1417 sort_score: Reverse<OrderedFloat<f64>>,
1418 sort_positions: Vec<usize>,
1419 sort_exact_case_matches: Reverse<usize>,
1420 sort_text: Option<&'a str>,
1421 sort_kind: usize,
1422 sort_label: &'a str,
1423 },
1424 OtherMatch {
1425 sort_score: Reverse<OrderedFloat<f64>>,
1426 },
1427 }
1428
1429 let query_start_lower = query
1430 .as_ref()
1431 .and_then(|q| q.chars().next())
1432 .and_then(|c| c.to_lowercase().next());
1433
1434 if snippet_sort_order == SnippetSortOrder::None {
1435 matches
1436 .retain(|string_match| !completions[string_match.candidate_id].is_snippet_kind());
1437 }
1438
1439 matches.sort_unstable_by_key(|string_match| {
1440 let completion = &completions[string_match.candidate_id];
1441
1442 let sort_text = match &completion.source {
1443 CompletionSource::Lsp { lsp_completion, .. } => lsp_completion.sort_text.as_deref(),
1444 CompletionSource::Dap { sort_text } => Some(sort_text.as_str()),
1445 _ => None,
1446 };
1447
1448 let (sort_kind, sort_label) = completion.sort_key();
1449
1450 let score = string_match.score;
1451 let sort_score = Reverse(OrderedFloat(score));
1452
1453 // Snippets do their own first-letter matching logic elsewhere.
1454 let is_snippet = completion.is_snippet_kind();
1455 let query_start_doesnt_match_split_words = !is_snippet
1456 && query_start_lower
1457 .map(|query_char| {
1458 !split_words(&string_match.string).any(|word| {
1459 word.chars().next().and_then(|c| c.to_lowercase().next())
1460 == Some(query_char)
1461 })
1462 })
1463 .unwrap_or(false);
1464
1465 if query_start_doesnt_match_split_words {
1466 MatchTier::OtherMatch { sort_score }
1467 } else {
1468 let sort_snippet = match snippet_sort_order {
1469 SnippetSortOrder::Top => Reverse(if is_snippet { 1 } else { 0 }),
1470 SnippetSortOrder::Bottom => Reverse(if is_snippet { 0 } else { 1 }),
1471 SnippetSortOrder::Inline => Reverse(0),
1472 SnippetSortOrder::None => Reverse(0),
1473 };
1474 let sort_positions = string_match.positions.clone();
1475 let sort_exact_case_matches = Reverse(exact_case_match_count(
1476 query.unwrap_or_default(),
1477 string_match,
1478 ));
1479 // This exact matching won't work for multi-word snippets, but it's fine
1480 let sort_exact = Reverse(if Some(completion.label.filter_text()) == query {
1481 1
1482 } else {
1483 0
1484 });
1485
1486 MatchTier::WordStartMatch {
1487 sort_exact,
1488 sort_snippet,
1489 sort_score,
1490 sort_positions,
1491 sort_exact_case_matches,
1492 sort_text,
1493 sort_kind,
1494 sort_label,
1495 }
1496 }
1497 });
1498
1499 matches
1500 }
1501
1502 pub fn preserve_markdown_cache(&mut self, prev_menu: CompletionsMenu) {
1503 self.markdown_cache = prev_menu.markdown_cache.clone();
1504
1505 // Convert ForCandidate cache keys to ForCompletionMatch keys.
1506 let prev_completions = prev_menu.completions.borrow();
1507 self.markdown_cache
1508 .borrow_mut()
1509 .retain_mut(|(key, _markdown)| match key {
1510 MarkdownCacheKey::ForCompletionMatch { .. } => true,
1511 MarkdownCacheKey::ForCandidate { candidate_id } => {
1512 if let Some(completion) = prev_completions.get(*candidate_id) {
1513 match &completion.documentation {
1514 Some(CompletionDocumentation::MultiLineMarkdown(source)) => {
1515 *key = MarkdownCacheKey::ForCompletionMatch {
1516 new_text: completion.new_text.clone(),
1517 markdown_source: source.clone(),
1518 };
1519 true
1520 }
1521 _ => false,
1522 }
1523 } else {
1524 false
1525 }
1526 }
1527 });
1528 }
1529
1530 pub fn scroll_aside(
1531 &mut self,
1532 amount: ScrollAmount,
1533 window: &mut Window,
1534 cx: &mut Context<Editor>,
1535 ) {
1536 let mut offset = self.scroll_handle_aside.offset();
1537
1538 offset.y -= amount.pixels(
1539 window.line_height(),
1540 self.scroll_handle_aside.bounds().size.height - px(16.),
1541 ) / 2.0;
1542
1543 cx.notify();
1544 self.scroll_handle_aside.set_offset(offset);
1545 }
1546}
1547
1548fn render_completion_kind_letter(
1549 kind: Option<CompletionItemKind>,
1550 item_ix: usize,
1551 style: &EditorStyle,
1552) -> AnyElement {
1553 let badge = div()
1554 .flex_none()
1555 .w(IconSize::XSmall.rems())
1556 .text_center()
1557 .text_size(rems_from_px(11.))
1558 .line_height(rems_from_px(14.));
1559
1560 let Some(kind) = kind else {
1561 return badge.into_any_element();
1562 };
1563 let Some(letter) = completion_kind_letter(kind) else {
1564 return badge.into_any_element();
1565 };
1566
1567 let color = completion_kind_highlight_name(kind)
1568 .and_then(|name| {
1569 style.syntax.style_for_name(name).or_else(|| {
1570 let (parent, _) = name.rsplit_once('.')?;
1571 style.syntax.style_for_name(parent)
1572 })
1573 })
1574 .and_then(|hl| hl.color);
1575
1576 badge
1577 .id(("completion-kind", item_ix))
1578 .tooltip(Tooltip::text(completion_kind_name(kind)))
1579 .child(letter)
1580 .when_some(color, |element, color| element.text_color(color))
1581 .into_any_element()
1582}
1583
1584fn completion_kind_name(kind: CompletionItemKind) -> &'static str {
1585 match kind {
1586 CompletionItemKind::TEXT => "Text",
1587 CompletionItemKind::METHOD => "Method",
1588 CompletionItemKind::FUNCTION => "Function",
1589 CompletionItemKind::CONSTRUCTOR => "Constructor",
1590 CompletionItemKind::FIELD => "Field",
1591 CompletionItemKind::VARIABLE => "Variable",
1592 CompletionItemKind::CLASS => "Class",
1593 CompletionItemKind::INTERFACE => "Interface",
1594 CompletionItemKind::MODULE => "Module",
1595 CompletionItemKind::PROPERTY => "Property",
1596 CompletionItemKind::UNIT => "Unit",
1597 CompletionItemKind::VALUE => "Value",
1598 CompletionItemKind::ENUM => "Enum",
1599 CompletionItemKind::KEYWORD => "Keyword",
1600 CompletionItemKind::SNIPPET => "Snippet",
1601 CompletionItemKind::COLOR => "Color",
1602 CompletionItemKind::FILE => "File",
1603 CompletionItemKind::REFERENCE => "Reference",
1604 CompletionItemKind::FOLDER => "Folder",
1605 CompletionItemKind::ENUM_MEMBER => "Enum Member",
1606 CompletionItemKind::CONSTANT => "Constant",
1607 CompletionItemKind::STRUCT => "Struct",
1608 CompletionItemKind::EVENT => "Event",
1609 CompletionItemKind::OPERATOR => "Operator",
1610 CompletionItemKind::TYPE_PARAMETER => "Type Parameter",
1611 _ => "Unknown",
1612 }
1613}
1614
1615fn completion_kind_letter(kind: CompletionItemKind) -> Option<&'static str> {
1616 Some(match kind {
1617 CompletionItemKind::TEXT => "t",
1618 CompletionItemKind::METHOD => "m",
1619 CompletionItemKind::FUNCTION => "f",
1620 CompletionItemKind::CONSTRUCTOR => "C",
1621 CompletionItemKind::FIELD => "f",
1622 CompletionItemKind::VARIABLE => "v",
1623 CompletionItemKind::CLASS => "c",
1624 CompletionItemKind::INTERFACE => "i",
1625 CompletionItemKind::MODULE => "M",
1626 CompletionItemKind::PROPERTY => "p",
1627 CompletionItemKind::UNIT => "u",
1628 CompletionItemKind::VALUE => "v",
1629 CompletionItemKind::ENUM => "e",
1630 CompletionItemKind::KEYWORD => "k",
1631 CompletionItemKind::SNIPPET => "s",
1632 CompletionItemKind::COLOR => "c",
1633 CompletionItemKind::FILE => "F",
1634 CompletionItemKind::REFERENCE => "r",
1635 CompletionItemKind::FOLDER => "D",
1636 CompletionItemKind::ENUM_MEMBER => "e",
1637 CompletionItemKind::CONSTANT => "c",
1638 CompletionItemKind::STRUCT => "S",
1639 CompletionItemKind::EVENT => "E",
1640 CompletionItemKind::OPERATOR => "o",
1641 CompletionItemKind::TYPE_PARAMETER => "T",
1642 _ => return None,
1643 })
1644}
1645
1646fn completion_kind_highlight_name(kind: CompletionItemKind) -> Option<&'static str> {
1647 Some(match kind {
1648 CompletionItemKind::CLASS => "type",
1649 CompletionItemKind::CONSTANT => "constant",
1650 CompletionItemKind::CONSTRUCTOR => "constructor",
1651 CompletionItemKind::ENUM => "enum",
1652 CompletionItemKind::ENUM_MEMBER => "variant",
1653 CompletionItemKind::FIELD => "property",
1654 CompletionItemKind::FUNCTION => "function",
1655 CompletionItemKind::INTERFACE => "type",
1656 CompletionItemKind::METHOD => "function.method",
1657 CompletionItemKind::MODULE => "namespace",
1658 CompletionItemKind::OPERATOR => "operator",
1659 CompletionItemKind::PROPERTY => "property",
1660 CompletionItemKind::STRUCT => "type",
1661 CompletionItemKind::TYPE_PARAMETER => "type",
1662 CompletionItemKind::VARIABLE => "variable",
1663 CompletionItemKind::KEYWORD => "keyword",
1664 CompletionItemKind::SNIPPET => "string",
1665 _ => return None,
1666 })
1667}
1668
1669fn split_completion_label<'a>(
1670 text: &'a str,
1671 filter_range: &Range<usize>,
1672 highlights: &[(Range<usize>, HighlightStyle)],
1673) -> (
1674 (&'a str, Vec<(Range<usize>, HighlightStyle)>),
1675 (&'a str, Vec<(Range<usize>, HighlightStyle)>),
1676) {
1677 let (main_text, suffix_text) = text.split_at(filter_range.end);
1678 let main_highlights = highlights
1679 .iter()
1680 .filter_map(|(range, highlight)| {
1681 if range.start >= filter_range.end {
1682 return None;
1683 }
1684 let clamped_end = range.end.min(filter_range.end);
1685 Some((range.start..clamped_end, *highlight))
1686 })
1687 .collect();
1688 let suffix_highlights = highlights
1689 .iter()
1690 .filter_map(|(range, highlight)| {
1691 if range.end <= filter_range.end {
1692 return None;
1693 }
1694 let shifted_start = range.start.saturating_sub(filter_range.end);
1695 let shifted_end = range.end - filter_range.end;
1696 Some((shifted_start..shifted_end, *highlight))
1697 })
1698 .collect();
1699 (
1700 (main_text, main_highlights),
1701 (suffix_text, suffix_highlights),
1702 )
1703}
1704
1705fn exact_case_match_count(query: &str, string_match: &StringMatch) -> usize {
1706 let mut exact_matches = 0;
1707 let mut query_chars = query.chars();
1708 let mut next_query_char = query_chars.next();
1709 let mut matched_positions = string_match.positions.iter().copied().peekable();
1710
1711 for (index, candidate_char) in string_match.string.char_indices() {
1712 if matched_positions.peek() == Some(&index) {
1713 let Some(query_char) = next_query_char else {
1714 break;
1715 };
1716
1717 if query_char == candidate_char {
1718 exact_matches += 1;
1719 }
1720
1721 matched_positions.next();
1722 next_query_char = query_chars.next();
1723 }
1724 }
1725
1726 exact_matches
1727}
1728
1729#[derive(Clone)]
1730pub struct AvailableCodeAction {
1731 pub action: CodeAction,
1732 pub provider: Rc<dyn CodeActionProvider>,
1733}
1734
1735#[derive(Clone)]
1736pub struct CodeActionContents {
1737 tasks: Option<Rc<ResolvedTasks>>,
1738 actions: Option<Rc<[AvailableCodeAction]>>,
1739 debug_scenarios: Vec<DebugScenario>,
1740 pub(crate) context: TaskContext,
1741}
1742
1743impl CodeActionContents {
1744 pub(crate) fn new(
1745 tasks: Option<ResolvedTasks>,
1746 actions: Option<Rc<[AvailableCodeAction]>>,
1747 debug_scenarios: Vec<DebugScenario>,
1748 context: TaskContext,
1749 ) -> Self {
1750 Self {
1751 tasks: tasks.map(Rc::new),
1752 actions,
1753 debug_scenarios,
1754 context,
1755 }
1756 }
1757
1758 pub fn tasks(&self) -> Option<&ResolvedTasks> {
1759 self.tasks.as_deref()
1760 }
1761
1762 fn len(&self) -> usize {
1763 let tasks_len = self.tasks.as_ref().map_or(0, |tasks| tasks.templates.len());
1764 let code_actions_len = self.actions.as_ref().map_or(0, |actions| actions.len());
1765 tasks_len + code_actions_len + self.debug_scenarios.len()
1766 }
1767
1768 pub fn is_empty(&self) -> bool {
1769 self.len() == 0
1770 }
1771
1772 fn iter(&self) -> impl Iterator<Item = CodeActionsItem> + '_ {
1773 self.tasks
1774 .iter()
1775 .flat_map(|tasks| {
1776 tasks
1777 .templates
1778 .iter()
1779 .map(|(kind, task)| CodeActionsItem::Task(kind.clone(), task.clone()))
1780 })
1781 .chain(self.actions.iter().flat_map(|actions| {
1782 actions.iter().map(|available| CodeActionsItem::CodeAction {
1783 action: available.action.clone(),
1784 provider: available.provider.clone(),
1785 })
1786 }))
1787 .chain(
1788 self.debug_scenarios
1789 .iter()
1790 .cloned()
1791 .map(CodeActionsItem::DebugScenario),
1792 )
1793 }
1794
1795 pub fn get(&self, mut index: usize) -> Option<CodeActionsItem> {
1796 if let Some(tasks) = &self.tasks {
1797 if let Some((kind, task)) = tasks.templates.get(index) {
1798 return Some(CodeActionsItem::Task(kind.clone(), task.clone()));
1799 } else {
1800 index -= tasks.templates.len();
1801 }
1802 }
1803 if let Some(actions) = &self.actions {
1804 if let Some(available) = actions.get(index) {
1805 return Some(CodeActionsItem::CodeAction {
1806 action: available.action.clone(),
1807 provider: available.provider.clone(),
1808 });
1809 } else {
1810 index -= actions.len();
1811 }
1812 }
1813
1814 self.debug_scenarios
1815 .get(index)
1816 .cloned()
1817 .map(CodeActionsItem::DebugScenario)
1818 }
1819}
1820
1821#[derive(Clone)]
1822pub enum CodeActionsItem {
1823 Task(TaskSourceKind, ResolvedTask),
1824 CodeAction {
1825 action: CodeAction,
1826 provider: Rc<dyn CodeActionProvider>,
1827 },
1828 DebugScenario(DebugScenario),
1829}
1830
1831impl CodeActionsItem {
1832 pub fn label(&self) -> String {
1833 match self {
1834 Self::CodeAction { action, .. } => action.lsp_action.title().to_owned(),
1835 Self::Task(_, task) => task.resolved_label.clone(),
1836 Self::DebugScenario(scenario) => scenario.label.to_string(),
1837 }
1838 }
1839
1840 pub fn menu_label(&self) -> String {
1841 match self {
1842 Self::CodeAction { action, .. } => action.lsp_action.title().replace("\n", ""),
1843 Self::Task(_, task) => task.resolved_label.replace("\n", ""),
1844 Self::DebugScenario(scenario) => format!("debug: {}", scenario.label),
1845 }
1846 }
1847}
1848
1849pub struct CodeActionsMenu {
1850 pub actions: CodeActionContents,
1851 pub buffer: Entity<Buffer>,
1852 pub selected_item: usize,
1853 pub scroll_handle: UniformListScrollHandle,
1854 pub deployed_from: Option<CodeActionSource>,
1855}
1856
1857impl CodeActionsMenu {
1858 fn select_first(&mut self, cx: &mut Context<Editor>) {
1859 self.selected_item = if self.scroll_handle.y_flipped() {
1860 self.actions.len() - 1
1861 } else {
1862 0
1863 };
1864 self.scroll_handle
1865 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1866 cx.notify()
1867 }
1868
1869 fn select_last(&mut self, cx: &mut Context<Editor>) {
1870 self.selected_item = if self.scroll_handle.y_flipped() {
1871 0
1872 } else {
1873 self.actions.len() - 1
1874 };
1875 self.scroll_handle
1876 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1877 cx.notify()
1878 }
1879
1880 fn select_prev(&mut self, cx: &mut Context<Editor>) {
1881 self.selected_item = if self.scroll_handle.y_flipped() {
1882 self.next_match_index()
1883 } else {
1884 self.prev_match_index()
1885 };
1886 self.scroll_handle
1887 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1888 cx.notify();
1889 }
1890
1891 fn select_next(&mut self, cx: &mut Context<Editor>) {
1892 self.selected_item = if self.scroll_handle.y_flipped() {
1893 self.prev_match_index()
1894 } else {
1895 self.next_match_index()
1896 };
1897 self.scroll_handle
1898 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1899 cx.notify();
1900 }
1901
1902 fn prev_match_index(&self) -> usize {
1903 if self.selected_item > 0 {
1904 self.selected_item - 1
1905 } else {
1906 self.actions.len() - 1
1907 }
1908 }
1909
1910 fn next_match_index(&self) -> usize {
1911 if self.selected_item + 1 < self.actions.len() {
1912 self.selected_item + 1
1913 } else {
1914 0
1915 }
1916 }
1917
1918 pub fn visible(&self) -> bool {
1919 !self.actions.is_empty()
1920 }
1921
1922 fn origin(&self) -> ContextMenuOrigin {
1923 match &self.deployed_from {
1924 Some(CodeActionSource::Indicator(row)) | Some(CodeActionSource::RunMenu(row)) => {
1925 ContextMenuOrigin::GutterIndicator(*row)
1926 }
1927 Some(CodeActionSource::QuickActionBar) => ContextMenuOrigin::QuickActionBar,
1928 None => ContextMenuOrigin::Cursor,
1929 }
1930 }
1931
1932 fn render(
1933 &self,
1934 _style: &EditorStyle,
1935 max_height_in_lines: u32,
1936 window: &mut Window,
1937 cx: &mut Context<Editor>,
1938 ) -> AnyElement {
1939 let actions = self.actions.clone();
1940 let selected_item = self.selected_item;
1941 let is_quick_action_bar = matches!(self.origin(), ContextMenuOrigin::QuickActionBar);
1942
1943 let list = uniform_list(
1944 "code_actions_menu",
1945 self.actions.len(),
1946 cx.processor(move |_this, range: Range<usize>, _, cx| {
1947 actions
1948 .iter()
1949 .skip(range.start)
1950 .take(range.end - range.start)
1951 .enumerate()
1952 .map(|(ix, action)| {
1953 let item_ix = range.start + ix;
1954 let selected = item_ix == selected_item;
1955 let colors = cx.theme().colors();
1956
1957 ListItem::new(item_ix)
1958 .inset(true)
1959 .toggle_state(selected)
1960 .overflow_x()
1961 .child(
1962 div()
1963 .min_w(CODE_ACTION_MENU_MIN_WIDTH)
1964 .max_w(CODE_ACTION_MENU_MAX_WIDTH)
1965 .overflow_hidden()
1966 .text_ellipsis()
1967 .when(is_quick_action_bar, |this| this.text_ui(cx))
1968 .when(selected, |this| this.text_color(colors.text_accent))
1969 .child(action.menu_label()),
1970 )
1971 .on_click(cx.listener(move |editor, _, window, cx| {
1972 cx.stop_propagation();
1973 if let Some(task) = editor.confirm_code_action(
1974 &ConfirmCodeAction {
1975 item_ix: Some(item_ix),
1976 },
1977 window,
1978 cx,
1979 ) {
1980 task.detach_and_log_err(cx)
1981 }
1982 }))
1983 })
1984 .collect()
1985 }),
1986 )
1987 .occlude()
1988 .max_h(max_height_in_lines as f32 * window.line_height())
1989 .track_scroll(&self.scroll_handle)
1990 .with_width_from_item(
1991 self.actions
1992 .iter()
1993 .enumerate()
1994 .max_by_key(|(_, action)| match action {
1995 CodeActionsItem::Task(_, task) => task.resolved_label.chars().count(),
1996 CodeActionsItem::CodeAction { action, .. } => {
1997 action.lsp_action.title().chars().count()
1998 }
1999 CodeActionsItem::DebugScenario(scenario) => {
2000 format!("debug: {}", scenario.label).chars().count()
2001 }
2002 })
2003 .map(|(ix, _)| ix),
2004 )
2005 .with_sizing_behavior(ListSizingBehavior::Infer);
2006
2007 Popover::new().child(list).into_any_element()
2008 }
2009
2010 fn render_aside(
2011 &mut self,
2012 max_size: Size<Pixels>,
2013 window: &mut Window,
2014 _cx: &mut Context<Editor>,
2015 ) -> Option<AnyElement> {
2016 let Some(action) = self.actions.get(self.selected_item) else {
2017 return None;
2018 };
2019
2020 let label = action.menu_label();
2021 let text_system = window.text_system();
2022 let mut line_wrapper = text_system.line_wrapper(
2023 window.text_style().font(),
2024 window.text_style().font_size.to_pixels(window.rem_size()),
2025 );
2026 let is_truncated = line_wrapper.should_truncate_line(
2027 &label,
2028 CODE_ACTION_MENU_MAX_WIDTH,
2029 "…",
2030 gpui::TruncateFrom::End,
2031 );
2032
2033 if is_truncated.is_none() {
2034 return None;
2035 }
2036
2037 Some(
2038 Popover::new()
2039 .child(
2040 div()
2041 .child(label)
2042 .id("code_actions_menu_extended")
2043 .px(MENU_ASIDE_X_PADDING / 2.)
2044 .max_w(max_size.width)
2045 .max_h(max_size.height)
2046 .occlude(),
2047 )
2048 .into_any_element(),
2049 )
2050 }
2051}
2052
2053#[cfg(test)]
2054mod tests {
2055 use super::*;
2056
2057 fn bold() -> HighlightStyle {
2058 FontWeight::BOLD.into()
2059 }
2060
2061 fn colored() -> HighlightStyle {
2062 HighlightStyle {
2063 fade_out: Some(0.5),
2064 ..Default::default()
2065 }
2066 }
2067
2068 #[test]
2069 fn test_split_completion_label_keeps_prefix_before_filter_range() {
2070 let ((main_text, main_highlights), (suffix_text, suffix_highlights)) =
2071 split_completion_label("&some_str: String", &(1..9), &[(11..17, colored())]);
2072
2073 assert_eq!(main_text, "&some_str");
2074 assert_eq!(suffix_text, ": String");
2075 assert_eq!(main_highlights, vec![]);
2076 assert_eq!(suffix_highlights, vec![(2..8, colored())]);
2077 }
2078
2079 #[test]
2080 fn test_split_completion_label_splits_boundary_spanning_highlight() {
2081 let ((main_text, main_highlights), (suffix_text, suffix_highlights)) =
2082 split_completion_label(
2083 "await.as_deref_mut(&mut self)",
2084 &(6..18),
2085 &[(0..29, bold())],
2086 );
2087
2088 assert_eq!(main_text, "await.as_deref_mut");
2089 assert_eq!(suffix_text, "(&mut self)");
2090 assert_eq!(main_highlights, vec![(0..18, bold())]);
2091 assert_eq!(suffix_highlights, vec![(0..11, bold())]);
2092 }
2093}
2094