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, Div, 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::{v_stack, HighlightedLabel, ListItem, Selectable};
 16use util::ResultExt;
 17use workspace::Workspace;
 18
 19actions!(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.build_view(|cx| Picker::new(delegate, cx));
 65        Self { picker }
 66    }
 67}
 68
 69impl Render for LanguageSelector {
 70    type Element = Div;
 71
 72    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
 73        v_stack().min_w_96().child(self.picker.clone())
 74    }
 75}
 76
 77impl FocusableView for LanguageSelector {
 78    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
 79        self.picker.focus_handle(cx)
 80    }
 81}
 82
 83impl EventEmitter<DismissEvent> for LanguageSelector {}
 84
 85pub struct LanguageSelectorDelegate {
 86    language_selector: WeakView<LanguageSelector>,
 87    buffer: Model<Buffer>,
 88    project: Model<Project>,
 89    language_registry: Arc<LanguageRegistry>,
 90    candidates: Vec<StringMatchCandidate>,
 91    matches: Vec<StringMatch>,
 92    selected_index: usize,
 93}
 94
 95impl LanguageSelectorDelegate {
 96    fn new(
 97        language_selector: WeakView<LanguageSelector>,
 98        buffer: Model<Buffer>,
 99        project: Model<Project>,
100        language_registry: Arc<LanguageRegistry>,
101    ) -> Self {
102        let candidates = language_registry
103            .language_names()
104            .into_iter()
105            .enumerate()
106            .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name))
107            .collect::<Vec<_>>();
108
109        Self {
110            language_selector,
111            buffer,
112            project,
113            language_registry,
114            candidates,
115            matches: vec![],
116            selected_index: 0,
117        }
118    }
119}
120
121impl PickerDelegate for LanguageSelectorDelegate {
122    type ListItem = ListItem;
123
124    fn placeholder_text(&self) -> Arc<str> {
125        "Select a language...".into()
126    }
127
128    fn match_count(&self) -> usize {
129        self.matches.len()
130    }
131
132    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
133        if let Some(mat) = self.matches.get(self.selected_index) {
134            let language_name = &self.candidates[mat.candidate_id].string;
135            let language = self.language_registry.language_for_name(language_name);
136            let project = self.project.downgrade();
137            let buffer = self.buffer.downgrade();
138            cx.spawn(|_, mut cx| async move {
139                let language = language.await?;
140                let project = project
141                    .upgrade()
142                    .ok_or_else(|| anyhow!("project was dropped"))?;
143                let buffer = buffer
144                    .upgrade()
145                    .ok_or_else(|| anyhow!("buffer was dropped"))?;
146                project.update(&mut cx, |project, cx| {
147                    project.set_language_for_buffer(&buffer, language, cx);
148                })
149            })
150            .detach_and_log_err(cx);
151        }
152        self.dismissed(cx);
153    }
154
155    fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
156        self.language_selector
157            .update(cx, |_, cx| cx.emit(DismissEvent))
158            .log_err();
159    }
160
161    fn selected_index(&self) -> usize {
162        self.selected_index
163    }
164
165    fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
166        self.selected_index = ix;
167    }
168
169    fn update_matches(
170        &mut self,
171        query: String,
172        cx: &mut ViewContext<Picker<Self>>,
173    ) -> gpui::Task<()> {
174        let background = cx.background_executor().clone();
175        let candidates = self.candidates.clone();
176        cx.spawn(|this, mut cx| async move {
177            let matches = if query.is_empty() {
178                candidates
179                    .into_iter()
180                    .enumerate()
181                    .map(|(index, candidate)| StringMatch {
182                        candidate_id: index,
183                        string: candidate.string,
184                        positions: Vec::new(),
185                        score: 0.0,
186                    })
187                    .collect()
188            } else {
189                match_strings(
190                    &candidates,
191                    &query,
192                    false,
193                    100,
194                    &Default::default(),
195                    background,
196                )
197                .await
198            };
199
200            this.update(&mut cx, |this, cx| {
201                let delegate = &mut this.delegate;
202                delegate.matches = matches;
203                delegate.selected_index = delegate
204                    .selected_index
205                    .min(delegate.matches.len().saturating_sub(1));
206                cx.notify();
207            })
208            .log_err();
209        })
210    }
211
212    fn render_match(
213        &self,
214        ix: usize,
215        selected: bool,
216        cx: &mut ViewContext<Picker<Self>>,
217    ) -> Option<Self::ListItem> {
218        let mat = &self.matches[ix];
219        let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name());
220        let mut label = mat.string.clone();
221        if buffer_language_name.as_deref() == Some(mat.string.as_str()) {
222            label.push_str(" (current)");
223        }
224
225        Some(
226            ListItem::new(ix)
227                .inset(true)
228                .selected(selected)
229                .child(HighlightedLabel::new(label, mat.positions.clone())),
230        )
231    }
232}