diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index d36f571308..5e18ad0b64 100644 --- a/crates/editor/src/display_map.rs +++ b/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, Arc<(HighlightStyle, Vec>)>>; + pub struct DisplayMap { buffer: ModelHandle, buffer_subscription: BufferSubscription, @@ -30,6 +35,7 @@ pub struct DisplayMap { tab_map: TabMap, wrap_map: ModelHandle, 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>, + style: HighlightStyle, + ) { + self.text_highlights + .insert(Some(type_id), Arc::new((style, ranges))); + } + + pub fn clear_text_highlights( + &mut self, + type_id: TypeId, + ) -> Option>)>> { + self.text_highlights.remove(&Some(type_id)) + } + pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext) { 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 { 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, 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 + 'a { @@ -1146,7 +1173,7 @@ mod tests { let mut chunks: Vec<(String, Option)> = 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 { diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 465e46af21..770ebc8fbe 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/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, language_aware: bool) -> BlockChunks<'a> { + pub fn chunks<'a>( + &'a self, + rows: Range, + 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::(); assert_eq!( diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index daafbee57b..8c315c84ed 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/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 { 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, language_aware: bool) -> FoldChunks<'a> { + pub fn chunks<'a>( + &'a self, + range: Range, + 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 + }; - 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; + 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 + }; + + 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>, + active_highlights: BTreeMap, 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..]; + 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); - // 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]; + 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, + style: HighlightStyle, +} + +impl PartialOrd for HighlightEndpoint { + fn partial_cmp(&self, other: &Self) -> Option { + 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::>(); + 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::>(); + + 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::(), text, diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index dc62783eb3..de76a6f261 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/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, language_aware: bool) -> TabChunks<'a> { + pub fn chunks<'a>( + &'a self, + range: Range, + 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::(), "chunks({:?}..{:?})", diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index 7858197902..2fab37fb30 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/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::::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 { - 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, language_aware: bool) -> WrapChunks<'a> { + pub fn chunks<'a>( + &'a self, + rows: Range, + 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::(); assert_eq!( diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d02ddf7162..1a1bf7cc66 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -133,6 +133,9 @@ action!(ConfirmCompletion, Option); action!(ConfirmCodeAction, Option); action!(OpenExcerpts); +enum DocumentHighlightRead {} +enum DocumentHighlightWrite {} + pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec>) { 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; + pub struct Editor { handle: WeakViewHandle, buffer: ModelHandle, @@ -429,16 +434,18 @@ pub struct Editor { autoscroll_request: Option, soft_wrap_mode_override: Option, get_field_editor_theme: Option, + override_text_style: Option>, project: Option>, 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>, highlighted_rows: Option>, - highlighted_ranges: BTreeMap>)>, + background_highlights: BTreeMap>)>, nav_history: Option, context_menu: Option, completion_tasks: Vec<(CompletionId, Task>)>, @@ -448,6 +455,7 @@ pub struct Editor { document_highlights_task: Option>, pending_rename: Option, 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::(); - 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::(), self.get_field_editor_theme, cx) + build_style( + cx.app_state::(), + 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.cursor_shape = cursor_shape; + cx.notify(); + } + pub fn scroll_position(&self, cx: &mut ViewContext) -> 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) { - 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::(cx) { editor.update(cx, |editor, cx| { let color = editor.style(cx).highlighted_line_background; - editor.highlight_ranges::(ranges_to_highlight, color, cx); + editor.highlight_background::(ranges_to_highlight, color, cx); }); } }); @@ -2374,6 +2395,10 @@ impl Editor { } fn refresh_document_highlights(&mut self, cx: &mut ViewContext) -> 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::( + this.highlight_background::( read_ranges, read_background, cx, ); - this.highlight_ranges::( + this.highlight_background::( write_ranges, write_background, cx, @@ -3340,7 +3366,7 @@ impl Editor { } pub fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext) { - 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.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::(cx) { editor.update(cx, |editor, cx| { let color = editor.style(cx).highlighted_line_background; - editor.highlight_ranges::(ranges_to_highlight, color, cx); + editor.highlight_background::(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::(); + .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::( - vec![Anchor::min()..Anchor::max()], - style.diff_background_inserted, - cx, - ); + editor.select_all(&SelectAll, cx); editor }); - this.highlight_ranges::( - 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::(cx) + .into_iter() + .flat_map(|(_, ranges)| ranges) + .chain( + this.clear_background_highlights::(cx) + .into_iter() + .flat_map(|(_, ranges)| ranges), + ) + .collect(); + this.highlight_text::( + 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::(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) -> Option { + fn take_rename( + &mut self, + moving_cursor: bool, + cx: &mut ViewContext, + ) -> Option { let rename = self.pending_rename.take()?; self.remove_blocks([rename.block_id].into_iter().collect(), cx); - self.clear_highlighted_ranges::(cx); + self.clear_text_highlights::(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::(&snapshot); + if moving_cursor { + let cursor_in_rename_editor = + rename.editor.read(cx).newest_selection::(cx).head(); - // 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, - ); + // 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::(cx); + self.clear_background_highlights::(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>) { + pub fn highlight_rows(&mut self, rows: Option>) { self.highlighted_rows = rows; } @@ -5264,27 +5305,27 @@ impl Editor { self.highlighted_rows.clone() } - pub fn highlight_ranges( + pub fn highlight_background( &mut self, ranges: Vec>, color: Color, cx: &mut ViewContext, ) { - self.highlighted_ranges + self.background_highlights .insert(TypeId::of::(), (color, ranges)); cx.notify(); } - pub fn clear_highlighted_ranges( + pub fn clear_background_highlights( &mut self, cx: &mut ViewContext, ) -> Option<(Color, Vec>)> { cx.notify(); - self.highlighted_ranges.remove(&TypeId::of::()) + self.background_highlights.remove(&TypeId::of::()) } #[cfg(feature = "test-support")] - pub fn all_highlighted_ranges( + pub fn all_background_highlights( &mut self, cx: &mut ViewContext, ) -> Vec<(Range, 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(&self) -> Option<(Color, &[Range])> { - self.highlighted_ranges + pub fn background_highlights_for_type(&self) -> Option<(Color, &[Range])> { + self.background_highlights .get(&TypeId::of::()) .map(|(color, ranges)| (*color, ranges.as_slice())) } - pub fn highlighted_ranges_in_range( + pub fn background_highlights_in_range( &self, search_range: Range, display_snapshot: &DisplaySnapshot, ) -> Vec<(Range, 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( + &mut self, + ranges: Vec>, + style: HighlightStyle, + cx: &mut ViewContext, + ) { + self.display_map.update(cx, |map, _| { + map.highlight_text(TypeId::of::(), ranges, style) + }); + cx.notify(); + } + + pub fn clear_text_highlights( + &mut self, + cx: &mut ViewContext, + ) -> Option>)>> { + cx.notify(); + self.display_map + .update(cx, |map, _| map.clear_text_highlights(TypeId::of::())) + } + 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.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.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, + 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 SelectionExt for Selection { @@ -8841,7 +8918,7 @@ mod tests { buffer.anchor_after(range.start)..buffer.anchor_after(range.end) }; - editor.highlight_ranges::( + editor.highlight_background::( 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::( + editor.highlight_background::( 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, ), diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 9ea0b4bcda..aa2c4fc155 100644 --- a/crates/editor/src/element.rs +++ b/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, style: EditorStyle, + cursor_shape: CursorShape, } impl EditorElement { - pub fn new(view: WeakViewHandle, style: EditorStyle) -> Self { - Self { view, style } + pub fn new( + view: WeakViewHandle, + 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 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 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 { - Some(HighlightStyle { - underline, - color: style.text.color, - font_properties: style.text.font_properties, - }) + highlight_style = Some(chunk_highlight); } - } else { - highlight_style - }; - (chunk.text, highlight) + } + + if let Some(severity) = chunk.diagnostic { + let diagnostic_style = super::diagnostic_style(severity, true, style); + 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 { + highlight_style = Some(diagnostic_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, } 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); diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 3678f8f116..fa35ad206b 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -36,6 +36,7 @@ pub type ExcerptId = Locator; pub struct MultiBuffer { snapshot: RefCell, buffers: RefCell>, + used_excerpt_ids: SumTree, 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 { - 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 { - 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::>(); 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::(); + 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, 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> { @@ -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 { + 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 { + 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::(&excerpt.buffer); let excerpt_buffer_end = excerpt.range.end.summary::(&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::(&excerpt.buffer); let excerpt_buffer_end = excerpt.range.end.summary::(&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::>(); 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 { + 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 { 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::(&anchors), - &[0, 2, 9, 13] + &[0, 2, 5, 13] ); let new_anchors = snapshot_3.refresh_anchors(&anchors); diff --git a/crates/editor/src/multi_buffer/anchor.rs b/crates/editor/src/multi_buffer/anchor.rs index c51eb2a4c7..33147ce285 100644 --- a/crates/editor/src/multi_buffer/anchor.rs +++ b/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() diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 5d416c0e56..0f21f268e1 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/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); } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index e91963bfa6..89cf9afba2 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -740,6 +740,7 @@ type ActionCallback = type GlobalActionCallback = dyn FnMut(&dyn AnyAction, &mut MutableAppContext); type SubscriptionCallback = Box bool>; +type GlobalSubscriptionCallback = Box; type ObservationCallback = Box bool>; type ReleaseObservationCallback = Box; @@ -756,8 +757,10 @@ pub struct MutableAppContext { next_window_id: usize, next_subscription_id: usize, frame_count: usize, - subscriptions: Arc>>>, - observations: Arc>>>, + subscriptions: Arc>>>>, + global_subscriptions: + Arc>>>>, + observations: Arc>>>>, release_observations: Arc>>>, presenters_and_platform_windows: HashMap>, Box)>, @@ -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(&mut self, payload: E) { + self.pending_effects.push_back(Effect::GlobalEvent { + payload: Box::new(payload), + }); + } + pub fn subscribe(&mut self, handle: &H, mut callback: F) -> Subscription where E: Entity, @@ -1075,6 +1085,31 @@ impl MutableAppContext { }) } + pub fn subscribe_global(&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::(); + 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(&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) { 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) { + 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, }, + GlobalEvent { + payload: Box, + }, 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>>>>, + subscriptions: + Option>>>>>, + }, + GlobalSubscription { + id: usize, + type_id: TypeId, + subscriptions: Option< + Weak>>>>, + >, }, Observation { id: usize, entity_id: usize, - observations: Option>>>>, + observations: + Option>>>>>, }, 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,14 +3908,61 @@ impl Subscription { impl Drop for Subscription { fn drop(&mut self) { match self { + Subscription::Subscription { + id, + entity_id, + subscriptions, + } => { + 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::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(); + } } } } @@ -3816,17 +3977,6 @@ impl Drop for Subscription { } } } - Subscription::Subscription { - id, - entity_id, - subscriptions, - } => { - if let Some(subscriptions) = subscriptions.as_ref().and_then(Weak::upgrade) { - if let Some(subscriptions) = subscriptions.lock().get_mut(entity_id) { - subscriptions.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) -> 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 { diff --git a/crates/gpui/src/color.rs b/crates/gpui/src/color.rs index 4c301b4e48..f31a80a831 100644 --- a/crates/gpui/src/color.rs +++ b/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 { diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index 7c983f1e6f..707bad55e6 100644 --- a/crates/gpui/src/elements/text.rs +++ b/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)>, - style: &'a TextStyle, + text_style: &'a TextStyle, text_layout_cache: &'a TextLayoutCache, font_cache: &'a Arc, max_line_len: usize, max_line_count: usize, ) -> Vec { 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; } } } diff --git a/crates/gpui/src/fonts.rs b/crates/gpui/src/fonts.rs index 7dc1173048..c0995b6808 100644 --- a/crates/gpui/src/fonts.rs +++ b/crates/gpui/src/fonts.rs @@ -31,13 +31,16 @@ pub struct TextStyle { pub underline: Option, } -#[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, + pub fade_out: Option, } +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, } #[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 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 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, }) } } diff --git a/crates/gpui/src/text_layout.rs b/crates/gpui/src/text_layout.rs index 6e371437bf..25f1cb82c0 100644 --- a/crates/gpui/src/text_layout.rs +++ b/crates/gpui/src/text_layout.rs @@ -186,7 +186,7 @@ pub struct Run { pub glyphs: Vec, } -#[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 { + 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 { if x >= self.layout.width { None diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 5f3ddb8b99..98ba799081 100644 --- a/crates/language/src/buffer.rs +++ b/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, + pub syntax_highlight_id: Option, + pub highlight_style: Option, pub diagnostic: Option, } @@ -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 { diff --git a/crates/language/src/highlight_map.rs b/crates/language/src/highlight_map.rs index 75e3c8526c..74734fbc5b 100644 --- a/crates/language/src/highlight_map.rs +++ b/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 } } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 8c1a34c010..f2e4fd1aa1 100644 --- a/crates/language/src/language.rs +++ b/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; diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index f5e797bca7..19bfc10cd0 100644 --- a/crates/language/src/proto.rs +++ b/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]>) -> Vec { 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) -> Arc<[Selection]> { Arc::from( selections diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 89c3e41cbb..2b4bafa479 100644 --- a/crates/outline/src/outline.rs +++ b/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); } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index b3269023c8..24b438c7e4 100644 --- a/crates/search/src/buffer_search.rs +++ b/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::(cx)); + editor.update(cx, |editor, cx| { + editor.clear_background_highlights::(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::(cx)); + editor.update(cx, |editor, cx| { + editor.clear_background_highlights::(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::(cx)); + editor.update(cx, |editor, cx| { + editor.clear_background_highlights::(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::().theme.search; - editor.highlight_ranges::( + editor.highlight_background::( 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), diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index e0f02edbc8..b09c88a4a0 100644 --- a/crates/search/src/project_search.rs +++ b/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::().theme.search; - editor.highlight_ranges::(match_ranges, theme.match_background, cx); + editor.highlight_background::(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), diff --git a/crates/sum_tree/src/tree_map.rs b/crates/sum_tree/src/tree_map.rs index 1de6b2f589..80143aad69 100644 --- a/crates/sum_tree/src/tree_map.rs +++ b/crates/sum_tree/src/tree_map.rs @@ -31,6 +31,10 @@ impl TreeMap { 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::>(); cursor.seek(&MapKeyRef(Some(key)), Bias::Left, &()); diff --git a/crates/text/src/locator.rs b/crates/text/src/locator.rs index cb1e78153c..f98f050339 100644 --- a/crates/text/src/locator.rs +++ b/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::*; diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index c96db5aab2..d372c2fd31 100644 --- a/crates/theme/src/theme.rs +++ b/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, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 69b6ae643f..5ea664bf5d 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1494,6 +1494,8 @@ fn open(action: &Open, cx: &mut MutableAppContext) { .detach(); } +pub struct WorkspaceCreated(WeakViewHandle); + pub fn open_paths( abs_paths: &[PathBuf], app_state: &Arc, @@ -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, 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())); } diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 59a488c896..97c3c55fcb 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/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"