language_selector.rs

  1mod active_buffer_language;
  2
  3pub use active_buffer_language::ActiveBufferLanguage;
  4use anyhow::anyhow;
  5use editor::Editor;
  6use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
  7use gpui::{
  8    actions, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
  9    ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
 10};
 11use language::{Buffer, LanguageRegistry};
 12use picker::{Picker, PickerDelegate};
 13use project::Project;
 14use std::sync::Arc;
 15use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
 16use util::ResultExt;
 17use workspace::{ModalView, Workspace};
 18
 19actions!(language_selector, [Toggle]);
 20
 21pub fn init(cx: &mut AppContext) {
 22    cx.observe_new_views(LanguageSelector::register).detach();
 23}
 24
 25pub struct LanguageSelector {
 26    picker: View<Picker<LanguageSelectorDelegate>>,
 27}
 28
 29impl LanguageSelector {
 30    fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
 31        workspace.register_action(move |workspace, _: &Toggle, cx| {
 32            Self::toggle(workspace, cx);
 33        });
 34    }
 35
 36    fn toggle(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<()> {
 37        let registry = workspace.app_state().languages.clone();
 38        let (_, buffer, _) = workspace
 39            .active_item(cx)?
 40            .act_as::<Editor>(cx)?
 41            .read(cx)
 42            .active_excerpt(cx)?;
 43        let project = workspace.project().clone();
 44
 45        workspace.toggle_modal(cx, move |cx| {
 46            LanguageSelector::new(buffer, project, registry, cx)
 47        });
 48        Some(())
 49    }
 50
 51    fn new(
 52        buffer: Model<Buffer>,
 53        project: Model<Project>,
 54        language_registry: Arc<LanguageRegistry>,
 55        cx: &mut ViewContext<Self>,
 56    ) -> Self {
 57        let delegate = LanguageSelectorDelegate::new(
 58            cx.view().downgrade(),
 59            buffer,
 60            project,
 61            language_registry,
 62        );
 63
 64        let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
 65        Self { picker }
 66    }
 67}
 68
 69impl Render for LanguageSelector {
 70    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
 71        v_flex().w(rems(34.)).child(self.picker.clone())
 72    }
 73}
 74
 75impl FocusableView for LanguageSelector {
 76    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
 77        self.picker.focus_handle(cx)
 78    }
 79}
 80
 81impl EventEmitter<DismissEvent> for LanguageSelector {}
 82impl ModalView for LanguageSelector {}
 83
 84pub struct LanguageSelectorDelegate {
 85    language_selector: WeakView<LanguageSelector>,
 86    buffer: Model<Buffer>,
 87    project: Model<Project>,
 88    language_registry: Arc<LanguageRegistry>,
 89    candidates: Vec<StringMatchCandidate>,
 90    matches: Vec<StringMatch>,
 91    selected_index: usize,
 92}
 93
 94impl LanguageSelectorDelegate {
 95    fn new(
 96        language_selector: WeakView<LanguageSelector>,
 97        buffer: Model<Buffer>,
 98        project: Model<Project>,
 99        language_registry: Arc<LanguageRegistry>,
100    ) -> Self {
101        let candidates = language_registry
102            .language_names()
103            .into_iter()
104            .enumerate()
105            .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name))
106            .collect::<Vec<_>>();
107
108        Self {
109            language_selector,
110            buffer,
111            project,
112            language_registry,
113            candidates,
114            matches: vec![],
115            selected_index: 0,
116        }
117    }
118}
119
120impl PickerDelegate for LanguageSelectorDelegate {
121    type ListItem = ListItem;
122
123    fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
124        "Select a language...".into()
125    }
126
127    fn match_count(&self) -> usize {
128        self.matches.len()
129    }
130
131    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
132        if let Some(mat) = self.matches.get(self.selected_index) {
133            let language_name = &self.candidates[mat.candidate_id].string;
134            let language = self.language_registry.language_for_name(language_name);
135            let project = self.project.downgrade();
136            let buffer = self.buffer.downgrade();
137            cx.spawn(|_, mut cx| async move {
138                let language = language.await?;
139                let project = project
140                    .upgrade()
141                    .ok_or_else(|| anyhow!("project was dropped"))?;
142                let buffer = buffer
143                    .upgrade()
144                    .ok_or_else(|| anyhow!("buffer was dropped"))?;
145                project.update(&mut cx, |project, cx| {
146                    project.set_language_for_buffer(&buffer, language, cx);
147                })
148            })
149            .detach_and_log_err(cx);
150        }
151        self.dismissed(cx);
152    }
153
154    fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
155        self.language_selector
156            .update(cx, |_, cx| cx.emit(DismissEvent))
157            .log_err();
158    }
159
160    fn selected_index(&self) -> usize {
161        self.selected_index
162    }
163
164    fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
165        self.selected_index = ix;
166    }
167
168    fn update_matches(
169        &mut self,
170        query: String,
171        cx: &mut ViewContext<Picker<Self>>,
172    ) -> gpui::Task<()> {
173        let background = cx.background_executor().clone();
174        let candidates = self.candidates.clone();
175        cx.spawn(|this, mut cx| async move {
176            let matches = if query.is_empty() {
177                candidates
178                    .into_iter()
179                    .enumerate()
180                    .map(|(index, candidate)| StringMatch {
181                        candidate_id: index,
182                        string: candidate.string,
183                        positions: Vec::new(),
184                        score: 0.0,
185                    })
186                    .collect()
187            } else {
188                match_strings(
189                    &candidates,
190                    &query,
191                    false,
192                    100,
193                    &Default::default(),
194                    background,
195                )
196                .await
197            };
198
199            this.update(&mut cx, |this, cx| {
200                let delegate = &mut this.delegate;
201                delegate.matches = matches;
202                delegate.selected_index = delegate
203                    .selected_index
204                    .min(delegate.matches.len().saturating_sub(1));
205                cx.notify();
206            })
207            .log_err();
208        })
209    }
210
211    fn render_match(
212        &self,
213        ix: usize,
214        selected: bool,
215        cx: &mut ViewContext<Picker<Self>>,
216    ) -> Option<Self::ListItem> {
217        let mat = &self.matches[ix];
218        let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name());
219        let mut label = mat.string.clone();
220        if buffer_language_name.map(|n| n.0).as_deref() == Some(mat.string.as_str()) {
221            label.push_str(" (current)");
222        }
223
224        Some(
225            ListItem::new(ix)
226                .inset(true)
227                .spacing(ListItemSpacing::Sparse)
228                .selected(selected)
229                .child(HighlightedLabel::new(label, mat.positions.clone())),
230        )
231    }
232}