extension_version_selector.rs

  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}