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