mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-19 18:41:56 +03:00
support vim replace command with range (#10709)
Release Notes: - Added support for line ranges in vim replace commands #9428 - not supporting anything other than bare line numbers right now - ~need to figure out how to show range in question in search bar~ @ConradIrwin implemented showing a highlight of the selected range for a short direction instead - ~tests lol~
This commit is contained in:
parent
0697b417a0
commit
bc736265be
@ -278,6 +278,8 @@ pub fn init(cx: &mut AppContext) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct SearchWithinRange;
|
||||||
|
|
||||||
trait InvalidationRegion {
|
trait InvalidationRegion {
|
||||||
fn ranges(&self) -> &[Range<Anchor>];
|
fn ranges(&self) -> &[Range<Anchor>];
|
||||||
}
|
}
|
||||||
@ -9264,6 +9266,18 @@ impl Editor {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_search_within_ranges(
|
||||||
|
&mut self,
|
||||||
|
ranges: &[Range<Anchor>],
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
self.highlight_background::<SearchWithinRange>(
|
||||||
|
ranges,
|
||||||
|
|colors| colors.editor_document_highlight_read_background,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn highlight_background<T: 'static>(
|
pub fn highlight_background<T: 'static>(
|
||||||
&mut self,
|
&mut self,
|
||||||
ranges: &[Range<Anchor>],
|
ranges: &[Range<Anchor>],
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
editor_settings::SeedQuerySetting, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll,
|
editor_settings::SeedQuerySetting, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll,
|
||||||
Editor, EditorEvent, EditorSettings, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot,
|
Editor, EditorEvent, EditorSettings, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot,
|
||||||
NavigationData, ToPoint as _,
|
NavigationData, SearchWithinRange, ToPoint as _,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Context as _, Result};
|
use anyhow::{anyhow, Context as _, Result};
|
||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
@ -16,12 +16,14 @@ use language::{
|
|||||||
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, OffsetRangeExt,
|
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, OffsetRangeExt,
|
||||||
Point, SelectionGoal,
|
Point, SelectionGoal,
|
||||||
};
|
};
|
||||||
|
use multi_buffer::AnchorRangeExt;
|
||||||
use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath};
|
use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath};
|
||||||
use rpc::proto::{self, update_view, PeerId};
|
use rpc::proto::{self, update_view, PeerId};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use workspace::item::{ItemSettings, TabContentParams};
|
use workspace::item::{ItemSettings, TabContentParams};
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
any::TypeId,
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
cmp::{self, Ordering},
|
cmp::{self, Ordering},
|
||||||
iter,
|
iter,
|
||||||
@ -999,6 +1001,10 @@ impl SearchableItem for Editor {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn has_filtered_search_ranges(&mut self) -> bool {
|
||||||
|
self.has_background_highlights::<SearchWithinRange>()
|
||||||
|
}
|
||||||
|
|
||||||
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
|
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
|
||||||
let setting = EditorSettings::get_global(cx).seed_search_query_from_cursor;
|
let setting = EditorSettings::get_global(cx).seed_search_query_from_cursor;
|
||||||
let snapshot = &self.snapshot(cx).buffer_snapshot;
|
let snapshot = &self.snapshot(cx).buffer_snapshot;
|
||||||
@ -1123,18 +1129,37 @@ impl SearchableItem for Editor {
|
|||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Task<Vec<Range<Anchor>>> {
|
) -> Task<Vec<Range<Anchor>>> {
|
||||||
let buffer = self.buffer().read(cx).snapshot(cx);
|
let buffer = self.buffer().read(cx).snapshot(cx);
|
||||||
|
let search_within_ranges = self
|
||||||
|
.background_highlights
|
||||||
|
.get(&TypeId::of::<SearchWithinRange>())
|
||||||
|
.map(|(_color, ranges)| {
|
||||||
|
ranges
|
||||||
|
.iter()
|
||||||
|
.map(|range| range.to_offset(&buffer))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
});
|
||||||
cx.background_executor().spawn(async move {
|
cx.background_executor().spawn(async move {
|
||||||
let mut ranges = Vec::new();
|
let mut ranges = Vec::new();
|
||||||
if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() {
|
if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() {
|
||||||
ranges.extend(
|
if let Some(search_within_ranges) = search_within_ranges {
|
||||||
query
|
for range in search_within_ranges {
|
||||||
.search(excerpt_buffer, None)
|
let offset = range.start;
|
||||||
.await
|
ranges.extend(
|
||||||
.into_iter()
|
query
|
||||||
.map(|range| {
|
.search(excerpt_buffer, Some(range))
|
||||||
buffer.anchor_after(range.start)..buffer.anchor_before(range.end)
|
.await
|
||||||
}),
|
.into_iter()
|
||||||
);
|
.map(|range| {
|
||||||
|
buffer.anchor_after(range.start + offset)
|
||||||
|
..buffer.anchor_before(range.end + offset)
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ranges.extend(query.search(excerpt_buffer, None).await.into_iter().map(
|
||||||
|
|range| buffer.anchor_after(range.start)..buffer.anchor_before(range.end),
|
||||||
|
));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) {
|
for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) {
|
||||||
let excerpt_range = excerpt.range.context.to_offset(&excerpt.buffer);
|
let excerpt_range = excerpt.range.context.to_offset(&excerpt.buffer);
|
||||||
|
@ -8,7 +8,7 @@ use crate::{
|
|||||||
motion::{EndOfDocument, Motion, StartOfDocument},
|
motion::{EndOfDocument, Motion, StartOfDocument},
|
||||||
normal::{
|
normal::{
|
||||||
move_cursor,
|
move_cursor,
|
||||||
search::{FindCommand, ReplaceCommand},
|
search::{range_regex, FindCommand, ReplaceCommand},
|
||||||
JoinLines,
|
JoinLines,
|
||||||
},
|
},
|
||||||
state::Mode,
|
state::Mode,
|
||||||
@ -340,6 +340,14 @@ pub fn command_interceptor(mut query: &str, cx: &AppContext) -> Option<CommandIn
|
|||||||
)
|
)
|
||||||
} else if let Ok(line) = query.parse::<u32>() {
|
} else if let Ok(line) = query.parse::<u32>() {
|
||||||
(query, GoToLine { line }.boxed_clone())
|
(query, GoToLine { line }.boxed_clone())
|
||||||
|
} else if range_regex().is_match(query) {
|
||||||
|
(
|
||||||
|
query,
|
||||||
|
ReplaceCommand {
|
||||||
|
query: query.to_string(),
|
||||||
|
}
|
||||||
|
.boxed_clone(),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
|
use std::{ops::Range, sync::OnceLock, time::Duration};
|
||||||
|
|
||||||
use gpui::{actions, impl_actions, ViewContext};
|
use gpui::{actions, impl_actions, ViewContext};
|
||||||
|
use language::Point;
|
||||||
|
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::{searchable::Direction, Workspace};
|
||||||
@ -47,6 +51,7 @@ struct Replacement {
|
|||||||
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]);
|
||||||
@ -55,6 +60,11 @@ 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);
|
||||||
@ -329,6 +339,22 @@ fn replace_command(
|
|||||||
) {
|
) {
|
||||||
let replacement = parse_replace_all(&action.query);
|
let replacement = parse_replace_all(&action.query);
|
||||||
let pane = workspace.active_pane().clone();
|
let pane = workspace.active_pane().clone();
|
||||||
|
let mut 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| {
|
||||||
|
let snapshot = &editor.snapshot(cx).buffer_snapshot;
|
||||||
|
let range = snapshot
|
||||||
|
.anchor_before(Point::new(range.start.saturating_sub(1) as u32, 0))
|
||||||
|
..snapshot.anchor_before(Point::new(range.end as u32, 0));
|
||||||
|
|
||||||
|
editor.set_search_within_ranges(&[range], cx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
pane.update(cx, |pane, cx| {
|
pane.update(cx, |pane, cx| {
|
||||||
let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
|
let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
|
||||||
return;
|
return;
|
||||||
@ -359,6 +385,19 @@ fn replace_command(
|
|||||||
if replacement.should_replace_all {
|
if replacement.should_replace_all {
|
||||||
search_bar.select_last_match(cx);
|
search_bar.select_last_match(cx);
|
||||||
search_bar.replace_all(&Default::default(), cx);
|
search_bar.replace_all(&Default::default(), cx);
|
||||||
|
if let Some(editor) = editor {
|
||||||
|
cx.spawn(|_, mut cx| async move {
|
||||||
|
cx.background_executor()
|
||||||
|
.timer(Duration::from_millis(200))
|
||||||
|
.await;
|
||||||
|
editor
|
||||||
|
.update(&mut cx, |editor, cx| {
|
||||||
|
editor.set_search_within_ranges(&[], cx)
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
move_cursor(
|
move_cursor(
|
||||||
vim,
|
vim,
|
||||||
@ -383,7 +422,20 @@ fn replace_command(
|
|||||||
// and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
|
// and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
|
||||||
fn parse_replace_all(query: &str) -> Replacement {
|
fn parse_replace_all(query: &str) -> Replacement {
|
||||||
let mut chars = query.chars();
|
let mut chars = query.chars();
|
||||||
if Some('%') != chars.next() || Some('s') != chars.next() {
|
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();
|
return Replacement::default();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -440,6 +492,7 @@ fn parse_replace_all(query: &str) -> Replacement {
|
|||||||
replacement,
|
replacement,
|
||||||
should_replace_all: true,
|
should_replace_all: true,
|
||||||
is_case_sensitive: true,
|
is_case_sensitive: true,
|
||||||
|
range,
|
||||||
};
|
};
|
||||||
|
|
||||||
for c in flags.chars() {
|
for c in flags.chars() {
|
||||||
@ -662,4 +715,36 @@ mod test {
|
|||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cargo test -p vim --features neovim test_replace_with_range
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_replace_with_range(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
|
||||||
|
cx.set_shared_state(indoc! {
|
||||||
|
"ˇa
|
||||||
|
a
|
||||||
|
a
|
||||||
|
a
|
||||||
|
a
|
||||||
|
a
|
||||||
|
a
|
||||||
|
"
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
cx.simulate_shared_keystrokes([":", "2", ",", "5", "s", "/", "a", "/", "b"])
|
||||||
|
.await;
|
||||||
|
cx.simulate_shared_keystrokes(["enter"]).await;
|
||||||
|
cx.assert_shared_state(indoc! {
|
||||||
|
"a
|
||||||
|
b
|
||||||
|
b
|
||||||
|
b
|
||||||
|
ˇb
|
||||||
|
a
|
||||||
|
a
|
||||||
|
"
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
12
crates/vim/test_data/test_replace_with_range.json
Normal file
12
crates/vim/test_data/test_replace_with_range.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{"Put":{"state":"ˇa\na\na\na\na\na\na\n "}}
|
||||||
|
{"Key":":"}
|
||||||
|
{"Key":"2"}
|
||||||
|
{"Key":","}
|
||||||
|
{"Key":"5"}
|
||||||
|
{"Key":"s"}
|
||||||
|
{"Key":"/"}
|
||||||
|
{"Key":"a"}
|
||||||
|
{"Key":"/"}
|
||||||
|
{"Key":"b"}
|
||||||
|
{"Key":"enter"}
|
||||||
|
{"Get":{"state":"a\nb\nb\nb\nˇb\na\na\n ","mode":"Normal"}}
|
@ -57,6 +57,10 @@ pub trait SearchableItem: Item + EventEmitter<SearchEvent> {
|
|||||||
|
|
||||||
fn search_bar_visibility_changed(&mut self, _visible: bool, _cx: &mut ViewContext<Self>) {}
|
fn search_bar_visibility_changed(&mut self, _visible: bool, _cx: &mut ViewContext<Self>) {}
|
||||||
|
|
||||||
|
fn has_filtered_search_ranges(&mut self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
fn clear_matches(&mut self, cx: &mut ViewContext<Self>);
|
fn clear_matches(&mut self, cx: &mut ViewContext<Self>);
|
||||||
fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>);
|
fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>);
|
||||||
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String;
|
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String;
|
||||||
|
Loading…
Reference in New Issue
Block a user