1use collections::HashMap;
2use command_palette;
3use editor::{CompletionProvider, Editor};
4use fuzzy::StringMatchCandidate;
5use gpui::{Context, Entity, SharedString, Window};
6use language::{self, ToOffset};
7use project::{self, CompletionDisplayOptions};
8
9pub struct ActionCompletionProvider {
10 action_names: Vec<&'static str>,
11 humanized_names: HashMap<&'static str, SharedString>,
12}
13
14impl ActionCompletionProvider {
15 pub fn new(
16 action_names: Vec<&'static str>,
17 humanized_names: HashMap<&'static str, SharedString>,
18 ) -> Self {
19 Self {
20 action_names,
21 humanized_names,
22 }
23 }
24}
25
26impl CompletionProvider for ActionCompletionProvider {
27 fn completions(
28 &self,
29 buffer: &Entity<language::Buffer>,
30 buffer_position: language::Anchor,
31 _trigger: editor::CompletionContext,
32 _window: &mut Window,
33 cx: &mut Context<Editor>,
34 ) -> gpui::Task<anyhow::Result<Vec<project::CompletionResponse>>> {
35 let buffer = buffer.read(cx);
36 let mut count_back = 0;
37
38 for char in buffer.reversed_chars_at(buffer_position) {
39 if char.is_ascii_alphanumeric() || char == '_' || char == ':' {
40 count_back += 1;
41 } else {
42 break;
43 }
44 }
45
46 let start_anchor = buffer.anchor_before(
47 buffer_position
48 .to_offset(&buffer)
49 .saturating_sub(count_back),
50 );
51
52 let replace_range = start_anchor..buffer_position;
53 let snapshot = buffer.text_snapshot();
54 let query: String = snapshot.text_for_range(replace_range.clone()).collect();
55 let normalized_query = command_palette::normalize_action_query(&query);
56
57 let candidates: Vec<StringMatchCandidate> = self
58 .action_names
59 .iter()
60 .enumerate()
61 .map(|(ix, &name)| {
62 let humanized = self
63 .humanized_names
64 .get(name)
65 .cloned()
66 .unwrap_or_else(|| name.into());
67 StringMatchCandidate::new(ix, &humanized)
68 })
69 .collect();
70
71 let executor = cx.background_executor().clone();
72 let executor_for_fuzzy = executor.clone();
73 let action_names = self.action_names.clone();
74 let humanized_names = self.humanized_names.clone();
75
76 executor.spawn(async move {
77 let matches = fuzzy::match_strings(
78 &candidates,
79 &normalized_query,
80 true,
81 true,
82 action_names.len(),
83 &Default::default(),
84 executor_for_fuzzy,
85 )
86 .await;
87
88 let completions: Vec<project::Completion> = matches
89 .iter()
90 .take(50)
91 .map(|m| {
92 let action_name = action_names[m.candidate_id];
93 let humanized = humanized_names
94 .get(action_name)
95 .cloned()
96 .unwrap_or_else(|| action_name.into());
97
98 project::Completion {
99 replace_range: replace_range.clone(),
100 label: language::CodeLabel::plain(humanized.to_string(), None),
101 new_text: action_name.to_string(),
102 documentation: None,
103 source: project::CompletionSource::Custom,
104 icon_path: None,
105 match_start: None,
106 snippet_deduplication_key: None,
107 insert_text_mode: None,
108 confirm: None,
109 }
110 })
111 .collect();
112
113 Ok(vec![project::CompletionResponse {
114 completions,
115 display_options: CompletionDisplayOptions {
116 dynamic_width: true,
117 },
118 is_incomplete: false,
119 }])
120 })
121 }
122
123 fn is_completion_trigger(
124 &self,
125 _buffer: &Entity<language::Buffer>,
126 _position: language::Anchor,
127 text: &str,
128 _trigger_in_words: bool,
129 _cx: &mut Context<Editor>,
130 ) -> bool {
131 text.chars().last().is_some_and(|last_char| {
132 last_char.is_ascii_alphanumeric() || last_char == '_' || last_char == ':'
133 })
134 }
135}