@@ -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.
@@ -5561,14 +5561,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 {
@@ -5884,6 +5888,411 @@ 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
+ // TODO: 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,
+ );
+
+ // TODO: 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,
+ &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, |_| {});