editor: Support rewrap in block comments (#34418)

claytonrcarter and Smit Barmase created

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>

Change summary

crates/editor/src/editor.rs       | 215 ++++++++++++---
crates/editor/src/editor_tests.rs | 425 ++++++++++++++++++++++++++++++++
2 files changed, 581 insertions(+), 59 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -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.

crates/editor/src/editor_tests.rs 🔗

@@ -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, |_| {});