helix-mods/helix-core/src/comment.rs
2021-03-31 15:45:18 +09:00

113 lines
3.5 KiB
Rust

use crate::{
find_first_non_whitespace_char2, Change, Rope, RopeSlice, Selection, Tendril, Transaction,
};
use core::ops::Range;
use std::borrow::Cow;
fn find_line_comment(
token: &str,
text: RopeSlice,
lines: Range<usize>,
) -> (bool, Vec<usize>, 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_char2(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<Change> = 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 - 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
}
}