Merge pull request #1966 from Silvertree2010/fix/h11008-delete-key-removes-only-the-firs

Delete a whole grapheme cluster with the forward Delete key
This commit is contained in:
FredApps authored and GitHub committed 2026-11-13 10:34:40 +01:00
commit 506db24b07
3 files changed
+44 -1

No files matched your search

+1 -1
View File
@@ -56,7 +56,7 @@ pub(crate) fn delete_dir(s: &mut Session, dir: &str) -> Result<Value> {
ed::delete(body, a, b)
} else {
let other = match dir.as_str() {
"forward" => ed::char_move(body, a, true),
"forward" => ed::cluster_end(body, a),
_ => {
// Backspace at the start of an indented/bulleted paragraph outdents first.
if a.1 == 0
+27
View File
@@ -98,6 +98,33 @@ fn text_editing_flow() {
assert_eq!(s.doc().unwrap().selection.shapes.len(), 1);
}
/// Forward Delete removes a whole grapheme cluster, not just its first scalar.
#[test]
fn forward_delete_removes_whole_grapheme_cluster() {
for (input, rest) in [("\u{1F44D}\u{1F3FD}y", "y"), ("e\u{301}x", "x")] {
let mut s = session();
let title = s.doc().unwrap().current_slide().unwrap().shapes[0].id.0;
s.execute("text.edit", &json!({"id": title})).unwrap();
s.execute("text.selectAll", &json!({})).unwrap();
s.execute("text.insert", &json!({"text": input})).unwrap();
s.execute("text.move", &json!({"to": "start"})).unwrap();
s.execute("text.delete", &json!({"dir": "forward"})).unwrap();
let t = s.execute("text.get", &json!({"id": title})).unwrap();
assert_eq!(t["text"], rest);
}
// Inside a cluster, Delete stops at that cluster's end and leaves the next flag intact.
let mut s = session();
let title = s.doc().unwrap().current_slide().unwrap().shapes[0].id.0;
s.execute("text.edit", &json!({"id": title})).unwrap();
s.execute("text.selectAll", &json!({})).unwrap();
s.execute("text.insert", &json!({"text": "\u{1F1FA}\u{1F1F8}\u{1F1E8}\u{1F1E6}"})).unwrap();
s.execute("text.move", &json!({"to": "start"})).unwrap();
s.execute("text.move", &json!({"to": "right"})).unwrap();
s.execute("text.delete", &json!({"dir": "forward"})).unwrap();
let t = s.execute("text.get", &json!({"id": title})).unwrap();
assert_eq!(t["text"], "\u{1F1FA}\u{1F1E8}\u{1F1E6}");
}
/// `text.set` treats CRLF (or a lone CR) as one paragraph break and keeps no `\r` in the text.
#[test]
fn text_set_crlf_makes_clean_paragraphs() {
+16
View File
@@ -3,6 +3,7 @@
use crate::text::{Paragraph, Run, RunKind, RunProps, TextBody};
use crate::words;
use unicode_segmentation::UnicodeSegmentation;
/// (paragraph, char offset in the paragraph).
pub type Pos = (usize, usize);
@@ -419,6 +420,21 @@ fn smart_space(body: &mut TextBody, at: Pos) -> Pos {
at
}
/// The position after the grapheme cluster at `pos`, or the next paragraph's start at a paragraph end.
pub fn cluster_end(body: &TextBody, pos: Pos) -> Pos {
let (pi, ch) = clamp(body, pos);
let text = body.paragraphs.get(pi).map(Paragraph::text).unwrap_or_default();
// Boundaries come from the whole paragraph, so a caret inside a cluster deletes only to its end.
let mut end = 0;
for g in text.graphemes(true) {
end += g.chars().count();
if end > ch {
return (pi, end);
}
}
char_move(body, pos, true)
}
/// One character left/right across paragraph boundaries.
pub fn char_move(body: &TextBody, pos: Pos, forward: bool) -> Pos {
let (pi, ch) = clamp(body, pos);