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