@@ -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 editor::actions::{SortLinesCaseInsensitive, SortLinesCaseSensitive};
-use gpui::{impl_actions, Action, AppContext, Global, ViewContext};
-use serde_derive::Deserialize;
+use editor::{
+ actions::{SortLinesCaseInsensitive, SortLinesCaseSensitive},
+ 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 workspace::{SaveIntent, Workspace};
+use workspace::{notifications::NotifyResultExt, SaveIntent, Workspace};
use crate::{
motion::{EndOfDocument, Motion, StartOfDocument},
normal::{
move_cursor,
- search::{range_regex, FindCommand, ReplaceCommand},
+ search::{FindCommand, ReplaceCommand, Replacement},
JoinLines,
},
state::Mode,
+ visual::VisualDeleteLine,
Vim,
};
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct GoToLine {
- pub line: u32,
+ range: CommandRange,
+}
+
+#[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_actions!(vim, [GoToLine]);
+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>) {
- 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.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 {
prefix: &'static str,
suffix: &'static str,
action: Option<Box<dyn Action>>,
action_name: Option<&'static str>,
bang_action: Option<Box<dyn Action>>,
+ has_range: bool,
+ has_count: bool,
}
impl VimCommand {
@@ -48,8 +157,7 @@ impl VimCommand {
prefix: pattern.0,
suffix: pattern.1,
action: Some(action.boxed_clone()),
- action_name: None,
- bang_action: None,
+ ..Default::default()
}
}
@@ -58,9 +166,8 @@ impl VimCommand {
Self {
prefix: pattern.0,
suffix: pattern.1,
- action: None,
action_name: Some(action_name),
- bang_action: None,
+ ..Default::default()
}
}
@@ -69,6 +176,15 @@ impl VimCommand {
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>> {
let has_bang = query.ends_with('!');
if has_bang {
@@ -92,6 +208,220 @@ impl VimCommand {
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> {
@@ -204,9 +534,9 @@ fn generate_commands(_: &AppContext) -> Vec<VimCommand> {
.bang(workspace::CloseActiveItem {
save_intent: Some(SaveIntent::Skip),
}),
- VimCommand::new(("bn", "ext"), workspace::ActivateNextItem),
- VimCommand::new(("bN", "ext"), workspace::ActivatePrevItem),
- VimCommand::new(("bp", "revious"), workspace::ActivatePrevItem),
+ VimCommand::new(("bn", "ext"), workspace::ActivateNextItem).count(),
+ VimCommand::new(("bN", "ext"), workspace::ActivatePrevItem).count(),
+ VimCommand::new(("bp", "revious"), workspace::ActivatePrevItem).count(),
VimCommand::new(("bf", "irst"), workspace::ActivateItem(0)),
VimCommand::new(("br", "ewind"), workspace::ActivateItem(0)),
VimCommand::new(("bl", "ast"), workspace::ActivateLastItem),
@@ -220,9 +550,9 @@ fn generate_commands(_: &AppContext) -> Vec<VimCommand> {
),
VimCommand::new(("tabe", "dit"), workspace::NewFile),
VimCommand::new(("tabnew", ""), workspace::NewFile),
- VimCommand::new(("tabn", "ext"), workspace::ActivateNextItem),
- VimCommand::new(("tabp", "revious"), workspace::ActivatePrevItem),
- VimCommand::new(("tabN", "ext"), workspace::ActivatePrevItem),
+ VimCommand::new(("tabn", "ext"), workspace::ActivateNextItem).count(),
+ VimCommand::new(("tabp", "revious"), workspace::ActivatePrevItem).count(),
+ VimCommand::new(("tabN", "ext"), workspace::ActivatePrevItem).count(),
VimCommand::new(
("tabc", "lose"),
workspace::CloseActiveItem {
@@ -250,15 +580,15 @@ fn generate_commands(_: &AppContext) -> Vec<VimCommand> {
VimCommand::str(("cl", "ist"), "diagnostics::Deploy"),
VimCommand::new(("cc", ""), editor::actions::Hover),
VimCommand::new(("ll", ""), editor::actions::Hover),
- VimCommand::new(("cn", "ext"), editor::actions::GoToDiagnostic),
- VimCommand::new(("cp", "revious"), editor::actions::GoToPrevDiagnostic),
- VimCommand::new(("cN", "ext"), editor::actions::GoToPrevDiagnostic),
- VimCommand::new(("lp", "revious"), editor::actions::GoToPrevDiagnostic),
- VimCommand::new(("lN", "ext"), editor::actions::GoToPrevDiagnostic),
- VimCommand::new(("j", "oin"), JoinLines),
- VimCommand::new(("d", "elete"), editor::actions::DeleteLine),
- VimCommand::new(("sor", "t"), SortLinesCaseSensitive),
- VimCommand::new(("sort i", ""), SortLinesCaseInsensitive),
+ VimCommand::new(("cn", "ext"), editor::actions::GoToDiagnostic).count(),
+ VimCommand::new(("cp", "revious"), editor::actions::GoToPrevDiagnostic).count(),
+ VimCommand::new(("cN", "ext"), editor::actions::GoToPrevDiagnostic).count(),
+ VimCommand::new(("lp", "revious"), editor::actions::GoToPrevDiagnostic).count(),
+ VimCommand::new(("lN", "ext"), editor::actions::GoToPrevDiagnostic).count(),
+ VimCommand::new(("j", "oin"), JoinLines).range(),
+ VimCommand::new(("d", "elete"), VisualDeleteLine).range(),
+ VimCommand::new(("sor", "t"), SortLinesCaseSensitive).range(),
+ VimCommand::new(("sort i", ""), SortLinesCaseInsensitive).range(),
VimCommand::str(("E", "xplore"), "project_panel::ToggleFocus"),
VimCommand::str(("H", "explore"), "project_panel::ToggleFocus"),
VimCommand::str(("L", "explore"), "project_panel::ToggleFocus"),
@@ -289,69 +619,95 @@ fn commands(cx: &AppContext) -> &Vec<VimCommand> {
.0
}
-pub fn command_interceptor(mut query: &str, cx: &AppContext) -> Option<CommandInterceptResult> {
- // Note: this is a very poor simulation of vim's command palette.
- // 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
+pub fn command_interceptor(mut input: &str, cx: &AppContext) -> Option<CommandInterceptResult> {
+ // NOTE: We also need to support passing arguments to commands like :w
// (ideally with filename autocompletion).
- while query.starts_with(':') {
- query = &query[1..];
+ while input.starts_with(':') {
+ input = &input[1..];
}
- for command in commands(cx).iter() {
- if let Some(action) = command.parse(query, cx) {
- let string = ":".to_owned() + command.prefix + command.suffix;
- let positions = generate_positions(&string, query);
-
- return Some(CommandInterceptResult {
- action,
- string,
- positions,
- });
- }
- }
+ let (range, query) = VimCommand::parse_range(input);
+ let range_prefix = input[0..(input.len() - query.len())].to_string();
+ let query = query.as_str();
- let (name, action) = if query.starts_with('/') || query.starts_with('?') {
- (
- query,
- FindCommand {
- query: query[1..].to_string(),
- backwards: query.starts_with('?'),
+ let action = if range.is_some() && query == "" {
+ Some(
+ GoToLine {
+ range: range.clone().unwrap(),
}
.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(),
+ } 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 {
- return None;
+ 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,
+ });
+ }
- let string = ":".to_owned() + name;
- let positions = generate_positions(&string, query);
+ for command in commands(cx).iter() {
+ if let Some(action) = command.parse(&query, cx) {
+ let string = ":".to_owned() + &range_prefix + command.prefix + command.suffix;
+ 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;
+ }
+ }
- Some(CommandInterceptResult {
- action,
- string,
- positions,
- })
+ return Some(CommandInterceptResult {
+ action,
+ string,
+ positions,
+ });
+ }
+ }
+ None
}
fn generate_positions(string: &str, query: &str) -> Vec<usize> {
@@ -506,4 +862,59 @@ mod test {
cx.simulate_keystrokes(": q a enter");
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");
+ }
}
@@ -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 language::Point;
-use multi_buffer::MultiBufferRow;
-use regex::Regex;
use search::{buffer_search, BufferSearchBar, SearchOptions};
use serde_derive::Deserialize;
-use workspace::{searchable::Direction, Workspace};
+use workspace::{notifications::NotifyResultExt, searchable::Direction, Workspace};
use crate::{
+ command::CommandRange,
motion::{search_motion, Motion},
normal::move_cursor,
state::{Mode, SearchState},
@@ -43,16 +42,16 @@ pub struct FindCommand {
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct ReplaceCommand {
- pub query: String,
+ pub(crate) range: Option<CommandRange>,
+ pub(crate) replacement: Replacement,
}
-#[derive(Debug, Default)]
-struct Replacement {
+#[derive(Debug, Default, PartialEq, Deserialize, Clone)]
+pub(crate) struct Replacement {
search: String,
replacement: String,
should_replace_all: bool,
is_case_sensitive: bool,
- range: Option<Range<usize>>,
}
actions!(vim, [SearchSubmit, MoveToNextMatch, MoveToPrevMatch]);
@@ -61,11 +60,6 @@ impl_actions!(
[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>) {
workspace.register_action(move_to_next);
workspace.register_action(move_to_prev);
@@ -354,23 +348,25 @@ fn replace_command(
action: &ReplaceCommand,
cx: &mut ViewContext<Workspace>,
) {
- let replacement = parse_replace_all(&action.query);
+ let replacement = action.replacement.clone();
let pane = workspace.active_pane().clone();
- let mut editor = Vim::read(cx)
+ let editor = Vim::read(cx)
.active_editor
.as_ref()
.and_then(|editor| editor.upgrade());
- if let Some(range) = &replacement.range {
- if let Some(editor) = editor.as_mut() {
- editor.update(cx, |editor, cx| {
+ if let Some(range) = &action.range {
+ if let Some(result) = Vim::update(cx, |vim, 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 end_row = MultiBufferRow(range.end.saturating_sub(1) as u32);
- let end_point = Point::new(end_row.0, snapshot.line_len(end_row));
- let range = snapshot
- .anchor_before(Point::new(range.start.saturating_sub(1) as u32, 0))
+ let end_point = Point::new(range.end.0, snapshot.line_len(range.end));
+ let range = snapshot.anchor_before(Point::new(range.start.0, 0))
..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| {
@@ -432,95 +428,81 @@ fn replace_command(
})
}
-// convert a vim query into something more usable by zed.
-// we don't attempt to fully convert between the two regex syntaxes,
-// but we do flip \( and \) to ( and ) (and vice-versa) in the pattern,
-// and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
-fn parse_replace_all(query: &str) -> Replacement {
- let mut chars = query.chars();
- let mut range = None;
- let maybe_line_range_and_rest: Option<(Range<usize>, &str)> =
- range_regex().captures(query).map(|captures| {
- (
- captures.get(1).unwrap().as_str().parse().unwrap()
- ..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 {
- return Replacement::default();
- };
+impl Replacement {
+ // convert a vim query into something more usable by zed.
+ // we don't attempt to fully convert between the two regex syntaxes,
+ // but we do flip \( and \) to ( and ) (and vice-versa) in the pattern,
+ // and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
+ pub(crate) fn parse(mut chars: Peekable<Chars>) -> Option<Replacement> {
+ let Some(delimiter) = chars
+ .next()
+ .filter(|c| !c.is_alphanumeric() && *c != '"' && *c != '|' && *c != '\'')
+ else {
+ return None;
+ };
- let mut search = String::new();
- 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;
-
- for c in chars {
- if escaped {
- escaped = false;
- if phase == 1 && c.is_digit(10) {
- buffer.push('$')
- // unescape escaped parens
- } else if phase == 0 && c == '(' || c == ')' {
- } else if c != delimiter {
- buffer.push('\\')
- }
- buffer.push(c)
- } else if c == '\\' {
- escaped = true;
- } else if c == delimiter {
- if phase == 0 {
- buffer = &mut replacement;
- phase = 1;
- } else if phase == 1 {
- buffer = &mut flags;
- phase = 2;
+ let mut search = String::new();
+ 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;
+
+ for c in chars {
+ if escaped {
+ escaped = false;
+ if phase == 1 && c.is_digit(10) {
+ buffer.push('$')
+ // unescape escaped parens
+ } else if phase == 0 && c == '(' || c == ')' {
+ } else if c != delimiter {
+ buffer.push('\\')
+ }
+ buffer.push(c)
+ } else if c == '\\' {
+ escaped = true;
+ } else if c == delimiter {
+ if phase == 0 {
+ buffer = &mut replacement;
+ phase = 1;
+ } else if phase == 1 {
+ buffer = &mut flags;
+ phase = 2;
+ } else {
+ break;
+ }
} else {
- break;
- }
- } else {
- // escape unescaped parens
- if phase == 0 && c == '(' || c == ')' {
- buffer.push('\\')
+ // escape unescaped parens
+ if phase == 0 && c == '(' || c == ')' {
+ buffer.push('\\')
+ }
+ buffer.push(c)
}
- buffer.push(c)
}
- }
- let mut replacement = Replacement {
- search,
- replacement,
- should_replace_all: true,
- is_case_sensitive: true,
- range,
- };
+ let mut replacement = Replacement {
+ 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,
- _ => {}
+ for c in flags.chars() {
+ match c {
+ 'g' | 'I' => {}
+ 'c' | 'n' => replacement.should_replace_all = false,
+ 'i' => replacement.is_case_sensitive = false,
+ _ => {}
+ }
}
- }
- replacement
+ Some(replacement)
+ }
}
#[cfg(test)]