From 6dcec47235fa85f0e416b9230e2fedc61de510ee Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 22 Oct 2024 13:23:13 -0600 Subject: [PATCH] Show invisibles in editor (#19298) Release Notes: - Added highlighting for "invisible" unicode characters Closes #16310 --------- Co-authored-by: dovakin0007 Co-authored-by: dovakin0007 <73059450+dovakin0007@users.noreply.github.com> --- 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 ++--- .../display_map/{tab_map.rs => 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(-) rename crates/editor/src/display_map/{tab_map.rs => char_map.rs} (82%) create mode 100644 crates/editor/src/display_map/invisibles.rs diff --git a/Cargo.lock b/Cargo.lock index b02184fdd7f98c..6a94aa1a4cd532 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3722,6 +3722,7 @@ dependencies = [ "tree-sitter-rust", "tree-sitter-typescript", "ui", + "unicode-segmentation", "unindent", "url", "util", diff --git a/Cargo.toml b/Cargo.toml index 9ffe48b4a1aaa2..633e7a1df07056 100644 --- a/Cargo.toml +++ b/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" diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index cfd9284f807650..f6a0058c7b3a9e 100644 --- a/crates/editor/Cargo.toml +++ b/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 diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 699258f9a58495..528385ebab22ff 100644 --- a/crates/editor/src/display_map.rs +++ b/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, /// 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 + '_ { + pub fn grapheme_at(&self, mut point: DisplayPoint) -> Option { 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::() + .graphemes(true) + .next() + .map(|s| s.to_owned()) } pub fn buffer_chars_at(&self, mut offset: usize) -> impl Iterator + '_ { @@ -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()); diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index f4ee57408b1ca8..6b0d45fc765f1d 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/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); diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/char_map.rs similarity index 82% rename from crates/editor/src/display_map/tab_map.rs rename to crates/editor/src/display_map/char_map.rs index 39742552262bde..443f8199a6157c 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/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, tab_size: NonZeroU32, - ) -> (TabSnapshot, Vec) { + ) -> (CharSnapshot, Vec) { 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) -> TextSummary { + pub fn text_summary_for_range(&self, range: Range) -> 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, + range: Range, 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 for TabPoint { +impl From for CharPoint { fn from(point: Point) -> Self { Self(point) } } -pub type TabEdit = text::Edit; +pub type TabEdit = text::Edit; #[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); } diff --git a/crates/editor/src/display_map/invisibles.rs b/crates/editor/src/display_map/invisibles.rs new file mode 100644 index 00000000000000..19a5bebbeb1ef9 --- /dev/null +++ b/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> = 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 +} diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index dc4d93058cdf7f..f418f45fec7fab 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/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; /// Handles soft wrapping of text. @@ -20,7 +20,7 @@ pub type WrapEdit = text::Edit; /// See the [`display_map` module documentation](crate::display_map) for more information. pub struct WrapMap { snapshot: WrapSnapshot, - pending_edits: VecDeque<(TabSnapshot, Vec)>, + pending_edits: VecDeque<(CharSnapshot, Vec)>, interpolated_edits: Patch, edits_since_sync: Patch, wrap_width: Option, @@ -30,7 +30,7 @@ pub struct WrapMap { #[derive(Clone)] pub struct WrapSnapshot { - tab_snapshot: TabSnapshot, + char_snapshot: CharSnapshot, transforms: SumTree, 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, @@ -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, cx: &mut ModelContext, ) -> (WrapSnapshot, Patch) { 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) { 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 { + fn interpolate( + &mut self, + new_char_snapshot: CharSnapshot, + tab_edits: &[TabEdit], + ) -> Patch { let mut new_transforms; if tab_edits.is_empty() { new_transforms = self.transforms.clone(); } else { - let mut old_cursor = self.transforms.cursor::(&()); + let mut old_cursor = self.transforms.cursor::(&()); 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::(&()); + let mut old_cursor = self.transforms.cursor::(&()); 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 { 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) -> Vec { 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') diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 77b78d059c955b..5a356965a491e9 100644 --- a/crates/editor/src/element.rs +++ b/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(); diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 9200dd7b8c697c..688ebf57bb30bc 100644 --- a/crates/editor/src/hover_popover.rs +++ b/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 = None; let mut background_color: Option = 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() diff --git a/crates/gpui/src/text_system/line.rs b/crates/gpui/src/text_system/line.rs index 240654e57e1488..ff5d91f0224713 100644 --- a/crates/gpui/src/text_system/line.rs +++ b/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, UnderlineStyle)> = None; let mut finished_strikethrough: Option<(Point, 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, diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 26fb620ac1498f..06492a0c58b423 100644 --- a/crates/language/src/buffer.rs +++ b/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, } @@ -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,