diff --git a/chiritori/src/chiritori.rs b/chiritori/src/chiritori.rs index fda5457..12fee60 100644 --- a/chiritori/src/chiritori.rs +++ b/chiritori/src/chiritori.rs @@ -17,7 +17,7 @@ use crate::{ }, removal_evaluator::RemovalEvaluator, Remover, - }, + }, utils::line_map::build_line_map, }, parser, tokenizer, }; @@ -79,7 +79,8 @@ pub fn list( .map(|v| (v, true)) .collect(); - build_list(&content, &markers) + let line_map = build_line_map(&content); + build_list(&content, &markers, Some(&line_map)) } pub fn list_all( @@ -94,7 +95,8 @@ pub fn list_all( let remover = build_remover(config, content.clone()); let markers = remover.build_remove_marker_all(&parsed); - build_list(&content, &markers) + let line_map = build_line_map(&content); + build_list(&content, &markers, Some(&line_map)) } fn build_remover(config: ChiritoriConfiguration, content: Rc) -> Remover { diff --git a/chiritori/src/code/list.rs b/chiritori/src/code/list.rs index dbd7782..1a5d764 100644 --- a/chiritori/src/code/list.rs +++ b/chiritori/src/code/list.rs @@ -1,25 +1,27 @@ + use super::{ remover::RemoveMarker, - utils::line_break_pos_finder::{find_next_line_break_pos, find_prev_line_break_pos}, + utils::{line_break_pos_finder::{find_next_line_break_pos, find_prev_line_break_pos}, line_map::find_line}, }; const MARKER_START: &str = "\x1b[32m_start\x1b[0m"; const MARKER_END: &str = "\x1b[32m‾end\x1b[0m"; const START_COLOR: &str = "\x1b[31m"; const START_COLOR_YELLOW: &str = "\x1b[33m"; -const END_COLOR: &str = "\x1b[0m"; +const RESET_COLOR: &str = "\x1b[0m"; const MARKER_START_LEN: usize = MARKER_START.len(); const MARKER_END_LEN: usize = MARKER_END.len(); const START_COLOR_LEN: usize = START_COLOR.len(); const START_COLOR_YELLOW_LEN: usize = START_COLOR_YELLOW.len(); -const END_COLOR_LEN: usize = END_COLOR.len(); +const RESET_COLOR_LEN: usize = RESET_COLOR.len(); const HEAD_START: &str = "-------- [ "; const HEAD_END: &str = "--------"; const REMOVAL_HEAD: &str = " ] Ready "; const PENDING_REMOVAL_HEAD: &str = " ] Pending "; +const LINE_COLUMN_WIDTH: usize = 9; -pub fn build_item(content: &str, start: usize, end: usize, is_removal: &bool) -> String { +pub fn build_item(content: &str, start: usize, end: usize, is_removal: &bool, line_map: Option<&Vec>) -> String { if end - start == 0 || content.is_empty() { return String::new(); } @@ -44,36 +46,62 @@ pub fn build_item(content: &str, start: usize, end: usize, is_removal: &bool) -> (START_COLOR_YELLOW, START_COLOR_YELLOW_LEN) }; + let mut removed = String::with_capacity( + (line_end - line_start) + + (content[color_start..color_end].lines().count() * (start_color_len + RESET_COLOR_LEN)) + ); + removed.push_str(&content[line_start..color_start]); + removed.push_str(&content[color_start..color_end].lines().map(|l| { + let mut str = String::with_capacity(start_color_len + l.len() + RESET_COLOR_LEN); + str.push_str(start_color); + str.push_str(l); + str.push_str(RESET_COLOR); + str + }).collect::>().join("\n")); + removed.push_str(&content[color_end..line_end]); + removed.push('\n'); + + let (code_block, column_ofs) = if let Some(line_map) = line_map { + let line_start_pos = find_line(line_map, line_start); + let line_end_pos = find_line(line_map, line_end); + let mut line_iter = removed.lines(); + ( + &(line_start_pos..=line_end_pos).map(|i| { + let line_column = format!("{:width$} {}", i, "|", width = LINE_COLUMN_WIDTH - 2); + line_iter.next().map(|l| format!("{line_column}{l}\n")).unwrap_or("".to_string()) + }).collect(), + LINE_COLUMN_WIDTH + ) + } else { + (&removed, 0) + }; + let marker_start_ofs_len = start - line_start; let marker_end_ofs_len = end - line_end_start_pos - 1; let mut result = String::with_capacity( - marker_start_ofs_len - + MARKER_START_LEN - + 1 - + start_color_len - + (line_end - line_start) - + END_COLOR_LEN - + 1 - + marker_end_ofs_len - + MARKER_END_LEN, + (marker_start_ofs_len + column_ofs + MARKER_START_LEN) // start marker + + 1 // \n + + removed.len() // code block + + 1 // \n + + (marker_end_ofs_len + column_ofs + MARKER_END_LEN) // end marker ); - result.push_str(&" ".repeat(marker_start_ofs_len)); + // Print a start marker + result.push_str(&" ".repeat(marker_start_ofs_len + column_ofs)); result.push_str(MARKER_START); result.push('\n'); - result.push_str(&content[line_start..color_start]); - result.push_str(start_color); - result.push_str(&content[color_start..color_end]); - result.push_str(END_COLOR); - result.push_str(&content[color_end..line_end]); - result.push('\n'); - result.push_str(&" ".repeat(marker_end_ofs_len)); + + // Print a code block + result.push_str(code_block); + + // Print an end marker + result.push_str(&" ".repeat(marker_end_ofs_len + column_ofs)); result.push_str(MARKER_END); result } -pub fn build_list(content: &str, markers: &[(RemoveMarker, bool)]) -> String { +pub fn build_list(content: &str, markers: &[(RemoveMarker, bool)], line_map: Option<&Vec>) -> String { let mut output: String = markers .iter() .zip(1..=markers.len()) @@ -88,7 +116,7 @@ pub fn build_list(content: &str, markers: &[(RemoveMarker, bool)]) -> String { }); res.push_str(HEAD_END); res.push('\n'); - res.push_str(&build_item(content, range.start, range.end, is_removal)); + res.push_str(&build_item(content, range.start, range.end, is_removal, line_map)); res }) @@ -101,6 +129,8 @@ pub fn build_list(content: &str, markers: &[(RemoveMarker, bool)]) -> String { #[cfg(test)] mod tests { + use crate::code::utils::line_map::build_line_map; + use super::*; use rstest::rstest; use std::ops::Range; @@ -108,13 +138,13 @@ mod tests { #[rstest] // 0 10 // 012345678901234567 - #[case("aaa+bbbb+ccc+dddd", 4..11, format!("{}{}+{}{}{}{}{}+{}{}", "", MARKER_START, "", START_COLOR, "bbbb+cc", END_COLOR, "c", " ", MARKER_END))] - #[case("aaa+bbbb+ccc+dddd", 3..11, format!("{}{}+{}{}{}{}{}+{}{}", " ", MARKER_START, "aaa", START_COLOR, "+bbbb+cc", END_COLOR, "c", " ", MARKER_END))] - #[case("aaa+bbbb+ccc+dddd", 2..11, format!("{}{}+{}{}{}{}{}+{}{}", " ", MARKER_START, "aa", START_COLOR, "a+bbbb+cc", END_COLOR, "c", " ", MARKER_END))] - #[case("aaa+bbbb+ccc+dddd", 4..12, format!("{}{}+{}{}{}{}{}+{}{}", "", MARKER_START, "", START_COLOR, "bbbb+ccc", END_COLOR, "", " ", MARKER_END))] - #[case("aaa+bbbb+ccc+dddd", 4..13, format!("{}{}+{}{}{}{}{}+{}{}", "", MARKER_START, "", START_COLOR, "bbbb+ccc", END_COLOR, "", " ", MARKER_END))] - #[case("aaa+bbbb+ccc+dddd", 4..16, format!("{}{}+{}{}{}{}{}+{}{}", "", MARKER_START, "", START_COLOR, "bbbb+ccc+ddd", END_COLOR, "d", " ", MARKER_END))] - #[case("abcd", 1..2, format!("{}{}+{}{}{}{}{}+{}{}", " ", MARKER_START, "a", START_COLOR, "b", END_COLOR, "cd", " ", MARKER_END))] + #[case("aaa+bbbb+ccc+dddd", 4..11, format!("{}{}{}{}{}{}{}{}{}{}{}{}{}", MARKER_START, "+", START_COLOR, "bbbb", RESET_COLOR, "+", START_COLOR, "cc", RESET_COLOR, "c", "+", " ", MARKER_END))] + #[case("aaa+bbbb+ccc+dddd", 3..11, format!("{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}", " ", MARKER_START, "+", "aaa", START_COLOR, "", RESET_COLOR, "+", START_COLOR, "bbbb", RESET_COLOR, "+", START_COLOR, "cc", RESET_COLOR, "c", "+", " ", MARKER_END))] + #[case("aaa+bbbb+ccc+dddd", 2..11, format!("{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}", " ", MARKER_START, "+", "aa", START_COLOR, "a", RESET_COLOR, "+", START_COLOR, "bbbb", RESET_COLOR, "+", START_COLOR, "cc", RESET_COLOR, "c", "+", " ", MARKER_END))] + #[case("aaa+bbbb+ccc+dddd", 4..12, format!("{}{}{}{}{}{}{}{}{}{}{}{}", MARKER_START, "+", START_COLOR, "bbbb", RESET_COLOR, "+", START_COLOR, "ccc", RESET_COLOR, "+", " ", MARKER_END))] + #[case("aaa+bbbb+ccc+dddd", 4..13, format!("{}{}{}{}{}{}{}{}{}{}{}{}", MARKER_START, "+", START_COLOR, "bbbb", RESET_COLOR, "+", START_COLOR, "ccc", RESET_COLOR, "+", " ", MARKER_END))] + #[case("aaa+bbbb+ccc+dddd", 4..16, format!("{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}", MARKER_START, "+", START_COLOR, "bbbb", RESET_COLOR, "+", START_COLOR, "ccc", RESET_COLOR, "+", START_COLOR, "ddd", RESET_COLOR, "d", "+", " ", MARKER_END))] + #[case("abcd", 1..2, format!("{}{}{}{}{}{}{}{}{}{}{}", " ", MARKER_START, "+", "a", START_COLOR, "b", RESET_COLOR, "cd", "+", " ", MARKER_END))] #[case("", 0..0, "")] fn test_build_item( #[case] content: &str, @@ -124,13 +154,32 @@ mod tests { let content = content.replace('+', "\n"); assert_eq!( - build_item(&content, range.start, range.end, &true), + build_item(&content, range.start, range.end, &true, None), expected.replace('+', "\n") ); } + #[test] + fn test_build_item_with_line_number() { + let content = "aaaaaaa +bbbbbbb +ccccccc +ddddddd +eeeeeee"; + assert_eq!( + build_item(content, 19, 30, &true, Some(&build_line_map(content))), + format!( + "{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}", + " ", MARKER_START, "+", + " 3 |", "ccc", START_COLOR, "cccc", RESET_COLOR, "+", + " 4 |", START_COLOR, "dddddd", RESET_COLOR, "d", "+", + " ", MARKER_END + ).replace('+', "\n") + ); + } + #[rstest] - #[case("aaa+bbbb+ccc+dddd", 4..11, format!("{}{}+{}{}{}{}{}+{}{}", "", MARKER_START, "", START_COLOR_YELLOW, "bbbb+cc", END_COLOR, "c", " ", MARKER_END))] + #[case("aaa+bbbb+ccc+dddd", 4..11, format!("{}{}{}{}{}{}{}{}{}{}{}{}{}", MARKER_START, "+", START_COLOR_YELLOW, "bbbb", RESET_COLOR, "+", START_COLOR_YELLOW, "cc", RESET_COLOR, "c", "+", " ", MARKER_END))] fn test_build_item_pending_removal_range( #[case] content: &str, #[case] range: Range, @@ -139,7 +188,7 @@ mod tests { let content = content.replace('+', "\n"); assert_eq!( - build_item(&content, range.start, range.end, &false), + build_item(&content, range.start, range.end, &false, None), expected.replace('+', "\n") ); } @@ -150,11 +199,11 @@ mod tests { let content = "aaaa+bbbb+cccc+dddd".replace('+', "\n"); let markers = [((1..2, None), true), ((7..12, None), false)]; - let expected_item1 = build_item(&content, 1, 2, &true); - let expected_item2 = build_item(&content, 7, 12, &false); + let expected_item1 = build_item(&content, 1, 2, &true, None); + let expected_item2 = build_item(&content, 7, 12, &false, None); assert_eq!( - build_list(&content, &markers), + build_list(&content, &markers, None), format!( "\n{}1{}{}\n{}\n{}2{}{}\n{}\n", HEAD_START, diff --git a/chiritori/src/code/utils.rs b/chiritori/src/code/utils.rs index 3badb53..3c22e35 100644 --- a/chiritori/src/code/utils.rs +++ b/chiritori/src/code/utils.rs @@ -1,2 +1,3 @@ pub mod char_pos_finder; pub mod line_break_pos_finder; +pub mod line_map; diff --git a/chiritori/src/code/utils/line_map.rs b/chiritori/src/code/utils/line_map.rs new file mode 100644 index 0000000..1a0dc56 --- /dev/null +++ b/chiritori/src/code/utils/line_map.rs @@ -0,0 +1,37 @@ +pub fn build_line_map(content: &str) -> Vec { + content.char_indices().fold(vec![], |mut acc, (byte_pos, c)| { + if c == '\n' { + acc.push(byte_pos) + } + + acc + }) +} + +pub fn find_line(line_map: &[usize], needle: usize) -> usize { + let found = line_map.iter().position(|v| *v > needle); + + if let Some(found) = found { + found + 1 + } else { + line_map.len() + 1 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const CONTENT: &str = "abc+def+efg+hijkl+mnopq"; + + #[test] + fn test_build_line_map() { + assert_eq!(build_line_map(&CONTENT.replace('+', "\n")), vec![3, 7, 11, 17]); + } + + #[test] + fn test_find_line() { + let mapped = build_line_map(&CONTENT.replace('+', "\n")); + assert_eq!(find_line(&mapped, 9), 3); + } +}