diff --git a/helix-core/src/movement.rs b/helix-core/src/movement.rs
index 0e2a2a42..2cb4b40d 100644
--- a/helix-core/src/movement.rs
+++ b/helix-core/src/movement.rs
@@ -56,7 +56,7 @@ pub fn move_horizontally(
     };
 
     // Compute the final new range.
-    range.put(slice, behaviour == Extend, new_pos)
+    range.put(slice, new_pos, behaviour == Extend)
 }
 
 pub fn move_vertically(
@@ -106,7 +106,7 @@ pub fn move_vertically(
                 new_pos
             };
 
-            let mut new_range = range.put(slice, true, new_head);
+            let mut new_range = range.put(slice, new_head, true);
             new_range.horiz = Some(horiz);
             new_range
         }
@@ -427,7 +427,7 @@ mod test {
     #[test]
     fn vertical_moves_in_single_column() {
         let text = Rope::from(MULTILINE_SAMPLE);
-        let slice = dbg!(&text).slice(..);
+        let slice = text.slice(..);
         let position = pos_at_coords(slice, (0, 0).into());
         let mut range = Range::point(position);
         let moves_and_expected_coordinates = IntoIter::new([
diff --git a/helix-core/src/search.rs b/helix-core/src/search.rs
index 73be68c7..d4eb11a9 100644
--- a/helix-core/src/search.rs
+++ b/helix-core/src/search.rs
@@ -7,12 +7,11 @@ pub fn find_nth_next(
     n: usize,
     inclusive: bool,
 ) -> Option<usize> {
-    if pos >= text.len_chars() {
+    if pos >= text.len_chars() || n == 0 {
         return None;
     }
 
-    // start searching right after pos
-    let mut chars = text.chars_at(pos + 1);
+    let mut chars = text.chars_at(pos);
 
     for _ in 0..n {
         loop {
@@ -40,14 +39,17 @@ pub fn find_nth_prev(
     n: usize,
     inclusive: bool,
 ) -> Option<usize> {
-    // start searching right before pos
+    if pos == 0 || n == 0 {
+        return None;
+    }
+
     let mut chars = text.chars_at(pos);
 
     for _ in 0..n {
         loop {
             let c = chars.prev()?;
 
-            pos = pos.saturating_sub(1);
+            pos -= 1;
 
             if c == ch {
                 break;
diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs
index 8951899b..21a6c108 100644
--- a/helix-core/src/selection.rs
+++ b/helix-core/src/selection.rs
@@ -217,7 +217,7 @@ impl Range {
     /// grapheme-aligned.
     #[must_use]
     #[inline]
-    pub fn put(self, text: RopeSlice, extend: bool, char_idx: usize) -> Range {
+    pub fn put(self, text: RopeSlice, char_idx: usize, extend: bool) -> Range {
         let anchor = if !extend {
             char_idx
         } else if self.head >= self.anchor && char_idx < self.anchor {
diff --git a/helix-core/src/surround.rs b/helix-core/src/surround.rs
index 52f60cab..af357c96 100644
--- a/helix-core/src/surround.rs
+++ b/helix-core/src/surround.rs
@@ -1,3 +1,4 @@
+use crate::graphemes::next_grapheme_boundary;
 use crate::{search, Selection};
 use ropey::RopeSlice;
 
@@ -40,23 +41,35 @@ pub fn find_nth_pairs_pos(
 ) -> Option<(usize, usize)> {
     let (open, close) = get_pair(ch);
 
-    let (open_pos, close_pos) = if open == close {
-        let prev = search::find_nth_prev(text, open, pos, n, true);
-        let next = search::find_nth_next(text, close, pos, n, true);
-        if text.char(pos) == open {
-            // cursor is *on* a pair
-            next.map(|n| (pos, n)).or_else(|| prev.map(|p| (p, pos)))?
+    if text.len_chars() < 2 || pos >= text.len_chars() {
+        return None;
+    }
+
+    if open == close {
+        if Some(open) == text.get_char(pos) {
+            // Special case: cursor is directly on a matching char.
+            match pos {
+                0 => Some((pos, search::find_nth_next(text, close, pos + 1, n, true)?)),
+                _ if (pos + 1) == text.len_chars() => Some((
+                    search::find_nth_prev(text, open, pos, n, true)?,
+                    text.len_chars(),
+                )),
+                // We return no match because there's no way to know which
+                // side of the char we should be searching on.
+                _ => None,
+            }
         } else {
-            (prev?, next?)
+            Some((
+                search::find_nth_prev(text, open, pos, n, true)?,
+                search::find_nth_next(text, close, pos, n, true)?,
+            ))
         }
     } else {
-        (
+        Some((
             find_nth_open_pair(text, open, close, pos, n)?,
-            find_nth_close_pair(text, open, close, pos, n)?,
-        )
-    };
-
-    Some((open_pos, close_pos))
+            next_grapheme_boundary(text, find_nth_close_pair(text, open, close, pos, n)?),
+        ))
+    }
 }
 
 fn find_nth_open_pair(
@@ -173,12 +186,13 @@ mod test {
         let slice = doc.slice(..);
 
         // cursor on [t]ext
-        assert_eq!(find_nth_pairs_pos(slice, '(', 6, 1), Some((5, 10)));
-        assert_eq!(find_nth_pairs_pos(slice, ')', 6, 1), Some((5, 10)));
+        assert_eq!(find_nth_pairs_pos(slice, '(', 6, 1), Some((5, 11)));
+        assert_eq!(find_nth_pairs_pos(slice, ')', 6, 1), Some((5, 11)));
         // cursor on so[m]e
         assert_eq!(find_nth_pairs_pos(slice, '(', 2, 1), None);
         // cursor on bracket itself
-        assert_eq!(find_nth_pairs_pos(slice, '(', 5, 1), Some((5, 10)));
+        assert_eq!(find_nth_pairs_pos(slice, '(', 5, 1), Some((5, 11)));
+        assert_eq!(find_nth_pairs_pos(slice, '(', 10, 1), Some((5, 11)));
     }
 
     #[test]
@@ -187,9 +201,9 @@ mod test {
         let slice = doc.slice(..);
 
         // cursor on go[o]d
-        assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((10, 15)));
-        assert_eq!(find_nth_pairs_pos(slice, '(', 13, 2), Some((4, 21)));
-        assert_eq!(find_nth_pairs_pos(slice, '(', 13, 3), Some((0, 27)));
+        assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((10, 16)));
+        assert_eq!(find_nth_pairs_pos(slice, '(', 13, 2), Some((4, 22)));
+        assert_eq!(find_nth_pairs_pos(slice, '(', 13, 3), Some((0, 28)));
     }
 
     #[test]
@@ -198,14 +212,14 @@ mod test {
         let slice = doc.slice(..);
 
         // cursor on go[o]d
-        assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 1), Some((10, 15)));
-        assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 2), Some((4, 21)));
-        assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 3), Some((0, 27)));
+        assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 1), Some((10, 16)));
+        assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 2), Some((4, 22)));
+        assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 3), Some((0, 28)));
         // cursor on the quotes
-        assert_eq!(find_nth_pairs_pos(slice, '\'', 10, 1), Some((10, 15)));
+        assert_eq!(find_nth_pairs_pos(slice, '\'', 10, 1), None);
         // this is the best we can do since opening and closing pairs are same
-        assert_eq!(find_nth_pairs_pos(slice, '\'', 0, 1), Some((0, 4)));
-        assert_eq!(find_nth_pairs_pos(slice, '\'', 27, 1), Some((21, 27)));
+        assert_eq!(find_nth_pairs_pos(slice, '\'', 0, 1), Some((0, 5)));
+        assert_eq!(find_nth_pairs_pos(slice, '\'', 27, 1), Some((21, 28)));
     }
 
     #[test]
@@ -214,8 +228,8 @@ mod test {
         let slice = doc.slice(..);
 
         // cursor on go[o]d
-        assert_eq!(find_nth_pairs_pos(slice, '(', 15, 1), Some((5, 24)));
-        assert_eq!(find_nth_pairs_pos(slice, '(', 15, 2), Some((0, 31)));
+        assert_eq!(find_nth_pairs_pos(slice, '(', 15, 1), Some((5, 25)));
+        assert_eq!(find_nth_pairs_pos(slice, '(', 15, 2), Some((0, 32)));
     }
 
     #[test]
@@ -224,9 +238,9 @@ mod test {
         let slice = doc.slice(..);
 
         // cursor on go[o]d
-        assert_eq!(find_nth_pairs_pos(slice, '{', 13, 1), Some((10, 15)));
-        assert_eq!(find_nth_pairs_pos(slice, '[', 13, 1), Some((4, 21)));
-        assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((0, 27)));
+        assert_eq!(find_nth_pairs_pos(slice, '{', 13, 1), Some((10, 16)));
+        assert_eq!(find_nth_pairs_pos(slice, '[', 13, 1), Some((4, 22)));
+        assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((0, 28)));
     }
 
     #[test]
@@ -243,7 +257,7 @@ mod test {
             get_surround_pos(slice, &selection, '(', 1)
                 .unwrap()
                 .as_slice(),
-            &[0, 5, 7, 13, 15, 23]
+            &[0, 6, 7, 14, 15, 24]
         );
     }
 
diff --git a/helix-core/src/textobject.rs b/helix-core/src/textobject.rs
index fbf66256..ae18d7cf 100644
--- a/helix-core/src/textobject.rs
+++ b/helix-core/src/textobject.rs
@@ -1,21 +1,16 @@
 use ropey::RopeSlice;
 
-use crate::chars::{categorize_char, char_is_line_ending, char_is_whitespace, CharCategory};
-use crate::movement::{self, Direction};
+use crate::chars::{categorize_char, CharCategory};
+use crate::graphemes::{next_grapheme_boundary, prev_grapheme_boundary};
+use crate::movement::Direction;
 use crate::surround;
 use crate::Range;
 
-fn this_word_end_pos(slice: RopeSlice, pos: usize) -> usize {
-    this_word_bound_pos(slice, pos, Direction::Forward)
-}
+fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction) -> usize {
+    use CharCategory::{Eol, Whitespace};
 
-fn this_word_start_pos(slice: RopeSlice, pos: usize) -> usize {
-    this_word_bound_pos(slice, pos, Direction::Backward)
-}
-
-fn this_word_bound_pos(slice: RopeSlice, mut pos: usize, direction: Direction) -> usize {
     let iter = match direction {
-        Direction::Forward => slice.chars_at(pos + 1),
+        Direction::Forward => slice.chars_at(pos),
         Direction::Backward => {
             let mut iter = slice.chars_at(pos);
             iter.reverse();
@@ -23,25 +18,32 @@ fn this_word_bound_pos(slice: RopeSlice, mut pos: usize, direction: Direction) -
         }
     };
 
-    match categorize_char(slice.char(pos)) {
-        CharCategory::Eol | CharCategory::Whitespace => pos,
-        category => {
-            for peek in iter {
-                let curr_category = categorize_char(peek);
-                if curr_category != category
-                    || curr_category == CharCategory::Eol
-                    || curr_category == CharCategory::Whitespace
-                {
+    let mut prev_category = match direction {
+        Direction::Forward if pos == 0 => Whitespace,
+        Direction::Forward => categorize_char(slice.char(pos - 1)),
+        Direction::Backward if pos == slice.len_chars() => Whitespace,
+        Direction::Backward => categorize_char(slice.char(pos)),
+    };
+
+    for ch in iter {
+        match categorize_char(ch) {
+            Eol | Whitespace => return pos,
+            category => {
+                if category != prev_category && pos != 0 && pos != slice.len_chars() {
                     return pos;
-                }
-                pos = match direction {
-                    Direction::Forward => pos + 1,
-                    Direction::Backward => pos.saturating_sub(1),
+                } else {
+                    if direction == Direction::Forward {
+                        pos += 1;
+                    } else {
+                        pos = pos.saturating_sub(1);
+                    }
+                    prev_category = category;
                 }
             }
-            pos
         }
     }
+
+    pos
 }
 
 #[derive(Copy, Clone, PartialEq, Eq, Debug)]
@@ -55,46 +57,42 @@ pub fn textobject_word(
     slice: RopeSlice,
     range: Range,
     textobject: TextObject,
-    count: usize,
+    _count: usize,
 ) -> Range {
-    let this_word_start = this_word_start_pos(slice, range.head);
-    let this_word_end = this_word_end_pos(slice, range.head);
-
-    let (anchor, head);
-    match textobject {
-        TextObject::Inside => {
-            anchor = this_word_start;
-            head = this_word_end;
-        }
-        TextObject::Around => {
-            if slice
-                .get_char(this_word_end + 1)
-                .map_or(true, char_is_line_ending)
-            {
-                head = this_word_end;
-                if slice
-                    .get_char(this_word_start.saturating_sub(1))
-                    .map_or(true, char_is_line_ending)
-                {
-                    // single word on a line
-                    anchor = this_word_start;
-                } else {
-                    // last word on a line, select the whitespace before it too
-                    anchor = movement::move_prev_word_end(slice, range, count).head;
-                }
-            } else if char_is_whitespace(slice.char(range.head)) {
-                // select whole whitespace and next word
-                head = movement::move_next_word_end(slice, range, count).head;
-                anchor = movement::backwards_skip_while(slice, range.head, |c| c.is_whitespace())
-                    .map(|p| p + 1) // p is first *non* whitespace char, so +1 to get whitespace pos
-                    .unwrap_or(0);
-            } else {
-                head = movement::move_next_word_start(slice, range, count).head;
-                anchor = this_word_start;
-            }
-        }
+    // For 1-width cursor semantics.
+    let head = if range.head > range.anchor {
+        prev_grapheme_boundary(slice, range.head)
+    } else {
+        range.head
     };
-    Range::new(anchor, head)
+
+    let word_start = find_word_boundary(slice, head, Direction::Backward);
+    let word_end = match slice.get_char(head).map(categorize_char) {
+        None | Some(CharCategory::Whitespace | CharCategory::Eol) => head,
+        _ => find_word_boundary(slice, head + 1, Direction::Forward),
+    };
+
+    // Special case.
+    if word_start == word_end {
+        return Range::new(word_start, word_end);
+    }
+
+    match textobject {
+        TextObject::Inside => Range::new(word_start, word_end),
+        TextObject::Around => Range::new(
+            match slice
+                .get_char(word_start.saturating_sub(1))
+                .map(categorize_char)
+            {
+                None | Some(CharCategory::Eol) => word_start,
+                _ => prev_grapheme_boundary(slice, word_start),
+            },
+            match slice.get_char(word_end).map(categorize_char) {
+                None | Some(CharCategory::Eol) => word_end,
+                _ => next_grapheme_boundary(slice, word_end),
+            },
+        ),
+    }
 }
 
 pub fn textobject_surround(
@@ -106,7 +104,10 @@ pub fn textobject_surround(
 ) -> Range {
     surround::find_nth_pairs_pos(slice, ch, range.head, count)
         .map(|(anchor, head)| match textobject {
-            TextObject::Inside => Range::new(anchor + 1, head.saturating_sub(1)),
+            TextObject::Inside => Range::new(
+                next_grapheme_boundary(slice, anchor),
+                prev_grapheme_boundary(slice, head),
+            ),
             TextObject::Around => Range::new(anchor, head),
         })
         .unwrap_or(range)
@@ -126,70 +127,70 @@ mod test {
         let tests = &[
             (
                 "cursor at beginning of doc",
-                vec![(0, Inside, (0, 5)), (0, Around, (0, 6))],
+                vec![(0, Inside, (0, 6)), (0, Around, (0, 7))],
             ),
             (
                 "cursor at middle of word",
                 vec![
-                    (13, Inside, (10, 15)),
-                    (10, Inside, (10, 15)),
-                    (15, Inside, (10, 15)),
-                    (13, Around, (10, 16)),
-                    (10, Around, (10, 16)),
-                    (15, Around, (10, 16)),
+                    (13, Inside, (10, 16)),
+                    (10, Inside, (10, 16)),
+                    (15, Inside, (10, 16)),
+                    (13, Around, (9, 17)),
+                    (10, Around, (9, 17)),
+                    (15, Around, (9, 17)),
                 ],
             ),
             (
                 "cursor between word whitespace",
-                vec![(6, Inside, (6, 6)), (6, Around, (6, 13))],
+                vec![(6, Inside, (6, 6)), (6, Around, (6, 6))],
             ),
             (
                 "cursor on word before newline\n",
                 vec![
-                    (22, Inside, (22, 28)),
-                    (28, Inside, (22, 28)),
-                    (25, Inside, (22, 28)),
-                    (22, Around, (21, 28)),
-                    (28, Around, (21, 28)),
-                    (25, Around, (21, 28)),
+                    (22, Inside, (22, 29)),
+                    (28, Inside, (22, 29)),
+                    (25, Inside, (22, 29)),
+                    (22, Around, (21, 29)),
+                    (28, Around, (21, 29)),
+                    (25, Around, (21, 29)),
                 ],
             ),
             (
                 "cursor on newline\nnext line",
-                vec![(17, Inside, (17, 17)), (17, Around, (17, 22))],
+                vec![(17, Inside, (17, 17)), (17, Around, (17, 17))],
             ),
             (
                 "cursor on word after newline\nnext line",
                 vec![
-                    (29, Inside, (29, 32)),
-                    (30, Inside, (29, 32)),
-                    (32, Inside, (29, 32)),
-                    (29, Around, (29, 33)),
-                    (30, Around, (29, 33)),
-                    (32, Around, (29, 33)),
+                    (29, Inside, (29, 33)),
+                    (30, Inside, (29, 33)),
+                    (32, Inside, (29, 33)),
+                    (29, Around, (29, 34)),
+                    (30, Around, (29, 34)),
+                    (32, Around, (29, 34)),
                 ],
             ),
             (
                 "cursor on #$%:;* punctuation",
                 vec![
-                    (13, Inside, (10, 15)),
-                    (10, Inside, (10, 15)),
-                    (15, Inside, (10, 15)),
-                    (13, Around, (10, 16)),
-                    (10, Around, (10, 16)),
-                    (15, Around, (10, 16)),
+                    (13, Inside, (10, 16)),
+                    (10, Inside, (10, 16)),
+                    (15, Inside, (10, 16)),
+                    (13, Around, (9, 17)),
+                    (10, Around, (9, 17)),
+                    (15, Around, (9, 17)),
                 ],
             ),
             (
                 "cursor on punc%^#$:;.tuation",
                 vec![
-                    (14, Inside, (14, 20)),
-                    (20, Inside, (14, 20)),
-                    (17, Inside, (14, 20)),
-                    (14, Around, (14, 20)),
+                    (14, Inside, (14, 21)),
+                    (20, Inside, (14, 21)),
+                    (17, Inside, (14, 21)),
+                    (14, Around, (13, 22)),
                     // FIXME: edge case
                     // (20, Around, (14, 20)),
-                    (17, Around, (14, 20)),
+                    (17, Around, (13, 22)),
                 ],
             ),
             (
@@ -198,14 +199,14 @@ mod test {
                     (9, Inside, (9, 9)),
                     (10, Inside, (10, 10)),
                     (11, Inside, (11, 11)),
-                    (9, Around, (9, 16)),
-                    (10, Around, (9, 16)),
-                    (11, Around, (9, 16)),
+                    (9, Around, (9, 9)),
+                    (10, Around, (10, 10)),
+                    (11, Around, (11, 11)),
                 ],
             ),
             (
                 "cursor at end of doc",
-                vec![(19, Inside, (17, 19)), (19, Around, (16, 19))],
+                vec![(19, Inside, (17, 20)), (19, Around, (16, 20))],
             ),
         ];
 
@@ -234,67 +235,67 @@ mod test {
                 "simple (single) surround pairs",
                 vec![
                     (3, Inside, (3, 3), '(', 1),
-                    (7, Inside, (8, 13), ')', 1),
-                    (10, Inside, (8, 13), '(', 1),
-                    (14, Inside, (8, 13), ')', 1),
+                    (7, Inside, (8, 14), ')', 1),
+                    (10, Inside, (8, 14), '(', 1),
+                    (14, Inside, (8, 14), ')', 1),
                     (3, Around, (3, 3), '(', 1),
-                    (7, Around, (7, 14), ')', 1),
-                    (10, Around, (7, 14), '(', 1),
-                    (14, Around, (7, 14), ')', 1),
+                    (7, Around, (7, 15), ')', 1),
+                    (10, Around, (7, 15), '(', 1),
+                    (14, Around, (7, 15), ')', 1),
                 ],
             ),
             (
                 "samexx 'single' surround pairs",
                 vec![
                     (3, Inside, (3, 3), '\'', 1),
-                    (7, Inside, (8, 13), '\'', 1),
-                    (10, Inside, (8, 13), '\'', 1),
-                    (14, Inside, (8, 13), '\'', 1),
+                    (7, Inside, (7, 7), '\'', 1),
+                    (10, Inside, (8, 14), '\'', 1),
+                    (14, Inside, (14, 14), '\'', 1),
                     (3, Around, (3, 3), '\'', 1),
-                    (7, Around, (7, 14), '\'', 1),
-                    (10, Around, (7, 14), '\'', 1),
-                    (14, Around, (7, 14), '\'', 1),
+                    (7, Around, (7, 7), '\'', 1),
+                    (10, Around, (7, 15), '\'', 1),
+                    (14, Around, (14, 14), '\'', 1),
                 ],
             ),
             (
                 "(nested (surround (pairs)) 3 levels)",
                 vec![
-                    (0, Inside, (1, 34), '(', 1),
-                    (6, Inside, (1, 34), ')', 1),
-                    (8, Inside, (9, 24), '(', 1),
-                    (8, Inside, (9, 34), ')', 2),
-                    (20, Inside, (9, 24), '(', 2),
-                    (20, Inside, (1, 34), ')', 3),
-                    (0, Around, (0, 35), '(', 1),
-                    (6, Around, (0, 35), ')', 1),
-                    (8, Around, (8, 25), '(', 1),
-                    (8, Around, (8, 35), ')', 2),
-                    (20, Around, (8, 25), '(', 2),
-                    (20, Around, (0, 35), ')', 3),
+                    (0, Inside, (1, 35), '(', 1),
+                    (6, Inside, (1, 35), ')', 1),
+                    (8, Inside, (9, 25), '(', 1),
+                    (8, Inside, (9, 35), ')', 2),
+                    (20, Inside, (9, 25), '(', 2),
+                    (20, Inside, (1, 35), ')', 3),
+                    (0, Around, (0, 36), '(', 1),
+                    (6, Around, (0, 36), ')', 1),
+                    (8, Around, (8, 26), '(', 1),
+                    (8, Around, (8, 36), ')', 2),
+                    (20, Around, (8, 26), '(', 2),
+                    (20, Around, (0, 36), ')', 3),
                 ],
             ),
             (
                 "(mixed {surround [pair] same} line)",
                 vec![
-                    (2, Inside, (1, 33), '(', 1),
-                    (9, Inside, (8, 27), '{', 1),
-                    (18, Inside, (18, 21), '[', 1),
-                    (2, Around, (0, 34), '(', 1),
-                    (9, Around, (7, 28), '{', 1),
-                    (18, Around, (17, 22), '[', 1),
+                    (2, Inside, (1, 34), '(', 1),
+                    (9, Inside, (8, 28), '{', 1),
+                    (18, Inside, (18, 22), '[', 1),
+                    (2, Around, (0, 35), '(', 1),
+                    (9, Around, (7, 29), '{', 1),
+                    (18, Around, (17, 23), '[', 1),
                 ],
             ),
             (
                 "(stepped (surround) pairs (should) skip)",
-                vec![(22, Inside, (1, 38), '(', 1), (22, Around, (0, 39), '(', 1)],
+                vec![(22, Inside, (1, 39), '(', 1), (22, Around, (0, 40), '(', 1)],
             ),
             (
                 "[surround pairs{\non different]\nlines}",
                 vec![
-                    (7, Inside, (1, 28), '[', 1),
-                    (15, Inside, (16, 35), '{', 1),
-                    (7, Around, (0, 29), '[', 1),
-                    (15, Around, (15, 36), '{', 1),
+                    (7, Inside, (1, 29), '[', 1),
+                    (15, Inside, (16, 36), '{', 1),
+                    (7, Around, (0, 30), '[', 1),
+                    (15, Around, (15, 37), '{', 1),
                 ],
             ),
         ];
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index fbeae5ff..51e633f6 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -1,9 +1,6 @@
 use helix_core::{
     comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes, indent,
-    line_ending::{
-        get_line_ending_of_str, line_end_char_index, rope_end_without_line_ending,
-        str_is_line_ending,
-    },
+    line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending},
     match_brackets,
     movement::{self, Direction},
     object, pos_at_coords,
@@ -392,20 +389,14 @@ fn goto_line_end(cx: &mut Context) {
     doc.set_selection(
         view.id,
         doc.selection(view.id).clone().transform(|range| {
-            let text = doc.text();
+            let text = doc.text().slice(..);
             let line = text.char_to_line(range.head);
 
-            let pos = line_end_char_index(&text.slice(..), line);
-            let pos = graphemes::nth_prev_grapheme_boundary(text.slice(..), pos, 1);
+            let pos = line_end_char_index(&text, line);
+            let pos = graphemes::nth_prev_grapheme_boundary(text, pos, 1);
             let pos = range.head.max(pos).max(text.line_to_char(line));
 
-            Range::new(
-                match doc.mode {
-                    Mode::Normal | Mode::Insert => pos,
-                    Mode::Select => range.anchor,
-                },
-                pos,
-            )
+            range.put(text, pos, doc.mode == Mode::Select)
         }),
     );
 }
@@ -416,11 +407,11 @@ fn goto_line_end_newline(cx: &mut Context) {
     doc.set_selection(
         view.id,
         doc.selection(view.id).clone().transform(|range| {
-            let text = doc.text();
+            let text = doc.text().slice(..);
             let line = text.char_to_line(range.head);
 
-            let pos = line_end_char_index(&text.slice(..), line);
-            Range::new(pos, pos)
+            let pos = line_end_char_index(&text, line);
+            range.put(text, pos, doc.mode == Mode::Select)
         }),
     );
 }
@@ -430,18 +421,12 @@ fn goto_line_start(cx: &mut Context) {
     doc.set_selection(
         view.id,
         doc.selection(view.id).clone().transform(|range| {
-            let text = doc.text();
+            let text = doc.text().slice(..);
             let line = text.char_to_line(range.head);
 
             // adjust to start of the line
             let pos = text.line_to_char(line);
-            Range::new(
-                match doc.mode {
-                    Mode::Normal | Mode::Insert => pos,
-                    Mode::Select => range.anchor,
-                },
-                pos,
-            )
+            range.put(text, pos, doc.mode == Mode::Select)
         }),
     );
 }
@@ -451,18 +436,12 @@ fn goto_first_nonwhitespace(cx: &mut Context) {
     doc.set_selection(
         view.id,
         doc.selection(view.id).clone().transform(|range| {
-            let text = doc.text();
+            let text = doc.text().slice(..);
             let line_idx = text.char_to_line(range.head);
 
             if let Some(pos) = find_first_non_whitespace_char(text.line(line_idx)) {
                 let pos = pos + text.line_to_char(line_idx);
-                Range::new(
-                    match doc.mode {
-                        Mode::Normal | Mode::Insert => pos,
-                        Mode::Select => range.anchor,
-                    },
-                    pos,
-                )
+                range.put(text, pos, doc.mode == Mode::Select)
             } else {
                 range
             }
@@ -581,9 +560,7 @@ fn goto_file_start(cx: &mut Context) {
 fn goto_file_end(cx: &mut Context) {
     push_jump(cx.editor);
     let (view, doc) = current!(cx.editor);
-    let text = doc.text();
-    let last_line = text.line_to_char(text.len_lines().saturating_sub(2));
-    doc.set_selection(view.id, Selection::point(last_line));
+    doc.set_selection(view.id, Selection::point(doc.text().len_chars()));
 }
 
 fn extend_next_word_start(cx: &mut Context) {
@@ -592,9 +569,10 @@ fn extend_next_word_start(cx: &mut Context) {
     doc.set_selection(
         view.id,
         doc.selection(view.id).clone().transform(|range| {
-            let word = movement::move_next_word_start(doc.text().slice(..), range, count);
+            let text = doc.text().slice(..);
+            let word = movement::move_next_word_start(text, range, count);
             let pos = word.head;
-            Range::new(range.anchor, pos)
+            range.put(text, pos, true)
         }),
     );
 }
@@ -605,9 +583,10 @@ fn extend_prev_word_start(cx: &mut Context) {
     doc.set_selection(
         view.id,
         doc.selection(view.id).clone().transform(|range| {
-            let word = movement::move_prev_word_start(doc.text().slice(..), range, count);
+            let text = doc.text().slice(..);
+            let word = movement::move_prev_word_start(text, range, count);
             let pos = word.head;
-            Range::new(range.anchor, pos)
+            range.put(text, pos, true)
         }),
     );
 }
@@ -618,9 +597,10 @@ fn extend_next_word_end(cx: &mut Context) {
     doc.set_selection(
         view.id,
         doc.selection(view.id).clone().transform(|range| {
-            let word = movement::move_next_word_end(doc.text().slice(..), range, count);
+            let text = doc.text().slice(..);
+            let word = movement::move_next_word_end(text, range, count);
             let pos = word.head;
-            Range::new(range.anchor, pos)
+            range.put(text, pos, true)
         }),
     );
 }
@@ -669,18 +649,9 @@ where
         doc.set_selection(
             view.id,
             doc.selection(view.id).clone().transform(|range| {
-                search_fn(doc.text().slice(..), ch, range.head, count, inclusive).map_or(
-                    range,
-                    |pos| {
-                        if extend {
-                            Range::new(range.anchor, pos)
-                        } else {
-                            // select
-                            Range::new(range.head, pos)
-                        }
-                        // or (pos, pos) to move to found val
-                    },
-                )
+                let text = doc.text().slice(..);
+                search_fn(text, ch, range.head, count, inclusive)
+                    .map_or(range, |pos| range.put(text, pos, extend))
             }),
         );
     })
@@ -940,7 +911,7 @@ fn extend_line_down(cx: &mut Context) {
 fn select_all(cx: &mut Context) {
     let (view, doc) = current!(cx.editor);
 
-    let end = rope_end_without_line_ending(&doc.text().slice(..));
+    let end = doc.text().len_chars();
     doc.set_selection(view.id, Selection::single(0, end))
 }
 
@@ -997,12 +968,10 @@ fn search_impl(doc: &mut Document, view: &mut View, contents: &str, regex: &Rege
             return;
         }
 
-        let head = end - 1;
-
         let selection = if extend {
-            selection.clone().push(Range::new(start, head))
+            selection.clone().push(Range::new(start, end))
         } else {
-            Selection::single(start, head)
+            Selection::single(start, end)
         };
 
         doc.set_selection(view.id, selection);
@@ -1143,9 +1112,15 @@ fn collapse_selection(cx: &mut Context) {
 
     doc.set_selection(
         view.id,
-        doc.selection(view.id)
-            .clone()
-            .transform(|range| Range::new(range.head, range.head)),
+        doc.selection(view.id).clone().transform(|range| {
+            let pos = if range.head > range.anchor {
+                // For 1-width cursor semantics.
+                graphemes::prev_grapheme_boundary(doc.text().slice(..), range.head)
+            } else {
+                range.head
+            };
+            Range::new(pos, pos)
+        }),
     );
 }
 
@@ -1184,10 +1159,13 @@ fn append_mode(cx: &mut Context) {
     doc.restore_cursor = true;
 
     let selection = doc.selection(view.id).clone().transform(|range| {
-        Range::new(
-            range.from(),
-            graphemes::next_grapheme_boundary(doc.text().slice(..), range.to()), // to() + next char
-        )
+        let to = if range.to() == range.from() {
+            // For 1-width cursor semantics.
+            graphemes::next_grapheme_boundary(doc.text().slice(..), range.to())
+        } else {
+            range.to()
+        };
+        Range::new(range.from(), to)
     });
 
     let end = doc.text().len_chars();