Add vim-specific interactions to command

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
This commit is contained in:
Conrad Irwin 2023-09-07 22:48:01 -06:00
parent d42093e069
commit ea3a1745f5
7 changed files with 406 additions and 26 deletions

View File

@ -18,6 +18,7 @@
}
}
],
":": "command_palette::Toggle",
"h": "vim::Left",
"left": "vim::Left",
"backspace": "vim::Backspace",

View File

@ -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() {

219
crates/vim/src/command.rs Normal file
View File

@ -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
}

View File

@ -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

View File

@ -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,

View File

@ -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)
}

View File

@ -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 {