diff --git a/Cargo.toml b/Cargo.toml
index 03eb4da9bf8c7478fa87a7c7bc8d2b5dc91bd327..b4dc8d0b68c040697f6c8d6d3a5a35cb1e4ccca9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -37,7 +37,7 @@ cosmic-text = { version = "0.11.2" }
 crossbeam-channel = "0.5.8"
 image = "0.24.6"
 sys-locale = "0.3.0"
-unicode-segmentation = { optional = true }
+unicode-segmentation = { version = "1.11.0", optional = true }
 
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
 arboard = "3.2.0"
diff --git a/src/buffer.rs b/src/buffer.rs
index 1d32295ba20b873ad0403f7aa34f931e0f1056f6..1244e35b633c7ac39d5acee4ab042c7f576e2d05 100644
--- a/src/buffer.rs
+++ b/src/buffer.rs
@@ -1,6 +1,36 @@
 use crate::*;
 use bevy::{prelude::*, window::PrimaryWindow};
 
+pub trait BufferExtras {
+    fn get_text(&self) -> String;
+}
+
+impl BufferExtras for Buffer {
+    /// Retrieves the text content from a buffer.
+    ///
+    /// # Arguments
+    ///
+    /// * none, takes the rust magic ref to self
+    ///
+    /// # Returns
+    ///
+    /// A `String` containing the cosmic text content.
+    fn get_text(&self) -> String {
+        let mut text = String::new();
+        let line_count = self.lines.len();
+
+        for (i, line) in self.lines.iter().enumerate() {
+            text.push_str(line.text());
+
+            if i < line_count - 1 {
+                text.push('\n');
+            }
+        }
+
+        text
+    }
+}
+
 #[derive(Component, Deref, DerefMut)]
 pub struct CosmicBuffer(pub Buffer);
 
@@ -66,29 +96,6 @@ impl<'s, 'r> CosmicBuffer {
         self
     }
 
-    /// Retrieves the cosmic text content from a buffer.
-    ///
-    /// # Arguments
-    ///
-    /// * none, takes the rust magic ref to self
-    ///
-    /// # Returns
-    ///
-    /// A `String` containing the cosmic text content.
-    pub fn get_text(&self) -> String {
-        let mut text = String::new();
-        let line_count = self.lines.len();
-
-        for (i, line) in self.lines.iter().enumerate() {
-            text.push_str(line.text());
-
-            if i < line_count - 1 {
-                text.push('\n');
-            }
-        }
-
-        text
-    }
     /// Returns texts from a MultiStyle buffer
     pub fn get_text_spans(&self, default_attrs: AttrsOwned) -> Vec<Vec<(String, AttrsOwned)>> {
         // TODO: untested!
diff --git a/src/input.rs b/src/input.rs
index c6862448503391938f07268b71eb4d5b3e62d052..aed352fee836d97ec6d0bc3722311f38f7930b78 100644
--- a/src/input.rs
+++ b/src/input.rs
@@ -20,7 +20,7 @@ use wasm_bindgen::prelude::*;
 use wasm_bindgen_futures::JsFuture;
 
 use crate::{
-    buffer::{get_x_offset_center, get_y_offset_center},
+    buffer::{get_x_offset_center, get_y_offset_center, BufferExtras},
     get_node_cursor_pos, CosmicBuffer, CosmicEditor, CosmicFontSystem, CosmicMaxChars,
     CosmicMaxLines, CosmicSource, CosmicTextChanged, CosmicTextPosition, FocusedWidget, ReadOnly,
     XOffset,
@@ -370,7 +370,6 @@ pub fn kb_move_cursor(
             if !shift {
                 editor.set_selection(Selection::None);
             }
-            return;
         }
     }
 }
diff --git a/src/plugins/password/mod.rs b/src/plugins/password/mod.rs
index d890369a2d7bc6dcde163017629b34772c96599a..7a7d045e14aff518bf59e8a3fc3b38f3795ae6cf 100644
--- a/src/plugins/password/mod.rs
+++ b/src/plugins/password/mod.rs
@@ -1,5 +1,6 @@
+use crate::buffer::BufferExtras;
 use bevy::prelude::*;
-use cosmic_text::{Buffer, Edit, Shaping};
+use cosmic_text::{Cursor, Edit, Selection, Shaping};
 use unicode_segmentation::UnicodeSegmentation;
 
 use crate::{
@@ -11,29 +12,20 @@ pub struct PasswordPlugin;
 
 impl Plugin for PasswordPlugin {
     fn build(&self, app: &mut App) {
-        app.add_systems(
-            PreUpdate,
-            (
-                hide_password_text.before(input_mouse),
-                restore_password_text.after(input_mouse),
-            ),
-        )
-        .add_systems(
-            Update,
-            (
-                hide_password_text.before(kb_move_cursor),
-                restore_password_text
+        app.add_systems(PreUpdate, (hide_password_text.before(input_mouse),))
+            .add_systems(
+                Update,
+                (restore_password_text
                     .before(kb_input_text)
-                    .after(kb_move_cursor),
-            ),
-        )
-        .add_systems(
-            PostUpdate,
-            (
-                hide_password_text.before(Render),
-                restore_password_text.after(Render),
-            ),
-        );
+                    .after(kb_move_cursor),),
+            )
+            .add_systems(
+                PostUpdate,
+                (
+                    hide_password_text.before(Render),
+                    restore_password_text.after(Render),
+                ),
+            );
     }
 }
 
@@ -52,22 +44,6 @@ impl Default for Password {
     }
 }
 
-// TODO: impl this on buffer
-fn get_text(buffer: &mut Buffer) -> String {
-    let mut text = String::new();
-    let line_count = buffer.lines.len();
-
-    for (i, line) in buffer.lines.iter().enumerate() {
-        text.push_str(line.text());
-
-        if i < line_count - 1 {
-            text.push('\n');
-        }
-    }
-
-    text
-}
-
 fn hide_password_text(
     mut q: Query<(
         &mut Password,
@@ -80,16 +56,35 @@ fn hide_password_text(
     for (mut password, mut buffer, attrs, editor_opt) in q.iter_mut() {
         if let Some(mut editor) = editor_opt {
             let mut cursor = editor.cursor();
+            let mut selection = editor.selection();
 
             editor.with_buffer_mut(|buffer| {
-                let text = get_text(buffer);
-
-                let (pre, _post) = text.split_at(cursor.index);
-
-                let graphemes = pre.graphemes(true).count();
-
-                cursor.index = graphemes * password.glyph.len_utf8();
+                let text = buffer.get_text();
+
+                // Translate cursor to correct position for blocker glyphs
+                let translate_cursor = |c: &mut Cursor| {
+                    let (pre, _post) = text.split_at(c.index);
+                    let graphemes = pre.graphemes(true).count();
+                    c.index = graphemes * password.glyph.len_utf8();
+                };
+
+                translate_cursor(&mut cursor);
+
+                // Translate selection cursor
+                match selection {
+                    Selection::None => {}
+                    Selection::Line(ref mut c) => {
+                        translate_cursor(c);
+                    }
+                    Selection::Word(ref mut c) => {
+                        translate_cursor(c);
+                    }
+                    Selection::Normal(ref mut c) => {
+                        translate_cursor(c);
+                    }
+                }
 
+                // Update text to blockers
                 buffer.set_text(
                     &mut font_system,
                     password
@@ -105,6 +100,7 @@ fn hide_password_text(
             });
 
             editor.set_cursor(cursor);
+            editor.set_selection(selection);
 
             continue;
         }
@@ -132,26 +128,38 @@ fn restore_password_text(
     for (password, mut buffer, attrs, editor_opt) in q.iter_mut() {
         if let Some(mut editor) = editor_opt {
             let mut cursor = editor.cursor();
-            let mut index = 0;
+            let mut selection = editor.selection();
 
             editor.with_buffer_mut(|buffer| {
-                let text = get_text(buffer);
-                let (pre, _post) = text.split_at(cursor.index);
-
-                let grapheme_count = pre.graphemes(true).count();
-
-                let mut g_idx = 0;
-                for (i, _c) in password.real_text.grapheme_indices(true) {
-                    if g_idx == grapheme_count {
-                        index = i;
+                let text = buffer.get_text();
+
+                // Find cursor position and translate back to correct position in real text
+                let restore_cursor = |c: &mut Cursor| {
+                    let (pre, _post) = text.split_at(c.index);
+                    let graphemes = pre.graphemes(true).count();
+                    let mut n_i = 0;
+                    if let Some((i, _)) = password.real_text.grapheme_indices(true).nth(graphemes) {
+                        n_i = i;
+                    } else if c.index > 0 {
+                        n_i = password.real_text.len();
                     }
-                    g_idx += 1;
-                }
+                    c.index = n_i;
+                };
 
-                // TODO: save/restore with selection bounds
+                restore_cursor(&mut cursor);
 
-                if cursor.index > 0 && index == 0 {
-                    index = password.real_text.len();
+                // Translate selection cursor
+                match selection {
+                    Selection::None => {}
+                    Selection::Line(ref mut c) => {
+                        restore_cursor(c);
+                    }
+                    Selection::Word(ref mut c) => {
+                        restore_cursor(c);
+                    }
+                    Selection::Normal(ref mut c) => {
+                        restore_cursor(c);
+                    }
                 }
 
                 buffer.set_text(
@@ -162,9 +170,8 @@ fn restore_password_text(
                 );
             });
 
-            cursor.index = index;
-
             editor.set_cursor(cursor);
+            editor.set_selection(selection);
 
             continue;
         }
diff --git a/src/plugins/placeholder/mod.rs b/src/plugins/placeholder/mod.rs
index 65d7b704b31b092676722bda9eb64a5c0c4d6678..7fa3d94bfa8650929993a641d32c8f46e1fedbf6 100644
--- a/src/plugins/placeholder/mod.rs
+++ b/src/plugins/placeholder/mod.rs
@@ -1,3 +1,4 @@
+use crate::buffer::BufferExtras;
 use bevy::prelude::*;
 use cosmic_text::{Attrs, Edit};