extension_version_selector.rs

  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}