snippets_ui.rs

  1use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
  2use gpui::{
  3    App, Context, DismissEvent, Entity, EventEmitter, Focusable, ParentElement, Render, Styled,
  4    WeakEntity, Window, actions,
  5};
  6use language::LanguageRegistry;
  7use paths::config_dir;
  8use picker::{Picker, PickerDelegate};
  9use std::{borrow::Borrow, fs, sync::Arc};
 10use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*};
 11use util::ResultExt;
 12use workspace::{ModalView, OpenOptions, OpenVisible, Workspace, notifications::NotifyResultExt};
 13
 14actions!(snippets, [ConfigureSnippets, OpenFolder]);
 15
 16pub fn init(cx: &mut App) {
 17    cx.observe_new(register).detach();
 18}
 19
 20fn register(workspace: &mut Workspace, _window: Option<&mut Window>, _: &mut Context<Workspace>) {
 21    workspace.register_action(configure_snippets);
 22    workspace.register_action(open_folder);
 23}
 24
 25fn configure_snippets(
 26    workspace: &mut Workspace,
 27    _: &ConfigureSnippets,
 28    window: &mut Window,
 29    cx: &mut Context<Workspace>,
 30) {
 31    let language_registry = workspace.app_state().languages.clone();
 32    let workspace_handle = workspace.weak_handle();
 33
 34    workspace.toggle_modal(window, cx, move |window, cx| {
 35        ScopeSelector::new(language_registry, workspace_handle, window, cx)
 36    });
 37}
 38
 39fn open_folder(
 40    workspace: &mut Workspace,
 41    _: &OpenFolder,
 42    _: &mut Window,
 43    cx: &mut Context<Workspace>,
 44) {
 45    fs::create_dir_all(config_dir().join("snippets")).notify_err(workspace, cx);
 46    cx.open_with_system(config_dir().join("snippets").borrow());
 47}
 48
 49pub struct ScopeSelector {
 50    picker: Entity<Picker<ScopeSelectorDelegate>>,
 51}
 52
 53impl ScopeSelector {
 54    fn new(
 55        language_registry: Arc<LanguageRegistry>,
 56        workspace: WeakEntity<Workspace>,
 57        window: &mut Window,
 58        cx: &mut Context<Self>,
 59    ) -> Self {
 60        let delegate =
 61            ScopeSelectorDelegate::new(workspace, cx.entity().downgrade(), language_registry);
 62
 63        let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
 64
 65        Self { picker }
 66    }
 67}
 68
 69impl ModalView for ScopeSelector {}
 70
 71impl EventEmitter<DismissEvent> for ScopeSelector {}
 72
 73impl Focusable for ScopeSelector {
 74    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
 75        self.picker.focus_handle(cx)
 76    }
 77}
 78
 79impl Render for ScopeSelector {
 80    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
 81        v_flex().w(rems(34.)).child(self.picker.clone())
 82    }
 83}
 84
 85pub struct ScopeSelectorDelegate {
 86    workspace: WeakEntity<Workspace>,
 87    scope_selector: WeakEntity<ScopeSelector>,
 88    language_registry: Arc<LanguageRegistry>,
 89    candidates: Vec<StringMatchCandidate>,
 90    matches: Vec<StringMatch>,
 91    selected_index: usize,
 92}
 93
 94impl ScopeSelectorDelegate {
 95    fn new(
 96        workspace: WeakEntity<Workspace>,
 97        scope_selector: WeakEntity<ScopeSelector>,
 98        language_registry: Arc<LanguageRegistry>,
 99    ) -> Self {
100        let candidates = Vec::from(["Global".to_string()]).into_iter();
101        let languages = language_registry.language_names().into_iter();
102
103        let candidates = candidates
104            .chain(languages)
105            .enumerate()
106            .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, &name))
107            .collect::<Vec<_>>();
108
109        Self {
110            workspace,
111            scope_selector,
112            language_registry,
113            candidates,
114            matches: vec![],
115            selected_index: 0,
116        }
117    }
118}
119
120impl PickerDelegate for ScopeSelectorDelegate {
121    type ListItem = ListItem;
122
123    fn placeholder_text(&self, _window: &mut Window, _: &mut App) -> Arc<str> {
124        "Select snippet scope...".into()
125    }
126
127    fn match_count(&self) -> usize {
128        self.matches.len()
129    }
130
131    fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
132        if let Some(mat) = self.matches.get(self.selected_index) {
133            let scope_name = self.candidates[mat.candidate_id].string.clone();
134            let language = self.language_registry.language_for_name(&scope_name);
135
136            if let Some(workspace) = self.workspace.upgrade() {
137                cx.spawn_in(window, async move |_, cx| {
138                    let scope = match scope_name.as_str() {
139                        "Global" => "snippets".to_string(),
140                        _ => language.await?.lsp_id(),
141                    };
142
143                    workspace.update_in(cx, |workspace, window, cx| {
144                        workspace
145                            .open_abs_path(
146                                config_dir().join("snippets").join(scope + ".json"),
147                                OpenOptions {
148                                    visible: Some(OpenVisible::None),
149                                    ..Default::default()
150                                },
151                                window,
152                                cx,
153                            )
154                            .detach();
155                    })
156                })
157                .detach_and_log_err(cx);
158            };
159        }
160        self.dismissed(window, cx);
161    }
162
163    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
164        self.scope_selector
165            .update(cx, |_, cx| cx.emit(DismissEvent))
166            .log_err();
167    }
168
169    fn selected_index(&self) -> usize {
170        self.selected_index
171    }
172
173    fn set_selected_index(
174        &mut self,
175        ix: usize,
176        _window: &mut Window,
177        _: &mut Context<Picker<Self>>,
178    ) {
179        self.selected_index = ix;
180    }
181
182    fn update_matches(
183        &mut self,
184        query: String,
185        window: &mut Window,
186        cx: &mut Context<Picker<Self>>,
187    ) -> gpui::Task<()> {
188        let background = cx.background_executor().clone();
189        let candidates = self.candidates.clone();
190        cx.spawn_in(window, async move |this, cx| {
191            let matches = if query.is_empty() {
192                candidates
193                    .into_iter()
194                    .enumerate()
195                    .map(|(index, candidate)| StringMatch {
196                        candidate_id: index,
197                        string: candidate.string,
198                        positions: Vec::new(),
199                        score: 0.0,
200                    })
201                    .collect()
202            } else {
203                match_strings(
204                    &candidates,
205                    &query,
206                    false,
207                    100,
208                    &Default::default(),
209                    background,
210                )
211                .await
212            };
213
214            this.update(cx, |this, cx| {
215                let delegate = &mut this.delegate;
216                delegate.matches = matches;
217                delegate.selected_index = delegate
218                    .selected_index
219                    .min(delegate.matches.len().saturating_sub(1));
220                cx.notify();
221            })
222            .log_err();
223        })
224    }
225
226    fn render_match(
227        &self,
228        ix: usize,
229        selected: bool,
230        _window: &mut Window,
231        _: &mut Context<Picker<Self>>,
232    ) -> Option<Self::ListItem> {
233        let mat = &self.matches[ix];
234        let label = mat.string.clone();
235
236        Some(
237            ListItem::new(ix)
238                .inset(true)
239                .spacing(ListItemSpacing::Sparse)
240                .toggle_state(selected)
241                .child(HighlightedLabel::new(label, mat.positions.clone())),
242        )
243    }
244}