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!(
 23    language_selector,
 24    [
 25        /// Toggles the language selector modal.
 26        Toggle
 27    ]
 28);
 29
 30pub fn init(cx: &mut App) {
 31    cx.observe_new(LanguageSelector::register).detach();
 32}
 33
 34pub struct LanguageSelector {
 35    picker: Entity<Picker<LanguageSelectorDelegate>>,
 36}
 37
 38impl LanguageSelector {
 39    fn register(
 40        workspace: &mut Workspace,
 41        _window: Option<&mut Window>,
 42        _: &mut Context<Workspace>,
 43    ) {
 44        workspace.register_action(move |workspace, _: &Toggle, window, cx| {
 45            Self::toggle(workspace, window, cx);
 46        });
 47    }
 48
 49    fn toggle(
 50        workspace: &mut Workspace,
 51        window: &mut Window,
 52        cx: &mut Context<Workspace>,
 53    ) -> Option<()> {
 54        let registry = workspace.app_state().languages.clone();
 55        let (_, buffer, _) = workspace
 56            .active_item(cx)?
 57            .act_as::<Editor>(cx)?
 58            .read(cx)
 59            .active_excerpt(cx)?;
 60        let project = workspace.project().clone();
 61
 62        workspace.toggle_modal(window, cx, move |window, cx| {
 63            LanguageSelector::new(buffer, project, registry, window, cx)
 64        });
 65        Some(())
 66    }
 67
 68    fn new(
 69        buffer: Entity<Buffer>,
 70        project: Entity<Project>,
 71        language_registry: Arc<LanguageRegistry>,
 72        window: &mut Window,
 73        cx: &mut Context<Self>,
 74    ) -> Self {
 75        let delegate = LanguageSelectorDelegate::new(
 76            cx.entity().downgrade(),
 77            buffer,
 78            project,
 79            language_registry,
 80        );
 81
 82        let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
 83        Self { picker }
 84    }
 85}
 86
 87impl Render for LanguageSelector {
 88    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
 89        v_flex()
 90            .key_context("LanguageSelector")
 91            .w(rems(34.))
 92            .child(self.picker.clone())
 93    }
 94}
 95
 96impl Focusable for LanguageSelector {
 97    fn focus_handle(&self, cx: &App) -> FocusHandle {
 98        self.picker.focus_handle(cx)
 99    }
100}
101
102impl EventEmitter<DismissEvent> for LanguageSelector {}
103impl ModalView for LanguageSelector {}
104
105pub struct LanguageSelectorDelegate {
106    language_selector: WeakEntity<LanguageSelector>,
107    buffer: Entity<Buffer>,
108    project: Entity<Project>,
109    language_registry: Arc<LanguageRegistry>,
110    candidates: Vec<StringMatchCandidate>,
111    matches: Vec<StringMatch>,
112    selected_index: usize,
113}
114
115impl LanguageSelectorDelegate {
116    fn new(
117        language_selector: WeakEntity<LanguageSelector>,
118        buffer: Entity<Buffer>,
119        project: Entity<Project>,
120        language_registry: Arc<LanguageRegistry>,
121    ) -> Self {
122        let candidates = language_registry
123            .language_names()
124            .into_iter()
125            .filter_map(|name| {
126                language_registry
127                    .available_language_for_name(name.as_ref())?
128                    .hidden()
129                    .not()
130                    .then_some(name)
131            })
132            .enumerate()
133            .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name.as_ref()))
134            .collect::<Vec<_>>();
135
136        Self {
137            language_selector,
138            buffer,
139            project,
140            language_registry,
141            candidates,
142            matches: vec![],
143            selected_index: 0,
144        }
145    }
146
147    fn language_data_for_match(&self, mat: &StringMatch, cx: &App) -> (String, Option<Icon>) {
148        let mut label = mat.string.clone();
149        let buffer_language = self.buffer.read(cx).language();
150        let need_icon = FileFinderSettings::get_global(cx).file_icons;
151
152        if let Some(buffer_language) = buffer_language
153            .filter(|buffer_language| buffer_language.name().as_ref() == mat.string.as_str())
154        {
155            label.push_str(" (current)");
156            let icon = need_icon
157                .then(|| self.language_icon(&buffer_language.config().matcher, cx))
158                .flatten();
159            (label, icon)
160        } else {
161            let icon = need_icon
162                .then(|| {
163                    let language_name = LanguageName::new(mat.string.as_str());
164                    self.language_registry
165                        .available_language_for_name(language_name.as_ref())
166                        .and_then(|available_language| {
167                            self.language_icon(available_language.matcher(), cx)
168                        })
169                })
170                .flatten();
171            (label, icon)
172        }
173    }
174
175    fn language_icon(&self, matcher: &LanguageMatcher, cx: &App) -> Option<Icon> {
176        matcher
177            .path_suffixes
178            .iter()
179            .find_map(|extension| FileIcons::get_icon(Path::new(extension), cx))
180            .map(Icon::from_path)
181            .map(|icon| icon.color(Color::Muted))
182    }
183}
184
185impl PickerDelegate for LanguageSelectorDelegate {
186    type ListItem = ListItem;
187
188    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
189        "Select a language…".into()
190    }
191
192    fn match_count(&self) -> usize {
193        self.matches.len()
194    }
195
196    fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
197        if let Some(mat) = self.matches.get(self.selected_index) {
198            let language_name = &self.candidates[mat.candidate_id].string;
199            let language = self.language_registry.language_for_name(language_name);
200            let project = self.project.downgrade();
201            let buffer = self.buffer.downgrade();
202            cx.spawn_in(window, async move |_, cx| {
203                let language = language.await?;
204                let project = project.upgrade().context("project was dropped")?;
205                let buffer = buffer.upgrade().context("buffer was dropped")?;
206                project.update(cx, |project, cx| {
207                    project.set_language_for_buffer(&buffer, language, cx);
208                })
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}