1mod active_buffer_language;
2
3pub use active_buffer_language::ActiveBufferLanguage;
4use anyhow::Context as _;
5use editor::Editor;
6use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
7use gpui::{
8 App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, ParentElement,
9 Render, Styled, WeakEntity, Window, actions,
10};
11use language::{Buffer, LanguageMatcher, LanguageName, LanguageRegistry};
12use open_path_prompt::file_finder_settings::FileFinderSettings;
13use picker::{Picker, PickerDelegate};
14use project::Project;
15use settings::Settings;
16use std::{ops::Not as _, path::Path, sync::Arc};
17use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*};
18use util::ResultExt;
19use workspace::{ModalView, Workspace};
20
21actions!(
22 language_selector,
23 [
24 /// Toggles the language selector modal.
25 Toggle
26 ]
27);
28
29pub fn init(cx: &mut App) {
30 cx.observe_new(LanguageSelector::register).detach();
31}
32
33pub struct LanguageSelector {
34 picker: Entity<Picker<LanguageSelectorDelegate>>,
35}
36
37impl LanguageSelector {
38 fn register(
39 workspace: &mut Workspace,
40 _window: Option<&mut Window>,
41 _: &mut Context<Workspace>,
42 ) {
43 workspace.register_action(move |workspace, _: &Toggle, window, cx| {
44 Self::toggle(workspace, window, cx);
45 });
46 }
47
48 fn toggle(
49 workspace: &mut Workspace,
50 window: &mut Window,
51 cx: &mut Context<Workspace>,
52 ) -> Option<()> {
53 let registry = workspace.app_state().languages.clone();
54 let (_, buffer, _) = workspace
55 .active_item(cx)?
56 .act_as::<Editor>(cx)?
57 .read(cx)
58 .active_excerpt(cx)?;
59 let project = workspace.project().clone();
60
61 workspace.toggle_modal(window, cx, move |window, cx| {
62 LanguageSelector::new(buffer, project, registry, window, cx)
63 });
64 Some(())
65 }
66
67 fn new(
68 buffer: Entity<Buffer>,
69 project: Entity<Project>,
70 language_registry: Arc<LanguageRegistry>,
71 window: &mut Window,
72 cx: &mut Context<Self>,
73 ) -> Self {
74 let delegate = LanguageSelectorDelegate::new(
75 cx.entity().downgrade(),
76 buffer,
77 project,
78 language_registry,
79 );
80
81 let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
82 Self { picker }
83 }
84}
85
86impl Render for LanguageSelector {
87 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
88 v_flex()
89 .key_context("LanguageSelector")
90 .w(rems(34.))
91 .child(self.picker.clone())
92 }
93}
94
95impl Focusable for LanguageSelector {
96 fn focus_handle(&self, cx: &App) -> FocusHandle {
97 self.picker.focus_handle(cx)
98 }
99}
100
101impl EventEmitter<DismissEvent> for LanguageSelector {}
102impl ModalView for LanguageSelector {}
103
104pub struct LanguageSelectorDelegate {
105 language_selector: WeakEntity<LanguageSelector>,
106 buffer: Entity<Buffer>,
107 project: Entity<Project>,
108 language_registry: Arc<LanguageRegistry>,
109 candidates: Vec<StringMatchCandidate>,
110 matches: Vec<StringMatch>,
111 selected_index: usize,
112}
113
114impl LanguageSelectorDelegate {
115 fn new(
116 language_selector: WeakEntity<LanguageSelector>,
117 buffer: Entity<Buffer>,
118 project: Entity<Project>,
119 language_registry: Arc<LanguageRegistry>,
120 ) -> Self {
121 let candidates = language_registry
122 .language_names()
123 .into_iter()
124 .filter_map(|name| {
125 language_registry
126 .available_language_for_name(name.as_ref())?
127 .hidden()
128 .not()
129 .then_some(name)
130 })
131 .enumerate()
132 .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name.as_ref()))
133 .collect::<Vec<_>>();
134
135 Self {
136 language_selector,
137 buffer,
138 project,
139 language_registry,
140 candidates,
141 matches: vec![],
142 selected_index: 0,
143 }
144 }
145
146 fn language_data_for_match(&self, mat: &StringMatch, cx: &App) -> (String, Option<Icon>) {
147 let mut label = mat.string.clone();
148 let buffer_language = self.buffer.read(cx).language();
149 let need_icon = FileFinderSettings::get_global(cx).file_icons;
150
151 if let Some(buffer_language) = buffer_language
152 .filter(|buffer_language| buffer_language.name().as_ref() == mat.string.as_str())
153 {
154 label.push_str(" (current)");
155 let icon = need_icon
156 .then(|| self.language_icon(&buffer_language.config().matcher, cx))
157 .flatten();
158 (label, icon)
159 } else {
160 let icon = need_icon
161 .then(|| {
162 let language_name = LanguageName::new(mat.string.as_str());
163 self.language_registry
164 .available_language_for_name(language_name.as_ref())
165 .and_then(|available_language| {
166 self.language_icon(available_language.matcher(), cx)
167 })
168 })
169 .flatten();
170 (label, icon)
171 }
172 }
173
174 fn language_icon(&self, matcher: &LanguageMatcher, cx: &App) -> Option<Icon> {
175 matcher
176 .path_suffixes
177 .iter()
178 .find_map(|extension| file_icons::FileIcons::get_icon(Path::new(extension), cx))
179 .map(Icon::from_path)
180 .map(|icon| icon.color(Color::Muted))
181 }
182}
183
184impl PickerDelegate for LanguageSelectorDelegate {
185 type ListItem = ListItem;
186
187 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
188 "Select a language…".into()
189 }
190
191 fn match_count(&self) -> usize {
192 self.matches.len()
193 }
194
195 fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
196 if let Some(mat) = self.matches.get(self.selected_index) {
197 let language_name = &self.candidates[mat.candidate_id].string;
198 let language = self.language_registry.language_for_name(language_name);
199 let project = self.project.downgrade();
200 let buffer = self.buffer.downgrade();
201 cx.spawn_in(window, async move |_, cx| {
202 let language = language.await?;
203 let project = project.upgrade().context("project was dropped")?;
204 let buffer = buffer.upgrade().context("buffer was dropped")?;
205 project.update(cx, |project, cx| {
206 project.set_language_for_buffer(&buffer, language, cx);
207 });
208 anyhow::Ok(())
209 })
210 .detach_and_log_err(cx);
211 }
212 self.dismissed(window, cx);
213 }
214
215 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
216 self.language_selector
217 .update(cx, |_, cx| cx.emit(DismissEvent))
218 .log_err();
219 }
220
221 fn selected_index(&self) -> usize {
222 self.selected_index
223 }
224
225 fn set_selected_index(
226 &mut self,
227 ix: usize,
228 _window: &mut Window,
229 _: &mut Context<Picker<Self>>,
230 ) {
231 self.selected_index = ix;
232 }
233
234 fn update_matches(
235 &mut self,
236 query: String,
237 window: &mut Window,
238 cx: &mut Context<Picker<Self>>,
239 ) -> gpui::Task<()> {
240 let background = cx.background_executor().clone();
241 let candidates = self.candidates.clone();
242 cx.spawn_in(window, async move |this, cx| {
243 let matches = if query.is_empty() {
244 candidates
245 .into_iter()
246 .enumerate()
247 .map(|(index, candidate)| StringMatch {
248 candidate_id: index,
249 string: candidate.string,
250 positions: Vec::new(),
251 score: 0.0,
252 })
253 .collect()
254 } else {
255 match_strings(
256 &candidates,
257 &query,
258 false,
259 true,
260 100,
261 &Default::default(),
262 background,
263 )
264 .await
265 };
266
267 this.update(cx, |this, cx| {
268 let delegate = &mut this.delegate;
269 delegate.matches = matches;
270 delegate.selected_index = delegate
271 .selected_index
272 .min(delegate.matches.len().saturating_sub(1));
273 cx.notify();
274 })
275 .log_err();
276 })
277 }
278
279 fn render_match(
280 &self,
281 ix: usize,
282 selected: bool,
283 _: &mut Window,
284 cx: &mut Context<Picker<Self>>,
285 ) -> Option<Self::ListItem> {
286 let mat = &self.matches.get(ix)?;
287 let (label, language_icon) = self.language_data_for_match(mat, cx);
288 Some(
289 ListItem::new(ix)
290 .inset(true)
291 .spacing(ListItemSpacing::Sparse)
292 .toggle_state(selected)
293 .start_slot::<Icon>(language_icon)
294 .child(HighlightedLabel::new(label, mat.positions.clone())),
295 )
296 }
297}