documents: undo and redo leave a block only past its own history; typing lands after undo empties the document
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
362a0fc9fa
commit
a3d9f089a3
5 files changed
+137
-87
No files matched your search
@@ -23,19 +23,19 @@ fn moved(at: usize, edit: &Option<(Range<usize>, usize)>) -> Option<usize> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MarkdownDocument {
|
impl MarkdownDocument {
|
||||||
/// An empty visual document takes typing at once.
|
/// An empty, read visual document takes typing at once.
|
||||||
pub(super) fn focused(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub(super) fn focused(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let empty = self.editor.read(cx).text().trim().is_empty();
|
let empty = self.editor.read(cx).text().trim().is_empty() && self.read_now(cx);
|
||||||
if self.mode == MarkdownMode::Visual && self.open.is_none() && empty {
|
if self.mode == MarkdownMode::Visual && self.open.is_none() && empty {
|
||||||
self.open(None, false, window, cx);
|
self.open(None, false, window, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The open block still holds what it opened with.
|
/// The open block has no step left that way.
|
||||||
pub(super) fn untouched(&self, cx: &App) -> bool {
|
pub(super) fn at_edge_of_history(&self, redo: bool, cx: &App) -> bool {
|
||||||
self.open.as_ref().is_some_and(|open| {
|
self.open
|
||||||
open.input.read(cx).text() == &self.editor.read(cx).text()[open.range.clone()]
|
.as_ref()
|
||||||
})
|
.is_some_and(|open| !open.input.read(cx).can_step(redo))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Undo or redo in the document's one history.
|
/// Undo or redo in the document's one history.
|
||||||
@@ -46,7 +46,8 @@ impl MarkdownDocument {
|
|||||||
false => editor.undo(cx),
|
false => editor.undo(cx),
|
||||||
});
|
});
|
||||||
window.focus(&self.focus, cx);
|
window.focus(&self.focus, cx);
|
||||||
self.focused(window, cx);
|
// Once the change is read, an empty end opens.
|
||||||
|
cx.defer_in(window, |document, window, cx| document.focused(window, cx));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opens block `ix` as shown, or one past the end.
|
/// Opens block `ix` as shown, or one past the end.
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
use gpui::{Entity, Modifiers, TestAppContext, VisualTestContext};
|
||||||
|
|
||||||
|
use super::{MarkdownDocument, MarkdownMode, TEXT, open, opened, press, text};
|
||||||
|
|
||||||
|
/// Focus on `document`'s own handle, as an owner gives it.
|
||||||
|
fn focus(document: &Entity<MarkdownDocument>, cx: &mut VisualTestContext) {
|
||||||
|
let handle = document.read_with(cx, |document, cx| {
|
||||||
|
gpui::Focusable::focus_handle(document, cx)
|
||||||
|
});
|
||||||
|
cx.update(|window, cx| window.focus(&handle, cx));
|
||||||
|
cx.run_until_parked();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn an_empty_visual_document_takes_typing_on_focus(cx: &mut TestAppContext) {
|
||||||
|
let (document, cx) = open("", MarkdownMode::Visual, cx);
|
||||||
|
focus(&document, cx);
|
||||||
|
cx.simulate_input("Hi");
|
||||||
|
assert_eq!(text(&document, cx), "Hi", "typing lands with no press");
|
||||||
|
cx.simulate_keystrokes("escape");
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert_eq!(text(&document, cx), "Hi", "escape keeps it");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn focus_opens_no_block_in_a_written_document(cx: &mut TestAppContext) {
|
||||||
|
let (document, cx) = open(TEXT, MarkdownMode::Visual, cx);
|
||||||
|
focus(&document, cx);
|
||||||
|
cx.simulate_input("x");
|
||||||
|
assert_eq!(text(&document, cx), TEXT);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mode(document: &Entity<MarkdownDocument>, mode: MarkdownMode, cx: &mut VisualTestContext) {
|
||||||
|
document.update_in(cx, |document, window, cx| {
|
||||||
|
document.set_mode(mode, window, cx)
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn preview_and_back_still_take_typing_in_an_empty_document(cx: &mut TestAppContext) {
|
||||||
|
let (document, cx) = open("", MarkdownMode::Visual, cx);
|
||||||
|
focus(&document, cx);
|
||||||
|
cx.simulate_keystrokes("escape");
|
||||||
|
mode(&document, MarkdownMode::Preview, cx);
|
||||||
|
mode(&document, MarkdownMode::Visual, cx);
|
||||||
|
cx.simulate_input("Hi");
|
||||||
|
assert_eq!(text(&document, cx), "Hi");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn undo_in_an_untouched_block_steps_the_document(cx: &mut TestAppContext) {
|
||||||
|
let (document, cx) = open("Seed", MarkdownMode::Source, cx);
|
||||||
|
document.update(cx, |document, cx| document.set_text("", cx));
|
||||||
|
mode(&document, MarkdownMode::Visual, cx);
|
||||||
|
cx.simulate_keystrokes("secondary-z");
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert_eq!(text(&document, cx), "Seed", "the source edit is undone");
|
||||||
|
cx.simulate_keystrokes("secondary-shift-z");
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert_eq!(text(&document, cx), "", "and redone");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn undo_in_a_typed_block_stays_in_the_block(cx: &mut TestAppContext) {
|
||||||
|
let (document, cx) = open("Seed", MarkdownMode::Source, cx);
|
||||||
|
document.update(cx, |document, cx| document.set_text(TEXT, cx));
|
||||||
|
mode(&document, MarkdownMode::Visual, cx);
|
||||||
|
press(1, Modifiers::none(), cx);
|
||||||
|
cx.simulate_input(" x");
|
||||||
|
cx.simulate_keystrokes("secondary-z");
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert_eq!(
|
||||||
|
text(&document, cx),
|
||||||
|
TEXT,
|
||||||
|
"the typing went, not the edit before"
|
||||||
|
);
|
||||||
|
assert_eq!(opened(&document, cx), "Blend toward white.", "still open");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn redo_after_undo_stays_in_the_block(cx: &mut TestAppContext) {
|
||||||
|
let (document, cx) = open("", MarkdownMode::Visual, cx);
|
||||||
|
focus(&document, cx);
|
||||||
|
cx.simulate_input("Hi");
|
||||||
|
cx.simulate_keystrokes("secondary-z");
|
||||||
|
assert_eq!(text(&document, cx), "");
|
||||||
|
cx.simulate_keystrokes("secondary-shift-z");
|
||||||
|
assert_eq!(text(&document, cx), "Hi", "the block's own redo");
|
||||||
|
assert_eq!(opened(&document, cx), "Hi", "the block stays open");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn redo_after_undo_in_a_written_block_stays_in_it(cx: &mut TestAppContext) {
|
||||||
|
let (document, cx) = open("Seed", MarkdownMode::Visual, cx);
|
||||||
|
press(0, Modifiers::none(), cx);
|
||||||
|
cx.simulate_input("!");
|
||||||
|
cx.simulate_keystrokes("secondary-z secondary-shift-z");
|
||||||
|
assert_eq!(text(&document, cx), "Seed!");
|
||||||
|
assert_eq!(opened(&document, cx), "Seed!", "the block stays open");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn typing_lands_after_undo_empties_the_document(cx: &mut TestAppContext) {
|
||||||
|
let (document, cx) = open("", MarkdownMode::Visual, cx);
|
||||||
|
focus(&document, cx);
|
||||||
|
cx.simulate_input("Hi");
|
||||||
|
mode(&document, MarkdownMode::Source, cx);
|
||||||
|
mode(&document, MarkdownMode::Visual, cx);
|
||||||
|
cx.simulate_keystrokes("secondary-z");
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert_eq!(text(&document, cx), "");
|
||||||
|
cx.simulate_input("Next");
|
||||||
|
assert_eq!(text(&document, cx), "Next");
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ pub(super) fn open<'a>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Presses block `ix` where it is drawn.
|
/// Presses block `ix` where it is drawn.
|
||||||
fn press(ix: usize, modifiers: Modifiers, cx: &mut VisualTestContext) {
|
pub(super) fn press(ix: usize, modifiers: Modifiers, cx: &mut VisualTestContext) {
|
||||||
let bounds = cx
|
let bounds = cx
|
||||||
.debug_bounds(Box::leak(format!("markdown-block-{ix}").into_boxed_str()))
|
.debug_bounds(Box::leak(format!("markdown-block-{ix}").into_boxed_str()))
|
||||||
.expect("the block is drawn");
|
.expect("the block is drawn");
|
||||||
@@ -365,6 +365,7 @@ fn a_press_after_a_write_back_waits_for_the_long_read(cx: &mut TestAppContext) {
|
|||||||
assert_eq!(opened(&document, cx), "B", "once read, B opens whole");
|
assert_eq!(opened(&document, cx), "B", "once read, B opens whole");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod history;
|
||||||
mod place;
|
mod place;
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
@@ -397,78 +398,3 @@ fn a_text_past_the_largest_read_is_never_read(cx: &mut TestAppContext) {
|
|||||||
});
|
});
|
||||||
assert_eq!((blocks, outline), (0, 0), "the old read is gone");
|
assert_eq!((blocks, outline), (0, 0), "the old read is gone");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Focus on `document`'s own handle, as an owner gives it.
|
|
||||||
fn focus(document: &Entity<MarkdownDocument>, cx: &mut VisualTestContext) {
|
|
||||||
let handle = document.read_with(cx, |document, cx| {
|
|
||||||
gpui::Focusable::focus_handle(document, cx)
|
|
||||||
});
|
|
||||||
cx.update(|window, cx| window.focus(&handle, cx));
|
|
||||||
cx.run_until_parked();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
fn an_empty_visual_document_takes_typing_on_focus(cx: &mut TestAppContext) {
|
|
||||||
let (document, cx) = open("", MarkdownMode::Visual, cx);
|
|
||||||
focus(&document, cx);
|
|
||||||
cx.simulate_input("Hi");
|
|
||||||
assert_eq!(text(&document, cx), "Hi", "typing lands with no press");
|
|
||||||
cx.simulate_keystrokes("escape");
|
|
||||||
cx.run_until_parked();
|
|
||||||
assert_eq!(text(&document, cx), "Hi", "escape keeps it");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
fn focus_opens_no_block_in_a_written_document(cx: &mut TestAppContext) {
|
|
||||||
let (document, cx) = open(TEXT, MarkdownMode::Visual, cx);
|
|
||||||
focus(&document, cx);
|
|
||||||
cx.simulate_input("x");
|
|
||||||
assert_eq!(text(&document, cx), TEXT);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mode(document: &Entity<MarkdownDocument>, mode: MarkdownMode, cx: &mut VisualTestContext) {
|
|
||||||
document.update_in(cx, |document, window, cx| {
|
|
||||||
document.set_mode(mode, window, cx)
|
|
||||||
});
|
|
||||||
cx.run_until_parked();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
fn preview_and_back_still_take_typing_in_an_empty_document(cx: &mut TestAppContext) {
|
|
||||||
let (document, cx) = open("", MarkdownMode::Visual, cx);
|
|
||||||
focus(&document, cx);
|
|
||||||
cx.simulate_keystrokes("escape");
|
|
||||||
mode(&document, MarkdownMode::Preview, cx);
|
|
||||||
mode(&document, MarkdownMode::Visual, cx);
|
|
||||||
cx.simulate_input("Hi");
|
|
||||||
assert_eq!(text(&document, cx), "Hi");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
fn undo_in_an_untouched_block_steps_the_document(cx: &mut TestAppContext) {
|
|
||||||
let (document, cx) = open("Seed", MarkdownMode::Source, cx);
|
|
||||||
document.update(cx, |document, cx| document.set_text("", cx));
|
|
||||||
mode(&document, MarkdownMode::Visual, cx);
|
|
||||||
cx.simulate_keystrokes("secondary-z");
|
|
||||||
cx.run_until_parked();
|
|
||||||
assert_eq!(text(&document, cx), "Seed", "the source edit is undone");
|
|
||||||
cx.simulate_keystrokes("secondary-shift-z");
|
|
||||||
cx.run_until_parked();
|
|
||||||
assert_eq!(text(&document, cx), "", "and redone");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
fn undo_in_a_typed_block_stays_in_the_block(cx: &mut TestAppContext) {
|
|
||||||
let (document, cx) = open("Seed", MarkdownMode::Source, cx);
|
|
||||||
document.update(cx, |document, cx| document.set_text(TEXT, cx));
|
|
||||||
mode(&document, MarkdownMode::Visual, cx);
|
|
||||||
press(1, Modifiers::none(), cx);
|
|
||||||
cx.simulate_input(" x");
|
|
||||||
cx.simulate_keystrokes("secondary-z");
|
|
||||||
cx.run_until_parked();
|
|
||||||
assert_eq!(
|
|
||||||
text(&document, cx),
|
|
||||||
TEXT,
|
|
||||||
"the typing went, not the edit before"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -164,15 +164,15 @@ impl Render for MarkdownDocument {
|
|||||||
.on_action(cx.listener(|document, _: &Redo, window, cx| {
|
.on_action(cx.listener(|document, _: &Redo, window, cx| {
|
||||||
document.history(true, window, cx);
|
document.history(true, window, cx);
|
||||||
}))
|
}))
|
||||||
// An untouched block leaves undo to the document.
|
// Past the block's own history, the document's.
|
||||||
.capture_action(cx.listener(|document, _: &forms::Undo, window, cx| {
|
.capture_action(cx.listener(|document, _: &forms::Undo, window, cx| {
|
||||||
if document.untouched(cx) {
|
if document.at_edge_of_history(false, cx) {
|
||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
document.history(false, window, cx);
|
document.history(false, window, cx);
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.capture_action(cx.listener(|document, _: &forms::Redo, window, cx| {
|
.capture_action(cx.listener(|document, _: &forms::Redo, window, cx| {
|
||||||
if document.untouched(cx) {
|
if document.at_edge_of_history(true, cx) {
|
||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
document.history(true, window, cx);
|
document.history(true, window, cx);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,6 +220,14 @@ impl TextInput {
|
|||||||
&self.label
|
&self.label
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A step left in that direction of its history.
|
||||||
|
pub(crate) fn can_step(&self, redo: bool) -> bool {
|
||||||
|
match redo {
|
||||||
|
true => self.history.can_redo(),
|
||||||
|
false => self.history.can_undo(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn selection(&self) -> Range<usize> {
|
pub fn selection(&self) -> Range<usize> {
|
||||||
self.selection.clone()
|
self.selection.clone()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user