editor: Add language setting for comment continuations (#2605)

Piotr Osiewicz created

Per @JosephTLyons request I've added a language setting for comment
continuations.

Release Notes:

- Added a language setting for comment continuations.

Change summary

assets/settings/default.json             |   2 
crates/editor/src/editor.rs              | 132 +++++++++++++------------
crates/editor/src/editor_tests.rs        |  34 ++++-
crates/language/src/language_settings.rs |   8 +
4 files changed, 101 insertions(+), 75 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -108,6 +108,8 @@
   // Whether or not to remove any trailing whitespace from lines of a buffer
   // before saving it.
   "remove_trailing_whitespace_on_save": true,
+  // Whether to start a new line with a comment when a previous line is a comment as well.
+  "extend_comment_on_newline": true,
   // Whether or not to ensure there's a single newline at the end of a buffer
   // when saving it.
   "ensure_final_newline_on_save": true,

crates/editor/src/editor.rs 🔗

@@ -2165,8 +2165,8 @@ impl Editor {
         self.transact(cx, |this, cx| {
             let (edits, selection_fixup_info): (Vec<_>, Vec<_>) = {
                 let selections = this.selections.all::<usize>(cx);
-
-                let buffer = this.buffer.read(cx).snapshot(cx);
+                let multi_buffer = this.buffer.read(cx);
+                let buffer = multi_buffer.snapshot(cx);
                 selections
                     .iter()
                     .map(|selection| {
@@ -2177,70 +2177,74 @@ impl Editor {
                         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)| {
-                                            let mut index_of_first_non_whitespace = 0;
-                                            let line_starts_with_comment = snapshot
-                                                .chars_for_range(range)
-                                                .skip_while(|c| {
-                                                    let should_skip = c.is_whitespace();
-                                                    if should_skip {
-                                                        index_of_first_non_whitespace += 1;
-                                                    }
-                                                    should_skip
-                                                })
-                                                .take(delimiter.len())
-                                                .eq(delimiter.chars());
-                                            let cursor_is_placed_after_comment_marker =
-                                                index_of_first_non_whitespace + delimiter.len()
-                                                    <= start_point.column as usize;
-                                            line_starts_with_comment
-                                                && cursor_is_placed_after_comment_marker
-                                        })
-                                        .then(|| delimiter.clone())
-                                } else {
-                                    None
-                                };
-                                (comment_delimiter, insert_extra_newline)
+                        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(|_| {
+                                let is_comment_extension_enabled =
+                                    multi_buffer.settings_at(0, cx).extend_comment_on_newline;
+                                is_cursor && is_comment_extension_enabled
+                            });
+                            let comment_delimiter = if let Some(delimiter) = comment_delimiter {
+                                buffer
+                                    .buffer_line_for_row(start_point.row)
+                                    .is_some_and(|(snapshot, range)| {
+                                        let mut index_of_first_non_whitespace = 0;
+                                        let line_starts_with_comment = snapshot
+                                            .chars_for_range(range)
+                                            .skip_while(|c| {
+                                                let should_skip = c.is_whitespace();
+                                                if should_skip {
+                                                    index_of_first_non_whitespace += 1;
+                                                }
+                                                should_skip
+                                            })
+                                            .take(delimiter.len())
+                                            .eq(delimiter.chars());
+                                        let cursor_is_placed_after_comment_marker =
+                                            index_of_first_non_whitespace + delimiter.len()
+                                                <= start_point.column as usize;
+                                        line_starts_with_comment
+                                            && cursor_is_placed_after_comment_marker
+                                    })
+                                    .then(|| delimiter.clone())
                             } else {
-                                (None, false)
+                                None
                             };
+                            (comment_delimiter, insert_extra_newline)
+                        } else {
+                            (None, false)
+                        };
 
                         let capacity_for_delimiter = comment_delimiter
                             .as_deref()

crates/editor/src/editor_tests.rs 🔗

@@ -1732,27 +1732,41 @@ async fn test_newline_comments(cx: &mut gpui::TestAppContext) {
         },
         None,
     ));
-
-    let mut cx = EditorTestContext::new(cx).await;
-    cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
-    cx.set_state(indoc! {"
+    {
+        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! {"
+        cx.update_editor(|e, cx| e.newline(&Newline, cx));
+        cx.assert_editor_state(indoc! {"
         // Foo
         //ˇ
     "});
-    // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
-    cx.set_state(indoc! {"
+        // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
+        cx.set_state(indoc! {"
         ˇ// Foo
     "});
-    cx.update_editor(|e, cx| e.newline(&Newline, cx));
-    cx.assert_editor_state(indoc! {"
+        cx.update_editor(|e, cx| e.newline(&Newline, cx));
+        cx.assert_editor_state(indoc! {"
 
         ˇ// Foo
     "});
+    }
+    // Ensure that comment continuations can be disabled.
+    update_test_settings(cx, |settings| {
+        settings.defaults.extend_comment_on_newline = Some(false);
+    });
+    let mut cx = EditorTestContext::new(cx).await;
+    cx.set_state(indoc! {"
+        // Fooˇ
+    "});
+    cx.update_editor(|e, cx| e.newline(&Newline, cx));
+    cx.assert_editor_state(indoc! {"
+        // Foo
+        ˇ
+    "});
 }
 
 #[gpui::test]

crates/language/src/language_settings.rs 🔗

@@ -51,6 +51,7 @@ pub struct LanguageSettings {
     pub enable_language_server: bool,
     pub show_copilot_suggestions: bool,
     pub show_whitespaces: ShowWhitespaceSetting,
+    pub extend_comment_on_newline: bool,
 }
 
 #[derive(Clone, Debug, Default)]
@@ -95,6 +96,8 @@ pub struct LanguageSettingsContent {
     pub show_copilot_suggestions: Option<bool>,
     #[serde(default)]
     pub show_whitespaces: Option<ShowWhitespaceSetting>,
+    #[serde(default)]
+    pub extend_comment_on_newline: Option<bool>,
 }
 
 #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
@@ -340,7 +343,10 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
         src.show_copilot_suggestions,
     );
     merge(&mut settings.show_whitespaces, src.show_whitespaces);
-
+    merge(
+        &mut settings.extend_comment_on_newline,
+        src.extend_comment_on_newline,
+    );
     fn merge<T>(target: &mut T, value: Option<T>) {
         if let Some(value) = value {
             *target = value;