vim: Sort whole buffer when no range is specified (#42376)

Dino created

- Introduce a `default_range` field to `VimCommand`, to be optionally
  used when no range is specified for the command
- Update `VimCommand.parse` to take into consideration the
  `default_range`
- Introduce `CommandRange::buffer` to obtain the `CommandRange` which
  corresponds to the whole buffer
- Update the `VimCommand` definitions for both `sort` and `sort i` to
  default to the whole buffer when no range is specified

Closes #41750 

Release Notes:

- Improved vim's `:sort` command to sort the buffer's content when no
selection is used

Change summary

crates/vim/src/command.rs | 133 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 131 insertions(+), 2 deletions(-)

Detailed changes

crates/vim/src/command.rs 🔗

@@ -725,6 +725,8 @@ struct VimCommand {
     args: Option<
         Box<dyn Fn(Box<dyn Action>, String) -> Option<Box<dyn Action>> + Send + Sync + 'static>,
     >,
+    /// Optional range Range to use if no range is specified.
+    default_range: Option<CommandRange>,
     range: Option<
         Box<
             dyn Fn(Box<dyn Action>, &CommandRange) -> Option<Box<dyn Action>>
@@ -793,6 +795,11 @@ impl VimCommand {
         self
     }
 
+    fn default_range(mut self, range: CommandRange) -> Self {
+        self.default_range = Some(range);
+        self
+    }
+
     fn count(mut self) -> Self {
         self.has_count = true;
         self
@@ -923,6 +930,7 @@ impl VimCommand {
             self.args.as_ref()?(action, args)?
         };
 
+        let range = range.as_ref().or(self.default_range.as_ref());
         if let Some(range) = range {
             self.range.as_ref().and_then(|f| f(action, range))
         } else {
@@ -1121,6 +1129,7 @@ impl CommandRange {
         self.end.as_ref().unwrap_or(&self.start)
     }
 
+    /// Convert the `CommandRange` into a `Range<MultiBufferRow>`.
     pub(crate) fn buffer_range(
         &self,
         vim: &Vim,
@@ -1152,6 +1161,14 @@ impl CommandRange {
             None
         }
     }
+
+    /// The `CommandRange` representing the entire buffer.
+    fn buffer() -> Self {
+        Self {
+            start: Position::Line { row: 1, offset: 0 },
+            end: Some(Position::LastLine { offset: 0 }),
+        }
+    }
 }
 
 fn generate_commands(_: &App) -> Vec<VimCommand> {
@@ -1421,8 +1438,12 @@ fn generate_commands(_: &App) -> Vec<VimCommand> {
         VimCommand::new(("delm", "arks"), ArgumentRequired)
             .bang(DeleteMarks::AllLocal)
             .args(|_, args| Some(DeleteMarks::Marks(args).boxed_clone())),
-        VimCommand::new(("sor", "t"), SortLinesCaseSensitive).range(select_range),
-        VimCommand::new(("sort i", ""), SortLinesCaseInsensitive).range(select_range),
+        VimCommand::new(("sor", "t"), SortLinesCaseSensitive)
+            .range(select_range)
+            .default_range(CommandRange::buffer()),
+        VimCommand::new(("sort i", ""), SortLinesCaseInsensitive)
+            .range(select_range)
+            .default_range(CommandRange::buffer()),
         VimCommand::str(("E", "xplore"), "project_panel::ToggleFocus"),
         VimCommand::str(("H", "explore"), "project_panel::ToggleFocus"),
         VimCommand::str(("L", "explore"), "project_panel::ToggleFocus"),
@@ -2898,4 +2919,112 @@ mod test {
             );
         });
     }
+
+    #[gpui::test]
+    async fn test_sort_commands(cx: &mut TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+
+        cx.set_state(
+            indoc! {"
+                «hornet
+                quirrel
+                elderbug
+                cornifer
+                idaˇ»
+            "},
+            Mode::Visual,
+        );
+
+        cx.simulate_keystrokes(": sort");
+        cx.simulate_keystrokes("enter");
+
+        cx.assert_state(
+            indoc! {"
+                ˇcornifer
+                elderbug
+                hornet
+                ida
+                quirrel
+            "},
+            Mode::Normal,
+        );
+
+        // Assert that, by default, `:sort` takes case into consideration.
+        cx.set_state(
+            indoc! {"
+                «hornet
+                quirrel
+                Elderbug
+                cornifer
+                idaˇ»
+            "},
+            Mode::Visual,
+        );
+
+        cx.simulate_keystrokes(": sort");
+        cx.simulate_keystrokes("enter");
+
+        cx.assert_state(
+            indoc! {"
+                ˇElderbug
+                cornifer
+                hornet
+                ida
+                quirrel
+            "},
+            Mode::Normal,
+        );
+
+        // Assert that, if the `i` option is passed, `:sort` ignores case.
+        cx.set_state(
+            indoc! {"
+                «hornet
+                quirrel
+                Elderbug
+                cornifer
+                idaˇ»
+            "},
+            Mode::Visual,
+        );
+
+        cx.simulate_keystrokes(": sort space i");
+        cx.simulate_keystrokes("enter");
+
+        cx.assert_state(
+            indoc! {"
+                ˇcornifer
+                Elderbug
+                hornet
+                ida
+                quirrel
+            "},
+            Mode::Normal,
+        );
+
+        // When no range is provided, sorts the whole buffer.
+        cx.set_state(
+            indoc! {"
+                ˇhornet
+                quirrel
+                elderbug
+                cornifer
+                ida
+            "},
+            Mode::Normal,
+        );
+
+        cx.simulate_keystrokes(": sort");
+        cx.simulate_keystrokes("enter");
+
+        cx.assert_state(
+            indoc! {"
+                ˇcornifer
+                elderbug
+                hornet
+                ida
+                quirrel
+            "},
+            Mode::Normal,
+        );
+    }
 }