vim: ! support (#23169)
Closes #22885 Closes #12565 This doesn't yet add history in the command palette, which is painfully missing. Release Notes: - vim: Added `:!`, `:<range>!` and `:r!` support - vim: Added `!` operator in normal/visual mode
This commit is contained in:
parent
21e7765a48
commit
f94efb5008
8 changed files with 452 additions and 12 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -14305,6 +14305,7 @@ dependencies = [
|
|||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
"libc",
|
||||
"log",
|
||||
"lsp",
|
||||
"multi_buffer",
|
||||
|
@ -14318,6 +14319,7 @@ dependencies = [
|
|||
"serde_derive",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"task",
|
||||
"theme",
|
||||
"tokio",
|
||||
"ui",
|
||||
|
|
|
@ -221,6 +221,7 @@
|
|||
">": ["vim::PushOperator", "Indent"],
|
||||
"<": ["vim::PushOperator", "Outdent"],
|
||||
"=": ["vim::PushOperator", "AutoIndent"],
|
||||
"!": ["vim::PushOperator", "ShellCommand"],
|
||||
"g u": ["vim::PushOperator", "Lowercase"],
|
||||
"g shift-u": ["vim::PushOperator", "Uppercase"],
|
||||
"g ~": ["vim::PushOperator", "OppositeCase"],
|
||||
|
@ -287,6 +288,7 @@
|
|||
">": "vim::Indent",
|
||||
"<": "vim::Outdent",
|
||||
"=": "vim::AutoIndent",
|
||||
"!": "vim::ShellCommand",
|
||||
"i": ["vim::PushOperator", { "Object": { "around": false } }],
|
||||
"a": ["vim::PushOperator", { "Object": { "around": true } }],
|
||||
"g c": "vim::ToggleComments",
|
||||
|
@ -498,6 +500,12 @@
|
|||
"=": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == sh",
|
||||
"bindings": {
|
||||
"!": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == gc",
|
||||
"bindings": {
|
||||
|
|
|
@ -13,7 +13,7 @@ use std::{
|
|||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use task::{Shell, SpawnInTerminal};
|
||||
use task::{Shell, ShellBuilder, SpawnInTerminal};
|
||||
use terminal::{
|
||||
terminal_settings::{self, TerminalSettings, VenvSettings},
|
||||
TaskState, TaskStatus, Terminal, TerminalBuilder,
|
||||
|
@ -64,7 +64,7 @@ impl Project {
|
|||
}
|
||||
}
|
||||
|
||||
fn ssh_details(&self, cx: &AppContext) -> Option<(String, SshCommand)> {
|
||||
pub fn ssh_details(&self, cx: &AppContext) -> Option<(String, SshCommand)> {
|
||||
if let Some(ssh_client) = &self.ssh_client {
|
||||
let ssh_client = ssh_client.read(cx);
|
||||
if let Some(args) = ssh_client.ssh_args() {
|
||||
|
@ -122,6 +122,63 @@ impl Project {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn terminal_settings<'a>(
|
||||
&'a self,
|
||||
path: &'a Option<PathBuf>,
|
||||
cx: &'a AppContext,
|
||||
) -> &'a TerminalSettings {
|
||||
let mut settings_location = None;
|
||||
if let Some(path) = path.as_ref() {
|
||||
if let Some((worktree, _)) = self.find_worktree(path, cx) {
|
||||
settings_location = Some(SettingsLocation {
|
||||
worktree_id: worktree.read(cx).id(),
|
||||
path,
|
||||
});
|
||||
}
|
||||
}
|
||||
TerminalSettings::get(settings_location, cx)
|
||||
}
|
||||
|
||||
pub fn exec_in_shell(&self, command: String, cx: &AppContext) -> std::process::Command {
|
||||
let path = self.first_project_directory(cx);
|
||||
let ssh_details = self.ssh_details(cx);
|
||||
let settings = self.terminal_settings(&path, cx).clone();
|
||||
|
||||
let builder = ShellBuilder::new(ssh_details.is_none(), &settings.shell);
|
||||
let (command, args) = builder.build(command, &Vec::new());
|
||||
|
||||
let mut env = self
|
||||
.environment
|
||||
.read(cx)
|
||||
.get_cli_environment()
|
||||
.unwrap_or_default();
|
||||
env.extend(settings.env.clone());
|
||||
|
||||
match &self.ssh_details(cx) {
|
||||
Some((_, ssh_command)) => {
|
||||
let (command, args) = wrap_for_ssh(
|
||||
ssh_command,
|
||||
Some((&command, &args)),
|
||||
path.as_deref(),
|
||||
env,
|
||||
None,
|
||||
);
|
||||
let mut command = std::process::Command::new(command);
|
||||
command.args(args);
|
||||
command
|
||||
}
|
||||
None => {
|
||||
let mut command = std::process::Command::new(command);
|
||||
command.args(args);
|
||||
command.envs(env);
|
||||
if let Some(path) = path {
|
||||
command.current_dir(path);
|
||||
}
|
||||
command
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_terminal_with_venv(
|
||||
&mut self,
|
||||
kind: TerminalKind,
|
||||
|
|
|
@ -23,9 +23,11 @@ collections.workspace = true
|
|||
command_palette.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
editor.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
libc.workspace = true
|
||||
log.workspace = true
|
||||
multi_buffer.workspace = true
|
||||
nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", branch = "master", features = ["use_tokio"], optional = true }
|
||||
|
@ -36,6 +38,7 @@ serde.workspace = true
|
|||
serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
task.workspace = true
|
||||
theme.workspace = true
|
||||
tokio = { version = "1.15", features = ["full"], optional = true }
|
||||
ui.workspace = true
|
||||
|
@ -47,7 +50,6 @@ zed_actions.workspace = true
|
|||
[dev-dependencies]
|
||||
command_palette.workspace = true
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
futures.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
indoc.workspace = true
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use collections::HashMap;
|
||||
use command_palette_hooks::CommandInterceptResult;
|
||||
use editor::{
|
||||
actions::{SortLinesCaseInsensitive, SortLinesCaseSensitive},
|
||||
display_map::ToDisplayPoint,
|
||||
scroll::Autoscroll,
|
||||
Bias, Editor, ToPoint,
|
||||
};
|
||||
use gpui::{
|
||||
|
@ -15,14 +17,19 @@ use schemars::JsonSchema;
|
|||
use search::{BufferSearchBar, SearchOptions};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
io::Write,
|
||||
iter::Peekable,
|
||||
ops::{Deref, Range},
|
||||
process::Stdio,
|
||||
str::Chars,
|
||||
sync::OnceLock,
|
||||
time::Instant,
|
||||
};
|
||||
use task::{HideStrategy, RevealStrategy, SpawnInTerminal, TaskId};
|
||||
use ui::ActiveTheme;
|
||||
use util::ResultExt;
|
||||
use workspace::{notifications::NotifyResultExt, SaveIntent};
|
||||
use zed_actions::RevealTarget;
|
||||
|
||||
use crate::{
|
||||
motion::{EndOfDocument, Motion, StartOfDocument},
|
||||
|
@ -30,6 +37,7 @@ use crate::{
|
|||
search::{FindCommand, ReplaceCommand, Replacement},
|
||||
JoinLines,
|
||||
},
|
||||
object::Object,
|
||||
state::Mode,
|
||||
visual::VisualDeleteLine,
|
||||
Vim,
|
||||
|
@ -61,10 +69,17 @@ pub struct WithCount {
|
|||
#[derive(Debug)]
|
||||
struct WrappedAction(Box<dyn Action>);
|
||||
|
||||
actions!(vim, [VisualCommand, CountCommand]);
|
||||
actions!(vim, [VisualCommand, CountCommand, ShellCommand]);
|
||||
impl_internal_actions!(
|
||||
vim,
|
||||
[GoToLine, YankCommand, WithRange, WithCount, OnMatchingLines]
|
||||
[
|
||||
GoToLine,
|
||||
YankCommand,
|
||||
WithRange,
|
||||
WithCount,
|
||||
OnMatchingLines,
|
||||
ShellExec
|
||||
]
|
||||
);
|
||||
|
||||
impl PartialEq for WrappedAction {
|
||||
|
@ -96,17 +111,27 @@ pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
|
|||
})
|
||||
});
|
||||
|
||||
Vim::action(editor, cx, |vim, _: &ShellCommand, cx| {
|
||||
let Some(workspace) = vim.workspace(cx) else {
|
||||
return;
|
||||
};
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
command_palette::CommandPalette::toggle(workspace, "'<,'>!", cx);
|
||||
})
|
||||
});
|
||||
|
||||
Vim::action(editor, cx, |vim, _: &CountCommand, cx| {
|
||||
let Some(workspace) = vim.workspace(cx) else {
|
||||
return;
|
||||
};
|
||||
let count = Vim::take_count(cx).unwrap_or(1);
|
||||
let n = if count > 1 {
|
||||
format!(".,.+{}", count.saturating_sub(1))
|
||||
} else {
|
||||
".".to_string()
|
||||
};
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
command_palette::CommandPalette::toggle(
|
||||
workspace,
|
||||
&format!(".,.+{}", count.saturating_sub(1)),
|
||||
cx,
|
||||
);
|
||||
command_palette::CommandPalette::toggle(workspace, &n, cx);
|
||||
})
|
||||
});
|
||||
|
||||
|
@ -209,6 +234,10 @@ pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
|
|||
|
||||
Vim::action(editor, cx, |vim, action: &OnMatchingLines, cx| {
|
||||
action.run(vim, cx)
|
||||
});
|
||||
|
||||
Vim::action(editor, cx, |vim, action: &ShellExec, cx| {
|
||||
action.run(vim, cx)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -817,6 +846,8 @@ pub fn command_interceptor(mut input: &str, cx: &AppContext) -> Option<CommandIn
|
|||
} else {
|
||||
None
|
||||
}
|
||||
} else if query.contains('!') {
|
||||
ShellExec::parse(query, range.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
@ -1057,6 +1088,333 @@ impl OnMatchingLines {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ShellExec {
|
||||
command: String,
|
||||
range: Option<CommandRange>,
|
||||
is_read: bool,
|
||||
}
|
||||
|
||||
impl Vim {
|
||||
pub fn cancel_running_command(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if self.running_command.take().is_some() {
|
||||
self.update_editor(cx, |_, editor, cx| {
|
||||
editor.transact(cx, |editor, _| {
|
||||
editor.clear_row_highlights::<ShellExec>();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_shell_command(&mut self, command: &str, cx: &mut ViewContext<Self>) -> String {
|
||||
let mut ret = String::new();
|
||||
// N.B. non-standard escaping rules:
|
||||
// * !echo % => "echo README.md"
|
||||
// * !echo \% => "echo %"
|
||||
// * !echo \\% => echo \%
|
||||
// * !echo \\\% => echo \\%
|
||||
for c in command.chars() {
|
||||
if c != '%' && c != '!' {
|
||||
ret.push(c);
|
||||
continue;
|
||||
} else if ret.chars().last() == Some('\\') {
|
||||
ret.pop();
|
||||
ret.push(c);
|
||||
continue;
|
||||
}
|
||||
match c {
|
||||
'%' => {
|
||||
self.update_editor(cx, |_, editor, cx| {
|
||||
if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
|
||||
if let Some(file) = buffer.read(cx).file() {
|
||||
if let Some(local) = file.as_local() {
|
||||
if let Some(str) = local.path().to_str() {
|
||||
ret.push_str(str)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
'!' => {
|
||||
if let Some(command) = &self.last_command {
|
||||
ret.push_str(command)
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
self.last_command = Some(ret.clone());
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn shell_command_motion(
|
||||
&mut self,
|
||||
motion: Motion,
|
||||
times: Option<usize>,
|
||||
cx: &mut ViewContext<Vim>,
|
||||
) {
|
||||
self.stop_recording(cx);
|
||||
let Some(workspace) = self.workspace(cx) else {
|
||||
return;
|
||||
};
|
||||
let command = self.update_editor(cx, |_, editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let start = editor.selections.newest_display(cx);
|
||||
let text_layout_details = editor.text_layout_details(cx);
|
||||
let mut range = motion
|
||||
.range(&snapshot, start.clone(), times, false, &text_layout_details)
|
||||
.unwrap_or(start.range());
|
||||
if range.start != start.start {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges([
|
||||
range.start.to_point(&snapshot)..range.start.to_point(&snapshot)
|
||||
]);
|
||||
})
|
||||
}
|
||||
if range.end.row() > range.start.row() && range.end.column() != 0 {
|
||||
*range.end.row_mut() -= 1
|
||||
}
|
||||
if range.end.row() == range.start.row() {
|
||||
".!".to_string()
|
||||
} else {
|
||||
format!(".,.+{}!", (range.end.row() - range.start.row()).0)
|
||||
}
|
||||
});
|
||||
if let Some(command) = command {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
command_palette::CommandPalette::toggle(workspace, &command, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shell_command_object(
|
||||
&mut self,
|
||||
object: Object,
|
||||
around: bool,
|
||||
cx: &mut ViewContext<Vim>,
|
||||
) {
|
||||
self.stop_recording(cx);
|
||||
let Some(workspace) = self.workspace(cx) else {
|
||||
return;
|
||||
};
|
||||
let command = self.update_editor(cx, |_, editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let start = editor.selections.newest_display(cx);
|
||||
let range = object
|
||||
.range(&snapshot, start.clone(), around)
|
||||
.unwrap_or(start.range());
|
||||
if range.start != start.start {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges([
|
||||
range.start.to_point(&snapshot)..range.start.to_point(&snapshot)
|
||||
]);
|
||||
})
|
||||
}
|
||||
if range.end.row() == range.start.row() {
|
||||
".!".to_string()
|
||||
} else {
|
||||
format!(".,.+{}!", (range.end.row() - range.start.row()).0)
|
||||
}
|
||||
});
|
||||
if let Some(command) = command {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
command_palette::CommandPalette::toggle(workspace, &command, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ShellExec {
|
||||
pub fn parse(query: &str, range: Option<CommandRange>) -> Option<Box<dyn Action>> {
|
||||
let (before, after) = query.split_once('!')?;
|
||||
let before = before.trim();
|
||||
|
||||
if !"read".starts_with(before) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
ShellExec {
|
||||
command: after.trim().to_string(),
|
||||
range,
|
||||
is_read: !before.is_empty(),
|
||||
}
|
||||
.boxed_clone(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn run(&self, vim: &mut Vim, cx: &mut ViewContext<Vim>) {
|
||||
let Some(workspace) = vim.workspace(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let project = workspace.read(cx).project().clone();
|
||||
let command = vim.prepare_shell_command(&self.command, cx);
|
||||
|
||||
if self.range.is_none() && !self.is_read {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let project = workspace.project().read(cx);
|
||||
let cwd = project.first_project_directory(cx);
|
||||
let shell = project.terminal_settings(&cwd, cx).shell.clone();
|
||||
cx.emit(workspace::Event::SpawnTask {
|
||||
action: Box::new(SpawnInTerminal {
|
||||
id: TaskId("vim".to_string()),
|
||||
full_label: self.command.clone(),
|
||||
label: self.command.clone(),
|
||||
command: command.clone(),
|
||||
args: Vec::new(),
|
||||
command_label: self.command.clone(),
|
||||
cwd,
|
||||
env: HashMap::default(),
|
||||
use_new_terminal: true,
|
||||
allow_concurrent_runs: true,
|
||||
reveal: RevealStrategy::NoFocus,
|
||||
reveal_target: RevealTarget::Dock,
|
||||
hide: HideStrategy::Never,
|
||||
shell,
|
||||
show_summary: false,
|
||||
show_command: false,
|
||||
}),
|
||||
});
|
||||
});
|
||||
return;
|
||||
};
|
||||
|
||||
let mut input_snapshot = None;
|
||||
let mut input_range = None;
|
||||
let mut needs_newline_prefix = false;
|
||||
vim.update_editor(cx, |vim, editor, cx| {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let range = if let Some(range) = self.range.clone() {
|
||||
let Some(range) = range.buffer_range(vim, editor, cx).log_err() else {
|
||||
return;
|
||||
};
|
||||
Point::new(range.start.0, 0)
|
||||
..snapshot.clip_point(Point::new(range.end.0 + 1, 0), Bias::Right)
|
||||
} else {
|
||||
let mut end = editor.selections.newest::<Point>(cx).range().end;
|
||||
end = snapshot.clip_point(Point::new(end.row + 1, 0), Bias::Right);
|
||||
needs_newline_prefix = end == snapshot.max_point();
|
||||
end..end
|
||||
};
|
||||
if self.is_read {
|
||||
input_range =
|
||||
Some(snapshot.anchor_after(range.end)..snapshot.anchor_after(range.end));
|
||||
} else {
|
||||
input_range =
|
||||
Some(snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end));
|
||||
}
|
||||
editor.highlight_rows::<ShellExec>(
|
||||
input_range.clone().unwrap(),
|
||||
cx.theme().status().unreachable_background,
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
|
||||
if !self.is_read {
|
||||
input_snapshot = Some(snapshot)
|
||||
}
|
||||
});
|
||||
|
||||
let Some(range) = input_range else { return };
|
||||
|
||||
let mut process = project.read(cx).exec_in_shell(command, cx);
|
||||
process.stdout(Stdio::piped());
|
||||
process.stderr(Stdio::piped());
|
||||
|
||||
if input_snapshot.is_some() {
|
||||
process.stdin(Stdio::piped());
|
||||
} else {
|
||||
process.stdin(Stdio::null());
|
||||
};
|
||||
|
||||
// https://registerspill.thorstenball.com/p/how-to-lose-control-of-your-shell
|
||||
//
|
||||
// safety: code in pre_exec should be signal safe.
|
||||
// https://man7.org/linux/man-pages/man7/signal-safety.7.html
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
unsafe {
|
||||
use std::os::unix::process::CommandExt;
|
||||
process.pre_exec(|| {
|
||||
libc::setsid();
|
||||
Ok(())
|
||||
});
|
||||
};
|
||||
let is_read = self.is_read;
|
||||
|
||||
let task = cx.spawn(|vim, mut cx| async move {
|
||||
let Some(mut running) = process.spawn().log_err() else {
|
||||
vim.update(&mut cx, |vim, cx| {
|
||||
vim.cancel_running_command(cx);
|
||||
})
|
||||
.log_err();
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(mut stdin) = running.stdin.take() {
|
||||
if let Some(snapshot) = input_snapshot {
|
||||
let range = range.clone();
|
||||
cx.background_executor()
|
||||
.spawn(async move {
|
||||
for chunk in snapshot.text_for_range(range) {
|
||||
if stdin.write_all(chunk.as_bytes()).log_err().is_none() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
stdin.flush().log_err();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
};
|
||||
|
||||
let output = cx
|
||||
.background_executor()
|
||||
.spawn(async move { running.wait_with_output() })
|
||||
.await;
|
||||
|
||||
let Some(output) = output.log_err() else {
|
||||
vim.update(&mut cx, |vim, cx| {
|
||||
vim.cancel_running_command(cx);
|
||||
})
|
||||
.log_err();
|
||||
return;
|
||||
};
|
||||
let mut text = String::new();
|
||||
if needs_newline_prefix {
|
||||
text.push('\n');
|
||||
}
|
||||
text.push_str(&String::from_utf8_lossy(&output.stdout));
|
||||
text.push_str(&String::from_utf8_lossy(&output.stderr));
|
||||
if !text.is_empty() && text.chars().last() != Some('\n') {
|
||||
text.push('\n');
|
||||
}
|
||||
|
||||
vim.update(&mut cx, |vim, cx| {
|
||||
vim.update_editor(cx, |_, editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.edit([(range.clone(), text)], cx);
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
let point = if is_read {
|
||||
let point = range.end.to_point(&snapshot);
|
||||
Point::new(point.row.saturating_sub(1), 0)
|
||||
} else {
|
||||
let point = range.start.to_point(&snapshot);
|
||||
Point::new(point.row, 0)
|
||||
};
|
||||
s.select_ranges([point..point]);
|
||||
})
|
||||
})
|
||||
});
|
||||
vim.cancel_running_command(cx);
|
||||
})
|
||||
.log_err();
|
||||
});
|
||||
vim.running_command.replace(task);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::path::Path;
|
||||
|
|
|
@ -160,6 +160,7 @@ impl Vim {
|
|||
Some(Operator::AutoIndent) => {
|
||||
self.indent_motion(motion, times, IndentDirection::Auto, cx)
|
||||
}
|
||||
Some(Operator::ShellCommand) => self.shell_command_motion(motion, times, cx),
|
||||
Some(Operator::Lowercase) => {
|
||||
self.change_case_motion(motion, times, CaseTarget::Lowercase, cx)
|
||||
}
|
||||
|
@ -195,6 +196,9 @@ impl Vim {
|
|||
Some(Operator::AutoIndent) => {
|
||||
self.indent_object(object, around, IndentDirection::Auto, cx)
|
||||
}
|
||||
Some(Operator::ShellCommand) => {
|
||||
self.shell_command_object(object, around, cx);
|
||||
}
|
||||
Some(Operator::Rewrap) => self.rewrap_object(object, around, cx),
|
||||
Some(Operator::Lowercase) => {
|
||||
self.change_case_object(object, around, CaseTarget::Lowercase, cx)
|
||||
|
|
|
@ -96,6 +96,7 @@ pub enum Operator {
|
|||
Outdent,
|
||||
AutoIndent,
|
||||
Rewrap,
|
||||
ShellCommand,
|
||||
Lowercase,
|
||||
Uppercase,
|
||||
OppositeCase,
|
||||
|
@ -495,6 +496,7 @@ impl Operator {
|
|||
Operator::Jump { line: false } => "`",
|
||||
Operator::Indent => ">",
|
||||
Operator::AutoIndent => "eq",
|
||||
Operator::ShellCommand => "sh",
|
||||
Operator::Rewrap => "gq",
|
||||
Operator::Outdent => "<",
|
||||
Operator::Uppercase => "gU",
|
||||
|
@ -516,6 +518,7 @@ impl Operator {
|
|||
prefix: Some(prefix),
|
||||
} => format!("^V{prefix}"),
|
||||
Operator::AutoIndent => "=".to_string(),
|
||||
Operator::ShellCommand => "=".to_string(),
|
||||
_ => self.id().to_string(),
|
||||
}
|
||||
}
|
||||
|
@ -544,6 +547,7 @@ impl Operator {
|
|||
| Operator::Indent
|
||||
| Operator::Outdent
|
||||
| Operator::AutoIndent
|
||||
| Operator::ShellCommand
|
||||
| Operator::Lowercase
|
||||
| Operator::Uppercase
|
||||
| Operator::Object { .. }
|
||||
|
|
|
@ -27,7 +27,7 @@ use editor::{
|
|||
};
|
||||
use gpui::{
|
||||
actions, impl_actions, Action, AppContext, Axis, Entity, EventEmitter, KeyContext,
|
||||
KeystrokeEvent, Render, Subscription, View, ViewContext, WeakView,
|
||||
KeystrokeEvent, Render, Subscription, Task, View, ViewContext, WeakView,
|
||||
};
|
||||
use insert::{NormalBefore, TemporaryNormal};
|
||||
use language::{CursorShape, Point, Selection, SelectionGoal, TransactionId};
|
||||
|
@ -76,7 +76,6 @@ actions!(
|
|||
ClearOperators,
|
||||
Tab,
|
||||
Enter,
|
||||
Object,
|
||||
InnerObject,
|
||||
FindForward,
|
||||
FindBackward,
|
||||
|
@ -221,6 +220,8 @@ pub(crate) struct Vim {
|
|||
|
||||
editor: WeakView<Editor>,
|
||||
|
||||
last_command: Option<String>,
|
||||
running_command: Option<Task<()>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
|
@ -264,6 +265,9 @@ impl Vim {
|
|||
selected_register: None,
|
||||
search: SearchState::default(),
|
||||
|
||||
last_command: None,
|
||||
running_command: None,
|
||||
|
||||
editor: editor.downgrade(),
|
||||
_subscriptions: vec![
|
||||
cx.observe_keystrokes(Self::observe_keystrokes),
|
||||
|
@ -519,6 +523,7 @@ impl Vim {
|
|||
self.mode = mode;
|
||||
self.operator_stack.clear();
|
||||
self.selected_register.take();
|
||||
self.cancel_running_command(cx);
|
||||
if mode == Mode::Normal || mode != last_mode {
|
||||
self.current_tx.take();
|
||||
self.current_anchor.take();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue