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