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