action_completion_provider.rs

  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}