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().w(rems(34.)).child(self.picker.clone())
 90    }
 91}
 92
 93impl Focusable for LanguageSelector {
 94    fn focus_handle(&self, cx: &App) -> FocusHandle {
 95        self.picker.focus_handle(cx)
 96    }
 97}
 98
 99impl EventEmitter<DismissEvent> for LanguageSelector {}
100impl ModalView for LanguageSelector {}
101
102pub struct LanguageSelectorDelegate {
103    language_selector: WeakEntity<LanguageSelector>,
104    buffer: Entity<Buffer>,
105    project: Entity<Project>,
106    language_registry: Arc<LanguageRegistry>,
107    candidates: Vec<StringMatchCandidate>,
108    matches: Vec<StringMatch>,
109    selected_index: usize,
110}
111
112impl LanguageSelectorDelegate {
113    fn new(
114        language_selector: WeakEntity<LanguageSelector>,
115        buffer: Entity<Buffer>,
116        project: Entity<Project>,
117        language_registry: Arc<LanguageRegistry>,
118    ) -> Self {
119        let candidates = language_registry
120            .language_names()
121            .into_iter()
122            .filter_map(|name| {
123                language_registry
124                    .available_language_for_name(name.as_ref())?
125                    .hidden()
126                    .not()
127                    .then_some(name)
128            })
129            .enumerate()
130            .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name.as_ref()))
131            .collect::<Vec<_>>();
132
133        Self {
134            language_selector,
135            buffer,
136            project,
137            language_registry,
138            candidates,
139            matches: vec![],
140            selected_index: 0,
141        }
142    }
143
144    fn language_data_for_match(&self, mat: &StringMatch, cx: &App) -> (String, Option<Icon>) {
145        let mut label = mat.string.clone();
146        let buffer_language = self.buffer.read(cx).language();
147        let need_icon = FileFinderSettings::get_global(cx).file_icons;
148
149        if let Some(buffer_language) = buffer_language
150            .filter(|buffer_language| buffer_language.name().as_ref() == mat.string.as_str())
151        {
152            label.push_str(" (current)");
153            let icon = need_icon
154                .then(|| self.language_icon(&buffer_language.config().matcher, cx))
155                .flatten();
156            (label, icon)
157        } else {
158            let icon = need_icon
159                .then(|| {
160                    let language_name = LanguageName::new(mat.string.as_str());
161                    self.language_registry
162                        .available_language_for_name(language_name.as_ref())
163                        .and_then(|available_language| {
164                            self.language_icon(available_language.matcher(), cx)
165                        })
166                })
167                .flatten();
168            (label, icon)
169        }
170    }
171
172    fn language_icon(&self, matcher: &LanguageMatcher, cx: &App) -> Option<Icon> {
173        matcher
174            .path_suffixes
175            .iter()
176            .find_map(|extension| FileIcons::get_icon(Path::new(extension), cx))
177            .map(Icon::from_path)
178            .map(|icon| icon.color(Color::Muted))
179    }
180}
181
182impl PickerDelegate for LanguageSelectorDelegate {
183    type ListItem = ListItem;
184
185    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
186        "Select a language…".into()
187    }
188
189    fn match_count(&self) -> usize {
190        self.matches.len()
191    }
192
193    fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
194        if let Some(mat) = self.matches.get(self.selected_index) {
195            let language_name = &self.candidates[mat.candidate_id].string;
196            let language = self.language_registry.language_for_name(language_name);
197            let project = self.project.downgrade();
198            let buffer = self.buffer.downgrade();
199            cx.spawn_in(window, async move |_, cx| {
200                let language = language.await?;
201                let project = project.upgrade().context("project was dropped")?;
202                let buffer = buffer.upgrade().context("buffer was dropped")?;
203                project.update(cx, |project, cx| {
204                    project.set_language_for_buffer(&buffer, language, cx);
205                })
206            })
207            .detach_and_log_err(cx);
208        }
209        self.dismissed(window, cx);
210    }
211
212    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
213        self.language_selector
214            .update(cx, |_, cx| cx.emit(DismissEvent))
215            .log_err();
216    }
217
218    fn selected_index(&self) -> usize {
219        self.selected_index
220    }
221
222    fn set_selected_index(
223        &mut self,
224        ix: usize,
225        _window: &mut Window,
226        _: &mut Context<Picker<Self>>,
227    ) {
228        self.selected_index = ix;
229    }
230
231    fn update_matches(
232        &mut self,
233        query: String,
234        window: &mut Window,
235        cx: &mut Context<Picker<Self>>,
236    ) -> gpui::Task<()> {
237        let background = cx.background_executor().clone();
238        let candidates = self.candidates.clone();
239        cx.spawn_in(window, async move |this, cx| {
240            let matches = if query.is_empty() {
241                candidates
242                    .into_iter()
243                    .enumerate()
244                    .map(|(index, candidate)| StringMatch {
245                        candidate_id: index,
246                        string: candidate.string,
247                        positions: Vec::new(),
248                        score: 0.0,
249                    })
250                    .collect()
251            } else {
252                match_strings(
253                    &candidates,
254                    &query,
255                    false,
256                    true,
257                    100,
258                    &Default::default(),
259                    background,
260                )
261                .await
262            };
263
264            this.update(cx, |this, cx| {
265                let delegate = &mut this.delegate;
266                delegate.matches = matches;
267                delegate.selected_index = delegate
268                    .selected_index
269                    .min(delegate.matches.len().saturating_sub(1));
270                cx.notify();
271            })
272            .log_err();
273        })
274    }
275
276    fn render_match(
277        &self,
278        ix: usize,
279        selected: bool,
280        _: &mut Window,
281        cx: &mut Context<Picker<Self>>,
282    ) -> Option<Self::ListItem> {
283        let mat = &self.matches[ix];
284        let (label, language_icon) = self.language_data_for_match(mat, cx);
285        Some(
286            ListItem::new(ix)
287                .inset(true)
288                .spacing(ListItemSpacing::Sparse)
289                .toggle_state(selected)
290                .start_slot::<Icon>(language_icon)
291                .child(HighlightedLabel::new(label, mat.positions.clone())),
292        )
293    }
294}