assets/keymaps/vim.json π
@@ -18,6 +18,7 @@
}
}
],
+ ":": "command_palette::Toggle",
"h": "vim::Left",
"left": "vim::Left",
"backspace": "vim::Backspace",
Conrad Irwin created
This mostly adds the commonly requested set (:wq and friends) and
a few that I use frequently
:<line> to go to a line number
:vsp / :sp to create a split
:cn / :cp to go to diagnostics
assets/keymaps/vim.json | 1
crates/command_palette/src/command_palette.rs | 47 ++++
crates/vim/src/command.rs | 219 +++++++++++++++++++++
crates/vim/src/vim.rs | 9
crates/workspace/src/pane.rs | 32 ++
crates/workspace/src/workspace.rs | 103 ++++++++-
crates/zed/src/menus.rs | 21 +
7 files changed, 406 insertions(+), 26 deletions(-)
@@ -18,6 +18,7 @@
}
}
],
+ ":": "command_palette::Toggle",
"h": "vim::Left",
"left": "vim::Left",
"backspace": "vim::Backspace",
@@ -18,6 +18,15 @@ actions!(command_palette, [Toggle]);
pub type CommandPalette = Picker<CommandPaletteDelegate>;
+pub type CommandPaletteInterceptor =
+ Box<dyn Fn(&str, &AppContext) -> Option<CommandInterceptResult>>;
+
+pub struct CommandInterceptResult {
+ pub action: Box<dyn Action>,
+ pub string: String,
+ pub positions: Vec<usize>,
+}
+
pub struct CommandPaletteDelegate {
actions: Vec<Command>,
matches: Vec<StringMatch>,
@@ -136,7 +145,7 @@ impl PickerDelegate for CommandPaletteDelegate {
char_bag: command.name.chars().collect(),
})
.collect::<Vec<_>>();
- let matches = if query.is_empty() {
+ let mut matches = if query.is_empty() {
candidates
.into_iter()
.enumerate()
@@ -158,6 +167,40 @@ impl PickerDelegate for CommandPaletteDelegate {
)
.await
};
+ let intercept_result = cx.read(|cx| {
+ if cx.has_global::<CommandPaletteInterceptor>() {
+ cx.global::<CommandPaletteInterceptor>()(&query, cx)
+ } else {
+ None
+ }
+ });
+ if let Some(CommandInterceptResult {
+ action,
+ string,
+ positions,
+ }) = intercept_result
+ {
+ if let Some(idx) = matches
+ .iter()
+ .position(|m| actions[m.candidate_id].action.id() == action.id())
+ {
+ matches.remove(idx);
+ }
+ actions.push(Command {
+ name: string.clone(),
+ action,
+ keystrokes: vec![],
+ });
+ matches.insert(
+ 0,
+ StringMatch {
+ candidate_id: actions.len() - 1,
+ string,
+ positions,
+ score: 0.0,
+ },
+ )
+ }
picker
.update(&mut cx, |picker, _| {
let delegate = picker.delegate_mut();
@@ -254,7 +297,7 @@ impl PickerDelegate for CommandPaletteDelegate {
}
}
-fn humanize_action_name(name: &str) -> String {
+pub fn humanize_action_name(name: &str) -> String {
let capacity = name.len() + name.chars().filter(|c| c.is_uppercase()).count();
let mut result = String::with_capacity(capacity);
for char in name.chars() {
@@ -0,0 +1,219 @@
+use command_palette::{humanize_action_name, CommandInterceptResult};
+use gpui::{actions, impl_actions, Action, AppContext, AsyncAppContext, ViewContext};
+use itertools::Itertools;
+use serde::{Deserialize, Serialize};
+use workspace::{SaveBehavior, Workspace};
+
+use crate::{
+ motion::{motion, Motion},
+ normal::JoinLines,
+ Vim,
+};
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct GoToLine {
+ pub line: u32,
+}
+
+impl_actions!(vim, [GoToLine]);
+
+pub fn init(cx: &mut AppContext) {
+ cx.add_action(|_: &mut Workspace, action: &GoToLine, cx| {
+ Vim::update(cx, |vim, cx| {
+ vim.push_operator(crate::state::Operator::Number(action.line as usize), cx)
+ });
+ motion(Motion::StartOfDocument, cx)
+ });
+}
+
+pub fn command_interceptor(mut query: &str, _: &AppContext) -> Option<CommandInterceptResult> {
+ while query.starts_with(":") {
+ query = &query[1..];
+ }
+
+ let (name, action) = match query {
+ // :w
+ "w" | "wr" | "wri" | "writ" | "write" => (
+ "write",
+ workspace::Save {
+ save_behavior: Some(SaveBehavior::PromptOnConflict),
+ }
+ .boxed_clone(),
+ ),
+ "w!" | "wr!" | "wri!" | "writ!" | "write!" => (
+ "write",
+ workspace::Save {
+ save_behavior: Some(SaveBehavior::SilentlyOverwrite),
+ }
+ .boxed_clone(),
+ ),
+
+ // :q
+ "q" | "qu" | "qui" | "quit" => (
+ "quit",
+ workspace::CloseActiveItem {
+ save_behavior: Some(SaveBehavior::PromptOnWrite),
+ }
+ .boxed_clone(),
+ ),
+ "q!" | "qu!" | "qui!" | "quit!" => (
+ "quit!",
+ workspace::CloseActiveItem {
+ save_behavior: Some(SaveBehavior::DontSave),
+ }
+ .boxed_clone(),
+ ),
+
+ // :wq
+ "wq" => (
+ "wq",
+ workspace::CloseActiveItem {
+ save_behavior: Some(SaveBehavior::PromptOnConflict),
+ }
+ .boxed_clone(),
+ ),
+ "wq!" => (
+ "wq!",
+ workspace::CloseActiveItem {
+ save_behavior: Some(SaveBehavior::SilentlyOverwrite),
+ }
+ .boxed_clone(),
+ ),
+ // :x
+ "x" | "xi" | "xit" | "exi" | "exit" => (
+ "exit",
+ workspace::CloseActiveItem {
+ save_behavior: Some(SaveBehavior::PromptOnConflict),
+ }
+ .boxed_clone(),
+ ),
+ "x!" | "xi!" | "xit!" | "exi!" | "exit!" => (
+ "xit",
+ workspace::CloseActiveItem {
+ save_behavior: Some(SaveBehavior::SilentlyOverwrite),
+ }
+ .boxed_clone(),
+ ),
+
+ // :wa
+ "wa" | "wal" | "wall" => (
+ "wall",
+ workspace::SaveAll {
+ save_behavior: Some(SaveBehavior::PromptOnConflict),
+ }
+ .boxed_clone(),
+ ),
+ "wa!" | "wal!" | "wall!" => (
+ "wall!",
+ workspace::SaveAll {
+ save_behavior: Some(SaveBehavior::SilentlyOverwrite),
+ }
+ .boxed_clone(),
+ ),
+
+ // :qa
+ "qa" | "qal" | "qall" | "quita" | "quital" | "quitall" => (
+ "quitall",
+ workspace::CloseAllItemsAndPanes {
+ save_behavior: Some(SaveBehavior::PromptOnWrite),
+ }
+ .boxed_clone(),
+ ),
+ "qa!" | "qal!" | "qall!" | "quita!" | "quital!" | "quitall!" => (
+ "quitall!",
+ workspace::CloseAllItemsAndPanes {
+ save_behavior: Some(SaveBehavior::DontSave),
+ }
+ .boxed_clone(),
+ ),
+
+ // :cq
+ "cq" | "cqu" | "cqui" | "cquit" | "cq!" | "cqu!" | "cqui!" | "cquit!" => (
+ "cquit!",
+ workspace::CloseAllItemsAndPanes {
+ save_behavior: Some(SaveBehavior::DontSave),
+ }
+ .boxed_clone(),
+ ),
+
+ // :xa
+ "xa" | "xal" | "xall" => (
+ "xall",
+ workspace::CloseAllItemsAndPanes {
+ save_behavior: Some(SaveBehavior::PromptOnConflict),
+ }
+ .boxed_clone(),
+ ),
+ "xa!" | "xal!" | "xall!" => (
+ "zall!",
+ workspace::CloseAllItemsAndPanes {
+ save_behavior: Some(SaveBehavior::SilentlyOverwrite),
+ }
+ .boxed_clone(),
+ ),
+
+ // :wqa
+ "wqa" | "wqal" | "wqall" => (
+ "wqall",
+ workspace::CloseAllItemsAndPanes {
+ save_behavior: Some(SaveBehavior::PromptOnConflict),
+ }
+ .boxed_clone(),
+ ),
+ "wqa!" | "wqal!" | "wqall!" => (
+ "wqall!",
+ workspace::CloseAllItemsAndPanes {
+ save_behavior: Some(SaveBehavior::SilentlyOverwrite),
+ }
+ .boxed_clone(),
+ ),
+
+ "j" | "jo" | "joi" | "join" => ("join", JoinLines.boxed_clone()),
+
+ "sp" | "spl" | "spli" | "split" => ("split", workspace::SplitUp.boxed_clone()),
+ "vs" | "vsp" | "vspl" | "vspli" | "vsplit" => {
+ ("vsplit", workspace::SplitLeft.boxed_clone())
+ }
+ "cn" | "cne" | "cnex" | "cnext" => ("cnext", editor::GoToDiagnostic.boxed_clone()),
+ "cp" | "cpr" | "cpre" | "cprev" => ("cprev", editor::GoToPrevDiagnostic.boxed_clone()),
+
+ _ => {
+ if let Ok(line) = query.parse::<u32>() {
+ (query, GoToLine { line }.boxed_clone())
+ } else {
+ return None;
+ }
+ }
+ };
+
+ let string = ":".to_owned() + name;
+ let positions = generate_positions(&string, query);
+
+ Some(CommandInterceptResult {
+ action,
+ string,
+ positions,
+ })
+}
+
+fn generate_positions(string: &str, query: &str) -> Vec<usize> {
+ let mut positions = Vec::new();
+ let mut chars = query.chars().into_iter();
+
+ let Some(mut current) = chars.next() else {
+ return positions;
+ };
+
+ for (i, c) in string.chars().enumerate() {
+ if c == current {
+ positions.push(i);
+ if let Some(c) = chars.next() {
+ current = c;
+ } else {
+ break;
+ }
+ }
+ }
+
+ positions
+}
@@ -1,6 +1,7 @@
#[cfg(test)]
mod test;
+mod command;
mod editor_events;
mod insert;
mod mode_indicator;
@@ -13,6 +14,7 @@ mod visual;
use anyhow::Result;
use collections::{CommandPaletteFilter, HashMap};
+use command_palette::CommandPaletteInterceptor;
use editor::{movement, Editor, EditorMode, Event};
use gpui::{
actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, Action,
@@ -63,6 +65,7 @@ pub fn init(cx: &mut AppContext) {
insert::init(cx);
object::init(cx);
motion::init(cx);
+ command::init(cx);
// Vim Actions
cx.add_action(|_: &mut Workspace, &SwitchMode(mode): &SwitchMode, cx| {
@@ -469,6 +472,12 @@ impl Vim {
}
});
+ if self.enabled {
+ cx.set_global::<CommandPaletteInterceptor>(Box::new(command::command_interceptor));
+ } else if cx.has_global::<CommandPaletteInterceptor>() {
+ let _ = cx.remove_global::<CommandPaletteInterceptor>();
+ }
+
cx.update_active_window(|cx| {
if self.enabled {
let active_editor = cx
@@ -78,10 +78,17 @@ pub struct CloseItemsToTheRightById {
}
#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
+#[serde(rename_all = "camelCase")]
pub struct CloseActiveItem {
pub save_behavior: Option<SaveBehavior>,
}
+#[derive(Clone, PartialEq, Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CloseAllItems {
+ pub save_behavior: Option<SaveBehavior>,
+}
+
actions!(
pane,
[
@@ -92,7 +99,6 @@ actions!(
CloseCleanItems,
CloseItemsToTheLeft,
CloseItemsToTheRight,
- CloseAllItems,
GoBack,
GoForward,
ReopenClosedItem,
@@ -103,7 +109,7 @@ actions!(
]
);
-impl_actions!(pane, [ActivateItem, CloseActiveItem]);
+impl_actions!(pane, [ActivateItem, CloseActiveItem, CloseAllItems]);
const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
@@ -829,14 +835,18 @@ impl Pane {
pub fn close_all_items(
&mut self,
- _: &CloseAllItems,
+ action: &CloseAllItems,
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
if self.items.is_empty() {
return None;
}
- Some(self.close_items(cx, SaveBehavior::PromptOnWrite, |_| true))
+ Some(self.close_items(
+ cx,
+ action.save_behavior.unwrap_or(SaveBehavior::PromptOnWrite),
+ |_| true,
+ ))
}
pub fn close_items(
@@ -1175,7 +1185,12 @@ impl Pane {
ContextMenuItem::action("Close Clean Items", CloseCleanItems),
ContextMenuItem::action("Close Items To The Left", CloseItemsToTheLeft),
ContextMenuItem::action("Close Items To The Right", CloseItemsToTheRight),
- ContextMenuItem::action("Close All Items", CloseAllItems),
+ ContextMenuItem::action(
+ "Close All Items",
+ CloseAllItems {
+ save_behavior: None,
+ },
+ ),
]
} else {
// In the case of the user right clicking on a non-active tab, for some item-closing commands, we need to provide the id of the tab, for the others, we can reuse the existing command.
@@ -1219,7 +1234,12 @@ impl Pane {
}
}
}),
- ContextMenuItem::action("Close All Items", CloseAllItems),
+ ContextMenuItem::action(
+ "Close All Items",
+ CloseAllItems {
+ save_behavior: None,
+ },
+ ),
]
},
cx,
@@ -122,13 +122,11 @@ actions!(
Open,
NewFile,
NewWindow,
- CloseWindow,
CloseInactiveTabsAndPanes,
AddFolderToProject,
Unfollow,
- Save,
SaveAs,
- SaveAll,
+ ReloadActiveItem,
ActivatePreviousPane,
ActivateNextPane,
FollowNextCollaborator,
@@ -158,6 +156,30 @@ pub struct ActivatePane(pub usize);
#[derive(Clone, Deserialize, PartialEq)]
pub struct ActivatePaneInDirection(pub SplitDirection);
+#[derive(Clone, PartialEq, Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SaveAll {
+ pub save_behavior: Option<SaveBehavior>,
+}
+
+#[derive(Clone, PartialEq, Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Save {
+ pub save_behavior: Option<SaveBehavior>,
+}
+
+#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
+#[serde(rename_all = "camelCase")]
+pub struct CloseWindow {
+ pub save_behavior: Option<SaveBehavior>,
+}
+
+#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
+#[serde(rename_all = "camelCase")]
+pub struct CloseAllItemsAndPanes {
+ pub save_behavior: Option<SaveBehavior>,
+}
+
#[derive(Deserialize)]
pub struct Toast {
id: usize,
@@ -210,7 +232,16 @@ pub struct OpenTerminal {
impl_actions!(
workspace,
- [ActivatePane, ActivatePaneInDirection, Toast, OpenTerminal]
+ [
+ ActivatePane,
+ ActivatePaneInDirection,
+ Toast,
+ OpenTerminal,
+ SaveAll,
+ Save,
+ CloseWindow,
+ CloseAllItemsAndPanes,
+ ]
);
pub type WorkspaceId = i64;
@@ -251,6 +282,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
cx.add_async_action(Workspace::follow_next_collaborator);
cx.add_async_action(Workspace::close);
cx.add_async_action(Workspace::close_inactive_items_and_panes);
+ cx.add_async_action(Workspace::close_all_items_and_panes);
cx.add_global_action(Workspace::close_global);
cx.add_global_action(restart);
cx.add_async_action(Workspace::save_all);
@@ -1262,11 +1294,15 @@ impl Workspace {
pub fn close(
&mut self,
- _: &CloseWindow,
+ action: &CloseWindow,
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
let window = cx.window();
- let prepare = self.prepare_to_close(false, cx);
+ let prepare = self.prepare_to_close(
+ false,
+ action.save_behavior.unwrap_or(SaveBehavior::PromptOnWrite),
+ cx,
+ );
Some(cx.spawn(|_, mut cx| async move {
if prepare.await? {
window.remove(&mut cx);
@@ -1323,8 +1359,17 @@ impl Workspace {
})
}
- fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
- let save_all = self.save_all_internal(SaveBehavior::PromptOnConflict, cx);
+ fn save_all(
+ &mut self,
+ action: &SaveAll,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<Task<Result<()>>> {
+ let save_all = self.save_all_internal(
+ action
+ .save_behavior
+ .unwrap_or(SaveBehavior::PromptOnConflict),
+ cx,
+ );
Some(cx.foreground().spawn(async move {
save_all.await?;
Ok(())
@@ -1691,24 +1736,52 @@ impl Workspace {
&mut self,
_: &CloseInactiveTabsAndPanes,
cx: &mut ViewContext<Self>,
+ ) -> Option<Task<Result<()>>> {
+ self.close_all_internal(true, SaveBehavior::PromptOnWrite, cx)
+ }
+
+ pub fn close_all_items_and_panes(
+ &mut self,
+ action: &CloseAllItemsAndPanes,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<Task<Result<()>>> {
+ self.close_all_internal(
+ false,
+ action.save_behavior.unwrap_or(SaveBehavior::PromptOnWrite),
+ cx,
+ )
+ }
+
+ fn close_all_internal(
+ &mut self,
+ retain_active_pane: bool,
+ save_behavior: SaveBehavior,
+ cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
let current_pane = self.active_pane();
let mut tasks = Vec::new();
- if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| {
- pane.close_inactive_items(&CloseInactiveItems, cx)
- }) {
- tasks.push(current_pane_close);
- };
+ if retain_active_pane {
+ if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| {
+ pane.close_inactive_items(&CloseInactiveItems, cx)
+ }) {
+ tasks.push(current_pane_close);
+ };
+ }
for pane in self.panes() {
- if pane.id() == current_pane.id() {
+ if retain_active_pane && pane.id() == current_pane.id() {
continue;
}
if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| {
- pane.close_all_items(&CloseAllItems, cx)
+ pane.close_all_items(
+ &CloseAllItems {
+ save_behavior: Some(save_behavior),
+ },
+ cx,
+ )
}) {
tasks.push(close_pane_items)
}
@@ -38,16 +38,31 @@ pub fn menus() -> Vec<Menu<'static>> {
MenuItem::action("Open Recent...", recent_projects::OpenRecent),
MenuItem::separator(),
MenuItem::action("Add Folder to Projectβ¦", workspace::AddFolderToProject),
- MenuItem::action("Save", workspace::Save),
+ MenuItem::action(
+ "Save",
+ workspace::Save {
+ save_behavior: None,
+ },
+ ),
MenuItem::action("Save Asβ¦", workspace::SaveAs),
- MenuItem::action("Save All", workspace::SaveAll),
+ MenuItem::action(
+ "Save All",
+ workspace::SaveAll {
+ save_behavior: None,
+ },
+ ),
MenuItem::action(
"Close Editor",
workspace::CloseActiveItem {
save_behavior: None,
},
),
- MenuItem::action("Close Window", workspace::CloseWindow),
+ MenuItem::action(
+ "Close Window",
+ workspace::CloseWindow {
+ save_behavior: None,
+ },
+ ),
],
},
Menu {