Skip to content

Commit be2884d

Browse files
authored
Continue line comments (#10996)
1 parent a145335 commit be2884d

File tree

2 files changed

+192
-74
lines changed

2 files changed

+192
-74
lines changed

helix-core/src/comment.rs

Lines changed: 123 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,24 @@ use crate::{
99
use helix_stdx::rope::RopeSliceExt;
1010
use std::borrow::Cow;
1111

12+
pub const DEFAULT_COMMENT_TOKEN: &str = "//";
13+
14+
/// Returns the longest matching comment token of the given line (if it exists).
15+
pub fn get_comment_token<'a, S: AsRef<str>>(
16+
text: RopeSlice,
17+
tokens: &'a [S],
18+
line_num: usize,
19+
) -> Option<&'a str> {
20+
let line = text.line(line_num);
21+
let start = line.first_non_whitespace_char()?;
22+
23+
tokens
24+
.iter()
25+
.map(AsRef::as_ref)
26+
.filter(|token| line.slice(start..).starts_with(token))
27+
.max_by_key(|token| token.len())
28+
}
29+
1230
/// Given text, a comment token, and a set of line indices, returns the following:
1331
/// - Whether the given lines should be considered commented
1432
/// - If any of the lines are uncommented, all lines are considered as such.
@@ -28,21 +46,20 @@ fn find_line_comment(
2846
let mut min = usize::MAX; // minimum col for first_non_whitespace_char
2947
let mut margin = 1;
3048
let token_len = token.chars().count();
49+
3150
for line in lines {
3251
let line_slice = text.line(line);
3352
if let Some(pos) = line_slice.first_non_whitespace_char() {
3453
let len = line_slice.len_chars();
3554

36-
if pos < min {
37-
min = pos;
38-
}
55+
min = std::cmp::min(min, pos);
3956

4057
// line can be shorter than pos + token len
4158
let fragment = Cow::from(line_slice.slice(pos..std::cmp::min(pos + token.len(), len)));
4259

60+
// as soon as one of the non-blank lines doesn't have a comment, the whole block is
61+
// considered uncommented.
4362
if fragment != token {
44-
// as soon as one of the non-blank lines doesn't have a comment, the whole block is
45-
// considered uncommented.
4663
commented = false;
4764
}
4865

@@ -56,14 +73,15 @@ fn find_line_comment(
5673
to_change.push(line);
5774
}
5875
}
76+
5977
(commented, to_change, min, margin)
6078
}
6179

6280
#[must_use]
6381
pub fn toggle_line_comments(doc: &Rope, selection: &Selection, token: Option<&str>) -> Transaction {
6482
let text = doc.slice(..);
6583

66-
let token = token.unwrap_or("//");
84+
let token = token.unwrap_or(DEFAULT_COMMENT_TOKEN);
6785
let comment = Tendril::from(format!("{} ", token));
6886

6987
let mut lines: Vec<usize> = Vec::with_capacity(selection.len());
@@ -317,56 +335,87 @@ pub fn split_lines_of_selection(text: RopeSlice, selection: &Selection) -> Selec
317335
mod test {
318336
use super::*;
319337

320-
#[test]
321-
fn test_find_line_comment() {
322-
// four lines, two space indented, except for line 1 which is blank.
323-
let mut doc = Rope::from(" 1\n\n 2\n 3");
324-
// select whole document
325-
let mut selection = Selection::single(0, doc.len_chars() - 1);
338+
mod find_line_comment {
339+
use super::*;
326340

327-
let text = doc.slice(..);
341+
#[test]
342+
fn not_commented() {
343+
// four lines, two space indented, except for line 1 which is blank.
344+
let doc = Rope::from(" 1\n\n 2\n 3");
328345

329-
let res = find_line_comment("//", text, 0..3);
330-
// (commented = true, to_change = [line 0, line 2], min = col 2, margin = 0)
331-
assert_eq!(res, (false, vec![0, 2], 2, 0));
346+
let text = doc.slice(..);
332347

333-
// comment
334-
let transaction = toggle_line_comments(&doc, &selection, None);
335-
transaction.apply(&mut doc);
336-
selection = selection.map(transaction.changes());
348+
let res = find_line_comment("//", text, 0..3);
349+
// (commented = false, to_change = [line 0, line 2], min = col 2, margin = 0)
350+
assert_eq!(res, (false, vec![0, 2], 2, 0));
351+
}
337352

338-
assert_eq!(doc, " // 1\n\n // 2\n // 3");
353+
#[test]
354+
fn is_commented() {
355+
// three lines where the second line is empty.
356+
let doc = Rope::from("// hello\n\n// there");
339357

340-
// uncomment
341-
let transaction = toggle_line_comments(&doc, &selection, None);
342-
transaction.apply(&mut doc);
343-
selection = selection.map(transaction.changes());
344-
assert_eq!(doc, " 1\n\n 2\n 3");
345-
assert!(selection.len() == 1); // to ignore the selection unused warning
358+
let res = find_line_comment("//", doc.slice(..), 0..3);
346359

347-
// 0 margin comments
348-
doc = Rope::from(" //1\n\n //2\n //3");
349-
// reset the selection.
350-
selection = Selection::single(0, doc.len_chars() - 1);
360+
// (commented = true, to_change = [line 0, line 2], min = col 0, margin = 1)
361+
assert_eq!(res, (true, vec![0, 2], 0, 1));
362+
}
363+
}
351364

352-
let transaction = toggle_line_comments(&doc, &selection, None);
353-
transaction.apply(&mut doc);
354-
selection = selection.map(transaction.changes());
355-
assert_eq!(doc, " 1\n\n 2\n 3");
356-
assert!(selection.len() == 1); // to ignore the selection unused warning
365+
// TODO: account for uncommenting with uneven comment indentation
366+
mod toggle_line_comment {
367+
use super::*;
357368

358-
// 0 margin comments, with no space
359-
doc = Rope::from("//");
360-
// reset the selection.
361-
selection = Selection::single(0, doc.len_chars() - 1);
369+
#[test]
370+
fn comment() {
371+
// four lines, two space indented, except for line 1 which is blank.
372+
let mut doc = Rope::from(" 1\n\n 2\n 3");
373+
// select whole document
374+
let selection = Selection::single(0, doc.len_chars() - 1);
362375

363-
let transaction = toggle_line_comments(&doc, &selection, None);
364-
transaction.apply(&mut doc);
365-
selection = selection.map(transaction.changes());
366-
assert_eq!(doc, "");
367-
assert!(selection.len() == 1); // to ignore the selection unused warning
376+
let transaction = toggle_line_comments(&doc, &selection, None);
377+
transaction.apply(&mut doc);
378+
379+
assert_eq!(doc, " // 1\n\n // 2\n // 3");
380+
}
368381

369-
// TODO: account for uncommenting with uneven comment indentation
382+
#[test]
383+
fn uncomment() {
384+
let mut doc = Rope::from(" // 1\n\n // 2\n // 3");
385+
let mut selection = Selection::single(0, doc.len_chars() - 1);
386+
387+
let transaction = toggle_line_comments(&doc, &selection, None);
388+
transaction.apply(&mut doc);
389+
selection = selection.map(transaction.changes());
390+
391+
assert_eq!(doc, " 1\n\n 2\n 3");
392+
assert!(selection.len() == 1); // to ignore the selection unused warning
393+
}
394+
395+
#[test]
396+
fn uncomment_0_margin_comments() {
397+
let mut doc = Rope::from(" //1\n\n //2\n //3");
398+
let mut selection = Selection::single(0, doc.len_chars() - 1);
399+
400+
let transaction = toggle_line_comments(&doc, &selection, None);
401+
transaction.apply(&mut doc);
402+
selection = selection.map(transaction.changes());
403+
404+
assert_eq!(doc, " 1\n\n 2\n 3");
405+
assert!(selection.len() == 1); // to ignore the selection unused warning
406+
}
407+
408+
#[test]
409+
fn uncomment_0_margin_comments_with_no_space() {
410+
let mut doc = Rope::from("//");
411+
let mut selection = Selection::single(0, doc.len_chars() - 1);
412+
413+
let transaction = toggle_line_comments(&doc, &selection, None);
414+
transaction.apply(&mut doc);
415+
selection = selection.map(transaction.changes());
416+
assert_eq!(doc, "");
417+
assert!(selection.len() == 1); // to ignore the selection unused warning
418+
}
370419
}
371420

372421
#[test]
@@ -413,4 +462,32 @@ mod test {
413462
transaction.apply(&mut doc);
414463
assert_eq!(doc, "");
415464
}
465+
466+
/// Test, if `get_comment_tokens` works, even if the content of the file includes chars, whose
467+
/// byte size unequal the amount of chars
468+
#[test]
469+
fn test_get_comment_with_char_boundaries() {
470+
let rope = Rope::from("··");
471+
let tokens = ["//", "///"];
472+
473+
assert_eq!(
474+
super::get_comment_token(rope.slice(..), tokens.as_slice(), 0),
475+
None
476+
);
477+
}
478+
479+
/// Test for `get_comment_token`.
480+
///
481+
/// Assuming the comment tokens are stored as `["///", "//"]`, `get_comment_token` should still
482+
/// return `///` instead of `//` if the user is in a doc-comment section.
483+
#[test]
484+
fn test_use_longest_comment() {
485+
let text = Rope::from(" /// amogus");
486+
let tokens = ["///", "//"];
487+
488+
assert_eq!(
489+
super::get_comment_token(text.slice(..), tokens.as_slice(), 0),
490+
Some("///")
491+
);
492+
}
416493
}

helix-term/src/commands.rs

Lines changed: 69 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ use helix_core::{
2222
encoding, find_workspace,
2323
graphemes::{self, next_grapheme_boundary, RevRopeGraphemes},
2424
history::UndoKind,
25-
increment, indent,
26-
indent::IndentStyle,
25+
increment,
26+
indent::{self, IndentStyle},
2727
line_ending::{get_line_ending_of_str, line_end_char_index},
2828
match_brackets,
2929
movement::{self, move_vertically_visual, Direction},
@@ -3467,31 +3467,51 @@ fn open(cx: &mut Context, open: Open) {
34673467
)
34683468
};
34693469

3470-
let indent = indent::indent_for_newline(
3471-
doc.language_config(),
3472-
doc.syntax(),
3473-
&doc.config.load().indent_heuristic,
3474-
&doc.indent_style,
3475-
doc.tab_width(),
3476-
text,
3477-
line_num,
3478-
line_end_index,
3479-
cursor_line,
3480-
);
3470+
let continue_comment_token = doc
3471+
.language_config()
3472+
.and_then(|config| config.comment_tokens.as_ref())
3473+
.and_then(|tokens| comment::get_comment_token(text, tokens, cursor_line));
3474+
3475+
let line = text.line(cursor_line);
3476+
let indent = match line.first_non_whitespace_char() {
3477+
Some(pos) if continue_comment_token.is_some() => line.slice(..pos).to_string(),
3478+
_ => indent::indent_for_newline(
3479+
doc.language_config(),
3480+
doc.syntax(),
3481+
&doc.config.load().indent_heuristic,
3482+
&doc.indent_style,
3483+
doc.tab_width(),
3484+
text,
3485+
line_num,
3486+
line_end_index,
3487+
cursor_line,
3488+
),
3489+
};
34813490

34823491
let indent_len = indent.len();
34833492
let mut text = String::with_capacity(1 + indent_len);
34843493
text.push_str(doc.line_ending.as_str());
34853494
text.push_str(&indent);
3495+
3496+
if let Some(token) = continue_comment_token {
3497+
text.push_str(token);
3498+
text.push(' ');
3499+
}
3500+
34863501
let text = text.repeat(count);
34873502

34883503
// calculate new selection ranges
34893504
let pos = offs + line_end_index + line_end_offset_width;
3505+
let comment_len = continue_comment_token
3506+
.map(|token| token.len() + 1) // `+ 1` for the extra space added
3507+
.unwrap_or_default();
34903508
for i in 0..count {
34913509
// pos -> beginning of reference line,
3492-
// + (i * (1+indent_len)) -> beginning of i'th line from pos
3493-
// + indent_len -> -> indent for i'th line
3494-
ranges.push(Range::point(pos + (i * (1 + indent_len)) + indent_len));
3510+
// + (i * (1+indent_len + comment_len)) -> beginning of i'th line from pos (possibly including comment token)
3511+
// + indent_len + comment_len -> -> indent for i'th line
3512+
ranges.push(Range::point(
3513+
pos + (i * (1 + indent_len + comment_len)) + indent_len + comment_len,
3514+
));
34953515
}
34963516

34973517
offs += text.chars().count();
@@ -3929,6 +3949,11 @@ pub mod insert {
39293949

39303950
let mut new_text = String::new();
39313951

3952+
let continue_comment_token = doc
3953+
.language_config()
3954+
.and_then(|config| config.comment_tokens.as_ref())
3955+
.and_then(|tokens| comment::get_comment_token(text, tokens, current_line));
3956+
39323957
// If the current line is all whitespace, insert a line ending at the beginning of
39333958
// the current line. This makes the current line empty and the new line contain the
39343959
// indentation of the old line.
@@ -3938,17 +3963,22 @@ pub mod insert {
39383963

39393964
(line_start, line_start, new_text.chars().count())
39403965
} else {
3941-
let indent = indent::indent_for_newline(
3942-
doc.language_config(),
3943-
doc.syntax(),
3944-
&doc.config.load().indent_heuristic,
3945-
&doc.indent_style,
3946-
doc.tab_width(),
3947-
text,
3948-
current_line,
3949-
pos,
3950-
current_line,
3951-
);
3966+
let line = text.line(current_line);
3967+
3968+
let indent = match line.first_non_whitespace_char() {
3969+
Some(pos) if continue_comment_token.is_some() => line.slice(..pos).to_string(),
3970+
_ => indent::indent_for_newline(
3971+
doc.language_config(),
3972+
doc.syntax(),
3973+
&doc.config.load().indent_heuristic,
3974+
&doc.indent_style,
3975+
doc.tab_width(),
3976+
text,
3977+
current_line,
3978+
pos,
3979+
current_line,
3980+
),
3981+
};
39523982

39533983
// If we are between pairs (such as brackets), we want to
39543984
// insert an additional line which is indented one level
@@ -3958,19 +3988,30 @@ pub mod insert {
39583988
.and_then(|pairs| pairs.get(prev))
39593989
.map_or(false, |pair| pair.open == prev && pair.close == curr);
39603990

3961-
let local_offs = if on_auto_pair {
3991+
let local_offs = if let Some(token) = continue_comment_token {
3992+
new_text.push_str(doc.line_ending.as_str());
3993+
new_text.push_str(&indent);
3994+
new_text.push_str(token);
3995+
new_text.push(' ');
3996+
new_text.chars().count()
3997+
} else if on_auto_pair {
3998+
// line where the cursor will be
39623999
let inner_indent = indent.clone() + doc.indent_style.as_str();
39634000
new_text.reserve_exact(2 + indent.len() + inner_indent.len());
39644001
new_text.push_str(doc.line_ending.as_str());
39654002
new_text.push_str(&inner_indent);
4003+
4004+
// line where the matching pair will be
39664005
let local_offs = new_text.chars().count();
39674006
new_text.push_str(doc.line_ending.as_str());
39684007
new_text.push_str(&indent);
4008+
39694009
local_offs
39704010
} else {
39714011
new_text.reserve_exact(1 + indent.len());
39724012
new_text.push_str(doc.line_ending.as_str());
39734013
new_text.push_str(&indent);
4014+
39744015
new_text.chars().count()
39754016
};
39764017

0 commit comments

Comments
 (0)