diff --git a/helix-core/src/movement.rs b/helix-core/src/movement.rs
index bbb37bf4..60af47e5 100644
--- a/helix-core/src/movement.rs
+++ b/helix-core/src/movement.rs
@@ -1474,7 +1474,7 @@ mod test {
             let text = Rope::from(s.as_str());
             let selection =
                 selection.transform(|r| move_prev_paragraph(text.slice(..), r, 1, Movement::Move));
-            let actual = crate::test::plain(&s, &selection);
+            let actual = crate::test::plain(s.as_ref(), &selection);
             assert_eq!(actual, expected, "\nbefore: `{:?}`", before);
         }
     }
@@ -1497,7 +1497,7 @@ mod test {
             let text = Rope::from(s.as_str());
             let selection =
                 selection.transform(|r| move_prev_paragraph(text.slice(..), r, 2, Movement::Move));
-            let actual = crate::test::plain(&s, &selection);
+            let actual = crate::test::plain(s.as_ref(), &selection);
             assert_eq!(actual, expected, "\nbefore: `{:?}`", before);
         }
     }
@@ -1520,7 +1520,7 @@ mod test {
             let text = Rope::from(s.as_str());
             let selection = selection
                 .transform(|r| move_prev_paragraph(text.slice(..), r, 1, Movement::Extend));
-            let actual = crate::test::plain(&s, &selection);
+            let actual = crate::test::plain(s.as_ref(), &selection);
             assert_eq!(actual, expected, "\nbefore: `{:?}`", before);
         }
     }
@@ -1562,7 +1562,7 @@ mod test {
             let text = Rope::from(s.as_str());
             let selection =
                 selection.transform(|r| move_next_paragraph(text.slice(..), r, 1, Movement::Move));
-            let actual = crate::test::plain(&s, &selection);
+            let actual = crate::test::plain(s.as_ref(), &selection);
             assert_eq!(actual, expected, "\nbefore: `{:?}`", before);
         }
     }
@@ -1585,7 +1585,7 @@ mod test {
             let text = Rope::from(s.as_str());
             let selection =
                 selection.transform(|r| move_next_paragraph(text.slice(..), r, 2, Movement::Move));
-            let actual = crate::test::plain(&s, &selection);
+            let actual = crate::test::plain(s.as_ref(), &selection);
             assert_eq!(actual, expected, "\nbefore: `{:?}`", before);
         }
     }
@@ -1608,7 +1608,7 @@ mod test {
             let text = Rope::from(s.as_str());
             let selection = selection
                 .transform(|r| move_next_paragraph(text.slice(..), r, 1, Movement::Extend));
-            let actual = crate::test::plain(&s, &selection);
+            let actual = crate::test::plain(s.as_ref(), &selection);
             assert_eq!(actual, expected, "\nbefore: `{:?}`", before);
         }
     }
diff --git a/helix-core/src/test.rs b/helix-core/src/test.rs
index 1d967f23..7183302c 100644
--- a/helix-core/src/test.rs
+++ b/helix-core/src/test.rs
@@ -1,5 +1,6 @@
 //! Test helpers.
 use crate::{Range, Selection};
+use ropey::Rope;
 use smallvec::SmallVec;
 use std::cmp::Reverse;
 use unicode_segmentation::UnicodeSegmentation;
@@ -148,10 +149,12 @@ pub fn print(s: &str) -> (String, Selection) {
 ///     "#[a|]#b#(|c)#".to_owned()
 /// );
 /// ```
-pub fn plain(s: &str, selection: &Selection) -> String {
+pub fn plain<R: Into<Rope>>(s: R, selection: &Selection) -> String {
+    let s = s.into();
     let primary = selection.primary_index();
-    let mut out = String::with_capacity(s.len() + 5 * selection.len());
-    out.push_str(s);
+    let mut out = String::with_capacity(s.len_bytes() + 5 * selection.len());
+    out.push_str(&s.to_string());
+
     let mut insertion: Vec<_> = selection
         .iter()
         .enumerate()
@@ -164,7 +167,9 @@ pub fn plain(s: &str, selection: &Selection) -> String {
                 (false, false) => [(range.anchor, ")#"), (range.head, "#(|")],
             }
         })
+        .map(|(char_idx, marker)| (s.char_to_byte(char_idx), marker))
         .collect();
+
     // insert in reverse order
     insertion.sort_unstable_by_key(|k| Reverse(k.0));
     for (i, s) in insertion {
@@ -173,7 +178,6 @@ pub fn plain(s: &str, selection: &Selection) -> String {
     out
 }
 
-#[allow(clippy::module_inception)]
 #[cfg(test)]
 #[allow(clippy::module_inception)]
 mod test {
@@ -289,4 +293,94 @@ mod test {
             print("hello #[|πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦]# goodbye")
         );
     }
+
+    #[test]
+    fn plain_single() {
+        assert_eq!("#[|h]#ello", plain("hello", &Selection::single(1, 0)));
+        assert_eq!("#[h|]#ello", plain("hello", &Selection::single(0, 1)));
+        assert_eq!("#[|hell]#o", plain("hello", &Selection::single(4, 0)));
+        assert_eq!("#[hell|]#o", plain("hello", &Selection::single(0, 4)));
+        assert_eq!("#[|hello]#", plain("hello", &Selection::single(5, 0)));
+        assert_eq!("#[hello|]#", plain("hello", &Selection::single(0, 5)));
+    }
+
+    #[test]
+    fn plain_multi() {
+        assert_eq!(
+            plain(
+                "hello",
+                &Selection::new(
+                    SmallVec::from_slice(&[Range::new(1, 0), Range::new(5, 4)]),
+                    0
+                )
+            ),
+            String::from("#[|h]#ell#(|o)#")
+        );
+        assert_eq!(
+            plain(
+                "hello",
+                &Selection::new(
+                    SmallVec::from_slice(&[Range::new(0, 1), Range::new(4, 5)]),
+                    0
+                )
+            ),
+            String::from("#[h|]#ell#(o|)#")
+        );
+        assert_eq!(
+            plain(
+                "hello",
+                &Selection::new(
+                    SmallVec::from_slice(&[Range::new(2, 0), Range::new(5, 3)]),
+                    0
+                )
+            ),
+            String::from("#[|he]#l#(|lo)#")
+        );
+        assert_eq!(
+            plain(
+                "hello\r\nhello\r\nhello\r\n",
+                &Selection::new(
+                    SmallVec::from_slice(&[
+                        Range::new(7, 5),
+                        Range::new(21, 19),
+                        Range::new(14, 12)
+                    ]),
+                    0
+                )
+            ),
+            String::from("hello#[|\r\n]#hello#(|\r\n)#hello#(|\r\n)#")
+        );
+    }
+
+    #[test]
+    fn plain_multi_byte_code_point() {
+        assert_eq!(
+            plain("β€žβ€œ", &Selection::single(1, 0)),
+            String::from("#[|β€ž]#β€œ")
+        );
+        assert_eq!(
+            plain("β€žβ€œ", &Selection::single(2, 1)),
+            String::from("β€ž#[|β€œ]#")
+        );
+        assert_eq!(
+            plain("β€žβ€œ", &Selection::single(0, 1)),
+            String::from("#[β€ž|]#β€œ")
+        );
+        assert_eq!(
+            plain("β€žβ€œ", &Selection::single(1, 2)),
+            String::from("β€ž#[β€œ|]#")
+        );
+        assert_eq!(
+            plain("they said β€žhelloβ€œ", &Selection::single(11, 10)),
+            String::from("they said #[|β€ž]#helloβ€œ")
+        );
+    }
+
+    #[test]
+    fn plain_multi_code_point_grapheme() {
+        assert_eq!(
+            plain("hello πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦ goodbye", &Selection::single(13, 6)),
+            String::from("hello #[|πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦]# goodbye")
+        );
+    }
 }
diff --git a/helix-core/src/textobject.rs b/helix-core/src/textobject.rs
index 6e3f18cf..bf00a458 100644
--- a/helix-core/src/textobject.rs
+++ b/helix-core/src/textobject.rs
@@ -437,7 +437,7 @@ mod test {
             let text = Rope::from(s.as_str());
             let selection = selection
                 .transform(|r| textobject_paragraph(text.slice(..), r, TextObject::Inside, 1));
-            let actual = crate::test::plain(&s, &selection);
+            let actual = crate::test::plain(s.as_ref(), &selection);
             assert_eq!(actual, expected, "\nbefore: `{:?}`", before);
         }
     }
@@ -460,7 +460,7 @@ mod test {
             let text = Rope::from(s.as_str());
             let selection = selection
                 .transform(|r| textobject_paragraph(text.slice(..), r, TextObject::Inside, 2));
-            let actual = crate::test::plain(&s, &selection);
+            let actual = crate::test::plain(s.as_ref(), &selection);
             assert_eq!(actual, expected, "\nbefore: `{:?}`", before);
         }
     }
@@ -491,7 +491,7 @@ mod test {
             let text = Rope::from(s.as_str());
             let selection = selection
                 .transform(|r| textobject_paragraph(text.slice(..), r, TextObject::Around, 1));
-            let actual = crate::test::plain(&s, &selection);
+            let actual = crate::test::plain(s.as_ref(), &selection);
             assert_eq!(actual, expected, "\nbefore: `{:?}`", before);
         }
     }
diff --git a/helix-term/tests/test/helpers.rs b/helix-term/tests/test/helpers.rs
index de81f758..77332fa5 100644
--- a/helix-term/tests/test/helpers.rs
+++ b/helix-term/tests/test/helpers.rs
@@ -65,7 +65,7 @@ pub async fn test_key_sequences(
 
     for (i, (in_keys, test_fn)) in inputs.into_iter().enumerate() {
         let (view, doc) = current_ref!(app.editor);
-        let state = test::plain(&doc.text().to_string(), doc.selection(view.id));
+        let state = test::plain(doc.text().slice(..), doc.selection(view.id));
 
         log::debug!("executing test with document state:\n\n-----\n\n{}", state);
 
@@ -81,7 +81,7 @@ pub async fn test_key_sequences(
 
         if !app_exited {
             let (view, doc) = current_ref!(app.editor);
-            let state = test::plain(&doc.text().to_string(), doc.selection(view.id));
+            let state = test::plain(doc.text().slice(..), doc.selection(view.id));
 
             log::debug!(
                 "finished running test with document state:\n\n-----\n\n{}",