summaryrefslogtreecommitdiff
path: root/helix-core/src/comment.rs
blob: 4fcece570d4938cfafe87b5029737d1681202c4b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
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<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_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, 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));

    for selection in selection {
        let (start, end) = selection.line_range(text);
        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, None);
        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, None);
        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
    }
}