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