language_selector.rs

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