vim: Add U to undo last line (#33571)
Closes #14760 Still TODO: * Vim actually undoes *many* changes if they're all on the same line. Release Notes: - vim: Add `U` to return to the last changed line and undo
This commit is contained in:
parent
df57754baf
commit
8e8a772c2d
7 changed files with 303 additions and 8 deletions
|
@ -24,9 +24,9 @@ use crate::{
|
|||
};
|
||||
use collections::BTreeSet;
|
||||
use convert::ConvertTarget;
|
||||
use editor::Bias;
|
||||
use editor::Editor;
|
||||
use editor::{Anchor, SelectionEffects};
|
||||
use editor::{Bias, ToPoint};
|
||||
use editor::{display_map::ToDisplayPoint, movement};
|
||||
use gpui::{Context, Window, actions};
|
||||
use language::{Point, SelectionGoal};
|
||||
|
@ -90,6 +90,8 @@ actions!(
|
|||
Undo,
|
||||
/// Redoes the last undone change.
|
||||
Redo,
|
||||
/// Undoes all changes to the most recently changed line.
|
||||
UndoLastLine,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -194,6 +196,120 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
|||
}
|
||||
});
|
||||
});
|
||||
Vim::action(editor, cx, |vim, _: &UndoLastLine, window, cx| {
|
||||
Vim::take_forced_motion(cx);
|
||||
vim.update_editor(window, cx, |vim, editor, window, cx| {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let Some(last_change) = editor.change_list.last_before_grouping() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let anchors = last_change.iter().cloned().collect::<Vec<_>>();
|
||||
let mut last_row = None;
|
||||
let ranges: Vec<_> = anchors
|
||||
.iter()
|
||||
.filter_map(|anchor| {
|
||||
let point = anchor.to_point(&snapshot);
|
||||
if last_row == Some(point.row) {
|
||||
return None;
|
||||
}
|
||||
last_row = Some(point.row);
|
||||
let line_range = Point::new(point.row, 0)
|
||||
..Point::new(point.row, snapshot.line_len(MultiBufferRow(point.row)));
|
||||
Some((
|
||||
snapshot.anchor_before(line_range.start)
|
||||
..snapshot.anchor_after(line_range.end),
|
||||
line_range,
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let edits = editor.buffer().update(cx, |buffer, cx| {
|
||||
let current_content = ranges
|
||||
.iter()
|
||||
.map(|(anchors, _)| {
|
||||
buffer
|
||||
.snapshot(cx)
|
||||
.text_for_range(anchors.clone())
|
||||
.collect::<String>()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut content_before_undo = current_content.clone();
|
||||
let mut undo_count = 0;
|
||||
|
||||
loop {
|
||||
let undone_tx = buffer.undo(cx);
|
||||
undo_count += 1;
|
||||
let mut content_after_undo = Vec::new();
|
||||
|
||||
let mut line_changed = false;
|
||||
for ((anchors, _), text_before_undo) in
|
||||
ranges.iter().zip(content_before_undo.iter())
|
||||
{
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
let text_after_undo =
|
||||
snapshot.text_for_range(anchors.clone()).collect::<String>();
|
||||
|
||||
if &text_after_undo != text_before_undo {
|
||||
line_changed = true;
|
||||
}
|
||||
content_after_undo.push(text_after_undo);
|
||||
}
|
||||
|
||||
content_before_undo = content_after_undo;
|
||||
if !line_changed {
|
||||
break;
|
||||
}
|
||||
if undone_tx == vim.undo_last_line_tx {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let edits = ranges
|
||||
.into_iter()
|
||||
.zip(content_before_undo.into_iter().zip(current_content))
|
||||
.filter_map(|((_, mut points), (mut old_text, new_text))| {
|
||||
if new_text == old_text {
|
||||
return None;
|
||||
}
|
||||
let common_suffix_starts_at = old_text
|
||||
.char_indices()
|
||||
.rev()
|
||||
.zip(new_text.chars().rev())
|
||||
.find_map(
|
||||
|((i, a), b)| {
|
||||
if a != b { Some(i + a.len_utf8()) } else { None }
|
||||
},
|
||||
)
|
||||
.unwrap_or(old_text.len());
|
||||
points.end.column -= (old_text.len() - common_suffix_starts_at) as u32;
|
||||
old_text = old_text.split_at(common_suffix_starts_at).0.to_string();
|
||||
let common_prefix_len = old_text
|
||||
.char_indices()
|
||||
.zip(new_text.chars())
|
||||
.find_map(|((i, a), b)| if a != b { Some(i) } else { None })
|
||||
.unwrap_or(0);
|
||||
points.start.column = common_prefix_len as u32;
|
||||
old_text = old_text.split_at(common_prefix_len).1.to_string();
|
||||
|
||||
Some((points, old_text))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for _ in 0..undo_count {
|
||||
buffer.redo(cx);
|
||||
}
|
||||
edits
|
||||
});
|
||||
vim.undo_last_line_tx = editor.transact(window, cx, |editor, window, cx| {
|
||||
editor.change_list.invert_last_group();
|
||||
editor.edit(edits, cx);
|
||||
editor.change_selections(SelectionEffects::default(), window, cx, |s| {
|
||||
s.select_anchor_ranges(anchors.into_iter().map(|a| a..a));
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
repeat::register(editor, cx);
|
||||
scroll::register(editor, cx);
|
||||
|
@ -1876,4 +1992,102 @@ mod test {
|
|||
cx.simulate_shared_keystrokes("ctrl-o").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_undo_last_line(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
ˇfn a() { }
|
||||
fn a() { }
|
||||
fn a() { }
|
||||
"})
|
||||
.await;
|
||||
// do a jump to reset vim's undo grouping
|
||||
cx.simulate_shared_keystrokes("shift-g").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
cx.simulate_shared_keystrokes("r a").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
cx.simulate_shared_keystrokes("shift-u").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
cx.simulate_shared_keystrokes("shift-u").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
cx.simulate_shared_keystrokes("g g shift-u").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_undo_last_line_newline(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
ˇfn a() { }
|
||||
fn a() { }
|
||||
fn a() { }
|
||||
"})
|
||||
.await;
|
||||
// do a jump to reset vim's undo grouping
|
||||
cx.simulate_shared_keystrokes("shift-g k").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
cx.simulate_shared_keystrokes("o h e l l o escape").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
cx.simulate_shared_keystrokes("shift-u").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
cx.simulate_shared_keystrokes("shift-u").await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_undo_last_line_newline_many_changes(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
ˇfn a() { }
|
||||
fn a() { }
|
||||
fn a() { }
|
||||
"})
|
||||
.await;
|
||||
// do a jump to reset vim's undo grouping
|
||||
cx.simulate_shared_keystrokes("x shift-g k").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
cx.simulate_shared_keystrokes("x f a x f { x").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
cx.simulate_shared_keystrokes("shift-u").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
cx.simulate_shared_keystrokes("shift-u").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
cx.simulate_shared_keystrokes("shift-u").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
cx.simulate_shared_keystrokes("shift-u").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_undo_last_line_multicursor(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
ˇone two ˇone
|
||||
two ˇone two
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes("3 r a");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
aaˇa two aaˇa
|
||||
two aaˇa two
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes("escape escape");
|
||||
cx.simulate_keystrokes("shift-u");
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
onˇe two onˇe
|
||||
two onˇe two
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -375,6 +375,7 @@ pub(crate) struct Vim {
|
|||
pub(crate) current_tx: Option<TransactionId>,
|
||||
pub(crate) current_anchor: Option<Selection<Anchor>>,
|
||||
pub(crate) undo_modes: HashMap<TransactionId, Mode>,
|
||||
pub(crate) undo_last_line_tx: Option<TransactionId>,
|
||||
|
||||
selected_register: Option<char>,
|
||||
pub search: SearchState,
|
||||
|
@ -422,6 +423,7 @@ impl Vim {
|
|||
|
||||
stored_visual_mode: None,
|
||||
current_tx: None,
|
||||
undo_last_line_tx: None,
|
||||
current_anchor: None,
|
||||
undo_modes: HashMap::default(),
|
||||
|
||||
|
|
14
crates/vim/test_data/test_undo_last_line.json
Normal file
14
crates/vim/test_data/test_undo_last_line.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{"Put":{"state":"ˇfn a() { }\nfn a() { }\nfn a() { }\n"}}
|
||||
{"Key":"shift-g"}
|
||||
{"Get":{"state":"fn a() { }\nfn a() { }\nfn a() { }\nˇ","mode":"Normal"}}
|
||||
{"Key":"r"}
|
||||
{"Key":"a"}
|
||||
{"Get":{"state":"fn a() { }\nfn a() { }\nfn a() { }\nˇ","mode":"Normal"}}
|
||||
{"Key":"shift-u"}
|
||||
{"Get":{"state":"ˇ\nfn a() { }\nfn a() { }\n","mode":"Normal"}}
|
||||
{"Key":"shift-u"}
|
||||
{"Get":{"state":"ˇfn a() { }\nfn a() { }\nfn a() { }\n","mode":"Normal"}}
|
||||
{"Key":"g"}
|
||||
{"Key":"g"}
|
||||
{"Key":"shift-u"}
|
||||
{"Get":{"state":"ˇ\nfn a() { }\nfn a() { }\n","mode":"Normal"}}
|
15
crates/vim/test_data/test_undo_last_line_newline.json
Normal file
15
crates/vim/test_data/test_undo_last_line_newline.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{"Put":{"state":"ˇfn a() { }\nfn a() { }\nfn a() { }\n"}}
|
||||
{"Key":"shift-g"}
|
||||
{"Key":"k"}
|
||||
{"Get":{"state":"fn a() { }\nfn a() { }\nˇfn a() { }\n","mode":"Normal"}}
|
||||
{"Key":"o"}
|
||||
{"Key":"h"}
|
||||
{"Key":"e"}
|
||||
{"Key":"l"}
|
||||
{"Key":"l"}
|
||||
{"Key":"o"}
|
||||
{"Key":"escape"}
|
||||
{"Get":{"state":"fn a() { }\nfn a() { }\nfn a() { }\nhellˇo\n","mode":"Normal"}}
|
||||
{"Key":"shift-u"}
|
||||
{"Get":{"state":"fn a() { }\nfn a() { }\nfn a() { }\nˇ\n","mode":"Normal"}}
|
||||
{"Key":"shift-u"}
|
|
@ -0,0 +1,21 @@
|
|||
{"Put":{"state":"ˇfn a() { }\nfn a() { }\nfn a() { }\n"}}
|
||||
{"Key":"x"}
|
||||
{"Key":"shift-g"}
|
||||
{"Key":"k"}
|
||||
{"Get":{"state":"n a() { }\nfn a() { }\nˇfn a() { }\n","mode":"Normal"}}
|
||||
{"Key":"x"}
|
||||
{"Key":"f"}
|
||||
{"Key":"a"}
|
||||
{"Key":"x"}
|
||||
{"Key":"f"}
|
||||
{"Key":"{"}
|
||||
{"Key":"x"}
|
||||
{"Get":{"state":"n a() { }\nfn a() { }\nn () ˇ }\n","mode":"Normal"}}
|
||||
{"Key":"shift-u"}
|
||||
{"Get":{"state":"n a() { }\nfn a() { }\nˇfn a() { }\n","mode":"Normal"}}
|
||||
{"Key":"shift-u"}
|
||||
{"Get":{"state":"n a() { }\nfn a() { }\nn () ˇ }\n","mode":"Normal"}}
|
||||
{"Key":"shift-u"}
|
||||
{"Get":{"state":"n a() { }\nfn a() { }\nˇfn a() { }\n","mode":"Normal"}}
|
||||
{"Key":"shift-u"}
|
||||
{"Get":{"state":"n a() { }\nfn a() { }\nn () ˇ }\n","mode":"Normal"}}
|
Loading…
Add table
Add a link
Reference in a new issue