editor: Support rewrap in block comments (#34418)
This updates `editor: rewrap` to work within doc comments, based on the code that extends such comments on newline. I added some tests, and I've tested it out in JS, C and PHP. (Though PHP depends on https://github.com/zed-extensions/php/pull/40) Closes #19794 Closes #18221 **Caveat:** ~~This will not rewrap an existing single-line block comment, such as the one provided in #18221:~~ this will now rewrap as expected ```c /* we can triangulate any convex polygon by picking a vertex and connecting it to the next two vertices; we first read two vertices, and then, for every subsequent vertex, we can form a triangle by connecting it to the first and previous vertex */ ``` However, it will rewrap a similar comment if it is shaped like a doc comment. In other words, this will rewrap as expected: ```c /* * we can triangulate any convex polygon by picking a vertex and connecting it to the next two vertices; we first read two vertices, and then, for every subsequent vertex, we can form a triangle by connecting it to the first and previous vertex */ ``` This seems like a reasonable improvement and limitation to me, especially as a first step. cc @smitbarmase because I think that you've been making a lot of the `newline` and `rewrap` changes recently. (Thank you for those, by the way!) Release Notes: - Added support for rewrap in block comments. --------- Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
This commit is contained in:
co-authored by
Smit Barmase
parent
965dbc988f
commit
2ba25b5c94
+164
-51
@@ -11816,6 +11816,18 @@ impl Editor {
|
||||
let buffer = self.buffer.read(cx).snapshot(cx);
|
||||
let selections = self.selections.all::<Point>(cx);
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
enum CommentFormat {
|
||||
/// single line comment, with prefix for line
|
||||
Line(String),
|
||||
/// single line within a block comment, with prefix for line
|
||||
BlockLine(String),
|
||||
/// a single line of a block comment that includes the initial delimiter
|
||||
BlockCommentWithStart(BlockCommentConfig),
|
||||
/// a single line of a block comment that includes the ending delimiter
|
||||
BlockCommentWithEnd(BlockCommentConfig),
|
||||
}
|
||||
|
||||
// Split selections to respect paragraph, indent, and comment prefix boundaries.
|
||||
let wrap_ranges = selections.into_iter().flat_map(|selection| {
|
||||
let mut non_blank_rows_iter = (selection.start.row..=selection.end.row)
|
||||
@@ -11832,37 +11844,75 @@ impl Editor {
|
||||
let language_scope = buffer.language_scope_at(selection.head());
|
||||
|
||||
let indent_and_prefix_for_row =
|
||||
|row: u32| -> (IndentSize, Option<String>, Option<String>) {
|
||||
|row: u32| -> (IndentSize, Option<CommentFormat>, Option<String>) {
|
||||
let indent = buffer.indent_size_for_line(MultiBufferRow(row));
|
||||
let (comment_prefix, rewrap_prefix) =
|
||||
if let Some(language_scope) = &language_scope {
|
||||
let indent_end = Point::new(row, indent.len);
|
||||
let comment_prefix = language_scope
|
||||
let (comment_prefix, rewrap_prefix) = if let Some(language_scope) =
|
||||
&language_scope
|
||||
{
|
||||
let indent_end = Point::new(row, indent.len);
|
||||
let line_end = Point::new(row, buffer.line_len(MultiBufferRow(row)));
|
||||
let line_text_after_indent = buffer
|
||||
.text_for_range(indent_end..line_end)
|
||||
.collect::<String>();
|
||||
|
||||
let is_within_comment_override = buffer
|
||||
.language_scope_at(indent_end)
|
||||
.is_some_and(|scope| scope.override_name() == Some("comment"));
|
||||
let comment_delimiters = if is_within_comment_override {
|
||||
// we are within a comment syntax node, but we don't
|
||||
// yet know what kind of comment: block, doc or line
|
||||
match (
|
||||
language_scope.documentation_comment(),
|
||||
language_scope.block_comment(),
|
||||
) {
|
||||
(Some(config), _) | (_, Some(config))
|
||||
if buffer.contains_str_at(indent_end, &config.start) =>
|
||||
{
|
||||
Some(CommentFormat::BlockCommentWithStart(config.clone()))
|
||||
}
|
||||
(Some(config), _) | (_, Some(config))
|
||||
if line_text_after_indent.ends_with(config.end.as_ref()) =>
|
||||
{
|
||||
Some(CommentFormat::BlockCommentWithEnd(config.clone()))
|
||||
}
|
||||
(Some(config), _) | (_, Some(config))
|
||||
if buffer.contains_str_at(indent_end, &config.prefix) =>
|
||||
{
|
||||
Some(CommentFormat::BlockLine(config.prefix.to_string()))
|
||||
}
|
||||
(_, _) => language_scope
|
||||
.line_comment_prefixes()
|
||||
.iter()
|
||||
.find(|prefix| buffer.contains_str_at(indent_end, prefix))
|
||||
.map(|prefix| CommentFormat::Line(prefix.to_string())),
|
||||
}
|
||||
} else {
|
||||
// we not in an overridden comment node, but we may
|
||||
// be within a non-overridden line comment node
|
||||
language_scope
|
||||
.line_comment_prefixes()
|
||||
.iter()
|
||||
.find(|prefix| buffer.contains_str_at(indent_end, prefix))
|
||||
.map(|prefix| prefix.to_string());
|
||||
let line_end = Point::new(row, buffer.line_len(MultiBufferRow(row)));
|
||||
let line_text_after_indent = buffer
|
||||
.text_for_range(indent_end..line_end)
|
||||
.collect::<String>();
|
||||
let rewrap_prefix = language_scope
|
||||
.rewrap_prefixes()
|
||||
.iter()
|
||||
.find_map(|prefix_regex| {
|
||||
prefix_regex.find(&line_text_after_indent).map(|mat| {
|
||||
if mat.start() == 0 {
|
||||
Some(mat.as_str().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.flatten();
|
||||
(comment_prefix, rewrap_prefix)
|
||||
} else {
|
||||
(None, None)
|
||||
.map(|prefix| CommentFormat::Line(prefix.to_string()))
|
||||
};
|
||||
|
||||
let rewrap_prefix = language_scope
|
||||
.rewrap_prefixes()
|
||||
.iter()
|
||||
.find_map(|prefix_regex| {
|
||||
prefix_regex.find(&line_text_after_indent).map(|mat| {
|
||||
if mat.start() == 0 {
|
||||
Some(mat.as_str().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.flatten();
|
||||
(comment_delimiters, rewrap_prefix)
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
(indent, comment_prefix, rewrap_prefix)
|
||||
};
|
||||
|
||||
@@ -11873,22 +11923,22 @@ impl Editor {
|
||||
let mut prev_row = first_row;
|
||||
let (
|
||||
mut current_range_indent,
|
||||
mut current_range_comment_prefix,
|
||||
mut current_range_comment_delimiters,
|
||||
mut current_range_rewrap_prefix,
|
||||
) = indent_and_prefix_for_row(first_row);
|
||||
|
||||
for row in non_blank_rows_iter.skip(1) {
|
||||
let has_paragraph_break = row > prev_row + 1;
|
||||
|
||||
let (row_indent, row_comment_prefix, row_rewrap_prefix) =
|
||||
let (row_indent, row_comment_delimiters, row_rewrap_prefix) =
|
||||
indent_and_prefix_for_row(row);
|
||||
|
||||
let has_indent_change = row_indent != current_range_indent;
|
||||
let has_comment_change = row_comment_prefix != current_range_comment_prefix;
|
||||
let has_comment_change = row_comment_delimiters != current_range_comment_delimiters;
|
||||
|
||||
let has_boundary_change = has_comment_change
|
||||
|| row_rewrap_prefix.is_some()
|
||||
|| (has_indent_change && current_range_comment_prefix.is_some());
|
||||
|| (has_indent_change && current_range_comment_delimiters.is_some());
|
||||
|
||||
if has_paragraph_break || has_boundary_change {
|
||||
ranges.push((
|
||||
@@ -11896,13 +11946,13 @@ impl Editor {
|
||||
Point::new(current_range_start, 0)
|
||||
..Point::new(prev_row, buffer.line_len(MultiBufferRow(prev_row))),
|
||||
current_range_indent,
|
||||
current_range_comment_prefix.clone(),
|
||||
current_range_comment_delimiters.clone(),
|
||||
current_range_rewrap_prefix.clone(),
|
||||
from_empty_selection,
|
||||
));
|
||||
current_range_start = row;
|
||||
current_range_indent = row_indent;
|
||||
current_range_comment_prefix = row_comment_prefix;
|
||||
current_range_comment_delimiters = row_comment_delimiters;
|
||||
current_range_rewrap_prefix = row_rewrap_prefix;
|
||||
}
|
||||
prev_row = row;
|
||||
@@ -11913,7 +11963,7 @@ impl Editor {
|
||||
Point::new(current_range_start, 0)
|
||||
..Point::new(prev_row, buffer.line_len(MultiBufferRow(prev_row))),
|
||||
current_range_indent,
|
||||
current_range_comment_prefix,
|
||||
current_range_comment_delimiters,
|
||||
current_range_rewrap_prefix,
|
||||
from_empty_selection,
|
||||
));
|
||||
@@ -11927,7 +11977,7 @@ impl Editor {
|
||||
for (
|
||||
language_settings,
|
||||
wrap_range,
|
||||
indent_size,
|
||||
mut indent_size,
|
||||
comment_prefix,
|
||||
rewrap_prefix,
|
||||
from_empty_selection,
|
||||
@@ -11947,16 +11997,26 @@ impl Editor {
|
||||
|
||||
let tab_size = language_settings.tab_size;
|
||||
|
||||
let (line_prefix, inside_comment) = match &comment_prefix {
|
||||
Some(CommentFormat::Line(prefix) | CommentFormat::BlockLine(prefix)) => {
|
||||
(Some(prefix.as_str()), true)
|
||||
}
|
||||
Some(CommentFormat::BlockCommentWithEnd(BlockCommentConfig { prefix, .. })) => {
|
||||
(Some(prefix.as_ref()), true)
|
||||
}
|
||||
Some(CommentFormat::BlockCommentWithStart(BlockCommentConfig {
|
||||
start: _,
|
||||
end: _,
|
||||
prefix,
|
||||
tab_size,
|
||||
})) => {
|
||||
indent_size.len += tab_size;
|
||||
(Some(prefix.as_ref()), true)
|
||||
}
|
||||
None => (None, false),
|
||||
};
|
||||
let indent_prefix = indent_size.chars().collect::<String>();
|
||||
let mut line_prefix = indent_prefix.clone();
|
||||
let mut inside_comment = false;
|
||||
if let Some(prefix) = &comment_prefix {
|
||||
line_prefix.push_str(prefix);
|
||||
inside_comment = true;
|
||||
}
|
||||
if let Some(prefix) = &rewrap_prefix {
|
||||
line_prefix.push_str(prefix);
|
||||
}
|
||||
let line_prefix = format!("{indent_prefix}{}", line_prefix.unwrap_or(""));
|
||||
|
||||
let allow_rewrap_based_on_language = match language_settings.allow_rewrap {
|
||||
RewrapBehavior::InComments => inside_comment,
|
||||
@@ -12001,6 +12061,8 @@ impl Editor {
|
||||
let start_offset = start.to_offset(&buffer);
|
||||
let end = Point::new(end_row, buffer.line_len(MultiBufferRow(end_row)));
|
||||
let selection_text = buffer.text_for_range(start..end).collect::<String>();
|
||||
let mut first_line_delimiter = None;
|
||||
let mut last_line_delimiter = None;
|
||||
let Some(lines_without_prefixes) = selection_text
|
||||
.lines()
|
||||
.enumerate()
|
||||
@@ -12008,6 +12070,46 @@ impl Editor {
|
||||
let line_trimmed = line.trim_start();
|
||||
if rewrap_prefix.is_some() && ix > 0 {
|
||||
Ok(line_trimmed)
|
||||
} else if let Some(
|
||||
CommentFormat::BlockCommentWithStart(BlockCommentConfig {
|
||||
start,
|
||||
prefix,
|
||||
end,
|
||||
tab_size,
|
||||
})
|
||||
| CommentFormat::BlockCommentWithEnd(BlockCommentConfig {
|
||||
start,
|
||||
prefix,
|
||||
end,
|
||||
tab_size,
|
||||
}),
|
||||
) = &comment_prefix
|
||||
{
|
||||
let line_trimmed = line_trimmed
|
||||
.strip_prefix(start.as_ref())
|
||||
.map(|s| {
|
||||
let mut indent_size = indent_size;
|
||||
indent_size.len -= tab_size;
|
||||
let indent_prefix: String = indent_size.chars().collect();
|
||||
first_line_delimiter = Some((indent_prefix, start));
|
||||
s.trim_start()
|
||||
})
|
||||
.unwrap_or(line_trimmed);
|
||||
let line_trimmed = line_trimmed
|
||||
.strip_suffix(end.as_ref())
|
||||
.map(|s| {
|
||||
last_line_delimiter = Some(end);
|
||||
s.trim_end()
|
||||
})
|
||||
.unwrap_or(line_trimmed);
|
||||
let line_trimmed = line_trimmed
|
||||
.strip_prefix(prefix.as_ref())
|
||||
.unwrap_or(line_trimmed);
|
||||
Ok(line_trimmed)
|
||||
} else if let Some(CommentFormat::BlockLine(prefix)) = &comment_prefix {
|
||||
line_trimmed.strip_prefix(prefix).with_context(|| {
|
||||
format!("line did not start with prefix {prefix:?}: {line:?}")
|
||||
})
|
||||
} else {
|
||||
line_trimmed
|
||||
.strip_prefix(&line_prefix.trim_start())
|
||||
@@ -12034,14 +12136,25 @@ impl Editor {
|
||||
line_prefix.clone()
|
||||
};
|
||||
|
||||
let wrapped_text = wrap_with_prefix(
|
||||
line_prefix,
|
||||
subsequent_lines_prefix,
|
||||
lines_without_prefixes.join("\n"),
|
||||
wrap_column,
|
||||
tab_size,
|
||||
options.preserve_existing_whitespace,
|
||||
);
|
||||
let wrapped_text = {
|
||||
let mut wrapped_text = wrap_with_prefix(
|
||||
line_prefix,
|
||||
subsequent_lines_prefix,
|
||||
lines_without_prefixes.join("\n"),
|
||||
wrap_column,
|
||||
tab_size,
|
||||
options.preserve_existing_whitespace,
|
||||
);
|
||||
|
||||
if let Some((indent, delimiter)) = first_line_delimiter {
|
||||
wrapped_text = format!("{indent}{delimiter}\n{wrapped_text}");
|
||||
}
|
||||
if let Some(last_line) = last_line_delimiter {
|
||||
wrapped_text = format!("{wrapped_text}\n{indent_prefix}{last_line}");
|
||||
}
|
||||
|
||||
wrapped_text
|
||||
};
|
||||
|
||||
// TODO: should always use char-based diff while still supporting cursor behavior that
|
||||
// matches vim.
|
||||
|
||||
Reference in New Issue
Block a user