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