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::{match_strings, StringMatch, StringMatchCandidate};
  9use gpui::{
 10    actions, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
 11    ParentElement, Render, Styled, WeakEntity, Window,
 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::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
 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        if let Some(buffer_language) = buffer_language {
143            let buffer_language_name = buffer_language.name();
144            if buffer_language_name.as_ref() == mat.string.as_str() {
145                label.push_str(" (current)");
146                let icon = need_icon
147                    .then(|| self.language_icon(&buffer_language.config().matcher, cx))
148                    .flatten();
149                return (label, icon);
150            }
151        }
152
153        if need_icon {
154            let language_name = LanguageName::new(mat.string.as_str());
155            match self
156                .language_registry
157                .available_language_for_name(language_name.as_ref())
158            {
159                Some(available_language) => {
160                    let icon = self.language_icon(available_language.matcher(), cx);
161                    (label, icon)
162                }
163                None => (label, None),
164            }
165        } else {
166            (label, None)
167        }
168    }
169
170    fn language_icon(&self, matcher: &LanguageMatcher, cx: &App) -> Option<Icon> {
171        matcher
172            .path_suffixes
173            .iter()
174            .find_map(|extension| {
175                if extension.contains('.') {
176                    None
177                } else {
178                    FileIcons::get_icon(Path::new(&format!("file.{extension}")), cx)
179                }
180            })
181            .map(Icon::from_path)
182            .map(|icon| icon.color(Color::Muted))
183    }
184}
185
186impl PickerDelegate for LanguageSelectorDelegate {
187    type ListItem = ListItem;
188
189    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
190        "Select a language…".into()
191    }
192
193    fn match_count(&self) -> usize {
194        self.matches.len()
195    }
196
197    fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
198        if let Some(mat) = self.matches.get(self.selected_index) {
199            let language_name = &self.candidates[mat.candidate_id].string;
200            let language = self.language_registry.language_for_name(language_name);
201            let project = self.project.downgrade();
202            let buffer = self.buffer.downgrade();
203            cx.spawn_in(window, |_, mut cx| async move {
204                let language = language.await?;
205                let project = project
206                    .upgrade()
207                    .ok_or_else(|| anyhow!("project was dropped"))?;
208                let buffer = buffer
209                    .upgrade()
210                    .ok_or_else(|| anyhow!("buffer was dropped"))?;
211                project.update(&mut cx, |project, cx| {
212                    project.set_language_for_buffer(&buffer, language, cx);
213                })
214            })
215            .detach_and_log_err(cx);
216        }
217        self.dismissed(window, cx);
218    }
219
220    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
221        self.language_selector
222            .update(cx, |_, cx| cx.emit(DismissEvent))
223            .log_err();
224    }
225
226    fn selected_index(&self) -> usize {
227        self.selected_index
228    }
229
230    fn set_selected_index(
231        &mut self,
232        ix: usize,
233        _window: &mut Window,
234        _: &mut Context<Picker<Self>>,
235    ) {
236        self.selected_index = ix;
237    }
238
239    fn update_matches(
240        &mut self,
241        query: String,
242        window: &mut Window,
243        cx: &mut Context<Picker<Self>>,
244    ) -> gpui::Task<()> {
245        let background = cx.background_executor().clone();
246        let candidates = self.candidates.clone();
247        cx.spawn_in(window, |this, mut cx| async move {
248            let matches = if query.is_empty() {
249                candidates
250                    .into_iter()
251                    .enumerate()
252                    .map(|(index, candidate)| StringMatch {
253                        candidate_id: index,
254                        string: candidate.string,
255                        positions: Vec::new(),
256                        score: 0.0,
257                    })
258                    .collect()
259            } else {
260                match_strings(
261                    &candidates,
262                    &query,
263                    false,
264                    100,
265                    &Default::default(),
266                    background,
267                )
268                .await
269            };
270
271            this.update(&mut cx, |this, cx| {
272                let delegate = &mut this.delegate;
273                delegate.matches = matches;
274                delegate.selected_index = delegate
275                    .selected_index
276                    .min(delegate.matches.len().saturating_sub(1));
277                cx.notify();
278            })
279            .log_err();
280        })
281    }
282
283    fn render_match(
284        &self,
285        ix: usize,
286        selected: bool,
287        _: &mut Window,
288        cx: &mut Context<Picker<Self>>,
289    ) -> Option<Self::ListItem> {
290        let mat = &self.matches[ix];
291        let (label, language_icon) = self.language_data_for_match(mat, cx);
292        Some(
293            ListItem::new(ix)
294                .inset(true)
295                .spacing(ListItemSpacing::Sparse)
296                .toggle_state(selected)
297                .start_slot::<Icon>(language_icon)
298                .child(HighlightedLabel::new(label, mat.positions.clone())),
299        )
300    }
301}