Skip to content

Commit 546b64b

Browse files
author
guttermonk
committed
Implemented the "press Enter twice to end list/comment" feature
1 parent 066dded commit 546b64b

File tree

3 files changed

+371
-23
lines changed

3 files changed

+371
-23
lines changed

helix-core/src/comment.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,31 @@ pub fn get_comment_token<'a, S: AsRef<str>>(
2828
.max_by_key(|token| token.len())
2929
}
3030

31+
/// Checks if a line contains only a comment token (with optional trailing whitespace).
32+
/// Returns `true` if the line is an "empty comment" that should be cleared when
33+
/// pressing Enter twice to end comment continuation.
34+
pub fn is_empty_comment_line(text: RopeSlice, token: &str, line_num: usize) -> bool {
35+
let line = text.line(line_num);
36+
let Some(start) = line.first_non_whitespace_char() else {
37+
return false;
38+
};
39+
40+
let content_after_indent = line.slice(start..);
41+
// Check if the line starts with the token
42+
if !content_after_indent.starts_with(token) {
43+
return false;
44+
}
45+
46+
// Get the content after the token
47+
let after_token = content_after_indent.slice(token.chars().count()..);
48+
49+
// The line is an "empty comment" if there's nothing after the token,
50+
// or only whitespace after the token
51+
after_token
52+
.chars()
53+
.all(|c| c.is_whitespace() || c == '\n' || c == '\r')
54+
}
55+
3156
/// Given text, a comment token, and a set of line indices, returns the following:
3257
/// - Whether the given lines should be considered commented
3358
/// - If any of the lines are uncommented, all lines are considered as such.

helix-term/src/commands.rs

Lines changed: 124 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3706,6 +3706,18 @@ fn open(cx: &mut Context, open: Open, comment_continuation: CommentContinuation)
37063706
let continue_comment_token = continue_comment_tokens
37073707
.and_then(|tokens| comment::get_comment_token(text, tokens, curr_line_num));
37083708

3709+
// Check if this is an empty comment line (just the token with optional whitespace).
3710+
// If so, we clear the comment token instead of continuing it.
3711+
let is_empty_comment = continue_comment_token
3712+
.is_some_and(|token| comment::is_empty_comment_line(text, token, curr_line_num));
3713+
3714+
// If empty comment, don't continue the comment
3715+
let continue_comment_token = if is_empty_comment {
3716+
None
3717+
} else {
3718+
continue_comment_token
3719+
};
3720+
37093721
// Index to insert newlines after, as well as the char width
37103722
// to use to compensate for those inserted newlines.
37113723
let (above_next_line_end_index, above_next_line_end_width) = if next_new_line_num == 0 {
@@ -3755,30 +3767,95 @@ fn open(cx: &mut Context, open: Open, comment_continuation: CommentContinuation)
37553767

37563768
let text = text.repeat(count);
37573769

3758-
// calculate new selection ranges
3759-
let pos = offs + above_next_line_end_index + above_next_line_end_width;
3760-
let comment_len = continue_comment_token
3761-
.map(|token| token.len() + 1) // `+ 1` for the extra space added
3762-
.unwrap_or_default();
3763-
for i in 0..count {
3764-
// pos -> beginning of reference line,
3765-
// + (i * (line_ending_len + indent_len + comment_len)) -> beginning of i'th line from pos (possibly including comment token)
3766-
// + indent_len + comment_len -> -> indent for i'th line
3767-
ranges.push(Range::point(
3768-
pos + (i * (doc.line_ending.len_chars() + indent_len + comment_len))
3769-
+ indent_len
3770-
+ comment_len,
3771-
));
3772-
}
3770+
// calculate new selection ranges and return the change
3771+
if is_empty_comment {
3772+
// For empty comment lines, delete the token from the current line
3773+
// and insert just the newline with indentation
3774+
let curr_line_start = contents.line_to_char(curr_line_num);
3775+
let indent_end_pos = curr_line_start + line.first_non_whitespace_char().unwrap_or(0);
3776+
let curr_line_end = line_end_char_index(&contents.slice(..), curr_line_num);
3777+
3778+
if open == Open::Above {
3779+
// For O (open above), we need to insert a new line above while clearing
3780+
// the token from the current line. We expand the replacement range to
3781+
// include the end of the previous line (or start of file).
3782+
let (replace_from, replacement_text) = if curr_line_num == 0 {
3783+
// First line: replace from start of file
3784+
// Replace " - " with " \n " (indent + newline + indent)
3785+
let mut replacement = String::with_capacity(indent_len * 2 + doc.line_ending.len_chars());
3786+
replacement.push_str(&indent);
3787+
replacement.push_str(doc.line_ending.as_str());
3788+
replacement.push_str(&indent);
3789+
(0, replacement)
3790+
} else {
3791+
// Not first line: replace from end of previous line
3792+
// Replace "\n - " with "\n \n " (newline + indent + newline + indent)
3793+
let mut replacement = String::with_capacity(indent_len * 2 + doc.line_ending.len_chars() * 2);
3794+
replacement.push_str(doc.line_ending.as_str());
3795+
replacement.push_str(&indent);
3796+
replacement.push_str(doc.line_ending.as_str());
3797+
replacement.push_str(&indent);
3798+
(above_next_line_end_index, replacement)
3799+
};
37733800

3774-
// update the offset for the next range
3775-
offs += text.chars().count();
3801+
let replacement_text = replacement_text.repeat(count);
3802+
let chars_deleted = curr_line_end - replace_from;
37763803

3777-
(
3778-
above_next_line_end_index,
3779-
above_next_line_end_index,
3780-
Some(text.into()),
3781-
)
3804+
// Cursor should be on the new line (which is above the cleared line)
3805+
for i in 0..count {
3806+
let cursor_pos = if curr_line_num == 0 {
3807+
// On first line, cursor at end of first indent
3808+
offs + indent_len + i * (doc.line_ending.len_chars() + indent_len)
3809+
} else {
3810+
// Not first line, cursor after first newline + indent
3811+
offs + replace_from + doc.line_ending.len_chars() + indent_len
3812+
+ i * (doc.line_ending.len_chars() + indent_len)
3813+
};
3814+
ranges.push(Range::point(cursor_pos));
3815+
}
3816+
3817+
offs += replacement_text.chars().count() - chars_deleted;
3818+
(replace_from, curr_line_end, Some(replacement_text.into()))
3819+
} else {
3820+
// For o (open below), replace the token with newline + indent
3821+
let chars_deleted = curr_line_end - indent_end_pos;
3822+
3823+
// Calculate cursor position on the new line
3824+
for i in 0..count {
3825+
ranges.push(Range::point(
3826+
offs + indent_end_pos
3827+
+ (i + 1) * (doc.line_ending.len_chars() + indent_len),
3828+
));
3829+
}
3830+
3831+
offs += text.chars().count() - chars_deleted;
3832+
(indent_end_pos, curr_line_end, Some(text.into()))
3833+
}
3834+
} else {
3835+
let pos = offs + above_next_line_end_index + above_next_line_end_width;
3836+
let comment_len = continue_comment_token
3837+
.map(|token| token.len() + 1) // `+ 1` for the extra space added
3838+
.unwrap_or_default();
3839+
for i in 0..count {
3840+
// pos -> beginning of reference line,
3841+
// + (i * (line_ending_len + indent_len + comment_len)) -> beginning of i'th line from pos (possibly including comment token)
3842+
// + indent_len + comment_len -> -> indent for i'th line
3843+
ranges.push(Range::point(
3844+
pos + (i * (doc.line_ending.len_chars() + indent_len + comment_len))
3845+
+ indent_len
3846+
+ comment_len,
3847+
));
3848+
}
3849+
3850+
// update the offset for the next range
3851+
offs += text.chars().count();
3852+
3853+
(
3854+
above_next_line_end_index,
3855+
above_next_line_end_index,
3856+
Some(text.into()),
3857+
)
3858+
}
37823859
});
37833860

37843861
transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
@@ -4321,7 +4398,31 @@ pub mod insert {
43214398
let continue_comment_token = continue_comment_tokens
43224399
.and_then(|tokens| comment::get_comment_token(text, tokens, current_line));
43234400

4324-
let (from, to, local_offs) = if let Some(idx) =
4401+
// Check if this is an empty comment line (just the token with optional whitespace).
4402+
// If so, we clear the comment token instead of continuing it.
4403+
let is_empty_comment = continue_comment_token
4404+
.is_some_and(|token| comment::is_empty_comment_line(text, token, current_line));
4405+
4406+
let (from, to, local_offs) = if is_empty_comment {
4407+
// Clear the empty comment token and insert a plain newline with just indentation
4408+
let line = text.line(current_line);
4409+
let indent_end = line.first_non_whitespace_char().unwrap_or(0);
4410+
let indent = line.slice(..indent_end).to_string();
4411+
4412+
new_text.reserve_exact(line_ending.len() + indent.len());
4413+
new_text.push_str(line_ending);
4414+
new_text.push_str(&indent);
4415+
4416+
let delete_from = line_start + indent_end;
4417+
chars_deleted = pos - delete_from;
4418+
last_pos = pos;
4419+
4420+
(
4421+
delete_from,
4422+
pos,
4423+
new_text.chars().count() as isize - chars_deleted as isize,
4424+
)
4425+
} else if let Some(idx) =
43254426
text.slice(line_start..pos).last_non_whitespace_char()
43264427
{
43274428
let first_trailing_whitespace_char = (line_start + idx + 1).clamp(last_pos, pos);

0 commit comments

Comments
 (0)