Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Determine whether to use a margin of 0 or 1 when uncommenting #476

Merged
merged 12 commits into from
Jul 26, 2021
89 changes: 57 additions & 32 deletions helix-core/src/comment.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
use crate::{
find_first_non_whitespace_char, Change, Rope, RopeSlice, Selection, Tendril, Transaction,
};
use core::ops::Range;
use std::borrow::Cow;

/// 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.
/// - The lines to change for toggling comments
/// - This is all provided lines excluding blanks lines.
/// - The column of the comment tokens
/// - Column of existing tokens, if the lines are commented; column to place tokens at otherwise.
/// - The margin to the right of the comment tokens
/// - Defaults to `1`. If any existing comment token is not followed by a space, changes to `0`.
fn find_line_comment(
Omnikar marked this conversation as resolved.
Show resolved Hide resolved
token: &str,
text: RopeSlice,
lines: Range<usize>,
) -> (bool, Vec<usize>, usize) {
lines: impl IntoIterator<Item = usize>,
) -> (bool, Vec<usize>, usize, usize) {
let mut commented = true;
let mut skipped = Vec::new();
let mut to_change = Vec::new();
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) = find_first_non_whitespace_char(line_slice) {
Expand All @@ -29,47 +39,53 @@ fn find_line_comment(
// considered uncommented.
commented = false;
}
} else {
// blank line
skipped.push(line);

// 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 != ' ') {
Omnikar marked this conversation as resolved.
Show resolved Hide resolved
margin = 0;
}

// blank lines don't get pushed.
to_change.push(line);
}
}
(commented, skipped, min)
(commented, to_change, min, margin)
}

#[must_use]
pub fn toggle_line_comments(doc: &Rope, selection: &Selection, token: Option<&str>) -> Transaction {
let text = doc.slice(..);
let mut changes: Vec<Change> = Vec::new();

let token = token.unwrap_or("//");
let comment = Tendril::from(format!("{} ", token));

let mut lines: Vec<usize> = Vec::new();

let mut min_next_line = 0;
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());
let start = text.char_to_line(selection.from()).max(min_next_line);
let end = text.char_to_line(selection.to()) + 1;
lines.extend(start..end);
min_next_line = end + 1;
}

changes.reserve((end - start).saturating_sub(skipped.len()));
let (commented, to_change, min, margin) = find_line_comment(&token, text, lines);

for line in lines {
if skipped.contains(&line) {
continue;
}
let mut changes: Vec<Change> = Vec::with_capacity(to_change.len());

let pos = text.line_to_char(line) + min;
for line in to_change {
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))
}
if !commented {
// comment line
changes.push((pos, pos, Some(comment.clone())));
} else {
// uncomment line
changes.push((pos, pos + token.len() + margin, None));
}
}

Transaction::change(doc, changes.into_iter())
}

Expand All @@ -91,23 +107,32 @@ mod test {
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));
// (commented = true, to_change = [line 0, line 2], min = col 2, margin = 1)
assert_eq!(res, (false, vec![0, 2], 2, 1));
Omnikar marked this conversation as resolved.
Show resolved Hide resolved

// comment
let transaction = toggle_line_comments(&state.doc, &state.selection, None);
transaction.apply(&mut state.doc);
state.selection = state.selection.clone().map(transaction.changes());
state.selection = state.selection.map(transaction.changes());

assert_eq!(state.doc, " // 1\n\n // 2\n // 3");

// uncomment
let transaction = toggle_line_comments(&state.doc, &state.selection, None);
transaction.apply(&mut state.doc);
state.selection = state.selection.clone().map(transaction.changes());
state.selection = state.selection.map(transaction.changes());
assert_eq!(state.doc, " 1\n\n 2\n 3");

// 0 margin comments
state.doc = Rope::from(" //1\n\n //2\n //3");
// reset the selection.
state.selection = Selection::single(0, state.doc.len_chars() - 1);

let transaction = toggle_line_comments(&state.doc, &state.selection, None);
transaction.apply(&mut state.doc);
state.selection = state.selection.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
}
}