Show invisibles in editor (#19298)

Conrad Irwin , dovakin0007 , and dovakin0007 created

Release Notes:

- Added highlighting for "invisible" unicode characters

Closes #16310

---------

Co-authored-by: dovakin0007 <dovakin0007@gmail.com>
Co-authored-by: dovakin0007 <73059450+dovakin0007@users.noreply.github.com>

Change summary

Cargo.lock                                  |   1 
Cargo.toml                                  |   2 
crates/editor/Cargo.toml                    |   1 
crates/editor/src/display_map.rs            | 118 ++++++++-----
crates/editor/src/display_map/block_map.rs  |  56 +++--
crates/editor/src/display_map/char_map.rs   | 158 +++++++++++-------
crates/editor/src/display_map/invisibles.rs | 157 ++++++++++++++++++
crates/editor/src/display_map/wrap_map.rs   | 191 +++++++++++-----------
crates/editor/src/element.rs                |  17 -
crates/editor/src/hover_popover.rs          |  45 ++++
crates/gpui/src/text_system/line.rs         |  51 +++++
crates/language/src/buffer.rs               |   3 
12 files changed, 552 insertions(+), 248 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3722,6 +3722,7 @@ dependencies = [
  "tree-sitter-rust",
  "tree-sitter-typescript",
  "ui",
+ "unicode-segmentation",
  "unindent",
  "url",
  "util",

Cargo.toml 🔗

@@ -468,7 +468,7 @@ tree-sitter-typescript = "0.23"
 tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "baff0b51c64ef6a1fb1f8390f3ad6015b83ec13a" }
 unicase = "2.6"
 unindent = "0.1.7"
-unicode-segmentation = "1.10"
+unicode-segmentation = "1.11"
 url = "2.2"
 uuid = { version = "1.1.2", features = ["v4", "v5", "serde"] }
 wasmparser = "0.215"

crates/editor/Cargo.toml 🔗

@@ -81,6 +81,7 @@ ui.workspace = true
 url.workspace = true
 util.workspace = true
 workspace.workspace = true
+unicode-segmentation.workspace = true
 
 [dev-dependencies]
 ctor.workspace = true

crates/editor/src/display_map.rs 🔗

@@ -8,7 +8,7 @@
 //! of several smaller structures that form a hierarchy (starting at the bottom):
 //! - [`InlayMap`] that decides where the [`Inlay`]s should be displayed.
 //! - [`FoldMap`] that decides where the fold indicators should be; it also tracks parts of a source file that are currently folded.
-//! - [`TabMap`] that keeps track of hard tabs in a buffer.
+//! - [`CharMap`] that replaces tabs and non-printable characters
 //! - [`WrapMap`] that handles soft wrapping.
 //! - [`BlockMap`] that tracks custom blocks such as diagnostics that should be displayed within buffer.
 //! - [`DisplayMap`] that adds background highlights to the regions of text.
@@ -18,10 +18,11 @@
 //! [EditorElement]: crate::element::EditorElement
 
 mod block_map;
+mod char_map;
 mod crease_map;
 mod fold_map;
 mod inlay_map;
-mod tab_map;
+mod invisibles;
 mod wrap_map;
 
 use crate::{
@@ -32,6 +33,7 @@ pub use block_map::{
     BlockMap, BlockPoint, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
 };
 use block_map::{BlockRow, BlockSnapshot};
+use char_map::{CharMap, CharSnapshot};
 use collections::{HashMap, HashSet};
 pub use crease_map::*;
 pub use fold_map::{Fold, FoldId, FoldPlaceholder, FoldPoint};
@@ -42,6 +44,7 @@ use gpui::{
 pub(crate) use inlay_map::Inlay;
 use inlay_map::{InlayMap, InlaySnapshot};
 pub use inlay_map::{InlayOffset, InlayPoint};
+pub use invisibles::is_invisible;
 use language::{
     language_settings::language_settings, ChunkRenderer, OffsetUtf16, Point,
     Subscription as BufferSubscription,
@@ -61,9 +64,9 @@ use std::{
     sync::Arc,
 };
 use sum_tree::{Bias, TreeMap};
-use tab_map::{TabMap, TabSnapshot};
 use text::LineIndent;
-use ui::WindowContext;
+use ui::{px, WindowContext};
+use unicode_segmentation::UnicodeSegmentation;
 use wrap_map::{WrapMap, WrapSnapshot};
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -94,7 +97,7 @@ pub struct DisplayMap {
     /// Decides where the fold indicators should be and tracks parts of a source file that are currently folded.
     fold_map: FoldMap,
     /// Keeps track of hard tabs in a buffer.
-    tab_map: TabMap,
+    char_map: CharMap,
     /// Handles soft wrapping.
     wrap_map: Model<WrapMap>,
     /// Tracks custom blocks such as diagnostics that should be displayed within buffer.
@@ -131,7 +134,7 @@ impl DisplayMap {
         let crease_map = CreaseMap::new(&buffer_snapshot);
         let (inlay_map, snapshot) = InlayMap::new(buffer_snapshot);
         let (fold_map, snapshot) = FoldMap::new(snapshot);
-        let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
+        let (char_map, snapshot) = CharMap::new(snapshot, tab_size);
         let (wrap_map, snapshot) = WrapMap::new(snapshot, font, font_size, wrap_width, cx);
         let block_map = BlockMap::new(
             snapshot,
@@ -148,7 +151,7 @@ impl DisplayMap {
             buffer_subscription,
             fold_map,
             inlay_map,
-            tab_map,
+            char_map,
             wrap_map,
             block_map,
             crease_map,
@@ -166,17 +169,17 @@ impl DisplayMap {
         let (inlay_snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits);
         let (fold_snapshot, edits) = self.fold_map.read(inlay_snapshot.clone(), edits);
         let tab_size = Self::tab_size(&self.buffer, cx);
-        let (tab_snapshot, edits) = self.tab_map.sync(fold_snapshot.clone(), edits, tab_size);
+        let (char_snapshot, edits) = self.char_map.sync(fold_snapshot.clone(), edits, tab_size);
         let (wrap_snapshot, edits) = self
             .wrap_map
-            .update(cx, |map, cx| map.sync(tab_snapshot.clone(), edits, cx));
+            .update(cx, |map, cx| map.sync(char_snapshot.clone(), edits, cx));
         let block_snapshot = self.block_map.read(wrap_snapshot.clone(), edits).snapshot;
 
         DisplaySnapshot {
             buffer_snapshot: self.buffer.read(cx).snapshot(cx),
             fold_snapshot,
             inlay_snapshot,
-            tab_snapshot,
+            char_snapshot,
             wrap_snapshot,
             block_snapshot,
             crease_snapshot: self.crease_map.snapshot(),
@@ -212,13 +215,13 @@ impl DisplayMap {
         let tab_size = Self::tab_size(&self.buffer, cx);
         let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
         let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
-        let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
+        let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
         let (snapshot, edits) = self
             .wrap_map
             .update(cx, |map, cx| map.sync(snapshot, edits, cx));
         self.block_map.read(snapshot, edits);
         let (snapshot, edits) = fold_map.fold(ranges);
-        let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
+        let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
         let (snapshot, edits) = self
             .wrap_map
             .update(cx, |map, cx| map.sync(snapshot, edits, cx));
@@ -236,13 +239,13 @@ impl DisplayMap {
         let tab_size = Self::tab_size(&self.buffer, cx);
         let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
         let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
-        let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
+        let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
         let (snapshot, edits) = self
             .wrap_map
             .update(cx, |map, cx| map.sync(snapshot, edits, cx));
         self.block_map.read(snapshot, edits);
         let (snapshot, edits) = fold_map.unfold(ranges, inclusive);
-        let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
+        let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
         let (snapshot, edits) = self
             .wrap_map
             .update(cx, |map, cx| map.sync(snapshot, edits, cx));
@@ -277,7 +280,7 @@ impl DisplayMap {
         let tab_size = Self::tab_size(&self.buffer, cx);
         let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
         let (snapshot, edits) = self.fold_map.read(snapshot, edits);
-        let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
+        let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
         let (snapshot, edits) = self
             .wrap_map
             .update(cx, |map, cx| map.sync(snapshot, edits, cx));
@@ -295,7 +298,7 @@ impl DisplayMap {
         let tab_size = Self::tab_size(&self.buffer, cx);
         let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
         let (snapshot, edits) = self.fold_map.read(snapshot, edits);
-        let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
+        let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
         let (snapshot, edits) = self
             .wrap_map
             .update(cx, |map, cx| map.sync(snapshot, edits, cx));
@@ -313,7 +316,7 @@ impl DisplayMap {
         let tab_size = Self::tab_size(&self.buffer, cx);
         let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
         let (snapshot, edits) = self.fold_map.read(snapshot, edits);
-        let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
+        let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
         let (snapshot, edits) = self
             .wrap_map
             .update(cx, |map, cx| map.sync(snapshot, edits, cx));
@@ -331,7 +334,7 @@ impl DisplayMap {
         let tab_size = Self::tab_size(&self.buffer, cx);
         let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
         let (snapshot, edits) = self.fold_map.read(snapshot, edits);
-        let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
+        let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
         let (snapshot, edits) = self
             .wrap_map
             .update(cx, |map, cx| map.sync(snapshot, edits, cx));
@@ -407,7 +410,7 @@ impl DisplayMap {
         let (snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits);
         let (snapshot, edits) = self.fold_map.read(snapshot, edits);
         let tab_size = Self::tab_size(&self.buffer, cx);
-        let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
+        let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
         let (snapshot, edits) = self
             .wrap_map
             .update(cx, |map, cx| map.sync(snapshot, edits, cx));
@@ -415,7 +418,7 @@ impl DisplayMap {
 
         let (snapshot, edits) = self.inlay_map.splice(to_remove, to_insert);
         let (snapshot, edits) = self.fold_map.read(snapshot, edits);
-        let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
+        let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
         let (snapshot, edits) = self
             .wrap_map
             .update(cx, |map, cx| map.sync(snapshot, edits, cx));
@@ -467,7 +470,7 @@ pub struct DisplaySnapshot {
     pub fold_snapshot: FoldSnapshot,
     pub crease_snapshot: CreaseSnapshot,
     inlay_snapshot: InlaySnapshot,
-    tab_snapshot: TabSnapshot,
+    char_snapshot: CharSnapshot,
     wrap_snapshot: WrapSnapshot,
     block_snapshot: BlockSnapshot,
     text_highlights: TextHighlights,
@@ -567,8 +570,8 @@ impl DisplaySnapshot {
     fn point_to_display_point(&self, point: MultiBufferPoint, bias: Bias) -> DisplayPoint {
         let inlay_point = self.inlay_snapshot.to_inlay_point(point);
         let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
-        let tab_point = self.tab_snapshot.to_tab_point(fold_point);
-        let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point);
+        let char_point = self.char_snapshot.to_char_point(fold_point);
+        let wrap_point = self.wrap_snapshot.char_point_to_wrap_point(char_point);
         let block_point = self.block_snapshot.to_block_point(wrap_point);
         DisplayPoint(block_point)
     }
@@ -596,21 +599,21 @@ impl DisplaySnapshot {
     fn display_point_to_inlay_point(&self, point: DisplayPoint, bias: Bias) -> InlayPoint {
         let block_point = point.0;
         let wrap_point = self.block_snapshot.to_wrap_point(block_point);
-        let tab_point = self.wrap_snapshot.to_tab_point(wrap_point);
-        let fold_point = self.tab_snapshot.to_fold_point(tab_point, bias).0;
+        let char_point = self.wrap_snapshot.to_char_point(wrap_point);
+        let fold_point = self.char_snapshot.to_fold_point(char_point, bias).0;
         fold_point.to_inlay_point(&self.fold_snapshot)
     }
 
     pub fn display_point_to_fold_point(&self, point: DisplayPoint, bias: Bias) -> FoldPoint {
         let block_point = point.0;
         let wrap_point = self.block_snapshot.to_wrap_point(block_point);
-        let tab_point = self.wrap_snapshot.to_tab_point(wrap_point);
-        self.tab_snapshot.to_fold_point(tab_point, bias).0
+        let char_point = self.wrap_snapshot.to_char_point(wrap_point);
+        self.char_snapshot.to_fold_point(char_point, bias).0
     }
 
     pub fn fold_point_to_display_point(&self, fold_point: FoldPoint) -> DisplayPoint {
-        let tab_point = self.tab_snapshot.to_tab_point(fold_point);
-        let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point);
+        let char_point = self.char_snapshot.to_char_point(fold_point);
+        let wrap_point = self.wrap_snapshot.char_point_to_wrap_point(char_point);
         let block_point = self.block_snapshot.to_block_point(wrap_point);
         DisplayPoint(block_point)
     }
@@ -688,6 +691,23 @@ impl DisplaySnapshot {
                 }
             }
 
+            if chunk.is_invisible {
+                let invisible_highlight = HighlightStyle {
+                    background_color: Some(editor_style.status.hint_background),
+                    underline: Some(UnderlineStyle {
+                        color: Some(editor_style.status.hint),
+                        thickness: px(1.),
+                        wavy: false,
+                    }),
+                    ..Default::default()
+                };
+                if let Some(highlight_style) = highlight_style.as_mut() {
+                    highlight_style.highlight(invisible_highlight);
+                } else {
+                    highlight_style = Some(invisible_highlight);
+                }
+            }
+
             let mut diagnostic_highlight = HighlightStyle::default();
 
             if chunk.is_unnecessary {
@@ -784,12 +804,11 @@ impl DisplaySnapshot {
         layout_line.closest_index_for_x(x) as u32
     }
 
-    pub fn display_chars_at(
-        &self,
-        mut point: DisplayPoint,
-    ) -> impl Iterator<Item = (char, DisplayPoint)> + '_ {
+    pub fn grapheme_at(&self, mut point: DisplayPoint) -> Option<String> {
         point = DisplayPoint(self.block_snapshot.clip_point(point.0, Bias::Left));
-        self.text_chunks(point.row())
+
+        let chars = self
+            .text_chunks(point.row())
             .flat_map(str::chars)
             .skip_while({
                 let mut column = 0;
@@ -799,16 +818,21 @@ impl DisplaySnapshot {
                     !at_point
                 }
             })
-            .map(move |ch| {
-                let result = (ch, point);
-                if ch == '\n' {
-                    *point.row_mut() += 1;
-                    *point.column_mut() = 0;
-                } else {
-                    *point.column_mut() += ch.len_utf8() as u32;
+            .take_while({
+                let mut prev = false;
+                move |char| {
+                    let now = char.is_ascii();
+                    let end = char.is_ascii() && (char.is_ascii_whitespace() || prev);
+                    prev = now;
+                    !end
                 }
-                result
-            })
+            });
+
+        chars
+            .collect::<String>()
+            .graphemes(true)
+            .next()
+            .map(|s| s.to_owned())
     }
 
     pub fn buffer_chars_at(&self, mut offset: usize) -> impl Iterator<Item = (char, usize)> + '_ {
@@ -1120,8 +1144,8 @@ impl DisplayPoint {
 
     pub fn to_offset(self, map: &DisplaySnapshot, bias: Bias) -> usize {
         let wrap_point = map.block_snapshot.to_wrap_point(self.0);
-        let tab_point = map.wrap_snapshot.to_tab_point(wrap_point);
-        let fold_point = map.tab_snapshot.to_fold_point(tab_point, bias).0;
+        let char_point = map.wrap_snapshot.to_char_point(wrap_point);
+        let fold_point = map.char_snapshot.to_fold_point(char_point, bias).0;
         let inlay_point = fold_point.to_inlay_point(&map.fold_snapshot);
         map.inlay_snapshot
             .to_buffer_offset(map.inlay_snapshot.to_offset(inlay_point))
@@ -1228,7 +1252,7 @@ pub mod tests {
         let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
         log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text());
         log::info!("fold text: {:?}", snapshot.fold_snapshot.text());
-        log::info!("tab text: {:?}", snapshot.tab_snapshot.text());
+        log::info!("char text: {:?}", snapshot.char_snapshot.text());
         log::info!("wrap text: {:?}", snapshot.wrap_snapshot.text());
         log::info!("block text: {:?}", snapshot.block_snapshot.text());
         log::info!("display text: {:?}", snapshot.text());
@@ -1345,7 +1369,7 @@ pub mod tests {
             fold_count = snapshot.fold_count();
             log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text());
             log::info!("fold text: {:?}", snapshot.fold_snapshot.text());
-            log::info!("tab text: {:?}", snapshot.tab_snapshot.text());
+            log::info!("char text: {:?}", snapshot.char_snapshot.text());
             log::info!("wrap text: {:?}", snapshot.wrap_snapshot.text());
             log::info!("block text: {:?}", snapshot.block_snapshot.text());
             log::info!("display text: {:?}", snapshot.text());

crates/editor/src/display_map/block_map.rs 🔗

@@ -1421,7 +1421,7 @@ fn offset_for_row(s: &str, target: u32) -> (u32, usize) {
 mod tests {
     use super::*;
     use crate::display_map::{
-        fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap, wrap_map::WrapMap,
+        char_map::CharMap, fold_map::FoldMap, inlay_map::InlayMap, wrap_map::WrapMap,
     };
     use gpui::{div, font, px, AppContext, Context as _, Element};
     use language::{Buffer, Capability};
@@ -1456,9 +1456,9 @@ mod tests {
         let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
         let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
         let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
-        let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap());
+        let (mut char_map, char_snapshot) = CharMap::new(fold_snapshot, 1.try_into().unwrap());
         let (wrap_map, wraps_snapshot) =
-            cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
+            cx.update(|cx| WrapMap::new(char_snapshot, font("Helvetica"), px(14.0), None, cx));
         let mut block_map = BlockMap::new(wraps_snapshot.clone(), true, 1, 1, 1);
 
         let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
@@ -1609,10 +1609,10 @@ mod tests {
         let (inlay_snapshot, inlay_edits) =
             inlay_map.sync(buffer_snapshot, subscription.consume().into_inner());
         let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
-        let (tab_snapshot, tab_edits) =
-            tab_map.sync(fold_snapshot, fold_edits, 4.try_into().unwrap());
+        let (char_snapshot, tab_edits) =
+            char_map.sync(fold_snapshot, fold_edits, 4.try_into().unwrap());
         let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
-            wrap_map.sync(tab_snapshot, tab_edits, cx)
+            wrap_map.sync(char_snapshot, tab_edits, cx)
         });
         let snapshot = block_map.read(wraps_snapshot, wrap_edits);
         assert_eq!(snapshot.text(), "aaa\n\nb!!!\n\n\nbb\nccc\nddd\n\n\n");
@@ -1672,8 +1672,9 @@ mod tests {
         let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
         let (_, inlay_snapshot) = InlayMap::new(multi_buffer_snapshot.clone());
         let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
-        let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
-        let (_, wraps_snapshot) = WrapMap::new(tab_snapshot, font, font_size, Some(wrap_width), cx);
+        let (_, char_snapshot) = CharMap::new(fold_snapshot, 4.try_into().unwrap());
+        let (_, wraps_snapshot) =
+            WrapMap::new(char_snapshot, font, font_size, Some(wrap_width), cx);
 
         let block_map = BlockMap::new(wraps_snapshot.clone(), true, 1, 1, 1);
         let snapshot = block_map.read(wraps_snapshot, Default::default());
@@ -1710,9 +1711,9 @@ mod tests {
         let _subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
         let (_inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
         let (_fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
-        let (_tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap());
+        let (_char_map, char_snapshot) = CharMap::new(fold_snapshot, 1.try_into().unwrap());
         let (_wrap_map, wraps_snapshot) =
-            cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
+            cx.update(|cx| WrapMap::new(char_snapshot, font("Helvetica"), px(14.0), None, cx));
         let mut block_map = BlockMap::new(wraps_snapshot.clone(), false, 1, 1, 0);
 
         let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
@@ -1815,9 +1816,15 @@ mod tests {
         let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
         let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
         let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
-        let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
+        let (_, char_snapshot) = CharMap::new(fold_snapshot, 4.try_into().unwrap());
         let (_, wraps_snapshot) = cx.update(|cx| {
-            WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), Some(px(60.)), cx)
+            WrapMap::new(
+                char_snapshot,
+                font("Helvetica"),
+                px(14.0),
+                Some(px(60.)),
+                cx,
+            )
         });
         let mut block_map = BlockMap::new(wraps_snapshot.clone(), true, 1, 1, 0);
 
@@ -1885,9 +1892,9 @@ mod tests {
         let mut buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
         let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
         let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
-        let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
+        let (mut char_map, char_snapshot) = CharMap::new(fold_snapshot, 4.try_into().unwrap());
         let (wrap_map, wraps_snapshot) = cx
-            .update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), font_size, wrap_width, cx));
+            .update(|cx| WrapMap::new(char_snapshot, font("Helvetica"), font_size, wrap_width, cx));
         let mut block_map = BlockMap::new(
             wraps_snapshot,
             true,
@@ -1944,10 +1951,10 @@ mod tests {
                     let (inlay_snapshot, inlay_edits) =
                         inlay_map.sync(buffer_snapshot.clone(), vec![]);
                     let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
-                    let (tab_snapshot, tab_edits) =
-                        tab_map.sync(fold_snapshot, fold_edits, tab_size);
+                    let (char_snapshot, tab_edits) =
+                        char_map.sync(fold_snapshot, fold_edits, tab_size);
                     let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
-                        wrap_map.sync(tab_snapshot, tab_edits, cx)
+                        wrap_map.sync(char_snapshot, tab_edits, cx)
                     });
                     let mut block_map = block_map.write(wraps_snapshot, wrap_edits);
                     let block_ids =
@@ -1976,10 +1983,10 @@ mod tests {
                     let (inlay_snapshot, inlay_edits) =
                         inlay_map.sync(buffer_snapshot.clone(), vec![]);
                     let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
-                    let (tab_snapshot, tab_edits) =
-                        tab_map.sync(fold_snapshot, fold_edits, tab_size);
+                    let (char_snapshot, tab_edits) =
+                        char_map.sync(fold_snapshot, fold_edits, tab_size);
                     let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
-                        wrap_map.sync(tab_snapshot, tab_edits, cx)
+                        wrap_map.sync(char_snapshot, tab_edits, cx)
                     });
                     let mut block_map = block_map.write(wraps_snapshot, wrap_edits);
                     block_map.remove(block_ids_to_remove);
@@ -1999,9 +2006,9 @@ mod tests {
             let (inlay_snapshot, inlay_edits) =
                 inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
             let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
-            let (tab_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
+            let (char_snapshot, tab_edits) = char_map.sync(fold_snapshot, fold_edits, tab_size);
             let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
-                wrap_map.sync(tab_snapshot, tab_edits, cx)
+                wrap_map.sync(char_snapshot, tab_edits, cx)
             });
             let blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits);
             assert_eq!(
@@ -2084,7 +2091,10 @@ mod tests {
                     }
                 }
 
-                let soft_wrapped = wraps_snapshot.to_tab_point(WrapPoint::new(row, 0)).column() > 0;
+                let soft_wrapped = wraps_snapshot
+                    .to_char_point(WrapPoint::new(row, 0))
+                    .column()
+                    > 0;
                 expected_buffer_rows.push(if soft_wrapped { None } else { buffer_row });
                 expected_text.push_str(input_line);
 

crates/editor/src/display_map/tab_map.rs → crates/editor/src/display_map/char_map.rs 🔗

@@ -1,5 +1,6 @@
 use super::{
     fold_map::{self, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot},
+    invisibles::{is_invisible, replacement},
     Highlights,
 };
 use language::{Chunk, Point};
@@ -9,14 +10,14 @@ use sum_tree::Bias;
 
 const MAX_EXPANSION_COLUMN: u32 = 256;
 
-/// Keeps track of hard tabs in a text buffer.
+/// Keeps track of hard tabs and non-printable characters in a text buffer.
 ///
 /// See the [`display_map` module documentation](crate::display_map) for more information.
-pub struct TabMap(TabSnapshot);
+pub struct CharMap(CharSnapshot);
 
-impl TabMap {
-    pub fn new(fold_snapshot: FoldSnapshot, tab_size: NonZeroU32) -> (Self, TabSnapshot) {
-        let snapshot = TabSnapshot {
+impl CharMap {
+    pub fn new(fold_snapshot: FoldSnapshot, tab_size: NonZeroU32) -> (Self, CharSnapshot) {
+        let snapshot = CharSnapshot {
             fold_snapshot,
             tab_size,
             max_expansion_column: MAX_EXPANSION_COLUMN,
@@ -26,7 +27,7 @@ impl TabMap {
     }
 
     #[cfg(test)]
-    pub fn set_max_expansion_column(&mut self, column: u32) -> TabSnapshot {
+    pub fn set_max_expansion_column(&mut self, column: u32) -> CharSnapshot {
         self.0.max_expansion_column = column;
         self.0.clone()
     }
@@ -36,9 +37,9 @@ impl TabMap {
         fold_snapshot: FoldSnapshot,
         mut fold_edits: Vec<FoldEdit>,
         tab_size: NonZeroU32,
-    ) -> (TabSnapshot, Vec<TabEdit>) {
+    ) -> (CharSnapshot, Vec<TabEdit>) {
         let old_snapshot = &mut self.0;
-        let mut new_snapshot = TabSnapshot {
+        let mut new_snapshot = CharSnapshot {
             fold_snapshot,
             tab_size,
             max_expansion_column: old_snapshot.max_expansion_column,
@@ -137,15 +138,15 @@ impl TabMap {
                 let new_start = fold_edit.new.start.to_point(&new_snapshot.fold_snapshot);
                 let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot);
                 tab_edits.push(TabEdit {
-                    old: old_snapshot.to_tab_point(old_start)..old_snapshot.to_tab_point(old_end),
-                    new: new_snapshot.to_tab_point(new_start)..new_snapshot.to_tab_point(new_end),
+                    old: old_snapshot.to_char_point(old_start)..old_snapshot.to_char_point(old_end),
+                    new: new_snapshot.to_char_point(new_start)..new_snapshot.to_char_point(new_end),
                 });
             }
         } else {
             new_snapshot.version += 1;
             tab_edits.push(TabEdit {
-                old: TabPoint::zero()..old_snapshot.max_point(),
-                new: TabPoint::zero()..new_snapshot.max_point(),
+                old: CharPoint::zero()..old_snapshot.max_point(),
+                new: CharPoint::zero()..new_snapshot.max_point(),
             });
         }
 
@@ -155,14 +156,14 @@ impl TabMap {
 }
 
 #[derive(Clone)]
-pub struct TabSnapshot {
+pub struct CharSnapshot {
     pub fold_snapshot: FoldSnapshot,
     pub tab_size: NonZeroU32,
     pub max_expansion_column: u32,
     pub version: usize,
 }
 
-impl TabSnapshot {
+impl CharSnapshot {
     pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
         &self.fold_snapshot.inlay_snapshot.buffer
     }
@@ -170,7 +171,7 @@ impl TabSnapshot {
     pub fn line_len(&self, row: u32) -> u32 {
         let max_point = self.max_point();
         if row < max_point.row() {
-            self.to_tab_point(FoldPoint::new(row, self.fold_snapshot.line_len(row)))
+            self.to_char_point(FoldPoint::new(row, self.fold_snapshot.line_len(row)))
                 .0
                 .column
         } else {
@@ -179,10 +180,10 @@ impl TabSnapshot {
     }
 
     pub fn text_summary(&self) -> TextSummary {
-        self.text_summary_for_range(TabPoint::zero()..self.max_point())
+        self.text_summary_for_range(CharPoint::zero()..self.max_point())
     }
 
-    pub fn text_summary_for_range(&self, range: Range<TabPoint>) -> TextSummary {
+    pub fn text_summary_for_range(&self, range: Range<CharPoint>) -> TextSummary {
         let input_start = self.to_fold_point(range.start, Bias::Left).0;
         let input_end = self.to_fold_point(range.end, Bias::Right).0;
         let input_summary = self
@@ -211,7 +212,7 @@ impl TabSnapshot {
         } else {
             for _ in self
                 .chunks(
-                    TabPoint::new(range.end.row(), 0)..range.end,
+                    CharPoint::new(range.end.row(), 0)..range.end,
                     false,
                     Highlights::default(),
                 )
@@ -232,7 +233,7 @@ impl TabSnapshot {
 
     pub fn chunks<'a>(
         &'a self,
-        range: Range<TabPoint>,
+        range: Range<CharPoint>,
         language_aware: bool,
         highlights: Highlights<'a>,
     ) -> TabChunks<'a> {
@@ -278,7 +279,7 @@ impl TabSnapshot {
     #[cfg(test)]
     pub fn text(&self) -> String {
         self.chunks(
-            TabPoint::zero()..self.max_point(),
+            CharPoint::zero()..self.max_point(),
             false,
             Highlights::default(),
         )
@@ -286,24 +287,24 @@ impl TabSnapshot {
         .collect()
     }
 
-    pub fn max_point(&self) -> TabPoint {
-        self.to_tab_point(self.fold_snapshot.max_point())
+    pub fn max_point(&self) -> CharPoint {
+        self.to_char_point(self.fold_snapshot.max_point())
     }
 
-    pub fn clip_point(&self, point: TabPoint, bias: Bias) -> TabPoint {
-        self.to_tab_point(
+    pub fn clip_point(&self, point: CharPoint, bias: Bias) -> CharPoint {
+        self.to_char_point(
             self.fold_snapshot
                 .clip_point(self.to_fold_point(point, bias).0, bias),
         )
     }
 
-    pub fn to_tab_point(&self, input: FoldPoint) -> TabPoint {
+    pub fn to_char_point(&self, input: FoldPoint) -> CharPoint {
         let chars = self.fold_snapshot.chars_at(FoldPoint::new(input.row(), 0));
         let expanded = self.expand_tabs(chars, input.column());
-        TabPoint::new(input.row(), expanded)
+        CharPoint::new(input.row(), expanded)
     }
 
-    pub fn to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, u32, u32) {
+    pub fn to_fold_point(&self, output: CharPoint, bias: Bias) -> (FoldPoint, u32, u32) {
         let chars = self.fold_snapshot.chars_at(FoldPoint::new(output.row(), 0));
         let expanded = output.column();
         let (collapsed, expanded_char_column, to_next_stop) =
@@ -315,13 +316,13 @@ impl TabSnapshot {
         )
     }
 
-    pub fn make_tab_point(&self, point: Point, bias: Bias) -> TabPoint {
+    pub fn make_char_point(&self, point: Point, bias: Bias) -> CharPoint {
         let inlay_point = self.fold_snapshot.inlay_snapshot.to_inlay_point(point);
         let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
-        self.to_tab_point(fold_point)
+        self.to_char_point(fold_point)
     }
 
-    pub fn to_point(&self, point: TabPoint, bias: Bias) -> Point {
+    pub fn to_point(&self, point: CharPoint, bias: Bias) -> Point {
         let fold_point = self.to_fold_point(point, bias).0;
         let inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
         self.fold_snapshot
@@ -344,6 +345,9 @@ impl TabSnapshot {
                 let tab_len = tab_size - expanded_chars % tab_size;
                 expanded_bytes += tab_len;
                 expanded_chars += tab_len;
+            } else if let Some(replacement) = replacement(c) {
+                expanded_chars += replacement.chars().count() as u32;
+                expanded_bytes += replacement.len() as u32;
             } else {
                 expanded_bytes += c.len_utf8() as u32;
                 expanded_chars += 1;
@@ -383,6 +387,9 @@ impl TabSnapshot {
                         Bias::Right => (collapsed_bytes + 1, expanded_chars, 0),
                     };
                 }
+            } else if let Some(replacement) = replacement(c) {
+                expanded_chars += replacement.chars().count() as u32;
+                expanded_bytes += replacement.len() as u32;
             } else {
                 expanded_chars += 1;
                 expanded_bytes += c.len_utf8() as u32;
@@ -404,9 +411,9 @@ impl TabSnapshot {
 }
 
 #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
-pub struct TabPoint(pub Point);
+pub struct CharPoint(pub Point);
 
-impl TabPoint {
+impl CharPoint {
     pub fn new(row: u32, column: u32) -> Self {
         Self(Point::new(row, column))
     }
@@ -424,13 +431,13 @@ impl TabPoint {
     }
 }
 
-impl From<Point> for TabPoint {
+impl From<Point> for CharPoint {
     fn from(point: Point) -> Self {
         Self(point)
     }
 }
 
-pub type TabEdit = text::Edit<TabPoint>;
+pub type TabEdit = text::Edit<CharPoint>;
 
 #[derive(Clone, Debug, Default, Eq, PartialEq)]
 pub struct TextSummary {
@@ -551,6 +558,37 @@ impl<'a> Iterator for TabChunks<'a> {
                     self.input_column = 0;
                     self.output_position += Point::new(1, 0);
                 }
+                _ if is_invisible(c) => {
+                    if ix > 0 {
+                        let (prefix, suffix) = self.chunk.text.split_at(ix);
+                        self.chunk.text = suffix;
+                        return Some(Chunk {
+                            text: prefix,
+                            is_invisible: false,
+                            ..self.chunk.clone()
+                        });
+                    }
+                    let c_len = c.len_utf8();
+                    let replacement = replacement(c).unwrap_or(&self.chunk.text[..c_len]);
+                    if self.chunk.text.len() >= c_len {
+                        self.chunk.text = &self.chunk.text[c_len..];
+                    } else {
+                        self.chunk.text = "";
+                    }
+                    let len = replacement.chars().count() as u32;
+                    let next_output_position = cmp::min(
+                        self.output_position + Point::new(0, len),
+                        self.max_output_position,
+                    );
+                    self.column += len;
+                    self.input_column += 1;
+                    self.output_position = next_output_position;
+                    return Some(Chunk {
+                        text: replacement,
+                        is_invisible: true,
+                        ..self.chunk.clone()
+                    });
+                }
                 _ => {
                     self.column += 1;
                     if !self.inside_leading_tab {
@@ -580,11 +618,11 @@ mod tests {
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
         let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
         let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
-        let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
+        let (_, char_snapshot) = CharMap::new(fold_snapshot, 4.try_into().unwrap());
 
-        assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 0), 0);
-        assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 1), 4);
-        assert_eq!(tab_snapshot.expand_tabs("\ta".chars(), 2), 5);
+        assert_eq!(char_snapshot.expand_tabs("\t".chars(), 0), 0);
+        assert_eq!(char_snapshot.expand_tabs("\t".chars(), 1), 4);
+        assert_eq!(char_snapshot.expand_tabs("\ta".chars(), 2), 5);
     }
 
     #[gpui::test]
@@ -597,16 +635,16 @@ mod tests {
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
         let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
         let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
-        let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
+        let (_, mut char_snapshot) = CharMap::new(fold_snapshot, 4.try_into().unwrap());
 
-        tab_snapshot.max_expansion_column = max_expansion_column;
-        assert_eq!(tab_snapshot.text(), output);
+        char_snapshot.max_expansion_column = max_expansion_column;
+        assert_eq!(char_snapshot.text(), output);
 
         for (ix, c) in input.char_indices() {
             assert_eq!(
-                tab_snapshot
+                char_snapshot
                     .chunks(
-                        TabPoint::new(0, ix as u32)..tab_snapshot.max_point(),
+                        CharPoint::new(0, ix as u32)..char_snapshot.max_point(),
                         false,
                         Highlights::default(),
                     )
@@ -620,13 +658,13 @@ mod tests {
                 let input_point = Point::new(0, ix as u32);
                 let output_point = Point::new(0, output.find(c).unwrap() as u32);
                 assert_eq!(
-                    tab_snapshot.to_tab_point(FoldPoint(input_point)),
-                    TabPoint(output_point),
-                    "to_tab_point({input_point:?})"
+                    char_snapshot.to_char_point(FoldPoint(input_point)),
+                    CharPoint(output_point),
+                    "to_char_point({input_point:?})"
                 );
                 assert_eq!(
-                    tab_snapshot
-                        .to_fold_point(TabPoint(output_point), Bias::Left)
+                    char_snapshot
+                        .to_fold_point(CharPoint(output_point), Bias::Left)
                         .0,
                     FoldPoint(input_point),
                     "to_fold_point({output_point:?})"
@@ -644,10 +682,10 @@ mod tests {
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
         let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
         let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
-        let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
+        let (_, mut char_snapshot) = CharMap::new(fold_snapshot, 4.try_into().unwrap());
 
-        tab_snapshot.max_expansion_column = max_expansion_column;
-        assert_eq!(tab_snapshot.text(), input);
+        char_snapshot.max_expansion_column = max_expansion_column;
+        assert_eq!(char_snapshot.text(), input);
     }
 
     #[gpui::test]
@@ -658,10 +696,10 @@ mod tests {
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
         let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
         let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
-        let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
+        let (_, char_snapshot) = CharMap::new(fold_snapshot, 4.try_into().unwrap());
 
         assert_eq!(
-            chunks(&tab_snapshot, TabPoint::zero()),
+            chunks(&char_snapshot, CharPoint::zero()),
             vec![
                 ("    ".to_string(), true),
                 (" ".to_string(), false),
@@ -670,7 +708,7 @@ mod tests {
             ]
         );
         assert_eq!(
-            chunks(&tab_snapshot, TabPoint::new(0, 2)),
+            chunks(&char_snapshot, CharPoint::new(0, 2)),
             vec![
                 ("  ".to_string(), true),
                 (" ".to_string(), false),
@@ -679,7 +717,7 @@ mod tests {
             ]
         );
 
-        fn chunks(snapshot: &TabSnapshot, start: TabPoint) -> Vec<(String, bool)> {
+        fn chunks(snapshot: &CharSnapshot, start: CharPoint) -> Vec<(String, bool)> {
             let mut chunks = Vec::new();
             let mut was_tab = false;
             let mut text = String::new();
@@ -725,12 +763,12 @@ mod tests {
         let (inlay_snapshot, _) = inlay_map.randomly_mutate(&mut 0, &mut rng);
         log::info!("InlayMap text: {:?}", inlay_snapshot.text());
 
-        let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
-        let tabs_snapshot = tab_map.set_max_expansion_column(32);
+        let (mut char_map, _) = CharMap::new(fold_snapshot.clone(), tab_size);
+        let tabs_snapshot = char_map.set_max_expansion_column(32);
 
         let text = text::Rope::from(tabs_snapshot.text().as_str());
         log::info!(
-            "TabMap text (tab size: {}): {:?}",
+            "CharMap text (tab size: {}): {:?}",
             tab_size,
             tabs_snapshot.text(),
         );
@@ -738,11 +776,11 @@ mod tests {
         for _ in 0..5 {
             let end_row = rng.gen_range(0..=text.max_point().row);
             let end_column = rng.gen_range(0..=text.line_len(end_row));
-            let mut end = TabPoint(text.clip_point(Point::new(end_row, end_column), Bias::Right));
+            let mut end = CharPoint(text.clip_point(Point::new(end_row, end_column), Bias::Right));
             let start_row = rng.gen_range(0..=text.max_point().row);
             let start_column = rng.gen_range(0..=text.line_len(start_row));
             let mut start =
-                TabPoint(text.clip_point(Point::new(start_row, start_column), Bias::Left));
+                CharPoint(text.clip_point(Point::new(start_row, start_column), Bias::Left));
             if start > end {
                 mem::swap(&mut start, &mut end);
             }

crates/editor/src/display_map/invisibles.rs 🔗

@@ -0,0 +1,157 @@
+use std::sync::LazyLock;
+
+use collections::HashMap;
+
+// Invisibility in a Unicode context is not well defined, so we have to guess.
+//
+// We highlight all ASCII control codes, and unicode whitespace because they are likely
+// confused with a normal space (U+0020).
+//
+// We also highlight the handful of blank non-space characters:
+//   U+2800 BRAILLE PATTERN BLANK - Category: So
+//   U+115F HANGUL CHOSEONG FILLER - Category: Lo
+//   U+1160 HANGUL CHOSEONG FILLER - Category: Lo
+//   U+3164 HANGUL FILLER - Category: Lo
+//   U+FFA0 HALFWIDTH HANGUL FILLER - Category: Lo
+//   U+FFFC OBJECT REPLACEMENT CHARACTER - Category: So
+//
+// For the rest of Unicode, invisibility happens for two reasons:
+// * A Format character (like a byte order mark or right-to-left override)
+// * An invisible Nonspacing Mark character (like U+034F, or variation selectors)
+//
+// We don't consider unassigned codepoints invisible as the font renderer already shows
+// a replacement character in that case (and there are a *lot* of them)
+//
+// Control characters are mostly fine to highlight; except:
+// * U+E0020..=U+E007F are used in emoji flags. We don't highlight them right now, but we could if we tightened our heuristics.
+// * U+200D is used to join characters. We highlight this but don't replace it. As our font system ignores mid-glyph highlights this mostly works to highlight unexpected uses.
+//
+// Nonspacing marks are handled like U+200D. This means that mid-glyph we ignore them, but
+// probably causes issues with end-of-glyph usage.
+//
+// ref: https://invisible-characters.com
+// ref: https://www.compart.com/en/unicode/category/Cf
+// ref: https://gist.github.com/ConradIrwin/f759e1fc29267143c4c7895aa495dca5?h=1
+// ref: https://unicode.org/Public/emoji/13.0/emoji-test.txt
+// https://github.com/bits/UTF-8-Unicode-Test-Documents/blob/master/UTF-8_sequence_separated/utf8_sequence_0-0x10ffff_assigned_including-unprintable-asis.txt
+pub fn is_invisible(c: char) -> bool {
+    if c <= '\u{1f}' {
+        c != '\t' && c != '\n' && c != '\r'
+    } else if c >= '\u{7f}' {
+        c <= '\u{9f}' || c.is_whitespace() || contains(c, &FORMAT) || contains(c, &OTHER)
+    } else {
+        false
+    }
+}
+
+pub(crate) fn replacement(c: char) -> Option<&'static str> {
+    if !is_invisible(c) {
+        return None;
+    }
+    if c <= '\x7f' {
+        REPLACEMENTS.get(&c).copied()
+    } else if contains(c, &PRESERVE) {
+        None
+    } else {
+        Some(" ")
+    }
+}
+
+const REPLACEMENTS: LazyLock<HashMap<char, &'static str>> = LazyLock::new(|| {
+    [
+        ('\x00', "␀"),
+        ('\x01', "␁"),
+        ('\x02', "␂"),
+        ('\x03', "␃"),
+        ('\x04', "␄"),
+        ('\x05', "␅"),
+        ('\x06', "␆"),
+        ('\x07', "␇"),
+        ('\x08', "␈"),
+        ('\x0B', "␋"),
+        ('\x0C', "␌"),
+        ('\x0D', "␍"),
+        ('\x0E', "␎"),
+        ('\x0F', "␏"),
+        ('\x10', "␐"),
+        ('\x11', "␑"),
+        ('\x12', "␒"),
+        ('\x13', "␓"),
+        ('\x14', "␔"),
+        ('\x15', "␕"),
+        ('\x16', "␖"),
+        ('\x17', "␗"),
+        ('\x18', "␘"),
+        ('\x19', "␙"),
+        ('\x1A', "␚"),
+        ('\x1B', "␛"),
+        ('\x1C', "␜"),
+        ('\x1D', "␝"),
+        ('\x1E', "␞"),
+        ('\x1F', "␟"),
+        ('\u{007F}', "␡"),
+    ]
+    .into_iter()
+    .collect()
+});
+
+// generated using ucd-generate: ucd-generate general-category --include Format --chars ucd-16.0.0
+pub const FORMAT: &'static [(char, char)] = &[
+    ('\u{ad}', '\u{ad}'),
+    ('\u{600}', '\u{605}'),
+    ('\u{61c}', '\u{61c}'),
+    ('\u{6dd}', '\u{6dd}'),
+    ('\u{70f}', '\u{70f}'),
+    ('\u{890}', '\u{891}'),
+    ('\u{8e2}', '\u{8e2}'),
+    ('\u{180e}', '\u{180e}'),
+    ('\u{200b}', '\u{200f}'),
+    ('\u{202a}', '\u{202e}'),
+    ('\u{2060}', '\u{2064}'),
+    ('\u{2066}', '\u{206f}'),
+    ('\u{feff}', '\u{feff}'),
+    ('\u{fff9}', '\u{fffb}'),
+    ('\u{110bd}', '\u{110bd}'),
+    ('\u{110cd}', '\u{110cd}'),
+    ('\u{13430}', '\u{1343f}'),
+    ('\u{1bca0}', '\u{1bca3}'),
+    ('\u{1d173}', '\u{1d17a}'),
+    ('\u{e0001}', '\u{e0001}'),
+    ('\u{e0020}', '\u{e007f}'),
+];
+
+// hand-made base on https://invisible-characters.com (Excluding Cf)
+pub const OTHER: &'static [(char, char)] = &[
+    ('\u{034f}', '\u{034f}'),
+    ('\u{115F}', '\u{1160}'),
+    ('\u{17b4}', '\u{17b5}'),
+    ('\u{180b}', '\u{180d}'),
+    ('\u{2800}', '\u{2800}'),
+    ('\u{3164}', '\u{3164}'),
+    ('\u{fe00}', '\u{fe0d}'),
+    ('\u{ffa0}', '\u{ffa0}'),
+    ('\u{fffc}', '\u{fffc}'),
+    ('\u{e0100}', '\u{e01ef}'),
+];
+
+// a subset of FORMAT/OTHER that may appear within glyphs
+const PRESERVE: &'static [(char, char)] = &[
+    ('\u{034f}', '\u{034f}'),
+    ('\u{200d}', '\u{200d}'),
+    ('\u{17b4}', '\u{17b5}'),
+    ('\u{180b}', '\u{180d}'),
+    ('\u{e0061}', '\u{e007a}'),
+    ('\u{e007f}', '\u{e007f}'),
+];
+
+fn contains(c: char, list: &[(char, char)]) -> bool {
+    for (start, end) in list {
+        if c < *start {
+            return false;
+        }
+        if c <= *end {
+            return true;
+        }
+    }
+    false
+}

crates/editor/src/display_map/wrap_map.rs 🔗

@@ -1,6 +1,6 @@
 use super::{
+    char_map::{self, CharPoint, CharSnapshot, TabEdit},
     fold_map::FoldBufferRows,
-    tab_map::{self, TabEdit, TabPoint, TabSnapshot},
     Highlights,
 };
 use gpui::{AppContext, Context, Font, LineWrapper, Model, ModelContext, Pixels, Task};
@@ -12,7 +12,7 @@ use std::{cmp, collections::VecDeque, mem, ops::Range, time::Duration};
 use sum_tree::{Bias, Cursor, SumTree};
 use text::Patch;
 
-pub use super::tab_map::TextSummary;
+pub use super::char_map::TextSummary;
 pub type WrapEdit = text::Edit<u32>;
 
 /// Handles soft wrapping of text.
@@ -20,7 +20,7 @@ pub type WrapEdit = text::Edit<u32>;
 /// See the [`display_map` module documentation](crate::display_map) for more information.
 pub struct WrapMap {
     snapshot: WrapSnapshot,
-    pending_edits: VecDeque<(TabSnapshot, Vec<TabEdit>)>,
+    pending_edits: VecDeque<(CharSnapshot, Vec<TabEdit>)>,
     interpolated_edits: Patch<u32>,
     edits_since_sync: Patch<u32>,
     wrap_width: Option<Pixels>,
@@ -30,7 +30,7 @@ pub struct WrapMap {
 
 #[derive(Clone)]
 pub struct WrapSnapshot {
-    tab_snapshot: TabSnapshot,
+    char_snapshot: CharSnapshot,
     transforms: SumTree<Transform>,
     interpolated: bool,
 }
@@ -51,11 +51,11 @@ struct TransformSummary {
 pub struct WrapPoint(pub Point);
 
 pub struct WrapChunks<'a> {
-    input_chunks: tab_map::TabChunks<'a>,
+    input_chunks: char_map::TabChunks<'a>,
     input_chunk: Chunk<'a>,
     output_position: WrapPoint,
     max_output_row: u32,
-    transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>,
+    transforms: Cursor<'a, Transform, (WrapPoint, CharPoint)>,
 }
 
 #[derive(Clone)]
@@ -65,12 +65,12 @@ pub struct WrapBufferRows<'a> {
     output_row: u32,
     soft_wrapped: bool,
     max_output_row: u32,
-    transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>,
+    transforms: Cursor<'a, Transform, (WrapPoint, CharPoint)>,
 }
 
 impl WrapMap {
     pub fn new(
-        tab_snapshot: TabSnapshot,
+        char_snapshot: CharSnapshot,
         font: Font,
         font_size: Pixels,
         wrap_width: Option<Pixels>,
@@ -83,7 +83,7 @@ impl WrapMap {
                 pending_edits: Default::default(),
                 interpolated_edits: Default::default(),
                 edits_since_sync: Default::default(),
-                snapshot: WrapSnapshot::new(tab_snapshot),
+                snapshot: WrapSnapshot::new(char_snapshot),
                 background_task: None,
             };
             this.set_wrap_width(wrap_width, cx);
@@ -101,17 +101,17 @@ impl WrapMap {
 
     pub fn sync(
         &mut self,
-        tab_snapshot: TabSnapshot,
+        char_snapshot: CharSnapshot,
         edits: Vec<TabEdit>,
         cx: &mut ModelContext<Self>,
     ) -> (WrapSnapshot, Patch<u32>) {
         if self.wrap_width.is_some() {
-            self.pending_edits.push_back((tab_snapshot, edits));
+            self.pending_edits.push_back((char_snapshot, edits));
             self.flush_edits(cx);
         } else {
             self.edits_since_sync = self
                 .edits_since_sync
-                .compose(self.snapshot.interpolate(tab_snapshot, &edits));
+                .compose(self.snapshot.interpolate(char_snapshot, &edits));
             self.snapshot.interpolated = false;
         }
 
@@ -161,11 +161,11 @@ impl WrapMap {
             let (font, font_size) = self.font_with_size.clone();
             let task = cx.background_executor().spawn(async move {
                 let mut line_wrapper = text_system.line_wrapper(font, font_size);
-                let tab_snapshot = new_snapshot.tab_snapshot.clone();
-                let range = TabPoint::zero()..tab_snapshot.max_point();
+                let char_snapshot = new_snapshot.char_snapshot.clone();
+                let range = CharPoint::zero()..char_snapshot.max_point();
                 let edits = new_snapshot
                     .update(
-                        tab_snapshot,
+                        char_snapshot,
                         &[TabEdit {
                             old: range.clone(),
                             new: range.clone(),
@@ -205,7 +205,7 @@ impl WrapMap {
         } else {
             let old_rows = self.snapshot.transforms.summary().output.lines.row + 1;
             self.snapshot.transforms = SumTree::default();
-            let summary = self.snapshot.tab_snapshot.text_summary();
+            let summary = self.snapshot.char_snapshot.text_summary();
             if !summary.lines.is_zero() {
                 self.snapshot
                     .transforms
@@ -223,8 +223,8 @@ impl WrapMap {
     fn flush_edits(&mut self, cx: &mut ModelContext<Self>) {
         if !self.snapshot.interpolated {
             let mut to_remove_len = 0;
-            for (tab_snapshot, _) in &self.pending_edits {
-                if tab_snapshot.version <= self.snapshot.tab_snapshot.version {
+            for (char_snapshot, _) in &self.pending_edits {
+                if char_snapshot.version <= self.snapshot.char_snapshot.version {
                     to_remove_len += 1;
                 } else {
                     break;
@@ -246,9 +246,9 @@ impl WrapMap {
                 let update_task = cx.background_executor().spawn(async move {
                     let mut edits = Patch::default();
                     let mut line_wrapper = text_system.line_wrapper(font, font_size);
-                    for (tab_snapshot, tab_edits) in pending_edits {
+                    for (char_snapshot, tab_edits) in pending_edits {
                         let wrap_edits = snapshot
-                            .update(tab_snapshot, &tab_edits, wrap_width, &mut line_wrapper)
+                            .update(char_snapshot, &tab_edits, wrap_width, &mut line_wrapper)
                             .await;
                         edits = edits.compose(&wrap_edits);
                     }
@@ -285,11 +285,11 @@ impl WrapMap {
 
         let was_interpolated = self.snapshot.interpolated;
         let mut to_remove_len = 0;
-        for (tab_snapshot, edits) in &self.pending_edits {
-            if tab_snapshot.version <= self.snapshot.tab_snapshot.version {
+        for (char_snapshot, edits) in &self.pending_edits {
+            if char_snapshot.version <= self.snapshot.char_snapshot.version {
                 to_remove_len += 1;
             } else {
-                let interpolated_edits = self.snapshot.interpolate(tab_snapshot.clone(), edits);
+                let interpolated_edits = self.snapshot.interpolate(char_snapshot.clone(), edits);
                 self.edits_since_sync = self.edits_since_sync.compose(&interpolated_edits);
                 self.interpolated_edits = self.interpolated_edits.compose(&interpolated_edits);
             }
@@ -302,45 +302,49 @@ impl WrapMap {
 }
 
 impl WrapSnapshot {
-    fn new(tab_snapshot: TabSnapshot) -> Self {
+    fn new(char_snapshot: CharSnapshot) -> Self {
         let mut transforms = SumTree::default();
-        let extent = tab_snapshot.text_summary();
+        let extent = char_snapshot.text_summary();
         if !extent.lines.is_zero() {
             transforms.push(Transform::isomorphic(extent), &());
         }
         Self {
             transforms,
-            tab_snapshot,
+            char_snapshot,
             interpolated: true,
         }
     }
 
     pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
-        self.tab_snapshot.buffer_snapshot()
+        self.char_snapshot.buffer_snapshot()
     }
 
-    fn interpolate(&mut self, new_tab_snapshot: TabSnapshot, tab_edits: &[TabEdit]) -> Patch<u32> {
+    fn interpolate(
+        &mut self,
+        new_char_snapshot: CharSnapshot,
+        tab_edits: &[TabEdit],
+    ) -> Patch<u32> {
         let mut new_transforms;
         if tab_edits.is_empty() {
             new_transforms = self.transforms.clone();
         } else {
-            let mut old_cursor = self.transforms.cursor::<TabPoint>(&());
+            let mut old_cursor = self.transforms.cursor::<CharPoint>(&());
 
             let mut tab_edits_iter = tab_edits.iter().peekable();
             new_transforms =
                 old_cursor.slice(&tab_edits_iter.peek().unwrap().old.start, Bias::Right, &());
 
             while let Some(edit) = tab_edits_iter.next() {
-                if edit.new.start > TabPoint::from(new_transforms.summary().input.lines) {
-                    let summary = new_tab_snapshot.text_summary_for_range(
-                        TabPoint::from(new_transforms.summary().input.lines)..edit.new.start,
+                if edit.new.start > CharPoint::from(new_transforms.summary().input.lines) {
+                    let summary = new_char_snapshot.text_summary_for_range(
+                        CharPoint::from(new_transforms.summary().input.lines)..edit.new.start,
                     );
                     new_transforms.push_or_extend(Transform::isomorphic(summary));
                 }
 
                 if !edit.new.is_empty() {
                     new_transforms.push_or_extend(Transform::isomorphic(
-                        new_tab_snapshot.text_summary_for_range(edit.new.clone()),
+                        new_char_snapshot.text_summary_for_range(edit.new.clone()),
                     ));
                 }
 
@@ -349,7 +353,7 @@ impl WrapSnapshot {
                     if next_edit.old.start > old_cursor.end(&()) {
                         if old_cursor.end(&()) > edit.old.end {
                             let summary = self
-                                .tab_snapshot
+                                .char_snapshot
                                 .text_summary_for_range(edit.old.end..old_cursor.end(&()));
                             new_transforms.push_or_extend(Transform::isomorphic(summary));
                         }
@@ -363,7 +367,7 @@ impl WrapSnapshot {
                 } else {
                     if old_cursor.end(&()) > edit.old.end {
                         let summary = self
-                            .tab_snapshot
+                            .char_snapshot
                             .text_summary_for_range(edit.old.end..old_cursor.end(&()));
                         new_transforms.push_or_extend(Transform::isomorphic(summary));
                     }
@@ -376,7 +380,7 @@ impl WrapSnapshot {
         let old_snapshot = mem::replace(
             self,
             WrapSnapshot {
-                tab_snapshot: new_tab_snapshot,
+                char_snapshot: new_char_snapshot,
                 transforms: new_transforms,
                 interpolated: true,
             },
@@ -387,7 +391,7 @@ impl WrapSnapshot {
 
     async fn update(
         &mut self,
-        new_tab_snapshot: TabSnapshot,
+        new_char_snapshot: CharSnapshot,
         tab_edits: &[TabEdit],
         wrap_width: Pixels,
         line_wrapper: &mut LineWrapper,
@@ -424,27 +428,27 @@ impl WrapSnapshot {
             new_transforms = self.transforms.clone();
         } else {
             let mut row_edits = row_edits.into_iter().peekable();
-            let mut old_cursor = self.transforms.cursor::<TabPoint>(&());
+            let mut old_cursor = self.transforms.cursor::<CharPoint>(&());
 
             new_transforms = old_cursor.slice(
-                &TabPoint::new(row_edits.peek().unwrap().old_rows.start, 0),
+                &CharPoint::new(row_edits.peek().unwrap().old_rows.start, 0),
                 Bias::Right,
                 &(),
             );
 
             while let Some(edit) = row_edits.next() {
                 if edit.new_rows.start > new_transforms.summary().input.lines.row {
-                    let summary = new_tab_snapshot.text_summary_for_range(
-                        TabPoint(new_transforms.summary().input.lines)
-                            ..TabPoint::new(edit.new_rows.start, 0),
+                    let summary = new_char_snapshot.text_summary_for_range(
+                        CharPoint(new_transforms.summary().input.lines)
+                            ..CharPoint::new(edit.new_rows.start, 0),
                     );
                     new_transforms.push_or_extend(Transform::isomorphic(summary));
                 }
 
                 let mut line = String::new();
                 let mut remaining = None;
-                let mut chunks = new_tab_snapshot.chunks(
-                    TabPoint::new(edit.new_rows.start, 0)..new_tab_snapshot.max_point(),
+                let mut chunks = new_char_snapshot.chunks(
+                    CharPoint::new(edit.new_rows.start, 0)..new_char_snapshot.max_point(),
                     false,
                     Highlights::default(),
                 );
@@ -491,19 +495,19 @@ impl WrapSnapshot {
                 }
                 new_transforms.extend(edit_transforms, &());
 
-                old_cursor.seek_forward(&TabPoint::new(edit.old_rows.end, 0), Bias::Right, &());
+                old_cursor.seek_forward(&CharPoint::new(edit.old_rows.end, 0), Bias::Right, &());
                 if let Some(next_edit) = row_edits.peek() {
                     if next_edit.old_rows.start > old_cursor.end(&()).row() {
-                        if old_cursor.end(&()) > TabPoint::new(edit.old_rows.end, 0) {
-                            let summary = self.tab_snapshot.text_summary_for_range(
-                                TabPoint::new(edit.old_rows.end, 0)..old_cursor.end(&()),
+                        if old_cursor.end(&()) > CharPoint::new(edit.old_rows.end, 0) {
+                            let summary = self.char_snapshot.text_summary_for_range(
+                                CharPoint::new(edit.old_rows.end, 0)..old_cursor.end(&()),
                             );
                             new_transforms.push_or_extend(Transform::isomorphic(summary));
                         }
                         old_cursor.next(&());
                         new_transforms.append(
                             old_cursor.slice(
-                                &TabPoint::new(next_edit.old_rows.start, 0),
+                                &CharPoint::new(next_edit.old_rows.start, 0),
                                 Bias::Right,
                                 &(),
                             ),
@@ -511,9 +515,9 @@ impl WrapSnapshot {
                         );
                     }
                 } else {
-                    if old_cursor.end(&()) > TabPoint::new(edit.old_rows.end, 0) {
-                        let summary = self.tab_snapshot.text_summary_for_range(
-                            TabPoint::new(edit.old_rows.end, 0)..old_cursor.end(&()),
+                    if old_cursor.end(&()) > CharPoint::new(edit.old_rows.end, 0) {
+                        let summary = self.char_snapshot.text_summary_for_range(
+                            CharPoint::new(edit.old_rows.end, 0)..old_cursor.end(&()),
                         );
                         new_transforms.push_or_extend(Transform::isomorphic(summary));
                     }
@@ -526,7 +530,7 @@ impl WrapSnapshot {
         let old_snapshot = mem::replace(
             self,
             WrapSnapshot {
-                tab_snapshot: new_tab_snapshot,
+                char_snapshot: new_char_snapshot,
                 transforms: new_transforms,
                 interpolated: false,
             },
@@ -579,17 +583,17 @@ impl WrapSnapshot {
     ) -> WrapChunks<'a> {
         let output_start = WrapPoint::new(rows.start, 0);
         let output_end = WrapPoint::new(rows.end, 0);
-        let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
+        let mut transforms = self.transforms.cursor::<(WrapPoint, CharPoint)>(&());
         transforms.seek(&output_start, Bias::Right, &());
-        let mut input_start = TabPoint(transforms.start().1 .0);
+        let mut input_start = CharPoint(transforms.start().1 .0);
         if transforms.item().map_or(false, |t| t.is_isomorphic()) {
             input_start.0 += output_start.0 - transforms.start().0 .0;
         }
         let input_end = self
-            .to_tab_point(output_end)
-            .min(self.tab_snapshot.max_point());
+            .to_char_point(output_end)
+            .min(self.char_snapshot.max_point());
         WrapChunks {
-            input_chunks: self.tab_snapshot.chunks(
+            input_chunks: self.char_snapshot.chunks(
                 input_start..input_end,
                 language_aware,
                 highlights,
@@ -606,7 +610,7 @@ impl WrapSnapshot {
     }
 
     pub fn line_len(&self, row: u32) -> u32 {
-        let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
+        let mut cursor = self.transforms.cursor::<(WrapPoint, CharPoint)>(&());
         cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Left, &());
         if cursor
             .item()
@@ -614,7 +618,7 @@ impl WrapSnapshot {
         {
             let overshoot = row - cursor.start().0.row();
             let tab_row = cursor.start().1.row() + overshoot;
-            let tab_line_len = self.tab_snapshot.line_len(tab_row);
+            let tab_line_len = self.char_snapshot.line_len(tab_row);
             if overshoot == 0 {
                 cursor.start().0.column() + (tab_line_len - cursor.start().1.column())
             } else {
@@ -642,14 +646,14 @@ impl WrapSnapshot {
     }
 
     pub fn buffer_rows(&self, start_row: u32) -> WrapBufferRows {
-        let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
+        let mut transforms = self.transforms.cursor::<(WrapPoint, CharPoint)>(&());
         transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left, &());
         let mut input_row = transforms.start().1.row();
         if transforms.item().map_or(false, |t| t.is_isomorphic()) {
             input_row += start_row - transforms.start().0.row();
         }
         let soft_wrapped = transforms.item().map_or(false, |t| !t.is_isomorphic());
-        let mut input_buffer_rows = self.tab_snapshot.buffer_rows(input_row);
+        let mut input_buffer_rows = self.char_snapshot.buffer_rows(input_row);
         let input_buffer_row = input_buffer_rows.next().unwrap();
         WrapBufferRows {
             transforms,
@@ -661,26 +665,26 @@ impl WrapSnapshot {
         }
     }
 
-    pub fn to_tab_point(&self, point: WrapPoint) -> TabPoint {
-        let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
+    pub fn to_char_point(&self, point: WrapPoint) -> CharPoint {
+        let mut cursor = self.transforms.cursor::<(WrapPoint, CharPoint)>(&());
         cursor.seek(&point, Bias::Right, &());
-        let mut tab_point = cursor.start().1 .0;
+        let mut char_point = cursor.start().1 .0;
         if cursor.item().map_or(false, |t| t.is_isomorphic()) {
-            tab_point += point.0 - cursor.start().0 .0;
+            char_point += point.0 - cursor.start().0 .0;
         }
-        TabPoint(tab_point)
+        CharPoint(char_point)
     }
 
     pub fn to_point(&self, point: WrapPoint, bias: Bias) -> Point {
-        self.tab_snapshot.to_point(self.to_tab_point(point), bias)
+        self.char_snapshot.to_point(self.to_char_point(point), bias)
     }
 
     pub fn make_wrap_point(&self, point: Point, bias: Bias) -> WrapPoint {
-        self.tab_point_to_wrap_point(self.tab_snapshot.make_tab_point(point, bias))
+        self.char_point_to_wrap_point(self.char_snapshot.make_char_point(point, bias))
     }
 
-    pub fn tab_point_to_wrap_point(&self, point: TabPoint) -> WrapPoint {
-        let mut cursor = self.transforms.cursor::<(TabPoint, WrapPoint)>(&());
+    pub fn char_point_to_wrap_point(&self, point: CharPoint) -> WrapPoint {
+        let mut cursor = self.transforms.cursor::<(CharPoint, WrapPoint)>(&());
         cursor.seek(&point, Bias::Right, &());
         WrapPoint(cursor.start().1 .0 + (point.0 - cursor.start().0 .0))
     }
@@ -695,7 +699,10 @@ impl WrapSnapshot {
             }
         }
 
-        self.tab_point_to_wrap_point(self.tab_snapshot.clip_point(self.to_tab_point(point), bias))
+        self.char_point_to_wrap_point(
+            self.char_snapshot
+                .clip_point(self.to_char_point(point), bias),
+        )
     }
 
     pub fn prev_row_boundary(&self, mut point: WrapPoint) -> u32 {
@@ -705,7 +712,7 @@ impl WrapSnapshot {
 
         *point.column_mut() = 0;
 
-        let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
+        let mut cursor = self.transforms.cursor::<(WrapPoint, CharPoint)>(&());
         cursor.seek(&point, Bias::Right, &());
         if cursor.item().is_none() {
             cursor.prev(&());
@@ -725,7 +732,7 @@ impl WrapSnapshot {
     pub fn next_row_boundary(&self, mut point: WrapPoint) -> Option<u32> {
         point.0 += Point::new(1, 0);
 
-        let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
+        let mut cursor = self.transforms.cursor::<(WrapPoint, CharPoint)>(&());
         cursor.seek(&point, Bias::Right, &());
         while let Some(transform) = cursor.item() {
             if transform.is_isomorphic() && cursor.start().1.column() == 0 {
@@ -742,8 +749,8 @@ impl WrapSnapshot {
         #[cfg(test)]
         {
             assert_eq!(
-                TabPoint::from(self.transforms.summary().input.lines),
-                self.tab_snapshot.max_point()
+                CharPoint::from(self.transforms.summary().input.lines),
+                self.char_snapshot.max_point()
             );
 
             {
@@ -756,18 +763,18 @@ impl WrapSnapshot {
             }
 
             let text = language::Rope::from(self.text().as_str());
-            let mut input_buffer_rows = self.tab_snapshot.buffer_rows(0);
+            let mut input_buffer_rows = self.char_snapshot.buffer_rows(0);
             let mut expected_buffer_rows = Vec::new();
             let mut prev_tab_row = 0;
             for display_row in 0..=self.max_point().row() {
-                let tab_point = self.to_tab_point(WrapPoint::new(display_row, 0));
-                if tab_point.row() == prev_tab_row && display_row != 0 {
+                let char_point = self.to_char_point(WrapPoint::new(display_row, 0));
+                if char_point.row() == prev_tab_row && display_row != 0 {
                     expected_buffer_rows.push(None);
                 } else {
                     expected_buffer_rows.push(input_buffer_rows.next().unwrap());
                 }
 
-                prev_tab_row = tab_point.row();
+                prev_tab_row = char_point.row();
                 assert_eq!(self.line_len(display_row), text.line_len(display_row));
             }
 
@@ -831,13 +838,11 @@ impl<'a> Iterator for WrapChunks<'a> {
             } else {
                 *self.output_position.column_mut() += char_len as u32;
             }
-
             if self.output_position >= transform_end {
                 self.transforms.next(&());
                 break;
             }
         }
-
         let (prefix, suffix) = self.input_chunk.text.split_at(input_len);
         self.input_chunk.text = suffix;
         Some(Chunk {
@@ -992,7 +997,7 @@ impl sum_tree::Summary for TransformSummary {
     }
 }
 
-impl<'a> sum_tree::Dimension<'a, TransformSummary> for TabPoint {
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for CharPoint {
     fn zero(_cx: &()) -> Self {
         Default::default()
     }
@@ -1002,7 +1007,7 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for TabPoint {
     }
 }
 
-impl<'a> sum_tree::SeekTarget<'a, TransformSummary, TransformSummary> for TabPoint {
+impl<'a> sum_tree::SeekTarget<'a, TransformSummary, TransformSummary> for CharPoint {
     fn cmp(&self, cursor_location: &TransformSummary, _: &()) -> std::cmp::Ordering {
         Ord::cmp(&self.0, &cursor_location.input.lines)
     }
@@ -1050,7 +1055,7 @@ fn consolidate_wrap_edits(edits: Vec<WrapEdit>) -> Vec<WrapEdit> {
 mod tests {
     use super::*;
     use crate::{
-        display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap},
+        display_map::{char_map::CharMap, fold_map::FoldMap, inlay_map::InlayMap},
         MultiBuffer,
     };
     use gpui::{font, px, test::observe};
@@ -1102,9 +1107,9 @@ mod tests {
         log::info!("InlayMap text: {:?}", inlay_snapshot.text());
         let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone());
         log::info!("FoldMap text: {:?}", fold_snapshot.text());
-        let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
-        let tabs_snapshot = tab_map.set_max_expansion_column(32);
-        log::info!("TabMap text: {:?}", tabs_snapshot.text());
+        let (mut char_map, _) = CharMap::new(fold_snapshot.clone(), tab_size);
+        let tabs_snapshot = char_map.set_max_expansion_column(32);
+        log::info!("CharMap text: {:?}", tabs_snapshot.text());
 
         let mut line_wrapper = text_system.line_wrapper(font.clone(), font_size);
         let unwrapped_text = tabs_snapshot.text();
@@ -1150,7 +1155,7 @@ mod tests {
                 20..=39 => {
                     for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
                         let (tabs_snapshot, tab_edits) =
-                            tab_map.sync(fold_snapshot, fold_edits, tab_size);
+                            char_map.sync(fold_snapshot, fold_edits, tab_size);
                         let (mut snapshot, wrap_edits) =
                             wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
                         snapshot.check_invariants();
@@ -1163,7 +1168,7 @@ mod tests {
                         inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
                     let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
                     let (tabs_snapshot, tab_edits) =
-                        tab_map.sync(fold_snapshot, fold_edits, tab_size);
+                        char_map.sync(fold_snapshot, fold_edits, tab_size);
                     let (mut snapshot, wrap_edits) =
                         wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
                     snapshot.check_invariants();
@@ -1187,8 +1192,8 @@ mod tests {
             log::info!("InlayMap text: {:?}", inlay_snapshot.text());
             let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
             log::info!("FoldMap text: {:?}", fold_snapshot.text());
-            let (tabs_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
-            log::info!("TabMap text: {:?}", tabs_snapshot.text());
+            let (tabs_snapshot, tab_edits) = char_map.sync(fold_snapshot, fold_edits, tab_size);
+            log::info!("CharMap text: {:?}", tabs_snapshot.text());
 
             let unwrapped_text = tabs_snapshot.text();
             let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
@@ -1234,7 +1239,7 @@ mod tests {
 
                 if tab_size.get() == 1
                     || !wrapped_snapshot
-                        .tab_snapshot
+                        .char_snapshot
                         .fold_snapshot
                         .text()
                         .contains('\t')

crates/editor/src/element.rs 🔗

@@ -68,6 +68,7 @@ use sum_tree::Bias;
 use theme::{ActiveTheme, Appearance, PlayerColor};
 use ui::prelude::*;
 use ui::{h_flex, ButtonLike, ButtonStyle, ContextMenu, Tooltip};
+use unicode_segmentation::UnicodeSegmentation;
 use util::RangeExt;
 use util::ResultExt;
 use workspace::{item::Item, Workspace};
@@ -1025,23 +1026,21 @@ impl EditorElement {
                     }
                     let block_text = if let CursorShape::Block = selection.cursor_shape {
                         snapshot
-                            .display_chars_at(cursor_position)
-                            .next()
+                            .grapheme_at(cursor_position)
                             .or_else(|| {
                                 if cursor_column == 0 {
-                                    snapshot
-                                        .placeholder_text()
-                                        .and_then(|s| s.chars().next())
-                                        .map(|c| (c, cursor_position))
+                                    snapshot.placeholder_text().and_then(|s| {
+                                        s.graphemes(true).next().map(|s| s.to_owned())
+                                    })
                                 } else {
                                     None
                                 }
                             })
-                            .and_then(|(character, _)| {
-                                let text = if character == '\n' {
+                            .and_then(|grapheme| {
+                                let text = if grapheme == "\n" {
                                     SharedString::from(" ")
                                 } else {
-                                    SharedString::from(character.to_string())
+                                    SharedString::from(grapheme)
                                 };
                                 let len = text.len();
 

crates/editor/src/hover_popover.rs 🔗

@@ -1,6 +1,7 @@
 use crate::{
     display_map::{InlayOffset, ToDisplayPoint},
     hover_links::{InlayHighlight, RangeInEditor},
+    is_invisible,
     scroll::ScrollAmount,
     Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, EditorSnapshot,
     Hover, RangeToAnchorExt,
@@ -11,7 +12,7 @@ use gpui::{
     StyleRefinement, Styled, Task, TextStyleRefinement, View, ViewContext,
 };
 use itertools::Itertools;
-use language::{DiagnosticEntry, Language, LanguageRegistry};
+use language::{Diagnostic, DiagnosticEntry, Language, LanguageRegistry};
 use lsp::DiagnosticSeverity;
 use markdown::{Markdown, MarkdownStyle};
 use multi_buffer::ToOffset;
@@ -199,7 +200,6 @@ fn show_hover(
     if editor.pending_rename.is_some() {
         return None;
     }
-
     let snapshot = editor.snapshot(cx);
 
     let (buffer, buffer_position) = editor
@@ -259,7 +259,7 @@ fn show_hover(
             }
 
             // If there's a diagnostic, assign it on the hover state and notify
-            let local_diagnostic = snapshot
+            let mut local_diagnostic = snapshot
                 .buffer_snapshot
                 .diagnostics_in_range::<_, usize>(anchor..anchor, false)
                 // Find the entry with the most specific range
@@ -281,6 +281,42 @@ fn show_hover(
                     })
             });
 
+            if let Some(invisible) = snapshot
+                .buffer_snapshot
+                .chars_at(anchor)
+                .next()
+                .filter(|&c| is_invisible(c))
+            {
+                let after = snapshot.buffer_snapshot.anchor_after(
+                    anchor.to_offset(&snapshot.buffer_snapshot) + invisible.len_utf8(),
+                );
+                local_diagnostic = Some(DiagnosticEntry {
+                    diagnostic: Diagnostic {
+                        severity: DiagnosticSeverity::HINT,
+                        message: format!("Unicode character U+{:02X}", invisible as u32),
+                        ..Default::default()
+                    },
+                    range: anchor..after,
+                })
+            } else if let Some(invisible) = snapshot
+                .buffer_snapshot
+                .reversed_chars_at(anchor)
+                .next()
+                .filter(|&c| is_invisible(c))
+            {
+                let before = snapshot.buffer_snapshot.anchor_before(
+                    anchor.to_offset(&snapshot.buffer_snapshot) - invisible.len_utf8(),
+                );
+                local_diagnostic = Some(DiagnosticEntry {
+                    diagnostic: Diagnostic {
+                        severity: DiagnosticSeverity::HINT,
+                        message: format!("Unicode character U+{:02X}", invisible as u32),
+                        ..Default::default()
+                    },
+                    range: before..anchor,
+                })
+            }
+
             let diagnostic_popover = if let Some(local_diagnostic) = local_diagnostic {
                 let text = match local_diagnostic.diagnostic.source {
                     Some(ref source) => {
@@ -288,7 +324,6 @@ fn show_hover(
                     }
                     None => local_diagnostic.diagnostic.message.clone(),
                 };
-
                 let mut border_color: Option<Hsla> = None;
                 let mut background_color: Option<Hsla> = None;
 
@@ -344,7 +379,6 @@ fn show_hover(
                         Markdown::new_text(text, markdown_style.clone(), None, cx, None)
                     })
                     .ok();
-
                 Some(DiagnosticPopover {
                     local_diagnostic,
                     primary_diagnostic,
@@ -432,7 +466,6 @@ fn show_hover(
                 cx.notify();
                 cx.refresh();
             })?;
-
             anyhow::Ok(())
         }
         .log_err()

crates/gpui/src/text_system/line.rs 🔗

@@ -1,6 +1,7 @@
 use crate::{
-    black, fill, point, px, size, Bounds, Hsla, LineLayout, Pixels, Point, Result, SharedString,
-    StrikethroughStyle, UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout,
+    black, fill, point, px, size, Bounds, Half, Hsla, LineLayout, Pixels, Point, Result,
+    SharedString, StrikethroughStyle, UnderlineStyle, WindowContext, WrapBoundary,
+    WrappedLineLayout,
 };
 use derive_more::{Deref, DerefMut};
 use smallvec::SmallVec;
@@ -129,8 +130,9 @@ fn paint_line(
         let text_system = cx.text_system().clone();
         let mut glyph_origin = origin;
         let mut prev_glyph_position = Point::default();
+        let mut max_glyph_size = size(px(0.), px(0.));
         for (run_ix, run) in layout.runs.iter().enumerate() {
-            let max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size;
+            max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size;
 
             for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
                 glyph_origin.x += glyph.position.x - prev_glyph_position.x;
@@ -139,6 +141,9 @@ fn paint_line(
                     wraps.next();
                     if let Some((background_origin, background_color)) = current_background.as_mut()
                     {
+                        if glyph_origin.x == background_origin.x {
+                            background_origin.x -= max_glyph_size.width.half()
+                        }
                         cx.paint_quad(fill(
                             Bounds {
                                 origin: *background_origin,
@@ -150,6 +155,9 @@ fn paint_line(
                         background_origin.y += line_height;
                     }
                     if let Some((underline_origin, underline_style)) = current_underline.as_mut() {
+                        if glyph_origin.x == underline_origin.x {
+                            underline_origin.x -= max_glyph_size.width.half();
+                        };
                         cx.paint_underline(
                             *underline_origin,
                             glyph_origin.x - underline_origin.x,
@@ -161,6 +169,9 @@ fn paint_line(
                     if let Some((strikethrough_origin, strikethrough_style)) =
                         current_strikethrough.as_mut()
                     {
+                        if glyph_origin.x == strikethrough_origin.x {
+                            strikethrough_origin.x -= max_glyph_size.width.half();
+                        };
                         cx.paint_strikethrough(
                             *strikethrough_origin,
                             glyph_origin.x - strikethrough_origin.x,
@@ -179,7 +190,18 @@ fn paint_line(
                 let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
                 let mut finished_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
                 if glyph.index >= run_end {
-                    if let Some(style_run) = decoration_runs.next() {
+                    let mut style_run = decoration_runs.next();
+
+                    // ignore style runs that apply to a partial glyph
+                    while let Some(run) = style_run {
+                        if glyph.index < run_end + (run.len as usize) {
+                            break;
+                        }
+                        run_end += run.len as usize;
+                        style_run = decoration_runs.next();
+                    }
+
+                    if let Some(style_run) = style_run {
                         if let Some((_, background_color)) = &mut current_background {
                             if style_run.background_color.as_ref() != Some(background_color) {
                                 finished_background = current_background.take();
@@ -240,10 +262,14 @@ fn paint_line(
                 }
 
                 if let Some((background_origin, background_color)) = finished_background {
+                    let mut width = glyph_origin.x - background_origin.x;
+                    if width == px(0.) {
+                        width = px(5.)
+                    };
                     cx.paint_quad(fill(
                         Bounds {
                             origin: background_origin,
-                            size: size(glyph_origin.x - background_origin.x, line_height),
+                            size: size(width, line_height),
                         },
                         background_color,
                     ));
@@ -299,7 +325,10 @@ fn paint_line(
             last_line_end_x -= glyph.position.x;
         }
 
-        if let Some((background_origin, background_color)) = current_background.take() {
+        if let Some((mut background_origin, background_color)) = current_background.take() {
+            if last_line_end_x == background_origin.x {
+                background_origin.x -= max_glyph_size.width.half()
+            };
             cx.paint_quad(fill(
                 Bounds {
                     origin: background_origin,
@@ -309,7 +338,10 @@ fn paint_line(
             ));
         }
 
-        if let Some((underline_start, underline_style)) = current_underline.take() {
+        if let Some((mut underline_start, underline_style)) = current_underline.take() {
+            if last_line_end_x == underline_start.x {
+                underline_start.x -= max_glyph_size.width.half()
+            };
             cx.paint_underline(
                 underline_start,
                 last_line_end_x - underline_start.x,
@@ -317,7 +349,10 @@ fn paint_line(
             );
         }
 
-        if let Some((strikethrough_start, strikethrough_style)) = current_strikethrough.take() {
+        if let Some((mut strikethrough_start, strikethrough_style)) = current_strikethrough.take() {
+            if last_line_end_x == strikethrough_start.x {
+                strikethrough_start.x -= max_glyph_size.width.half()
+            };
             cx.paint_strikethrough(
                 strikethrough_start,
                 last_line_end_x - strikethrough_start.x,

crates/language/src/buffer.rs 🔗

@@ -501,6 +501,8 @@ pub struct Chunk<'a> {
     pub is_unnecessary: bool,
     /// Whether this chunk of text was originally a tab character.
     pub is_tab: bool,
+    /// Whether this chunk of text is an invisible character.
+    pub is_invisible: bool,
     /// An optional recipe for how the chunk should be presented.
     pub renderer: Option<ChunkRenderer>,
 }
@@ -4211,7 +4213,6 @@ impl<'a> Iterator for BufferChunks<'a> {
             if self.range.start == self.chunks.offset() + chunk.len() {
                 self.chunks.next().unwrap();
             }
-
             Some(Chunk {
                 text: slice,
                 syntax_highlight_id: highlight_id,