use std::borrow::Cow;
use std::collections::HashMap;
use std::io;
use chrono::{DateTime, Utc};
use regex::Regex;
use syntect::highlighting::Style;
use crate::gist::Gist;
use crate::language::{CodeHighlight, Language};
use crate::utils;
#[derive(Serialize, Deserialize, Debug)]
pub struct Snippet {
#[serde(default)]
pub index: usize,
pub description: String,
pub language: String,
pub code: String,
#[serde(default)]
pub extension: String,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default = "Utc::now")]
pub date: DateTime<Utc>,
#[serde(default = "Utc::now")]
pub updated: DateTime<Utc>,
}
impl Snippet {
fn new(
index: usize,
description: String,
language: String,
extension: String,
tags: &str,
date: DateTime<Utc>,
updated: DateTime<Utc>,
code: String,
) -> Self {
Self {
index,
description,
language,
extension,
tags: utils::split_tags(tags),
date,
updated,
code,
}
}
pub(crate) fn set_extension(
&mut self,
language_name: &str,
languages: &HashMap<String, Language>,
) {
self.extension = Language::get_extension(language_name, languages);
}
pub(crate) fn from_user(
index: usize,
languages: &HashMap<String, Language>,
old_snippet: Option<&Self>,
) -> color_eyre::Result<Self> {
let (old_description, old_language, old_tags, old_date, old_code) = match old_snippet {
Some(s) => (
Some(s.description.as_str()),
Some(s.language.as_str()),
Some(s.tags.join(" ")),
Some(s.date.date().format("%Y-%m-%d").to_string()),
Some(s.code.as_str()),
),
None => (None, None, None, None, None),
};
let description = utils::user_input("Description", old_description, true, false)?;
let language =
utils::user_input("Language", old_language, true, false)?.to_ascii_lowercase();
let extension = Language::get_extension(&language, languages);
let tags = utils::user_input("Tags (space separated)", old_tags.as_deref(), true, true)?;
let date = match old_date {
Some(_) => utils::parse_date(&utils::user_input(
"Date",
old_date.as_deref(),
true,
false,
)?)?
.and_hms(0, 0, 0),
None => Utc::now(),
};
let code = match old_code {
Some(old) => {
if utils::confirm("Edit snippet? [y/N]", false)? {
utils::external_editor_input(old_code, &extension)?
} else {
old.to_string()
}
}
None => {
let mut input = utils::user_input(
"Code snippet (leave empty to open external editor)",
None, false, true, )?;
if input.is_empty() {
input = utils::external_editor_input(None, &extension)?;
}
input
}
};
Ok(Self::new(
index,
description,
language,
extension,
&tags,
date,
Utc::now(),
code,
))
}
pub(crate) fn cmd_from_user(index: usize, code: Option<&str>) -> color_eyre::Result<Self> {
let code = utils::user_input("Command", code, true, false)?;
let description = utils::user_input("Description", None, true, false)?;
let tags = utils::user_input("Tags (space separated)", None, true, true)?;
Ok(Self::new(
index,
description,
"sh".into(),
".sh".into(),
&tags,
Utc::now(),
Utc::now(),
code,
))
}
pub(crate) fn to_bytes(&self) -> color_eyre::Result<Vec<u8>> {
Ok(bincode::serialize(&self)?)
}
pub(crate) fn from_bytes(bytes: &[u8]) -> color_eyre::Result<Self> {
Ok(bincode::deserialize(bytes)?)
}
pub(crate) fn read(
json_reader: &mut dyn io::Read,
) -> impl Iterator<Item = serde_json::Result<Self>> + '_ {
serde_json::Deserializer::from_reader(json_reader).into_iter::<Self>()
}
pub(crate) fn to_json(&self, json_writer: &mut dyn io::Write) -> color_eyre::Result<()> {
serde_json::to_writer(json_writer, self)?;
Ok(())
}
pub(crate) fn from_gist(
start_index: usize,
languages: &HashMap<String, Language>,
gist: &Gist,
) -> Vec<Self> {
let mut index = start_index;
let mut snippets = Vec::new();
for (file_name, gist_file) in &gist.files {
let code = &gist_file.content;
let description = format!("{} - {} - {}", gist.description, gist.id, file_name);
let language = &gist_file.language;
let tags = "gist";
let extension = Language::get_extension(language, languages);
let snippet = Self::new(
index,
description,
language.to_string(),
extension.to_string(),
tags,
Utc::now(),
Utc::now(),
code.to_string(),
);
snippets.push(snippet);
index += 1;
}
snippets
}
pub(crate) fn filter_in_date_range(
snippets: Vec<Self>,
from_date: DateTime<Utc>,
to_date: DateTime<Utc>,
) -> Vec<Self> {
snippets
.into_iter()
.filter(|snippet| snippet.in_date_range(from_date, to_date))
.collect()
}
pub(crate) fn in_date_range(&self, from_date: DateTime<Utc>, to_date: DateTime<Utc>) -> bool {
from_date <= self.date && self.date < to_date
}
pub(crate) fn has_tag(&self, tag: &str) -> bool {
self.tags.contains(&tag.into())
}
pub(crate) fn pretty_print_header(
&self,
highlighter: &CodeHighlight,
language: &Language,
) -> Vec<(Style, String)> {
let mut colorized = Vec::new();
let block = CodeHighlight::highlight_block(language.color);
colorized.push(block);
let text = format!("#{}. {} ", self.index, self.description);
colorized.push((highlighter.main_style, text));
let text = format!("| {} ", self.language);
colorized.push((highlighter.accent_style, text));
let text = format!(":{}:\n", self.tags.join(":"));
colorized.push((highlighter.tag_style, text));
colorized
}
pub(crate) fn pretty_print(
&self,
highlighter: &CodeHighlight,
language: &Language,
) -> Vec<(Style, String)> {
let mut colorized = vec![(Style::default(), String::from("\n"))];
colorized.extend_from_slice(&self.pretty_print_header(highlighter, language));
colorized.push((Style::default(), String::from("\n")));
colorized.extend_from_slice(&highlighter.highlight_code(&self.code, &self.extension));
colorized.push((Style::default(), String::from("\n\n")));
colorized
}
fn is_shell_snippet(&self) -> bool {
matches!(self.language.as_str(), "sh" | "bash" | "csh" | "tcsh")
}
pub(crate) fn fill_snippet(&self, highlight_style: Style) -> color_eyre::Result<Cow<str>> {
if !self.is_shell_snippet() {
return Ok(Cow::Borrowed(self.code.as_str()));
}
let re1 = Regex::new("<(?P<parameter>[^<>]+)>")?;
let re2 = Regex::new("(?P<match><[^<>]+>)")?;
eprintln!(
"{}",
re2.replace_all(&self.code, |caps: ®ex::Captures| {
utils::highlight_string(&caps["match"], highlight_style)
})
);
let mut filled_parameters = HashMap::new();
for capture in re1.captures_iter(&self.code) {
let mut parts = capture["parameter"].split('=');
let parameter_name = parts.next().unwrap().to_owned();
let default = parts.next();
if !filled_parameters.contains_key(¶meter_name) {
let filled = utils::user_input(¶meter_name, default, true, false)?;
filled_parameters.insert(parameter_name, filled);
}
}
Ok(re2.replace_all(&self.code, |caps: ®ex::Captures| {
let parameter = caps["match"][1..caps["match"].len() - 1]
.split('=')
.next()
.unwrap();
&filled_parameters[parameter]
}))
}
}