mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-24 17:28:40 +00:00
Add highlighted_ranges API to editor
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
parent
d8e4464a89
commit
34ed734749
2 changed files with 273 additions and 71 deletions
|
@ -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<Arc<str>>,
|
||||
highlighted_rows: Option<Range<u32>>,
|
||||
highlighted_ranges: BTreeMap<TypeId, (Color, Vec<Range<Anchor>>)>,
|
||||
nav_history: Option<ItemNavHistory>,
|
||||
}
|
||||
|
||||
|
@ -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<T: 'static>(
|
||||
&mut self,
|
||||
ranges: Vec<Range<Anchor>>,
|
||||
color: Color,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.highlighted_ranges
|
||||
.insert(TypeId::of::<T>(), (color, ranges));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn clear_highlighted_ranges<T: 'static>(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.highlighted_ranges.remove(&TypeId::of::<T>());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn highlighted_ranges_in_range(
|
||||
&self,
|
||||
search_range: Range<Anchor>,
|
||||
display_snapshot: &DisplaySnapshot,
|
||||
) -> Vec<(Color, Range<DisplayPoint>)> {
|
||||
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<Point>| {
|
||||
buffer.anchor_after(range.start)..buffer.anchor_after(range.end)
|
||||
};
|
||||
|
||||
editor.highlight_ranges::<Type1>(
|
||||
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::<Type2>(
|
||||
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<DisplayPoint> {
|
||||
let point = DisplayPoint::new(row as u32, column as u32);
|
||||
point..point
|
||||
|
|
|
@ -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<DisplayPoint>,
|
||||
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<DisplayPoint>)>,
|
||||
selections: HashMap<ReplicaId, Vec<text::Selection<DisplayPoint>>>,
|
||||
text_offset: Vector2F,
|
||||
}
|
||||
|
@ -1036,20 +1081,21 @@ impl Cursor {
|
|||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Selection {
|
||||
struct HighlightedRange {
|
||||
start_y: f32,
|
||||
line_height: f32,
|
||||
lines: Vec<SelectionLine>,
|
||||
lines: Vec<HighlightedRangeLine>,
|
||||
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)));
|
||||
|
|
Loading…
Reference in a new issue