Z 1618/extend comments (#2585)

Piotr Osiewicz created

Fixes Z-1618. In the current state, this only works for line comments
such as `//` (and whatever's set in `{language}.toml` as a
line_comment).

Release Notes:

- Comments are now extended on new line.

Change summary

crates/editor/src/editor.rs       | 96 ++++++++++++++++++++++-----------
crates/editor/src/editor_tests.rs | 27 +++++++++
2 files changed, 91 insertions(+), 32 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -2179,40 +2179,72 @@ impl Editor {
                         indent.len = cmp::min(indent.len, start_point.column);
                         let start = selection.start;
                         let end = selection.end;
+                        let is_cursor = start == end;
+                        let language_scope = buffer.language_scope_at(start);
+                        let (comment_delimiter, insert_extra_newline) =
+                            if let Some(language) = &language_scope {
+                                let leading_whitespace_len = buffer
+                                    .reversed_chars_at(start)
+                                    .take_while(|c| c.is_whitespace() && *c != '\n')
+                                    .map(|c| c.len_utf8())
+                                    .sum::<usize>();
+
+                                let trailing_whitespace_len = buffer
+                                    .chars_at(end)
+                                    .take_while(|c| c.is_whitespace() && *c != '\n')
+                                    .map(|c| c.len_utf8())
+                                    .sum::<usize>();
+
+                                let insert_extra_newline =
+                                    language.brackets().any(|(pair, enabled)| {
+                                        let pair_start = pair.start.trim_end();
+                                        let pair_end = pair.end.trim_start();
+
+                                        enabled
+                                            && pair.newline
+                                            && buffer.contains_str_at(
+                                                end + trailing_whitespace_len,
+                                                pair_end,
+                                            )
+                                            && buffer.contains_str_at(
+                                                (start - leading_whitespace_len)
+                                                    .saturating_sub(pair_start.len()),
+                                                pair_start,
+                                            )
+                                    });
+                                // Comment extension on newline is allowed only for cursor selections
+                                let comment_delimiter =
+                                    language.line_comment_prefix().filter(|_| is_cursor);
+                                let comment_delimiter = if let Some(delimiter) = comment_delimiter {
+                                    buffer
+                                        .buffer_line_for_row(start_point.row)
+                                        .is_some_and(|(snapshot, range)| {
+                                            snapshot
+                                                .chars_for_range(range)
+                                                .skip_while(|c| c.is_whitespace())
+                                                .take(delimiter.len())
+                                                .eq(delimiter.chars())
+                                        })
+                                        .then(|| delimiter.clone())
+                                } else {
+                                    None
+                                };
+                                (comment_delimiter, insert_extra_newline)
+                            } else {
+                                (None, false)
+                            };
 
-                        let mut insert_extra_newline = false;
-                        if let Some(language) = buffer.language_scope_at(start) {
-                            let leading_whitespace_len = buffer
-                                .reversed_chars_at(start)
-                                .take_while(|c| c.is_whitespace() && *c != '\n')
-                                .map(|c| c.len_utf8())
-                                .sum::<usize>();
-
-                            let trailing_whitespace_len = buffer
-                                .chars_at(end)
-                                .take_while(|c| c.is_whitespace() && *c != '\n')
-                                .map(|c| c.len_utf8())
-                                .sum::<usize>();
-
-                            insert_extra_newline = language.brackets().any(|(pair, enabled)| {
-                                let pair_start = pair.start.trim_end();
-                                let pair_end = pair.end.trim_start();
-
-                                enabled
-                                    && pair.newline
-                                    && buffer
-                                        .contains_str_at(end + trailing_whitespace_len, pair_end)
-                                    && buffer.contains_str_at(
-                                        (start - leading_whitespace_len)
-                                            .saturating_sub(pair_start.len()),
-                                        pair_start,
-                                    )
-                            });
-                        }
-
-                        let mut new_text = String::with_capacity(1 + indent.len as usize);
-                        new_text.push('\n');
+                        let capacity_for_delimiter = comment_delimiter
+                            .as_deref()
+                            .map(str::len)
+                            .unwrap_or_default();
+                        let mut new_text =
+                            String::with_capacity(1 + capacity_for_delimiter + indent.len as usize);
+                        new_text.push_str("\n");
                         new_text.extend(indent.chars());
+                        if let Some(delimiter) = &comment_delimiter {
+                            new_text.push_str(&delimiter);
+                        }
                         if insert_extra_newline {
                             new_text = new_text.repeat(2);
                         }

crates/editor/src/editor_tests.rs 🔗

@@ -1719,6 +1719,33 @@ async fn test_newline_below(cx: &mut gpui::TestAppContext) {
     "});
 }
 
+#[gpui::test]
+async fn test_newline_comments(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |settings| {
+        settings.defaults.tab_size = NonZeroU32::new(4)
+    });
+
+    let language = Arc::new(Language::new(
+        LanguageConfig {
+            line_comment: Some("//".into()),
+            ..LanguageConfig::default()
+        },
+        None,
+    ));
+
+    let mut cx = EditorTestContext::new(cx).await;
+    cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+    cx.set_state(indoc! {"
+        // Fooˇ
+    "});
+
+    cx.update_editor(|e, cx| e.newline(&Newline, cx));
+    cx.assert_editor_state(indoc! {"
+        // Foo
+        //ˇ
+    "});
+}
+
 #[gpui::test]
 fn test_insert_with_old_selections(cx: &mut TestAppContext) {
     init_test(cx, |_| {});