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