1mod format;
2
3use std::{
4 path::{Path, PathBuf},
5 sync::Arc,
6 time::Duration,
7};
8
9use anyhow::Result;
10use collections::{BTreeMap, BTreeSet, HashMap};
11use format::VSSnippetsFile;
12use fs::Fs;
13use futures::stream::StreamExt;
14use gpui::{AppContext, AsyncAppContext, Context, Model, ModelContext, Task, WeakModel};
15use util::ResultExt;
16
17// Is `None` if the snippet file is global.
18type SnippetKind = Option<String>;
19fn file_stem_to_key(stem: &str) -> SnippetKind {
20 if stem == "snippets" {
21 None
22 } else {
23 Some(stem.to_owned())
24 }
25}
26
27fn file_to_snippets(file_contents: VSSnippetsFile) -> Vec<Arc<Snippet>> {
28 let mut snippets = vec![];
29 for (prefix, snippet) in file_contents.snippets {
30 let prefixes = snippet
31 .prefix
32 .map_or_else(move || vec![prefix], |prefixes| prefixes.into());
33 let description = snippet
34 .description
35 .map(|description| description.to_string());
36 let body = snippet.body.to_string();
37 if snippet::Snippet::parse(&body).log_err().is_none() {
38 continue;
39 };
40 snippets.push(Arc::new(Snippet {
41 body,
42 prefix: prefixes,
43 description,
44 }));
45 }
46 snippets
47}
48// Snippet with all of the metadata
49#[derive(Debug)]
50pub struct Snippet {
51 pub prefix: Vec<String>,
52 pub body: String,
53 pub description: Option<String>,
54}
55
56async fn process_updates(
57 this: WeakModel<SnippetProvider>,
58 entries: Vec<PathBuf>,
59 mut cx: AsyncAppContext,
60) -> Result<()> {
61 let fs = this.update(&mut cx, |this, _| this.fs.clone())?;
62 for entry_path in entries {
63 if !entry_path
64 .extension()
65 .map_or(false, |extension| extension == "json")
66 {
67 continue;
68 }
69 let entry_metadata = fs.metadata(&entry_path).await;
70 // Entry could have been removed, in which case we should no longer show completions for it.
71 let entry_exists = entry_metadata.is_ok();
72 if entry_metadata.map_or(false, |entry| entry.map_or(false, |e| e.is_dir)) {
73 // Don't process dirs.
74 continue;
75 }
76 let Some(stem) = entry_path.file_stem().and_then(|s| s.to_str()) else {
77 continue;
78 };
79 let key = file_stem_to_key(stem);
80
81 let contents = if entry_exists {
82 fs.load(&entry_path).await.ok()
83 } else {
84 None
85 };
86
87 this.update(&mut cx, move |this, _| {
88 let snippets_of_kind = this.snippets.entry(key).or_default();
89 if entry_exists {
90 let Some(file_contents) = contents else {
91 return;
92 };
93 let Ok(as_json) = serde_json::from_str::<VSSnippetsFile>(&file_contents) else {
94 return;
95 };
96 let snippets = file_to_snippets(as_json);
97 *snippets_of_kind.entry(entry_path).or_default() = snippets;
98 } else {
99 snippets_of_kind.remove(&entry_path);
100 }
101 })?;
102 }
103 Ok(())
104}
105
106async fn initial_scan(
107 this: WeakModel<SnippetProvider>,
108 path: Arc<Path>,
109 mut cx: AsyncAppContext,
110) -> Result<()> {
111 let fs = this.update(&mut cx, |this, _| this.fs.clone())?;
112 let entries = fs.read_dir(&path).await;
113 if let Ok(entries) = entries {
114 let entries = entries
115 .collect::<Vec<_>>()
116 .await
117 .into_iter()
118 .collect::<Result<Vec<_>>>()?;
119 process_updates(this, entries, cx).await?;
120 }
121 Ok(())
122}
123
124pub struct SnippetProvider {
125 fs: Arc<dyn Fs>,
126 snippets: HashMap<SnippetKind, BTreeMap<PathBuf, Vec<Arc<Snippet>>>>,
127}
128
129impl SnippetProvider {
130 pub fn new(
131 fs: Arc<dyn Fs>,
132 dirs_to_watch: BTreeSet<PathBuf>,
133 cx: &mut AppContext,
134 ) -> Model<Self> {
135 cx.new_model(move |cx| {
136 let mut this = Self {
137 fs,
138 snippets: Default::default(),
139 };
140
141 let mut task_handles = vec![];
142 for dir in dirs_to_watch {
143 task_handles.push(this.watch_directory(&dir, cx));
144 }
145 cx.spawn(|_, _| async move {
146 futures::future::join_all(task_handles).await;
147 })
148 .detach();
149
150 this
151 })
152 }
153
154 /// Add directory to be watched for content changes
155 fn watch_directory(&mut self, path: &Path, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
156 let path: Arc<Path> = Arc::from(path);
157
158 cx.spawn(|this, mut cx| async move {
159 let fs = this.update(&mut cx, |this, _| this.fs.clone())?;
160 let watched_path = path.clone();
161 let watcher = fs.watch(&watched_path, Duration::from_secs(1));
162 initial_scan(this.clone(), path, cx.clone()).await?;
163
164 let (mut entries, _) = watcher.await;
165 while let Some(entries) = entries.next().await {
166 process_updates(this.clone(), entries, cx.clone()).await?;
167 }
168 Ok(())
169 })
170 }
171 fn lookup_snippets<'a>(
172 &'a self,
173 language: &'a SnippetKind,
174 ) -> Option<impl Iterator<Item = Arc<Snippet>> + 'a> {
175 Some(
176 self.snippets
177 .get(&language)?
178 .iter()
179 .flat_map(|(_, snippets)| snippets.iter().cloned()),
180 )
181 }
182
183 pub fn snippets_for(&self, language: SnippetKind) -> Vec<Arc<Snippet>> {
184 let mut requested_snippets: Vec<_> = self
185 .lookup_snippets(&language)
186 .map(|snippets| snippets.collect())
187 .unwrap_or_default();
188 if language.is_some() {
189 // Look up global snippets as well.
190 if let Some(global_snippets) = self.lookup_snippets(&None) {
191 requested_snippets.extend(global_snippets);
192 }
193 }
194 requested_snippets
195 }
196}