language_selector.rs

  1use std::sync::Arc;
  2
  3use editor::Editor;
  4use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
  5use gpui::{
  6    actions, elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MouseState,
  7    MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
  8};
  9use language::{Buffer, LanguageRegistry};
 10use picker::{Picker, PickerDelegate};
 11use project::Project;
 12use settings::Settings;
 13use workspace::{AppState, Workspace};
 14
 15actions!(language_selector, [Toggle]);
 16
 17pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
 18    Picker::<LanguageSelector>::init(cx);
 19    cx.add_action({
 20        let language_registry = app_state.languages.clone();
 21        move |workspace, _: &Toggle, cx| {
 22            LanguageSelector::toggle(workspace, language_registry.clone(), cx)
 23        }
 24    });
 25}
 26
 27pub enum Event {
 28    Dismissed,
 29}
 30
 31pub struct LanguageSelector {
 32    buffer: ModelHandle<Buffer>,
 33    project: ModelHandle<Project>,
 34    language_registry: Arc<LanguageRegistry>,
 35    candidates: Vec<StringMatchCandidate>,
 36    matches: Vec<StringMatch>,
 37    picker: ViewHandle<Picker<Self>>,
 38    selected_index: usize,
 39}
 40
 41impl LanguageSelector {
 42    fn new(
 43        buffer: ModelHandle<Buffer>,
 44        project: ModelHandle<Project>,
 45        language_registry: Arc<LanguageRegistry>,
 46        cx: &mut ViewContext<Self>,
 47    ) -> Self {
 48        let handle = cx.weak_handle();
 49        let picker = cx.add_view(|cx| Picker::new("Select Language...", handle, cx));
 50
 51        let candidates = language_registry
 52            .language_names()
 53            .into_iter()
 54            .enumerate()
 55            .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name))
 56            .collect::<Vec<_>>();
 57        let mut matches = candidates
 58            .iter()
 59            .map(|candidate| StringMatch {
 60                candidate_id: candidate.id,
 61                score: 0.,
 62                positions: Default::default(),
 63                string: candidate.string.clone(),
 64            })
 65            .collect::<Vec<_>>();
 66        matches.sort_unstable_by(|mat1, mat2| mat1.string.cmp(&mat2.string));
 67
 68        Self {
 69            buffer,
 70            project,
 71            language_registry,
 72            candidates,
 73            matches,
 74            picker,
 75            selected_index: 0,
 76        }
 77    }
 78
 79    fn toggle(
 80        workspace: &mut Workspace,
 81        registry: Arc<LanguageRegistry>,
 82        cx: &mut ViewContext<Workspace>,
 83    ) {
 84        if let Some((_, buffer, _)) = workspace
 85            .active_item(cx)
 86            .and_then(|active_item| active_item.act_as::<Editor>(cx))
 87            .and_then(|editor| editor.read(cx).active_excerpt(cx))
 88        {
 89            workspace.toggle_modal(cx, |workspace, cx| {
 90                let project = workspace.project().clone();
 91                let this = cx.add_view(|cx| Self::new(buffer, project, registry, cx));
 92                cx.subscribe(&this, Self::on_event).detach();
 93                this
 94            });
 95        }
 96    }
 97
 98    fn on_event(
 99        workspace: &mut Workspace,
100        _: ViewHandle<LanguageSelector>,
101        event: &Event,
102        cx: &mut ViewContext<Workspace>,
103    ) {
104        match event {
105            Event::Dismissed => {
106                workspace.dismiss_modal(cx);
107            }
108        }
109    }
110}
111
112impl Entity for LanguageSelector {
113    type Event = Event;
114}
115
116impl View for LanguageSelector {
117    fn ui_name() -> &'static str {
118        "LanguageSelector"
119    }
120
121    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
122        ChildView::new(self.picker.clone(), cx).boxed()
123    }
124
125    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
126        if cx.is_self_focused() {
127            cx.focus(&self.picker);
128        }
129    }
130}
131
132impl PickerDelegate for LanguageSelector {
133    fn match_count(&self) -> usize {
134        self.matches.len()
135    }
136
137    fn confirm(&mut self, cx: &mut ViewContext<Self>) {
138        if let Some(mat) = self.matches.get(self.selected_index) {
139            let language_name = &self.candidates[mat.candidate_id].string;
140            let language = self.language_registry.language_for_name(language_name);
141            cx.spawn(|this, mut cx| async move {
142                let language = language.await?;
143                this.update(&mut cx, |this, cx| {
144                    this.project.update(cx, |project, cx| {
145                        project.set_language_for_buffer(&this.buffer, language, cx);
146                    });
147                });
148                anyhow::Ok(())
149            })
150            .detach_and_log_err(cx);
151        }
152
153        cx.emit(Event::Dismissed);
154    }
155
156    fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
157        cx.emit(Event::Dismissed);
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<Self>) {
165        self.selected_index = ix;
166    }
167
168    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> gpui::Task<()> {
169        let background = cx.background().clone();
170        let candidates = self.candidates.clone();
171        cx.spawn(|this, mut cx| async move {
172            let matches = if query.is_empty() {
173                candidates
174                    .into_iter()
175                    .enumerate()
176                    .map(|(index, candidate)| StringMatch {
177                        candidate_id: index,
178                        string: candidate.string,
179                        positions: Vec::new(),
180                        score: 0.0,
181                    })
182                    .collect()
183            } else {
184                match_strings(
185                    &candidates,
186                    &query,
187                    false,
188                    100,
189                    &Default::default(),
190                    background,
191                )
192                .await
193            };
194
195            this.update(&mut cx, |this, cx| {
196                this.matches = matches;
197                this.selected_index = this
198                    .selected_index
199                    .min(this.matches.len().saturating_sub(1));
200                cx.notify();
201            });
202        })
203    }
204
205    fn render_match(
206        &self,
207        ix: usize,
208        mouse_state: &mut MouseState,
209        selected: bool,
210        cx: &AppContext,
211    ) -> ElementBox {
212        let settings = cx.global::<Settings>();
213        let theme = &settings.theme;
214        let mat = &self.matches[ix];
215        let style = theme.picker.item.style_for(mouse_state, selected);
216        let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name());
217        let mut label = mat.string.clone();
218        if buffer_language_name.as_deref() == Some(mat.string.as_str()) {
219            label.push_str(" (current)");
220        }
221
222        Label::new(label, style.label.clone())
223            .with_highlights(mat.positions.clone())
224            .contained()
225            .with_style(style.container)
226            .boxed()
227    }
228}