use crate::{ find_first_non_whitespace_char, Change, Rope, RopeSlice, Selection, Tendril, Transaction, }; use core::ops::Range; use std::borrow::Cow; fn find_line_comment( token: &str, text: RopeSlice, lines: Range, ) -> (bool, Vec, usize) { let mut commented = true; let mut skipped = Vec::new(); let mut min = usize::MAX; // minimum col for find_first_non_whitespace_char for line in lines { let line_slice = text.line(line); if let Some(pos) = find_first_non_whitespace_char(line_slice) { let len = line_slice.len_chars(); if pos < min { min = pos; } // line can be shorter than pos + token len let fragment = Cow::from(line_slice.slice(pos..std::cmp::min(pos + token.len(), len))); if fragment != token { // as soon as one of the non-blank lines doesn't have a comment, the whole block is // considered uncommented. commented = false; } } else { // blank line skipped.push(line); } } (commented, skipped, min) } #[must_use] pub fn toggle_line_comments(doc: &Rope, selection: &Selection) -> Transaction { let text = doc.slice(..); let mut changes: Vec = Vec::new(); let token = "//"; let comment = Tendril::from(format!("{} ", token)); for selection in selection { let start = text.char_to_line(selection.from()); let end = text.char_to_line(selection.to()); let lines = start..end + 1; let (commented, skipped, min) = find_line_comment(token, text, lines.clone()); changes.reserve((end - start).saturating_sub(skipped.len())); for line in lines { if skipped.contains(&line) { continue; } let pos = text.line_to_char(line) + min; if !commented { // comment line changes.push((pos, pos, Some(comment.clone()))) } else { // uncomment line let margin = 1; // TODO: margin is hardcoded 1 but could easily be 0 changes.push((pos, pos + token.len() + margin, None)) } } } Transaction::change(doc, changes.into_iter()) } #[cfg(test)] mod test { use super::*; #[test] fn test_find_line_comment() { use crate::State; // four lines, two space indented, except for line 1 which is blank. let doc = Rope::from(" 1\n\n 2\n 3"); let mut state = State::new(doc); // select whole document state.selection = Selection::single(0, state.doc.len_chars() - 1); let text = state.doc.slice(..); let res = find_line_comment("//", text, 0..3); // (commented = true, skipped = [line 1], min = col 2) assert_eq!(res, (false, vec![1], 2)); // comment let transaction = toggle_line_comments(&state.doc, &state.selection); transaction.apply(&mut state.doc); state.selection = state.selection.clone().map(transaction.changes()); assert_eq!(state.doc, " // 1\n\n // 2\n // 3"); // uncomment let transaction = toggle_line_comments(&state.doc, &state.selection); transaction.apply(&mut state.doc); state.selection = state.selection.clone().map(transaction.changes()); assert_eq!(state.doc, " 1\n\n 2\n 3"); // TODO: account for no margin after comment // TODO: account for uncommenting with uneven comment indentation } }