This commit is contained in:
claytonrcarter 2025-08-27 01:40:58 +08:00 committed by GitHub
commit 90467f0150
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 583 additions and 60 deletions

View file

@ -11737,6 +11737,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)
@ -11753,37 +11765,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)
};
@ -11794,22 +11844,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((
@ -11817,13 +11867,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;
@ -11834,7 +11884,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,
));
@ -11848,7 +11898,7 @@ impl Editor {
for (
language_settings,
wrap_range,
indent_size,
mut indent_size,
comment_prefix,
rewrap_prefix,
from_empty_selection,
@ -11868,16 +11918,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,
@ -11922,13 +11982,55 @@ 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()
.map(|(ix, line)| {
let line_trimmed = line.trim_start();
let line_trimmed = line.trim();
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())
@ -11955,14 +12057,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.

View file

@ -5438,14 +5438,18 @@ async fn test_rewrap(cx: &mut TestAppContext) {
},
None,
));
let rust_language = Arc::new(Language::new(
LanguageConfig {
name: "Rust".into(),
line_comments: vec!["// ".into(), "/// ".into()],
..LanguageConfig::default()
},
Some(tree_sitter_rust::LANGUAGE.into()),
));
let rust_language = Arc::new(
Language::new(
LanguageConfig {
name: "Rust".into(),
line_comments: vec!["// ".into(), "/// ".into()],
..LanguageConfig::default()
},
Some(tree_sitter_rust::LANGUAGE.into()),
)
.with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
.unwrap(),
);
let plaintext_language = Arc::new(Language::new(
LanguageConfig {
@ -5761,6 +5765,412 @@ async fn test_rewrap(cx: &mut TestAppContext) {
}
}
#[gpui::test]
async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
init_test(cx, |settings| {
settings.languages.0.extend([(
"Rust".into(),
LanguageSettingsContent {
allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
preferred_line_length: Some(40),
..Default::default()
},
)])
});
let mut cx = EditorTestContext::new(cx).await;
let rust_lang = Arc::new(
Language::new(
LanguageConfig {
name: "Rust".into(),
line_comments: vec!["// ".into()],
block_comment: Some(BlockCommentConfig {
start: "/*".into(),
end: "*/".into(),
prefix: "* ".into(),
tab_size: 1,
}),
documentation_comment: Some(BlockCommentConfig {
start: "/**".into(),
end: "*/".into(),
prefix: "* ".into(),
tab_size: 1,
}),
..LanguageConfig::default()
},
Some(tree_sitter_rust::LANGUAGE.into()),
)
.with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
.unwrap(),
);
// regular block comment
assert_rewrap(
indoc! {"
/*
*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
*/
/*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
"},
indoc! {"
/*
*ˇ Lorem ipsum dolor sit amet,
* consectetur adipiscing elit.
*/
/*
*ˇ Lorem ipsum dolor sit amet,
* consectetur adipiscing elit.
*/
"},
rust_lang.clone(),
&mut cx,
);
// indent is respected
assert_rewrap(
indoc! {"
{}
/*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
"},
indoc! {"
{}
/*
*ˇ Lorem ipsum dolor sit amet,
* consectetur adipiscing elit.
*/
"},
rust_lang.clone(),
&mut cx,
);
// short block comments with inline delimiters
assert_rewrap(
indoc! {"
/*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
/*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
*/
/*
*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
"},
indoc! {"
/*
*ˇ Lorem ipsum dolor sit amet,
* consectetur adipiscing elit.
*/
/*
*ˇ Lorem ipsum dolor sit amet,
* consectetur adipiscing elit.
*/
/*
*ˇ Lorem ipsum dolor sit amet,
* consectetur adipiscing elit.
*/
"},
rust_lang.clone(),
&mut cx,
);
// multiline block comment with inline start/end delimiters
assert_rewrap(
indoc! {"
/*ˇ Lorem ipsum dolor sit amet,
* consectetur adipiscing elit. */
"},
indoc! {"
/*
*ˇ Lorem ipsum dolor sit amet,
* consectetur adipiscing elit.
*/
"},
rust_lang.clone(),
&mut cx,
);
// block comment rewrap still respects paragraph bounds
assert_rewrap(
indoc! {"
/*
*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
*
* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
*/
"},
indoc! {"
/*
*ˇ Lorem ipsum dolor sit amet,
* consectetur adipiscing elit.
*
* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
*/
"},
rust_lang.clone(),
&mut cx,
);
// documentation comments
assert_rewrap(
indoc! {"
/**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
/**
*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
*/
"},
indoc! {"
/**
*ˇ Lorem ipsum dolor sit amet,
* consectetur adipiscing elit.
*/
/**
*ˇ Lorem ipsum dolor sit amet,
* consectetur adipiscing elit.
*/
"},
rust_lang.clone(),
&mut cx,
);
// different, adjacent comments
assert_rewrap(
indoc! {"
/**
*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
*/
/*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
//ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
"},
indoc! {"
/**
*ˇ Lorem ipsum dolor sit amet,
* consectetur adipiscing elit.
*/
/*
*ˇ Lorem ipsum dolor sit amet,
* consectetur adipiscing elit.
*/
//ˇ Lorem ipsum dolor sit amet,
// consectetur adipiscing elit.
"},
rust_lang.clone(),
&mut cx,
);
// selection w/ single short block comment
assert_rewrap(
indoc! {"
«/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
"},
indoc! {"
«/*
* Lorem ipsum dolor sit amet,
* consectetur adipiscing elit.
*/ˇ»
"},
rust_lang.clone(),
&mut cx,
);
// rewrapping a single comment w/ abutting comments
assert_rewrap(
indoc! {"
/* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
"},
indoc! {"
/*
* ˇLorem ipsum dolor sit amet,
* consectetur adipiscing elit.
*/
/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
"},
rust_lang.clone(),
&mut cx,
);
// selection w/ non-abutting short block comments
assert_rewrap(
indoc! {"
«/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
"},
indoc! {"
«/*
* Lorem ipsum dolor sit amet,
* consectetur adipiscing elit.
*/
/*
* Lorem ipsum dolor sit amet,
* consectetur adipiscing elit.
*/ˇ»
"},
rust_lang.clone(),
&mut cx,
);
// selection of multiline block comments
assert_rewrap(
indoc! {"
«/* Lorem ipsum dolor sit amet,
* consectetur adipiscing elit. */ˇ»
"},
indoc! {"
«/*
* Lorem ipsum dolor sit amet,
* consectetur adipiscing elit.
*/ˇ»
"},
rust_lang.clone(),
&mut cx,
);
// partial selection of multiline block comments
assert_rewrap(
indoc! {"
«/* Lorem ipsum dolor sit amet,ˇ»
* consectetur adipiscing elit. */
/* Lorem ipsum dolor sit amet,
«* consectetur adipiscing elit. */ˇ»
"},
indoc! {"
«/*
* Lorem ipsum dolor sit amet,ˇ»
* consectetur adipiscing elit. */
/* Lorem ipsum dolor sit amet,
«* consectetur adipiscing elit.
*/ˇ»
"},
rust_lang.clone(),
&mut cx,
);
// selection w/ abutting short block comments
// FIXME unhandled edge case; not correct, just documenting known issues
// should not be combined; should rewrap as 2 comments
assert_rewrap(
indoc! {"
«/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
"},
// desired behavior:
// indoc! {"
// «/*
// * Lorem ipsum dolor sit amet,
// * consectetur adipiscing elit.
// */
// /*
// * Lorem ipsum dolor sit amet,
// * consectetur adipiscing elit.
// */ˇ»
// "},
// actual behaviour:
indoc! {"
«/*
* Lorem ipsum dolor sit amet,
* consectetur adipiscing elit. Lorem
* ipsum dolor sit amet, consectetur
* adipiscing elit.
*/ˇ»
"},
rust_lang.clone(),
&mut cx,
);
// FIXME same as above, but with delimiters on separate line
// assert_rewrap(
// indoc! {"
// «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
// */
// /*
// * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
// "},
// // desired:
// // indoc! {"
// // «/*
// // * Lorem ipsum dolor sit amet,
// // * consectetur adipiscing elit.
// // */
// // /*
// // * Lorem ipsum dolor sit amet,
// // * consectetur adipiscing elit.
// // */ˇ»
// // "},
// // actual: (but with trailing w/s on the empty lines)
// indoc! {"
// «/*
// * Lorem ipsum dolor sit amet,
// * consectetur adipiscing elit.
// *
// */
// /*
// *
// * Lorem ipsum dolor sit amet,
// * consectetur adipiscing elit.
// */ˇ»
// "},
// rust_lang.clone(),
// &mut cx,
// );
// TODO these are unhandled edge cases; not correct, just documenting known issues
assert_rewrap(
indoc! {"
/*
//ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
*/
/*
//ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
/*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
"},
// desired:
// indoc! {"
// /*
// *ˇ Lorem ipsum dolor sit amet,
// * consectetur adipiscing elit.
// */
// /*
// *ˇ Lorem ipsum dolor sit amet,
// * consectetur adipiscing elit.
// */
// /*
// *ˇ Lorem ipsum dolor sit amet
// */ /* consectetur adipiscing elit. */
// "},
// actual:
indoc! {"
/*
//ˇ Lorem ipsum dolor sit amet,
// consectetur adipiscing elit.
*/
/*
* //ˇ Lorem ipsum dolor sit amet,
* consectetur adipiscing elit.
*/
/*
*ˇ Lorem ipsum dolor sit amet */ /*
* consectetur adipiscing elit.
*/
"},
rust_lang.clone(),
&mut cx,
);
#[track_caller]
fn assert_rewrap(
unwrapped_text: &str,
wrapped_text: &str,
language: Arc<Language>,
cx: &mut EditorTestContext,
) {
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
cx.set_state(unwrapped_text);
cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
cx.assert_editor_state(wrapped_text);
}
}
#[gpui::test]
async fn test_hard_wrap(cx: &mut TestAppContext) {
init_test(cx, |_| {});