use std::{
borrow::Cow,
collections::HashSet,
path::{Path, PathBuf},
};
use clap::ValueEnum;
use colored::Colorize;
use serde::{
Deserialize, Serialize,
ser::{SerializeStruct, Serializer},
};
use crate::po::{entry::Entry, message::Message};
const HIGHLIGHT_COLOR: &str = "bright yellow";
const HIGHLIGHT_ON_COLOR: &str = "red";
#[derive(
Debug,
Default,
Clone,
Copy,
PartialEq,
Eq,
Ord,
PartialOrd,
Hash,
Serialize,
Deserialize,
ValueEnum,
)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
#[default]
Info,
Warning,
Error,
}
#[derive(Debug, Default)]
pub struct DiagnosticLine {
pub line_number: usize,
pub message: String,
pub highlights: Vec<(usize, usize)>,
}
#[derive(Debug, Default, Serialize)]
pub struct Diagnostic {
pub path: PathBuf,
pub rule: &'static str,
pub severity: Severity,
pub message: String,
pub lines: Vec<DiagnosticLine>,
pub misspelled_words: HashSet<String>,
}
impl std::fmt::Display for Severity {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let s = match self {
Self::Info => "info".cyan(),
Self::Warning => "warning".yellow(),
Self::Error => "error".bright_red().bold(),
};
write!(f, "{s}")
}
}
impl Serialize for DiagnosticLine {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_struct("DiagnosticLine", 3)?;
state.serialize_field("line_number", &self.line_number)?;
state.serialize_field("message", &self.message)?;
let hl: Vec<_> = self
.highlights
.iter()
.map(|(s, e)| {
(
self.message[..*s].chars().count(),
self.message[..*e].chars().count(),
)
})
.collect();
state.serialize_field("highlights", &hl)?;
state.end()
}
}
impl DiagnosticLine {
fn highlight_list_pos(s: &str, list_pos: &[(usize, usize)]) -> String {
let mut result = String::new();
let mut pos = 0;
for (start, end) in list_pos {
if *start < pos {
continue;
}
result.push_str(&s[pos..*start]);
result.push_str(
&s[*start..*end]
.color(HIGHLIGHT_COLOR)
.bold()
.on_color(HIGHLIGHT_ON_COLOR)
.to_string(),
);
pos = *end;
}
result.push_str(&s[pos..]);
result
}
fn message_hl_color(&self) -> Cow<'_, str> {
if self.highlights.is_empty() {
Cow::Borrowed(&self.message)
} else {
Cow::Owned(Self::highlight_list_pos(&self.message, &self.highlights))
}
}
}
impl Diagnostic {
#[allow(clippy::too_many_arguments)]
pub fn new(path: &Path, rule: &'static str, severity: Severity, message: String) -> Self {
Self {
path: PathBuf::from(path),
rule,
severity,
message,
..Default::default()
}
}
pub fn with_keywords(mut self, entry: &Entry) -> Self {
for line in entry.keywords_to_po_lines() {
self.add_line(0, &line, &[]);
}
self
}
pub fn with_entry(mut self, entry: &Entry) -> Self {
for (line_no, line) in entry.msg_to_po_lines() {
self.add_line(line_no, &line, &[]);
}
self
}
pub fn with_msg(mut self, msg: &Message) -> Self {
self.add_line(msg.line_number, &msg.value, &[]);
self
}
pub fn with_msg_hl(mut self, msg: &Message, hl: &[(usize, usize)]) -> Self {
self.add_line(msg.line_number, &msg.value, hl);
self
}
pub fn with_msgs(mut self, msgid: &Message, msgstr: &Message) -> Self {
self.add_line(msgid.line_number, &msgid.value, &[]);
self.add_line(0, "", &[]);
self.add_line(msgstr.line_number, &msgstr.value, &[]);
self
}
pub fn with_msgs_hl(
mut self,
msgid: &Message,
hl_id: &[(usize, usize)],
msgstr: &Message,
hl_str: &[(usize, usize)],
) -> Self {
self.add_line(msgid.line_number, &msgid.value, hl_id);
self.add_line(0, "", &[]);
self.add_line(msgstr.line_number, &msgstr.value, hl_str);
self
}
pub fn with_multiline(mut self, lines: &str) -> Self {
if !lines.trim().is_empty() {
for line in lines.lines() {
self.add_line(0, line, &[]);
}
}
self
}
pub fn with_misspelled_words(mut self, misspelled_words: HashSet<&str>) -> Self {
self.misspelled_words = misspelled_words.into_iter().map(String::from).collect();
self
}
pub fn add_line(
&mut self,
line: usize,
message: impl Into<String>,
highlights: &[(usize, usize)],
) {
self.lines.push(DiagnosticLine {
line_number: line,
message: message.into(),
highlights: highlights.to_vec(),
});
}
pub fn build_message(&self) -> Cow<'_, str> {
if self.misspelled_words.is_empty() {
Cow::Borrowed(&self.message)
} else {
let mut list_words = self
.misspelled_words
.iter()
.map(String::as_str)
.collect::<Vec<&str>>();
list_words.sort_unstable();
Cow::Owned(format!("{}: {}", self.message, list_words.join(", ")))
}
}
fn format_line(line: &DiagnosticLine) -> String {
let prefix_lf_empty = " | ".cyan().to_string();
let prefix_line = if line.line_number > 0 {
format!("{:7} | ", line.line_number).cyan().to_string()
} else {
prefix_lf_empty.clone()
};
if line.message.is_empty() {
return prefix_line;
}
let mut out = String::new();
for (idx, line) in line.message_hl_color().lines().enumerate() {
if idx == 0 {
out.push_str(&prefix_line);
} else {
out.push('\n');
out.push_str(&prefix_lf_empty);
}
out.push_str(line);
}
out
}
fn format_lines(&self) -> String {
if self.lines.is_empty() {
"\n".to_string()
} else {
let mut list_lines = Vec::with_capacity(self.lines.len() + 2);
list_lines.push(String::new());
list_lines.push(" |".cyan().to_string());
for line in &self.lines {
list_lines.push(Self::format_line(line));
}
list_lines.push(" |".cyan().to_string());
list_lines.push(String::new());
list_lines.join("\n")
}
}
}
impl std::fmt::Display for Diagnostic {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let str_first_line = self
.lines
.iter()
.find(|line| line.line_number > 0)
.map_or_else(String::new, |line| format!(":{}", line.line_number));
write!(
f,
"{}{str_first_line}: [{}:{}] {}{}",
self.path.display().to_string().white().bold(),
self.severity,
self.rule,
self.build_message(),
self.format_lines(),
)
}
}