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 .enumerate()
108 .filter_map(|(candidate_id, name)| {
109 language_registry
110 .available_language_for_name(&name)?
111 .hidden()
112 .not()
113 .then(|| StringMatchCandidate::new(candidate_id, name))
114 })
115 .collect::<Vec<_>>();
116
117 Self {
118 language_selector,
119 buffer,
120 project,
121 language_registry,
122 candidates,
123 matches: vec![],
124 selected_index: 0,
125 }
126 }
127
128 fn language_data_for_match(
129 &self,
130 mat: &StringMatch,
131 cx: &AppContext,
132 ) -> (String, Option<Icon>) {
133 let mut label = mat.string.clone();
134 let buffer_language = self.buffer.read(cx).language();
135 let need_icon = FileFinderSettings::get_global(cx).file_icons;
136 if let Some(buffer_language) = buffer_language {
137 let buffer_language_name = buffer_language.name();
138 if buffer_language_name.0.as_ref() == mat.string.as_str() {
139 label.push_str(" (current)");
140 let icon = need_icon
141 .then(|| self.language_icon(&buffer_language.config().matcher, cx))
142 .flatten();
143 return (label, icon);
144 }
145 }
146
147 if need_icon {
148 let language_name = LanguageName::new(mat.string.as_str());
149 match self
150 .language_registry
151 .available_language_for_name(&language_name.0)
152 {
153 Some(available_language) => {
154 let icon = self.language_icon(available_language.matcher(), cx);
155 (label, icon)
156 }
157 None => (label, None),
158 }
159 } else {
160 (label, None)
161 }
162 }
163
164 fn language_icon(&self, matcher: &LanguageMatcher, cx: &AppContext) -> Option<Icon> {
165 matcher
166 .path_suffixes
167 .iter()
168 .find_map(|extension| {
169 if extension.contains('.') {
170 None
171 } else {
172 FileIcons::get_icon(Path::new(&format!("file.{extension}")), cx)
173 }
174 })
175 .map(Icon::from_path)
176 .map(|icon| icon.color(Color::Muted))
177 }
178}
179
180impl PickerDelegate for LanguageSelectorDelegate {
181 type ListItem = ListItem;
182
183 fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
184 "Select a language…".into()
185 }
186
187 fn match_count(&self) -> usize {
188 self.matches.len()
189 }
190
191 fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
192 if let Some(mat) = self.matches.get(self.selected_index) {
193 let language_name = &self.candidates[mat.candidate_id].string;
194 let language = self.language_registry.language_for_name(language_name);
195 let project = self.project.downgrade();
196 let buffer = self.buffer.downgrade();
197 cx.spawn(|_, mut cx| async move {
198 let language = language.await?;
199 let project = project
200 .upgrade()
201 .ok_or_else(|| anyhow!("project was dropped"))?;
202 let buffer = buffer
203 .upgrade()
204 .ok_or_else(|| anyhow!("buffer was dropped"))?;
205 project.update(&mut cx, |project, cx| {
206 project.set_language_for_buffer(&buffer, language, cx);
207 })
208 })
209 .detach_and_log_err(cx);
210 }
211 self.dismissed(cx);
212 }
213
214 fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
215 self.language_selector
216 .update(cx, |_, cx| cx.emit(DismissEvent))
217 .log_err();
218 }
219
220 fn selected_index(&self) -> usize {
221 self.selected_index
222 }
223
224 fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
225 self.selected_index = ix;
226 }
227
228 fn update_matches(
229 &mut self,
230 query: String,
231 cx: &mut ViewContext<Picker<Self>>,
232 ) -> gpui::Task<()> {
233 let background = cx.background_executor().clone();
234 let candidates = self.candidates.clone();
235 cx.spawn(|this, mut cx| async move {
236 let matches = if query.is_empty() {
237 candidates
238 .into_iter()
239 .enumerate()
240 .map(|(index, candidate)| StringMatch {
241 candidate_id: index,
242 string: candidate.string,
243 positions: Vec::new(),
244 score: 0.0,
245 })
246 .collect()
247 } else {
248 match_strings(
249 &candidates,
250 &query,
251 false,
252 100,
253 &Default::default(),
254 background,
255 )
256 .await
257 };
258
259 this.update(&mut cx, |this, cx| {
260 let delegate = &mut this.delegate;
261 delegate.matches = matches;
262 delegate.selected_index = delegate
263 .selected_index
264 .min(delegate.matches.len().saturating_sub(1));
265 cx.notify();
266 })
267 .log_err();
268 })
269 }
270
271 fn render_match(
272 &self,
273 ix: usize,
274 selected: bool,
275 cx: &mut ViewContext<Picker<Self>>,
276 ) -> Option<Self::ListItem> {
277 let mat = &self.matches[ix];
278 let (label, language_icon) = self.language_data_for_match(mat, cx);
279 Some(
280 ListItem::new(ix)
281 .inset(true)
282 .spacing(ListItemSpacing::Sparse)
283 .selected(selected)
284 .start_slot::<Icon>(language_icon)
285 .child(HighlightedLabel::new(label, mat.positions.clone())),
286 )
287 }
288}