diff --git a/Cargo.lock b/Cargo.lock
index c9e9adbf..eff47941 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -436,6 +436,12 @@ dependencies = [
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
 [[package]]
 name = "libc"
 version = "0.2.153"
@@ -700,8 +706,10 @@ dependencies = [
  "fd-lock",
  "gethostname",
  "itertools",
+ "lazy_static",
  "nu-ansi-term",
  "pretty_assertions",
+ "regex",
  "rstest",
  "rusqlite",
  "serde",
diff --git a/Cargo.toml b/Cargo.toml
index 5c387c50..04dffb2e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -24,7 +24,9 @@ crossbeam = { version = "0.8.2", optional = true }
 crossterm = { version = "0.28.1", features = ["serde"] }
 fd-lock = "4.0.2"
 itertools = "0.13.0"
+lazy_static = "1.5.0"
 nu-ansi-term = "0.50.0"
+regex = "1.11"
 rusqlite = { version = "0.31.0", optional = true }
 serde = { version = "1.0", features = ["derive"] }
 serde_json = { version = "1.0.79", optional = true }
diff --git a/examples/fuzzy_completions.rs b/examples/fuzzy_completions.rs
new file mode 100644
index 00000000..7a2fb5a6
--- /dev/null
+++ b/examples/fuzzy_completions.rs
@@ -0,0 +1,138 @@
+// Modifies the completions example to demonstrate highlighting of fuzzy completions
+// cargo run --example fuzzy_completions
+//
+// One of the suggestions is "multiple 汉 by̆tes字👩🏾". Try typing in "y" or "👩" and note how
+// the entire grapheme "y̆" or "👩🏾" is highlighted (might not look right in your terminal).
+
+use nu_ansi_term::{Color, Style};
+use reedline::{
+    default_emacs_keybindings, ColumnarMenu, Completer, DefaultPrompt, EditCommand, Emacs, KeyCode,
+    KeyModifiers, Keybindings, MenuBuilder, Reedline, ReedlineEvent, ReedlineMenu, Signal, Span,
+    Suggestion,
+};
+use std::io;
+use unicode_segmentation::UnicodeSegmentation;
+
+struct HomegrownFuzzyCompleter(Vec<String>);
+
+impl Completer for HomegrownFuzzyCompleter {
+    fn complete(&mut self, line: &str, pos: usize) -> Vec<reedline::Suggestion> {
+        // Grandma's fuzzy matching recipe. She swears it's better than that crates.io-bought stuff
+        self.0
+            .iter()
+            .filter_map(|command_str| {
+                let command = command_str.graphemes(true).collect::<Vec<_>>();
+                let mut ind = 0;
+                let mut match_indices = Vec::new();
+                for g in line[..pos].graphemes(true) {
+                    while ind < command.len() && command[ind] != g {
+                        ind += 1;
+                    }
+                    if ind == command.len() {
+                        return None;
+                    }
+                    match_indices.push(ind);
+                    ind += 1;
+                }
+
+                Some(Suggestion {
+                    value: command_str.to_string(),
+                    description: None,
+                    style: None,
+                    extra: None,
+                    span: Span::new(0, pos),
+                    append_whitespace: false,
+                    match_indices: Some(match_indices),
+                })
+            })
+            .collect()
+    }
+}
+
+fn add_menu_keybindings(keybindings: &mut Keybindings) {
+    keybindings.add_binding(
+        KeyModifiers::NONE,
+        KeyCode::Tab,
+        ReedlineEvent::UntilFound(vec![
+            ReedlineEvent::Menu("completion_menu".to_string()),
+            ReedlineEvent::MenuNext,
+        ]),
+    );
+    keybindings.add_binding(
+        KeyModifiers::ALT,
+        KeyCode::Enter,
+        ReedlineEvent::Edit(vec![EditCommand::InsertNewline]),
+    );
+}
+
+fn main() -> io::Result<()> {
+    // Number of columns
+    let columns: u16 = 4;
+    // Column width
+    let col_width: Option<usize> = None;
+    // Column padding
+    let col_padding: usize = 2;
+
+    let commands = vec![
+        "test".into(),
+        "clear".into(),
+        "exit".into(),
+        "history 1".into(),
+        "history 2".into(),
+        "logout".into(),
+        "login".into(),
+        "hello world".into(),
+        "hello world reedline".into(),
+        "hello world something".into(),
+        "hello world another".into(),
+        "hello world 1".into(),
+        "hello world 2".into(),
+        "hello another very large option for hello word that will force one column".into(),
+        "this is the reedline crate".into(),
+        "abaaabas".into(),
+        "abaaacas".into(),
+        "ababac".into(),
+        "abacaxyc".into(),
+        "abadarabc".into(),
+        "multiple 汉 by̆tes字👩🏾".into(),
+        "ab汉 by̆tes👩🏾".into(),
+    ];
+
+    let completer = Box::new(HomegrownFuzzyCompleter(commands));
+
+    // Use the interactive menu to select options from the completer
+    let columnar_menu = ColumnarMenu::default()
+        .with_name("completion_menu")
+        .with_columns(columns)
+        .with_column_width(col_width)
+        .with_column_padding(col_padding)
+        .with_text_style(Style::new().italic().on(Color::LightGreen))
+        .with_match_text_style(Style::new().on(Color::LightBlue));
+
+    let completion_menu = Box::new(columnar_menu);
+
+    let mut keybindings = default_emacs_keybindings();
+    add_menu_keybindings(&mut keybindings);
+
+    let edit_mode = Box::new(Emacs::new(keybindings));
+
+    let mut line_editor = Reedline::create()
+        .with_completer(completer)
+        .with_menu(ReedlineMenu::EngineCompleter(completion_menu))
+        .with_edit_mode(edit_mode);
+
+    let prompt = DefaultPrompt::default();
+
+    loop {
+        let sig = line_editor.read_line(&prompt)?;
+        match sig {
+            Signal::Success(buffer) => {
+                println!("We processed: {buffer}");
+            }
+            Signal::CtrlD | Signal::CtrlC => {
+                println!("\nAborted!");
+                break Ok(());
+            }
+        }
+    }
+}
diff --git a/src/completion/base.rs b/src/completion/base.rs
index 909c467b..bfbec118 100644
--- a/src/completion/base.rs
+++ b/src/completion/base.rs
@@ -90,4 +90,7 @@ pub struct Suggestion {
     /// Whether to append a space after selecting this suggestion.
     /// This helps to avoid that a completer repeats the complete suggestion.
     pub append_whitespace: bool,
+    /// Indices of the graphemes in the suggestion that matched the typed text.
+    /// Useful if using fuzzy matching.
+    pub match_indices: Option<Vec<usize>>,
 }
diff --git a/src/completion/default.rs b/src/completion/default.rs
index 882debb6..a4f4d607 100644
--- a/src/completion/default.rs
+++ b/src/completion/default.rs
@@ -55,17 +55,17 @@ impl Completer for DefaultCompleter {
     /// assert_eq!(
     ///     completions.complete("bat",3),
     ///     vec![
-    ///         Suggestion {value: "batcave".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 3 }, append_whitespace: false},
-    ///         Suggestion {value: "batman".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 3 }, append_whitespace: false},
-    ///         Suggestion {value: "batmobile".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 3 }, append_whitespace: false},
+    ///         Suggestion {value: "batcave".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 3 }, append_whitespace: false, ..Default::default()},
+    ///         Suggestion {value: "batman".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 3 }, append_whitespace: false, ..Default::default()},
+    ///         Suggestion {value: "batmobile".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 3 }, append_whitespace: false, ..Default::default()},
     ///     ]);
     ///
     /// assert_eq!(
     ///     completions.complete("to the\r\nbat",11),
     ///     vec![
-    ///         Suggestion {value: "batcave".into(), description: None, style: None, extra: None, span: Span { start: 8, end: 11 }, append_whitespace: false},
-    ///         Suggestion {value: "batman".into(), description: None, style: None, extra: None, span: Span { start: 8, end: 11 }, append_whitespace: false},
-    ///         Suggestion {value: "batmobile".into(), description: None, style: None, extra: None, span: Span { start: 8, end: 11 }, append_whitespace: false},
+    ///         Suggestion {value: "batcave".into(), description: None, style: None, extra: None, span: Span { start: 8, end: 11 }, append_whitespace: false, ..Default::default()},
+    ///         Suggestion {value: "batman".into(), description: None, style: None, extra: None, span: Span { start: 8, end: 11 }, append_whitespace: false, ..Default::default()},
+    ///         Suggestion {value: "batmobile".into(), description: None, style: None, extra: None, span: Span { start: 8, end: 11 }, append_whitespace: false, ..Default::default()},
     ///     ]);
     /// ```
     fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
@@ -110,6 +110,7 @@ impl Completer for DefaultCompleter {
                                         extra: None,
                                         span,
                                         append_whitespace: false,
+                                        ..Default::default()
                                     }
                                 })
                                 .filter(|t| t.value.len() > (t.span.end - t.span.start))
@@ -182,15 +183,15 @@ impl DefaultCompleter {
     /// completions.insert(vec!["test-hyphen","test_underscore"].iter().map(|s| s.to_string()).collect());
     /// assert_eq!(
     ///     completions.complete("te",2),
-    ///     vec![Suggestion {value: "test".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 2 }, append_whitespace: false}]);
+    ///     vec![Suggestion {value: "test".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 2 }, append_whitespace: false, ..Default::default()}]);
     ///
     /// let mut completions = DefaultCompleter::with_inclusions(&['-', '_']);
     /// completions.insert(vec!["test-hyphen","test_underscore"].iter().map(|s| s.to_string()).collect());
     /// assert_eq!(
     ///     completions.complete("te",2),
     ///     vec![
-    ///         Suggestion {value: "test-hyphen".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 2 }, append_whitespace: false},
-    ///         Suggestion {value: "test_underscore".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 2 }, append_whitespace: false},
+    ///         Suggestion {value: "test-hyphen".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 2 }, append_whitespace: false, ..Default::default()},
+    ///         Suggestion {value: "test_underscore".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 2 }, append_whitespace: false, ..Default::default()},
     ///     ]);
     /// ```
     pub fn with_inclusions(incl: &[char]) -> Self {
@@ -384,6 +385,7 @@ mod tests {
                     extra: None,
                     span: Span { start: 0, end: 3 },
                     append_whitespace: false,
+                    ..Default::default()
                 },
                 Suggestion {
                     value: "number".into(),
@@ -392,6 +394,7 @@ mod tests {
                     extra: None,
                     span: Span { start: 0, end: 3 },
                     append_whitespace: false,
+                    ..Default::default()
                 },
                 Suggestion {
                     value: "nushell".into(),
@@ -400,6 +403,7 @@ mod tests {
                     extra: None,
                     span: Span { start: 0, end: 3 },
                     append_whitespace: false,
+                    ..Default::default()
                 },
             ]
         );
@@ -428,6 +432,7 @@ mod tests {
                     extra: None,
                     span: Span { start: 8, end: 9 },
                     append_whitespace: false,
+                    ..Default::default()
                 },
                 Suggestion {
                     value: "this is the reedline crate".into(),
@@ -436,6 +441,7 @@ mod tests {
                     extra: None,
                     span: Span { start: 8, end: 9 },
                     append_whitespace: false,
+                    ..Default::default()
                 },
                 Suggestion {
                     value: "this is the reedline crate".into(),
@@ -444,6 +450,7 @@ mod tests {
                     extra: None,
                     span: Span { start: 0, end: 9 },
                     append_whitespace: false,
+                    ..Default::default()
                 },
             ]
         );
diff --git a/src/completion/history.rs b/src/completion/history.rs
index a29e2b04..a48cffca 100644
--- a/src/completion/history.rs
+++ b/src/completion/history.rs
@@ -65,6 +65,7 @@ impl<'menu> HistoryCompleter<'menu> {
             extra: None,
             span,
             append_whitespace: false,
+            ..Default::default()
         }
     }
 }
diff --git a/src/menu/columnar_menu.rs b/src/menu/columnar_menu.rs
index 8aae2f08..ea1154cd 100644
--- a/src/menu/columnar_menu.rs
+++ b/src/menu/columnar_menu.rs
@@ -1,7 +1,9 @@
 use super::{Menu, MenuBuilder, MenuEvent, MenuSettings};
 use crate::{
     core_editor::Editor,
-    menu_functions::{can_partially_complete, completer_input, replace_in_buffer},
+    menu_functions::{
+        can_partially_complete, completer_input, replace_in_buffer, style_suggestion,
+    },
     painting::Painter,
     Completer, Suggestion,
 };
@@ -307,50 +309,31 @@ impl ColumnarMenu {
                 .unwrap_or(shortest_base);
             let match_len = shortest_base.len();
 
-            // Find match position - look for the base string in the suggestion (case-insensitive)
-            let match_position = suggestion
-                .value
-                .to_lowercase()
-                .find(&shortest_base.to_lowercase())
-                .unwrap_or(0);
-
-            // The match is just the part that matches the shortest_base
-            let match_str = &suggestion.value[match_position
-                ..match_position + match_len.min(suggestion.value.len() - match_position)];
-
-            // Prefix is everything before the match
-            let prefix = &suggestion.value[..match_position];
-
-            // Remaining is everything after the match
-            let remaining_str = &suggestion.value[match_position + match_str.len()..];
-
-            let suggestion_style_prefix = suggestion
-                .style
-                .unwrap_or(self.settings.color.text_style)
-                .prefix();
+            let suggestion_style = suggestion.style.unwrap_or(self.settings.color.text_style);
 
             let left_text_size = self.longest_suggestion + self.default_details.col_padding;
             let right_text_size = self.get_width().saturating_sub(left_text_size);
 
-            let max_remaining = left_text_size.saturating_sub(match_str.width() + prefix.width());
-            let max_match = max_remaining.saturating_sub(remaining_str.width());
+            let default_indices = (0..match_len).collect();
+            let match_indices = suggestion
+                .match_indices
+                .as_ref()
+                .unwrap_or(&default_indices);
 
             if index == self.index() {
                 if let Some(description) = &suggestion.description {
                     format!(
-                        "{}{}{}{}{}{}{}{}{}{:max_match$}{:max_remaining$}{}{}{}{}{}{}",
-                        suggestion_style_prefix,
-                        self.settings.color.selected_text_style.prefix(),
-                        prefix,
-                        RESET,
-                        suggestion_style_prefix,
-                        self.settings.color.selected_match_style.prefix(),
-                        match_str,
-                        RESET,
-                        suggestion_style_prefix,
-                        self.settings.color.selected_text_style.prefix(),
-                        remaining_str,
-                        RESET,
+                        "{:left_text_size$}{}{}{}{}{}",
+                        style_suggestion(
+                            &self
+                                .settings
+                                .color
+                                .selected_text_style
+                                .paint(&suggestion.value)
+                                .to_string(),
+                            match_indices,
+                            &self.settings.color.selected_match_style,
+                        ),
                         self.settings.color.description_style.prefix(),
                         self.settings.color.selected_text_style.prefix(),
                         description
@@ -363,19 +346,17 @@ impl ColumnarMenu {
                     )
                 } else {
                     format!(
-                        "{}{}{}{}{}{}{}{}{}{}{}{}{:>empty$}{}",
-                        suggestion_style_prefix,
-                        self.settings.color.selected_text_style.prefix(),
-                        prefix,
-                        RESET,
-                        suggestion_style_prefix,
-                        self.settings.color.selected_match_style.prefix(),
-                        match_str,
-                        RESET,
-                        suggestion_style_prefix,
-                        self.settings.color.selected_text_style.prefix(),
-                        remaining_str,
-                        RESET,
+                        "{}{:>empty$}{}",
+                        style_suggestion(
+                            &self
+                                .settings
+                                .color
+                                .selected_text_style
+                                .paint(&suggestion.value)
+                                .to_string(),
+                            match_indices,
+                            &self.settings.color.selected_match_style,
+                        ),
                         "",
                         self.end_of_line(column),
                         empty = empty_space,
@@ -383,17 +364,12 @@ impl ColumnarMenu {
                 }
             } else if let Some(description) = &suggestion.description {
                 format!(
-                    "{}{}{}{}{}{}{}{:max_match$}{:max_remaining$}{}{}{}{}{}",
-                    suggestion_style_prefix,
-                    prefix,
-                    RESET,
-                    suggestion_style_prefix,
-                    self.settings.color.match_style.prefix(),
-                    match_str,
-                    RESET,
-                    suggestion_style_prefix,
-                    remaining_str,
-                    RESET,
+                    "{:left_text_size$}{}{}{}{}",
+                    style_suggestion(
+                        &suggestion_style.paint(&suggestion.value).to_string(),
+                        match_indices,
+                        &self.settings.color.match_style,
+                    ),
                     self.settings.color.description_style.prefix(),
                     description
                         .chars()
@@ -405,17 +381,12 @@ impl ColumnarMenu {
                 )
             } else {
                 format!(
-                    "{}{}{}{}{}{}{}{}{}{}{}{:>empty$}{}{}",
-                    suggestion_style_prefix,
-                    prefix,
-                    RESET,
-                    suggestion_style_prefix,
-                    self.settings.color.match_style.prefix(),
-                    match_str,
-                    RESET,
-                    suggestion_style_prefix,
-                    remaining_str,
-                    RESET,
+                    "{}{}{:>empty$}{}{}",
+                    style_suggestion(
+                        &suggestion_style.paint(&suggestion.value).to_string(),
+                        match_indices,
+                        &self.settings.color.match_style,
+                    ),
                     self.settings.color.description_style.prefix(),
                     "",
                     RESET,
@@ -784,6 +755,7 @@ mod tests {
             extra: None,
             span: Span { start: 0, end: pos },
             append_whitespace: false,
+            ..Default::default()
         }
     }
 
diff --git a/src/menu/ide_menu.rs b/src/menu/ide_menu.rs
index 24c82378..a3696538 100644
--- a/src/menu/ide_menu.rs
+++ b/src/menu/ide_menu.rs
@@ -1,7 +1,9 @@
 use super::{Menu, MenuBuilder, MenuEvent, MenuSettings};
 use crate::{
     core_editor::Editor,
-    menu_functions::{can_partially_complete, completer_input, replace_in_buffer},
+    menu_functions::{
+        can_partially_complete, completer_input, replace_in_buffer, style_suggestion,
+    },
     painting::Painter,
     Completer, Suggestion,
 };
@@ -520,62 +522,45 @@ impl IdeMenu {
                 .unwrap_or(shortest_base);
             let match_len = shortest_base.len().min(string.len());
 
-            // Find match position - look for the base string in the suggestion (case-insensitive)
-            let match_position = suggestion
-                .value
-                .to_lowercase()
-                .find(&shortest_base.to_lowercase())
-                .unwrap_or(0);
-
-            // The match is just the part that matches the shortest_base
-            let match_str = &string
-                [match_position..match_position + match_len.min(string.len() - match_position)];
-
-            // Prefix is everything before the match
-            let prefix = &string[..match_position];
+            let default_indices = (0..match_len).collect();
+            let match_indices = suggestion
+                .match_indices
+                .as_ref()
+                .unwrap_or(&default_indices);
 
-            // Remaining is everything after the match
-            let remaining_str = &string[match_position + match_str.len()..];
-
-            let suggestion_style_prefix = suggestion
-                .style
-                .unwrap_or(self.settings.color.text_style)
-                .prefix();
+            let suggestion_style = suggestion.style.unwrap_or(self.settings.color.text_style);
 
             if index == self.index() {
                 format!(
-                    "{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}",
+                    "{}{}{}{}{}{}{}",
                     vertical_border,
-                    suggestion_style_prefix,
-                    self.settings.color.selected_text_style.prefix(),
-                    prefix,
-                    RESET,
-                    suggestion_style_prefix,
+                    suggestion_style.prefix(),
                     " ".repeat(padding),
-                    self.settings.color.selected_match_style.prefix(),
-                    match_str,
-                    RESET,
-                    suggestion_style_prefix,
-                    self.settings.color.selected_text_style.prefix(),
-                    remaining_str,
+                    style_suggestion(
+                        &self
+                            .settings
+                            .color
+                            .selected_text_style
+                            .paint(&suggestion.value)
+                            .to_string(),
+                        match_indices,
+                        &self.settings.color.selected_match_style,
+                    ),
                     " ".repeat(padding_right),
                     RESET,
                     vertical_border,
                 )
             } else {
                 format!(
-                    "{}{}{}{}{}{}{}{}{}{}{}{}{}{}",
+                    "{}{}{}{}{}{}{}",
                     vertical_border,
-                    suggestion_style_prefix,
-                    prefix,
-                    RESET,
-                    suggestion_style_prefix,
+                    suggestion_style.prefix(),
                     " ".repeat(padding),
-                    self.settings.color.match_style.prefix(),
-                    match_str,
-                    RESET,
-                    suggestion_style_prefix,
-                    remaining_str,
+                    style_suggestion(
+                        &suggestion_style.paint(&suggestion.value).to_string(),
+                        match_indices,
+                        &self.settings.color.match_style,
+                    ),
                     " ".repeat(padding_right),
                     RESET,
                     vertical_border,
@@ -1405,6 +1390,7 @@ mod tests {
             extra: None,
             span: Span { start: 0, end: pos },
             append_whitespace: false,
+            ..Default::default()
         }
     }
 
diff --git a/src/menu/menu_functions.rs b/src/menu/menu_functions.rs
index 84575f0c..676f7df0 100644
--- a/src/menu/menu_functions.rs
+++ b/src/menu/menu_functions.rs
@@ -1,6 +1,17 @@
 //! Collection of common functions that can be used to create menus
+use nu_ansi_term::{ansi::RESET, Style};
+use regex::Regex;
+use lazy_static::lazy_static;
+use unicode_segmentation::UnicodeSegmentation;
+
 use crate::{Editor, Suggestion, UndoBehavior};
 
+lazy_static! {
+    /// Matches ANSI escapes. Stolen from https://github.com/dbkaplun/parse-ansi, which got it from
+    /// https://github.com/nodejs/node/blob/641d4a4159aaa96eece8356e03ec6c7248ae3e73/lib/internal/readline.js#L9
+    static ref ANSI_REGEX: Regex = Regex::new(r"[\x1b\x9b]\[[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]").unwrap();
+}
+
 /// Index result obtained from parsing a string with an index marker
 /// For example, the next string:
 ///     "this is an example :10"
@@ -354,10 +365,65 @@ pub fn can_partially_complete(values: &[Suggestion], editor: &mut Editor) -> boo
     }
 }
 
+/// Style a suggestion to be shown in a completer menu
+///
+/// * `match_indices` - Indices of graphemes (NOT bytes or chars) that matched the typed text
+/// * `match_style` - Style to use for matched characters
+pub fn style_suggestion(suggestion: &str, match_indices: &[usize], match_style: &Style) -> String {
+    let escapes = ANSI_REGEX.find_iter(suggestion).collect::<Vec<_>>();
+    let mut segments = Vec::new();
+    if escapes.is_empty() {
+        segments.push((0, 0, suggestion.len()));
+    } else if escapes[0].start() > 0 {
+        segments.push((0, 0, escapes[0].start()));
+    }
+    for (i, m) in escapes.iter().enumerate() {
+        let next = if i + 1 == escapes.len() {
+            suggestion.len()
+        } else {
+            escapes[i + 1].start()
+        };
+        segments.push((m.start(), m.end(), next));
+    }
+
+    let mut res = String::new();
+
+    let mut offset = 0;
+    for (escape_start, text_start, text_end) in segments {
+        let escape = &suggestion[escape_start..text_start];
+        let text = &suggestion[text_start..text_end];
+        let graphemes = text.graphemes(true).collect::<Vec<_>>();
+        let mut prev_matched = false;
+
+        res.push_str(escape);
+        for (i, grapheme) in graphemes.iter().enumerate() {
+            let is_match = match_indices.contains(&(i + offset));
+
+            if is_match && !prev_matched {
+                res.push_str(&match_style.prefix().to_string());
+            } else if !is_match && prev_matched && i != 0 {
+                res.push_str(RESET);
+                res.push_str(escape);
+            }
+            res.push_str(grapheme);
+            prev_matched = is_match;
+        }
+
+        if prev_matched {
+            res.push_str(RESET);
+        }
+
+        offset += graphemes.len();
+    }
+
+    res
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
     use crate::{EditCommand, LineBuffer, Span};
+    use nu_ansi_term::Color;
     use rstest::rstest;
 
     #[test]
@@ -612,6 +678,7 @@ mod tests {
                 extra: None,
                 span: Span::new(0, s.len()),
                 append_whitespace: false,
+                ..Default::default()
             })
             .collect();
         let res = find_common_string(&input);
@@ -632,6 +699,7 @@ mod tests {
                 extra: None,
                 span: Span::new(0, s.len()),
                 append_whitespace: false,
+                ..Default::default()
             })
             .collect();
         let res = find_common_string(&input);
@@ -687,6 +755,7 @@ mod tests {
                 extra: None,
                 span: Span::new(start, end),
                 append_whitespace: false,
+                ..Default::default()
             }),
             &mut editor,
         );
@@ -697,4 +766,52 @@ mod tests {
         assert_eq!(orig_buffer, editor.get_buffer());
         assert_eq!(orig_insertion_point, editor.insertion_point());
     }
+
+    #[test]
+    fn parse_ansi() {
+        assert_eq!(
+            ANSI_REGEX
+                .find_iter("before \x1b[31;4mred underline\x1b[0m after")
+                .map(|m| (m.start(), m.end(), m.as_str()))
+                .collect::<Vec<_>>(),
+            vec![(7, 14, "\x1b[31;4m"), (27, 31, "\x1b[0m")]
+        );
+    }
+
+    #[test]
+    fn style_fuzzy_suggestion() {
+        let match_style = Style::new().underline();
+        let style1 = Style::new().on(Color::Blue);
+        let style2 = Style::new().on(Color::Green);
+
+        let expected = format!(
+            "{}{}{}{}{}{}{}{}{}{}{}{}{}",
+            style1.prefix(),
+            "ab",
+            match_style.paint("汉"),
+            style1.prefix(),
+            "d",
+            RESET,
+            style2.prefix(),
+            match_style.paint("y̆👩🏾"),
+            style2.prefix(),
+            "e",
+            RESET,
+            "b@",
+            match_style.paint("r"),
+        );
+        let match_indices = &[
+            2, // 汉
+            4, 5, // y̆👩🏾
+            9, // r
+        ];
+        assert_eq!(
+            expected,
+            style_suggestion(
+                &format!("{}{}{}", style1.paint("ab汉d"), style2.paint("y̆👩🏾e"), "b@r"),
+                match_indices,
+                &match_style
+            )
+        );
+    }
 }