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",
|
"indoc",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"language",
|
"language",
|
||||||
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"lsp",
|
"lsp",
|
||||||
"multi_buffer",
|
"multi_buffer",
|
||||||
|
@ -14318,6 +14319,7 @@ dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"settings",
|
"settings",
|
||||||
|
"task",
|
||||||
"theme",
|
"theme",
|
||||||
"tokio",
|
"tokio",
|
||||||
"ui",
|
"ui",
|
||||||
|
|
|
@ -221,6 +221,7 @@
|
||||||
">": ["vim::PushOperator", "Indent"],
|
">": ["vim::PushOperator", "Indent"],
|
||||||
"<": ["vim::PushOperator", "Outdent"],
|
"<": ["vim::PushOperator", "Outdent"],
|
||||||
"=": ["vim::PushOperator", "AutoIndent"],
|
"=": ["vim::PushOperator", "AutoIndent"],
|
||||||
|
"!": ["vim::PushOperator", "ShellCommand"],
|
||||||
"g u": ["vim::PushOperator", "Lowercase"],
|
"g u": ["vim::PushOperator", "Lowercase"],
|
||||||
"g shift-u": ["vim::PushOperator", "Uppercase"],
|
"g shift-u": ["vim::PushOperator", "Uppercase"],
|
||||||
"g ~": ["vim::PushOperator", "OppositeCase"],
|
"g ~": ["vim::PushOperator", "OppositeCase"],
|
||||||
|
@ -287,6 +288,7 @@
|
||||||
">": "vim::Indent",
|
">": "vim::Indent",
|
||||||
"<": "vim::Outdent",
|
"<": "vim::Outdent",
|
||||||
"=": "vim::AutoIndent",
|
"=": "vim::AutoIndent",
|
||||||
|
"!": "vim::ShellCommand",
|
||||||
"i": ["vim::PushOperator", { "Object": { "around": false } }],
|
"i": ["vim::PushOperator", { "Object": { "around": false } }],
|
||||||
"a": ["vim::PushOperator", { "Object": { "around": true } }],
|
"a": ["vim::PushOperator", { "Object": { "around": true } }],
|
||||||
"g c": "vim::ToggleComments",
|
"g c": "vim::ToggleComments",
|
||||||
|
@ -498,6 +500,12 @@
|
||||||
"=": "vim::CurrentLine"
|
"=": "vim::CurrentLine"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "vim_operator == sh",
|
||||||
|
"bindings": {
|
||||||
|
"!": "vim::CurrentLine"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_operator == gc",
|
"context": "vim_operator == gc",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
|
|
|
@ -13,7 +13,7 @@ use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
use task::{Shell, SpawnInTerminal};
|
use task::{Shell, ShellBuilder, SpawnInTerminal};
|
||||||
use terminal::{
|
use terminal::{
|
||||||
terminal_settings::{self, TerminalSettings, VenvSettings},
|
terminal_settings::{self, TerminalSettings, VenvSettings},
|
||||||
TaskState, TaskStatus, Terminal, TerminalBuilder,
|
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 {
|
if let Some(ssh_client) = &self.ssh_client {
|
||||||
let ssh_client = ssh_client.read(cx);
|
let ssh_client = ssh_client.read(cx);
|
||||||
if let Some(args) = ssh_client.ssh_args() {
|
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(
|
pub fn create_terminal_with_venv(
|
||||||
&mut self,
|
&mut self,
|
||||||
kind: TerminalKind,
|
kind: TerminalKind,
|
||||||
|
|
|
@ -23,9 +23,11 @@ collections.workspace = true
|
||||||
command_palette.workspace = true
|
command_palette.workspace = true
|
||||||
command_palette_hooks.workspace = true
|
command_palette_hooks.workspace = true
|
||||||
editor.workspace = true
|
editor.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
|
libc.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
multi_buffer.workspace = true
|
multi_buffer.workspace = true
|
||||||
nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", branch = "master", features = ["use_tokio"], optional = 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_derive.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
settings.workspace = true
|
settings.workspace = true
|
||||||
|
task.workspace = true
|
||||||
theme.workspace = true
|
theme.workspace = true
|
||||||
tokio = { version = "1.15", features = ["full"], optional = true }
|
tokio = { version = "1.15", features = ["full"], optional = true }
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
|
@ -47,7 +50,6 @@ zed_actions.workspace = true
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
command_palette.workspace = true
|
command_palette.workspace = true
|
||||||
editor = { workspace = true, features = ["test-support"] }
|
editor = { workspace = true, features = ["test-support"] }
|
||||||
futures.workspace = true
|
|
||||||
gpui = { workspace = true, features = ["test-support"] }
|
gpui = { workspace = true, features = ["test-support"] }
|
||||||
indoc.workspace = true
|
indoc.workspace = true
|
||||||
language = { workspace = true, features = ["test-support"] }
|
language = { workspace = true, features = ["test-support"] }
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
|
use collections::HashMap;
|
||||||
use command_palette_hooks::CommandInterceptResult;
|
use command_palette_hooks::CommandInterceptResult;
|
||||||
use editor::{
|
use editor::{
|
||||||
actions::{SortLinesCaseInsensitive, SortLinesCaseSensitive},
|
actions::{SortLinesCaseInsensitive, SortLinesCaseSensitive},
|
||||||
display_map::ToDisplayPoint,
|
display_map::ToDisplayPoint,
|
||||||
|
scroll::Autoscroll,
|
||||||
Bias, Editor, ToPoint,
|
Bias, Editor, ToPoint,
|
||||||
};
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
|
@ -15,14 +17,19 @@ use schemars::JsonSchema;
|
||||||
use search::{BufferSearchBar, SearchOptions};
|
use search::{BufferSearchBar, SearchOptions};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{
|
use std::{
|
||||||
|
io::Write,
|
||||||
iter::Peekable,
|
iter::Peekable,
|
||||||
ops::{Deref, Range},
|
ops::{Deref, Range},
|
||||||
|
process::Stdio,
|
||||||
str::Chars,
|
str::Chars,
|
||||||
sync::OnceLock,
|
sync::OnceLock,
|
||||||
time::Instant,
|
time::Instant,
|
||||||
};
|
};
|
||||||
|
use task::{HideStrategy, RevealStrategy, SpawnInTerminal, TaskId};
|
||||||
|
use ui::ActiveTheme;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::{notifications::NotifyResultExt, SaveIntent};
|
use workspace::{notifications::NotifyResultExt, SaveIntent};
|
||||||
|
use zed_actions::RevealTarget;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
motion::{EndOfDocument, Motion, StartOfDocument},
|
motion::{EndOfDocument, Motion, StartOfDocument},
|
||||||
|
@ -30,6 +37,7 @@ use crate::{
|
||||||
search::{FindCommand, ReplaceCommand, Replacement},
|
search::{FindCommand, ReplaceCommand, Replacement},
|
||||||
JoinLines,
|
JoinLines,
|
||||||
},
|
},
|
||||||
|
object::Object,
|
||||||
state::Mode,
|
state::Mode,
|
||||||
visual::VisualDeleteLine,
|
visual::VisualDeleteLine,
|
||||||
Vim,
|
Vim,
|
||||||
|
@ -61,10 +69,17 @@ pub struct WithCount {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct WrappedAction(Box<dyn Action>);
|
struct WrappedAction(Box<dyn Action>);
|
||||||
|
|
||||||
actions!(vim, [VisualCommand, CountCommand]);
|
actions!(vim, [VisualCommand, CountCommand, ShellCommand]);
|
||||||
impl_internal_actions!(
|
impl_internal_actions!(
|
||||||
vim,
|
vim,
|
||||||
[GoToLine, YankCommand, WithRange, WithCount, OnMatchingLines]
|
[
|
||||||
|
GoToLine,
|
||||||
|
YankCommand,
|
||||||
|
WithRange,
|
||||||
|
WithCount,
|
||||||
|
OnMatchingLines,
|
||||||
|
ShellExec
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
impl PartialEq for WrappedAction {
|
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| {
|
Vim::action(editor, cx, |vim, _: &CountCommand, cx| {
|
||||||
let Some(workspace) = vim.workspace(cx) else {
|
let Some(workspace) = vim.workspace(cx) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let count = Vim::take_count(cx).unwrap_or(1);
|
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| {
|
workspace.update(cx, |workspace, cx| {
|
||||||
command_palette::CommandPalette::toggle(
|
command_palette::CommandPalette::toggle(workspace, &n, cx);
|
||||||
workspace,
|
|
||||||
&format!(".,.+{}", count.saturating_sub(1)),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -209,6 +234,10 @@ pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
|
||||||
|
|
||||||
Vim::action(editor, cx, |vim, action: &OnMatchingLines, cx| {
|
Vim::action(editor, cx, |vim, action: &OnMatchingLines, cx| {
|
||||||
action.run(vim, 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 {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
} else if query.contains('!') {
|
||||||
|
ShellExec::parse(query, range.clone())
|
||||||
} else {
|
} else {
|
||||||
None
|
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)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
|
@ -160,6 +160,7 @@ impl Vim {
|
||||||
Some(Operator::AutoIndent) => {
|
Some(Operator::AutoIndent) => {
|
||||||
self.indent_motion(motion, times, IndentDirection::Auto, cx)
|
self.indent_motion(motion, times, IndentDirection::Auto, cx)
|
||||||
}
|
}
|
||||||
|
Some(Operator::ShellCommand) => self.shell_command_motion(motion, times, cx),
|
||||||
Some(Operator::Lowercase) => {
|
Some(Operator::Lowercase) => {
|
||||||
self.change_case_motion(motion, times, CaseTarget::Lowercase, cx)
|
self.change_case_motion(motion, times, CaseTarget::Lowercase, cx)
|
||||||
}
|
}
|
||||||
|
@ -195,6 +196,9 @@ impl Vim {
|
||||||
Some(Operator::AutoIndent) => {
|
Some(Operator::AutoIndent) => {
|
||||||
self.indent_object(object, around, IndentDirection::Auto, cx)
|
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::Rewrap) => self.rewrap_object(object, around, cx),
|
||||||
Some(Operator::Lowercase) => {
|
Some(Operator::Lowercase) => {
|
||||||
self.change_case_object(object, around, CaseTarget::Lowercase, cx)
|
self.change_case_object(object, around, CaseTarget::Lowercase, cx)
|
||||||
|
|
|
@ -96,6 +96,7 @@ pub enum Operator {
|
||||||
Outdent,
|
Outdent,
|
||||||
AutoIndent,
|
AutoIndent,
|
||||||
Rewrap,
|
Rewrap,
|
||||||
|
ShellCommand,
|
||||||
Lowercase,
|
Lowercase,
|
||||||
Uppercase,
|
Uppercase,
|
||||||
OppositeCase,
|
OppositeCase,
|
||||||
|
@ -495,6 +496,7 @@ impl Operator {
|
||||||
Operator::Jump { line: false } => "`",
|
Operator::Jump { line: false } => "`",
|
||||||
Operator::Indent => ">",
|
Operator::Indent => ">",
|
||||||
Operator::AutoIndent => "eq",
|
Operator::AutoIndent => "eq",
|
||||||
|
Operator::ShellCommand => "sh",
|
||||||
Operator::Rewrap => "gq",
|
Operator::Rewrap => "gq",
|
||||||
Operator::Outdent => "<",
|
Operator::Outdent => "<",
|
||||||
Operator::Uppercase => "gU",
|
Operator::Uppercase => "gU",
|
||||||
|
@ -516,6 +518,7 @@ impl Operator {
|
||||||
prefix: Some(prefix),
|
prefix: Some(prefix),
|
||||||
} => format!("^V{prefix}"),
|
} => format!("^V{prefix}"),
|
||||||
Operator::AutoIndent => "=".to_string(),
|
Operator::AutoIndent => "=".to_string(),
|
||||||
|
Operator::ShellCommand => "=".to_string(),
|
||||||
_ => self.id().to_string(),
|
_ => self.id().to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -544,6 +547,7 @@ impl Operator {
|
||||||
| Operator::Indent
|
| Operator::Indent
|
||||||
| Operator::Outdent
|
| Operator::Outdent
|
||||||
| Operator::AutoIndent
|
| Operator::AutoIndent
|
||||||
|
| Operator::ShellCommand
|
||||||
| Operator::Lowercase
|
| Operator::Lowercase
|
||||||
| Operator::Uppercase
|
| Operator::Uppercase
|
||||||
| Operator::Object { .. }
|
| Operator::Object { .. }
|
||||||
|
|
|
@ -27,7 +27,7 @@ use editor::{
|
||||||
};
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, impl_actions, Action, AppContext, Axis, Entity, EventEmitter, KeyContext,
|
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 insert::{NormalBefore, TemporaryNormal};
|
||||||
use language::{CursorShape, Point, Selection, SelectionGoal, TransactionId};
|
use language::{CursorShape, Point, Selection, SelectionGoal, TransactionId};
|
||||||
|
@ -76,7 +76,6 @@ actions!(
|
||||||
ClearOperators,
|
ClearOperators,
|
||||||
Tab,
|
Tab,
|
||||||
Enter,
|
Enter,
|
||||||
Object,
|
|
||||||
InnerObject,
|
InnerObject,
|
||||||
FindForward,
|
FindForward,
|
||||||
FindBackward,
|
FindBackward,
|
||||||
|
@ -221,6 +220,8 @@ pub(crate) struct Vim {
|
||||||
|
|
||||||
editor: WeakView<Editor>,
|
editor: WeakView<Editor>,
|
||||||
|
|
||||||
|
last_command: Option<String>,
|
||||||
|
running_command: Option<Task<()>>,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -264,6 +265,9 @@ impl Vim {
|
||||||
selected_register: None,
|
selected_register: None,
|
||||||
search: SearchState::default(),
|
search: SearchState::default(),
|
||||||
|
|
||||||
|
last_command: None,
|
||||||
|
running_command: None,
|
||||||
|
|
||||||
editor: editor.downgrade(),
|
editor: editor.downgrade(),
|
||||||
_subscriptions: vec![
|
_subscriptions: vec![
|
||||||
cx.observe_keystrokes(Self::observe_keystrokes),
|
cx.observe_keystrokes(Self::observe_keystrokes),
|
||||||
|
@ -519,6 +523,7 @@ impl Vim {
|
||||||
self.mode = mode;
|
self.mode = mode;
|
||||||
self.operator_stack.clear();
|
self.operator_stack.clear();
|
||||||
self.selected_register.take();
|
self.selected_register.take();
|
||||||
|
self.cancel_running_command(cx);
|
||||||
if mode == Mode::Normal || mode != last_mode {
|
if mode == Mode::Normal || mode != last_mode {
|
||||||
self.current_tx.take();
|
self.current_tx.take();
|
||||||
self.current_anchor.take();
|
self.current_anchor.take();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue