use std::{
collections::HashMap,
path::{Path, PathBuf},
};
use anyhow::Result;
use async_lsp::lsp_types::{CompletionItem, CompletionItemKind};
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use std::sync::OnceLock;
use crate::{
fuzzy::fuzzy_match,
loader::{Dirs, config_dir},
parser::{Parser, StrOrSeq, parse},
variables::{VariableInit, Variables},
};
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct Snippet {
prefix: StrOrSeq, body: StrOrSeq, description: Option<StrOrSeq>,
}
fn to_completion_item(prefix: String, body: String, detail: String) -> CompletionItem {
let mut c = CompletionItem::new_simple(prefix, detail);
c.kind = Some(CompletionItemKind::SNIPPET);
c.insert_text = Some(body);
c
}
impl Snippet {
fn to_completion_item(&self, variable_init: &VariableInit) -> Vec<CompletionItem> {
let body = self.body.to_string();
let body = Variables::replace_all(&body, variable_init);
match &self.prefix {
StrOrSeq::String(s) => {
[to_completion_item(s.to_owned(), body, self.description())].to_vec()
}
StrOrSeq::Array(arr) => arr
.iter()
.map(|s| to_completion_item(s.to_owned(), body.to_owned(), self.description()))
.collect(),
}
}
fn description(&self) -> String {
match &self.description {
Some(s) => s.to_string(),
None => String::new(),
}
}
}
fn snippets_list() -> &'static Mutex<HashMap<String, Snippets>> {
static SNIPPETS: OnceLock<Mutex<HashMap<String, Snippets>>> = OnceLock::new();
SNIPPETS.get_or_init(|| Mutex::new(HashMap::new()))
}
pub(crate) fn snippets_list_clear() {
let mut snippets_list = snippets_list().lock();
snippets_list.clear();
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct Snippets {
name: String,
snippets: HashMap<String, Snippet>,
}
impl Default for Snippets {
fn default() -> Self {
Snippets::new("default".to_owned(), HashMap::new())
}
}
impl Parser for Snippets {
type Item = Snippet;
fn set_name(&mut self, name: String) {
self.name = name;
}
fn set_hasmap(&mut self, hs: HashMap<String, Self::Item>) {
self.snippets = hs;
}
}
impl Snippets {
pub fn new(name: String, snippets: HashMap<String, Snippet>) -> Snippets {
Snippets { name, snippets }
}
pub fn get_global(project_root: &Path) -> Snippets {
let name = "global";
let mut snippets = snippets_list().lock();
match snippets.get(name) {
Some(has) => has.clone(),
None => {
let global_snippets = from_files(
name.to_owned(),
[
read_names(&project_root.join(".helix").join(Dirs::Snippets.to_string())),
read_names(&config_dir(Dirs::Snippets)),
]
.concat(),
);
snippets.insert(name.to_owned(), global_snippets.clone());
global_snippets
}
}
}
pub fn get_lang(lang_name: String, project_root: &Path) -> Snippets {
let mut snippets_list = snippets_list().lock();
match snippets_list.get(&lang_name) {
Some(has) => has.clone(),
None => {
let file_name = format!("{}.json", lang_name.clone().to_lowercase());
let lang_snippets = from_files(
lang_name.clone(),
[
project_root
.join(".helix")
.join(Dirs::Snippets.to_string())
.join(&file_name),
config_dir(Dirs::Snippets).join(&file_name),
]
.to_vec(),
);
snippets_list.insert(lang_name, lang_snippets.clone());
lang_snippets
}
}
}
pub fn extend(&mut self, other: Snippets) {
self.snippets.extend(other.snippets);
}
pub fn to_completion_items(&self, variable_init: &VariableInit) -> Vec<CompletionItem> {
self.snippets
.values()
.map(|snippet| snippet.to_completion_item(variable_init))
.fold(Vec::<CompletionItem>::new(), |mut a, b| {
a.extend(b);
a
})
}
pub fn filter(&self, word: &str) -> Result<Snippets> {
let names: HashMap<String, String> = self
.clone()
.snippets
.into_iter()
.map(|(title, snippet)| (snippet.prefix.to_string(), title))
.collect();
let re = fuzzy_match(word, names.clone().into_keys(), false)
.into_iter()
.filter_map(|(name, _)| names.get(&name))
.filter_map(|f| self.snippets.get_key_value(f))
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
Ok(Snippets::new(self.name.clone(), re))
}
}
fn from_files(name: String, files: Vec<PathBuf>) -> Snippets {
files
.into_iter()
.rev()
.filter(|p| p.exists())
.filter_map(|p| {
parse::<Snippets>(&p, p.file_stem().unwrap().to_string_lossy().into_owned()).ok()
})
.fold(Snippets::new(name, HashMap::new()), |mut acc, map| {
acc.extend(map);
acc
})
}
fn read_names(path: &PathBuf) -> Vec<PathBuf> {
std::fs::read_dir(path)
.map(|entries| {
entries
.filter_map(|entry| {
let entry = entry.ok()?;
let path = entry.path();
(path.extension()? == "code-snippets").then_some(path)
})
.collect()
})
.unwrap_or_default()
}
#[cfg(test)]
mod test {
use super::Snippets;
#[test]
fn test_get_lang() {
let root = std::env::current_dir().ok().unwrap();
let lang = Snippets::get_lang("markdown".to_owned(), &root);
println!("{:?}", lang);
assert_eq!(lang.name, "markdown".to_owned(),);
assert!(lang.snippets.contains_key("markdown a"));
}
}