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