pub mod plugin;
use crate::ported::lib::humanize_bytes::humanize_bytes;
use std::collections::HashMap;
use std::sync::OnceLock;
pub fn window_cached<F>(func: F) -> F {
func
}
pub fn vim_modes() -> &'static HashMap<&'static str, &'static str> {
static M: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
M.get_or_init(|| {
let mut m = HashMap::new();
m.insert("n", "NORMAL");
m.insert("no", "N-OPER");
m.insert("v", "VISUAL");
m.insert("V", "V-LINE");
m.insert("^V", "V-BLCK");
m.insert("s", "SELECT");
m.insert("S", "S-LINE");
m.insert("^S", "S-BLCK");
m.insert("i", "INSERT");
m.insert("ic", "I-COMP");
m.insert("ix", "I-C_X ");
m.insert("R", "RPLACE");
m.insert("Rv", "V-RPLC");
m.insert("Rc", "R-COMP");
m.insert("Rx", "R-C_X ");
m.insert("c", "COMMND");
m.insert("cv", "VIM-EX");
m.insert("ce", "NRM-EX");
m.insert("r", "PROMPT");
m.insert("rm", "-MORE-");
m.insert("r?", "CNFIRM");
m.insert("!", "!SHELL");
m.insert("t", "TERM ");
m
})
}
pub fn mode_translation(mode_code: &str, override_map: Option<&HashMap<String, String>>) -> String {
if let Some(o) = override_map {
if let Some(s) = o.get(mode_code) {
return s.clone();
}
}
vim_modes()
.get(mode_code)
.copied()
.map(String::from)
.unwrap_or_else(|| mode_code.to_string())
}
pub fn position_value(
winline_first: i64,
winline_last: i64,
line_last: i64,
) -> (f64, PositionContent) {
if winline_first == 1 && winline_last == line_last {
return (0.0, PositionContent::All);
}
if winline_first == 1 {
return (0.0, PositionContent::Top);
}
if winline_last == line_last {
return (100.0, PositionContent::Bottom);
}
let pct = winline_first as f64 * 100.0 / (line_last - winline_last + winline_first) as f64;
(pct, PositionContent::Percent(pct.round() as u32))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PositionContent {
All,
Top,
Bottom,
Percent(u32),
}
pub fn line_percent_value(line_current: u64, line_count: u64) -> (u32, f64) {
if line_count == 0 {
return (0, 0.0);
}
let pct = line_current as f64 * 100.0 / line_count as f64;
(pct.round() as u32, pct)
}
pub fn visual_range_text(
mode_code: &str,
rows: u64,
vcols: u64,
ctrl_v_text: &str,
v_oneline: &str,
v_multiline: &str,
v_block_text: &str,
) -> String {
match mode_code {
"^V" => ctrl_v_text
.replace("{rows}", &rows.to_string())
.replace("{vcols}", &vcols.to_string()),
"v" => {
if rows == 1 {
v_oneline.replace("{vcols}", &vcols.to_string())
} else {
v_multiline.replace("{rows}", &rows.to_string())
}
}
"V" => v_block_text.replace("{rows}", &rows.to_string()),
_ => String::new(),
}
}
pub fn file_size_text(bytes_count: i64, suffix: &str, si_prefix: bool) -> Option<String> {
if bytes_count <= 0 {
return None;
}
Some(humanize_bytes(bytes_count as f64, suffix, si_prefix))
}
pub fn default_position_strings() -> HashMap<&'static str, &'static str> {
let mut m = HashMap::new();
m.insert("top", "Top");
m.insert("bottom", "Bot");
m.insert("all", "All");
m
}
#[allow(non_snake_case)]
pub fn SCHEME_RE() -> &'static regex::Regex {
static R: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
R.get_or_init(|| regex::Regex::new(r"^(\w[\w\d+\-.]*):").unwrap())
}
pub fn mode(mode_str: &str, override_map: Option<&HashMap<String, String>>) -> Option<String> {
if mode_str == "nc" {
return None;
}
let mut current = mode_str.to_string();
let modes = vim_modes();
loop {
if current.is_empty() {
break;
}
if let Some(map) = override_map {
if let Some(v) = map.get(¤t) {
return Some(v.clone());
}
}
if let Some(v) = modes.get(current.as_str()) {
return Some(v.to_string());
}
current.pop();
}
Some("BUG".to_string())
}
pub fn modified_indicator(modified: bool, text: &str) -> Option<String> {
if modified {
Some(text.to_string())
} else {
None
}
}
pub fn paste_indicator(paste_enabled: bool, text: &str) -> Option<String> {
if paste_enabled {
Some(text.to_string())
} else {
None
}
}
pub fn readonly_indicator(readonly: bool, text: &str) -> Option<String> {
if readonly {
Some(text.to_string())
} else {
None
}
}
pub fn file_scheme(name: &str) -> Option<String> {
if name.is_empty() {
return None;
}
SCHEME_RE()
.captures(name)
.and_then(|c| c.get(1))
.map(|m| m.as_str().to_string())
}
pub fn file_format(fileformat: &str) -> Option<String> {
if fileformat.is_empty() {
None
} else {
Some(fileformat.to_string())
}
}
pub fn file_encoding(fileencoding: &str) -> Option<String> {
if fileencoding.is_empty() {
None
} else {
Some(fileencoding.to_string())
}
}
pub fn file_bom(bomb: bool) -> Option<&'static str> {
if bomb {
Some("bom")
} else {
None
}
}
pub fn file_type(filetype: &str) -> Option<String> {
if filetype.is_empty() {
None
} else {
Some(filetype.to_string())
}
}
pub fn line_percent(line_current: u64, line_last: u64, gradient: bool) -> serde_json::Value {
let percentage = (line_current as f64) * 100.0 / (line_last.max(1) as f64);
let rounded = percentage.round() as i64;
if !gradient {
return serde_json::Value::String(rounded.to_string());
}
serde_json::json!([{
"contents": rounded.to_string(),
"highlight_groups": ["line_percent_gradient", "line_percent"],
"gradient_level": percentage,
}])
}
pub fn position(
line_last: u64,
winline_first: u64,
winline_last: u64,
position_strings: &HashMap<&str, &str>,
gradient: bool,
) -> serde_json::Value {
let (percentage, content) = if winline_first == 1 && winline_last == line_last {
(
0.0_f64,
position_strings
.get("all")
.copied()
.unwrap_or("All")
.to_string(),
)
} else if winline_first == 1 {
(
0.0_f64,
position_strings
.get("top")
.copied()
.unwrap_or("Top")
.to_string(),
)
} else if winline_last == line_last {
(
100.0_f64,
position_strings
.get("bottom")
.copied()
.unwrap_or("Bot")
.to_string(),
)
} else {
let pct = (winline_first as f64) * 100.0
/ ((line_last as f64) - (winline_last as f64) + (winline_first as f64));
let s = format!("{}%", pct.round() as i64);
(pct, s)
};
if !gradient {
return serde_json::Value::String(content);
}
serde_json::json!([{
"contents": content,
"highlight_groups": ["position_gradient", "position"],
"gradient_level": percentage,
}])
}
pub fn line_current(cursor_line: u64) -> String {
cursor_line.to_string()
}
pub fn line_count(buffer_len: u64) -> String {
buffer_len.to_string()
}
pub fn col_current(cursor_col: u64) -> String {
(cursor_col + 1).to_string()
}
pub fn virtcol_current(virtcol: u64, textwidth: u64, gradient: bool) -> serde_json::Value {
let mut entry = serde_json::json!({
"contents": virtcol.to_string(),
"highlight_groups": ["virtcol_current", "col_current"],
});
if gradient {
let level: f64 = if textwidth > 0 {
((virtcol as f64) * 100.0 / (textwidth as f64)).min(100.0)
} else {
0.0
};
let hl = entry["highlight_groups"]
.as_array_mut()
.expect("highlight_groups initialised as array above");
hl.insert(
0,
serde_json::Value::String("virtcol_current_gradient".to_string()),
);
entry["gradient_level"] = serde_json::Value::from(level);
}
serde_json::json!([entry])
}
pub fn modified_buffers(modified_bufnrs: &[u64], text: &str, join_str: &str) -> Option<String> {
if modified_bufnrs.is_empty() {
return None;
}
let numbers: Vec<String> = modified_bufnrs.iter().map(|n| n.to_string()).collect();
let joined = numbers.join(join_str);
Some(format!("{}{}", text, joined))
}
pub fn tabnr(this_tabnr: u64, current_tabnr: u64, show_current: bool) -> Option<String> {
if show_current || this_tabnr != current_tabnr {
Some(this_tabnr.to_string())
} else {
None
}
}
pub fn bufnr(this_bufnr: u64, current_bufnr: u64, show_current: bool) -> Option<String> {
if show_current || this_bufnr != current_bufnr {
Some(this_bufnr.to_string())
} else {
None
}
}
pub fn winnr(this_winnr: u64, current_winnr: u64, show_current: bool) -> Option<String> {
if show_current || this_winnr != current_winnr {
Some(this_winnr.to_string())
} else {
None
}
}
pub fn tab_modified_indicator(has_modified_buffer: bool, text: &str) -> Option<serde_json::Value> {
if has_modified_buffer {
Some(serde_json::json!([{
"contents": text,
"highlight_groups": ["tab_modified_indicator", "modified_indicator"],
}]))
} else {
None
}
}
pub fn window_title(quickfix_title: Option<&str>) -> Option<String> {
quickfix_title.map(String::from)
}
pub fn file_size(byte_count: i64, suffix: &str, si_prefix: bool) -> Option<String> {
let count = byte_count.max(0);
Some(humanize_bytes(count as f64, suffix, si_prefix))
}
pub fn detect_text_csv_dialect(
sniffed_delimiter: char,
display_name: &str,
auto_has_header: bool,
) -> (char, bool) {
let has_header = if display_name == "auto" {
auto_has_header
} else {
!display_name.is_empty() && display_name != "false"
};
(sniffed_delimiter, has_header)
}
pub fn ret<C>(
window_id: u64,
mode: &str,
cache: &std::collections::HashMap<u64, String>,
compute: C,
) -> Option<String>
where
C: FnOnce() -> Option<String>,
{
if mode == "nc" {
return cache.get(&window_id).cloned();
}
compute()
}
pub fn visual_range(
mode: &str,
rows: u64,
vcols: u64,
ctrl_v_text: &str,
v_text_oneline: &str,
v_text_multiline: &str,
v_text: &str,
) -> Option<String> {
let r = visual_range_text(
mode,
rows,
vcols,
ctrl_v_text,
v_text_oneline,
v_text_multiline,
v_text,
);
if r.is_empty() {
None
} else {
Some(r)
}
}
pub fn file_directory(
dirname: Option<&str>,
shorten_home: bool,
shorten_cwd: bool,
home: Option<&str>,
cwd: Option<&str>,
) -> Option<String> {
let mut path = dirname?.to_string();
if shorten_home {
if let Some(h) = home {
if let Some(stripped) = path.strip_prefix(h) {
path = format!("~{}", stripped);
}
}
}
if shorten_cwd {
if let Some(c) = cwd {
if path == c {
return Some(".".to_string());
}
}
}
Some(path)
}
pub fn file_name(name: Option<&str>, display_no_file: bool, no_file_text: &str) -> Option<String> {
match name {
Some(n) if !n.is_empty() => Some(n.to_string()),
_ if display_no_file => Some(no_file_text.to_string()),
_ => None,
}
}
pub fn get_directory(buftype: &str, buffer_name: Option<&str>) -> Option<String> {
if !buftype.is_empty() {
return None;
}
buffer_name.map(String::from)
}
pub fn file_vcs_status(
status: Option<&str>,
highlight_group: Option<&str>,
) -> Option<serde_json::Value> {
let status = status?;
if status.is_empty() {
return None;
}
let mut seg = serde_json::Map::new();
seg.insert(
"contents".to_string(),
serde_json::Value::String(status.to_string()),
);
if let Some(hg) = highlight_group {
seg.insert(
"highlight_groups".to_string(),
serde_json::Value::Array(vec![serde_json::Value::String(hg.to_string())]),
);
}
Some(serde_json::Value::Object(seg))
}
pub fn trailing_whitespace(first_match_line: i64) -> Option<String> {
if first_match_line > 0 {
Some(first_match_line.to_string())
} else {
None
}
}
pub fn read_csv(line: &str, dialect: char) -> Vec<String> {
line.split(dialect).map(String::from).collect()
}
pub fn process_csv_buffer(
cursor_col: usize,
header: &[String],
display_name: bool,
) -> Option<(usize, Option<String>)> {
if cursor_col >= header.len() {
return None;
}
let column_name = if display_name {
Some(header[cursor_col].clone())
} else {
None
};
Some((cursor_col + 1, column_name))
}
pub fn csv_col_current(
column_index: usize,
column_name: Option<&str>,
name_format: &str,
) -> Option<String> {
let mut out = (column_index + 1).to_string();
if let Some(name) = column_name {
let truncated: String = name.chars().take(15).collect();
out.push_str(&name_format.replace("{column_name:.15}", &truncated));
}
Some(out)
}
pub fn tab(tabnr: Option<u64>, end: bool) -> Option<serde_json::Value> {
let n = tabnr?;
let literal = if end {
"%T".to_string()
} else {
format!("%{}T", n)
};
Some(serde_json::json!([{
"contents": serde_json::Value::Null,
"literal_contents": [0, literal],
}]))
}
pub const CSV_SNIFF_LINES: usize = 100;
pub const CSV_PARSE_LINES: usize = 10;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn vim_modes_has_24_entries() {
let m = vim_modes();
assert_eq!(m.len(), 23); }
#[test]
fn vim_modes_normal_translates_to_normal() {
let m = vim_modes();
assert_eq!(m.get("n"), Some(&"NORMAL"));
}
#[test]
fn vim_modes_visual_line_translates() {
let m = vim_modes();
assert_eq!(m.get("V"), Some(&"V-LINE"));
}
#[test]
fn vim_modes_insert_translates() {
let m = vim_modes();
assert_eq!(m.get("i"), Some(&"INSERT"));
}
#[test]
fn vim_modes_terminal_translates() {
let m = vim_modes();
assert_eq!(m.get("t"), Some(&"TERM "));
}
#[test]
fn mode_translation_falls_back_to_vim_modes() {
let r = mode_translation("n", None);
assert_eq!(r, "NORMAL");
}
#[test]
fn mode_translation_unknown_returns_input() {
let r = mode_translation("unknown_mode", None);
assert_eq!(r, "unknown_mode");
}
#[test]
fn mode_translation_override_takes_precedence() {
let mut o = HashMap::new();
o.insert("n".to_string(), "Normaal".to_string());
let r = mode_translation("n", Some(&o));
assert_eq!(r, "Normaal");
}
#[test]
fn mode_translation_override_with_no_match_falls_through() {
let mut o = HashMap::new();
o.insert("v".to_string(), "Visueel".to_string());
let r = mode_translation("n", Some(&o));
assert_eq!(r, "NORMAL");
}
#[test]
fn position_value_all_visible_returns_all() {
let (pct, c) = position_value(1, 100, 100);
assert_eq!(pct, 0.0);
assert_eq!(c, PositionContent::All);
}
#[test]
fn position_value_at_top_returns_top() {
let (pct, c) = position_value(1, 50, 100);
assert_eq!(pct, 0.0);
assert_eq!(c, PositionContent::Top);
}
#[test]
fn position_value_at_bottom_returns_bottom() {
let (pct, c) = position_value(50, 100, 100);
assert_eq!(pct, 100.0);
assert_eq!(c, PositionContent::Bottom);
}
#[test]
fn position_value_middle_returns_percent() {
let (pct, c) = position_value(50, 80, 100);
assert!((pct - 71.428_571).abs() < 1e-3);
match c {
PositionContent::Percent(_) => {}
_ => panic!("expected Percent"),
}
}
#[test]
fn line_percent_value_zero_total_returns_zero() {
let (i, f) = line_percent_value(0, 0);
assert_eq!(i, 0);
assert_eq!(f, 0.0);
}
#[test]
fn line_percent_value_at_start_is_low() {
let (i, _f) = line_percent_value(1, 100);
assert_eq!(i, 1);
}
#[test]
fn line_percent_value_at_end_is_100() {
let (i, _f) = line_percent_value(100, 100);
assert_eq!(i, 100);
}
#[test]
fn line_percent_value_midpoint_is_50() {
let (i, _f) = line_percent_value(50, 100);
assert_eq!(i, 50);
}
#[test]
fn visual_range_text_blockwise_uses_ctrl_v_format() {
let r = visual_range_text(
"^V",
3,
5,
"{rows} x {vcols}",
"C:{vcols}",
"L:{rows}",
"L:{rows}",
);
assert_eq!(r, "3 x 5");
}
#[test]
fn visual_range_text_visual_oneline_uses_v_oneline() {
let r = visual_range_text(
"v",
1,
5,
"{rows} x {vcols}",
"C:{vcols}",
"L:{rows}",
"L:{rows}",
);
assert_eq!(r, "C:5");
}
#[test]
fn visual_range_text_visual_multiline_uses_v_multiline() {
let r = visual_range_text(
"v",
3,
5,
"{rows} x {vcols}",
"C:{vcols}",
"L:{rows}",
"L:{rows}",
);
assert_eq!(r, "L:3");
}
#[test]
fn visual_range_text_v_line_uses_v_block_text() {
let r = visual_range_text(
"V",
3,
5,
"{rows} x {vcols}",
"C:{vcols}",
"L:{rows}",
"L:{rows}",
);
assert_eq!(r, "L:3");
}
#[test]
fn visual_range_text_normal_mode_returns_empty() {
let r = visual_range_text(
"n",
3,
5,
"{rows} x {vcols}",
"C:{vcols}",
"L:{rows}",
"L:{rows}",
);
assert!(r.is_empty());
}
#[test]
fn file_size_text_zero_returns_none() {
let r = file_size_text(0, "B", false);
assert!(r.is_none());
}
#[test]
fn file_size_text_positive_formats_via_humanize_bytes() {
let r = file_size_text(1024, "B", false);
assert!(r.is_some());
}
#[test]
fn default_position_strings_matches_upstream() {
let s = default_position_strings();
assert_eq!(s.get("top"), Some(&"Top"));
assert_eq!(s.get("bottom"), Some(&"Bot"));
assert_eq!(s.get("all"), Some(&"All"));
}
#[test]
fn window_cached_passes_function_through() {
let f = window_cached(|x: u32| x + 1);
assert_eq!(f(5), 6);
}
#[test]
fn scheme_re_matches_zipfile_prefix() {
let re = SCHEME_RE();
let c = re.captures("zipfile:/path/x.zip::file.txt").unwrap();
assert_eq!(c.get(1).unwrap().as_str(), "zipfile");
}
#[test]
fn scheme_re_accepts_digit_prefix() {
let re = SCHEME_RE();
assert!(re.find("1bad:foo").is_some());
}
#[test]
fn scheme_re_no_match_when_no_colon() {
let re = SCHEME_RE();
assert!(re.find("plain/file.txt").is_none());
}
#[test]
fn mode_returns_none_for_nc() {
assert_eq!(mode("nc", None), None);
}
#[test]
fn mode_translates_normal() {
assert_eq!(mode("n", None), Some("NORMAL".to_string()));
}
#[test]
fn mode_translates_visual_block_via_caret_v() {
assert_eq!(mode("^V", None), Some("V-BLCK".to_string()));
}
#[test]
fn mode_trims_unknown_suffix() {
assert_eq!(mode("iXXX", None), Some("INSERT".to_string()));
}
#[test]
fn mode_override_takes_precedence() {
let mut o = HashMap::new();
o.insert("n".to_string(), "NORM".to_string());
assert_eq!(mode("n", Some(&o)), Some("NORM".to_string()));
}
#[test]
fn mode_empty_returns_bug() {
assert_eq!(mode("", None), Some("BUG".to_string()));
}
#[test]
fn modified_indicator_returns_text_when_modified() {
assert_eq!(modified_indicator(true, "+"), Some("+".to_string()));
assert_eq!(modified_indicator(false, "+"), None);
}
#[test]
fn paste_indicator_returns_text_when_paste_enabled() {
assert_eq!(paste_indicator(true, "PASTE"), Some("PASTE".to_string()));
assert_eq!(paste_indicator(false, "PASTE"), None);
}
#[test]
fn readonly_indicator_returns_text_when_readonly() {
assert_eq!(readonly_indicator(true, "RO"), Some("RO".to_string()));
assert_eq!(readonly_indicator(false, "RO"), None);
}
#[test]
fn file_scheme_extracts_prefix() {
assert_eq!(
file_scheme("zipfile:/path/x.zip::file.txt"),
Some("zipfile".to_string())
);
}
#[test]
fn file_scheme_returns_none_for_no_scheme() {
assert_eq!(file_scheme("plain/file.txt"), None);
}
#[test]
fn file_scheme_returns_none_for_empty_name() {
assert_eq!(file_scheme(""), None);
}
#[test]
fn file_format_returns_value_or_none() {
assert_eq!(file_format("unix"), Some("unix".to_string()));
assert_eq!(file_format(""), None);
}
#[test]
fn file_encoding_returns_value_or_none() {
assert_eq!(file_encoding("utf-8"), Some("utf-8".to_string()));
assert_eq!(file_encoding(""), None);
}
#[test]
fn file_bom_returns_bom_or_none() {
assert_eq!(file_bom(true), Some("bom"));
assert_eq!(file_bom(false), None);
}
#[test]
fn file_type_returns_value_or_none() {
assert_eq!(file_type("rust"), Some("rust".to_string()));
assert_eq!(file_type(""), None);
}
#[test]
fn line_percent_no_gradient_returns_string() {
let v = line_percent(50, 100, false);
assert_eq!(v.as_str(), Some("50"));
}
#[test]
fn line_percent_with_gradient_returns_list() {
let v = line_percent(75, 100, true);
let arr = v.as_array().unwrap();
assert_eq!(arr[0]["contents"], "75");
assert_eq!(arr[0]["highlight_groups"][0], "line_percent_gradient");
assert_eq!(arr[0]["gradient_level"], 75.0);
}
#[test]
fn line_percent_at_first_line_emits_one() {
let v = line_percent(1, 100, false);
assert_eq!(v.as_str(), Some("1"));
}
#[test]
fn line_percent_at_last_line_emits_100() {
let v = line_percent(100, 100, false);
assert_eq!(v.as_str(), Some("100"));
}
#[test]
fn position_top_when_winline_first_is_one_and_not_all() {
let ps = default_position_strings();
let strs: HashMap<&str, &str> = ps.iter().map(|(k, v)| (*k, *v)).collect();
let v = position(100, 1, 50, &strs, false);
assert_eq!(v.as_str(), Some("Top"));
}
#[test]
fn position_all_when_window_shows_entire_buffer() {
let ps = default_position_strings();
let strs: HashMap<&str, &str> = ps.iter().map(|(k, v)| (*k, *v)).collect();
let v = position(50, 1, 50, &strs, false);
assert_eq!(v.as_str(), Some("All"));
}
#[test]
fn position_bottom_when_winline_last_is_buffer_end() {
let ps = default_position_strings();
let strs: HashMap<&str, &str> = ps.iter().map(|(k, v)| (*k, *v)).collect();
let v = position(100, 50, 100, &strs, false);
assert_eq!(v.as_str(), Some("Bot"));
}
#[test]
fn position_middle_emits_percentage() {
let ps = default_position_strings();
let strs: HashMap<&str, &str> = ps.iter().map(|(k, v)| (*k, *v)).collect();
let v = position(100, 10, 20, &strs, false);
let s = v.as_str().unwrap();
assert!(s.ends_with('%'));
}
#[test]
fn position_gradient_emits_full_dict() {
let ps = default_position_strings();
let strs: HashMap<&str, &str> = ps.iter().map(|(k, v)| (*k, *v)).collect();
let v = position(100, 50, 100, &strs, true);
let arr = v.as_array().unwrap();
assert_eq!(arr[0]["contents"], "Bot");
assert_eq!(arr[0]["highlight_groups"][0], "position_gradient");
assert_eq!(arr[0]["gradient_level"], 100.0);
}
#[test]
fn line_current_returns_cursor_row() {
assert_eq!(line_current(42), "42");
}
#[test]
fn line_count_returns_buffer_len() {
assert_eq!(line_count(100), "100");
}
#[test]
fn col_current_adds_one_to_zero_based_col() {
assert_eq!(col_current(0), "1");
assert_eq!(col_current(42), "43");
}
#[test]
fn virtcol_current_no_gradient_omits_level() {
let v = virtcol_current(40, 80, false);
let arr = v.as_array().unwrap();
assert_eq!(arr[0]["contents"], "40");
assert!(arr[0].get("gradient_level").is_none());
}
#[test]
fn virtcol_current_with_gradient_computes_level() {
let v = virtcol_current(40, 80, true);
let arr = v.as_array().unwrap();
assert_eq!(arr[0]["gradient_level"], 50.0);
assert_eq!(arr[0]["highlight_groups"][0], "virtcol_current_gradient");
}
#[test]
fn virtcol_current_clamps_gradient_to_100() {
let v = virtcol_current(120, 80, true);
let arr = v.as_array().unwrap();
assert_eq!(arr[0]["gradient_level"], 100.0);
}
#[test]
fn virtcol_current_zero_textwidth_gives_zero_gradient() {
let v = virtcol_current(40, 0, true);
let arr = v.as_array().unwrap();
assert_eq!(arr[0]["gradient_level"], 0.0);
}
#[test]
fn modified_buffers_with_empty_list_returns_none() {
assert_eq!(modified_buffers(&[], "+ ", ","), None);
}
#[test]
fn modified_buffers_joins_list_with_prefix() {
assert_eq!(
modified_buffers(&[1, 3, 5], "+ ", ","),
Some("+ 1,3,5".to_string())
);
}
#[test]
fn modified_buffers_uses_custom_separator() {
assert_eq!(
modified_buffers(&[2, 4], "M:", " | "),
Some("M:2 | 4".to_string())
);
}
#[test]
fn tabnr_shows_current_when_flag_set() {
assert_eq!(tabnr(1, 1, true), Some("1".to_string()));
}
#[test]
fn tabnr_hides_current_when_flag_unset() {
assert_eq!(tabnr(1, 1, false), None);
}
#[test]
fn tabnr_shows_other_tabnr_regardless_of_flag() {
assert_eq!(tabnr(2, 1, false), Some("2".to_string()));
}
#[test]
fn bufnr_show_current_paths() {
assert_eq!(bufnr(1, 1, true), Some("1".to_string()));
assert_eq!(bufnr(1, 1, false), None);
assert_eq!(bufnr(2, 1, false), Some("2".to_string()));
}
#[test]
fn winnr_show_current_paths() {
assert_eq!(winnr(1, 1, true), Some("1".to_string()));
assert_eq!(winnr(1, 1, false), None);
assert_eq!(winnr(2, 1, false), Some("2".to_string()));
}
#[test]
fn csv_sniff_lines_constant() {
assert_eq!(CSV_SNIFF_LINES, 100);
}
#[test]
fn csv_parse_lines_constant() {
assert_eq!(CSV_PARSE_LINES, 10);
}
#[test]
fn tab_modified_indicator_returns_segment_when_modified() {
let r = tab_modified_indicator(true, "+").unwrap();
let arr = r.as_array().unwrap();
assert_eq!(arr[0]["contents"], "+");
let hl = arr[0]["highlight_groups"].as_array().unwrap();
assert_eq!(hl[0], "tab_modified_indicator");
assert_eq!(hl[1], "modified_indicator");
}
#[test]
fn tab_modified_indicator_returns_none_when_no_modified() {
assert!(tab_modified_indicator(false, "+").is_none());
}
#[test]
fn window_title_returns_value_when_set() {
assert_eq!(
window_title(Some("Quickfix List")),
Some("Quickfix List".to_string())
);
}
#[test]
fn window_title_returns_none_when_unset() {
assert!(window_title(None).is_none());
}
#[test]
fn file_size_clamps_negative_to_zero() {
let r = file_size(-1, "B", false);
assert!(r.is_some());
assert!(!r.unwrap().is_empty());
}
#[test]
fn file_size_humanizes_byte_count() {
let r = file_size(1024, "B", false);
assert!(r.is_some());
let s = r.unwrap();
assert!(s.contains('K') || s.contains('k') || s.contains("1024"));
}
#[test]
fn detect_text_csv_dialect_auto_uses_caller_has_header() {
let (delim, has_header) = detect_text_csv_dialect(',', "auto", true);
assert_eq!(delim, ',');
assert!(has_header);
let (_, has_header) = detect_text_csv_dialect(',', "auto", false);
assert!(!has_header);
}
#[test]
fn detect_text_csv_dialect_non_auto_uses_display_name() {
let (_, has_header) = detect_text_csv_dialect(',', "true", false);
assert!(has_header);
let (_, has_header) = detect_text_csv_dialect(',', "false", true);
assert!(!has_header);
}
#[test]
fn tab_returns_segment_with_tabnr_marker() {
let r = tab(Some(3), false).unwrap();
let arr = r.as_array().unwrap();
assert_eq!(arr[0]["contents"], serde_json::Value::Null);
let literal = arr[0]["literal_contents"].as_array().unwrap();
assert_eq!(literal[0], 0);
assert_eq!(literal[1], "%3T");
}
#[test]
fn tab_end_emits_empty_tabnr() {
let r = tab(Some(3), true).unwrap();
let arr = r.as_array().unwrap();
let literal = arr[0]["literal_contents"].as_array().unwrap();
assert_eq!(literal[1], "%T");
}
#[test]
fn tab_returns_none_when_no_tabnr() {
assert!(tab(None, false).is_none());
}
}