language_selector.rs

  1mod active_buffer_language;
  2
  3pub use active_buffer_language::ActiveBufferLanguage;
  4use anyhow::anyhow;
  5use editor::Editor;
  6use file_finder::file_finder_settings::FileFinderSettings;
  7use file_icons::FileIcons;
  8use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
  9use gpui::{
 10    App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, ParentElement,
 11    Render, Styled, WeakEntity, Window, actions,
 12};
 13use language::{Buffer, LanguageMatcher, LanguageName, LanguageRegistry};
 14use picker::{Picker, PickerDelegate};
 15use project::Project;
 16use settings::Settings;
 17use std::{ops::Not as _, path::Path, sync::Arc};
 18use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*};
 19use util::ResultExt;
 20use workspace::{ModalView, Workspace};
 21
 22actions!(language_selector, [Toggle]);
 23
 24pub fn init(cx: &mut App) {
 25    cx.observe_new(LanguageSelector::register).detach();
 26}
 27
 28pub struct LanguageSelector {
 29    picker: Entity<Picker<LanguageSelectorDelegate>>,
 30}
 31
 32impl LanguageSelector {
 33    fn register(
 34        workspace: &mut Workspace,
 35        _window: Option<&mut Window>,
 36        _: &mut Context<Workspace>,
 37    ) {
 38        workspace.register_action(move |workspace, _: &Toggle, window, cx| {
 39            Self::toggle(workspace, window, cx);
 40        });
 41    }
 42
 43    fn toggle(
 44        workspace: &mut Workspace,
 45        window: &mut Window,
 46        cx: &mut Context<Workspace>,
 47    ) -> Option<()> {
 48        let registry = workspace.app_state().languages.clone();
 49        let (_, buffer, _) = workspace
 50            .active_item(cx)?
 51            .act_as::<Editor>(cx)?
 52            .read(cx)
 53            .active_excerpt(cx)?;
 54        let project = workspace.project().clone();
 55
 56        workspace.toggle_modal(window, cx, move |window, cx| {
 57            LanguageSelector::new(buffer, project, registry, window, cx)
 58        });
 59        Some(())
 60    }
 61
 62    fn new(
 63        buffer: Entity<Buffer>,
 64        project: Entity<Project>,
 65        language_registry: Arc<LanguageRegistry>,
 66        window: &mut Window,
 67        cx: &mut Context<Self>,
 68    ) -> Self {
 69        let delegate = LanguageSelectorDelegate::new(
 70            cx.entity().downgrade(),
 71            buffer,
 72            project,
 73            language_registry,
 74        );
 75
 76        let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
 77        Self { picker }
 78    }
 79}
 80
 81impl Render for LanguageSelector {
 82    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
 83        v_flex().w(rems(34.)).child(self.picker.clone())
 84    }
 85}
 86
 87impl Focusable for LanguageSelector {
 88    fn focus_handle(&self, cx: &App) -> FocusHandle {
 89        self.picker.focus_handle(cx)
 90    }
 91}
 92
 93impl EventEmitter<DismissEvent> for LanguageSelector {}
 94impl ModalView for LanguageSelector {}
 95
 96pub struct LanguageSelectorDelegate {
 97    language_selector: WeakEntity<LanguageSelector>,
 98    buffer: Entity<Buffer>,
 99    project: Entity<Project>,
100    language_registry: Arc<LanguageRegistry>,
101    candidates: Vec<StringMatchCandidate>,
102    matches: Vec<StringMatch>,
103    selected_index: usize,
104}
105
106impl LanguageSelectorDelegate {
107    fn new(
108        language_selector: WeakEntity<LanguageSelector>,
109        buffer: Entity<Buffer>,
110        project: Entity<Project>,
111        language_registry: Arc<LanguageRegistry>,
112    ) -> Self {
113        let candidates = language_registry
114            .language_names()
115            .into_iter()
116            .filter_map(|name| {
117                language_registry
118                    .available_language_for_name(&name)?
119                    .hidden()
120                    .not()
121                    .then_some(name)
122            })
123            .enumerate()
124            .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, &name))
125            .collect::<Vec<_>>();
126
127        Self {
128            language_selector,
129            buffer,
130            project,
131            language_registry,
132            candidates,
133            matches: vec![],
134            selected_index: 0,
135        }
136    }
137
138    fn language_data_for_match(&self, mat: &StringMatch, cx: &App) -> (String, Option<Icon>) {
139        let mut label = mat.string.clone();
140        let buffer_language = self.buffer.read(cx).language();
141        let need_icon = FileFinderSettings::get_global(cx).file_icons;
142
143        if let Some(buffer_language) = buffer_language
144            .filter(|buffer_language| buffer_language.name().as_ref() == mat.string.as_str())
145        {
146            label.push_str(" (current)");
147            let icon = need_icon
148                .then(|| self.language_icon(&buffer_language.config().matcher, cx))
149                .flatten();
150            (label, icon)
151        } else {
152            let icon = need_icon
153                .then(|| {
154                    let language_name = LanguageName::new(mat.string.as_str());
155                    self.language_registry
156                        .available_language_for_name(language_name.as_ref())
157                        .and_then(|available_language| {
158                            self.language_icon(available_language.matcher(), cx)
159                        })
160                })
161                .flatten();
162            (label, icon)
163        }
164    }
165
166    fn language_icon(&self, matcher: &LanguageMatcher, cx: &App) -> Option<Icon> {
167        matcher
168            .path_suffixes
169            .iter()
170            .find_map(|extension| FileIcons::get_icon(Path::new(extension), cx))
171            .map(Icon::from_path)
172            .map(|icon| icon.color(Color::Muted))
173    }
174}
175
176impl PickerDelegate for LanguageSelectorDelegate {
177    type ListItem = ListItem;
178
179    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
180        "Select a language…".into()
181    }
182
183    fn match_count(&self) -> usize {
184        self.matches.len()
185    }
186
187    fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
188        if let Some(mat) = self.matches.get(self.selected_index) {
189            let language_name = &self.candidates[mat.candidate_id].string;
190            let language = self.language_registry.language_for_name(language_name);
191            let project = self.project.downgrade();
192            let buffer = self.buffer.downgrade();
193            cx.spawn_in(window, async move |_, cx| {
194                let language = language.await?;
195                let project = project
196                    .upgrade()
197                    .ok_or_else(|| anyhow!("project was dropped"))?;
198                let buffer = buffer
199                    .upgrade()
200                    .ok_or_else(|| anyhow!("buffer was dropped"))?;
201                project.update(cx, |project, cx| {
202                    project.set_language_for_buffer(&buffer, language, cx);
203                })
204            })
205            .detach_and_log_err(cx);
206        }
207        self.dismissed(window, cx);
208    }
209
210    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
211        self.language_selector
212            .update(cx, |_, cx| cx.emit(DismissEvent))
213            .log_err();
214    }
215
216    fn selected_index(&self) -> usize {
217        self.selected_index
218    }
219
220    fn set_selected_index(
221        &mut self,
222        ix: usize,
223        _window: &mut Window,
224        _: &mut Context<Picker<Self>>,
225    ) {
226        self.selected_index = ix;
227    }
228
229    fn update_matches(
230        &mut self,
231        query: String,
232        window: &mut Window,
233        cx: &mut Context<Picker<Self>>,
234    ) -> gpui::Task<()> {
235        let background = cx.background_executor().clone();
236        let candidates = self.candidates.clone();
237        cx.spawn_in(window, async move |this, cx| {
238            let matches = if query.is_empty() {
239                candidates
240                    .into_iter()
241                    .enumerate()
242                    .map(|(index, candidate)| StringMatch {
243                        candidate_id: index,
244                        string: candidate.string,
245                        positions: Vec::new(),
246                        score: 0.0,
247                    })
248                    .collect()
249            } else {
250                match_strings(
251                    &candidates,
252                    &query,
253                    false,
254                    100,
255                    &Default::default(),
256                    background,
257                )
258                .await
259            };
260
261            this.update(cx, |this, cx| {
262                let delegate = &mut this.delegate;
263                delegate.matches = matches;
264                delegate.selected_index = delegate
265                    .selected_index
266                    .min(delegate.matches.len().saturating_sub(1));
267                cx.notify();
268            })
269            .log_err();
270        })
271    }
272
273    fn render_match(
274        &self,
275        ix: usize,
276        selected: bool,
277        _: &mut Window,
278        cx: &mut Context<Picker<Self>>,
279    ) -> Option<Self::ListItem> {
280        let mat = &self.matches[ix];
281        let (label, language_icon) = self.language_data_for_match(mat, cx);
282        Some(
283            ListItem::new(ix)
284                .inset(true)
285                .spacing(ListItemSpacing::Sparse)
286                .toggle_state(selected)
287                .start_slot::<Icon>(language_icon)
288                .child(HighlightedLabel::new(label, mat.positions.clone())),
289        )
290    }
291}