1use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
2use gpui::{
3 actions, App, Context, DismissEvent, Entity, EventEmitter, Focusable, ParentElement, Render,
4 Styled, WeakEntity, Window,
5};
6use language::LanguageRegistry;
7use paths::config_dir;
8use picker::{Picker, PickerDelegate};
9use std::{borrow::Borrow, fs, sync::Arc};
10use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
11use util::ResultExt;
12use workspace::{notifications::NotifyResultExt, ModalView, Workspace};
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, |_, mut cx| async move {
138 let scope = match scope_name.as_str() {
139 "Global" => "snippets".to_string(),
140 _ => language.await?.lsp_id(),
141 };
142
143 workspace.update_in(&mut cx, |workspace, window, cx| {
144 workspace
145 .open_abs_path(
146 config_dir().join("snippets").join(scope + ".json"),
147 false,
148 window,
149 cx,
150 )
151 .detach();
152 })
153 })
154 .detach_and_log_err(cx);
155 };
156 }
157 self.dismissed(window, cx);
158 }
159
160 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
161 self.scope_selector
162 .update(cx, |_, cx| cx.emit(DismissEvent))
163 .log_err();
164 }
165
166 fn selected_index(&self) -> usize {
167 self.selected_index
168 }
169
170 fn set_selected_index(
171 &mut self,
172 ix: usize,
173 _window: &mut Window,
174 _: &mut Context<Picker<Self>>,
175 ) {
176 self.selected_index = ix;
177 }
178
179 fn update_matches(
180 &mut self,
181 query: String,
182 window: &mut Window,
183 cx: &mut Context<Picker<Self>>,
184 ) -> gpui::Task<()> {
185 let background = cx.background_executor().clone();
186 let candidates = self.candidates.clone();
187 cx.spawn_in(window, |this, mut cx| async move {
188 let matches = if query.is_empty() {
189 candidates
190 .into_iter()
191 .enumerate()
192 .map(|(index, candidate)| StringMatch {
193 candidate_id: index,
194 string: candidate.string,
195 positions: Vec::new(),
196 score: 0.0,
197 })
198 .collect()
199 } else {
200 match_strings(
201 &candidates,
202 &query,
203 false,
204 100,
205 &Default::default(),
206 background,
207 )
208 .await
209 };
210
211 this.update(&mut cx, |this, cx| {
212 let delegate = &mut this.delegate;
213 delegate.matches = matches;
214 delegate.selected_index = delegate
215 .selected_index
216 .min(delegate.matches.len().saturating_sub(1));
217 cx.notify();
218 })
219 .log_err();
220 })
221 }
222
223 fn render_match(
224 &self,
225 ix: usize,
226 selected: bool,
227 _window: &mut Window,
228 _: &mut Context<Picker<Self>>,
229 ) -> Option<Self::ListItem> {
230 let mat = &self.matches[ix];
231 let label = mat.string.clone();
232
233 Some(
234 ListItem::new(ix)
235 .inset(true)
236 .spacing(ListItemSpacing::Sparse)
237 .toggle_state(selected)
238 .child(HighlightedLabel::new(label, mat.positions.clone())),
239 )
240 }
241}