Merge branch 'main' into settings-file

Max Brunsfeld created

Change summary

crates/editor/src/display_map.rs           |  39 +
crates/editor/src/display_map/block_map.rs |  31 +
crates/editor/src/display_map/fold_map.rs  | 214 +++++++++-
crates/editor/src/display_map/tab_map.rs   |  35 +
crates/editor/src/display_map/wrap_map.rs  |  21 
crates/editor/src/editor.rs                | 287 +++++++++-----
crates/editor/src/element.rs               | 206 +++++++--
crates/editor/src/multi_buffer.rs          | 129 +++--
crates/editor/src/multi_buffer/anchor.rs   |  49 -
crates/go_to_line/src/go_to_line.rs        |   4 
crates/gpui/src/app.rs                     | 459 +++++++++++++++++++++--
crates/gpui/src/color.rs                   |  26 +
crates/gpui/src/elements/text.rs           |  43 +
crates/gpui/src/fonts.rs                   |  41 ++
crates/gpui/src/text_layout.rs             |  20 
crates/language/src/buffer.rs              |  10 
crates/language/src/highlight_map.rs       |   8 
crates/language/src/language.rs            |   2 
crates/language/src/proto.rs               |  44 --
crates/outline/src/outline.rs              |   4 
crates/search/src/buffer_search.rs         |  22 
crates/search/src/project_search.rs        |   4 
crates/sum_tree/src/tree_map.rs            |   4 
crates/text/src/locator.rs                 |  29 +
crates/theme/src/theme.rs                  |   1 
crates/workspace/src/workspace.rs          |  21 
crates/zed/assets/themes/_base.toml        |   1 
27 files changed, 1,305 insertions(+), 449 deletions(-)

Detailed changes

crates/editor/src/display_map.rs 🔗

@@ -7,10 +7,13 @@ use crate::{Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
 use block_map::{BlockMap, BlockPoint};
 use collections::{HashMap, HashSet};
 use fold_map::FoldMap;
-use gpui::{fonts::FontId, Entity, ModelContext, ModelHandle};
+use gpui::{
+    fonts::{FontId, HighlightStyle},
+    Entity, ModelContext, ModelHandle,
+};
 use language::{Point, Subscription as BufferSubscription};
-use std::ops::Range;
-use sum_tree::Bias;
+use std::{any::TypeId, ops::Range, sync::Arc};
+use sum_tree::{Bias, TreeMap};
 use tab_map::TabMap;
 use wrap_map::WrapMap;
 
@@ -23,6 +26,8 @@ pub trait ToDisplayPoint {
     fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint;
 }
 
+type TextHighlights = TreeMap<Option<TypeId>, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>;
+
 pub struct DisplayMap {
     buffer: ModelHandle<MultiBuffer>,
     buffer_subscription: BufferSubscription,
@@ -30,6 +35,7 @@ pub struct DisplayMap {
     tab_map: TabMap,
     wrap_map: ModelHandle<WrapMap>,
     block_map: BlockMap,
+    text_highlights: TextHighlights,
 }
 
 impl Entity for DisplayMap {
@@ -60,6 +66,7 @@ impl DisplayMap {
             tab_map,
             wrap_map,
             block_map,
+            text_highlights: Default::default(),
         }
     }
 
@@ -79,6 +86,7 @@ impl DisplayMap {
             tabs_snapshot,
             wraps_snapshot,
             blocks_snapshot,
+            text_highlights: self.text_highlights.clone(),
         }
     }
 
@@ -156,6 +164,23 @@ impl DisplayMap {
         block_map.remove(ids);
     }
 
+    pub fn highlight_text(
+        &mut self,
+        type_id: TypeId,
+        ranges: Vec<Range<Anchor>>,
+        style: HighlightStyle,
+    ) {
+        self.text_highlights
+            .insert(Some(type_id), Arc::new((style, ranges)));
+    }
+
+    pub fn clear_text_highlights(
+        &mut self,
+        type_id: TypeId,
+    ) -> Option<Arc<(HighlightStyle, Vec<Range<Anchor>>)>> {
+        self.text_highlights.remove(&Some(type_id))
+    }
+
     pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) {
         self.wrap_map
             .update(cx, |map, cx| map.set_font(font_id, font_size, cx));
@@ -178,6 +203,7 @@ pub struct DisplaySnapshot {
     tabs_snapshot: tab_map::TabSnapshot,
     wraps_snapshot: wrap_map::WrapSnapshot,
     blocks_snapshot: block_map::BlockSnapshot,
+    text_highlights: TextHighlights,
 }
 
 impl DisplaySnapshot {
@@ -252,7 +278,7 @@ impl DisplaySnapshot {
 
     pub fn text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
         self.blocks_snapshot
-            .chunks(display_row..self.max_point().row() + 1, false)
+            .chunks(display_row..self.max_point().row() + 1, false, None)
             .map(|h| h.text)
     }
 
@@ -261,7 +287,8 @@ impl DisplaySnapshot {
         display_rows: Range<u32>,
         language_aware: bool,
     ) -> DisplayChunks<'a> {
-        self.blocks_snapshot.chunks(display_rows, language_aware)
+        self.blocks_snapshot
+            .chunks(display_rows, language_aware, Some(&self.text_highlights))
     }
 
     pub fn chars_at<'a>(&'a self, point: DisplayPoint) -> impl Iterator<Item = char> + 'a {
@@ -1146,7 +1173,7 @@ mod tests {
         let mut chunks: Vec<(String, Option<Color>)> = Vec::new();
         for chunk in snapshot.chunks(rows, true) {
             let color = chunk
-                .highlight_id
+                .syntax_highlight_id
                 .and_then(|id| id.style(theme).map(|s| s.color));
             if let Some((last_chunk, last_color)) = chunks.last_mut() {
                 if color == *last_color {

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

@@ -1,4 +1,7 @@
-use super::wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot};
+use super::{
+    wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot},
+    TextHighlights,
+};
 use crate::{Anchor, ToPoint as _};
 use collections::{Bound, HashMap, HashSet};
 use gpui::{AppContext, ElementBox};
@@ -555,12 +558,17 @@ impl<'a> BlockMapWriter<'a> {
 impl BlockSnapshot {
     #[cfg(test)]
     pub fn text(&self) -> String {
-        self.chunks(0..self.transforms.summary().output_rows, false)
+        self.chunks(0..self.transforms.summary().output_rows, false, None)
             .map(|chunk| chunk.text)
             .collect()
     }
 
-    pub fn chunks<'a>(&'a self, rows: Range<u32>, language_aware: bool) -> BlockChunks<'a> {
+    pub fn chunks<'a>(
+        &'a self,
+        rows: Range<u32>,
+        language_aware: bool,
+        text_highlights: Option<&'a TextHighlights>,
+    ) -> BlockChunks<'a> {
         let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows);
         let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
         let input_end = {
@@ -588,9 +596,11 @@ impl BlockSnapshot {
             cursor.start().1 .0 + overshoot
         };
         BlockChunks {
-            input_chunks: self
-                .wrap_snapshot
-                .chunks(input_start..input_end, language_aware),
+            input_chunks: self.wrap_snapshot.chunks(
+                input_start..input_end,
+                language_aware,
+                text_highlights,
+            ),
             input_chunk: Default::default(),
             transforms: cursor,
             output_row: rows.start,
@@ -807,7 +817,8 @@ impl<'a> Iterator for BlockChunks<'a> {
 
             return Some(Chunk {
                 text: unsafe { std::str::from_utf8_unchecked(&NEWLINES[..line_count as usize]) },
-                highlight_id: None,
+                syntax_highlight_id: None,
+                highlight_style: None,
                 diagnostic: None,
             });
         }
@@ -1435,7 +1446,11 @@ mod tests {
             for start_row in 0..expected_row_count {
                 let expected_text = expected_lines[start_row..].join("\n");
                 let actual_text = blocks_snapshot
-                    .chunks(start_row as u32..blocks_snapshot.max_point().row + 1, false)
+                    .chunks(
+                        start_row as u32..blocks_snapshot.max_point().row + 1,
+                        false,
+                        None,
+                    )
                     .map(|chunk| chunk.text)
                     .collect::<String>();
                 assert_eq!(

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

@@ -1,14 +1,19 @@
+use super::TextHighlights;
 use crate::{
     multi_buffer::MultiBufferRows, Anchor, AnchorRangeExt, MultiBufferChunks, MultiBufferSnapshot,
     ToOffset,
 };
+use collections::BTreeMap;
+use gpui::fonts::HighlightStyle;
 use language::{Chunk, Edit, Point, PointUtf16, TextSummary};
 use parking_lot::Mutex;
 use std::{
+    any::TypeId,
     cmp::{self, Ordering},
-    iter,
+    iter::{self, Peekable},
     ops::{Range, Sub},
     sync::atomic::{AtomicUsize, Ordering::SeqCst},
+    vec,
 };
 use sum_tree::{Bias, Cursor, FilterCursor, SumTree};
 
@@ -71,6 +76,12 @@ impl FoldPoint {
     }
 }
 
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for FoldPoint {
+    fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+        self.0 += &summary.output.lines;
+    }
+}
+
 pub struct FoldMapWriter<'a>(&'a mut FoldMap);
 
 impl<'a> FoldMapWriter<'a> {
@@ -241,6 +252,14 @@ impl FoldMap {
                 self.buffer.lock().len(),
                 "transform tree does not match buffer's length"
             );
+
+            let mut folds = self.folds.iter().peekable();
+            while let Some(fold) = folds.next() {
+                if let Some(next_fold) = folds.peek() {
+                    let comparison = fold.0.cmp(&next_fold.0, &self.buffer.lock()).unwrap();
+                    assert!(comparison.is_le());
+                }
+            }
         }
     }
 
@@ -476,7 +495,7 @@ impl FoldSnapshot {
 
     #[cfg(test)]
     pub fn text(&self) -> String {
-        self.chunks(FoldOffset(0)..self.len(), false)
+        self.chunks(FoldOffset(0)..self.len(), false, None)
             .map(|c| c.text)
             .collect()
     }
@@ -634,20 +653,96 @@ impl FoldSnapshot {
 
     pub fn chars_at(&self, start: FoldPoint) -> impl '_ + Iterator<Item = char> {
         let start = start.to_offset(self);
-        self.chunks(start..self.len(), false)
+        self.chunks(start..self.len(), false, None)
             .flat_map(|chunk| chunk.text.chars())
     }
 
-    pub fn chunks<'a>(&'a self, range: Range<FoldOffset>, language_aware: bool) -> FoldChunks<'a> {
+    pub fn chunks<'a>(
+        &'a self,
+        range: Range<FoldOffset>,
+        language_aware: bool,
+        text_highlights: Option<&'a TextHighlights>,
+    ) -> FoldChunks<'a> {
+        let mut highlight_endpoints = Vec::new();
         let mut transform_cursor = self.transforms.cursor::<(FoldOffset, usize)>();
 
-        transform_cursor.seek(&range.end, Bias::Right, &());
-        let overshoot = range.end.0 - transform_cursor.start().0 .0;
-        let buffer_end = transform_cursor.start().1 + overshoot;
+        let buffer_end = {
+            transform_cursor.seek(&range.end, Bias::Right, &());
+            let overshoot = range.end.0 - transform_cursor.start().0 .0;
+            transform_cursor.start().1 + overshoot
+        };
+
+        let buffer_start = {
+            transform_cursor.seek(&range.start, Bias::Right, &());
+            let overshoot = range.start.0 - transform_cursor.start().0 .0;
+            transform_cursor.start().1 + overshoot
+        };
 
-        transform_cursor.seek(&range.start, Bias::Right, &());
-        let overshoot = range.start.0 - transform_cursor.start().0 .0;
-        let buffer_start = transform_cursor.start().1 + overshoot;
+        if let Some(text_highlights) = text_highlights {
+            if !text_highlights.is_empty() {
+                while transform_cursor.start().0 < range.end {
+                    if !transform_cursor.item().unwrap().is_fold() {
+                        let transform_start = self
+                            .buffer_snapshot
+                            .anchor_after(cmp::max(buffer_start, transform_cursor.start().1));
+
+                        let transform_end = {
+                            let overshoot = range.end.0 - transform_cursor.start().0 .0;
+                            self.buffer_snapshot.anchor_before(cmp::min(
+                                transform_cursor.end(&()).1,
+                                transform_cursor.start().1 + overshoot,
+                            ))
+                        };
+
+                        for (tag, highlights) in text_highlights.iter() {
+                            let style = highlights.0;
+                            let ranges = &highlights.1;
+
+                            let start_ix = match ranges.binary_search_by(|probe| {
+                                let cmp = probe
+                                    .end
+                                    .cmp(&transform_start, &self.buffer_snapshot())
+                                    .unwrap();
+                                if cmp.is_gt() {
+                                    Ordering::Greater
+                                } else {
+                                    Ordering::Less
+                                }
+                            }) {
+                                Ok(i) | Err(i) => i,
+                            };
+                            for range in &ranges[start_ix..] {
+                                if range
+                                    .start
+                                    .cmp(&transform_end, &self.buffer_snapshot)
+                                    .unwrap()
+                                    .is_ge()
+                                {
+                                    break;
+                                }
+
+                                highlight_endpoints.push(HighlightEndpoint {
+                                    offset: range.start.to_offset(&self.buffer_snapshot),
+                                    is_start: true,
+                                    tag: *tag,
+                                    style,
+                                });
+                                highlight_endpoints.push(HighlightEndpoint {
+                                    offset: range.end.to_offset(&self.buffer_snapshot),
+                                    is_start: false,
+                                    tag: *tag,
+                                    style,
+                                });
+                            }
+                        }
+                    }
+
+                    transform_cursor.next(&());
+                }
+                highlight_endpoints.sort();
+                transform_cursor.seek(&range.start, Bias::Right, &());
+            }
+        }
 
         FoldChunks {
             transform_cursor,
@@ -658,6 +753,8 @@ impl FoldSnapshot {
             buffer_offset: buffer_start,
             output_offset: range.start.0,
             max_output_offset: range.end.0,
+            highlight_endpoints: highlight_endpoints.into_iter().peekable(),
+            active_highlights: Default::default(),
         }
     }
 
@@ -946,6 +1043,8 @@ pub struct FoldChunks<'a> {
     buffer_offset: usize,
     output_offset: usize,
     max_output_offset: usize,
+    highlight_endpoints: Peekable<vec::IntoIter<HighlightEndpoint>>,
+    active_highlights: BTreeMap<Option<TypeId>, HighlightStyle>,
 }
 
 impl<'a> Iterator for FoldChunks<'a> {
@@ -978,11 +1077,27 @@ impl<'a> Iterator for FoldChunks<'a> {
             self.output_offset += output_text.len();
             return Some(Chunk {
                 text: output_text,
-                highlight_id: None,
+                syntax_highlight_id: None,
+                highlight_style: None,
                 diagnostic: None,
             });
         }
 
+        let mut next_highlight_endpoint = usize::MAX;
+        while let Some(endpoint) = self.highlight_endpoints.peek().copied() {
+            if endpoint.offset <= self.buffer_offset {
+                if endpoint.is_start {
+                    self.active_highlights.insert(endpoint.tag, endpoint.style);
+                } else {
+                    self.active_highlights.remove(&endpoint.tag);
+                }
+                self.highlight_endpoints.next();
+            } else {
+                next_highlight_endpoint = endpoint.offset;
+                break;
+            }
+        }
+
         // Retrieve a chunk from the current location in the buffer.
         if self.buffer_chunk.is_none() {
             let chunk_offset = self.buffer_chunks.offset();
@@ -990,20 +1105,31 @@ impl<'a> Iterator for FoldChunks<'a> {
         }
 
         // Otherwise, take a chunk from the buffer's text.
-        if let Some((chunk_offset, mut chunk)) = self.buffer_chunk {
-            let offset_in_chunk = self.buffer_offset - chunk_offset;
-            chunk.text = &chunk.text[offset_in_chunk..];
-
-            // Truncate the chunk so that it ends at the next fold.
-            let region_end = self.transform_cursor.end(&()).1 - self.buffer_offset;
-            if chunk.text.len() >= region_end {
-                chunk.text = &chunk.text[0..region_end];
+        if let Some((buffer_chunk_start, mut chunk)) = self.buffer_chunk {
+            let buffer_chunk_end = buffer_chunk_start + chunk.text.len();
+            let transform_end = self.transform_cursor.end(&()).1;
+            let chunk_end = buffer_chunk_end
+                .min(transform_end)
+                .min(next_highlight_endpoint);
+
+            chunk.text = &chunk.text
+                [self.buffer_offset - buffer_chunk_start..chunk_end - buffer_chunk_start];
+
+            if !self.active_highlights.is_empty() {
+                let mut highlight_style = HighlightStyle::default();
+                for active_highlight in self.active_highlights.values() {
+                    highlight_style.highlight(*active_highlight);
+                }
+                chunk.highlight_style = Some(highlight_style);
+            }
+
+            if chunk_end == transform_end {
                 self.transform_cursor.next(&());
-            } else {
+            } else if chunk_end == buffer_chunk_end {
                 self.buffer_chunk.take();
             }
 
-            self.buffer_offset += chunk.text.len();
+            self.buffer_offset = chunk_end;
             self.output_offset += chunk.text.len();
             return Some(chunk);
         }
@@ -1012,9 +1138,25 @@ impl<'a> Iterator for FoldChunks<'a> {
     }
 }
 
-impl<'a> sum_tree::Dimension<'a, TransformSummary> for FoldPoint {
-    fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
-        self.0 += &summary.output.lines;
+#[derive(Copy, Clone, Eq, PartialEq)]
+struct HighlightEndpoint {
+    offset: usize,
+    is_start: bool,
+    tag: Option<TypeId>,
+    style: HighlightStyle,
+}
+
+impl PartialOrd for HighlightEndpoint {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl Ord for HighlightEndpoint {
+    fn cmp(&self, other: &Self) -> Ordering {
+        self.offset
+            .cmp(&other.offset)
+            .then_with(|| self.is_start.cmp(&other.is_start))
     }
 }
 
@@ -1071,7 +1213,8 @@ mod tests {
     use super::*;
     use crate::{MultiBuffer, ToPoint};
     use rand::prelude::*;
-    use std::{env, mem};
+    use std::{cmp::Reverse, env, mem, sync::Arc};
+    use sum_tree::TreeMap;
     use text::RandomCharIter;
     use util::test::sample_text;
     use Bias::{Left, Right};
@@ -1276,6 +1419,25 @@ mod tests {
         let (mut initial_snapshot, _) = map.read(buffer_snapshot.clone(), vec![]);
         let mut snapshot_edits = Vec::new();
 
+        let mut highlights = TreeMap::default();
+        let highlight_count = rng.gen_range(0_usize..10);
+        let mut highlight_ranges = (0..highlight_count)
+            .map(|_| buffer_snapshot.random_byte_range(0, &mut rng))
+            .collect::<Vec<_>>();
+        highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end)));
+        log::info!("highlighting ranges {:?}", highlight_ranges);
+        let highlight_ranges = highlight_ranges
+            .into_iter()
+            .map(|range| {
+                buffer_snapshot.anchor_before(range.start)..buffer_snapshot.anchor_after(range.end)
+            })
+            .collect::<Vec<_>>();
+
+        highlights.insert(
+            Some(TypeId::of::<()>()),
+            Arc::new((HighlightStyle::default(), highlight_ranges)),
+        );
+
         for _ in 0..operations {
             log::info!("text: {:?}", buffer_snapshot.text());
             let mut buffer_edits = Vec::new();
@@ -1400,7 +1562,7 @@ mod tests {
                 let text = &expected_text[start.0..end.0];
                 assert_eq!(
                     snapshot
-                        .chunks(start..end, false)
+                        .chunks(start..end, false, Some(&highlights))
                         .map(|c| c.text)
                         .collect::<String>(),
                     text,

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

@@ -1,4 +1,7 @@
-use super::fold_map::{self, FoldEdit, FoldPoint, FoldSnapshot};
+use super::{
+    fold_map::{self, FoldEdit, FoldPoint, FoldSnapshot},
+    TextHighlights,
+};
 use crate::MultiBufferSnapshot;
 use language::{rope, Chunk};
 use parking_lot::Mutex;
@@ -32,9 +35,10 @@ impl TabMap {
         let mut tab_edits = Vec::with_capacity(fold_edits.len());
         for fold_edit in &mut fold_edits {
             let mut delta = 0;
-            for chunk in old_snapshot
-                .fold_snapshot
-                .chunks(fold_edit.old.end..max_offset, false)
+            for chunk in
+                old_snapshot
+                    .fold_snapshot
+                    .chunks(fold_edit.old.end..max_offset, false, None)
             {
                 let patterns: &[_] = &['\t', '\n'];
                 if let Some(ix) = chunk.text.find(patterns) {
@@ -109,7 +113,7 @@ impl TabSnapshot {
             self.max_point()
         };
         for c in self
-            .chunks(range.start..line_end, false)
+            .chunks(range.start..line_end, false, None)
             .flat_map(|chunk| chunk.text.chars())
         {
             if c == '\n' {
@@ -123,7 +127,7 @@ impl TabSnapshot {
             last_line_chars = first_line_chars;
         } else {
             for _ in self
-                .chunks(TabPoint::new(range.end.row(), 0)..range.end, false)
+                .chunks(TabPoint::new(range.end.row(), 0)..range.end, false, None)
                 .flat_map(|chunk| chunk.text.chars())
             {
                 last_line_chars += 1;
@@ -143,7 +147,12 @@ impl TabSnapshot {
         self.fold_snapshot.version
     }
 
-    pub fn chunks<'a>(&'a self, range: Range<TabPoint>, language_aware: bool) -> TabChunks<'a> {
+    pub fn chunks<'a>(
+        &'a self,
+        range: Range<TabPoint>,
+        language_aware: bool,
+        text_highlights: Option<&'a TextHighlights>,
+    ) -> TabChunks<'a> {
         let (input_start, expanded_char_column, to_next_stop) =
             self.to_fold_point(range.start, Bias::Left);
         let input_start = input_start.to_offset(&self.fold_snapshot);
@@ -158,9 +167,11 @@ impl TabSnapshot {
         };
 
         TabChunks {
-            fold_chunks: self
-                .fold_snapshot
-                .chunks(input_start..input_end, language_aware),
+            fold_chunks: self.fold_snapshot.chunks(
+                input_start..input_end,
+                language_aware,
+                text_highlights,
+            ),
             column: expanded_char_column,
             output_position: range.start.0,
             max_output_position: range.end.0,
@@ -179,7 +190,7 @@ impl TabSnapshot {
 
     #[cfg(test)]
     pub fn text(&self) -> String {
-        self.chunks(TabPoint::zero()..self.max_point(), false)
+        self.chunks(TabPoint::zero()..self.max_point(), false, None)
             .map(|chunk| chunk.text)
             .collect()
     }
@@ -492,7 +503,7 @@ mod tests {
             assert_eq!(
                 expected_text,
                 tabs_snapshot
-                    .chunks(start..end, false)
+                    .chunks(start..end, false, None)
                     .map(|c| c.text)
                     .collect::<String>(),
                 "chunks({:?}..{:?})",

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

@@ -1,6 +1,7 @@
 use super::{
     fold_map,
     tab_map::{self, TabEdit, TabPoint, TabSnapshot},
+    TextHighlights,
 };
 use crate::{MultiBufferSnapshot, Point};
 use gpui::{
@@ -433,6 +434,7 @@ impl WrapSnapshot {
                 let mut chunks = new_tab_snapshot.chunks(
                     TabPoint::new(edit.new_rows.start, 0)..new_tab_snapshot.max_point(),
                     false,
+                    None,
                 );
                 let mut edit_transforms = Vec::<Transform>::new();
                 for _ in edit.new_rows.start..edit.new_rows.end {
@@ -558,11 +560,16 @@ impl WrapSnapshot {
     }
 
     pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator<Item = &str> {
-        self.chunks(wrap_row..self.max_point().row() + 1, false)
+        self.chunks(wrap_row..self.max_point().row() + 1, false, None)
             .map(|h| h.text)
     }
 
-    pub fn chunks<'a>(&'a self, rows: Range<u32>, language_aware: bool) -> WrapChunks<'a> {
+    pub fn chunks<'a>(
+        &'a self,
+        rows: Range<u32>,
+        language_aware: bool,
+        text_highlights: Option<&'a TextHighlights>,
+    ) -> 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)>();
@@ -575,9 +582,11 @@ impl WrapSnapshot {
             .to_tab_point(output_end)
             .min(self.tab_snapshot.max_point());
         WrapChunks {
-            input_chunks: self
-                .tab_snapshot
-                .chunks(input_start..input_end, language_aware),
+            input_chunks: self.tab_snapshot.chunks(
+                input_start..input_end,
+                language_aware,
+                text_highlights,
+            ),
             input_chunk: Default::default(),
             output_position: output_start,
             max_output_row: rows.end,
@@ -1280,7 +1289,7 @@ mod tests {
                 }
 
                 let actual_text = self
-                    .chunks(start_row..end_row, true)
+                    .chunks(start_row..end_row, true, None)
                     .map(|c| c.text)
                     .collect::<String>();
                 assert_eq!(

crates/editor/src/editor.rs 🔗

@@ -133,6 +133,9 @@ action!(ConfirmCompletion, Option<usize>);
 action!(ConfirmCodeAction, Option<usize>);
 action!(OpenExcerpts);
 
+enum DocumentHighlightRead {}
+enum DocumentHighlightWrite {}
+
 pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpener>>) {
     path_openers.push(Box::new(items::BufferOpener));
     cx.add_bindings(vec![
@@ -408,6 +411,8 @@ type CompletionId = usize;
 
 pub type GetFieldEditorTheme = fn(&theme::Theme) -> theme::FieldEditor;
 
+type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
+
 pub struct Editor {
     handle: WeakViewHandle<Self>,
     buffer: ModelHandle<MultiBuffer>,
@@ -429,16 +434,18 @@ pub struct Editor {
     autoscroll_request: Option<Autoscroll>,
     soft_wrap_mode_override: Option<settings::SoftWrap>,
     get_field_editor_theme: Option<GetFieldEditorTheme>,
+    override_text_style: Option<Box<OverrideTextStyle>>,
     project: Option<ModelHandle<Project>>,
     focused: bool,
     show_local_cursors: bool,
+    show_local_selections: bool,
     blink_epoch: usize,
     blinking_paused: bool,
     mode: EditorMode,
     vertical_scroll_margin: f32,
     placeholder_text: Option<Arc<str>>,
     highlighted_rows: Option<Range<u32>>,
-    highlighted_ranges: BTreeMap<TypeId, (Color, Vec<Range<Anchor>>)>,
+    background_highlights: BTreeMap<TypeId, (Color, Vec<Range<Anchor>>)>,
     nav_history: Option<ItemNavHistory>,
     context_menu: Option<ContextMenu>,
     completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
@@ -448,6 +455,7 @@ pub struct Editor {
     document_highlights_task: Option<Task<()>>,
     pending_rename: Option<RenameState>,
     searchable: bool,
+    cursor_shape: CursorShape,
 }
 
 pub struct EditorSnapshot {
@@ -849,7 +857,7 @@ impl Editor {
     ) -> Self {
         let display_map = cx.add_model(|cx| {
             let settings = cx.app_state::<Settings>();
-            let style = build_style(settings, get_field_editor_theme, cx);
+            let style = build_style(&*settings, get_field_editor_theme, None, cx);
             DisplayMap::new(
                 buffer.clone(),
                 settings.tab_size,
@@ -898,13 +906,14 @@ impl Editor {
             autoscroll_request: None,
             focused: false,
             show_local_cursors: false,
+            show_local_selections: true,
             blink_epoch: 0,
             blinking_paused: false,
             mode,
             vertical_scroll_margin: 3.0,
             placeholder_text: None,
             highlighted_rows: None,
-            highlighted_ranges: Default::default(),
+            background_highlights: Default::default(),
             nav_history: None,
             context_menu: None,
             completion_tasks: Default::default(),
@@ -914,6 +923,8 @@ impl Editor {
             document_highlights_task: Default::default(),
             pending_rename: Default::default(),
             searchable: true,
+            override_text_style: None,
+            cursor_shape: Default::default(),
         };
         this.end_selection(cx);
         this
@@ -966,7 +977,12 @@ impl Editor {
     }
 
     fn style(&self, cx: &AppContext) -> EditorStyle {
-        build_style(cx.app_state::<Settings>(), self.get_field_editor_theme, cx)
+        build_style(
+            cx.app_state::<Settings>(),
+            self.get_field_editor_theme,
+            self.override_text_style.as_deref(),
+            cx,
+        )
     }
 
     pub fn set_placeholder_text(
@@ -1005,6 +1021,11 @@ impl Editor {
         cx.notify();
     }
 
+    pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape, cx: &mut ViewContext<Self>) {
+        self.cursor_shape = cursor_shape;
+        cx.notify();
+    }
+
     pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         compute_scroll_position(&display_map, self.scroll_position, &self.scroll_top_anchor)
@@ -1478,7 +1499,7 @@ impl Editor {
     }
 
     pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
-        if self.take_rename(cx).is_some() {
+        if self.take_rename(false, cx).is_some() {
             return;
         }
 
@@ -2334,7 +2355,7 @@ impl Editor {
             if let Some(editor) = editor.act_as::<Self>(cx) {
                 editor.update(cx, |editor, cx| {
                     let color = editor.style(cx).highlighted_line_background;
-                    editor.highlight_ranges::<Self>(ranges_to_highlight, color, cx);
+                    editor.highlight_background::<Self>(ranges_to_highlight, color, cx);
                 });
             }
         });
@@ -2374,6 +2395,10 @@ impl Editor {
     }
 
     fn refresh_document_highlights(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
+        if self.pending_rename.is_some() {
+            return None;
+        }
+
         let project = self.project.as_ref()?;
         let buffer = self.buffer.read(cx);
         let newest_selection = self.newest_anchor_selection().clone();
@@ -2389,13 +2414,14 @@ impl Editor {
             project.document_highlights(&cursor_buffer, cursor_buffer_position, cx)
         });
 
-        enum DocumentHighlightRead {}
-        enum DocumentHighlightWrite {}
-
         self.document_highlights_task = Some(cx.spawn_weak(|this, mut cx| async move {
             let highlights = highlights.log_err().await;
             if let Some((this, highlights)) = this.upgrade(&cx).zip(highlights) {
                 this.update(&mut cx, |this, cx| {
+                    if this.pending_rename.is_some() {
+                        return;
+                    }
+
                     let buffer_id = cursor_position.buffer_id;
                     let excerpt_id = cursor_position.excerpt_id.clone();
                     let style = this.style(cx);
@@ -2428,12 +2454,12 @@ impl Editor {
                         }
                     }
 
-                    this.highlight_ranges::<DocumentHighlightRead>(
+                    this.highlight_background::<DocumentHighlightRead>(
                         read_ranges,
                         read_background,
                         cx,
                     );
-                    this.highlight_ranges::<DocumentHighlightWrite>(
+                    this.highlight_background::<DocumentHighlightWrite>(
                         write_ranges,
                         write_background,
                         cx,
@@ -3340,7 +3366,7 @@ impl Editor {
     }
 
     pub fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
-        if self.take_rename(cx).is_some() {
+        if self.take_rename(true, cx).is_some() {
             return;
         }
 
@@ -3388,7 +3414,7 @@ impl Editor {
     }
 
     pub fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext<Self>) {
-        self.take_rename(cx);
+        self.take_rename(true, cx);
 
         if let Some(context_menu) = self.context_menu.as_mut() {
             if context_menu.select_next(cx) {
@@ -4332,7 +4358,7 @@ impl Editor {
                 if let Some(editor) = editor.act_as::<Self>(cx) {
                     editor.update(cx, |editor, cx| {
                         let color = editor.style(cx).highlighted_line_background;
-                        editor.highlight_ranges::<Self>(ranges_to_highlight, color, cx);
+                        editor.highlight_background::<Self>(ranges_to_highlight, color, cx);
                     });
                 }
             });
@@ -4350,7 +4376,7 @@ impl Editor {
             .buffer
             .read(cx)
             .text_anchor_for_position(selection.head(), cx)?;
-        let (tail_buffer, tail_buffer_position) = self
+        let (tail_buffer, _) = self
             .buffer
             .read(cx)
             .text_anchor_for_position(selection.tail(), cx)?;
@@ -4360,7 +4386,6 @@ impl Editor {
 
         let snapshot = cursor_buffer.read(cx).snapshot();
         let cursor_buffer_offset = cursor_buffer_position.to_offset(&snapshot);
-        let tail_buffer_offset = tail_buffer_position.to_offset(&snapshot);
         let prepare_rename = project.update(cx, |project, cx| {
             project.prepare_rename(cursor_buffer, cursor_buffer_offset, cx)
         });
@@ -4370,54 +4395,59 @@ impl Editor {
                 let rename_buffer_range = rename_range.to_offset(&snapshot);
                 let cursor_offset_in_rename_range =
                     cursor_buffer_offset.saturating_sub(rename_buffer_range.start);
-                let tail_offset_in_rename_range =
-                    tail_buffer_offset.saturating_sub(rename_buffer_range.start);
 
                 this.update(&mut cx, |this, cx| {
-                    this.take_rename(cx);
+                    this.take_rename(false, cx);
                     let style = this.style(cx);
                     let buffer = this.buffer.read(cx).read(cx);
                     let cursor_offset = selection.head().to_offset(&buffer);
                     let rename_start = cursor_offset.saturating_sub(cursor_offset_in_rename_range);
                     let rename_end = rename_start + rename_buffer_range.len();
                     let range = buffer.anchor_before(rename_start)..buffer.anchor_after(rename_end);
+                    let mut old_highlight_id = None;
                     let old_name = buffer
-                        .text_for_range(rename_start..rename_end)
-                        .collect::<String>();
+                        .chunks(rename_start..rename_end, true)
+                        .map(|chunk| {
+                            if old_highlight_id.is_none() {
+                                old_highlight_id = chunk.syntax_highlight_id;
+                            }
+                            chunk.text
+                        })
+                        .collect();
+
                     drop(buffer);
 
                     // Position the selection in the rename editor so that it matches the current selection.
+                    this.show_local_selections = false;
                     let rename_editor = cx.add_view(|cx| {
                         let mut editor = Editor::single_line(None, cx);
+                        if let Some(old_highlight_id) = old_highlight_id {
+                            editor.override_text_style =
+                                Some(Box::new(move |style| old_highlight_id.style(&style.syntax)));
+                        }
                         editor
                             .buffer
                             .update(cx, |buffer, cx| buffer.edit([0..0], &old_name, cx));
-                        editor.select_ranges(
-                            [tail_offset_in_rename_range..cursor_offset_in_rename_range],
-                            None,
-                            cx,
-                        );
-                        editor.highlight_ranges::<Rename>(
-                            vec![Anchor::min()..Anchor::max()],
-                            style.diff_background_inserted,
-                            cx,
-                        );
+                        editor.select_all(&SelectAll, cx);
                         editor
                     });
-                    this.highlight_ranges::<Rename>(
-                        vec![range.clone()],
-                        style.diff_background_deleted,
-                        cx,
-                    );
-                    this.update_selections(
-                        vec![Selection {
-                            id: selection.id,
-                            start: rename_end,
-                            end: rename_end,
-                            reversed: false,
-                            goal: SelectionGoal::None,
-                        }],
-                        None,
+
+                    let ranges = this
+                        .clear_background_highlights::<DocumentHighlightWrite>(cx)
+                        .into_iter()
+                        .flat_map(|(_, ranges)| ranges)
+                        .chain(
+                            this.clear_background_highlights::<DocumentHighlightRead>(cx)
+                                .into_iter()
+                                .flat_map(|(_, ranges)| ranges),
+                        )
+                        .collect();
+                    this.highlight_text::<Rename>(
+                        ranges,
+                        HighlightStyle {
+                            fade_out: Some(style.rename_fade),
+                            ..Default::default()
+                        },
                         cx,
                     );
                     cx.focus(&rename_editor);
@@ -4459,7 +4489,7 @@ impl Editor {
         let editor = workspace.active_item(cx)?.act_as::<Editor>(cx)?;
 
         let (buffer, range, old_name, new_name) = editor.update(cx, |editor, cx| {
-            let rename = editor.take_rename(cx)?;
+            let rename = editor.take_rename(false, cx)?;
             let buffer = editor.buffer.read(cx);
             let (start_buffer, start) =
                 buffer.text_anchor_for_position(rename.range.start.clone(), cx)?;
@@ -4483,48 +4513,59 @@ impl Editor {
             )
         });
 
-        Some(cx.spawn(|workspace, cx| async move {
+        Some(cx.spawn(|workspace, mut cx| async move {
             let project_transaction = rename.await?;
             Self::open_project_transaction(
-                editor,
+                editor.clone(),
                 workspace,
                 project_transaction,
                 format!("Rename: {} → {}", old_name, new_name),
-                cx,
+                cx.clone(),
             )
-            .await
+            .await?;
+
+            editor.update(&mut cx, |editor, cx| {
+                editor.refresh_document_highlights(cx);
+            });
+            Ok(())
         }))
     }
 
-    fn take_rename(&mut self, cx: &mut ViewContext<Self>) -> Option<RenameState> {
+    fn take_rename(
+        &mut self,
+        moving_cursor: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<RenameState> {
         let rename = self.pending_rename.take()?;
         self.remove_blocks([rename.block_id].into_iter().collect(), cx);
-        self.clear_highlighted_ranges::<Rename>(cx);
+        self.clear_text_highlights::<Rename>(cx);
+        self.show_local_selections = true;
 
-        let editor = rename.editor.read(cx);
-        let snapshot = self.buffer.read(cx).snapshot(cx);
-        let selection = editor.newest_selection_with_snapshot::<usize>(&snapshot);
-
-        // Update the selection to match the position of the selection inside
-        // the rename editor.
-        let rename_range = rename.range.to_offset(&snapshot);
-        let start = snapshot
-            .clip_offset(rename_range.start + selection.start, Bias::Left)
-            .min(rename_range.end);
-        let end = snapshot
-            .clip_offset(rename_range.start + selection.end, Bias::Left)
-            .min(rename_range.end);
-        self.update_selections(
-            vec![Selection {
-                id: self.newest_anchor_selection().id,
-                start,
-                end,
-                reversed: selection.reversed,
-                goal: SelectionGoal::None,
-            }],
-            None,
-            cx,
-        );
+        if moving_cursor {
+            let cursor_in_rename_editor =
+                rename.editor.read(cx).newest_selection::<usize>(cx).head();
+
+            // Update the selection to match the position of the selection inside
+            // the rename editor.
+            let snapshot = self.buffer.read(cx).read(cx);
+            let rename_range = rename.range.to_offset(&snapshot);
+            let cursor_in_editor = snapshot
+                .clip_offset(rename_range.start + cursor_in_rename_editor, Bias::Left)
+                .min(rename_range.end);
+            drop(snapshot);
+
+            self.update_selections(
+                vec![Selection {
+                    id: self.newest_anchor_selection().id,
+                    start: cursor_in_editor,
+                    end: cursor_in_editor,
+                    reversed: false,
+                    goal: SelectionGoal::None,
+                }],
+                None,
+                cx,
+            );
+        }
 
         Some(rename)
     }
@@ -4544,7 +4585,7 @@ impl Editor {
             }
             let rename = self.pending_rename.take().unwrap();
             self.remove_blocks([rename.block_id].into_iter().collect(), cx);
-            self.clear_highlighted_ranges::<Rename>(cx);
+            self.clear_background_highlights::<Rename>(cx);
         }
     }
 
@@ -5256,7 +5297,7 @@ impl Editor {
             .update(cx, |map, cx| map.set_wrap_width(width, cx))
     }
 
-    pub fn set_highlighted_rows(&mut self, rows: Option<Range<u32>>) {
+    pub fn highlight_rows(&mut self, rows: Option<Range<u32>>) {
         self.highlighted_rows = rows;
     }
 
@@ -5264,27 +5305,27 @@ impl Editor {
         self.highlighted_rows.clone()
     }
 
-    pub fn highlight_ranges<T: 'static>(
+    pub fn highlight_background<T: 'static>(
         &mut self,
         ranges: Vec<Range<Anchor>>,
         color: Color,
         cx: &mut ViewContext<Self>,
     ) {
-        self.highlighted_ranges
+        self.background_highlights
             .insert(TypeId::of::<T>(), (color, ranges));
         cx.notify();
     }
 
-    pub fn clear_highlighted_ranges<T: 'static>(
+    pub fn clear_background_highlights<T: 'static>(
         &mut self,
         cx: &mut ViewContext<Self>,
     ) -> Option<(Color, Vec<Range<Anchor>>)> {
         cx.notify();
-        self.highlighted_ranges.remove(&TypeId::of::<T>())
+        self.background_highlights.remove(&TypeId::of::<T>())
     }
 
     #[cfg(feature = "test-support")]
-    pub fn all_highlighted_ranges(
+    pub fn all_background_highlights(
         &mut self,
         cx: &mut ViewContext<Self>,
     ) -> Vec<(Range<DisplayPoint>, Color)> {
@@ -5292,23 +5333,23 @@ impl Editor {
         let buffer = &snapshot.buffer_snapshot;
         let start = buffer.anchor_before(0);
         let end = buffer.anchor_after(buffer.len());
-        self.highlighted_ranges_in_range(start..end, &snapshot)
+        self.background_highlights_in_range(start..end, &snapshot)
     }
 
-    pub fn highlighted_ranges_for_type<T: 'static>(&self) -> Option<(Color, &[Range<Anchor>])> {
-        self.highlighted_ranges
+    pub fn background_highlights_for_type<T: 'static>(&self) -> Option<(Color, &[Range<Anchor>])> {
+        self.background_highlights
             .get(&TypeId::of::<T>())
             .map(|(color, ranges)| (*color, ranges.as_slice()))
     }
 
-    pub fn highlighted_ranges_in_range(
+    pub fn background_highlights_in_range(
         &self,
         search_range: Range<Anchor>,
         display_snapshot: &DisplaySnapshot,
     ) -> Vec<(Range<DisplayPoint>, Color)> {
         let mut results = Vec::new();
         let buffer = &display_snapshot.buffer_snapshot;
-        for (color, ranges) in self.highlighted_ranges.values() {
+        for (color, ranges) in self.background_highlights.values() {
             let start_ix = match ranges.binary_search_by(|probe| {
                 let cmp = probe.end.cmp(&search_range.start, &buffer).unwrap();
                 if cmp.is_gt() {
@@ -5337,6 +5378,27 @@ impl Editor {
         results
     }
 
+    pub fn highlight_text<T: 'static>(
+        &mut self,
+        ranges: Vec<Range<Anchor>>,
+        style: HighlightStyle,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.display_map.update(cx, |map, _| {
+            map.highlight_text(TypeId::of::<T>(), ranges, style)
+        });
+        cx.notify();
+    }
+
+    pub fn clear_text_highlights<T: 'static>(
+        &mut self,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Arc<(HighlightStyle, Vec<Range<Anchor>>)>> {
+        cx.notify();
+        self.display_map
+            .update(cx, |map, _| map.clear_text_highlights(TypeId::of::<T>()))
+    }
+
     fn next_blink_epoch(&mut self) -> usize {
         self.blink_epoch += 1;
         self.blink_epoch
@@ -5560,7 +5622,7 @@ impl View for Editor {
         self.display_map.update(cx, |map, cx| {
             map.set_font(style.text.font_id, style.text.font_size, cx)
         });
-        EditorElement::new(self.handle.clone(), style.clone()).boxed()
+        EditorElement::new(self.handle.clone(), style.clone(), self.cursor_shape).boxed()
     }
 
     fn ui_name() -> &'static str {
@@ -5568,17 +5630,20 @@ impl View for Editor {
     }
 
     fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
-        self.focused = true;
-        self.blink_cursors(self.blink_epoch, cx);
-        self.buffer.update(cx, |buffer, cx| {
-            buffer.finalize_last_transaction(cx);
-            buffer.set_active_selections(&self.selections, cx)
-        });
+        if let Some(rename) = self.pending_rename.as_ref() {
+            cx.focus(&rename.editor);
+        } else {
+            self.focused = true;
+            self.blink_cursors(self.blink_epoch, cx);
+            self.buffer.update(cx, |buffer, cx| {
+                buffer.finalize_last_transaction(cx);
+                buffer.set_active_selections(&self.selections, cx)
+            });
+        }
     }
 
     fn on_blur(&mut self, cx: &mut ViewContext<Self>) {
         self.focused = false;
-        self.show_local_cursors = false;
         self.buffer
             .update(cx, |buffer, cx| buffer.remove_active_selections(cx));
         self.hide_context_menu(cx);
@@ -5613,14 +5678,14 @@ impl View for Editor {
 fn build_style(
     settings: &Settings,
     get_field_editor_theme: Option<GetFieldEditorTheme>,
+    override_text_style: Option<&OverrideTextStyle>,
     cx: &AppContext,
 ) -> EditorStyle {
+    let font_cache = cx.font_cache();
+
     let mut theme = settings.theme.editor.clone();
-    if let Some(get_field_editor_theme) = get_field_editor_theme {
+    let mut style = if let Some(get_field_editor_theme) = get_field_editor_theme {
         let field_editor_theme = get_field_editor_theme(&settings.theme);
-        if let Some(background) = field_editor_theme.container.background_color {
-            theme.background = background;
-        }
         theme.text_color = field_editor_theme.text.color;
         theme.selection = field_editor_theme.selection;
         EditorStyle {
@@ -5629,7 +5694,6 @@ fn build_style(
             theme,
         }
     } else {
-        let font_cache = cx.font_cache();
         let font_family_id = settings.buffer_font_family;
         let font_family_name = cx.font_cache().family_name(font_family_id).unwrap();
         let font_properties = Default::default();
@@ -5650,7 +5714,20 @@ fn build_style(
             placeholder_text: None,
             theme,
         }
+    };
+
+    if let Some(highlight_style) = override_text_style.and_then(|build_style| build_style(&style)) {
+        if let Some(highlighted) = style
+            .text
+            .clone()
+            .highlight(highlight_style, font_cache)
+            .log_err()
+        {
+            style.text = highlighted;
+        }
     }
+
+    style
 }
 
 impl<T: ToPoint + ToOffset> SelectionExt for Selection<T> {
@@ -8841,7 +8918,7 @@ mod tests {
                 buffer.anchor_after(range.start)..buffer.anchor_after(range.end)
             };
 
-            editor.highlight_ranges::<Type1>(
+            editor.highlight_background::<Type1>(
                 vec![
                     anchor_range(Point::new(2, 1)..Point::new(2, 3)),
                     anchor_range(Point::new(4, 2)..Point::new(4, 4)),
@@ -8851,7 +8928,7 @@ mod tests {
                 Color::red(),
                 cx,
             );
-            editor.highlight_ranges::<Type2>(
+            editor.highlight_background::<Type2>(
                 vec![
                     anchor_range(Point::new(3, 2)..Point::new(3, 5)),
                     anchor_range(Point::new(5, 3)..Point::new(5, 6)),
@@ -8863,7 +8940,7 @@ mod tests {
             );
 
             let snapshot = editor.snapshot(cx);
-            let mut highlighted_ranges = editor.highlighted_ranges_in_range(
+            let mut highlighted_ranges = editor.background_highlights_in_range(
                 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
                 &snapshot,
             );
@@ -8892,7 +8969,7 @@ mod tests {
                 ]
             );
             assert_eq!(
-                editor.highlighted_ranges_in_range(
+                editor.background_highlights_in_range(
                     anchor_range(Point::new(5, 6)..Point::new(6, 4)),
                     &snapshot,
                 ),

crates/editor/src/element.rs 🔗

@@ -16,7 +16,7 @@ use gpui::{
         PathBuilder,
     },
     json::{self, ToJson},
-    text_layout::{self, RunStyle, TextLayoutCache},
+    text_layout::{self, Line, RunStyle, TextLayoutCache},
     AppContext, Axis, Border, Element, ElementBox, Event, EventContext, LayoutContext,
     MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle,
 };
@@ -32,11 +32,20 @@ use std::{
 pub struct EditorElement {
     view: WeakViewHandle<Editor>,
     style: EditorStyle,
+    cursor_shape: CursorShape,
 }
 
 impl EditorElement {
-    pub fn new(view: WeakViewHandle<Editor>, style: EditorStyle) -> Self {
-        Self { view, style }
+    pub fn new(
+        view: WeakViewHandle<Editor>,
+        style: EditorStyle,
+        cursor_shape: CursorShape,
+    ) -> Self {
+        Self {
+            view,
+            style,
+            cursor_shape,
+        }
     }
 
     fn view<'a>(&self, cx: &'a AppContext) -> &'a Editor {
@@ -338,7 +347,7 @@ impl EditorElement {
 
         let mut cursors = SmallVec::<[Cursor; 32]>::new();
         for (replica_id, selections) in &layout.selections {
-            let style = style.replica_selection_style(*replica_id);
+            let selection_style = style.replica_selection_style(*replica_id);
             let corner_radius = 0.15 * layout.line_height;
 
             for selection in selections {
@@ -346,7 +355,7 @@ impl EditorElement {
                     selection.start..selection.end,
                     start_row,
                     end_row,
-                    style.selection,
+                    selection_style.selection,
                     corner_radius,
                     corner_radius * 2.,
                     layout,
@@ -362,13 +371,50 @@ impl EditorElement {
                     if (start_row..end_row).contains(&cursor_position.row()) {
                         let cursor_row_layout =
                             &layout.line_layouts[(cursor_position.row() - start_row) as usize];
-                        let x = cursor_row_layout.x_for_index(cursor_position.column() as usize)
-                            - scroll_left;
+                        let cursor_column = cursor_position.column() as usize;
+
+                        let cursor_character_x = cursor_row_layout.x_for_index(cursor_column);
+                        let mut block_width =
+                            cursor_row_layout.x_for_index(cursor_column + 1) - cursor_character_x;
+                        if block_width == 0.0 {
+                            block_width = layout.em_width;
+                        }
+
+                        let block_text =
+                            if matches!(self.cursor_shape, CursorShape::Block) {
+                                layout.snapshot.chars_at(cursor_position).next().and_then(
+                                    |character| {
+                                        let font_id =
+                                            cursor_row_layout.font_for_index(cursor_column)?;
+                                        let text = character.to_string();
+
+                                        Some(cx.text_layout_cache.layout_str(
+                                            &text,
+                                            cursor_row_layout.font_size(),
+                                            &[(
+                                                text.len(),
+                                                RunStyle {
+                                                    font_id,
+                                                    color: style.background,
+                                                    underline: None,
+                                                },
+                                            )],
+                                        ))
+                                    },
+                                )
+                            } else {
+                                None
+                            };
+
+                        let x = cursor_character_x - scroll_left;
                         let y = cursor_position.row() as f32 * layout.line_height - scroll_top;
                         cursors.push(Cursor {
-                            color: style.cursor,
+                            color: selection_style.cursor,
+                            block_width,
                             origin: content_origin + vec2f(x, y),
                             line_height: layout.line_height,
+                            shape: self.cursor_shape,
+                            block_text,
                         });
                     }
                 }
@@ -606,30 +652,37 @@ impl EditorElement {
         } else {
             let style = &self.style;
             let chunks = snapshot.chunks(rows.clone(), true).map(|chunk| {
-                let highlight_style = chunk
-                    .highlight_id
-                    .and_then(|highlight_id| highlight_id.style(&style.syntax));
-                let highlight = if let Some(severity) = chunk.diagnostic {
+                let mut highlight_style = chunk
+                    .syntax_highlight_id
+                    .and_then(|id| id.style(&style.syntax));
+
+                if let Some(chunk_highlight) = chunk.highlight_style {
+                    if let Some(highlight_style) = highlight_style.as_mut() {
+                        highlight_style.highlight(chunk_highlight);
+                    } else {
+                        highlight_style = Some(chunk_highlight);
+                    }
+                }
+
+                if let Some(severity) = chunk.diagnostic {
                     let diagnostic_style = super::diagnostic_style(severity, true, style);
-                    let underline = Some(Underline {
-                        color: diagnostic_style.message.text.color,
-                        thickness: 1.0.into(),
-                        squiggly: true,
-                    });
-                    if let Some(mut highlight) = highlight_style {
-                        highlight.underline = underline;
-                        Some(highlight)
+                    let diagnostic_highlight = HighlightStyle {
+                        underline: Some(Underline {
+                            color: diagnostic_style.message.text.color,
+                            thickness: 1.0.into(),
+                            squiggly: true,
+                        }),
+                        ..Default::default()
+                    };
+
+                    if let Some(highlight_style) = highlight_style.as_mut() {
+                        highlight_style.highlight(diagnostic_highlight);
                     } else {
-                        Some(HighlightStyle {
-                            underline,
-                            color: style.text.color,
-                            font_properties: style.text.font_properties,
-                        })
+                        highlight_style = Some(diagnostic_highlight);
                     }
-                } else {
-                    highlight_style
-                };
-                (chunk.text, highlight)
+                }
+
+                (chunk.text, highlight_style)
             });
             layout_highlighted_chunks(
                 chunks,
@@ -852,37 +905,42 @@ impl Element for EditorElement {
             let display_map = view.display_map.update(cx, |map, cx| map.snapshot(cx));
 
             highlighted_rows = view.highlighted_rows();
-            highlighted_ranges = view.highlighted_ranges_in_range(
+            highlighted_ranges = view.background_highlights_in_range(
                 start_anchor.clone()..end_anchor.clone(),
                 &display_map,
             );
 
-            let local_selections = view
-                .local_selections_in_range(start_anchor.clone()..end_anchor.clone(), &display_map);
-            for selection in &local_selections {
-                let is_empty = selection.start == selection.end;
-                let selection_start = snapshot.prev_line_boundary(selection.start).1;
-                let selection_end = snapshot.next_line_boundary(selection.end).1;
-                for row in cmp::max(selection_start.row(), start_row)
-                    ..=cmp::min(selection_end.row(), end_row)
-                {
-                    let contains_non_empty_selection = active_rows.entry(row).or_insert(!is_empty);
-                    *contains_non_empty_selection |= !is_empty;
+            if view.show_local_selections {
+                let local_selections = view.local_selections_in_range(
+                    start_anchor.clone()..end_anchor.clone(),
+                    &display_map,
+                );
+                for selection in &local_selections {
+                    let is_empty = selection.start == selection.end;
+                    let selection_start = snapshot.prev_line_boundary(selection.start).1;
+                    let selection_end = snapshot.next_line_boundary(selection.end).1;
+                    for row in cmp::max(selection_start.row(), start_row)
+                        ..=cmp::min(selection_end.row(), end_row)
+                    {
+                        let contains_non_empty_selection =
+                            active_rows.entry(row).or_insert(!is_empty);
+                        *contains_non_empty_selection |= !is_empty;
+                    }
                 }
+                selections.insert(
+                    view.replica_id(cx),
+                    local_selections
+                        .into_iter()
+                        .map(|selection| crate::Selection {
+                            id: selection.id,
+                            goal: selection.goal,
+                            reversed: selection.reversed,
+                            start: selection.start.to_display_point(&display_map),
+                            end: selection.end.to_display_point(&display_map),
+                        })
+                        .collect(),
+                );
             }
-            selections.insert(
-                view.replica_id(cx),
-                local_selections
-                    .into_iter()
-                    .map(|selection| crate::Selection {
-                        id: selection.id,
-                        goal: selection.goal,
-                        reversed: selection.reversed,
-                        start: selection.start.to_display_point(&display_map),
-                        end: selection.end.to_display_point(&display_map),
-                    })
-                    .collect(),
-            );
 
             for (replica_id, selection) in display_map
                 .buffer_snapshot
@@ -1161,6 +1219,7 @@ fn layout_line(
         while !line.is_char_boundary(len) {
             len -= 1;
         }
+
         line.truncate(len);
     }
 
@@ -1212,20 +1271,51 @@ impl PaintState {
     }
 }
 
+#[derive(Copy, Clone)]
+pub enum CursorShape {
+    Bar,
+    Block,
+    Underscore,
+}
+
+impl Default for CursorShape {
+    fn default() -> Self {
+        CursorShape::Bar
+    }
+}
+
 struct Cursor {
     origin: Vector2F,
+    block_width: f32,
     line_height: f32,
     color: Color,
+    shape: CursorShape,
+    block_text: Option<Line>,
 }
 
 impl Cursor {
     fn paint(&self, cx: &mut PaintContext) {
+        let bounds = match self.shape {
+            CursorShape::Bar => RectF::new(self.origin, vec2f(2.0, self.line_height)),
+            CursorShape::Block => {
+                RectF::new(self.origin, vec2f(self.block_width, self.line_height))
+            }
+            CursorShape::Underscore => RectF::new(
+                self.origin + Vector2F::new(0.0, self.line_height - 2.0),
+                vec2f(self.block_width, 2.0),
+            ),
+        };
+
         cx.scene.push_quad(Quad {
-            bounds: RectF::new(self.origin, vec2f(2.0, self.line_height)),
+            bounds,
             background: Some(self.color),
             border: Border::new(0., Color::black()),
             corner_radius: 0.,
         });
+
+        if let Some(block_text) = &self.block_text {
+            block_text.paint(self.origin, bounds, self.line_height, cx);
+        }
     }
 }
 
@@ -1388,7 +1478,11 @@ mod tests {
         let (window_id, editor) = cx.add_window(Default::default(), |cx| {
             Editor::new(EditorMode::Full, buffer, None, None, cx)
         });
-        let element = EditorElement::new(editor.downgrade(), editor.read(cx).style(cx));
+        let element = EditorElement::new(
+            editor.downgrade(),
+            editor.read(cx).style(cx),
+            CursorShape::Bar,
+        );
 
         let layouts = editor.update(cx, |editor, cx| {
             let snapshot = editor.snapshot(cx);

crates/editor/src/multi_buffer.rs 🔗

@@ -36,6 +36,7 @@ pub type ExcerptId = Locator;
 pub struct MultiBuffer {
     snapshot: RefCell<MultiBufferSnapshot>,
     buffers: RefCell<HashMap<usize, BufferState>>,
+    used_excerpt_ids: SumTree<ExcerptId>,
     subscriptions: Topic,
     singleton: bool,
     replica_id: ReplicaId,
@@ -155,6 +156,7 @@ impl MultiBuffer {
         Self {
             snapshot: Default::default(),
             buffers: Default::default(),
+            used_excerpt_ids: Default::default(),
             subscriptions: Default::default(),
             singleton: false,
             replica_id,
@@ -192,6 +194,7 @@ impl MultiBuffer {
         Self {
             snapshot: RefCell::new(self.snapshot.borrow().clone()),
             buffers: RefCell::new(buffers),
+            used_excerpt_ids: Default::default(),
             subscriptions: Default::default(),
             singleton: self.singleton,
             replica_id: self.replica_id,
@@ -213,25 +216,6 @@ impl MultiBuffer {
         this
     }
 
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn build_simple(text: &str, cx: &mut gpui::MutableAppContext) -> ModelHandle<Self> {
-        let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
-        cx.add_model(|cx| Self::singleton(buffer, cx))
-    }
-
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn build_random(
-        rng: &mut impl rand::Rng,
-        cx: &mut gpui::MutableAppContext,
-    ) -> ModelHandle<Self> {
-        cx.add_model(|cx| {
-            let mut multibuffer = MultiBuffer::new(0);
-            let mutation_count = rng.gen_range(1..=5);
-            multibuffer.randomly_edit_excerpts(rng, mutation_count, cx);
-            multibuffer
-        })
-    }
-
     pub fn replica_id(&self) -> ReplicaId {
         self.replica_id
     }
@@ -748,20 +732,27 @@ impl MultiBuffer {
         let mut cursor = snapshot.excerpts.cursor::<Option<&ExcerptId>>();
         let mut new_excerpts = cursor.slice(&Some(prev_excerpt_id), Bias::Right, &());
 
-        let mut prev_id = ExcerptId::min();
         let edit_start = new_excerpts.summary().text.bytes;
         new_excerpts.update_last(
             |excerpt| {
                 excerpt.has_trailing_newline = true;
-                prev_id = excerpt.id.clone();
             },
             &(),
         );
 
-        let mut next_id = ExcerptId::max();
-        if let Some(next_excerpt) = cursor.item() {
-            next_id = next_excerpt.id.clone();
-        }
+        let mut used_cursor = self.used_excerpt_ids.cursor::<Locator>();
+        used_cursor.seek(prev_excerpt_id, Bias::Right, &());
+        let mut prev_id = if let Some(excerpt_id) = used_cursor.prev_item() {
+            excerpt_id.clone()
+        } else {
+            ExcerptId::min()
+        };
+        let next_id = if let Some(excerpt_id) = used_cursor.item() {
+            excerpt_id.clone()
+        } else {
+            ExcerptId::max()
+        };
+        drop(used_cursor);
 
         let mut ids = Vec::new();
         while let Some(range) = ranges.next() {
@@ -782,6 +773,10 @@ impl MultiBuffer {
             prev_id = id.clone();
             ids.push(id);
         }
+        self.used_excerpt_ids.edit(
+            ids.iter().cloned().map(sum_tree::Edit::Insert).collect(),
+            &(),
+        );
 
         let edit_end = new_excerpts.summary().text.bytes;
 
@@ -963,10 +958,13 @@ impl MultiBuffer {
     ) -> Option<(ModelHandle<Buffer>, language::Anchor)> {
         let snapshot = self.read(cx);
         let anchor = snapshot.anchor_before(position);
-        Some((
-            self.buffers.borrow()[&anchor.buffer_id?].buffer.clone(),
-            anchor.text_anchor,
-        ))
+        let buffer = self
+            .buffers
+            .borrow()
+            .get(&anchor.buffer_id?)?
+            .buffer
+            .clone();
+        Some((buffer, anchor.text_anchor))
     }
 
     fn on_buffer_event(
@@ -1020,14 +1018,19 @@ impl MultiBuffer {
 
         let snapshot = self.snapshot(cx);
         let anchor = snapshot.anchor_before(position);
-        anchor.buffer_id.map_or(false, |buffer_id| {
-            let buffer = self.buffers.borrow()[&buffer_id].buffer.clone();
-            buffer
-                .read(cx)
-                .completion_triggers()
-                .iter()
-                .any(|string| string == text)
-        })
+        anchor
+            .buffer_id
+            .and_then(|buffer_id| {
+                let buffer = self.buffers.borrow().get(&buffer_id)?.buffer.clone();
+                Some(
+                    buffer
+                        .read(cx)
+                        .completion_triggers()
+                        .iter()
+                        .any(|string| string == text),
+                )
+            })
+            .unwrap_or(false)
     }
 
     pub fn language<'a>(&self, cx: &'a AppContext) -> Option<&'a Arc<Language>> {
@@ -1170,6 +1173,23 @@ impl MultiBuffer {
 
 #[cfg(any(test, feature = "test-support"))]
 impl MultiBuffer {
+    pub fn build_simple(text: &str, cx: &mut gpui::MutableAppContext) -> ModelHandle<Self> {
+        let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
+        cx.add_model(|cx| Self::singleton(buffer, cx))
+    }
+
+    pub fn build_random(
+        rng: &mut impl rand::Rng,
+        cx: &mut gpui::MutableAppContext,
+    ) -> ModelHandle<Self> {
+        cx.add_model(|cx| {
+            let mut multibuffer = MultiBuffer::new(0);
+            let mutation_count = rng.gen_range(1..=5);
+            multibuffer.randomly_edit_excerpts(rng, mutation_count, cx);
+            multibuffer
+        })
+    }
+
     pub fn randomly_edit(
         &mut self,
         rng: &mut impl rand::Rng,
@@ -1757,7 +1777,7 @@ impl MultiBufferSnapshot {
 
         let mut position = D::from_text_summary(&cursor.start().text);
         if let Some(excerpt) = cursor.item() {
-            if excerpt.id == anchor.excerpt_id && Some(excerpt.buffer_id) == anchor.buffer_id {
+            if excerpt.id == anchor.excerpt_id {
                 let excerpt_buffer_start = excerpt.range.start.summary::<D>(&excerpt.buffer);
                 let excerpt_buffer_end = excerpt.range.end.summary::<D>(&excerpt.buffer);
                 let buffer_position = cmp::min(
@@ -1788,10 +1808,9 @@ impl MultiBufferSnapshot {
         let mut summaries = Vec::new();
         while let Some(anchor) = anchors.peek() {
             let excerpt_id = &anchor.excerpt_id;
-            let buffer_id = anchor.buffer_id;
             let excerpt_anchors = iter::from_fn(|| {
                 let anchor = anchors.peek()?;
-                if anchor.excerpt_id == *excerpt_id && anchor.buffer_id == buffer_id {
+                if anchor.excerpt_id == *excerpt_id {
                     Some(&anchors.next().unwrap().text_anchor)
                 } else {
                     None
@@ -1805,7 +1824,7 @@ impl MultiBufferSnapshot {
 
             let position = D::from_text_summary(&cursor.start().text);
             if let Some(excerpt) = cursor.item() {
-                if excerpt.id == *excerpt_id && Some(excerpt.buffer_id) == buffer_id {
+                if excerpt.id == *excerpt_id {
                     let excerpt_buffer_start = excerpt.range.start.summary::<D>(&excerpt.buffer);
                     let excerpt_buffer_end = excerpt.range.end.summary::<D>(&excerpt.buffer);
                     summaries.extend(
@@ -1998,10 +2017,8 @@ impl MultiBufferSnapshot {
     pub fn can_resolve(&self, anchor: &Anchor) -> bool {
         if anchor.excerpt_id == ExcerptId::min() || anchor.excerpt_id == ExcerptId::max() {
             true
-        } else if let Some((buffer_id, buffer_snapshot)) =
-            self.buffer_snapshot_for_excerpt(&anchor.excerpt_id)
-        {
-            anchor.buffer_id == Some(buffer_id) && buffer_snapshot.can_resolve(&anchor.text_anchor)
+        } else if let Some(excerpt) = self.excerpt(&anchor.excerpt_id) {
+            excerpt.buffer.can_resolve(&anchor.text_anchor)
         } else {
             false
         }
@@ -2231,15 +2248,12 @@ impl MultiBufferSnapshot {
         ))
     }
 
-    fn buffer_snapshot_for_excerpt<'a>(
-        &'a self,
-        excerpt_id: &'a ExcerptId,
-    ) -> Option<(usize, &'a BufferSnapshot)> {
+    fn excerpt<'a>(&'a self, excerpt_id: &'a ExcerptId) -> Option<&'a Excerpt> {
         let mut cursor = self.excerpts.cursor::<Option<&ExcerptId>>();
         cursor.seek(&Some(excerpt_id), Bias::Left, &());
         if let Some(excerpt) = cursor.item() {
             if excerpt.id == *excerpt_id {
-                return Some((excerpt.buffer_id, &excerpt.buffer));
+                return Some(excerpt);
             }
         }
         None
@@ -2300,6 +2314,15 @@ impl MultiBufferSnapshot {
     }
 }
 
+#[cfg(any(test, feature = "test-support"))]
+impl MultiBufferSnapshot {
+    pub fn random_byte_range(&self, start_offset: usize, rng: &mut impl rand::Rng) -> Range<usize> {
+        let end = self.clip_offset(rng.gen_range(start_offset..=self.len()), Bias::Right);
+        let start = self.clip_offset(rng.gen_range(start_offset..=end), Bias::Right);
+        start..end
+    }
+}
+
 impl History {
     fn start_transaction(&mut self, now: Instant) -> Option<TransactionId> {
         self.transaction_depth += 1;
@@ -3213,8 +3236,8 @@ mod tests {
         let snapshot_2 = multibuffer.read(cx).snapshot(cx);
         assert_eq!(snapshot_2.text(), "ABCD\nGHIJ\nMNOP");
 
-        // The old excerpt id has been reused.
-        assert_eq!(excerpt_id_2, excerpt_id_1);
+        // The old excerpt id doesn't get reused.
+        assert_ne!(excerpt_id_2, excerpt_id_1);
 
         // Resolve some anchors from the previous snapshot in the new snapshot.
         // Although there is still an excerpt with the same id, it is for
@@ -3266,7 +3289,7 @@ mod tests {
         ];
         assert_eq!(
             snapshot_3.summaries_for_anchors::<usize, _>(&anchors),
-            &[0, 2, 9, 13]
+            &[0, 2, 5, 13]
         );
 
         let new_anchors = snapshot_3.refresh_anchors(&anchors);

crates/editor/src/multi_buffer/anchor.rs 🔗

@@ -40,21 +40,8 @@ impl Anchor {
         if excerpt_id_cmp.is_eq() {
             if self.excerpt_id == ExcerptId::min() || self.excerpt_id == ExcerptId::max() {
                 Ok(Ordering::Equal)
-            } else if let Some((buffer_id, buffer_snapshot)) =
-                snapshot.buffer_snapshot_for_excerpt(&self.excerpt_id)
-            {
-                // Even though the anchor refers to a valid excerpt the underlying buffer might have
-                // changed. In that case, treat the anchor as if it were at the start of that
-                // excerpt.
-                if self.buffer_id == Some(buffer_id) && other.buffer_id == Some(buffer_id) {
-                    self.text_anchor.cmp(&other.text_anchor, buffer_snapshot)
-                } else if self.buffer_id == Some(buffer_id) {
-                    Ok(Ordering::Greater)
-                } else if other.buffer_id == Some(buffer_id) {
-                    Ok(Ordering::Less)
-                } else {
-                    Ok(Ordering::Equal)
-                }
+            } else if let Some(excerpt) = snapshot.excerpt(&self.excerpt_id) {
+                self.text_anchor.cmp(&other.text_anchor, &excerpt.buffer)
             } else {
                 Ok(Ordering::Equal)
             }
@@ -65,16 +52,12 @@ impl Anchor {
 
     pub fn bias_left(&self, snapshot: &MultiBufferSnapshot) -> Anchor {
         if self.text_anchor.bias != Bias::Left {
-            if let Some((buffer_id, buffer_snapshot)) =
-                snapshot.buffer_snapshot_for_excerpt(&self.excerpt_id)
-            {
-                if self.buffer_id == Some(buffer_id) {
-                    return Self {
-                        buffer_id: self.buffer_id,
-                        excerpt_id: self.excerpt_id.clone(),
-                        text_anchor: self.text_anchor.bias_left(buffer_snapshot),
-                    };
-                }
+            if let Some(excerpt) = snapshot.excerpt(&self.excerpt_id) {
+                return Self {
+                    buffer_id: self.buffer_id,
+                    excerpt_id: self.excerpt_id.clone(),
+                    text_anchor: self.text_anchor.bias_left(&excerpt.buffer),
+                };
             }
         }
         self.clone()
@@ -82,16 +65,12 @@ impl Anchor {
 
     pub fn bias_right(&self, snapshot: &MultiBufferSnapshot) -> Anchor {
         if self.text_anchor.bias != Bias::Right {
-            if let Some((buffer_id, buffer_snapshot)) =
-                snapshot.buffer_snapshot_for_excerpt(&self.excerpt_id)
-            {
-                if self.buffer_id == Some(buffer_id) {
-                    return Self {
-                        buffer_id: self.buffer_id,
-                        excerpt_id: self.excerpt_id.clone(),
-                        text_anchor: self.text_anchor.bias_right(buffer_snapshot),
-                    };
-                }
+            if let Some(excerpt) = snapshot.excerpt(&self.excerpt_id) {
+                return Self {
+                    buffer_id: self.buffer_id,
+                    excerpt_id: self.excerpt_id.clone(),
+                    text_anchor: self.text_anchor.bias_right(&excerpt.buffer),
+                };
             }
         }
         self.clone()

crates/go_to_line/src/go_to_line.rs 🔗

@@ -115,7 +115,7 @@ impl GoToLine {
                         let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left);
                         let display_point = point.to_display_point(&snapshot);
                         let row = display_point.row();
-                        active_editor.set_highlighted_rows(Some(row..row + 1));
+                        active_editor.highlight_rows(Some(row..row + 1));
                         active_editor.request_autoscroll(Autoscroll::Center, cx);
                     });
                     cx.notify();
@@ -132,7 +132,7 @@ impl Entity for GoToLine {
     fn release(&mut self, cx: &mut MutableAppContext) {
         let scroll_position = self.prev_scroll_position.take();
         self.active_editor.update(cx, |editor, cx| {
-            editor.set_highlighted_rows(None);
+            editor.highlight_rows(None);
             if let Some(scroll_position) = scroll_position {
                 editor.set_scroll_position(scroll_position, cx);
             }

crates/gpui/src/app.rs 🔗

@@ -740,6 +740,7 @@ type ActionCallback =
 type GlobalActionCallback = dyn FnMut(&dyn AnyAction, &mut MutableAppContext);
 
 type SubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext) -> bool>;
+type GlobalSubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
 type ObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
 type ReleaseObservationCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
 
@@ -756,8 +757,10 @@ pub struct MutableAppContext {
     next_window_id: usize,
     next_subscription_id: usize,
     frame_count: usize,
-    subscriptions: Arc<Mutex<HashMap<usize, BTreeMap<usize, SubscriptionCallback>>>>,
-    observations: Arc<Mutex<HashMap<usize, BTreeMap<usize, ObservationCallback>>>>,
+    subscriptions: Arc<Mutex<HashMap<usize, BTreeMap<usize, Option<SubscriptionCallback>>>>>,
+    global_subscriptions:
+        Arc<Mutex<HashMap<TypeId, BTreeMap<usize, Option<GlobalSubscriptionCallback>>>>>,
+    observations: Arc<Mutex<HashMap<usize, BTreeMap<usize, Option<ObservationCallback>>>>>,
     release_observations: Arc<Mutex<HashMap<usize, BTreeMap<usize, ReleaseObservationCallback>>>>,
     presenters_and_platform_windows:
         HashMap<usize, (Rc<RefCell<Presenter>>, Box<dyn platform::Window>)>,
@@ -804,6 +807,7 @@ impl MutableAppContext {
             next_subscription_id: 0,
             frame_count: 0,
             subscriptions: Default::default(),
+            global_subscriptions: Default::default(),
             observations: Default::default(),
             release_observations: Default::default(),
             presenters_and_platform_windows: HashMap::new(),
@@ -1062,6 +1066,12 @@ impl MutableAppContext {
         self.foreground_platform.prompt_for_new_path(directory)
     }
 
+    pub fn emit_global<E: Any>(&mut self, payload: E) {
+        self.pending_effects.push_back(Effect::GlobalEvent {
+            payload: Box::new(payload),
+        });
+    }
+
     pub fn subscribe<E, H, F>(&mut self, handle: &H, mut callback: F) -> Subscription
     where
         E: Entity,
@@ -1075,6 +1085,31 @@ impl MutableAppContext {
         })
     }
 
+    pub fn subscribe_global<E, F>(&mut self, mut callback: F) -> Subscription
+    where
+        E: Any,
+        F: 'static + FnMut(&E, &mut Self),
+    {
+        let id = post_inc(&mut self.next_subscription_id);
+        let type_id = TypeId::of::<E>();
+        self.global_subscriptions
+            .lock()
+            .entry(type_id)
+            .or_default()
+            .insert(
+                id,
+                Some(Box::new(move |payload, cx| {
+                    let payload = payload.downcast_ref().expect("downcast is type safe");
+                    callback(payload, cx)
+                })),
+            );
+        Subscription::GlobalSubscription {
+            id,
+            type_id,
+            subscriptions: Some(Arc::downgrade(&self.global_subscriptions)),
+        }
+    }
+
     pub fn observe<E, H, F>(&mut self, handle: &H, mut callback: F) -> Subscription
     where
         E: Entity,
@@ -1103,14 +1138,14 @@ impl MutableAppContext {
             .or_default()
             .insert(
                 id,
-                Box::new(move |payload, cx| {
+                Some(Box::new(move |payload, cx| {
                     if let Some(emitter) = H::upgrade_from(&emitter, cx.as_ref()) {
                         let payload = payload.downcast_ref().expect("downcast is type safe");
                         callback(emitter, payload, cx)
                     } else {
                         false
                     }
-                }),
+                })),
             );
         Subscription::Subscription {
             id,
@@ -1134,13 +1169,13 @@ impl MutableAppContext {
             .or_default()
             .insert(
                 id,
-                Box::new(move |cx| {
+                Some(Box::new(move |cx| {
                     if let Some(observed) = H::upgrade_from(&observed, cx) {
                         callback(observed, cx)
                     } else {
                         false
                     }
-                }),
+                })),
             );
         Subscription::Observation {
             id,
@@ -1573,6 +1608,7 @@ impl MutableAppContext {
                 if let Some(effect) = self.pending_effects.pop_front() {
                     match effect {
                         Effect::Event { entity_id, payload } => self.emit_event(entity_id, payload),
+                        Effect::GlobalEvent { payload } => self.emit_global_event(payload),
                         Effect::ModelNotification { model_id } => {
                             self.notify_model_observers(model_id)
                         }
@@ -1687,14 +1723,51 @@ impl MutableAppContext {
     fn emit_event(&mut self, entity_id: usize, payload: Box<dyn Any>) {
         let callbacks = self.subscriptions.lock().remove(&entity_id);
         if let Some(callbacks) = callbacks {
-            for (id, mut callback) in callbacks {
-                let alive = callback(payload.as_ref(), self);
-                if alive {
-                    self.subscriptions
+            for (id, callback) in callbacks {
+                if let Some(mut callback) = callback {
+                    let alive = callback(payload.as_ref(), self);
+                    if alive {
+                        match self
+                            .subscriptions
+                            .lock()
+                            .entry(entity_id)
+                            .or_default()
+                            .entry(id)
+                        {
+                            collections::btree_map::Entry::Vacant(entry) => {
+                                entry.insert(Some(callback));
+                            }
+                            collections::btree_map::Entry::Occupied(entry) => {
+                                entry.remove();
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    fn emit_global_event(&mut self, payload: Box<dyn Any>) {
+        let type_id = (&*payload).type_id();
+        let callbacks = self.global_subscriptions.lock().remove(&type_id);
+        if let Some(callbacks) = callbacks {
+            for (id, callback) in callbacks {
+                if let Some(mut callback) = callback {
+                    callback(payload.as_ref(), self);
+                    match self
+                        .global_subscriptions
                         .lock()
-                        .entry(entity_id)
+                        .entry(type_id)
                         .or_default()
-                        .insert(id, callback);
+                        .entry(id)
+                    {
+                        collections::btree_map::Entry::Vacant(entry) => {
+                            entry.insert(Some(callback));
+                        }
+                        collections::btree_map::Entry::Occupied(entry) => {
+                            entry.remove();
+                        }
+                    }
                 }
             }
         }
@@ -1704,14 +1777,25 @@ impl MutableAppContext {
         let callbacks = self.observations.lock().remove(&observed_id);
         if let Some(callbacks) = callbacks {
             if self.cx.models.contains_key(&observed_id) {
-                for (id, mut callback) in callbacks {
-                    let alive = callback(self);
-                    if alive {
-                        self.observations
-                            .lock()
-                            .entry(observed_id)
-                            .or_default()
-                            .insert(id, callback);
+                for (id, callback) in callbacks {
+                    if let Some(mut callback) = callback {
+                        let alive = callback(self);
+                        if alive {
+                            match self
+                                .observations
+                                .lock()
+                                .entry(observed_id)
+                                .or_default()
+                                .entry(id)
+                            {
+                                collections::btree_map::Entry::Vacant(entry) => {
+                                    entry.insert(Some(callback));
+                                }
+                                collections::btree_map::Entry::Occupied(entry) => {
+                                    entry.remove();
+                                }
+                            }
+                        }
                     }
                 }
             }
@@ -1734,14 +1818,25 @@ impl MutableAppContext {
                 .views
                 .contains_key(&(observed_window_id, observed_view_id))
             {
-                for (id, mut callback) in callbacks {
-                    let alive = callback(self);
-                    if alive {
-                        self.observations
-                            .lock()
-                            .entry(observed_view_id)
-                            .or_default()
-                            .insert(id, callback);
+                for (id, callback) in callbacks {
+                    if let Some(mut callback) = callback {
+                        let alive = callback(self);
+                        if alive {
+                            match self
+                                .observations
+                                .lock()
+                                .entry(observed_view_id)
+                                .or_default()
+                                .entry(id)
+                            {
+                                collections::btree_map::Entry::Vacant(entry) => {
+                                    entry.insert(Some(callback));
+                                }
+                                collections::btree_map::Entry::Occupied(entry) => {
+                                    entry.remove();
+                                }
+                            }
+                        }
                     }
                 }
             }
@@ -2071,6 +2166,9 @@ pub enum Effect {
         entity_id: usize,
         payload: Box<dyn Any>,
     },
+    GlobalEvent {
+        payload: Box<dyn Any>,
+    },
     ModelNotification {
         model_id: usize,
     },
@@ -2104,6 +2202,10 @@ impl Debug for Effect {
                 .debug_struct("Effect::Event")
                 .field("entity_id", entity_id)
                 .finish(),
+            Effect::GlobalEvent { payload, .. } => f
+                .debug_struct("Effect::GlobalEvent")
+                .field("type_id", &(&*payload).type_id())
+                .finish(),
             Effect::ModelNotification { model_id } => f
                 .debug_struct("Effect::ModelNotification")
                 .field("model_id", model_id)
@@ -3760,12 +3862,21 @@ pub enum Subscription {
     Subscription {
         id: usize,
         entity_id: usize,
-        subscriptions: Option<Weak<Mutex<HashMap<usize, BTreeMap<usize, SubscriptionCallback>>>>>,
+        subscriptions:
+            Option<Weak<Mutex<HashMap<usize, BTreeMap<usize, Option<SubscriptionCallback>>>>>>,
+    },
+    GlobalSubscription {
+        id: usize,
+        type_id: TypeId,
+        subscriptions: Option<
+            Weak<Mutex<HashMap<TypeId, BTreeMap<usize, Option<GlobalSubscriptionCallback>>>>>,
+        >,
     },
     Observation {
         id: usize,
         entity_id: usize,
-        observations: Option<Weak<Mutex<HashMap<usize, BTreeMap<usize, ObservationCallback>>>>>,
+        observations:
+            Option<Weak<Mutex<HashMap<usize, BTreeMap<usize, Option<ObservationCallback>>>>>>,
     },
     ReleaseObservation {
         id: usize,
@@ -3781,6 +3892,9 @@ impl Subscription {
             Subscription::Subscription { subscriptions, .. } => {
                 subscriptions.take();
             }
+            Subscription::GlobalSubscription { subscriptions, .. } => {
+                subscriptions.take();
+            }
             Subscription::Observation { observations, .. } => {
                 observations.take();
             }
@@ -3794,36 +3908,72 @@ impl Subscription {
 impl Drop for Subscription {
     fn drop(&mut self) {
         match self {
-            Subscription::Observation {
+            Subscription::Subscription {
                 id,
                 entity_id,
-                observations,
+                subscriptions,
             } => {
-                if let Some(observations) = observations.as_ref().and_then(Weak::upgrade) {
-                    if let Some(observations) = observations.lock().get_mut(entity_id) {
-                        observations.remove(id);
+                if let Some(subscriptions) = subscriptions.as_ref().and_then(Weak::upgrade) {
+                    match subscriptions
+                        .lock()
+                        .entry(*entity_id)
+                        .or_default()
+                        .entry(*id)
+                    {
+                        collections::btree_map::Entry::Vacant(entry) => {
+                            entry.insert(None);
+                        }
+                        collections::btree_map::Entry::Occupied(entry) => {
+                            entry.remove();
+                        }
                     }
                 }
             }
-            Subscription::ReleaseObservation {
+            Subscription::GlobalSubscription {
+                id,
+                type_id,
+                subscriptions,
+            } => {
+                if let Some(subscriptions) = subscriptions.as_ref().and_then(Weak::upgrade) {
+                    match subscriptions.lock().entry(*type_id).or_default().entry(*id) {
+                        collections::btree_map::Entry::Vacant(entry) => {
+                            entry.insert(None);
+                        }
+                        collections::btree_map::Entry::Occupied(entry) => {
+                            entry.remove();
+                        }
+                    }
+                }
+            }
+            Subscription::Observation {
                 id,
                 entity_id,
                 observations,
             } => {
                 if let Some(observations) = observations.as_ref().and_then(Weak::upgrade) {
-                    if let Some(observations) = observations.lock().get_mut(entity_id) {
-                        observations.remove(id);
+                    match observations
+                        .lock()
+                        .entry(*entity_id)
+                        .or_default()
+                        .entry(*id)
+                    {
+                        collections::btree_map::Entry::Vacant(entry) => {
+                            entry.insert(None);
+                        }
+                        collections::btree_map::Entry::Occupied(entry) => {
+                            entry.remove();
+                        }
                     }
                 }
             }
-            Subscription::Subscription {
+            Subscription::ReleaseObservation {
                 id,
                 entity_id,
-                subscriptions,
+                observations,
             } => {
-                if let Some(subscriptions) = subscriptions.as_ref().and_then(Weak::upgrade) {
-                    if let Some(subscriptions) = subscriptions.lock().get_mut(entity_id) {
-                        subscriptions.remove(id);
+                if let Some(observations) = observations.as_ref().and_then(Weak::upgrade) {
+                    if let Some(observations) = observations.lock().get_mut(entity_id) {
+                        observations.remove(id);
                     }
                 }
             }
@@ -4392,6 +4542,98 @@ mod tests {
         assert_eq!(handle_1.read(cx).events, vec![7, 5, 10, 9]);
     }
 
+    #[crate::test(self)]
+    fn test_global_events(cx: &mut MutableAppContext) {
+        #[derive(Clone, Debug, Eq, PartialEq)]
+        struct GlobalEvent(u64);
+
+        let events = Rc::new(RefCell::new(Vec::new()));
+        let first_subscription;
+        let second_subscription;
+
+        {
+            let events = events.clone();
+            first_subscription = cx.subscribe_global(move |e: &GlobalEvent, _| {
+                events.borrow_mut().push(("First", e.clone()));
+            });
+        }
+
+        {
+            let events = events.clone();
+            second_subscription = cx.subscribe_global(move |e: &GlobalEvent, _| {
+                events.borrow_mut().push(("Second", e.clone()));
+            });
+        }
+
+        cx.update(|cx| {
+            cx.emit_global(GlobalEvent(1));
+            cx.emit_global(GlobalEvent(2));
+        });
+
+        drop(first_subscription);
+
+        cx.update(|cx| {
+            cx.emit_global(GlobalEvent(3));
+        });
+
+        drop(second_subscription);
+
+        cx.update(|cx| {
+            cx.emit_global(GlobalEvent(4));
+        });
+
+        assert_eq!(
+            &*events.borrow(),
+            &[
+                ("First", GlobalEvent(1)),
+                ("Second", GlobalEvent(1)),
+                ("First", GlobalEvent(2)),
+                ("Second", GlobalEvent(2)),
+                ("Second", GlobalEvent(3)),
+            ]
+        );
+    }
+
+    #[crate::test(self)]
+    fn test_global_nested_events(cx: &mut MutableAppContext) {
+        #[derive(Clone, Debug, Eq, PartialEq)]
+        struct GlobalEvent(u64);
+
+        let events = Rc::new(RefCell::new(Vec::new()));
+
+        {
+            let events = events.clone();
+            cx.subscribe_global(move |e: &GlobalEvent, cx| {
+                events.borrow_mut().push(("Outer", e.clone()));
+
+                let events = events.clone();
+                cx.subscribe_global(move |e: &GlobalEvent, _| {
+                    events.borrow_mut().push(("Inner", e.clone()));
+                })
+                .detach();
+            })
+            .detach();
+        }
+
+        cx.update(|cx| {
+            cx.emit_global(GlobalEvent(1));
+            cx.emit_global(GlobalEvent(2));
+            cx.emit_global(GlobalEvent(3));
+        });
+
+        assert_eq!(
+            &*events.borrow(),
+            &[
+                ("Outer", GlobalEvent(1)),
+                ("Outer", GlobalEvent(2)),
+                ("Inner", GlobalEvent(2)),
+                ("Outer", GlobalEvent(3)),
+                ("Inner", GlobalEvent(3)),
+                ("Inner", GlobalEvent(3)),
+            ]
+        );
+    }
+
     #[crate::test(self)]
     fn test_dropping_subscribers(cx: &mut MutableAppContext) {
         struct View;
@@ -4530,6 +4772,137 @@ mod tests {
         observed_model.update(cx, |_, cx| cx.notify());
     }
 
+    #[crate::test(self)]
+    fn test_dropping_subscriptions_during_callback(cx: &mut MutableAppContext) {
+        struct Model;
+
+        impl Entity for Model {
+            type Event = u64;
+        }
+
+        // Events
+        let observing_model = cx.add_model(|_| Model);
+        let observed_model = cx.add_model(|_| Model);
+
+        let events = Rc::new(RefCell::new(Vec::new()));
+
+        observing_model.update(cx, |_, cx| {
+            let events = events.clone();
+            let subscription = Rc::new(RefCell::new(None));
+            *subscription.borrow_mut() = Some(cx.subscribe(&observed_model, {
+                let subscription = subscription.clone();
+                move |_, _, e, _| {
+                    subscription.borrow_mut().take();
+                    events.borrow_mut().push(e.clone());
+                }
+            }));
+        });
+
+        observed_model.update(cx, |_, cx| {
+            cx.emit(1);
+            cx.emit(2);
+        });
+
+        assert_eq!(*events.borrow(), [1]);
+
+        // Global Events
+        #[derive(Clone, Debug, Eq, PartialEq)]
+        struct GlobalEvent(u64);
+
+        let events = Rc::new(RefCell::new(Vec::new()));
+
+        {
+            let events = events.clone();
+            let subscription = Rc::new(RefCell::new(None));
+            *subscription.borrow_mut() = Some(cx.subscribe_global({
+                let subscription = subscription.clone();
+                move |e: &GlobalEvent, _| {
+                    subscription.borrow_mut().take();
+                    events.borrow_mut().push(e.clone());
+                }
+            }));
+        }
+
+        cx.update(|cx| {
+            cx.emit_global(GlobalEvent(1));
+            cx.emit_global(GlobalEvent(2));
+        });
+
+        assert_eq!(*events.borrow(), [GlobalEvent(1)]);
+
+        // Model Observation
+        let observing_model = cx.add_model(|_| Model);
+        let observed_model = cx.add_model(|_| Model);
+
+        let observation_count = Rc::new(RefCell::new(0));
+
+        observing_model.update(cx, |_, cx| {
+            let observation_count = observation_count.clone();
+            let subscription = Rc::new(RefCell::new(None));
+            *subscription.borrow_mut() = Some(cx.observe(&observed_model, {
+                let subscription = subscription.clone();
+                move |_, _, _| {
+                    subscription.borrow_mut().take();
+                    *observation_count.borrow_mut() += 1;
+                }
+            }));
+        });
+
+        observed_model.update(cx, |_, cx| {
+            cx.notify();
+        });
+
+        observed_model.update(cx, |_, cx| {
+            cx.notify();
+        });
+
+        assert_eq!(*observation_count.borrow(), 1);
+
+        // View Observation
+        struct View;
+
+        impl Entity for View {
+            type Event = ();
+        }
+
+        impl super::View for View {
+            fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
+                Empty::new().boxed()
+            }
+
+            fn ui_name() -> &'static str {
+                "View"
+            }
+        }
+
+        let (window_id, _) = cx.add_window(Default::default(), |_| View);
+        let observing_view = cx.add_view(window_id, |_| View);
+        let observed_view = cx.add_view(window_id, |_| View);
+
+        let observation_count = Rc::new(RefCell::new(0));
+        observing_view.update(cx, |_, cx| {
+            let observation_count = observation_count.clone();
+            let subscription = Rc::new(RefCell::new(None));
+            *subscription.borrow_mut() = Some(cx.observe(&observed_view, {
+                let subscription = subscription.clone();
+                move |_, _, _| {
+                    subscription.borrow_mut().take();
+                    *observation_count.borrow_mut() += 1;
+                }
+            }));
+        });
+
+        observed_view.update(cx, |_, cx| {
+            cx.notify();
+        });
+
+        observed_view.update(cx, |_, cx| {
+            cx.notify();
+        });
+
+        assert_eq!(*observation_count.borrow(), 1);
+    }
+
     #[crate::test(self)]
     fn test_focus(cx: &mut MutableAppContext) {
         struct View {

crates/gpui/src/color.rs 🔗

@@ -5,7 +5,7 @@ use std::{
 };
 
 use crate::json::ToJson;
-use pathfinder_color::ColorU;
+use pathfinder_color::{ColorF, ColorU};
 use serde::{
     de::{self, Unexpected},
     Deserialize, Deserializer,
@@ -48,6 +48,30 @@ impl Color {
     pub fn from_u32(rgba: u32) -> Self {
         Self(ColorU::from_u32(rgba))
     }
+
+    pub fn blend(source: Color, dest: Color) -> Color {
+        // Skip blending if we don't need it.
+        if source.a == 255 {
+            return source;
+        } else if source.a == 0 {
+            return dest;
+        }
+
+        let source = source.0.to_f32();
+        let dest = dest.0.to_f32();
+
+        let a = source.a() + (dest.a() * (1. - source.a()));
+        let r = ((source.r() * source.a()) + (dest.r() * dest.a() * (1. - source.a()))) / a;
+        let g = ((source.g() * source.a()) + (dest.g() * dest.a() * (1. - source.a()))) / a;
+        let b = ((source.b() * source.a()) + (dest.b() * dest.a() * (1. - source.a()))) / a;
+
+        Self(ColorF::new(r, g, b, a).to_u8())
+    }
+
+    pub fn fade_out(&mut self, fade: f32) {
+        let fade = fade.clamp(0., 1.);
+        self.0.a = (self.0.a as f32 * (1. - fade)) as u8;
+    }
 }
 
 impl<'de> Deserialize<'de> for Color {

crates/gpui/src/elements/text.rs 🔗

@@ -67,12 +67,12 @@ impl Element for Text {
         let mut highlight_ranges = self.highlights.iter().peekable();
         let chunks = std::iter::from_fn(|| {
             let result;
-            if let Some((range, highlight)) = highlight_ranges.peek() {
+            if let Some((range, highlight_style)) = highlight_ranges.peek() {
                 if offset < range.start {
                     result = Some((&self.text[offset..range.start], None));
                     offset = range.start;
                 } else {
-                    result = Some((&self.text[range.clone()], Some(*highlight)));
+                    result = Some((&self.text[range.clone()], Some(*highlight_style)));
                     highlight_ranges.next();
                     offset = range.end;
                 }
@@ -198,23 +198,23 @@ impl Element for Text {
 /// Perform text layout on a series of highlighted chunks of text.
 pub fn layout_highlighted_chunks<'a>(
     chunks: impl Iterator<Item = (&'a str, Option<HighlightStyle>)>,
-    style: &'a TextStyle,
+    text_style: &'a TextStyle,
     text_layout_cache: &'a TextLayoutCache,
     font_cache: &'a Arc<FontCache>,
     max_line_len: usize,
     max_line_count: usize,
 ) -> Vec<Line> {
     let mut layouts = Vec::with_capacity(max_line_count);
-    let mut prev_font_properties = style.font_properties.clone();
-    let mut prev_font_id = style.font_id;
+    let mut prev_font_properties = text_style.font_properties.clone();
+    let mut prev_font_id = text_style.font_id;
     let mut line = String::new();
     let mut styles = Vec::new();
     let mut row = 0;
     let mut line_exceeded_max_len = false;
-    for (chunk, highlight_style) in chunks.chain([("\n", None)]) {
+    for (chunk, highlight_style) in chunks.chain([("\n", Default::default())]) {
         for (ix, mut line_chunk) in chunk.split('\n').enumerate() {
             if ix > 0 {
-                layouts.push(text_layout_cache.layout_str(&line, style.font_size, &styles));
+                layouts.push(text_layout_cache.layout_str(&line, text_style.font_size, &styles));
                 line.clear();
                 styles.clear();
                 row += 1;
@@ -225,15 +225,30 @@ pub fn layout_highlighted_chunks<'a>(
             }
 
             if !line_chunk.is_empty() && !line_exceeded_max_len {
-                let highlight_style = highlight_style.unwrap_or(style.clone().into());
+                let font_properties;
+                let mut color;
+                let underline;
+
+                if let Some(highlight_style) = highlight_style {
+                    font_properties = highlight_style.font_properties;
+                    color = Color::blend(highlight_style.color, text_style.color);
+                    if let Some(fade) = highlight_style.fade_out {
+                        color.fade_out(fade);
+                    }
+                    underline = highlight_style.underline;
+                } else {
+                    font_properties = text_style.font_properties;
+                    color = text_style.color;
+                    underline = None;
+                }
 
                 // Avoid a lookup if the font properties match the previous ones.
-                let font_id = if highlight_style.font_properties == prev_font_properties {
+                let font_id = if font_properties == prev_font_properties {
                     prev_font_id
                 } else {
                     font_cache
-                        .select_font(style.font_family_id, &highlight_style.font_properties)
-                        .unwrap_or(style.font_id)
+                        .select_font(text_style.font_family_id, &font_properties)
+                        .unwrap_or(text_style.font_id)
                 };
 
                 if line.len() + line_chunk.len() > max_line_len {
@@ -250,12 +265,12 @@ pub fn layout_highlighted_chunks<'a>(
                     line_chunk.len(),
                     RunStyle {
                         font_id,
-                        color: highlight_style.color,
-                        underline: highlight_style.underline,
+                        color,
+                        underline,
                     },
                 ));
                 prev_font_id = font_id;
-                prev_font_properties = highlight_style.font_properties;
+                prev_font_properties = font_properties;
             }
         }
     }

crates/gpui/src/fonts.rs 🔗

@@ -31,13 +31,16 @@ pub struct TextStyle {
     pub underline: Option<Underline>,
 }
 
-#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
+#[derive(Copy, Clone, Debug, Default, PartialEq)]
 pub struct HighlightStyle {
     pub color: Color,
     pub font_properties: Properties,
     pub underline: Option<Underline>,
+    pub fade_out: Option<f32>,
 }
 
+impl Eq for HighlightStyle {}
+
 #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
 pub struct Underline {
     pub color: Color,
@@ -83,6 +86,8 @@ struct HighlightStyleJson {
     italic: bool,
     #[serde(default)]
     underline: UnderlineStyleJson,
+    #[serde(default)]
+    fade_out: Option<f32>,
 }
 
 #[derive(Deserialize)]
@@ -131,7 +136,10 @@ impl TextStyle {
         if self.font_properties != style.font_properties {
             self.font_id = font_cache.select_font(self.font_family_id, &style.font_properties)?;
         }
-        self.color = style.color;
+        self.color = Color::blend(style.color, self.color);
+        if let Some(factor) = style.fade_out {
+            self.color.fade_out(factor);
+        }
         self.underline = style.underline;
         Ok(self)
     }
@@ -199,10 +207,17 @@ impl TextStyle {
 
 impl From<TextStyle> for HighlightStyle {
     fn from(other: TextStyle) -> Self {
+        Self::from(&other)
+    }
+}
+
+impl From<&TextStyle> for HighlightStyle {
+    fn from(other: &TextStyle) -> Self {
         Self {
             color: other.color,
             font_properties: other.font_properties,
             underline: other.underline,
+            fade_out: None,
         }
     }
 }
@@ -246,6 +261,26 @@ impl HighlightStyle {
             color: json.color,
             font_properties,
             underline: underline_from_json(json.underline, json.color),
+            fade_out: json.fade_out,
+        }
+    }
+
+    pub fn highlight(&mut self, other: HighlightStyle) {
+        self.color = Color::blend(other.color, self.color);
+        match (other.fade_out, self.fade_out) {
+            (Some(source_fade), None) => self.fade_out = Some(source_fade),
+            (Some(source_fade), Some(dest_fade)) => {
+                let source_alpha = 1. - source_fade;
+                let dest_alpha = 1. - dest_fade;
+                let blended_alpha = source_alpha + (dest_alpha * source_fade);
+                let blended_fade = 1. - blended_alpha;
+                self.fade_out = Some(blended_fade);
+            }
+            _ => {}
+        }
+        self.font_properties = other.font_properties;
+        if other.underline.is_some() {
+            self.underline = other.underline;
         }
     }
 }
@@ -256,6 +291,7 @@ impl From<Color> for HighlightStyle {
             color,
             font_properties: Default::default(),
             underline: None,
+            fade_out: None,
         }
     }
 }
@@ -295,6 +331,7 @@ impl<'de> Deserialize<'de> for HighlightStyle {
                 color: serde_json::from_value(json).map_err(de::Error::custom)?,
                 font_properties: Properties::new(),
                 underline: None,
+                fade_out: None,
             })
         }
     }

crates/gpui/src/text_layout.rs 🔗

@@ -186,7 +186,7 @@ pub struct Run {
     pub glyphs: Vec<Glyph>,
 }
 
-#[derive(Debug)]
+#[derive(Clone, Debug)]
 pub struct Glyph {
     pub id: GlyphId,
     pub position: Vector2F,
@@ -210,10 +210,14 @@ impl Line {
         self.layout.width
     }
 
+    pub fn font_size(&self) -> f32 {
+        self.layout.font_size
+    }
+
     pub fn x_for_index(&self, index: usize) -> f32 {
         for run in &self.layout.runs {
             for glyph in &run.glyphs {
-                if glyph.index == index {
+                if glyph.index >= index {
                     return glyph.position.x();
                 }
             }
@@ -221,6 +225,18 @@ impl Line {
         self.layout.width
     }
 
+    pub fn font_for_index(&self, index: usize) -> Option<FontId> {
+        for run in &self.layout.runs {
+            for glyph in &run.glyphs {
+                if glyph.index >= index {
+                    return Some(run.font_id);
+                }
+            }
+        }
+
+        None
+    }
+
     pub fn index_for_x(&self, x: f32) -> Option<usize> {
         if x >= self.layout.width {
             None

crates/language/src/buffer.rs 🔗

@@ -12,7 +12,7 @@ use crate::{
 use anyhow::{anyhow, Result};
 use clock::ReplicaId;
 use futures::FutureExt as _;
-use gpui::{AppContext, Entity, ModelContext, MutableAppContext, Task};
+use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, MutableAppContext, Task};
 use lazy_static::lazy_static;
 use parking_lot::Mutex;
 use similar::{ChangeTag, TextDiff};
@@ -246,7 +246,8 @@ pub struct BufferChunks<'a> {
 #[derive(Clone, Copy, Debug, Default)]
 pub struct Chunk<'a> {
     pub text: &'a str,
-    pub highlight_id: Option<HighlightId>,
+    pub syntax_highlight_id: Option<HighlightId>,
+    pub highlight_style: Option<HighlightStyle>,
     pub diagnostic: Option<DiagnosticSeverity>,
 }
 
@@ -1728,7 +1729,7 @@ impl BufferSnapshot {
                             offset += chunk.text.len();
                         }
                         let style = chunk
-                            .highlight_id
+                            .syntax_highlight_id
                             .zip(theme)
                             .and_then(|(highlight, theme)| highlight.style(theme));
                         if let Some(style) = style {
@@ -2102,7 +2103,8 @@ impl<'a> Iterator for BufferChunks<'a> {
 
             Some(Chunk {
                 text: slice,
-                highlight_id,
+                syntax_highlight_id: highlight_id,
+                highlight_style: None,
                 diagnostic: self.current_diagnostic_severity(),
             })
         } else {

crates/language/src/highlight_map.rs 🔗

@@ -8,7 +8,7 @@ pub struct HighlightMap(Arc<[HighlightId]>);
 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
 pub struct HighlightId(pub u32);
 
-const DEFAULT_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX);
+const DEFAULT_SYNTAX_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX);
 
 impl HighlightMap {
     pub fn new(capture_names: &[String], theme: &SyntaxTheme) -> Self {
@@ -36,7 +36,7 @@ impl HighlightMap {
                             Some((i, len))
                         })
                         .max_by_key(|(_, len)| *len)
-                        .map_or(DEFAULT_HIGHLIGHT_ID, |(i, _)| HighlightId(i as u32))
+                        .map_or(DEFAULT_SYNTAX_HIGHLIGHT_ID, |(i, _)| HighlightId(i as u32))
                 })
                 .collect(),
         )
@@ -46,7 +46,7 @@ impl HighlightMap {
         self.0
             .get(capture_id as usize)
             .copied()
-            .unwrap_or(DEFAULT_HIGHLIGHT_ID)
+            .unwrap_or(DEFAULT_SYNTAX_HIGHLIGHT_ID)
     }
 }
 
@@ -72,7 +72,7 @@ impl Default for HighlightMap {
 
 impl Default for HighlightId {
     fn default() -> Self {
-        DEFAULT_HIGHLIGHT_ID
+        DEFAULT_SYNTAX_HIGHLIGHT_ID
     }
 }
 

crates/language/src/language.rs 🔗

@@ -516,7 +516,7 @@ impl Language {
             for chunk in BufferChunks::new(text, range, Some(&tree), self.grammar.as_ref(), vec![])
             {
                 let end_offset = offset + chunk.text.len();
-                if let Some(highlight_id) = chunk.highlight_id {
+                if let Some(highlight_id) = chunk.syntax_highlight_id {
                     result.push((offset..end_offset, highlight_id));
                 }
                 offset = end_offset;

crates/language/src/proto.rs 🔗

@@ -4,7 +4,6 @@ use crate::{
 };
 use anyhow::{anyhow, Result};
 use clock::ReplicaId;
-use collections::HashSet;
 use lsp::DiagnosticSeverity;
 use rpc::proto;
 use std::{ops::Range, sync::Arc};
@@ -100,26 +99,6 @@ pub fn serialize_undo_map_entry(
     }
 }
 
-pub fn serialize_buffer_fragment(fragment: &text::Fragment) -> proto::BufferFragment {
-    proto::BufferFragment {
-        replica_id: fragment.insertion_timestamp.replica_id as u32,
-        local_timestamp: fragment.insertion_timestamp.local,
-        lamport_timestamp: fragment.insertion_timestamp.lamport,
-        insertion_offset: fragment.insertion_offset as u32,
-        len: fragment.len as u32,
-        visible: fragment.visible,
-        deletions: fragment
-            .deletions
-            .iter()
-            .map(|clock| proto::VectorClockEntry {
-                replica_id: clock.replica_id as u32,
-                timestamp: clock.value,
-            })
-            .collect(),
-        max_undos: serialize_version(&fragment.max_undos),
-    }
-}
-
 pub fn serialize_selections(selections: &Arc<[Selection<Anchor>]>) -> Vec<proto::Selection> {
     selections
         .iter()
@@ -290,29 +269,6 @@ pub fn deserialize_undo_map_entry(
     )
 }
 
-pub fn deserialize_buffer_fragment(
-    message: proto::BufferFragment,
-    ix: usize,
-    count: usize,
-) -> Fragment {
-    Fragment {
-        id: locator::Locator::from_index(ix, count),
-        insertion_timestamp: InsertionTimestamp {
-            replica_id: message.replica_id as ReplicaId,
-            local: message.local_timestamp,
-            lamport: message.lamport_timestamp,
-        },
-        insertion_offset: message.insertion_offset as usize,
-        len: message.len as usize,
-        visible: message.visible,
-        deletions: HashSet::from_iter(message.deletions.into_iter().map(|entry| clock::Local {
-            replica_id: entry.replica_id as ReplicaId,
-            value: entry.timestamp,
-        })),
-        max_undos: deserialize_version(message.max_undos),
-    }
-}
-
 pub fn deserialize_selections(selections: Vec<proto::Selection>) -> Arc<[Selection<Anchor>]> {
     Arc::from(
         selections

crates/outline/src/outline.rs 🔗

@@ -174,7 +174,7 @@ impl OutlineView {
                 let end = outline_item.range.end.to_point(&buffer_snapshot);
                 let display_rows = start.to_display_point(&snapshot).row()
                     ..end.to_display_point(&snapshot).row() + 1;
-                active_editor.set_highlighted_rows(Some(display_rows));
+                active_editor.highlight_rows(Some(display_rows));
                 active_editor.request_autoscroll(Autoscroll::Center, cx);
             });
         }
@@ -195,7 +195,7 @@ impl OutlineView {
 
     fn restore_active_editor(&mut self, cx: &mut MutableAppContext) {
         self.active_editor.update(cx, |editor, cx| {
-            editor.set_highlighted_rows(None);
+            editor.highlight_rows(None);
             if let Some(scroll_position) = self.prev_scroll_position {
                 editor.set_scroll_position(scroll_position, cx);
             }

crates/search/src/buffer_search.rs 🔗

@@ -149,7 +149,9 @@ impl Toolbar for SearchBar {
         self.dismissed = true;
         for (editor, _) in &self.editors_with_matches {
             if let Some(editor) = editor.upgrade(cx) {
-                editor.update(cx, |editor, cx| editor.clear_highlighted_ranges::<Self>(cx));
+                editor.update(cx, |editor, cx| {
+                    editor.clear_background_highlights::<Self>(cx)
+                });
             }
         }
     }
@@ -388,7 +390,9 @@ impl SearchBar {
                 if Some(&editor) == self.active_editor.as_ref() {
                     active_editor_matches = Some((editor.downgrade(), ranges));
                 } else {
-                    editor.update(cx, |editor, cx| editor.clear_highlighted_ranges::<Self>(cx));
+                    editor.update(cx, |editor, cx| {
+                        editor.clear_background_highlights::<Self>(cx)
+                    });
                 }
             }
         }
@@ -401,7 +405,9 @@ impl SearchBar {
         if let Some(editor) = self.active_editor.as_ref() {
             if query.is_empty() {
                 self.active_match_index.take();
-                editor.update(cx, |editor, cx| editor.clear_highlighted_ranges::<Self>(cx));
+                editor.update(cx, |editor, cx| {
+                    editor.clear_background_highlights::<Self>(cx)
+                });
             } else {
                 let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
                 let query = if self.regex {
@@ -470,7 +476,7 @@ impl SearchBar {
                                     }
 
                                     let theme = &cx.app_state::<Settings>().theme.search;
-                                    editor.highlight_ranges::<Self>(
+                                    editor.highlight_background::<Self>(
                                         ranges,
                                         theme.match_background,
                                         cx,
@@ -547,7 +553,7 @@ mod tests {
         editor.next_notification(&cx).await;
         editor.update(cx, |editor, cx| {
             assert_eq!(
-                editor.all_highlighted_ranges(cx),
+                editor.all_background_highlights(cx),
                 &[
                     (
                         DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
@@ -568,7 +574,7 @@ mod tests {
         editor.next_notification(&cx).await;
         editor.update(cx, |editor, cx| {
             assert_eq!(
-                editor.all_highlighted_ranges(cx),
+                editor.all_background_highlights(cx),
                 &[(
                     DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
                     Color::red(),
@@ -584,7 +590,7 @@ mod tests {
         editor.next_notification(&cx).await;
         editor.update(cx, |editor, cx| {
             assert_eq!(
-                editor.all_highlighted_ranges(cx),
+                editor.all_background_highlights(cx),
                 &[
                     (
                         DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
@@ -625,7 +631,7 @@ mod tests {
         editor.next_notification(&cx).await;
         editor.update(cx, |editor, cx| {
             assert_eq!(
-                editor.all_highlighted_ranges(cx),
+                editor.all_background_highlights(cx),
                 &[
                     (
                         DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),

crates/search/src/project_search.rs 🔗

@@ -553,7 +553,7 @@ impl ProjectSearchView {
                     editor.select_ranges(match_ranges.first().cloned(), Some(Autoscroll::Fit), cx);
                 }
                 let theme = &cx.app_state::<Settings>().theme.search;
-                editor.highlight_ranges::<Self>(match_ranges, theme.match_background, cx);
+                editor.highlight_background::<Self>(match_ranges, theme.match_background, cx);
             });
             if self.query_editor.is_focused(cx) {
                 self.focus_results_editor(cx);
@@ -752,7 +752,7 @@ mod tests {
             assert_eq!(
                 search_view
                     .results_editor
-                    .update(cx, |editor, cx| editor.all_highlighted_ranges(cx)),
+                    .update(cx, |editor, cx| editor.all_background_highlights(cx)),
                 &[
                     (
                         DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35),

crates/sum_tree/src/tree_map.rs 🔗

@@ -31,6 +31,10 @@ impl<K: Clone + Debug + Default + Ord, V: Clone + Debug> TreeMap<K, V> {
         Self(tree)
     }
 
+    pub fn is_empty(&self) -> bool {
+        self.0.is_empty()
+    }
+
     pub fn get<'a>(&self, key: &'a K) -> Option<&V> {
         let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>();
         cursor.seek(&MapKeyRef(Some(key)), Bias::Left, &());

crates/text/src/locator.rs 🔗

@@ -19,11 +19,6 @@ impl Locator {
         Self(smallvec![u64::MAX])
     }
 
-    pub fn from_index(ix: usize, count: usize) -> Self {
-        let id = (1 + ix as u64) * (u64::MAX / (count as u64 + 2));
-        Self(smallvec![id])
-    }
-
     pub fn assign(&mut self, other: &Self) {
         self.0.resize(other.0.len(), 0);
         self.0.copy_from_slice(&other.0);
@@ -54,6 +49,30 @@ impl Default for Locator {
     }
 }
 
+impl sum_tree::Item for Locator {
+    type Summary = Locator;
+
+    fn summary(&self) -> Self::Summary {
+        self.clone()
+    }
+}
+
+impl sum_tree::KeyedItem for Locator {
+    type Key = Locator;
+
+    fn key(&self) -> Self::Key {
+        self.clone()
+    }
+}
+
+impl sum_tree::Summary for Locator {
+    type Context = ();
+
+    fn add_summary(&mut self, summary: &Self, _: &()) {
+        self.assign(summary);
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;

crates/theme/src/theme.rs 🔗

@@ -282,6 +282,7 @@ pub struct Editor {
     pub gutter_padding_factor: f32,
     pub active_line_background: Color,
     pub highlighted_line_background: Color,
+    pub rename_fade: f32,
     pub document_highlight_read_background: Color,
     pub document_highlight_write_background: Color,
     pub diff_background_deleted: Color,

crates/workspace/src/workspace.rs 🔗

@@ -1494,6 +1494,8 @@ fn open(action: &Open, cx: &mut MutableAppContext) {
     .detach();
 }
 
+pub struct WorkspaceCreated(WeakViewHandle<Workspace>);
+
 pub fn open_paths(
     abs_paths: &[PathBuf],
     app_state: &Arc<AppState>,
@@ -1520,7 +1522,7 @@ pub fn open_paths(
     }
 
     let workspace = existing.unwrap_or_else(|| {
-        cx.add_window((app_state.build_window_options)(), |cx| {
+        let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
             let project = Project::local(
                 app_state.client.clone(),
                 app_state.user_store.clone(),
@@ -1529,8 +1531,9 @@ pub fn open_paths(
                 cx,
             );
             (app_state.build_workspace)(project, &app_state, cx)
-        })
-        .1
+        });
+        cx.emit_global(WorkspaceCreated(workspace.downgrade()));
+        workspace
     });
 
     let task = workspace.update(cx, |workspace, cx| workspace.open_paths(abs_paths, cx));
@@ -1564,12 +1567,13 @@ pub fn join_project(
             &mut cx,
         )
         .await?;
-        let (_, workspace) = cx.update(|cx| {
-            cx.add_window((app_state.build_window_options)(), |cx| {
+        Ok(cx.update(|cx| {
+            let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
                 (app_state.build_workspace)(project, &app_state, cx)
-            })
-        });
-        Ok(workspace)
+            });
+            cx.emit_global(WorkspaceCreated(workspace.downgrade()));
+            workspace
+        }))
     })
 }
 
@@ -1584,5 +1588,6 @@ fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
         );
         (app_state.build_workspace)(project, &app_state, cx)
     });
+    cx.emit_global(WorkspaceCreated(workspace.downgrade()));
     cx.dispatch_action(window_id, vec![workspace.id()], &OpenNew(app_state.clone()));
 }

crates/zed/assets/themes/_base.toml 🔗

@@ -249,6 +249,7 @@ gutter_background = "$surface.1"
 gutter_padding_factor = 2.5
 active_line_background = "$state.active_line"
 highlighted_line_background = "$state.highlighted_line"
+rename_fade = 0.6
 document_highlight_read_background = "#99999920"
 document_highlight_write_background = "#99999916"
 diff_background_deleted = "$state.deleted_line"