1use std::str::FromStr;
2use std::sync::Arc;
3
4use client::ExtensionMetadata;
5use extension::{ExtensionSettings, ExtensionStore};
6use fs::Fs;
7use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
8use gpui::{
9 prelude::*, AppContext, DismissEvent, EventEmitter, FocusableView, Task, View, WeakView,
10};
11use picker::{Picker, PickerDelegate};
12use settings::update_settings_file;
13use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
14use util::{ResultExt, SemanticVersion};
15use workspace::ModalView;
16
17pub struct ExtensionVersionSelector {
18 picker: View<Picker<ExtensionVersionSelectorDelegate>>,
19}
20
21impl ModalView for ExtensionVersionSelector {}
22
23impl EventEmitter<DismissEvent> for ExtensionVersionSelector {}
24
25impl FocusableView for ExtensionVersionSelector {
26 fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
27 self.picker.focus_handle(cx)
28 }
29}
30
31impl Render for ExtensionVersionSelector {
32 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
33 v_flex().w(rems(34.)).child(self.picker.clone())
34 }
35}
36
37impl ExtensionVersionSelector {
38 pub fn new(delegate: ExtensionVersionSelectorDelegate, cx: &mut ViewContext<Self>) -> Self {
39 let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
40 Self { picker }
41 }
42}
43
44pub struct ExtensionVersionSelectorDelegate {
45 fs: Arc<dyn Fs>,
46 view: WeakView<ExtensionVersionSelector>,
47 extension_versions: Vec<ExtensionMetadata>,
48 selected_index: usize,
49 matches: Vec<StringMatch>,
50}
51
52impl ExtensionVersionSelectorDelegate {
53 pub fn new(
54 fs: Arc<dyn Fs>,
55 weak_view: WeakView<ExtensionVersionSelector>,
56 mut extension_versions: Vec<ExtensionMetadata>,
57 ) -> Self {
58 extension_versions.sort_unstable_by(|a, b| {
59 let a_version = SemanticVersion::from_str(&a.manifest.version);
60 let b_version = SemanticVersion::from_str(&b.manifest.version);
61
62 match (a_version, b_version) {
63 (Ok(a_version), Ok(b_version)) => b_version.cmp(&a_version),
64 _ => b.published_at.cmp(&a.published_at),
65 }
66 });
67
68 let matches = extension_versions
69 .iter()
70 .map(|extension| StringMatch {
71 candidate_id: 0,
72 score: 0.0,
73 positions: Default::default(),
74 string: format!("v{}", extension.manifest.version),
75 })
76 .collect();
77
78 Self {
79 fs,
80 view: weak_view,
81 extension_versions,
82 selected_index: 0,
83 matches,
84 }
85 }
86}
87
88impl PickerDelegate for ExtensionVersionSelectorDelegate {
89 type ListItem = ui::ListItem;
90
91 fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
92 "Select extension version...".into()
93 }
94
95 fn match_count(&self) -> usize {
96 self.matches.len()
97 }
98
99 fn selected_index(&self) -> usize {
100 self.selected_index
101 }
102
103 fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
104 self.selected_index = ix;
105 }
106
107 fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
108 let background_executor = cx.background_executor().clone();
109 let candidates = self
110 .extension_versions
111 .iter()
112 .enumerate()
113 .map(|(id, extension)| {
114 let text = format!("v{}", extension.manifest.version);
115
116 StringMatchCandidate {
117 id,
118 char_bag: text.as_str().into(),
119 string: text,
120 }
121 })
122 .collect::<Vec<_>>();
123
124 cx.spawn(move |this, mut cx| async move {
125 let matches = if query.is_empty() {
126 candidates
127 .into_iter()
128 .enumerate()
129 .map(|(index, candidate)| StringMatch {
130 candidate_id: index,
131 string: candidate.string,
132 positions: Vec::new(),
133 score: 0.0,
134 })
135 .collect()
136 } else {
137 match_strings(
138 &candidates,
139 &query,
140 false,
141 100,
142 &Default::default(),
143 background_executor,
144 )
145 .await
146 };
147
148 this.update(&mut cx, |this, _cx| {
149 this.delegate.matches = matches;
150 this.delegate.selected_index = this
151 .delegate
152 .selected_index
153 .min(this.delegate.matches.len().saturating_sub(1));
154 })
155 .log_err();
156 })
157 }
158
159 fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
160 if self.matches.is_empty() {
161 self.dismissed(cx);
162 return;
163 }
164
165 let candidate_id = self.matches[self.selected_index].candidate_id;
166 let extension_version = &self.extension_versions[candidate_id];
167
168 if !extension::is_version_compatible(extension_version) {
169 return;
170 }
171
172 let extension_store = ExtensionStore::global(cx);
173 extension_store.update(cx, |store, cx| {
174 let extension_id = extension_version.id.clone();
175 let version = extension_version.manifest.version.clone();
176
177 update_settings_file::<ExtensionSettings>(self.fs.clone(), cx, {
178 let extension_id = extension_id.clone();
179 move |settings| {
180 settings.auto_update_extensions.insert(extension_id, false);
181 }
182 });
183
184 store.install_extension(extension_id, version, cx);
185 });
186 }
187
188 fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
189 self.view
190 .update(cx, |_, cx| cx.emit(DismissEvent))
191 .log_err();
192 }
193
194 fn render_match(
195 &self,
196 ix: usize,
197 selected: bool,
198 _cx: &mut ViewContext<Picker<Self>>,
199 ) -> Option<Self::ListItem> {
200 let version_match = &self.matches[ix];
201 let extension_version = &self.extension_versions[version_match.candidate_id];
202
203 let is_version_compatible = extension::is_version_compatible(extension_version);
204 let disabled = !is_version_compatible;
205
206 Some(
207 ListItem::new(ix)
208 .inset(true)
209 .spacing(ListItemSpacing::Sparse)
210 .selected(selected)
211 .disabled(disabled)
212 .child(
213 HighlightedLabel::new(
214 version_match.string.clone(),
215 version_match.positions.clone(),
216 )
217 .when(disabled, |label| label.color(Color::Muted)),
218 )
219 .end_slot(
220 h_flex()
221 .gap_2()
222 .when(!is_version_compatible, |this| {
223 this.child(Label::new("Incompatible").color(Color::Muted))
224 })
225 .child(
226 Label::new(
227 extension_version
228 .published_at
229 .format("%Y-%m-%d")
230 .to_string(),
231 )
232 .when(disabled, |label| label.color(Color::Muted)),
233 ),
234 ),
235 )
236 }
237}