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:
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 | |
|
| 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 | 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 | 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.
|
/// Records the document before a change to its blocks.
|
||||||
pub(crate) fn before_change(&mut self, cx: &mut Context<Self>) {
|
pub(crate) fn before_change(&mut self, cx: &mut Context<Self>) {
|
||||||
self.settle(true, cx);
|
self.settle(true, cx);
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ pub struct BlockEditor {
|
|||||||
current: Snapshot,
|
current: Snapshot,
|
||||||
/// The field that last held focus, as block and field.
|
/// The field that last held focus, as block and field.
|
||||||
caret: Option<(u64, usize)>,
|
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>)>,
|
typed_from: Option<(u64, usize, std::ops::Range<usize>)>,
|
||||||
pub(crate) people: Vec<SharedString>,
|
pub(crate) people: Vec<SharedString>,
|
||||||
pub(crate) diagram: Option<RenderDiagram>,
|
pub(crate) diagram: Option<RenderDiagram>,
|
||||||
@@ -288,6 +288,8 @@ impl BlockEditor {
|
|||||||
.insert(name.clone(), text);
|
.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);
|
self.settle(true, cx);
|
||||||
}
|
}
|
||||||
InputEvent::Focus => {
|
InputEvent::Focus => {
|
||||||
|
|||||||
@@ -114,7 +114,6 @@ impl BlockEditor {
|
|||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
editor.on_submit(key, window, cx);
|
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())
|
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||||
.child(field.clone());
|
.child(field.clone());
|
||||||
let slash = move |query: &str, at: usize, caret: usize| -> (Vec<Choice>, Pick) {
|
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"
|
"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 actions;
|
||||||
mod edit;
|
mod edit;
|
||||||
mod element;
|
mod element;
|
||||||
|
mod geometry;
|
||||||
mod highlight;
|
mod highlight;
|
||||||
mod ime;
|
mod ime;
|
||||||
|
|
||||||
@@ -61,6 +62,8 @@ pub struct TextInput {
|
|||||||
highlighter: Option<Highlighter>,
|
highlighter: Option<Highlighter>,
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
history: History<Snapshot>,
|
history: History<Snapshot>,
|
||||||
|
/// The selection the last edit replaced.
|
||||||
|
edited_from: Range<usize>,
|
||||||
pub(crate) layout: Option<Layout>,
|
pub(crate) layout: Option<Layout>,
|
||||||
pub(crate) scroll: Point<Pixels>,
|
pub(crate) scroll: Point<Pixels>,
|
||||||
selecting: bool,
|
selecting: bool,
|
||||||
@@ -109,6 +112,7 @@ impl TextInput {
|
|||||||
highlighter: None,
|
highlighter: None,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
history: History::default(),
|
history: History::default(),
|
||||||
|
edited_from: 0..0,
|
||||||
layout: None,
|
layout: None,
|
||||||
scroll: Point::default(),
|
scroll: Point::default(),
|
||||||
selecting: false,
|
selecting: false,
|
||||||
@@ -250,25 +254,6 @@ impl TextInput {
|
|||||||
self.restart_blink(cx);
|
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`.
|
/// Keeps what `filter` and `max_len` allow of `incoming`.
|
||||||
fn admit(&self, incoming: &str, keep: usize) -> String {
|
fn admit(&self, incoming: &str, keep: usize) -> String {
|
||||||
let allowed: String = incoming
|
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(
|
pub(crate) fn replace(
|
||||||
&mut self,
|
&mut self,
|
||||||
range: Range<usize>,
|
range: Range<usize>,
|
||||||
@@ -291,6 +281,7 @@ impl TextInput {
|
|||||||
if self.disabled {
|
if self.disabled {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
self.edited_from = self.selection.clone();
|
||||||
let incoming = if self.rows.is_none() {
|
let incoming = if self.rows.is_none() {
|
||||||
incoming.replace(['\n', '\r'], " ")
|
incoming.replace(['\n', '\r'], " ")
|
||||||
} else {
|
} 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)> {
|
pub(crate) fn highlights(&self, cx: &App) -> Vec<(Range<usize>, Highlight)> {
|
||||||
match (&self.highlighter, self.masked) {
|
match (&self.highlighter, self.masked) {
|
||||||
(Some(highlight), false) => highlight(&self.text, cx),
|
(Some(highlight), false) => highlight(&self.text, cx),
|
||||||
|
|||||||
Reference in new issue
Block a user