1mod active_buffer_language;
2
3pub use active_buffer_language::ActiveBufferLanguage;
4use anyhow::anyhow;
5use editor::Editor;
6use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
7use gpui::{
8 actions, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
9 ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
10};
11use language::{Buffer, LanguageRegistry};
12use picker::{Picker, PickerDelegate};
13use project::Project;
14use std::sync::Arc;
15use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
16use util::ResultExt;
17use workspace::{ModalView, Workspace};
18
19actions!(language_selector, [Toggle]);
20
21pub fn init(cx: &mut AppContext) {
22 cx.observe_new_views(LanguageSelector::register).detach();
23}
24
25pub struct LanguageSelector {
26 picker: View<Picker<LanguageSelectorDelegate>>,
27}
28
29impl LanguageSelector {
30 fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
31 workspace.register_action(move |workspace, _: &Toggle, cx| {
32 Self::toggle(workspace, cx);
33 });
34 }
35
36 fn toggle(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<()> {
37 let registry = workspace.app_state().languages.clone();
38 let (_, buffer, _) = workspace
39 .active_item(cx)?
40 .act_as::<Editor>(cx)?
41 .read(cx)
42 .active_excerpt(cx)?;
43 let project = workspace.project().clone();
44
45 workspace.toggle_modal(cx, move |cx| {
46 LanguageSelector::new(buffer, project, registry, cx)
47 });
48 Some(())
49 }
50
51 fn new(
52 buffer: Model<Buffer>,
53 project: Model<Project>,
54 language_registry: Arc<LanguageRegistry>,
55 cx: &mut ViewContext<Self>,
56 ) -> Self {
57 let delegate = LanguageSelectorDelegate::new(
58 cx.view().downgrade(),
59 buffer,
60 project,
61 language_registry,
62 );
63
64 let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
65 Self { picker }
66 }
67}
68
69impl Render for LanguageSelector {
70 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
71 v_flex().w(rems(34.)).child(self.picker.clone())
72 }
73}
74
75impl FocusableView for LanguageSelector {
76 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
77 self.picker.focus_handle(cx)
78 }
79}
80
81impl EventEmitter<DismissEvent> for LanguageSelector {}
82impl ModalView for LanguageSelector {}
83
84pub struct LanguageSelectorDelegate {
85 language_selector: WeakView<LanguageSelector>,
86 buffer: Model<Buffer>,
87 project: Model<Project>,
88 language_registry: Arc<LanguageRegistry>,
89 candidates: Vec<StringMatchCandidate>,
90 matches: Vec<StringMatch>,
91 selected_index: usize,
92}
93
94impl LanguageSelectorDelegate {
95 fn new(
96 language_selector: WeakView<LanguageSelector>,
97 buffer: Model<Buffer>,
98 project: Model<Project>,
99 language_registry: Arc<LanguageRegistry>,
100 ) -> Self {
101 let candidates = language_registry
102 .language_names()
103 .into_iter()
104 .enumerate()
105 .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name))
106 .collect::<Vec<_>>();
107
108 Self {
109 language_selector,
110 buffer,
111 project,
112 language_registry,
113 candidates,
114 matches: vec![],
115 selected_index: 0,
116 }
117 }
118}
119
120impl PickerDelegate for LanguageSelectorDelegate {
121 type ListItem = ListItem;
122
123 fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
124 "Select a language...".into()
125 }
126
127 fn match_count(&self) -> usize {
128 self.matches.len()
129 }
130
131 fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
132 if let Some(mat) = self.matches.get(self.selected_index) {
133 let language_name = &self.candidates[mat.candidate_id].string;
134 let language = self.language_registry.language_for_name(language_name);
135 let project = self.project.downgrade();
136 let buffer = self.buffer.downgrade();
137 cx.spawn(|_, mut cx| async move {
138 let language = language.await?;
139 let project = project
140 .upgrade()
141 .ok_or_else(|| anyhow!("project was dropped"))?;
142 let buffer = buffer
143 .upgrade()
144 .ok_or_else(|| anyhow!("buffer was dropped"))?;
145 project.update(&mut cx, |project, cx| {
146 project.set_language_for_buffer(&buffer, language, cx);
147 })
148 })
149 .detach_and_log_err(cx);
150 }
151 self.dismissed(cx);
152 }
153
154 fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
155 self.language_selector
156 .update(cx, |_, cx| cx.emit(DismissEvent))
157 .log_err();
158 }
159
160 fn selected_index(&self) -> usize {
161 self.selected_index
162 }
163
164 fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
165 self.selected_index = ix;
166 }
167
168 fn update_matches(
169 &mut self,
170 query: String,
171 cx: &mut ViewContext<Picker<Self>>,
172 ) -> gpui::Task<()> {
173 let background = cx.background_executor().clone();
174 let candidates = self.candidates.clone();
175 cx.spawn(|this, mut cx| async move {
176 let matches = if query.is_empty() {
177 candidates
178 .into_iter()
179 .enumerate()
180 .map(|(index, candidate)| StringMatch {
181 candidate_id: index,
182 string: candidate.string,
183 positions: Vec::new(),
184 score: 0.0,
185 })
186 .collect()
187 } else {
188 match_strings(
189 &candidates,
190 &query,
191 false,
192 100,
193 &Default::default(),
194 background,
195 )
196 .await
197 };
198
199 this.update(&mut cx, |this, cx| {
200 let delegate = &mut this.delegate;
201 delegate.matches = matches;
202 delegate.selected_index = delegate
203 .selected_index
204 .min(delegate.matches.len().saturating_sub(1));
205 cx.notify();
206 })
207 .log_err();
208 })
209 }
210
211 fn render_match(
212 &self,
213 ix: usize,
214 selected: bool,
215 cx: &mut ViewContext<Picker<Self>>,
216 ) -> Option<Self::ListItem> {
217 let mat = &self.matches[ix];
218 let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name());
219 let mut label = mat.string.clone();
220 if buffer_language_name.map(|n| n.0).as_deref() == Some(mat.string.as_str()) {
221 label.push_str(" (current)");
222 }
223
224 Some(
225 ListItem::new(ix)
226 .inset(true)
227 .spacing(ListItemSpacing::Sparse)
228 .selected(selected)
229 .child(HighlightedLabel::new(label, mat.positions.clone())),
230 )
231 }
232}