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}