diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 27a8c79ba3..c57bbf0aed 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -9,12 +9,13 @@ mod test; use aho_corasick::AhoCorasick; use clock::ReplicaId; -use collections::{HashMap, HashSet}; +use collections::{BTreeMap, HashMap, HashSet}; pub use display_map::DisplayPoint; use display_map::*; pub use element::*; use gpui::{ action, + color::Color, elements::*, fonts::TextStyle, geometry::vector::{vec2f, Vector2F}, @@ -37,7 +38,8 @@ use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use smol::Timer; use std::{ - cmp, + any::TypeId, + cmp::{self, Ordering}, iter::{self, FromIterator}, mem, ops::{Deref, Range, RangeInclusive, Sub}, @@ -382,6 +384,7 @@ pub struct Editor { vertical_scroll_margin: f32, placeholder_text: Option>, highlighted_rows: Option>, + highlighted_ranges: BTreeMap>)>, nav_history: Option, } @@ -522,6 +525,7 @@ impl Editor { vertical_scroll_margin: 3.0, placeholder_text: None, highlighted_rows: None, + highlighted_ranges: Default::default(), nav_history: None, }; let selection = Selection { @@ -3721,6 +3725,58 @@ impl Editor { self.highlighted_rows.clone() } + pub fn highlight_ranges( + &mut self, + ranges: Vec>, + color: Color, + cx: &mut ViewContext, + ) { + self.highlighted_ranges + .insert(TypeId::of::(), (color, ranges)); + cx.notify(); + } + + pub fn clear_highlighted_ranges(&mut self, cx: &mut ViewContext) { + self.highlighted_ranges.remove(&TypeId::of::()); + cx.notify(); + } + + pub fn highlighted_ranges_in_range( + &self, + search_range: Range, + display_snapshot: &DisplaySnapshot, + ) -> Vec<(Color, Range)> { + let mut results = Vec::new(); + let buffer = &display_snapshot.buffer_snapshot; + for (color, ranges) in self.highlighted_ranges.values() { + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = probe.end.cmp(&search_range.start, &buffer).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(&search_range.end, &buffer).unwrap().is_ge() { + break; + } + let start = range + .start + .to_point(buffer) + .to_display_point(display_snapshot); + let end = range + .end + .to_point(buffer) + .to_display_point(display_snapshot); + results.push((*color, start..end)) + } + } + results + } + fn next_blink_epoch(&mut self) -> usize { self.blink_epoch += 1; self.blink_epoch @@ -6555,6 +6611,83 @@ mod tests { }); } + #[gpui::test] + fn test_highlighted_ranges(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); + let settings = EditorSettings::test(&cx); + let (_, editor) = cx.add_window(Default::default(), |cx| { + build_editor(buffer.clone(), settings, cx) + }); + + editor.update(cx, |editor, cx| { + struct Type1; + struct Type2; + + let buffer = buffer.read(cx).snapshot(cx); + + let anchor_range = |range: Range| { + buffer.anchor_after(range.start)..buffer.anchor_after(range.end) + }; + + editor.highlight_ranges::( + vec![ + anchor_range(Point::new(2, 1)..Point::new(2, 3)), + anchor_range(Point::new(4, 2)..Point::new(4, 4)), + anchor_range(Point::new(6, 3)..Point::new(6, 5)), + anchor_range(Point::new(8, 4)..Point::new(8, 6)), + ], + Color::red(), + cx, + ); + editor.highlight_ranges::( + vec![ + anchor_range(Point::new(3, 2)..Point::new(3, 5)), + anchor_range(Point::new(5, 3)..Point::new(5, 6)), + anchor_range(Point::new(7, 4)..Point::new(7, 7)), + anchor_range(Point::new(9, 5)..Point::new(9, 8)), + ], + Color::green(), + cx, + ); + + let snapshot = editor.snapshot(cx); + assert_eq!( + editor.highlighted_ranges_in_range( + anchor_range(Point::new(3, 4)..Point::new(7, 4)), + &snapshot, + ), + &[ + ( + Color::red(), + DisplayPoint::new(4, 2)..DisplayPoint::new(4, 4), + ), + ( + Color::red(), + DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), + ), + ( + Color::green(), + DisplayPoint::new(3, 2)..DisplayPoint::new(3, 5) + ), + ( + Color::green(), + DisplayPoint::new(5, 3)..DisplayPoint::new(5, 6) + ), + ] + ); + assert_eq!( + editor.highlighted_ranges_in_range( + anchor_range(Point::new(5, 6)..Point::new(6, 4)), + &snapshot, + ), + &[( + Color::red(), + DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), + ),] + ); + }); + } + fn empty_range(row: usize, column: usize) -> Range { let point = DisplayPoint::new(row as u32, column as u32); point..point diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index f1af1b4ce8..1b1ae46491 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -312,64 +312,45 @@ impl EditorElement { let end_row = ((scroll_top + bounds.height()) / layout.line_height).ceil() as u32 + 1; // Add 1 to ensure selections bleed off screen let max_glyph_width = layout.em_width; let scroll_left = scroll_position.x() * max_glyph_width; + let content_origin = bounds.origin() + layout.text_offset; cx.scene.push_layer(Some(bounds)); - // Draw selections - let corner_radius = 2.5; + for (color, range) in &layout.highlighted_ranges { + self.paint_highlighted_range( + range.clone(), + start_row, + end_row, + *color, + 0., + layout, + content_origin, + scroll_top, + scroll_left, + bounds, + cx, + ); + } + let mut cursors = SmallVec::<[Cursor; 32]>::new(); - - let content_origin = bounds.origin() + layout.text_offset; - for (replica_id, selections) in &layout.selections { let style = style.replica_selection_style(*replica_id); + let corner_radius = 0.15 * layout.line_height; for selection in selections { - if selection.start != selection.end { - let row_range = if selection.end.column() == 0 { - cmp::max(selection.start.row(), start_row) - ..cmp::min(selection.end.row(), end_row) - } else { - cmp::max(selection.start.row(), start_row) - ..cmp::min(selection.end.row() + 1, end_row) - }; - - let selection = Selection { - color: style.selection, - line_height: layout.line_height, - start_y: content_origin.y() + row_range.start as f32 * layout.line_height - - scroll_top, - lines: row_range - .into_iter() - .map(|row| { - let line_layout = &layout.line_layouts[(row - start_row) as usize]; - SelectionLine { - start_x: if row == selection.start.row() { - content_origin.x() - + line_layout - .x_for_index(selection.start.column() as usize) - - scroll_left - } else { - content_origin.x() - scroll_left - }, - end_x: if row == selection.end.row() { - content_origin.x() - + line_layout - .x_for_index(selection.end.column() as usize) - - scroll_left - } else { - content_origin.x() - + line_layout.width() - + corner_radius * 2.0 - - scroll_left - }, - } - }) - .collect(), - }; - - selection.paint(bounds, cx.scene); - } + self.paint_highlighted_range( + selection.start..selection.end, + start_row, + end_row, + style.selection, + corner_radius, + layout, + content_origin, + scroll_top, + scroll_left, + bounds, + cx, + ); if view.show_local_cursors() || *replica_id != local_replica_id { let cursor_position = selection.head(); @@ -412,6 +393,62 @@ impl EditorElement { cx.scene.pop_layer(); } + fn paint_highlighted_range( + &self, + range: Range, + start_row: u32, + end_row: u32, + color: Color, + corner_radius: f32, + layout: &LayoutState, + content_origin: Vector2F, + scroll_top: f32, + scroll_left: f32, + bounds: RectF, + cx: &mut PaintContext, + ) { + if range.start != range.end { + let row_range = if range.end.column() == 0 { + cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row) + } else { + cmp::max(range.start.row(), start_row)..cmp::min(range.end.row() + 1, end_row) + }; + + let selection = HighlightedRange { + color, + line_height: layout.line_height, + corner_radius, + start_y: content_origin.y() + row_range.start as f32 * layout.line_height + - scroll_top, + lines: row_range + .into_iter() + .map(|row| { + let line_layout = &layout.line_layouts[(row - start_row) as usize]; + HighlightedRangeLine { + start_x: if row == range.start.row() { + content_origin.x() + + line_layout.x_for_index(range.start.column() as usize) + - scroll_left + } else { + content_origin.x() - scroll_left + }, + end_x: if row == range.end.row() { + content_origin.x() + + line_layout.x_for_index(range.end.column() as usize) + - scroll_left + } else { + content_origin.x() + line_layout.width() + corner_radius * 2.0 + - scroll_left + }, + } + }) + .collect(), + }; + + selection.paint(bounds, cx.scene); + } + } + fn paint_blocks( &mut self, bounds: RectF, @@ -715,10 +752,16 @@ impl Element for EditorElement { let mut selections = HashMap::default(); let mut active_rows = BTreeMap::new(); let mut highlighted_rows = None; + let mut highlighted_ranges = Vec::new(); self.update_view(cx.app, |view, cx| { - highlighted_rows = view.highlighted_rows(); 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( + 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 { @@ -837,6 +880,7 @@ impl Element for EditorElement { snapshot, active_rows, highlighted_rows, + highlighted_ranges, line_layouts, line_number_layouts, blocks, @@ -950,6 +994,7 @@ pub struct LayoutState { line_height: f32, em_width: f32, em_advance: f32, + highlighted_ranges: Vec<(Color, Range)>, selections: HashMap>>, text_offset: Vector2F, } @@ -1036,20 +1081,21 @@ impl Cursor { } #[derive(Debug)] -struct Selection { +struct HighlightedRange { start_y: f32, line_height: f32, - lines: Vec, + lines: Vec, color: Color, + corner_radius: f32, } #[derive(Debug)] -struct SelectionLine { +struct HighlightedRangeLine { start_x: f32, end_x: f32, } -impl Selection { +impl HighlightedRange { fn paint(&self, bounds: RectF, scene: &mut Scene) { if self.lines.len() >= 2 && self.lines[0].start_x > self.lines[1].end_x { self.paint_lines(self.start_y, &self.lines[0..1], bounds, scene); @@ -1064,26 +1110,31 @@ impl Selection { } } - fn paint_lines(&self, start_y: f32, lines: &[SelectionLine], bounds: RectF, scene: &mut Scene) { + fn paint_lines( + &self, + start_y: f32, + lines: &[HighlightedRangeLine], + bounds: RectF, + scene: &mut Scene, + ) { if lines.is_empty() { return; } let mut path = PathBuilder::new(); - let corner_radius = 0.15 * self.line_height; let first_line = lines.first().unwrap(); let last_line = lines.last().unwrap(); let first_top_left = vec2f(first_line.start_x, start_y); let first_top_right = vec2f(first_line.end_x, start_y); - let curve_height = vec2f(0., corner_radius); + let curve_height = vec2f(0., self.corner_radius); let curve_width = |start_x: f32, end_x: f32| { let max = (end_x - start_x) / 2.; - let width = if max < corner_radius { + let width = if max < self.corner_radius { max } else { - corner_radius + self.corner_radius }; vec2f(width, 0.) @@ -1107,26 +1158,38 @@ impl Selection { Ordering::Less => { let curve_width = curve_width(next_top_right.x(), bottom_right.x()); path.line_to(bottom_right - curve_height); - path.curve_to(bottom_right - curve_width, bottom_right); + if self.corner_radius > 0. { + path.curve_to(bottom_right - curve_width, bottom_right); + } path.line_to(next_top_right + curve_width); - path.curve_to(next_top_right + curve_height, next_top_right); + if self.corner_radius > 0. { + path.curve_to(next_top_right + curve_height, next_top_right); + } } Ordering::Greater => { let curve_width = curve_width(bottom_right.x(), next_top_right.x()); path.line_to(bottom_right - curve_height); - path.curve_to(bottom_right + curve_width, bottom_right); + if self.corner_radius > 0. { + path.curve_to(bottom_right + curve_width, bottom_right); + } path.line_to(next_top_right - curve_width); - path.curve_to(next_top_right + curve_height, next_top_right); + if self.corner_radius > 0. { + path.curve_to(next_top_right + curve_height, next_top_right); + } } } } else { let curve_width = curve_width(line.start_x, line.end_x); path.line_to(bottom_right - curve_height); - path.curve_to(bottom_right - curve_width, bottom_right); + if self.corner_radius > 0. { + path.curve_to(bottom_right - curve_width, bottom_right); + } let bottom_left = vec2f(line.start_x, bottom_right.y()); path.line_to(bottom_left + curve_width); - path.curve_to(bottom_left - curve_height, bottom_left); + if self.corner_radius > 0. { + path.curve_to(bottom_left - curve_height, bottom_left); + } } } @@ -1134,14 +1197,20 @@ impl Selection { let curve_width = curve_width(last_line.start_x, first_line.start_x); let second_top_left = vec2f(last_line.start_x, start_y + self.line_height); path.line_to(second_top_left + curve_height); - path.curve_to(second_top_left + curve_width, second_top_left); + if self.corner_radius > 0. { + path.curve_to(second_top_left + curve_width, second_top_left); + } let first_bottom_left = vec2f(first_line.start_x, second_top_left.y()); path.line_to(first_bottom_left - curve_width); - path.curve_to(first_bottom_left - curve_height, first_bottom_left); + if self.corner_radius > 0. { + path.curve_to(first_bottom_left - curve_height, first_bottom_left); + } } path.line_to(first_top_left + curve_height); - path.curve_to(first_top_left + top_curve_width, first_top_left); + if self.corner_radius > 0. { + path.curve_to(first_top_left + top_curve_width, first_top_left); + } path.line_to(first_top_right - top_curve_width); scene.push_path(path.build(self.color, Some(bounds)));