language_selector.rs

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