mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-19 02:17:35 +03:00
vim: Support ranges in command (#15985)
The most requested feature here is "search and replace in visual mode", but as a happy side effect we can now support things like :2,12j to join those lines, and much much more. Release Notes: - vim: Added support for range syntax in command ([#9428](https://github.com/zed-industries/zed/issues/9428)). - vim: Prefill command with `:'<,'>` from visual mode ([#13535](https://github.com/zed-industries/zed/issues/13535)).
This commit is contained in:
parent
b7d6b0a096
commit
bd59af1df5
@ -4,7 +4,6 @@
|
|||||||
"bindings": {
|
"bindings": {
|
||||||
"i": ["vim::PushOperator", { "Object": { "around": false } }],
|
"i": ["vim::PushOperator", { "Object": { "around": false } }],
|
||||||
"a": ["vim::PushOperator", { "Object": { "around": true } }],
|
"a": ["vim::PushOperator", { "Object": { "around": true } }],
|
||||||
":": "command_palette::Toggle",
|
|
||||||
"h": "vim::Left",
|
"h": "vim::Left",
|
||||||
"left": "vim::Left",
|
"left": "vim::Left",
|
||||||
"backspace": "vim::Backspace",
|
"backspace": "vim::Backspace",
|
||||||
@ -199,17 +198,12 @@
|
|||||||
"ctrl-6": "pane::AlternateFile"
|
"ctrl-6": "pane::AlternateFile"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"context": "VimControl && VimCount",
|
|
||||||
"bindings": {
|
|
||||||
"0": ["vim::Number", 0]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"context": "vim_mode == normal",
|
"context": "vim_mode == normal",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"escape": "editor::Cancel",
|
"escape": "editor::Cancel",
|
||||||
"ctrl-[": "editor::Cancel",
|
"ctrl-[": "editor::Cancel",
|
||||||
|
":": "command_palette::Toggle",
|
||||||
".": "vim::Repeat",
|
".": "vim::Repeat",
|
||||||
"c": ["vim::PushOperator", "Change"],
|
"c": ["vim::PushOperator", "Change"],
|
||||||
"shift-c": "vim::ChangeToEndOfLine",
|
"shift-c": "vim::ChangeToEndOfLine",
|
||||||
@ -257,9 +251,17 @@
|
|||||||
"g c": ["vim::PushOperator", "ToggleComments"]
|
"g c": ["vim::PushOperator", "ToggleComments"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "VimControl && VimCount",
|
||||||
|
"bindings": {
|
||||||
|
"0": ["vim::Number", 0],
|
||||||
|
":": "vim::CountCommand"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_mode == visual",
|
"context": "vim_mode == visual",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
|
":": "vim::VisualCommand",
|
||||||
"u": "vim::ConvertToLowerCase",
|
"u": "vim::ConvertToLowerCase",
|
||||||
"U": "vim::ConvertToUpperCase",
|
"U": "vim::ConvertToUpperCase",
|
||||||
"o": "vim::OtherEnd",
|
"o": "vim::OtherEnd",
|
||||||
|
@ -58,20 +58,23 @@ fn trim_consecutive_whitespaces(input: &str) -> String {
|
|||||||
|
|
||||||
impl CommandPalette {
|
impl CommandPalette {
|
||||||
fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||||
workspace.register_action(|workspace, _: &Toggle, cx| {
|
workspace.register_action(|workspace, _: &Toggle, cx| Self::toggle(workspace, "", cx));
|
||||||
let Some(previous_focus_handle) = cx.focused() else {
|
}
|
||||||
return;
|
|
||||||
};
|
pub fn toggle(workspace: &mut Workspace, query: &str, cx: &mut ViewContext<Workspace>) {
|
||||||
let telemetry = workspace.client().telemetry().clone();
|
let Some(previous_focus_handle) = cx.focused() else {
|
||||||
workspace.toggle_modal(cx, move |cx| {
|
return;
|
||||||
CommandPalette::new(previous_focus_handle, telemetry, cx)
|
};
|
||||||
});
|
let telemetry = workspace.client().telemetry().clone();
|
||||||
|
workspace.toggle_modal(cx, move |cx| {
|
||||||
|
CommandPalette::new(previous_focus_handle, telemetry, query, cx)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new(
|
fn new(
|
||||||
previous_focus_handle: FocusHandle,
|
previous_focus_handle: FocusHandle,
|
||||||
telemetry: Arc<Telemetry>,
|
telemetry: Arc<Telemetry>,
|
||||||
|
query: &str,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let filter = CommandPaletteFilter::try_global(cx);
|
let filter = CommandPaletteFilter::try_global(cx);
|
||||||
@ -98,9 +101,18 @@ impl CommandPalette {
|
|||||||
previous_focus_handle,
|
previous_focus_handle,
|
||||||
);
|
);
|
||||||
|
|
||||||
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
|
let picker = cx.new_view(|cx| {
|
||||||
|
let picker = Picker::uniform_list(delegate, cx);
|
||||||
|
picker.set_query(query, cx);
|
||||||
|
picker
|
||||||
|
});
|
||||||
Self { picker }
|
Self { picker }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_query(&mut self, query: &str, cx: &mut ViewContext<Self>) {
|
||||||
|
self.picker
|
||||||
|
.update(cx, |picker, cx| picker.set_query(query, cx))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<DismissEvent> for CommandPalette {}
|
impl EventEmitter<DismissEvent> for CommandPalette {}
|
||||||
|
@ -393,7 +393,7 @@ impl<'a> MutableSelectionsCollection<'a> {
|
|||||||
self.collection.display_map(self.cx)
|
self.collection.display_map(self.cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn buffer(&self) -> Ref<MultiBufferSnapshot> {
|
pub fn buffer(&self) -> Ref<MultiBufferSnapshot> {
|
||||||
self.collection.buffer(self.cx)
|
self.collection.buffer(self.cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ anyhow.workspace = true
|
|||||||
async-compat = { version = "0.2.1", "optional" = true }
|
async-compat = { version = "0.2.1", "optional" = true }
|
||||||
async-trait = { workspace = true, "optional" = true }
|
async-trait = { workspace = true, "optional" = true }
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
|
command_palette.workspace = true
|
||||||
command_palette_hooks.workspace = true
|
command_palette_hooks.workspace = true
|
||||||
editor.workspace = true
|
editor.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
@ -1,45 +1,154 @@
|
|||||||
use std::sync::OnceLock;
|
use std::{iter::Peekable, ops::Range, str::Chars, sync::OnceLock};
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
use command_palette_hooks::CommandInterceptResult;
|
use command_palette_hooks::CommandInterceptResult;
|
||||||
use editor::actions::{SortLinesCaseInsensitive, SortLinesCaseSensitive};
|
use editor::{
|
||||||
use gpui::{impl_actions, Action, AppContext, Global, ViewContext};
|
actions::{SortLinesCaseInsensitive, SortLinesCaseSensitive},
|
||||||
use serde_derive::Deserialize;
|
Editor, ToPoint,
|
||||||
|
};
|
||||||
|
use gpui::{actions, impl_actions, Action, AppContext, Global, ViewContext};
|
||||||
|
use language::Point;
|
||||||
|
use multi_buffer::MultiBufferRow;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use ui::WindowContext;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::{SaveIntent, Workspace};
|
use workspace::{notifications::NotifyResultExt, SaveIntent, Workspace};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
motion::{EndOfDocument, Motion, StartOfDocument},
|
motion::{EndOfDocument, Motion, StartOfDocument},
|
||||||
normal::{
|
normal::{
|
||||||
move_cursor,
|
move_cursor,
|
||||||
search::{range_regex, FindCommand, ReplaceCommand},
|
search::{FindCommand, ReplaceCommand, Replacement},
|
||||||
JoinLines,
|
JoinLines,
|
||||||
},
|
},
|
||||||
state::Mode,
|
state::Mode,
|
||||||
|
visual::VisualDeleteLine,
|
||||||
Vim,
|
Vim,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||||
pub struct GoToLine {
|
pub struct GoToLine {
|
||||||
pub line: u32,
|
range: CommandRange,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_actions!(vim, [GoToLine]);
|
#[derive(Debug)]
|
||||||
|
pub struct WithRange {
|
||||||
|
is_count: bool,
|
||||||
|
range: CommandRange,
|
||||||
|
action: Box<dyn Action>,
|
||||||
|
}
|
||||||
|
|
||||||
|
actions!(vim, [VisualCommand, CountCommand]);
|
||||||
|
impl_actions!(vim, [GoToLine, WithRange]);
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for WithRange {
|
||||||
|
fn deserialize<D>(_: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
Err(serde::de::Error::custom("Cannot deserialize WithRange"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for WithRange {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.range == other.range && self.action.partial_eq(&*other.action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for WithRange {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
is_count: self.is_count,
|
||||||
|
range: self.range.clone(),
|
||||||
|
action: self.action.boxed_clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||||
workspace.register_action(|_: &mut Workspace, action: &GoToLine, cx| {
|
workspace.register_action(|workspace, _: &VisualCommand, cx| {
|
||||||
|
command_palette::CommandPalette::toggle(workspace, "'<,'>", cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
workspace.register_action(|workspace, _: &CountCommand, cx| {
|
||||||
|
let count = Vim::update(cx, |vim, cx| vim.take_count(cx)).unwrap_or(1);
|
||||||
|
command_palette::CommandPalette::toggle(
|
||||||
|
workspace,
|
||||||
|
&format!(".,.+{}", count.saturating_sub(1)),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
workspace.register_action(|workspace: &mut Workspace, action: &GoToLine, cx| {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
vim.switch_mode(Mode::Normal, false, cx);
|
vim.switch_mode(Mode::Normal, false, cx);
|
||||||
move_cursor(vim, Motion::StartOfDocument, Some(action.line as usize), cx);
|
let result = vim.update_active_editor(cx, |vim, editor, cx| {
|
||||||
});
|
action.range.head().buffer_row(vim, editor, cx)
|
||||||
|
});
|
||||||
|
let Some(buffer_row) = result else {
|
||||||
|
return anyhow::Ok(());
|
||||||
|
};
|
||||||
|
move_cursor(
|
||||||
|
vim,
|
||||||
|
Motion::StartOfDocument,
|
||||||
|
Some(buffer_row?.0 as usize + 1),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.notify_err(workspace, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
workspace.register_action(|workspace: &mut Workspace, action: &WithRange, cx| {
|
||||||
|
if action.is_count {
|
||||||
|
for _ in 0..action.range.as_count() {
|
||||||
|
cx.dispatch_action(action.action.boxed_clone())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Vim::update(cx, |vim, cx| {
|
||||||
|
let result = vim.update_active_editor(cx, |vim, editor, cx| {
|
||||||
|
action.range.buffer_range(vim, editor, cx)
|
||||||
|
});
|
||||||
|
let Some(range) = result else {
|
||||||
|
return anyhow::Ok(());
|
||||||
|
};
|
||||||
|
let range = range?;
|
||||||
|
vim.update_active_editor(cx, |_, editor, cx| {
|
||||||
|
editor.change_selections(None, cx, |s| {
|
||||||
|
let end = Point::new(range.end.0, s.buffer().line_len(range.end));
|
||||||
|
s.select_ranges([end..Point::new(range.start.0, 0)]);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
cx.dispatch_action(action.action.boxed_clone());
|
||||||
|
cx.defer(move |cx| {
|
||||||
|
Vim::update(cx, |vim, cx| {
|
||||||
|
vim.update_active_editor(cx, |_, editor, cx| {
|
||||||
|
editor.change_selections(None, cx, |s| {
|
||||||
|
s.select_ranges([
|
||||||
|
Point::new(range.start.0, 0)..Point::new(range.start.0, 0)
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.notify_err(workspace, cx);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
struct VimCommand {
|
struct VimCommand {
|
||||||
prefix: &'static str,
|
prefix: &'static str,
|
||||||
suffix: &'static str,
|
suffix: &'static str,
|
||||||
action: Option<Box<dyn Action>>,
|
action: Option<Box<dyn Action>>,
|
||||||
action_name: Option<&'static str>,
|
action_name: Option<&'static str>,
|
||||||
bang_action: Option<Box<dyn Action>>,
|
bang_action: Option<Box<dyn Action>>,
|
||||||
|
has_range: bool,
|
||||||
|
has_count: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VimCommand {
|
impl VimCommand {
|
||||||
@ -48,8 +157,7 @@ impl VimCommand {
|
|||||||
prefix: pattern.0,
|
prefix: pattern.0,
|
||||||
suffix: pattern.1,
|
suffix: pattern.1,
|
||||||
action: Some(action.boxed_clone()),
|
action: Some(action.boxed_clone()),
|
||||||
action_name: None,
|
..Default::default()
|
||||||
bang_action: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,9 +166,8 @@ impl VimCommand {
|
|||||||
Self {
|
Self {
|
||||||
prefix: pattern.0,
|
prefix: pattern.0,
|
||||||
suffix: pattern.1,
|
suffix: pattern.1,
|
||||||
action: None,
|
|
||||||
action_name: Some(action_name),
|
action_name: Some(action_name),
|
||||||
bang_action: None,
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,6 +176,15 @@ impl VimCommand {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn range(mut self) -> Self {
|
||||||
|
self.has_range = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
fn count(mut self) -> Self {
|
||||||
|
self.has_count = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
fn parse(&self, mut query: &str, cx: &AppContext) -> Option<Box<dyn Action>> {
|
fn parse(&self, mut query: &str, cx: &AppContext) -> Option<Box<dyn Action>> {
|
||||||
let has_bang = query.ends_with('!');
|
let has_bang = query.ends_with('!');
|
||||||
if has_bang {
|
if has_bang {
|
||||||
@ -92,6 +208,220 @@ impl VimCommand {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: ranges with search queries
|
||||||
|
fn parse_range(query: &str) -> (Option<CommandRange>, String) {
|
||||||
|
let mut chars = query.chars().peekable();
|
||||||
|
|
||||||
|
match chars.peek() {
|
||||||
|
Some('%') => {
|
||||||
|
chars.next();
|
||||||
|
return (
|
||||||
|
Some(CommandRange {
|
||||||
|
start: Position::Line { row: 1, offset: 0 },
|
||||||
|
end: Some(Position::LastLine { offset: 0 }),
|
||||||
|
}),
|
||||||
|
chars.collect(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Some('*') => {
|
||||||
|
chars.next();
|
||||||
|
return (
|
||||||
|
Some(CommandRange {
|
||||||
|
start: Position::Mark {
|
||||||
|
name: '<',
|
||||||
|
offset: 0,
|
||||||
|
},
|
||||||
|
end: Some(Position::Mark {
|
||||||
|
name: '>',
|
||||||
|
offset: 0,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
chars.collect(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let start = Self::parse_position(&mut chars);
|
||||||
|
|
||||||
|
match chars.peek() {
|
||||||
|
Some(',' | ';') => {
|
||||||
|
chars.next();
|
||||||
|
(
|
||||||
|
Some(CommandRange {
|
||||||
|
start: start.unwrap_or(Position::CurrentLine { offset: 0 }),
|
||||||
|
end: Self::parse_position(&mut chars),
|
||||||
|
}),
|
||||||
|
chars.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => (
|
||||||
|
start.map(|start| CommandRange { start, end: None }),
|
||||||
|
chars.collect(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_position(chars: &mut Peekable<Chars>) -> Option<Position> {
|
||||||
|
match chars.peek()? {
|
||||||
|
'0'..='9' => {
|
||||||
|
let row = Self::parse_u32(chars);
|
||||||
|
Some(Position::Line {
|
||||||
|
row,
|
||||||
|
offset: Self::parse_offset(chars),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
'\'' => {
|
||||||
|
chars.next();
|
||||||
|
let name = chars.next()?;
|
||||||
|
Some(Position::Mark {
|
||||||
|
name,
|
||||||
|
offset: Self::parse_offset(chars),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
'.' => {
|
||||||
|
chars.next();
|
||||||
|
Some(Position::CurrentLine {
|
||||||
|
offset: Self::parse_offset(chars),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
'+' | '-' => Some(Position::CurrentLine {
|
||||||
|
offset: Self::parse_offset(chars),
|
||||||
|
}),
|
||||||
|
'$' => {
|
||||||
|
chars.next();
|
||||||
|
Some(Position::LastLine {
|
||||||
|
offset: Self::parse_offset(chars),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_offset(chars: &mut Peekable<Chars>) -> i32 {
|
||||||
|
let mut res: i32 = 0;
|
||||||
|
while matches!(chars.peek(), Some('+' | '-')) {
|
||||||
|
let sign = if chars.next().unwrap() == '+' { 1 } else { -1 };
|
||||||
|
let amount = if matches!(chars.peek(), Some('0'..='9')) {
|
||||||
|
(Self::parse_u32(chars) as i32).saturating_mul(sign)
|
||||||
|
} else {
|
||||||
|
sign
|
||||||
|
};
|
||||||
|
res = res.saturating_add(amount)
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_u32(chars: &mut Peekable<Chars>) -> u32 {
|
||||||
|
let mut res: u32 = 0;
|
||||||
|
while matches!(chars.peek(), Some('0'..='9')) {
|
||||||
|
res = res
|
||||||
|
.saturating_mul(10)
|
||||||
|
.saturating_add(chars.next().unwrap() as u32 - '0' as u32);
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||||
|
enum Position {
|
||||||
|
Line { row: u32, offset: i32 },
|
||||||
|
Mark { name: char, offset: i32 },
|
||||||
|
LastLine { offset: i32 },
|
||||||
|
CurrentLine { offset: i32 },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Position {
|
||||||
|
fn buffer_row(
|
||||||
|
&self,
|
||||||
|
vim: &Vim,
|
||||||
|
editor: &mut Editor,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Result<MultiBufferRow> {
|
||||||
|
let snapshot = editor.snapshot(cx);
|
||||||
|
let target = match self {
|
||||||
|
Position::Line { row, offset } => row.saturating_add_signed(offset.saturating_sub(1)),
|
||||||
|
Position::Mark { name, offset } => {
|
||||||
|
let Some(mark) = vim
|
||||||
|
.state()
|
||||||
|
.marks
|
||||||
|
.get(&name.to_string())
|
||||||
|
.and_then(|vec| vec.last())
|
||||||
|
else {
|
||||||
|
return Err(anyhow!("mark {} not set", name));
|
||||||
|
};
|
||||||
|
mark.to_point(&snapshot.buffer_snapshot)
|
||||||
|
.row
|
||||||
|
.saturating_add_signed(*offset)
|
||||||
|
}
|
||||||
|
Position::LastLine { offset } => {
|
||||||
|
snapshot.max_buffer_row().0.saturating_add_signed(*offset)
|
||||||
|
}
|
||||||
|
Position::CurrentLine { offset } => editor
|
||||||
|
.selections
|
||||||
|
.newest_anchor()
|
||||||
|
.head()
|
||||||
|
.to_point(&snapshot.buffer_snapshot)
|
||||||
|
.row
|
||||||
|
.saturating_add_signed(*offset),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(MultiBufferRow(target).min(snapshot.max_buffer_row()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||||
|
pub(crate) struct CommandRange {
|
||||||
|
start: Position,
|
||||||
|
end: Option<Position>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandRange {
|
||||||
|
fn head(&self) -> &Position {
|
||||||
|
self.end.as_ref().unwrap_or(&self.start)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn buffer_range(
|
||||||
|
&self,
|
||||||
|
vim: &Vim,
|
||||||
|
editor: &mut Editor,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Result<Range<MultiBufferRow>> {
|
||||||
|
let start = self.start.buffer_row(vim, editor, cx)?;
|
||||||
|
let end = if let Some(end) = self.end.as_ref() {
|
||||||
|
end.buffer_row(vim, editor, cx)?
|
||||||
|
} else {
|
||||||
|
start
|
||||||
|
};
|
||||||
|
if end < start {
|
||||||
|
anyhow::Ok(end..start)
|
||||||
|
} else {
|
||||||
|
anyhow::Ok(start..end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_count(&self) -> u32 {
|
||||||
|
if let CommandRange {
|
||||||
|
start: Position::Line { row, offset: 0 },
|
||||||
|
end: None,
|
||||||
|
} = &self
|
||||||
|
{
|
||||||
|
*row
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_count(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
&self,
|
||||||
|
CommandRange {
|
||||||
|
start: Position::Line { row: _, offset: 0 },
|
||||||
|
end: None
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_commands(_: &AppContext) -> Vec<VimCommand> {
|
fn generate_commands(_: &AppContext) -> Vec<VimCommand> {
|
||||||
@ -204,9 +534,9 @@ fn generate_commands(_: &AppContext) -> Vec<VimCommand> {
|
|||||||
.bang(workspace::CloseActiveItem {
|
.bang(workspace::CloseActiveItem {
|
||||||
save_intent: Some(SaveIntent::Skip),
|
save_intent: Some(SaveIntent::Skip),
|
||||||
}),
|
}),
|
||||||
VimCommand::new(("bn", "ext"), workspace::ActivateNextItem),
|
VimCommand::new(("bn", "ext"), workspace::ActivateNextItem).count(),
|
||||||
VimCommand::new(("bN", "ext"), workspace::ActivatePrevItem),
|
VimCommand::new(("bN", "ext"), workspace::ActivatePrevItem).count(),
|
||||||
VimCommand::new(("bp", "revious"), workspace::ActivatePrevItem),
|
VimCommand::new(("bp", "revious"), workspace::ActivatePrevItem).count(),
|
||||||
VimCommand::new(("bf", "irst"), workspace::ActivateItem(0)),
|
VimCommand::new(("bf", "irst"), workspace::ActivateItem(0)),
|
||||||
VimCommand::new(("br", "ewind"), workspace::ActivateItem(0)),
|
VimCommand::new(("br", "ewind"), workspace::ActivateItem(0)),
|
||||||
VimCommand::new(("bl", "ast"), workspace::ActivateLastItem),
|
VimCommand::new(("bl", "ast"), workspace::ActivateLastItem),
|
||||||
@ -220,9 +550,9 @@ fn generate_commands(_: &AppContext) -> Vec<VimCommand> {
|
|||||||
),
|
),
|
||||||
VimCommand::new(("tabe", "dit"), workspace::NewFile),
|
VimCommand::new(("tabe", "dit"), workspace::NewFile),
|
||||||
VimCommand::new(("tabnew", ""), workspace::NewFile),
|
VimCommand::new(("tabnew", ""), workspace::NewFile),
|
||||||
VimCommand::new(("tabn", "ext"), workspace::ActivateNextItem),
|
VimCommand::new(("tabn", "ext"), workspace::ActivateNextItem).count(),
|
||||||
VimCommand::new(("tabp", "revious"), workspace::ActivatePrevItem),
|
VimCommand::new(("tabp", "revious"), workspace::ActivatePrevItem).count(),
|
||||||
VimCommand::new(("tabN", "ext"), workspace::ActivatePrevItem),
|
VimCommand::new(("tabN", "ext"), workspace::ActivatePrevItem).count(),
|
||||||
VimCommand::new(
|
VimCommand::new(
|
||||||
("tabc", "lose"),
|
("tabc", "lose"),
|
||||||
workspace::CloseActiveItem {
|
workspace::CloseActiveItem {
|
||||||
@ -250,15 +580,15 @@ fn generate_commands(_: &AppContext) -> Vec<VimCommand> {
|
|||||||
VimCommand::str(("cl", "ist"), "diagnostics::Deploy"),
|
VimCommand::str(("cl", "ist"), "diagnostics::Deploy"),
|
||||||
VimCommand::new(("cc", ""), editor::actions::Hover),
|
VimCommand::new(("cc", ""), editor::actions::Hover),
|
||||||
VimCommand::new(("ll", ""), editor::actions::Hover),
|
VimCommand::new(("ll", ""), editor::actions::Hover),
|
||||||
VimCommand::new(("cn", "ext"), editor::actions::GoToDiagnostic),
|
VimCommand::new(("cn", "ext"), editor::actions::GoToDiagnostic).count(),
|
||||||
VimCommand::new(("cp", "revious"), editor::actions::GoToPrevDiagnostic),
|
VimCommand::new(("cp", "revious"), editor::actions::GoToPrevDiagnostic).count(),
|
||||||
VimCommand::new(("cN", "ext"), editor::actions::GoToPrevDiagnostic),
|
VimCommand::new(("cN", "ext"), editor::actions::GoToPrevDiagnostic).count(),
|
||||||
VimCommand::new(("lp", "revious"), editor::actions::GoToPrevDiagnostic),
|
VimCommand::new(("lp", "revious"), editor::actions::GoToPrevDiagnostic).count(),
|
||||||
VimCommand::new(("lN", "ext"), editor::actions::GoToPrevDiagnostic),
|
VimCommand::new(("lN", "ext"), editor::actions::GoToPrevDiagnostic).count(),
|
||||||
VimCommand::new(("j", "oin"), JoinLines),
|
VimCommand::new(("j", "oin"), JoinLines).range(),
|
||||||
VimCommand::new(("d", "elete"), editor::actions::DeleteLine),
|
VimCommand::new(("d", "elete"), VisualDeleteLine).range(),
|
||||||
VimCommand::new(("sor", "t"), SortLinesCaseSensitive),
|
VimCommand::new(("sor", "t"), SortLinesCaseSensitive).range(),
|
||||||
VimCommand::new(("sort i", ""), SortLinesCaseInsensitive),
|
VimCommand::new(("sort i", ""), SortLinesCaseInsensitive).range(),
|
||||||
VimCommand::str(("E", "xplore"), "project_panel::ToggleFocus"),
|
VimCommand::str(("E", "xplore"), "project_panel::ToggleFocus"),
|
||||||
VimCommand::str(("H", "explore"), "project_panel::ToggleFocus"),
|
VimCommand::str(("H", "explore"), "project_panel::ToggleFocus"),
|
||||||
VimCommand::str(("L", "explore"), "project_panel::ToggleFocus"),
|
VimCommand::str(("L", "explore"), "project_panel::ToggleFocus"),
|
||||||
@ -289,21 +619,86 @@ fn commands(cx: &AppContext) -> &Vec<VimCommand> {
|
|||||||
.0
|
.0
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn command_interceptor(mut query: &str, cx: &AppContext) -> Option<CommandInterceptResult> {
|
pub fn command_interceptor(mut input: &str, cx: &AppContext) -> Option<CommandInterceptResult> {
|
||||||
// Note: this is a very poor simulation of vim's command palette.
|
// NOTE: We also need to support passing arguments to commands like :w
|
||||||
// In the future we should adjust it to handle parsing range syntax,
|
|
||||||
// and then calling the appropriate commands with/without ranges.
|
|
||||||
//
|
|
||||||
// We also need to support passing arguments to commands like :w
|
|
||||||
// (ideally with filename autocompletion).
|
// (ideally with filename autocompletion).
|
||||||
while query.starts_with(':') {
|
while input.starts_with(':') {
|
||||||
query = &query[1..];
|
input = &input[1..];
|
||||||
|
}
|
||||||
|
|
||||||
|
let (range, query) = VimCommand::parse_range(input);
|
||||||
|
let range_prefix = input[0..(input.len() - query.len())].to_string();
|
||||||
|
let query = query.as_str();
|
||||||
|
|
||||||
|
let action = if range.is_some() && query == "" {
|
||||||
|
Some(
|
||||||
|
GoToLine {
|
||||||
|
range: range.clone().unwrap(),
|
||||||
|
}
|
||||||
|
.boxed_clone(),
|
||||||
|
)
|
||||||
|
} else if query.starts_with('/') || query.starts_with('?') {
|
||||||
|
Some(
|
||||||
|
FindCommand {
|
||||||
|
query: query[1..].to_string(),
|
||||||
|
backwards: query.starts_with('?'),
|
||||||
|
}
|
||||||
|
.boxed_clone(),
|
||||||
|
)
|
||||||
|
} else if query.starts_with('s') {
|
||||||
|
let mut substitute = "substitute".chars().peekable();
|
||||||
|
let mut query = query.chars().peekable();
|
||||||
|
while substitute
|
||||||
|
.peek()
|
||||||
|
.is_some_and(|char| Some(char) == query.peek())
|
||||||
|
{
|
||||||
|
substitute.next();
|
||||||
|
query.next();
|
||||||
|
}
|
||||||
|
if let Some(replacement) = Replacement::parse(query) {
|
||||||
|
Some(
|
||||||
|
ReplaceCommand {
|
||||||
|
replacement,
|
||||||
|
range: range.clone(),
|
||||||
|
}
|
||||||
|
.boxed_clone(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
if let Some(action) = action {
|
||||||
|
let string = input.to_string();
|
||||||
|
let positions = generate_positions(&string, &(range_prefix + query));
|
||||||
|
return Some(CommandInterceptResult {
|
||||||
|
action,
|
||||||
|
string,
|
||||||
|
positions,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for command in commands(cx).iter() {
|
for command in commands(cx).iter() {
|
||||||
if let Some(action) = command.parse(query, cx) {
|
if let Some(action) = command.parse(&query, cx) {
|
||||||
let string = ":".to_owned() + command.prefix + command.suffix;
|
let string = ":".to_owned() + &range_prefix + command.prefix + command.suffix;
|
||||||
let positions = generate_positions(&string, query);
|
let positions = generate_positions(&string, &(range_prefix + query));
|
||||||
|
|
||||||
|
if let Some(range) = &range {
|
||||||
|
if command.has_range || (range.is_count() && command.has_count) {
|
||||||
|
return Some(CommandInterceptResult {
|
||||||
|
action: Box::new(WithRange {
|
||||||
|
is_count: command.has_count,
|
||||||
|
range: range.clone(),
|
||||||
|
action,
|
||||||
|
}),
|
||||||
|
string,
|
||||||
|
positions,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Some(CommandInterceptResult {
|
return Some(CommandInterceptResult {
|
||||||
action,
|
action,
|
||||||
@ -312,46 +707,7 @@ pub fn command_interceptor(mut query: &str, cx: &AppContext) -> Option<CommandIn
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
None
|
||||||
let (name, action) = if query.starts_with('/') || query.starts_with('?') {
|
|
||||||
(
|
|
||||||
query,
|
|
||||||
FindCommand {
|
|
||||||
query: query[1..].to_string(),
|
|
||||||
backwards: query.starts_with('?'),
|
|
||||||
}
|
|
||||||
.boxed_clone(),
|
|
||||||
)
|
|
||||||
} else if query.starts_with('%') {
|
|
||||||
(
|
|
||||||
query,
|
|
||||||
ReplaceCommand {
|
|
||||||
query: query.to_string(),
|
|
||||||
}
|
|
||||||
.boxed_clone(),
|
|
||||||
)
|
|
||||||
} else if let Ok(line) = query.parse::<u32>() {
|
|
||||||
(query, GoToLine { line }.boxed_clone())
|
|
||||||
} else if range_regex().is_match(query) {
|
|
||||||
(
|
|
||||||
query,
|
|
||||||
ReplaceCommand {
|
|
||||||
query: query.to_string(),
|
|
||||||
}
|
|
||||||
.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> {
|
fn generate_positions(string: &str, query: &str) -> Vec<usize> {
|
||||||
@ -506,4 +862,59 @@ mod test {
|
|||||||
cx.simulate_keystrokes(": q a enter");
|
cx.simulate_keystrokes(": q a enter");
|
||||||
cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 0));
|
cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_offsets(cx: &mut TestAppContext) {
|
||||||
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
|
||||||
|
cx.set_shared_state("ˇ1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n")
|
||||||
|
.await;
|
||||||
|
|
||||||
|
cx.simulate_shared_keystrokes(": + enter").await;
|
||||||
|
cx.shared_state()
|
||||||
|
.await
|
||||||
|
.assert_eq("1\nˇ2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n");
|
||||||
|
|
||||||
|
cx.simulate_shared_keystrokes(": 1 0 - enter").await;
|
||||||
|
cx.shared_state()
|
||||||
|
.await
|
||||||
|
.assert_eq("1\n2\n3\n4\n5\n6\n7\n8\nˇ9\n10\n11\n");
|
||||||
|
|
||||||
|
cx.simulate_shared_keystrokes(": . - 2 enter").await;
|
||||||
|
cx.shared_state()
|
||||||
|
.await
|
||||||
|
.assert_eq("1\n2\n3\n4\n5\n6\nˇ7\n8\n9\n10\n11\n");
|
||||||
|
|
||||||
|
cx.simulate_shared_keystrokes(": % enter").await;
|
||||||
|
cx.shared_state()
|
||||||
|
.await
|
||||||
|
.assert_eq("1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\nˇ");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_command_ranges(cx: &mut TestAppContext) {
|
||||||
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
|
||||||
|
cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await;
|
||||||
|
|
||||||
|
cx.simulate_shared_keystrokes(": 2 , 4 d enter").await;
|
||||||
|
cx.shared_state().await.assert_eq("1\nˇ4\n3\n2\n1");
|
||||||
|
|
||||||
|
cx.simulate_shared_keystrokes(": 2 , 4 s o r t enter").await;
|
||||||
|
cx.shared_state().await.assert_eq("1\nˇ2\n3\n4\n1");
|
||||||
|
|
||||||
|
cx.simulate_shared_keystrokes(": 2 , 4 j o i n enter").await;
|
||||||
|
cx.shared_state().await.assert_eq("1\nˇ2 3 4\n1");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_command_visual_replace(cx: &mut TestAppContext) {
|
||||||
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
|
||||||
|
cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await;
|
||||||
|
|
||||||
|
cx.simulate_shared_keystrokes("v 2 j : s / . / k enter")
|
||||||
|
.await;
|
||||||
|
cx.shared_state().await.assert_eq("k\nk\nˇk\n4\n4\n3\n2\n1");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,6 +49,7 @@ fn blurred(editor: View<Editor>, cx: &mut WindowContext) {
|
|||||||
.upgrade()
|
.upgrade()
|
||||||
.is_some_and(|previous| previous == editor.clone())
|
.is_some_and(|previous| previous == editor.clone())
|
||||||
{
|
{
|
||||||
|
vim.store_visual_marks(cx);
|
||||||
vim.clear_operator(cx);
|
vim.clear_operator(cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
use std::{ops::Range, sync::OnceLock, time::Duration};
|
use std::{iter::Peekable, str::Chars, time::Duration};
|
||||||
|
|
||||||
use gpui::{actions, impl_actions, ViewContext};
|
use gpui::{actions, impl_actions, ViewContext};
|
||||||
use language::Point;
|
use language::Point;
|
||||||
use multi_buffer::MultiBufferRow;
|
|
||||||
use regex::Regex;
|
|
||||||
use search::{buffer_search, BufferSearchBar, SearchOptions};
|
use search::{buffer_search, BufferSearchBar, SearchOptions};
|
||||||
use serde_derive::Deserialize;
|
use serde_derive::Deserialize;
|
||||||
use workspace::{searchable::Direction, Workspace};
|
use workspace::{notifications::NotifyResultExt, searchable::Direction, Workspace};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
command::CommandRange,
|
||||||
motion::{search_motion, Motion},
|
motion::{search_motion, Motion},
|
||||||
normal::move_cursor,
|
normal::move_cursor,
|
||||||
state::{Mode, SearchState},
|
state::{Mode, SearchState},
|
||||||
@ -43,16 +42,16 @@ pub struct FindCommand {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||||
pub struct ReplaceCommand {
|
pub struct ReplaceCommand {
|
||||||
pub query: String,
|
pub(crate) range: Option<CommandRange>,
|
||||||
|
pub(crate) replacement: Replacement,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default, PartialEq, Deserialize, Clone)]
|
||||||
struct Replacement {
|
pub(crate) struct Replacement {
|
||||||
search: String,
|
search: String,
|
||||||
replacement: String,
|
replacement: String,
|
||||||
should_replace_all: bool,
|
should_replace_all: bool,
|
||||||
is_case_sensitive: bool,
|
is_case_sensitive: bool,
|
||||||
range: Option<Range<usize>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
actions!(vim, [SearchSubmit, MoveToNextMatch, MoveToPrevMatch]);
|
actions!(vim, [SearchSubmit, MoveToNextMatch, MoveToPrevMatch]);
|
||||||
@ -61,11 +60,6 @@ impl_actions!(
|
|||||||
[FindCommand, ReplaceCommand, Search, MoveToPrev, MoveToNext]
|
[FindCommand, ReplaceCommand, Search, MoveToPrev, MoveToNext]
|
||||||
);
|
);
|
||||||
|
|
||||||
static RANGE_REGEX: OnceLock<Regex> = OnceLock::new();
|
|
||||||
pub(crate) fn range_regex() -> &'static Regex {
|
|
||||||
RANGE_REGEX.get_or_init(|| Regex::new(r"^(\d+),(\d+)s(.*)").unwrap())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||||
workspace.register_action(move_to_next);
|
workspace.register_action(move_to_next);
|
||||||
workspace.register_action(move_to_prev);
|
workspace.register_action(move_to_prev);
|
||||||
@ -354,23 +348,25 @@ fn replace_command(
|
|||||||
action: &ReplaceCommand,
|
action: &ReplaceCommand,
|
||||||
cx: &mut ViewContext<Workspace>,
|
cx: &mut ViewContext<Workspace>,
|
||||||
) {
|
) {
|
||||||
let replacement = parse_replace_all(&action.query);
|
let replacement = action.replacement.clone();
|
||||||
let pane = workspace.active_pane().clone();
|
let pane = workspace.active_pane().clone();
|
||||||
let mut editor = Vim::read(cx)
|
let editor = Vim::read(cx)
|
||||||
.active_editor
|
.active_editor
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|editor| editor.upgrade());
|
.and_then(|editor| editor.upgrade());
|
||||||
if let Some(range) = &replacement.range {
|
if let Some(range) = &action.range {
|
||||||
if let Some(editor) = editor.as_mut() {
|
if let Some(result) = Vim::update(cx, |vim, cx| {
|
||||||
editor.update(cx, |editor, cx| {
|
vim.update_active_editor(cx, |vim, editor, cx| {
|
||||||
|
let range = range.buffer_range(vim, editor, cx)?;
|
||||||
let snapshot = &editor.snapshot(cx).buffer_snapshot;
|
let snapshot = &editor.snapshot(cx).buffer_snapshot;
|
||||||
let end_row = MultiBufferRow(range.end.saturating_sub(1) as u32);
|
let end_point = Point::new(range.end.0, snapshot.line_len(range.end));
|
||||||
let end_point = Point::new(end_row.0, snapshot.line_len(end_row));
|
let range = snapshot.anchor_before(Point::new(range.start.0, 0))
|
||||||
let range = snapshot
|
|
||||||
.anchor_before(Point::new(range.start.saturating_sub(1) as u32, 0))
|
|
||||||
..snapshot.anchor_after(end_point);
|
..snapshot.anchor_after(end_point);
|
||||||
editor.set_search_within_ranges(&[range], cx)
|
editor.set_search_within_ranges(&[range], cx);
|
||||||
|
anyhow::Ok(())
|
||||||
})
|
})
|
||||||
|
}) {
|
||||||
|
result.notify_err(workspace, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pane.update(cx, |pane, cx| {
|
pane.update(cx, |pane, cx| {
|
||||||
@ -432,95 +428,81 @@ fn replace_command(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// convert a vim query into something more usable by zed.
|
impl Replacement {
|
||||||
// we don't attempt to fully convert between the two regex syntaxes,
|
// convert a vim query into something more usable by zed.
|
||||||
// but we do flip \( and \) to ( and ) (and vice-versa) in the pattern,
|
// we don't attempt to fully convert between the two regex syntaxes,
|
||||||
// and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
|
// but we do flip \( and \) to ( and ) (and vice-versa) in the pattern,
|
||||||
fn parse_replace_all(query: &str) -> Replacement {
|
// and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
|
||||||
let mut chars = query.chars();
|
pub(crate) fn parse(mut chars: Peekable<Chars>) -> Option<Replacement> {
|
||||||
let mut range = None;
|
let Some(delimiter) = chars
|
||||||
let maybe_line_range_and_rest: Option<(Range<usize>, &str)> =
|
.next()
|
||||||
range_regex().captures(query).map(|captures| {
|
.filter(|c| !c.is_alphanumeric() && *c != '"' && *c != '|' && *c != '\'')
|
||||||
(
|
else {
|
||||||
captures.get(1).unwrap().as_str().parse().unwrap()
|
return None;
|
||||||
..captures.get(2).unwrap().as_str().parse().unwrap(),
|
};
|
||||||
captures.get(3).unwrap().as_str(),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
if maybe_line_range_and_rest.is_some() {
|
|
||||||
let (line_range, rest) = maybe_line_range_and_rest.unwrap();
|
|
||||||
range = Some(line_range);
|
|
||||||
chars = rest.chars();
|
|
||||||
} else if Some('%') != chars.next() || Some('s') != chars.next() {
|
|
||||||
return Replacement::default();
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(delimiter) = chars.next() else {
|
let mut search = String::new();
|
||||||
return Replacement::default();
|
let mut replacement = String::new();
|
||||||
};
|
let mut flags = String::new();
|
||||||
|
|
||||||
let mut search = String::new();
|
let mut buffer = &mut search;
|
||||||
let mut replacement = String::new();
|
|
||||||
let mut flags = String::new();
|
|
||||||
|
|
||||||
let mut buffer = &mut search;
|
let mut escaped = false;
|
||||||
|
// 0 - parsing search
|
||||||
|
// 1 - parsing replacement
|
||||||
|
// 2 - parsing flags
|
||||||
|
let mut phase = 0;
|
||||||
|
|
||||||
let mut escaped = false;
|
for c in chars {
|
||||||
// 0 - parsing search
|
if escaped {
|
||||||
// 1 - parsing replacement
|
escaped = false;
|
||||||
// 2 - parsing flags
|
if phase == 1 && c.is_digit(10) {
|
||||||
let mut phase = 0;
|
buffer.push('$')
|
||||||
|
// unescape escaped parens
|
||||||
for c in chars {
|
} else if phase == 0 && c == '(' || c == ')' {
|
||||||
if escaped {
|
} else if c != delimiter {
|
||||||
escaped = false;
|
buffer.push('\\')
|
||||||
if phase == 1 && c.is_digit(10) {
|
}
|
||||||
buffer.push('$')
|
buffer.push(c)
|
||||||
// unescape escaped parens
|
} else if c == '\\' {
|
||||||
} else if phase == 0 && c == '(' || c == ')' {
|
escaped = true;
|
||||||
} else if c != delimiter {
|
} else if c == delimiter {
|
||||||
buffer.push('\\')
|
if phase == 0 {
|
||||||
}
|
buffer = &mut replacement;
|
||||||
buffer.push(c)
|
phase = 1;
|
||||||
} else if c == '\\' {
|
} else if phase == 1 {
|
||||||
escaped = true;
|
buffer = &mut flags;
|
||||||
} else if c == delimiter {
|
phase = 2;
|
||||||
if phase == 0 {
|
} else {
|
||||||
buffer = &mut replacement;
|
break;
|
||||||
phase = 1;
|
}
|
||||||
} else if phase == 1 {
|
|
||||||
buffer = &mut flags;
|
|
||||||
phase = 2;
|
|
||||||
} else {
|
} else {
|
||||||
break;
|
// escape unescaped parens
|
||||||
|
if phase == 0 && c == '(' || c == ')' {
|
||||||
|
buffer.push('\\')
|
||||||
|
}
|
||||||
|
buffer.push(c)
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
// escape unescaped parens
|
|
||||||
if phase == 0 && c == '(' || c == ')' {
|
let mut replacement = Replacement {
|
||||||
buffer.push('\\')
|
search,
|
||||||
|
replacement,
|
||||||
|
should_replace_all: true,
|
||||||
|
is_case_sensitive: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
for c in flags.chars() {
|
||||||
|
match c {
|
||||||
|
'g' | 'I' => {}
|
||||||
|
'c' | 'n' => replacement.should_replace_all = false,
|
||||||
|
'i' => replacement.is_case_sensitive = false,
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
buffer.push(c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Some(replacement)
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut replacement = Replacement {
|
|
||||||
search,
|
|
||||||
replacement,
|
|
||||||
should_replace_all: true,
|
|
||||||
is_case_sensitive: true,
|
|
||||||
range,
|
|
||||||
};
|
|
||||||
|
|
||||||
for c in flags.chars() {
|
|
||||||
match c {
|
|
||||||
'g' | 'I' => {}
|
|
||||||
'c' | 'n' => replacement.should_replace_all = false,
|
|
||||||
'i' => replacement.is_case_sensitive = false,
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
replacement
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
28
crates/vim/test_data/test_command_ranges.json
Normal file
28
crates/vim/test_data/test_command_ranges.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{"Put":{"state":"ˇ1\n2\n3\n4\n4\n3\n2\n1"}}
|
||||||
|
{"Key":":"}
|
||||||
|
{"Key":"2"}
|
||||||
|
{"Key":","}
|
||||||
|
{"Key":"4"}
|
||||||
|
{"Key":"d"}
|
||||||
|
{"Key":"enter"}
|
||||||
|
{"Get":{"state":"1\nˇ4\n3\n2\n1","mode":"Normal"}}
|
||||||
|
{"Key":":"}
|
||||||
|
{"Key":"2"}
|
||||||
|
{"Key":","}
|
||||||
|
{"Key":"4"}
|
||||||
|
{"Key":"s"}
|
||||||
|
{"Key":"o"}
|
||||||
|
{"Key":"r"}
|
||||||
|
{"Key":"t"}
|
||||||
|
{"Key":"enter"}
|
||||||
|
{"Get":{"state":"1\nˇ2\n3\n4\n1","mode":"Normal"}}
|
||||||
|
{"Key":":"}
|
||||||
|
{"Key":"2"}
|
||||||
|
{"Key":","}
|
||||||
|
{"Key":"4"}
|
||||||
|
{"Key":"j"}
|
||||||
|
{"Key":"o"}
|
||||||
|
{"Key":"i"}
|
||||||
|
{"Key":"n"}
|
||||||
|
{"Key":"enter"}
|
||||||
|
{"Get":{"state":"1\nˇ2 3 4\n1","mode":"Normal"}}
|
12
crates/vim/test_data/test_command_visual_replace.json
Normal file
12
crates/vim/test_data/test_command_visual_replace.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{"Put":{"state":"ˇ1\n2\n3\n4\n4\n3\n2\n1"}}
|
||||||
|
{"Key":"v"}
|
||||||
|
{"Key":"2"}
|
||||||
|
{"Key":"j"}
|
||||||
|
{"Key":":"}
|
||||||
|
{"Key":"s"}
|
||||||
|
{"Key":"/"}
|
||||||
|
{"Key":"."}
|
||||||
|
{"Key":"/"}
|
||||||
|
{"Key":"k"}
|
||||||
|
{"Key":"enter"}
|
||||||
|
{"Get":{"state":"k\nk\nˇk\n4\n4\n3\n2\n1","mode":"Normal"}}
|
21
crates/vim/test_data/test_offsets.json
Normal file
21
crates/vim/test_data/test_offsets.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{"Put":{"state":"ˇ1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n"}}
|
||||||
|
{"Key":":"}
|
||||||
|
{"Key":"+"}
|
||||||
|
{"Key":"enter"}
|
||||||
|
{"Get":{"state":"1\nˇ2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n","mode":"Normal"}}
|
||||||
|
{"Key":":"}
|
||||||
|
{"Key":"1"}
|
||||||
|
{"Key":"0"}
|
||||||
|
{"Key":"-"}
|
||||||
|
{"Key":"enter"}
|
||||||
|
{"Get":{"state":"1\n2\n3\n4\n5\n6\n7\n8\nˇ9\n10\n11\n","mode":"Normal"}}
|
||||||
|
{"Key":":"}
|
||||||
|
{"Key":"."}
|
||||||
|
{"Key":"-"}
|
||||||
|
{"Key":"2"}
|
||||||
|
{"Key":"enter"}
|
||||||
|
{"Get":{"state":"1\n2\n3\n4\n5\n6\nˇ7\n8\n9\n10\n11\n","mode":"Normal"}}
|
||||||
|
{"Key":":"}
|
||||||
|
{"Key":"%"}
|
||||||
|
{"Key":"enter"}
|
||||||
|
{"Get":{"state":"1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\nˇ","mode":"Normal"}}
|
Loading…
Reference in New Issue
Block a user