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