Continue line comments (#10996)

pull/11830/head^2
TornaxO7 4 weeks ago committed by GitHub
parent a1453350df
commit be2884d800
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -9,6 +9,24 @@ use crate::{
use helix_stdx::rope::RopeSliceExt; use helix_stdx::rope::RopeSliceExt;
use std::borrow::Cow; use std::borrow::Cow;
pub const DEFAULT_COMMENT_TOKEN: &str = "//";
/// Returns the longest matching comment token of the given line (if it exists).
pub fn get_comment_token<'a, S: AsRef<str>>(
text: RopeSlice,
tokens: &'a [S],
line_num: usize,
) -> Option<&'a str> {
let line = text.line(line_num);
let start = line.first_non_whitespace_char()?;
tokens
.iter()
.map(AsRef::as_ref)
.filter(|token| line.slice(start..).starts_with(token))
.max_by_key(|token| token.len())
}
/// Given text, a comment token, and a set of line indices, returns the following: /// Given text, a comment token, and a set of line indices, returns the following:
/// - Whether the given lines should be considered commented /// - Whether the given lines should be considered commented
/// - If any of the lines are uncommented, all lines are considered as such. /// - If any of the lines are uncommented, all lines are considered as such.
@ -28,21 +46,20 @@ fn find_line_comment(
let mut min = usize::MAX; // minimum col for first_non_whitespace_char let mut min = usize::MAX; // minimum col for first_non_whitespace_char
let mut margin = 1; let mut margin = 1;
let token_len = token.chars().count(); let token_len = token.chars().count();
for line in lines { for line in lines {
let line_slice = text.line(line); let line_slice = text.line(line);
if let Some(pos) = line_slice.first_non_whitespace_char() { if let Some(pos) = line_slice.first_non_whitespace_char() {
let len = line_slice.len_chars(); let len = line_slice.len_chars();
if pos < min { min = std::cmp::min(min, pos);
min = pos;
}
// line can be shorter than pos + token len // line can be shorter than pos + token len
let fragment = Cow::from(line_slice.slice(pos..std::cmp::min(pos + token.len(), len))); let fragment = Cow::from(line_slice.slice(pos..std::cmp::min(pos + token.len(), len)));
// as soon as one of the non-blank lines doesn't have a comment, the whole block is
// considered uncommented.
if fragment != token { 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; commented = false;
} }
@ -56,6 +73,7 @@ fn find_line_comment(
to_change.push(line); to_change.push(line);
} }
} }
(commented, to_change, min, margin) (commented, to_change, min, margin)
} }
@ -63,7 +81,7 @@ fn find_line_comment(
pub fn toggle_line_comments(doc: &Rope, selection: &Selection, token: Option<&str>) -> Transaction { pub fn toggle_line_comments(doc: &Rope, selection: &Selection, token: Option<&str>) -> Transaction {
let text = doc.slice(..); let text = doc.slice(..);
let token = token.unwrap_or("//"); let token = token.unwrap_or(DEFAULT_COMMENT_TOKEN);
let comment = Tendril::from(format!("{} ", token)); let comment = Tendril::from(format!("{} ", token));
let mut lines: Vec<usize> = Vec::with_capacity(selection.len()); 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
mod test { mod test {
use super::*; use super::*;
#[test] mod find_line_comment {
fn test_find_line_comment() { use super::*;
// four lines, two space indented, except for line 1 which is blank.
let mut doc = Rope::from(" 1\n\n 2\n 3");
// select whole document
let mut selection = Selection::single(0, doc.len_chars() - 1);
let text = doc.slice(..); #[test]
fn not_commented() {
// four lines, two space indented, except for line 1 which is blank.
let doc = Rope::from(" 1\n\n 2\n 3");
let res = find_line_comment("//", text, 0..3); let text = doc.slice(..);
// (commented = true, to_change = [line 0, line 2], min = col 2, margin = 0)
assert_eq!(res, (false, vec![0, 2], 2, 0));
// comment let res = find_line_comment("//", text, 0..3);
let transaction = toggle_line_comments(&doc, &selection, None); // (commented = false, to_change = [line 0, line 2], min = col 2, margin = 0)
transaction.apply(&mut doc); assert_eq!(res, (false, vec![0, 2], 2, 0));
selection = selection.map(transaction.changes()); }
assert_eq!(doc, " // 1\n\n // 2\n // 3"); #[test]
fn is_commented() {
// three lines where the second line is empty.
let doc = Rope::from("// hello\n\n// there");
// uncomment let res = find_line_comment("//", doc.slice(..), 0..3);
let transaction = toggle_line_comments(&doc, &selection, None);
transaction.apply(&mut doc);
selection = selection.map(transaction.changes());
assert_eq!(doc, " 1\n\n 2\n 3");
assert!(selection.len() == 1); // to ignore the selection unused warning
// 0 margin comments // (commented = true, to_change = [line 0, line 2], min = col 0, margin = 1)
doc = Rope::from(" //1\n\n //2\n //3"); assert_eq!(res, (true, vec![0, 2], 0, 1));
// reset the selection. }
selection = Selection::single(0, doc.len_chars() - 1); }
let transaction = toggle_line_comments(&doc, &selection, None); // TODO: account for uncommenting with uneven comment indentation
transaction.apply(&mut doc); mod toggle_line_comment {
selection = selection.map(transaction.changes()); use super::*;
assert_eq!(doc, " 1\n\n 2\n 3");
assert!(selection.len() == 1); // to ignore the selection unused warning
// 0 margin comments, with no space #[test]
doc = Rope::from("//"); fn comment() {
// reset the selection. // four lines, two space indented, except for line 1 which is blank.
selection = Selection::single(0, doc.len_chars() - 1); let mut doc = Rope::from(" 1\n\n 2\n 3");
// select whole document
let selection = Selection::single(0, doc.len_chars() - 1);
let transaction = toggle_line_comments(&doc, &selection, None); let transaction = toggle_line_comments(&doc, &selection, None);
transaction.apply(&mut doc); transaction.apply(&mut doc);
selection = selection.map(transaction.changes());
assert_eq!(doc, ""); assert_eq!(doc, " // 1\n\n // 2\n // 3");
assert!(selection.len() == 1); // to ignore the selection unused warning }
// TODO: account for uncommenting with uneven comment indentation #[test]
fn uncomment() {
let mut doc = Rope::from(" // 1\n\n // 2\n // 3");
let mut selection = Selection::single(0, doc.len_chars() - 1);
let transaction = toggle_line_comments(&doc, &selection, None);
transaction.apply(&mut doc);
selection = selection.map(transaction.changes());
assert_eq!(doc, " 1\n\n 2\n 3");
assert!(selection.len() == 1); // to ignore the selection unused warning
}
#[test]
fn uncomment_0_margin_comments() {
let mut doc = Rope::from(" //1\n\n //2\n //3");
let mut selection = Selection::single(0, doc.len_chars() - 1);
let transaction = toggle_line_comments(&doc, &selection, None);
transaction.apply(&mut doc);
selection = selection.map(transaction.changes());
assert_eq!(doc, " 1\n\n 2\n 3");
assert!(selection.len() == 1); // to ignore the selection unused warning
}
#[test]
fn uncomment_0_margin_comments_with_no_space() {
let mut doc = Rope::from("//");
let mut selection = Selection::single(0, doc.len_chars() - 1);
let transaction = toggle_line_comments(&doc, &selection, None);
transaction.apply(&mut doc);
selection = selection.map(transaction.changes());
assert_eq!(doc, "");
assert!(selection.len() == 1); // to ignore the selection unused warning
}
} }
#[test] #[test]
@ -413,4 +462,32 @@ mod test {
transaction.apply(&mut doc); transaction.apply(&mut doc);
assert_eq!(doc, ""); assert_eq!(doc, "");
} }
/// Test, if `get_comment_tokens` works, even if the content of the file includes chars, whose
/// byte size unequal the amount of chars
#[test]
fn test_get_comment_with_char_boundaries() {
let rope = Rope::from("··");
let tokens = ["//", "///"];
assert_eq!(
super::get_comment_token(rope.slice(..), tokens.as_slice(), 0),
None
);
}
/// Test for `get_comment_token`.
///
/// Assuming the comment tokens are stored as `["///", "//"]`, `get_comment_token` should still
/// return `///` instead of `//` if the user is in a doc-comment section.
#[test]
fn test_use_longest_comment() {
let text = Rope::from(" /// amogus");
let tokens = ["///", "//"];
assert_eq!(
super::get_comment_token(text.slice(..), tokens.as_slice(), 0),
Some("///")
);
}
} }

@ -22,8 +22,8 @@ use helix_core::{
encoding, find_workspace, encoding, find_workspace,
graphemes::{self, next_grapheme_boundary, RevRopeGraphemes}, graphemes::{self, next_grapheme_boundary, RevRopeGraphemes},
history::UndoKind, history::UndoKind,
increment, indent, increment,
indent::IndentStyle, indent::{self, IndentStyle},
line_ending::{get_line_ending_of_str, line_end_char_index}, line_ending::{get_line_ending_of_str, line_end_char_index},
match_brackets, match_brackets,
movement::{self, move_vertically_visual, Direction}, movement::{self, move_vertically_visual, Direction},
@ -3467,31 +3467,51 @@ fn open(cx: &mut Context, open: Open) {
) )
}; };
let indent = indent::indent_for_newline( let continue_comment_token = doc
doc.language_config(), .language_config()
doc.syntax(), .and_then(|config| config.comment_tokens.as_ref())
&doc.config.load().indent_heuristic, .and_then(|tokens| comment::get_comment_token(text, tokens, cursor_line));
&doc.indent_style,
doc.tab_width(), let line = text.line(cursor_line);
text, let indent = match line.first_non_whitespace_char() {
line_num, Some(pos) if continue_comment_token.is_some() => line.slice(..pos).to_string(),
line_end_index, _ => indent::indent_for_newline(
cursor_line, doc.language_config(),
); doc.syntax(),
&doc.config.load().indent_heuristic,
&doc.indent_style,
doc.tab_width(),
text,
line_num,
line_end_index,
cursor_line,
),
};
let indent_len = indent.len(); let indent_len = indent.len();
let mut text = String::with_capacity(1 + indent_len); let mut text = String::with_capacity(1 + indent_len);
text.push_str(doc.line_ending.as_str()); text.push_str(doc.line_ending.as_str());
text.push_str(&indent); text.push_str(&indent);
if let Some(token) = continue_comment_token {
text.push_str(token);
text.push(' ');
}
let text = text.repeat(count); let text = text.repeat(count);
// calculate new selection ranges // calculate new selection ranges
let pos = offs + line_end_index + line_end_offset_width; let pos = offs + line_end_index + line_end_offset_width;
let comment_len = continue_comment_token
.map(|token| token.len() + 1) // `+ 1` for the extra space added
.unwrap_or_default();
for i in 0..count { for i in 0..count {
// pos -> beginning of reference line, // pos -> beginning of reference line,
// + (i * (1+indent_len)) -> beginning of i'th line from pos // + (i * (1+indent_len + comment_len)) -> beginning of i'th line from pos (possibly including comment token)
// + indent_len -> -> indent for i'th line // + indent_len + comment_len -> -> indent for i'th line
ranges.push(Range::point(pos + (i * (1 + indent_len)) + indent_len)); ranges.push(Range::point(
pos + (i * (1 + indent_len + comment_len)) + indent_len + comment_len,
));
} }
offs += text.chars().count(); offs += text.chars().count();
@ -3929,6 +3949,11 @@ pub mod insert {
let mut new_text = String::new(); let mut new_text = String::new();
let continue_comment_token = doc
.language_config()
.and_then(|config| config.comment_tokens.as_ref())
.and_then(|tokens| comment::get_comment_token(text, tokens, current_line));
// If the current line is all whitespace, insert a line ending at the beginning of // If the current line is all whitespace, insert a line ending at the beginning of
// the current line. This makes the current line empty and the new line contain the // the current line. This makes the current line empty and the new line contain the
// indentation of the old line. // indentation of the old line.
@ -3938,17 +3963,22 @@ pub mod insert {
(line_start, line_start, new_text.chars().count()) (line_start, line_start, new_text.chars().count())
} else { } else {
let indent = indent::indent_for_newline( let line = text.line(current_line);
doc.language_config(),
doc.syntax(), let indent = match line.first_non_whitespace_char() {
&doc.config.load().indent_heuristic, Some(pos) if continue_comment_token.is_some() => line.slice(..pos).to_string(),
&doc.indent_style, _ => indent::indent_for_newline(
doc.tab_width(), doc.language_config(),
text, doc.syntax(),
current_line, &doc.config.load().indent_heuristic,
pos, &doc.indent_style,
current_line, doc.tab_width(),
); text,
current_line,
pos,
current_line,
),
};
// If we are between pairs (such as brackets), we want to // If we are between pairs (such as brackets), we want to
// insert an additional line which is indented one level // insert an additional line which is indented one level
@ -3958,19 +3988,30 @@ pub mod insert {
.and_then(|pairs| pairs.get(prev)) .and_then(|pairs| pairs.get(prev))
.map_or(false, |pair| pair.open == prev && pair.close == curr); .map_or(false, |pair| pair.open == prev && pair.close == curr);
let local_offs = if on_auto_pair { let local_offs = if let Some(token) = continue_comment_token {
new_text.push_str(doc.line_ending.as_str());
new_text.push_str(&indent);
new_text.push_str(token);
new_text.push(' ');
new_text.chars().count()
} else if on_auto_pair {
// line where the cursor will be
let inner_indent = indent.clone() + doc.indent_style.as_str(); let inner_indent = indent.clone() + doc.indent_style.as_str();
new_text.reserve_exact(2 + indent.len() + inner_indent.len()); new_text.reserve_exact(2 + indent.len() + inner_indent.len());
new_text.push_str(doc.line_ending.as_str()); new_text.push_str(doc.line_ending.as_str());
new_text.push_str(&inner_indent); new_text.push_str(&inner_indent);
// line where the matching pair will be
let local_offs = new_text.chars().count(); let local_offs = new_text.chars().count();
new_text.push_str(doc.line_ending.as_str()); new_text.push_str(doc.line_ending.as_str());
new_text.push_str(&indent); new_text.push_str(&indent);
local_offs local_offs
} else { } else {
new_text.reserve_exact(1 + indent.len()); new_text.reserve_exact(1 + indent.len());
new_text.push_str(doc.line_ending.as_str()); new_text.push_str(doc.line_ending.as_str());
new_text.push_str(&indent); new_text.push_str(&indent);
new_text.chars().count() new_text.chars().count()
}; };

Loading…
Cancel
Save