Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-core/src/comment.rs')
| -rw-r--r-- | helix-core/src/comment.rs | 416 |
1 files changed, 35 insertions, 381 deletions
diff --git a/helix-core/src/comment.rs b/helix-core/src/comment.rs index 5985cac7..ec5d7a45 100644 --- a/helix-core/src/comment.rs +++ b/helix-core/src/comment.rs @@ -1,33 +1,11 @@ //! This module contains the functionality toggle comments on lines over the selection //! using the comment character defined in the user's `languages.toml` -use smallvec::SmallVec; - use crate::{ - syntax::config::BlockCommentToken, Change, Range, Rope, RopeSlice, Selection, Tendril, - Transaction, + find_first_non_whitespace_char, Change, Rope, RopeSlice, Selection, Tendril, Transaction, }; -use helix_stdx::rope::RopeSliceExt; use std::borrow::Cow; -pub const DEFAULT_COMMENT_TOKEN: &str = "#"; - -/// Returns the longest matching comment token of the given line (if it exists). -pub fn get_comment_token<'a, S: AsRef<str>>( - text: RopeSlice, - tokens: &'a [S], - line_num: usize, -) -> Option<&'a str> { - let line = text.line(line_num); - let start = line.first_non_whitespace_char()?; - - tokens - .iter() - .map(AsRef::as_ref) - .filter(|token| line.slice(start..).starts_with(token)) - .max_by_key(|token| token.len()) -} - /// Given text, a comment token, and a set of line indices, returns the following: /// - Whether the given lines should be considered commented /// - If any of the lines are uncommented, all lines are considered as such. @@ -44,29 +22,30 @@ fn find_line_comment( ) -> (bool, Vec<usize>, usize, usize) { let mut commented = true; let mut to_change = Vec::new(); - let mut min = usize::MAX; // minimum col for first_non_whitespace_char + let mut min = usize::MAX; // minimum col for find_first_non_whitespace_char let mut margin = 1; let token_len = token.chars().count(); - for line in lines { let line_slice = text.line(line); - if let Some(pos) = line_slice.first_non_whitespace_char() { + if let Some(pos) = find_first_non_whitespace_char(line_slice) { let len = line_slice.len_chars(); - min = std::cmp::min(min, pos); + 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))); - // as soon as one of the non-blank lines doesn't have a comment, the whole block is - // considered uncommented. 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; } // determine margin of 0 or 1 for uncommenting; if any comment token is not followed by a space, // a margin of 0 is used for all lines. - if !matches!(line_slice.get_char(pos + token_len), Some(c) if c == ' ') { + if matches!(line_slice.get_char(pos + token_len), Some(c) if c != ' ') { margin = 0; } @@ -74,7 +53,6 @@ fn find_line_comment( to_change.push(line); } } - (commented, to_change, min, margin) } @@ -82,7 +60,7 @@ fn find_line_comment( pub fn toggle_line_comments(doc: &Rope, selection: &Selection, token: Option<&str>) -> Transaction { let text = doc.slice(..); - let token = token.unwrap_or(DEFAULT_COMMENT_TOKEN); + let token = token.unwrap_or("//"); let comment = Tendril::from(format!("{} ", token)); let mut lines: Vec<usize> = Vec::with_capacity(selection.len()); @@ -90,7 +68,7 @@ pub fn toggle_line_comments(doc: &Rope, selection: &Selection, token: Option<&st let mut min_next_line = 0; for selection in selection { let (start, end) = selection.line_range(text); - let start = start.clamp(min_next_line, text.len_lines()); + let start = start.max(min_next_line).min(text.len_lines()); let end = (end + 1).min(text.len_lines()); lines.extend(start..end); @@ -116,372 +94,48 @@ pub fn toggle_line_comments(doc: &Rope, selection: &Selection, token: Option<&st Transaction::change(doc, changes.into_iter()) } -#[derive(Debug, PartialEq, Eq)] -pub enum CommentChange { - Commented { - range: Range, - start_pos: usize, - end_pos: usize, - start_margin: bool, - end_margin: bool, - start_token: String, - end_token: String, - }, - Uncommented { - range: Range, - start_pos: usize, - end_pos: usize, - start_token: String, - end_token: String, - }, - Whitespace { - range: Range, - }, -} - -pub fn find_block_comments( - tokens: &[BlockCommentToken], - text: RopeSlice, - selection: &Selection, -) -> (bool, Vec<CommentChange>) { - let mut commented = true; - let mut only_whitespace = true; - let mut comment_changes = Vec::with_capacity(selection.len()); - let default_tokens = tokens.first().cloned().unwrap_or_default(); - let mut start_token = default_tokens.start.clone(); - let mut end_token = default_tokens.end.clone(); - - let mut tokens = tokens.to_vec(); - // sort the tokens by length, so longer tokens will match first - tokens.sort_by(|a, b| { - if a.start.len() == b.start.len() { - b.end.len().cmp(&a.end.len()) - } else { - b.start.len().cmp(&a.start.len()) - } - }); - for range in selection { - let selection_slice = range.slice(text); - if let (Some(start_pos), Some(end_pos)) = ( - selection_slice.first_non_whitespace_char(), - selection_slice.last_non_whitespace_char(), - ) { - let mut line_commented = false; - let mut after_start = 0; - let mut before_end = 0; - let len = (end_pos + 1) - start_pos; - - for BlockCommentToken { start, end } in &tokens { - let start_len = start.chars().count(); - let end_len = end.chars().count(); - after_start = start_pos + start_len; - before_end = end_pos.saturating_sub(end_len); - - if len >= start_len + end_len { - let start_fragment = selection_slice.slice(start_pos..after_start); - let end_fragment = selection_slice.slice(before_end + 1..end_pos + 1); - - // block commented with these tokens - if start_fragment == start.as_str() && end_fragment == end.as_str() { - start_token = start.to_string(); - end_token = end.to_string(); - line_commented = true; - break; - } - } - } - - if !line_commented { - comment_changes.push(CommentChange::Uncommented { - range: *range, - start_pos, - end_pos, - start_token: default_tokens.start.clone(), - end_token: default_tokens.end.clone(), - }); - commented = false; - } else { - comment_changes.push(CommentChange::Commented { - range: *range, - start_pos, - end_pos, - start_margin: selection_slice.get_char(after_start) == Some(' '), - end_margin: after_start != before_end - && (selection_slice.get_char(before_end) == Some(' ')), - start_token: start_token.to_string(), - end_token: end_token.to_string(), - }); - } - only_whitespace = false; - } else { - comment_changes.push(CommentChange::Whitespace { range: *range }); - } - } - if only_whitespace { - commented = false; - } - (commented, comment_changes) -} - -#[must_use] -pub fn create_block_comment_transaction( - doc: &Rope, - selection: &Selection, - commented: bool, - comment_changes: Vec<CommentChange>, -) -> (Transaction, SmallVec<[Range; 1]>) { - let mut changes: Vec<Change> = Vec::with_capacity(selection.len() * 2); - let mut ranges: SmallVec<[Range; 1]> = SmallVec::with_capacity(selection.len()); - let mut offs = 0; - for change in comment_changes { - if commented { - if let CommentChange::Commented { - range, - start_pos, - end_pos, - start_token, - end_token, - start_margin, - end_margin, - } = change - { - let from = range.from(); - changes.push(( - from + start_pos, - from + start_pos + start_token.len() + start_margin as usize, - None, - )); - changes.push(( - from + end_pos - end_token.len() - end_margin as usize + 1, - from + end_pos + 1, - None, - )); - } - } else { - // uncommented so manually map ranges through changes - match change { - CommentChange::Uncommented { - range, - start_pos, - end_pos, - start_token, - end_token, - } => { - let from = range.from(); - changes.push(( - from + start_pos, - from + start_pos, - Some(Tendril::from(format!("{} ", start_token))), - )); - changes.push(( - from + end_pos + 1, - from + end_pos + 1, - Some(Tendril::from(format!(" {}", end_token))), - )); - - let offset = start_token.chars().count() + end_token.chars().count() + 2; - ranges.push( - Range::new(from + offs, from + offs + end_pos + 1 + offset) - .with_direction(range.direction()), - ); - offs += offset; - } - CommentChange::Commented { range, .. } | CommentChange::Whitespace { range } => { - ranges.push(Range::new(range.from() + offs, range.to() + offs)); - } - } - } - } - (Transaction::change(doc, changes.into_iter()), ranges) -} - -#[must_use] -pub fn toggle_block_comments( - doc: &Rope, - selection: &Selection, - tokens: &[BlockCommentToken], -) -> Transaction { - let text = doc.slice(..); - let (commented, comment_changes) = find_block_comments(tokens, text, selection); - let (mut transaction, ranges) = - create_block_comment_transaction(doc, selection, commented, comment_changes); - if !commented { - transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index())); - } - transaction -} - -pub fn split_lines_of_selection(text: RopeSlice, selection: &Selection) -> Selection { - let mut ranges = SmallVec::new(); - for range in selection.ranges() { - let (line_start, line_end) = range.line_range(text.slice(..)); - let mut pos = text.line_to_char(line_start); - for line in text.slice(pos..text.line_to_char(line_end + 1)).lines() { - let start = pos; - pos += line.len_chars(); - ranges.push(Range::new(start, pos)); - } - } - Selection::new(ranges, 0) -} - #[cfg(test)] mod test { use super::*; - mod find_line_comment { - use super::*; - - #[test] - fn not_commented() { - // four lines, two space indented, except for line 1 which is blank. - let doc = Rope::from(" 1\n\n 2\n 3"); - - let text = doc.slice(..); - - let res = find_line_comment("//", text, 0..3); - // (commented = false, to_change = [line 0, line 2], min = col 2, margin = 0) - assert_eq!(res, (false, vec![0, 2], 2, 0)); - } - - #[test] - fn is_commented() { - // three lines where the second line is empty. - let doc = Rope::from("// hello\n\n// there"); - - let res = find_line_comment("//", doc.slice(..), 0..3); - - // (commented = true, to_change = [line 0, line 2], min = col 0, margin = 1) - assert_eq!(res, (true, vec![0, 2], 0, 1)); - } - } - - // TODO: account for uncommenting with uneven comment indentation - mod toggle_line_comment { - use super::*; - - #[test] - fn comment() { - // four lines, two space indented, except for line 1 which is blank. - let mut doc = Rope::from(" 1\n\n 2\n 3"); - // select whole document - let selection = Selection::single(0, doc.len_chars() - 1); - - let transaction = toggle_line_comments(&doc, &selection, None); - transaction.apply(&mut doc); - - assert_eq!(doc, " # 1\n\n # 2\n # 3"); - } - - #[test] - fn uncomment() { - let mut doc = Rope::from(" # 1\n\n # 2\n # 3"); - let mut selection = Selection::single(0, doc.len_chars() - 1); - - let transaction = toggle_line_comments(&doc, &selection, None); - transaction.apply(&mut doc); - selection = selection.map(transaction.changes()); - - assert_eq!(doc, " 1\n\n 2\n 3"); - assert!(selection.len() == 1); // to ignore the selection unused warning - } - - #[test] - fn uncomment_0_margin_comments() { - let mut doc = Rope::from(" #1\n\n #2\n #3"); - let mut selection = Selection::single(0, doc.len_chars() - 1); - - let transaction = toggle_line_comments(&doc, &selection, None); - transaction.apply(&mut doc); - selection = selection.map(transaction.changes()); - - assert_eq!(doc, " 1\n\n 2\n 3"); - assert!(selection.len() == 1); // to ignore the selection unused warning - } - - #[test] - fn uncomment_0_margin_comments_with_no_space() { - let mut doc = Rope::from("#"); - let mut selection = Selection::single(0, doc.len_chars() - 1); - - let transaction = toggle_line_comments(&doc, &selection, None); - transaction.apply(&mut doc); - selection = selection.map(transaction.changes()); - assert_eq!(doc, ""); - assert!(selection.len() == 1); // to ignore the selection unused warning - } - } - #[test] - fn test_find_block_comments() { - // three lines 5 characters. - let mut doc = Rope::from("1\n2\n3"); + fn test_find_line_comment() { + // four lines, two space indented, except for line 1 which is blank. + let mut doc = Rope::from(" 1\n\n 2\n 3"); // select whole document - let selection = Selection::single(0, doc.len_chars()); + let mut selection = Selection::single(0, doc.len_chars() - 1); let text = doc.slice(..); - let res = find_block_comments(&[BlockCommentToken::default()], text, &selection); - - assert_eq!( - res, - ( - false, - vec![CommentChange::Uncommented { - range: Range::new(0, 5), - start_pos: 0, - end_pos: 4, - start_token: "/*".to_string(), - end_token: "*/".to_string(), - }] - ) - ); + let res = find_line_comment("//", text, 0..3); + // (commented = true, to_change = [line 0, line 2], min = col 2, margin = 1) + assert_eq!(res, (false, vec![0, 2], 2, 1)); // comment - let transaction = toggle_block_comments(&doc, &selection, &[BlockCommentToken::default()]); + let transaction = toggle_line_comments(&doc, &selection, None); transaction.apply(&mut doc); + selection = selection.map(transaction.changes()); - assert_eq!(doc, "/* 1\n2\n3 */"); + assert_eq!(doc, " // 1\n\n // 2\n // 3"); // uncomment - let selection = Selection::single(0, doc.len_chars()); - let transaction = toggle_block_comments(&doc, &selection, &[BlockCommentToken::default()]); + let transaction = toggle_line_comments(&doc, &selection, None); transaction.apply(&mut doc); - assert_eq!(doc, "1\n2\n3"); - - // don't panic when there is just a space in comment - doc = Rope::from("/* */"); - let selection = Selection::single(0, doc.len_chars()); - let transaction = toggle_block_comments(&doc, &selection, &[BlockCommentToken::default()]); - transaction.apply(&mut doc); - assert_eq!(doc, ""); - } + selection = selection.map(transaction.changes()); + assert_eq!(doc, " 1\n\n 2\n 3"); + assert!(selection.len() == 1); // to ignore the selection unused warning - /// Test, if `get_comment_tokens` works, even if the content of the file includes chars, whose - /// byte size unequal the amount of chars - #[test] - fn test_get_comment_with_char_boundaries() { - let rope = Rope::from("ยทยท"); - let tokens = ["//", "///"]; - - assert_eq!( - super::get_comment_token(rope.slice(..), tokens.as_slice(), 0), - None - ); - } + // 0 margin comments + doc = Rope::from(" //1\n\n //2\n //3"); + // reset the selection. + selection = Selection::single(0, doc.len_chars() - 1); - /// Test for `get_comment_token`. - /// - /// Assuming the comment tokens are stored as `["///", "//"]`, `get_comment_token` should still - /// return `///` instead of `//` if the user is in a doc-comment section. - #[test] - fn test_use_longest_comment() { - let text = Rope::from(" /// amogus"); - let tokens = ["///", "//"]; + let transaction = toggle_line_comments(&doc, &selection, None); + transaction.apply(&mut doc); + selection = selection.map(transaction.changes()); + assert_eq!(doc, " 1\n\n 2\n 3"); + assert!(selection.len() == 1); // to ignore the selection unused warning - assert_eq!( - super::get_comment_token(text.slice(..), tokens.as_slice(), 0), - Some("///") - ); + // TODO: account for uncommenting with uneven comment indentation } } |