T21a round 3 fix, after the cap
- TextInput records the selection each edit replaces, where every edit passes; the block editor's undo goes back to it, for typing, delete, cut and paste - TextInput's layout geometry moves to forms/text/geometry.rs - Reviews: T21a done, fixed after the cap, no fourth review
This commit is contained in:
1 parent
187f54253e
commit
14a8990793
7 files changed
+109
-74
No files matched your search
@@ -249,4 +249,4 @@ Per-component lines live in `tasks/`. Tags there:
|
||||
| T20 | 2 | PASS | |
|
||||
| T21a | 1 | FAIL | Enter kept a selection; undo of a split left focus on a gone field; italic inside bold ate the bold; keys went into merged cells; undo kept a changed kind's highlighter |
|
||||
| T21a | 2 | FAIL | undo of typing lost the selection it replaced; Up from below a table landed in a hidden merged cell |
|
||||
| T21a | 3 | — | pending review |
|
||||
| T21a | 3 | FAIL | undo after a delete or a paste lost the selection they replaced; fixed after the cap by recording the replaced selection where every TextInput edit passes, verified by a red-green test, no fourth review |
|
||||
@@ -174,14 +174,6 @@ impl BlockEditor {
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a key went down, before the field acts on it: the start a typing burst's undo goes back to.
|
||||
pub(crate) fn note_key(&mut self, key: u64, field: usize, cx: &Context<Self>) {
|
||||
let selection = self.blocks[self.index(key)].fields[field]
|
||||
.read(cx)
|
||||
.selection();
|
||||
self.typed_from = Some((key, field, selection));
|
||||
}
|
||||
|
||||
/// Records the document before a change to its blocks.
|
||||
pub(crate) fn before_change(&mut self, cx: &mut Context<Self>) {
|
||||
self.settle(true, cx);
|
||||
|
||||
@@ -76,7 +76,7 @@ pub struct BlockEditor {
|
||||
current: Snapshot,
|
||||
/// The field that last held focus, as block and field.
|
||||
caret: Option<(u64, usize)>,
|
||||
/// The selection a key went down on, before its field changed.
|
||||
/// The selection the last edit in a field replaced.
|
||||
typed_from: Option<(u64, usize, std::ops::Range<usize>)>,
|
||||
pub(crate) people: Vec<SharedString>,
|
||||
pub(crate) diagram: Option<RenderDiagram>,
|
||||
@@ -288,6 +288,8 @@ impl BlockEditor {
|
||||
.insert(name.clone(), text);
|
||||
}
|
||||
}
|
||||
let at = self.blocks[ix].fields.iter().position(|each| each == field);
|
||||
self.typed_from = at.map(|at| (key, at, field.read(cx).edited_from()));
|
||||
self.settle(true, cx);
|
||||
}
|
||||
InputEvent::Focus => {
|
||||
|
||||
@@ -114,7 +114,6 @@ impl BlockEditor {
|
||||
cx.stop_propagation();
|
||||
editor.on_submit(key, window, cx);
|
||||
}))
|
||||
.capture_key_down(cx.listener(move |editor, _, _, cx| editor.note_key(key, ix, cx)))
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||
.child(field.clone());
|
||||
let slash = move |query: &str, at: usize, caret: usize| -> (Vec<Choice>, Pick) {
|
||||
|
||||
@@ -344,3 +344,30 @@ fn up_into_a_table_skips_its_hidden_cells(cx: &mut TestAppContext) {
|
||||
"up lands on the cell that shows in the last row"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn undo_of_a_delete_or_a_paste_restores_the_selection(cx: &mut TestAppContext) {
|
||||
let (editor, cx) = open(vec![BlockData::new(BlockKind::Paragraph, ["abcdef"])], cx);
|
||||
let selection = |cx: &mut VisualTestContext| {
|
||||
cx.update(|_, cx| editor.read(cx).blocks[0].fields[0].read(cx).selection())
|
||||
};
|
||||
select(&editor, 0, 0, 2..4, cx);
|
||||
cx.simulate_keystrokes("backspace");
|
||||
settle(cx);
|
||||
assert_eq!(blocks(&editor, cx), [text(BlockKind::Paragraph, "abef")]);
|
||||
cx.simulate_keystrokes("cmd-z");
|
||||
settle(cx);
|
||||
assert_eq!(selection(cx), 2..4, "undo of a delete selects what it took");
|
||||
cx.update(|_, cx| cx.write_to_clipboard(gpui::ClipboardItem::new_string("xy".into())));
|
||||
select(&editor, 0, 0, 2..4, cx);
|
||||
cx.simulate_keystrokes("cmd-v");
|
||||
settle(cx);
|
||||
assert_eq!(blocks(&editor, cx), [text(BlockKind::Paragraph, "abxyef")]);
|
||||
cx.simulate_keystrokes("cmd-z");
|
||||
settle(cx);
|
||||
assert_eq!(
|
||||
selection(cx),
|
||||
2..4,
|
||||
"undo of a paste selects what it replaced"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
use gpui::{Bounds, Pixels, Point};
|
||||
|
||||
use super::TextInput;
|
||||
|
||||
impl TextInput {
|
||||
/// The caret's box in window coordinates, once laid out.
|
||||
pub fn caret_bounds(&self) -> Option<Bounds<Pixels>> {
|
||||
self.bounds_for(self.cursor())
|
||||
}
|
||||
|
||||
/// An offset's box in the window, from the last layout; later offsets clamp to its end.
|
||||
pub fn bounds_for(&self, offset: usize) -> Option<Bounds<Pixels>> {
|
||||
let layout = self.layout.as_ref()?;
|
||||
let laid = layout
|
||||
.lines
|
||||
.last()
|
||||
.map_or(0, |(start, line)| start + line.len());
|
||||
let at = self.position_for(self.display_offset(offset).min(laid))?;
|
||||
Some(Bounds::new(
|
||||
layout.bounds.origin + at - self.scroll,
|
||||
gpui::size(Pixels::ZERO, layout.line_height),
|
||||
))
|
||||
}
|
||||
|
||||
/// Top-left of a display offset, relative to the text's origin.
|
||||
pub(crate) fn position_for(&self, display: usize) -> Option<Point<Pixels>> {
|
||||
let layout = self.layout.as_ref()?;
|
||||
if layout.placeholder {
|
||||
return Some(Point::default());
|
||||
}
|
||||
let mut top = Pixels::ZERO;
|
||||
for (start, line) in &layout.lines {
|
||||
if display <= start + line.len() {
|
||||
let at = line.position_for_index(display - start, layout.line_height)?;
|
||||
return Some(gpui::point(at.x, at.y + top));
|
||||
}
|
||||
top += line.size(layout.line_height).height;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// The content offset nearest a window position.
|
||||
pub(crate) fn offset_for_point(&self, point: Point<Pixels>) -> usize {
|
||||
let Some(layout) = self.layout.as_ref() else {
|
||||
return 0;
|
||||
};
|
||||
if layout.placeholder {
|
||||
return 0;
|
||||
}
|
||||
let local = point - layout.bounds.origin + self.scroll;
|
||||
if local.y < Pixels::ZERO {
|
||||
return 0;
|
||||
}
|
||||
let mut top = Pixels::ZERO;
|
||||
for (start, line) in &layout.lines {
|
||||
let height = line.size(layout.line_height).height;
|
||||
if local.y < top + height {
|
||||
let inside = gpui::point(local.x.max(Pixels::ZERO), local.y - top);
|
||||
let index = match line.closest_index_for_position(inside, layout.line_height) {
|
||||
Ok(index) | Err(index) => index,
|
||||
};
|
||||
return self.content_offset(start + index.min(line.len()));
|
||||
}
|
||||
top += height;
|
||||
}
|
||||
self.text.len()
|
||||
}
|
||||
}
|
||||
+10
-63
@@ -1,6 +1,7 @@
|
||||
mod actions;
|
||||
mod edit;
|
||||
mod element;
|
||||
mod geometry;
|
||||
mod highlight;
|
||||
mod ime;
|
||||
|
||||
@@ -61,6 +62,8 @@ pub struct TextInput {
|
||||
highlighter: Option<Highlighter>,
|
||||
disabled: bool,
|
||||
history: History<Snapshot>,
|
||||
/// The selection the last edit replaced.
|
||||
edited_from: Range<usize>,
|
||||
pub(crate) layout: Option<Layout>,
|
||||
pub(crate) scroll: Point<Pixels>,
|
||||
selecting: bool,
|
||||
@@ -109,6 +112,7 @@ impl TextInput {
|
||||
highlighter: None,
|
||||
disabled: false,
|
||||
history: History::default(),
|
||||
edited_from: 0..0,
|
||||
layout: None,
|
||||
scroll: Point::default(),
|
||||
selecting: false,
|
||||
@@ -250,25 +254,6 @@ impl TextInput {
|
||||
self.restart_blink(cx);
|
||||
}
|
||||
|
||||
/// The caret's box in window coordinates, once laid out.
|
||||
pub fn caret_bounds(&self) -> Option<Bounds<Pixels>> {
|
||||
self.bounds_for(self.cursor())
|
||||
}
|
||||
|
||||
/// An offset's box in the window, from the last layout; later offsets clamp to its end.
|
||||
pub fn bounds_for(&self, offset: usize) -> Option<Bounds<Pixels>> {
|
||||
let layout = self.layout.as_ref()?;
|
||||
let laid = layout
|
||||
.lines
|
||||
.last()
|
||||
.map_or(0, |(start, line)| start + line.len());
|
||||
let at = self.position_for(self.display_offset(offset).min(laid))?;
|
||||
Some(Bounds::new(
|
||||
layout.bounds.origin + at - self.scroll,
|
||||
gpui::size(Pixels::ZERO, layout.line_height),
|
||||
))
|
||||
}
|
||||
|
||||
/// Keeps what `filter` and `max_len` allow of `incoming`.
|
||||
fn admit(&self, incoming: &str, keep: usize) -> String {
|
||||
let allowed: String = incoming
|
||||
@@ -281,6 +266,11 @@ impl TextInput {
|
||||
}
|
||||
}
|
||||
|
||||
/// The selection the last edit replaced, for an owner's undo to go back to.
|
||||
pub(crate) fn edited_from(&self) -> Range<usize> {
|
||||
self.edited_from.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn replace(
|
||||
&mut self,
|
||||
range: Range<usize>,
|
||||
@@ -291,6 +281,7 @@ impl TextInput {
|
||||
if self.disabled {
|
||||
return;
|
||||
}
|
||||
self.edited_from = self.selection.clone();
|
||||
let incoming = if self.rows.is_none() {
|
||||
incoming.replace(['\n', '\r'], " ")
|
||||
} else {
|
||||
@@ -423,50 +414,6 @@ impl TextInput {
|
||||
}
|
||||
}
|
||||
|
||||
/// Top-left of a display offset, relative to the text's origin.
|
||||
pub(crate) fn position_for(&self, display: usize) -> Option<Point<Pixels>> {
|
||||
let layout = self.layout.as_ref()?;
|
||||
if layout.placeholder {
|
||||
return Some(Point::default());
|
||||
}
|
||||
let mut top = Pixels::ZERO;
|
||||
for (start, line) in &layout.lines {
|
||||
if display <= start + line.len() {
|
||||
let at = line.position_for_index(display - start, layout.line_height)?;
|
||||
return Some(gpui::point(at.x, at.y + top));
|
||||
}
|
||||
top += line.size(layout.line_height).height;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// The content offset nearest a window position.
|
||||
pub(crate) fn offset_for_point(&self, point: Point<Pixels>) -> usize {
|
||||
let Some(layout) = self.layout.as_ref() else {
|
||||
return 0;
|
||||
};
|
||||
if layout.placeholder {
|
||||
return 0;
|
||||
}
|
||||
let local = point - layout.bounds.origin + self.scroll;
|
||||
if local.y < Pixels::ZERO {
|
||||
return 0;
|
||||
}
|
||||
let mut top = Pixels::ZERO;
|
||||
for (start, line) in &layout.lines {
|
||||
let height = line.size(layout.line_height).height;
|
||||
if local.y < top + height {
|
||||
let inside = gpui::point(local.x.max(Pixels::ZERO), local.y - top);
|
||||
let index = match line.closest_index_for_position(inside, layout.line_height) {
|
||||
Ok(index) | Err(index) => index,
|
||||
};
|
||||
return self.content_offset(start + index.min(line.len()));
|
||||
}
|
||||
top += height;
|
||||
}
|
||||
self.text.len()
|
||||
}
|
||||
|
||||
pub(crate) fn highlights(&self, cx: &App) -> Vec<(Range<usize>, Highlight)> {
|
||||
match (&self.highlighter, self.masked) {
|
||||
(Some(highlight), false) => highlight(&self.text, cx),
|
||||
|
||||
Reference in new issue
Block a user