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!(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.upgrade().context("project was dropped")?;
196                let buffer = buffer.upgrade().context("buffer was dropped")?;
197                project.update(cx, |project, cx| {
198                    project.set_language_for_buffer(&buffer, language, cx);
199                })
200            })
201            .detach_and_log_err(cx);
202        }
203        self.dismissed(window, cx);
204    }
205
206    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
207        self.language_selector
208            .update(cx, |_, cx| cx.emit(DismissEvent))
209            .log_err();
210    }
211
212    fn selected_index(&self) -> usize {
213        self.selected_index
214    }
215
216    fn set_selected_index(
217        &mut self,
218        ix: usize,
219        _window: &mut Window,
220        _: &mut Context<Picker<Self>>,
221    ) {
222        self.selected_index = ix;
223    }
224
225    fn update_matches(
226        &mut self,
227        query: String,
228        window: &mut Window,
229        cx: &mut Context<Picker<Self>>,
230    ) -> gpui::Task<()> {
231        let background = cx.background_executor().clone();
232        let candidates = self.candidates.clone();
233        cx.spawn_in(window, async move |this, cx| {
234            let matches = if query.is_empty() {
235                candidates
236                    .into_iter()
237                    .enumerate()
238                    .map(|(index, candidate)| StringMatch {
239                        candidate_id: index,
240                        string: candidate.string,
241                        positions: Vec::new(),
242                        score: 0.0,
243                    })
244                    .collect()
245            } else {
246                match_strings(
247                    &candidates,
248                    &query,
249                    false,
250                    100,
251                    &Default::default(),
252                    background,
253                )
254                .await
255            };
256
257            this.update(cx, |this, cx| {
258                let delegate = &mut this.delegate;
259                delegate.matches = matches;
260                delegate.selected_index = delegate
261                    .selected_index
262                    .min(delegate.matches.len().saturating_sub(1));
263                cx.notify();
264            })
265            .log_err();
266        })
267    }
268
269    fn render_match(
270        &self,
271        ix: usize,
272        selected: bool,
273        _: &mut Window,
274        cx: &mut Context<Picker<Self>>,
275    ) -> Option<Self::ListItem> {
276        let mat = &self.matches[ix];
277        let (label, language_icon) = self.language_data_for_match(mat, cx);
278        Some(
279            ListItem::new(ix)
280                .inset(true)
281                .spacing(ListItemSpacing::Sparse)
282                .toggle_state(selected)
283                .start_slot::<Icon>(language_icon)
284                .child(HighlightedLabel::new(label, mat.positions.clone())),
285        )
286    }
287}