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