Cargo.lock 🔗
@@ -3722,6 +3722,7 @@ dependencies = [
"tree-sitter-rust",
"tree-sitter-typescript",
"ui",
+ "unicode-segmentation",
"unindent",
"url",
"util",
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>
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(-)
@@ -3722,6 +3722,7 @@ dependencies = [
"tree-sitter-rust",
"tree-sitter-typescript",
"ui",
+ "unicode-segmentation",
"unindent",
"url",
"util",
@@ -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"
@@ -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
@@ -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());
@@ -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);
@@ -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);
}
@@ -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
+}
@@ -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')
@@ -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();
@@ -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()
@@ -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,
@@ -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,