Add `allow_rewrap` setting to control `editor::Rewrap` behavior for a given language (#25173)

Marshall Bowers created

This PR adds a new `allow_rewrap` setting to control how
`editor::Rewrap` behaves for a given language.

This is a language setting, so it can either be configured globally or
within the context of an individual language.

For example:

```json
{
  "allow_rewrap": "in_selections",
  "languages": {
    "Typst": {
      "allow_rewrap": "anywhere"
    }
  }
}
```

There are three different values:

- `in_comment`: Only perform rewrapping within comments.
- `in_selections`: Only perform rewrapping within the current
selection(s).
- `anywhere`: Allow rewrapping anywhere.

The global default is `in_comment`, as it is the most conservative
option and allows rewrapping comments without risking breaking other
syntax.

The `Markdown` and `Plain Text` languages default to `anywhere`, which
mirrors the previous behavior for those language that was hard-coded
into the rewrap implementation.

This setting does not have any effect in Vim mode, as Vim mode already
allowed rewrapping anywhere.

Closes https://github.com/zed-industries/zed/issues/24242.

Release Notes:

- Added an `allow_rewrap` setting to control the `editor::Rewrap`
behavior for a given language.

Change summary

assets/settings/default.json             | 21 ++++++++++++++++++++
crates/editor/src/editor.rs              | 26 ++++++++++++------------
crates/editor/src/editor_tests.rs        | 19 +++++++++++++++++
crates/language/src/language_settings.rs | 27 +++++++++++++++++++++++++
4 files changed, 79 insertions(+), 14 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -204,6 +204,23 @@
   // Otherwise(when `true`), the closing characters are always skipped over and auto-removed
   // no matter how they were inserted.
   "always_treat_brackets_as_autoclosed": false,
+  // Controls where the `editor::Rewrap` action is allowed in the current language scope.
+  //
+  // This setting can take three values:
+  //
+  // 1. Only allow rewrapping in comments:
+  //    "in_comments"
+  // 2. Only allow rewrapping in the current selection(s):
+  //    "in_selections"
+  // 3. Allow rewrapping anywhere:
+  //    "anywhere"
+  //
+  // When using values other than `in_comments`, it is possible for the rewrapping to produce code
+  // that is syntactically invalid. Keep this in mind when selecting which behavior you would like
+  // to use.
+  //
+  // Note: This setting has no effect in Vim mode, as rewrap is already allowed everywhere.
+  "allow_rewrap": "in_comments",
   // Controls whether edit predictions are shown immediately (true)
   // or manually by triggering `editor::ShowEditPrediction` (false).
   "show_edit_predictions": true,
@@ -1103,6 +1120,7 @@
     "Markdown": {
       "format_on_save": "off",
       "use_on_type_format": false,
+      "allow_rewrap": "anywhere",
       "prettier": {
         "allowed": true
       }
@@ -1115,6 +1133,9 @@
         "parser": "php"
       }
     },
+    "Plain Text": {
+      "allow_rewrap": "anywhere"
+    },
     "Ruby": {
       "language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "..."]
     },

crates/editor/src/editor.rs 🔗

@@ -97,7 +97,9 @@ use inline_completion::{EditPredictionProvider, InlineCompletionProviderHandle};
 pub use items::MAX_TAB_TITLE_LEN;
 use itertools::Itertools;
 use language::{
-    language_settings::{self, all_language_settings, language_settings, InlayHintSettings},
+    language_settings::{
+        self, all_language_settings, language_settings, InlayHintSettings, RewrapBehavior,
+    },
     point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CharKind, CodeLabel,
     CursorShape, Diagnostic, DiskState, EditPredictionsMode, EditPreview, HighlightedText,
     IndentKind, IndentSize, Language, OffsetRangeExt, Point, Selection, SelectionGoal, TextObject,
@@ -7744,17 +7746,6 @@ impl Editor {
                 continue;
             }
 
-            let mut should_rewrap = is_vim_mode == IsVimMode::Yes;
-
-            if let Some(language_scope) = buffer.language_scope_at(selection.head()) {
-                match language_scope.language_name().as_ref() {
-                    "Markdown" | "Plain Text" => {
-                        should_rewrap = true;
-                    }
-                    _ => {}
-                }
-            }
-
             let tab_size = buffer.settings_at(selection.head(), cx).tab_size;
 
             // Since not all lines in the selection may be at the same indent
@@ -7785,6 +7776,7 @@ impl Editor {
 
             let mut line_prefix = indent_size.chars().collect::<String>();
 
+            let mut inside_comment = false;
             if let Some(comment_prefix) =
                 buffer
                     .language_scope_at(selection.head())
@@ -7797,9 +7789,17 @@ impl Editor {
                     })
             {
                 line_prefix.push_str(&comment_prefix);
-                should_rewrap = true;
+                inside_comment = true;
             }
 
+            let language_settings = buffer.settings_at(selection.head(), cx);
+            let allow_rewrap_based_on_language = match language_settings.allow_rewrap {
+                RewrapBehavior::InComments => inside_comment,
+                RewrapBehavior::InSelections => !selection.is_empty(),
+                RewrapBehavior::Anywhere => true,
+            };
+
+            let should_rewrap = is_vim_mode == IsVimMode::Yes || allow_rewrap_based_on_language;
             if !should_rewrap {
                 continue;
             }

crates/editor/src/editor_tests.rs 🔗

@@ -4321,7 +4321,24 @@ fn test_transpose(cx: &mut TestAppContext) {
 
 #[gpui::test]
 async fn test_rewrap(cx: &mut TestAppContext) {
-    init_test(cx, |_| {});
+    init_test(cx, |settings| {
+        settings.languages.extend([
+            (
+                "Markdown".into(),
+                LanguageSettingsContent {
+                    allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
+                    ..Default::default()
+                },
+            ),
+            (
+                "Plain Text".into(),
+                LanguageSettingsContent {
+                    allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
+                    ..Default::default()
+                },
+            ),
+        ])
+    });
 
     let mut cx = EditorTestContext::new(cx).await;
 

crates/language/src/language_settings.rs 🔗

@@ -109,6 +109,11 @@ pub struct LanguageSettings {
     /// - `"!<language_server_id>"` - A language server ID prefixed with a `!` will be disabled.
     /// - `"..."` - A placeholder to refer to the **rest** of the registered language servers for this language.
     pub language_servers: Vec<String>,
+    /// Controls where the `editor::Rewrap` action is allowed for this language.
+    ///
+    /// Note: This setting has no effect in Vim mode, as rewrap is already
+    /// allowed everywhere.
+    pub allow_rewrap: RewrapBehavior,
     /// Controls whether edit predictions are shown immediately (true)
     /// or manually by triggering `editor::ShowEditPrediction` (false).
     pub show_edit_predictions: bool,
@@ -349,6 +354,14 @@ pub struct LanguageSettingsContent {
     /// Default: ["..."]
     #[serde(default)]
     pub language_servers: Option<Vec<String>>,
+    /// Controls where the `editor::Rewrap` action is allowed for this language.
+    ///
+    /// Note: This setting has no effect in Vim mode, as rewrap is already
+    /// allowed everywhere.
+    ///
+    /// Default: "in_comments"
+    #[serde(default)]
+    pub allow_rewrap: Option<RewrapBehavior>,
     /// Controls whether edit predictions are shown immediately (true)
     /// or manually by triggering `editor::ShowEditPrediction` (false).
     ///
@@ -427,6 +440,19 @@ pub struct LanguageSettingsContent {
     pub show_completion_documentation: Option<bool>,
 }
 
+/// The behavior of `editor::Rewrap`.
+#[derive(Debug, PartialEq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum RewrapBehavior {
+    /// Only rewrap within comments.
+    #[default]
+    InComments,
+    /// Only rewrap within the current selection(s).
+    InSelections,
+    /// Allow rewrapping anywhere.
+    Anywhere,
+}
+
 /// The contents of the edit prediction settings.
 #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
 pub struct EditPredictionSettingsContent {
@@ -1224,6 +1250,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
         src.enable_language_server,
     );
     merge(&mut settings.language_servers, src.language_servers.clone());
+    merge(&mut settings.allow_rewrap, src.allow_rewrap);
     merge(
         &mut settings.show_edit_predictions,
         src.show_edit_predictions,