lib.rs

  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}