Add support for resizing panes using vim motions (#21038)

AidanV and Conrad Irwin created

Closes #8628

Release Notes:

- Added support for resizing the current pane using vim keybinds with
the intention to follow the functionality of vim
  - "ctrl-w +" to make a pane taller 
  - "ctrl-w -" to make the pane shorter
  - "ctrl-w >" to make a pane wider
  - "ctrl-w <" to make the pane narrower
- Changed vim pre_count and post_count to globals to allow for other
crates to use the vim count. In this case, it allows for resizing by
more than one unit. For example, "10 ctrl-w -" will decrease the height
of the pane 10 times more than "ctrl-w -"
- This pr does **not** add keybinds for making all panes in an axis
equal size and does **not** add support for resizing docks. This is
mentioned because these could be implied by the original issue

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>

Change summary

Cargo.lock                          |   1 
assets/keymaps/vim.json             |   4 
crates/vim/Cargo.toml               |   1 
crates/vim/src/change_list.rs       |   2 
crates/vim/src/command.rs           |   2 
crates/vim/src/indent.rs            |   4 
crates/vim/src/insert.rs            |   2 
crates/vim/src/mode_indicator.rs    |  14 ++
crates/vim/src/motion.rs            |   4 
crates/vim/src/normal.rs            |  18 ++--
crates/vim/src/normal/case.rs       |   2 
crates/vim/src/normal/increment.rs  |   4 
crates/vim/src/normal/paste.rs      |   2 
crates/vim/src/normal/repeat.rs     |   4 
crates/vim/src/normal/scroll.rs     |   2 
crates/vim/src/normal/search.rs     |   6 
crates/vim/src/normal/substitute.rs |   4 
crates/vim/src/replace.rs           |   2 
crates/vim/src/rewrap.rs            |   2 
crates/vim/src/state.rs             |   5 +
crates/vim/src/surrounds.rs         |   2 
crates/vim/src/vim.rs               |  77 +++++++++++-----
crates/vim/src/visual.rs            |  12 +-
crates/workspace/src/pane_group.rs  | 137 ++++++++++++++++++++++++++++++
crates/workspace/src/workspace.rs   |   6 +
25 files changed, 251 insertions(+), 68 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -13832,6 +13832,7 @@ dependencies = [
  "serde_derive",
  "serde_json",
  "settings",
+ "theme",
  "tokio",
  "ui",
  "util",

assets/keymaps/vim.json 🔗

@@ -557,6 +557,10 @@
       "ctrl-w shift-l": ["workspace::SwapPaneInDirection", "Right"],
       "ctrl-w shift-k": ["workspace::SwapPaneInDirection", "Up"],
       "ctrl-w shift-j": ["workspace::SwapPaneInDirection", "Down"],
+      "ctrl-w >": ["vim::ResizePane", "Widen"],
+      "ctrl-w <": ["vim::ResizePane", "Narrow"],
+      "ctrl-w -": ["vim::ResizePane", "Shorten"],
+      "ctrl-w +": ["vim::ResizePane", "Lengthen"],
       "ctrl-w g t": "pane::ActivateNextItem",
       "ctrl-w ctrl-g t": "pane::ActivateNextItem",
       "ctrl-w g shift-t": "pane::ActivatePrevItem",

crates/vim/Cargo.toml 🔗

@@ -36,6 +36,7 @@ serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true
 settings.workspace = true
+theme.workspace = true
 tokio = { version = "1.15", features = ["full"], optional = true }
 ui.workspace = true
 util.workspace = true

crates/vim/src/change_list.rs 🔗

@@ -16,7 +16,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
 
 impl Vim {
     fn move_to_change(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
-        let count = self.take_count(cx).unwrap_or(1);
+        let count = Vim::take_count(cx).unwrap_or(1);
         if self.change_list.is_empty() {
             return;
         }

crates/vim/src/command.rs 🔗

@@ -101,7 +101,7 @@ pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
         let Some(workspace) = vim.workspace(cx) else {
             return;
         };
-        let count = vim.take_count(cx).unwrap_or(1);
+        let count = Vim::take_count(cx).unwrap_or(1);
         workspace.update(cx, |workspace, cx| {
             command_palette::CommandPalette::toggle(
                 workspace,

crates/vim/src/indent.rs 🔗

@@ -16,7 +16,7 @@ actions!(vim, [Indent, Outdent,]);
 pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
     Vim::action(editor, cx, |vim, _: &Indent, cx| {
         vim.record_current_action(cx);
-        let count = vim.take_count(cx).unwrap_or(1);
+        let count = Vim::take_count(cx).unwrap_or(1);
         vim.store_visual_marks(cx);
         vim.update_editor(cx, |vim, editor, cx| {
             editor.transact(cx, |editor, cx| {
@@ -34,7 +34,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
 
     Vim::action(editor, cx, |vim, _: &Outdent, cx| {
         vim.record_current_action(cx);
-        let count = vim.take_count(cx).unwrap_or(1);
+        let count = Vim::take_count(cx).unwrap_or(1);
         vim.store_visual_marks(cx);
         vim.update_editor(cx, |vim, editor, cx| {
             editor.transact(cx, |editor, cx| {

crates/vim/src/insert.rs 🔗

@@ -17,7 +17,7 @@ impl Vim {
             self.sync_vim_settings(cx);
             return;
         }
-        let count = self.take_count(cx).unwrap_or(1);
+        let count = Vim::take_count(cx).unwrap_or(1);
         self.stop_recording_immediately(action.boxed_clone(), cx);
         if count <= 1 || Vim::globals(cx).dot_replaying {
             self.create_mark("^".into(), false, cx);

crates/vim/src/mode_indicator.rs 🔗

@@ -2,7 +2,7 @@ use gpui::{div, Element, Render, Subscription, View, ViewContext, WeakView};
 use itertools::Itertools;
 use workspace::{item::ItemHandle, ui::prelude::*, StatusItemView};
 
-use crate::{Vim, VimEvent};
+use crate::{Vim, VimEvent, VimGlobals};
 
 /// The ModeIndicator displays the current mode in the status bar.
 pub struct ModeIndicator {
@@ -68,14 +68,22 @@ impl ModeIndicator {
 
         let vim = vim.read(cx);
         recording
-            .chain(vim.pre_count.map(|count| format!("{}", count)))
+            .chain(
+                cx.global::<VimGlobals>()
+                    .pre_count
+                    .map(|count| format!("{}", count)),
+            )
             .chain(vim.selected_register.map(|reg| format!("\"{reg}")))
             .chain(
                 vim.operator_stack
                     .iter()
                     .map(|item| item.status().to_string()),
             )
-            .chain(vim.post_count.map(|count| format!("{}", count)))
+            .chain(
+                cx.global::<VimGlobals>()
+                    .post_count
+                    .map(|count| format!("{}", count)),
+            )
             .collect::<Vec<_>>()
             .join("")
     }

crates/vim/src/motion.rs 🔗

@@ -490,7 +490,7 @@ impl Vim {
             self.pop_operator(cx);
         }
 
-        let count = self.take_count(cx);
+        let count = Vim::take_count(cx);
         let active_operator = self.active_operator();
         let mut waiting_operator: Option<Operator> = None;
         match self.mode {
@@ -510,7 +510,7 @@ impl Vim {
         self.clear_operator(cx);
         if let Some(operator) = waiting_operator {
             self.push_operator(operator, cx);
-            self.pre_count = count
+            Vim::globals(cx).pre_count = count
         }
     }
 }

crates/vim/src/normal.rs 🔗

@@ -77,17 +77,17 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
 
     Vim::action(editor, cx, |vim, _: &DeleteLeft, cx| {
         vim.record_current_action(cx);
-        let times = vim.take_count(cx);
+        let times = Vim::take_count(cx);
         vim.delete_motion(Motion::Left, times, cx);
     });
     Vim::action(editor, cx, |vim, _: &DeleteRight, cx| {
         vim.record_current_action(cx);
-        let times = vim.take_count(cx);
+        let times = Vim::take_count(cx);
         vim.delete_motion(Motion::Right, times, cx);
     });
     Vim::action(editor, cx, |vim, _: &ChangeToEndOfLine, cx| {
         vim.start_recording(cx);
-        let times = vim.take_count(cx);
+        let times = Vim::take_count(cx);
         vim.change_motion(
             Motion::EndOfLine {
                 display_lines: false,
@@ -98,7 +98,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
     });
     Vim::action(editor, cx, |vim, _: &DeleteToEndOfLine, cx| {
         vim.record_current_action(cx);
-        let times = vim.take_count(cx);
+        let times = Vim::take_count(cx);
         vim.delete_motion(
             Motion::EndOfLine {
                 display_lines: false,
@@ -109,7 +109,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
     });
     Vim::action(editor, cx, |vim, _: &JoinLines, cx| {
         vim.record_current_action(cx);
-        let mut times = vim.take_count(cx).unwrap_or(1);
+        let mut times = Vim::take_count(cx).unwrap_or(1);
         if vim.mode.is_visual() {
             times = 1;
         } else if times > 1 {
@@ -130,7 +130,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
     });
 
     Vim::action(editor, cx, |vim, _: &Undo, cx| {
-        let times = vim.take_count(cx);
+        let times = Vim::take_count(cx);
         vim.update_editor(cx, |_, editor, cx| {
             for _ in 0..times.unwrap_or(1) {
                 editor.undo(&editor::actions::Undo, cx);
@@ -138,7 +138,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
         });
     });
     Vim::action(editor, cx, |vim, _: &Redo, cx| {
-        let times = vim.take_count(cx);
+        let times = Vim::take_count(cx);
         vim.update_editor(cx, |_, editor, cx| {
             for _ in 0..times.unwrap_or(1) {
                 editor.redo(&editor::actions::Redo, cx);
@@ -396,7 +396,7 @@ impl Vim {
     }
 
     fn yank_line(&mut self, _: &YankLine, cx: &mut ViewContext<Self>) {
-        let count = self.take_count(cx);
+        let count = Vim::take_count(cx);
         self.yank_motion(motion::Motion::CurrentLine, count, cx)
     }
 
@@ -416,7 +416,7 @@ impl Vim {
     }
 
     pub(crate) fn normal_replace(&mut self, text: Arc<str>, cx: &mut ViewContext<Self>) {
-        let count = self.take_count(cx).unwrap_or(1);
+        let count = Vim::take_count(cx).unwrap_or(1);
         self.stop_recording(cx);
         self.update_editor(cx, |_, editor, cx| {
             editor.transact(cx, |editor, cx| {

crates/vim/src/normal/case.rs 🔗

@@ -118,7 +118,7 @@ impl Vim {
     {
         self.record_current_action(cx);
         self.store_visual_marks(cx);
-        let count = self.take_count(cx).unwrap_or(1) as u32;
+        let count = Vim::take_count(cx).unwrap_or(1) as u32;
 
         self.update_editor(cx, |vim, editor, cx| {
             let mut ranges = Vec::new();

crates/vim/src/normal/increment.rs 🔗

@@ -26,13 +26,13 @@ impl_actions!(vim, [Increment, Decrement]);
 pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
     Vim::action(editor, cx, |vim, action: &Increment, cx| {
         vim.record_current_action(cx);
-        let count = vim.take_count(cx).unwrap_or(1);
+        let count = Vim::take_count(cx).unwrap_or(1);
         let step = if action.step { 1 } else { 0 };
         vim.increment(count as i64, step, cx)
     });
     Vim::action(editor, cx, |vim, action: &Decrement, cx| {
         vim.record_current_action(cx);
-        let count = vim.take_count(cx).unwrap_or(1);
+        let count = Vim::take_count(cx).unwrap_or(1);
         let step = if action.step { -1 } else { 0 };
         vim.increment(-(count as i64), step, cx)
     });

crates/vim/src/normal/paste.rs 🔗

@@ -25,7 +25,7 @@ impl Vim {
     pub fn paste(&mut self, action: &Paste, cx: &mut ViewContext<Self>) {
         self.record_current_action(cx);
         self.store_visual_marks(cx);
-        let count = self.take_count(cx).unwrap_or(1);
+        let count = Vim::take_count(cx).unwrap_or(1);
 
         self.update_editor(cx, |vim, editor, cx| {
             let text_layout_details = editor.text_layout_details(cx);

crates/vim/src/normal/repeat.rs 🔗

@@ -158,7 +158,7 @@ impl Vim {
     }
 
     pub(crate) fn replay_register(&mut self, mut register: char, cx: &mut ViewContext<Self>) {
-        let mut count = self.take_count(cx).unwrap_or(1);
+        let mut count = Vim::take_count(cx).unwrap_or(1);
         self.clear_operator(cx);
 
         let globals = Vim::globals(cx);
@@ -184,7 +184,7 @@ impl Vim {
     }
 
     pub(crate) fn repeat(&mut self, from_insert_mode: bool, cx: &mut ViewContext<Self>) {
-        let count = self.take_count(cx);
+        let count = Vim::take_count(cx);
         let Some((mut actions, selection, mode)) = Vim::update_globals(cx, |globals, _| {
             let actions = globals.recorded_actions.clone();
             if actions.is_empty() {

crates/vim/src/normal/scroll.rs 🔗

@@ -53,7 +53,7 @@ impl Vim {
         cx: &mut ViewContext<Self>,
         by: fn(c: Option<f32>) -> ScrollAmount,
     ) {
-        let amount = by(self.take_count(cx).map(|c| c as f32));
+        let amount = by(Vim::take_count(cx).map(|c| c as f32));
         self.update_editor(cx, |_, editor, cx| {
             scroll_editor(editor, move_cursor, &amount, cx)
         });

crates/vim/src/normal/search.rs 🔗

@@ -120,7 +120,7 @@ impl Vim {
         } else {
             Direction::Next
         };
-        let count = self.take_count(cx).unwrap_or(1);
+        let count = Vim::take_count(cx).unwrap_or(1);
         let prior_selections = self.editor_selections(cx);
         pane.update(cx, |pane, cx| {
             if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
@@ -226,7 +226,7 @@ impl Vim {
 
     pub fn move_to_match_internal(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
         let Some(pane) = self.pane(cx) else { return };
-        let count = self.take_count(cx).unwrap_or(1);
+        let count = Vim::take_count(cx).unwrap_or(1);
         let prior_selections = self.editor_selections(cx);
 
         let success = pane.update(cx, |pane, cx| {
@@ -264,7 +264,7 @@ impl Vim {
         cx: &mut ViewContext<Self>,
     ) {
         let Some(pane) = self.pane(cx) else { return };
-        let count = self.take_count(cx).unwrap_or(1);
+        let count = Vim::take_count(cx).unwrap_or(1);
         let prior_selections = self.editor_selections(cx);
         let vim = cx.view().clone();
 

crates/vim/src/normal/substitute.rs 🔗

@@ -9,7 +9,7 @@ actions!(vim, [Substitute, SubstituteLine]);
 pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
     Vim::action(editor, cx, |vim, _: &Substitute, cx| {
         vim.start_recording(cx);
-        let count = vim.take_count(cx);
+        let count = Vim::take_count(cx);
         vim.substitute(count, vim.mode == Mode::VisualLine, cx);
     });
 
@@ -18,7 +18,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
         if matches!(vim.mode, Mode::VisualBlock | Mode::Visual) {
             vim.switch_mode(Mode::VisualLine, false, cx)
         }
-        let count = vim.take_count(cx);
+        let count = Vim::take_count(cx);
         vim.substitute(count, true, cx)
     });
 }

crates/vim/src/replace.rs 🔗

@@ -22,7 +22,7 @@ pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
         if vim.mode != Mode::Replace {
             return;
         }
-        let count = vim.take_count(cx);
+        let count = Vim::take_count(cx);
         vim.undo_replace(count, cx)
     });
 }

crates/vim/src/rewrap.rs 🔗

@@ -10,7 +10,7 @@ actions!(vim, [Rewrap]);
 pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
     Vim::action(editor, cx, |vim, _: &Rewrap, cx| {
         vim.record_current_action(cx);
-        vim.take_count(cx);
+        Vim::take_count(cx);
         vim.store_visual_marks(cx);
         vim.update_editor(cx, |vim, editor, cx| {
             editor.transact(cx, |editor, cx| {

crates/vim/src/state.rs 🔗

@@ -150,6 +150,11 @@ pub struct VimGlobals {
     pub dot_recording: bool,
     pub dot_replaying: bool,
 
+    /// pre_count is the number before an operator is specified (3 in 3d2d)
+    pub pre_count: Option<usize>,
+    /// post_count is the number after an operator is specified (2 in 3d2d)
+    pub post_count: Option<usize>,
+
     pub stop_recording_after_next_action: bool,
     pub ignore_current_insertion: bool,
     pub recorded_count: Option<usize>,

crates/vim/src/surrounds.rs 🔗

@@ -35,7 +35,7 @@ impl Vim {
         cx: &mut ViewContext<Self>,
     ) {
         self.stop_recording(cx);
-        let count = self.take_count(cx);
+        let count = Vim::take_count(cx);
         let mode = self.mode;
         self.update_editor(cx, |_, editor, cx| {
             let text_layout_details = editor.text_layout_details(cx);

crates/vim/src/vim.rs 🔗

@@ -25,8 +25,8 @@ use editor::{
     Anchor, Bias, Editor, EditorEvent, EditorMode, ToPoint,
 };
 use gpui::{
-    actions, impl_actions, Action, AppContext, Entity, EventEmitter, KeyContext, KeystrokeEvent,
-    Render, Subscription, View, ViewContext, WeakView,
+    actions, impl_actions, Action, AppContext, Axis, Entity, EventEmitter, KeyContext,
+    KeystrokeEvent, Render, Subscription, View, ViewContext, WeakView,
 };
 use insert::{NormalBefore, TemporaryNormal};
 use language::{CursorShape, Point, Selection, SelectionGoal, TransactionId};
@@ -40,12 +40,17 @@ use settings::{update_settings_file, Settings, SettingsSources, SettingsStore};
 use state::{Mode, Operator, RecordedSelection, SearchState, VimGlobals};
 use std::{mem, ops::Range, sync::Arc};
 use surrounds::SurroundsType;
+use theme::ThemeSettings;
 use ui::{IntoElement, VisualContext};
 use vim_mode_setting::VimModeSetting;
-use workspace::{self, Pane, Workspace};
+use workspace::{self, Pane, ResizeIntent, Workspace};
 
 use crate::state::ReplayableAction;
 
+/// Used to resize the current pane
+#[derive(Clone, Deserialize, PartialEq)]
+pub struct ResizePane(pub ResizeIntent);
+
 /// An Action to Switch between modes
 #[derive(Clone, Deserialize, PartialEq)]
 pub struct SwitchMode(pub Mode);
@@ -81,7 +86,10 @@ actions!(
 // in the workspace namespace so it's not filtered out when vim is disabled.
 actions!(workspace, [ToggleVimMode]);
 
-impl_actions!(vim, [SwitchMode, PushOperator, Number, SelectRegister]);
+impl_actions!(
+    vim,
+    [ResizePane, SwitchMode, PushOperator, Number, SelectRegister]
+);
 
 /// Initializes the `vim` crate.
 pub fn init(cx: &mut AppContext) {
@@ -109,6 +117,30 @@ pub fn init(cx: &mut AppContext) {
             });
         });
 
+        workspace.register_action(|workspace, action: &ResizePane, cx| {
+            let count = Vim::take_count(cx.window_context()).unwrap_or(1) as f32;
+            let theme = ThemeSettings::get_global(cx);
+            let Ok(font_id) = cx.text_system().font_id(&theme.buffer_font) else {
+                return;
+            };
+            let Ok(width) = cx
+                .text_system()
+                .advance(font_id, theme.buffer_font_size(cx), 'm')
+            else {
+                return;
+            };
+            let height = theme.buffer_font_size(cx) * theme.buffer_line_height.value();
+
+            let (axis, amount) = match action.0 {
+                ResizeIntent::Lengthen => (Axis::Vertical, height),
+                ResizeIntent::Shorten => (Axis::Vertical, height * -1.),
+                ResizeIntent::Widen => (Axis::Horizontal, width.width),
+                ResizeIntent::Narrow => (Axis::Horizontal, width.width * -1.),
+            };
+
+            workspace.resize_pane(axis, amount * count, cx);
+        });
+
         workspace.register_action(|workspace, _: &SearchSubmit, cx| {
             let vim = workspace
                 .focused_pane(cx)
@@ -131,7 +163,7 @@ pub(crate) struct VimAddon {
 
 impl editor::Addon for VimAddon {
     fn extend_key_context(&self, key_context: &mut KeyContext, cx: &AppContext) {
-        self.view.read(cx).extend_key_context(key_context)
+        self.view.read(cx).extend_key_context(key_context, cx)
     }
 
     fn to_any(&self) -> &dyn std::any::Any {
@@ -146,11 +178,6 @@ pub(crate) struct Vim {
     pub temp_mode: bool,
     pub exit_temporary_mode: bool,
 
-    /// pre_count is the number before an operator is specified (3 in 3d2d)
-    pre_count: Option<usize>,
-    /// post_count is the number after an operator is specified (2 in 3d2d)
-    post_count: Option<usize>,
-
     operator_stack: Vec<Operator>,
     pub(crate) replacements: Vec<(Range<editor::Anchor>, String)>,
 
@@ -197,8 +224,6 @@ impl Vim {
             last_mode: Mode::Normal,
             temp_mode: false,
             exit_temporary_mode: false,
-            pre_count: None,
-            post_count: None,
             operator_stack: Vec::new(),
             replacements: Vec::new(),
 
@@ -471,7 +496,7 @@ impl Vim {
             self.current_anchor.take();
         }
         if mode != Mode::Insert && mode != Mode::Replace {
-            self.take_count(cx);
+            Vim::take_count(cx);
         }
 
         // Sync editor settings like clip mode
@@ -551,22 +576,24 @@ impl Vim {
         });
     }
 
-    fn take_count(&mut self, cx: &mut ViewContext<Self>) -> Option<usize> {
+    pub fn take_count(cx: &mut AppContext) -> Option<usize> {
         let global_state = cx.global_mut::<VimGlobals>();
         if global_state.dot_replaying {
             return global_state.recorded_count;
         }
 
-        let count = if self.post_count.is_none() && self.pre_count.is_none() {
+        let count = if global_state.post_count.is_none() && global_state.pre_count.is_none() {
             return None;
         } else {
-            Some(self.post_count.take().unwrap_or(1) * self.pre_count.take().unwrap_or(1))
+            Some(
+                global_state.post_count.take().unwrap_or(1)
+                    * global_state.pre_count.take().unwrap_or(1),
+            )
         };
 
         if global_state.dot_recording {
             global_state.recorded_count = count;
         }
-        self.sync_vim_settings(cx);
         count
     }
 
@@ -613,7 +640,7 @@ impl Vim {
         }
     }
 
-    pub fn extend_key_context(&self, context: &mut KeyContext) {
+    pub fn extend_key_context(&self, context: &mut KeyContext, cx: &AppContext) {
         let mut mode = match self.mode {
             Mode::Normal => "normal",
             Mode::Visual | Mode::VisualLine | Mode::VisualBlock => "visual",
@@ -625,8 +652,8 @@ impl Vim {
         let mut operator_id = "none";
 
         let active_operator = self.active_operator();
-        if active_operator.is_none() && self.pre_count.is_some()
-            || active_operator.is_some() && self.post_count.is_some()
+        if active_operator.is_none() && cx.global::<VimGlobals>().pre_count.is_some()
+            || active_operator.is_some() && cx.global::<VimGlobals>().post_count.is_some()
         {
             context.add("VimCount");
         }
@@ -837,18 +864,18 @@ impl Vim {
 
     fn push_count_digit(&mut self, number: usize, cx: &mut ViewContext<Self>) {
         if self.active_operator().is_some() {
-            let post_count = self.post_count.unwrap_or(0);
+            let post_count = Vim::globals(cx).post_count.unwrap_or(0);
 
-            self.post_count = Some(
+            Vim::globals(cx).post_count = Some(
                 post_count
                     .checked_mul(10)
                     .and_then(|post_count| post_count.checked_add(number))
                     .unwrap_or(post_count),
             )
         } else {
-            let pre_count = self.pre_count.unwrap_or(0);
+            let pre_count = Vim::globals(cx).pre_count.unwrap_or(0);
 
-            self.pre_count = Some(
+            Vim::globals(cx).pre_count = Some(
                 pre_count
                     .checked_mul(10)
                     .and_then(|pre_count| pre_count.checked_add(number))
@@ -880,7 +907,7 @@ impl Vim {
     }
 
     fn clear_operator(&mut self, cx: &mut ViewContext<Self>) {
-        self.take_count(cx);
+        Vim::take_count(cx);
         self.selected_register.take();
         self.operator_stack.clear();
         self.sync_vim_settings(cx);

crates/vim/src/visual.rs 🔗

@@ -538,9 +538,8 @@ impl Vim {
     }
 
     pub fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
-        let count = self
-            .take_count(cx)
-            .unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
+        let count =
+            Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
         self.update_editor(cx, |_, editor, cx| {
             editor.set_clip_at_line_ends(false, cx);
             for _ in 0..count {
@@ -556,9 +555,8 @@ impl Vim {
     }
 
     pub fn select_previous(&mut self, _: &SelectPrevious, cx: &mut ViewContext<Self>) {
-        let count = self
-            .take_count(cx)
-            .unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
+        let count =
+            Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
         self.update_editor(cx, |_, editor, cx| {
             for _ in 0..count {
                 if editor
@@ -573,7 +571,7 @@ impl Vim {
     }
 
     pub fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
-        let count = self.take_count(cx).unwrap_or(1);
+        let count = Vim::take_count(cx).unwrap_or(1);
         let Some(pane) = self.pane(cx) else {
             return;
         };

crates/workspace/src/pane_group.rs 🔗

@@ -8,8 +8,8 @@ use call::{ActiveCall, ParticipantLocation};
 use client::proto::PeerId;
 use collections::HashMap;
 use gpui::{
-    point, size, AnyView, AnyWeakView, Axis, Bounds, IntoElement, Model, MouseButton, Pixels,
-    Point, StyleRefinement, View, ViewContext,
+    point, size, Along, AnyView, AnyWeakView, Axis, Bounds, IntoElement, Model, MouseButton,
+    Pixels, Point, StyleRefinement, View, ViewContext,
 };
 use parking_lot::Mutex;
 use project::Project;
@@ -90,6 +90,21 @@ impl PaneGroup {
         }
     }
 
+    pub fn resize(
+        &mut self,
+        pane: &View<Pane>,
+        direction: Axis,
+        amount: Pixels,
+        bounds: &Bounds<Pixels>,
+    ) {
+        match &mut self.root {
+            Member::Pane(_) => {}
+            Member::Axis(axis) => {
+                let _ = axis.resize(pane, direction, amount, bounds);
+            }
+        };
+    }
+
     pub fn swap(&mut self, from: &View<Pane>, to: &View<Pane>) {
         match &mut self.root {
             Member::Pane(_) => {}
@@ -445,6 +460,116 @@ impl PaneAxis {
         }
     }
 
+    fn resize(
+        &mut self,
+        pane: &View<Pane>,
+        axis: Axis,
+        amount: Pixels,
+        bounds: &Bounds<Pixels>,
+    ) -> Option<bool> {
+        let container_size = self
+            .bounding_boxes
+            .lock()
+            .iter()
+            .filter_map(|e| *e)
+            .reduce(|acc, e| acc.union(&e))
+            .unwrap_or(*bounds)
+            .size;
+
+        let found_pane = self
+            .members
+            .iter()
+            .any(|member| matches!(member, Member::Pane(p) if p == pane));
+
+        if found_pane && self.axis != axis {
+            return Some(false); // pane found but this is not the correct axis direction
+        }
+        let mut found_axis_index: Option<usize> = None;
+        if !found_pane {
+            for (i, pa) in self.members.iter_mut().enumerate() {
+                if let Member::Axis(pa) = pa {
+                    if let Some(done) = pa.resize(pane, axis, amount, bounds) {
+                        if done {
+                            return Some(true); // pane found and operations already done
+                        } else if self.axis != axis {
+                            return Some(false); // pane found but this is not the correct axis direction
+                        } else {
+                            found_axis_index = Some(i); // pane found and this is correct direction
+                        }
+                    }
+                }
+            }
+            found_axis_index?; // no pane found
+        }
+
+        let min_size = match axis {
+            Axis::Horizontal => px(HORIZONTAL_MIN_SIZE),
+            Axis::Vertical => px(VERTICAL_MIN_SIZE),
+        };
+        let mut flexes = self.flexes.lock();
+
+        let ix = if found_pane {
+            self.members.iter().position(|m| {
+                if let Member::Pane(p) = m {
+                    p == pane
+                } else {
+                    false
+                }
+            })
+        } else {
+            found_axis_index
+        };
+
+        if ix.is_none() {
+            return Some(true);
+        }
+
+        let ix = ix.unwrap_or(0);
+
+        let size = move |ix, flexes: &[f32]| {
+            container_size.along(axis) * (flexes[ix] / flexes.len() as f32)
+        };
+
+        // Don't allow resizing to less than the minimum size, if elements are already too small
+        if min_size - px(1.) > size(ix, flexes.as_slice()) {
+            return Some(true);
+        }
+
+        let flex_changes = |pixel_dx, target_ix, next: isize, flexes: &[f32]| {
+            let flex_change = flexes.len() as f32 * pixel_dx / container_size.along(axis);
+            let current_target_flex = flexes[target_ix] + flex_change;
+            let next_target_flex = flexes[(target_ix as isize + next) as usize] - flex_change;
+            (current_target_flex, next_target_flex)
+        };
+
+        let apply_changes =
+            |current_ix: usize, proposed_current_pixel_change: Pixels, flexes: &mut [f32]| {
+                let next_target_size = Pixels::max(
+                    size(current_ix + 1, flexes) - proposed_current_pixel_change,
+                    min_size,
+                );
+                let current_target_size = Pixels::max(
+                    size(current_ix, flexes) + size(current_ix + 1, flexes) - next_target_size,
+                    min_size,
+                );
+
+                let current_pixel_change = current_target_size - size(current_ix, flexes);
+
+                let (current_target_flex, next_target_flex) =
+                    flex_changes(current_pixel_change, current_ix, 1, flexes);
+
+                flexes[current_ix] = current_target_flex;
+                flexes[current_ix + 1] = next_target_flex;
+            };
+
+        if ix + 1 == flexes.len() {
+            apply_changes(ix - 1, -1.0 * amount, flexes.as_mut_slice());
+        } else {
+            apply_changes(ix, amount, flexes.as_mut_slice());
+        }
+        Some(true)
+    }
+
     fn swap(&mut self, from: &View<Pane>, to: &View<Pane>) {
         for member in self.members.iter_mut() {
             match member {
@@ -625,6 +750,14 @@ impl SplitDirection {
     }
 }
 
+#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
+pub enum ResizeIntent {
+    Lengthen,
+    Shorten,
+    Widen,
+    Narrow,
+}
+
 mod element {
 
     use std::mem;

crates/workspace/src/workspace.rs 🔗

@@ -2988,6 +2988,12 @@ impl Workspace {
         }
     }
 
+    pub fn resize_pane(&mut self, axis: gpui::Axis, amount: Pixels, cx: &mut ViewContext<Self>) {
+        self.center
+            .resize(&self.active_pane.clone(), axis, amount, &self.bounds);
+        cx.notify();
+    }
+
     fn handle_pane_focused(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
         // This is explicitly hoisted out of the following check for pane identity as
         // terminal panel panes are not registered as a center panes.