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