use std::borrow::Cow;
use std::cmp::{max, min};
use std::prelude::v1::*;
use regex::{Captures, Regex};
use tuikit::prelude::*;
use unicode_width::UnicodeWidthChar;
use crate::field::get_string_by_range;
use crate::AnsiString;
use bitflags::_core::str::FromStr;
lazy_static! {
static ref RE_ESCAPE: Regex = Regex::new(r"['\U{00}]").unwrap();
static ref RE_NUMBER: Regex = Regex::new(r"[+|-]?\d+").unwrap();
}
pub fn escape_single_quote(text: &str) -> String {
RE_ESCAPE
.replace_all(text, |x: &Captures| match x.get(0).unwrap().as_str() {
"'" => "'\\''".to_string(),
"\0" => "\\0".to_string(),
_ => "".to_string(),
})
.to_string()
}
pub struct LinePrinter {
start: usize,
end: usize,
current_pos: i32,
screen_col: usize,
row: usize,
col: usize,
tabstop: usize,
shift: usize,
text_width: usize,
container_width: usize,
hscroll_offset: i64,
}
impl LinePrinter {
pub fn builder() -> Self {
LinePrinter {
start: 0,
end: 0,
current_pos: -1,
screen_col: 0,
row: 0,
col: 0,
tabstop: 8,
shift: 0,
text_width: 0,
container_width: 0,
hscroll_offset: 0,
}
}
pub fn row(mut self, row: usize) -> Self {
self.row = row;
self
}
pub fn col(mut self, col: usize) -> Self {
self.col = col;
self
}
pub fn tabstop(mut self, tabstop: usize) -> Self {
self.tabstop = tabstop;
self
}
pub fn hscroll_offset(mut self, offset: i64) -> Self {
self.hscroll_offset = offset;
self
}
pub fn text_width(mut self, width: usize) -> Self {
self.text_width = width;
self
}
pub fn container_width(mut self, width: usize) -> Self {
self.container_width = width;
self
}
pub fn shift(mut self, shift: usize) -> Self {
self.shift = shift;
self
}
pub fn build(mut self) -> Self {
self.reset();
self
}
pub fn reset(&mut self) {
self.current_pos = 0;
self.screen_col = self.col;
self.start = max(self.shift as i64 + self.hscroll_offset, 0) as usize;
self.end = self.start + self.container_width;
}
fn print_ch_to_canvas(&mut self, canvas: &mut dyn Canvas, ch: char, attr: Attr, skip: bool) {
let w = ch.width().unwrap_or(2);
if !skip {
let _ = canvas.put_cell(self.row, self.screen_col, Cell::default().ch(ch).attribute(attr));
}
self.screen_col += w;
}
fn print_char_raw(&mut self, canvas: &mut dyn Canvas, ch: char, attr: Attr, skip: bool) {
let w = ch.width().unwrap_or(2);
assert!(self.current_pos >= 0);
let current = self.current_pos as usize;
if current < self.start || current >= self.end {
} else if current < self.start + 2 && self.start > 0 {
for _ in 0..min(w, current - self.start + 1) {
self.print_ch_to_canvas(canvas, '.', attr, skip);
}
} else if self.end - current <= 2 && (self.text_width > self.end) {
for _ in 0..min(w, self.end - current) {
self.print_ch_to_canvas(canvas, '.', attr, skip);
}
} else {
self.print_ch_to_canvas(canvas, ch, attr, skip);
}
self.current_pos += w as i32;
}
pub fn print_char(&mut self, canvas: &mut dyn Canvas, ch: char, attr: Attr, skip: bool) {
match ch {
'\u{08}' => {
}
'\t' => {
let rest = if self.current_pos < 0 {
self.tabstop
} else {
self.tabstop - (self.current_pos as usize) % self.tabstop
};
for _ in 0..rest {
self.print_char_raw(canvas, ' ', attr, skip);
}
}
ch => self.print_char_raw(canvas, ch, attr, skip),
}
}
}
pub fn print_item(canvas: &mut dyn Canvas, printer: &mut LinePrinter, content: AnsiString, default_attr: Attr) {
for (ch, attr) in content.iter() {
printer.print_char(canvas, ch, default_attr.extend(attr), false);
}
}
pub fn accumulate_text_width(text: &str, tabstop: usize) -> Vec<usize> {
let mut ret = Vec::new();
let mut w = 0;
for ch in text.chars() {
w += if ch == '\t' {
tabstop - (w % tabstop)
} else {
ch.width().unwrap_or(2)
};
ret.push(w);
}
ret
}
pub fn reshape_string(
text: &str,
container_width: usize,
match_start: usize,
match_end: usize,
tabstop: usize,
) -> (usize, usize) {
if text.is_empty() {
return (0, 0);
}
let acc_width = accumulate_text_width(text, tabstop);
let full_width = acc_width[acc_width.len() - 1];
if full_width <= container_width {
return (0, full_width);
}
let w1 = if match_start == 0 {
0
} else {
acc_width[match_start - 1]
};
let w2 = if match_end >= acc_width.len() {
full_width - w1
} else {
acc_width[match_end] - w1
};
let w3 = acc_width[acc_width.len() - 1] - w1 - w2;
if (w1 > w3 && w2 + w3 <= container_width) || (w3 <= 2) {
(full_width - container_width, full_width)
} else if w1 <= w3 && w1 + w2 <= container_width {
(0, full_width)
} else {
(acc_width[match_end] - container_width + 2, full_width)
}
}
pub fn margin_string_to_size(margin: &str) -> Size {
if margin.ends_with('%') {
Size::Percent(min(100, margin[0..margin.len() - 1].parse::<usize>().unwrap_or(100)))
} else {
Size::Fixed(margin.parse::<usize>().unwrap_or(0))
}
}
pub fn parse_margin(margin_option: &str) -> (Size, Size, Size, Size) {
let margins = margin_option.split(',').collect::<Vec<&str>>();
match margins.len() {
1 => {
let margin = margin_string_to_size(margins[0]);
(margin, margin, margin, margin)
}
2 => {
let margin_tb = margin_string_to_size(margins[0]);
let margin_rl = margin_string_to_size(margins[1]);
(margin_tb, margin_rl, margin_tb, margin_rl)
}
3 => {
let margin_top = margin_string_to_size(margins[0]);
let margin_rl = margin_string_to_size(margins[1]);
let margin_bottom = margin_string_to_size(margins[2]);
(margin_top, margin_rl, margin_bottom, margin_rl)
}
4 => {
let margin_top = margin_string_to_size(margins[0]);
let margin_right = margin_string_to_size(margins[1]);
let margin_bottom = margin_string_to_size(margins[2]);
let margin_left = margin_string_to_size(margins[3]);
(margin_top, margin_right, margin_bottom, margin_left)
}
_ => (Size::Fixed(0), Size::Fixed(0), Size::Fixed(0), Size::Fixed(0)),
}
}
#[derive(Copy, Clone)]
pub struct InjectContext<'a> {
pub delimiter: &'a Regex,
pub current_index: usize,
pub current_selection: &'a str,
pub indices: &'a [usize],
pub selections: &'a [&'a str],
pub query: &'a str,
pub cmd_query: &'a str,
}
lazy_static! {
static ref RE_ITEMS: Regex = Regex::new(r"\\?(\{ *-?[0-9.+]*? *})").unwrap();
static ref RE_FIELDS: Regex = Regex::new(r"\\?(\{ *-?[0-9.,cq+n]*? *})").unwrap();
}
pub fn depends_on_items(cmd: &str) -> bool {
RE_ITEMS.is_match(cmd)
}
pub fn inject_command<'a>(cmd: &'a str, context: InjectContext<'a>) -> Cow<'a, str> {
RE_FIELDS.replace_all(cmd, |caps: &Captures| {
if &caps[0][0..1] == "\\" {
return caps[0].to_string();
}
let range = &caps[1];
assert!(range.len() >= 2);
let range = &range[1..range.len() - 1];
let range = range.trim();
if range.starts_with('+') {
let current_selection = vec![context.current_selection];
let selections = if context.selections.is_empty() {
¤t_selection
} else {
context.selections
};
let current_index = vec![context.current_index];
let indices = if context.indices.is_empty() {
¤t_index
} else {
context.indices
};
return selections
.iter()
.zip(indices.iter())
.map(|(&s, &i)| {
let rest = &range[1..];
let index_str = format!("{}", i);
let replacement = match rest {
"" => s,
"n" => &index_str,
_ => get_string_by_range(context.delimiter, s, rest).unwrap_or(""),
};
format!("'{}'", escape_single_quote(replacement))
})
.collect::<Vec<_>>()
.join(" ");
}
let index_str = format!("{}", context.current_index);
let replacement = match range {
"" => context.current_selection,
x if x.starts_with('+') => unreachable!(),
"n" => &index_str,
"q" => context.query,
"cq" => context.cmd_query,
_ => get_string_by_range(context.delimiter, context.current_selection, range).unwrap_or(""),
};
format!("'{}'", escape_single_quote(replacement))
})
}
pub fn str_lines(string: &str) -> Vec<&str> {
string.trim_end().split('\n').collect()
}
pub fn atoi<T: FromStr>(string: &str) -> Option<T> {
RE_NUMBER.find(string).and_then(|mat| mat.as_str().parse::<T>().ok())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_accumulate_text_width() {
assert_eq!(accumulate_text_width("abcdefg", 8), vec![1, 2, 3, 4, 5, 6, 7]);
assert_eq!(accumulate_text_width("ab中de国g", 8), vec![1, 2, 4, 5, 6, 8, 9]);
assert_eq!(accumulate_text_width("ab\tdefg", 8), vec![1, 2, 8, 9, 10, 11, 12]);
assert_eq!(accumulate_text_width("ab中\te国g", 8), vec![1, 2, 4, 8, 9, 11, 12]);
}
#[test]
fn test_reshape_string() {
assert_eq!(reshape_string("abc", 10, 0, 0, 8), (0, 3));
assert_eq!(reshape_string("a\tbc", 8, 0, 0, 8), (0, 10));
assert_eq!(reshape_string("a\tb\tc", 10, 0, 0, 8), (0, 17));
assert_eq!(reshape_string("a\t中b\tc", 8, 0, 0, 8), (0, 17));
assert_eq!(reshape_string("a\t中b\tc012345", 8, 0, 0, 8), (0, 23));
}
#[test]
fn test_inject_command() {
let delimiter = Regex::new(r",").unwrap();
let current_selection = "a,b,c";
let selections = vec!["a,b,c", "x,y,z"];
let query = "query";
let cmd_query = "cmd_query";
let default_context = InjectContext {
current_index: 0,
delimiter: &delimiter,
current_selection,
selections: &selections,
indices: &[0, 1],
query,
cmd_query,
};
assert_eq!("'a,b,c'", inject_command("{}", default_context));
assert_eq!("'a,b,c'", inject_command("{ }", default_context));
assert_eq!("'a'", inject_command("{1}", default_context));
assert_eq!("'b'", inject_command("{2}", default_context));
assert_eq!("'c'", inject_command("{3}", default_context));
assert_eq!("''", inject_command("{4}", default_context));
assert_eq!("'c'", inject_command("{-1}", default_context));
assert_eq!("'b'", inject_command("{-2}", default_context));
assert_eq!("'a'", inject_command("{-3}", default_context));
assert_eq!("''", inject_command("{-4}", default_context));
assert_eq!("'a,b'", inject_command("{1..2}", default_context));
assert_eq!("'b,c'", inject_command("{2..}", default_context));
assert_eq!("'query'", inject_command("{q}", default_context));
assert_eq!("'cmd_query'", inject_command("{cq}", default_context));
assert_eq!("'a,b,c' 'x,y,z'", inject_command("{+}", default_context));
assert_eq!("'0'", inject_command("{n}", default_context));
assert_eq!("'a' 'x'", inject_command("{+1}", default_context));
assert_eq!("'b' 'y'", inject_command("{+2}", default_context));
assert_eq!("'0' '1'", inject_command("{+n}", default_context));
}
#[test]
fn test_escape_single_quote() {
assert_eq!("'\\''a'\\''\\0", escape_single_quote("'a'\0"));
}
#[test]
fn test_atoi() {
assert_eq!(None, atoi::<usize>(""));
assert_eq!(Some(1), atoi::<usize>("1"));
assert_eq!(Some(8589934592), atoi::<usize>("8589934592"));
assert_eq!(Some(1), atoi::<usize>("a1"));
assert_eq!(Some(1), atoi::<usize>("1b"));
assert_eq!(Some(1), atoi::<usize>("a1b"));
assert_eq!(None, atoi::<usize>("-1"));
assert_eq!(Some(-1), atoi::<i32>("a-1b"));
assert_eq!(None, atoi::<i32>("8589934592"));
assert_eq!(Some(123), atoi::<i32>("+'123'"));
}
}