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