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