Detailed changes
@@ -7,6 +7,8 @@ use clock::ReplicaId;
use collections::{BTreeMap, HashMap};
use gpui::{
color::Color,
+ elements::layout_highlighted_chunks,
+ fonts::HighlightStyle,
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
@@ -19,7 +21,7 @@ use gpui::{
MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle,
};
use json::json;
-use language::{Bias, Chunk};
+use language::Bias;
use smallvec::SmallVec;
use std::{
cmp::{self, Ordering},
@@ -541,86 +543,37 @@ impl EditorElement {
)
})
.collect();
- }
-
- let style = &self.settings.style;
- let mut prev_font_properties = style.text.font_properties.clone();
- let mut prev_font_id = style.text.font_id;
-
- let mut layouts = Vec::with_capacity(rows.len());
- let mut line = String::new();
- let mut styles = Vec::new();
- let mut row = rows.start;
- let mut line_exceeded_max_len = false;
- let chunks = snapshot.chunks(rows.clone(), Some(&style.syntax));
-
- let newline_chunk = Chunk {
- text: "\n",
- ..Default::default()
- };
- 'outer: for chunk in chunks.chain([newline_chunk]) {
- for (ix, mut line_chunk) in chunk.text.split('\n').enumerate() {
- if ix > 0 {
- layouts.push(cx.text_layout_cache.layout_str(
- &line,
- style.text.font_size,
- &styles,
- ));
- line.clear();
- styles.clear();
- row += 1;
- line_exceeded_max_len = false;
- if row == rows.end {
- break 'outer;
- }
- }
-
- if !line_chunk.is_empty() && !line_exceeded_max_len {
- let highlight_style =
- chunk.highlight_style.unwrap_or(style.text.clone().into());
- // Avoid a lookup if the font properties match the previous ones.
- let font_id = if highlight_style.font_properties == prev_font_properties {
- prev_font_id
- } else {
- cx.font_cache
- .select_font(
- style.text.font_family_id,
- &highlight_style.font_properties,
- )
- .unwrap_or(style.text.font_id)
- };
-
- if line.len() + line_chunk.len() > MAX_LINE_LEN {
- let mut chunk_len = MAX_LINE_LEN - line.len();
- while !line_chunk.is_char_boundary(chunk_len) {
- chunk_len -= 1;
+ } else {
+ let style = &self.settings.style;
+ let chunks = snapshot
+ .chunks(rows.clone(), Some(&style.syntax))
+ .map(|chunk| {
+ let highlight = if let Some(severity) = chunk.diagnostic {
+ let underline = Some(super::diagnostic_style(severity, true, style).text);
+ if let Some(mut highlight) = chunk.highlight_style {
+ highlight.underline = underline;
+ Some(highlight)
+ } else {
+ Some(HighlightStyle {
+ underline,
+ color: style.text.color,
+ font_properties: style.text.font_properties,
+ })
}
- line_chunk = &line_chunk[..chunk_len];
- line_exceeded_max_len = true;
- }
-
- let underline = if let Some(severity) = chunk.diagnostic {
- Some(super::diagnostic_style(severity, true, style).text)
} else {
- highlight_style.underline
+ chunk.highlight_style
};
-
- line.push_str(line_chunk);
- styles.push((
- line_chunk.len(),
- RunStyle {
- font_id,
- color: highlight_style.color,
- underline,
- },
- ));
- prev_font_id = font_id;
- prev_font_properties = highlight_style.font_properties;
- }
- }
+ (chunk.text, highlight)
+ });
+ layout_highlighted_chunks(
+ chunks,
+ &style.text,
+ &cx.text_layout_cache,
+ &cx.font_cache,
+ MAX_LINE_LEN,
+ rows.len() as usize,
+ )
}
-
- layouts
}
fn layout_blocks(
@@ -1711,7 +1711,7 @@ impl MultiBufferSnapshot {
range: self.anchor_in_excerpt(excerpt_id.clone(), item.range.start)
..self.anchor_in_excerpt(excerpt_id.clone(), item.range.end),
text: item.text,
- text_runs: item.text_runs,
+ highlight_ranges: item.highlight_ranges,
name_ranges: item.name_ranges,
})
.collect(),
@@ -1,13 +1,16 @@
+use std::{ops::Range, sync::Arc};
+
use crate::{
color::Color,
- fonts::TextStyle,
+ fonts::{HighlightStyle, TextStyle},
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json::{ToJson, Value},
- text_layout::{Line, ShapedBoundary},
- DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
+ text_layout::{Line, RunStyle, ShapedBoundary},
+ DebugContext, Element, Event, EventContext, FontCache, LayoutContext, PaintContext,
+ SizeConstraint, TextLayoutCache,
};
use serde_json::json;
@@ -15,10 +18,12 @@ pub struct Text {
text: String,
style: TextStyle,
soft_wrap: bool,
+ highlights: Vec<(Range<usize>, HighlightStyle)>,
}
pub struct LayoutState {
- lines: Vec<(Line, Vec<ShapedBoundary>)>,
+ shaped_lines: Vec<Line>,
+ wrap_boundaries: Vec<Vec<ShapedBoundary>>,
line_height: f32,
}
@@ -28,6 +33,7 @@ impl Text {
text,
style,
soft_wrap: true,
+ highlights: Vec::new(),
}
}
@@ -36,6 +42,11 @@ impl Text {
self
}
+ pub fn with_highlights(mut self, runs: Vec<(Range<usize>, HighlightStyle)>) -> Self {
+ self.highlights = runs;
+ self
+ }
+
pub fn with_soft_wrap(mut self, soft_wrap: bool) -> Self {
self.soft_wrap = soft_wrap;
self
@@ -51,32 +62,59 @@ impl Element for Text {
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
- let font_id = self.style.font_id;
- let line_height = cx.font_cache.line_height(font_id, self.style.font_size);
+ // Convert the string and highlight ranges into an iterator of highlighted chunks.
+ let mut offset = 0;
+ 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 offset < range.start {
+ result = Some((&self.text[offset..range.start], None));
+ offset = range.start;
+ } else {
+ result = Some((&self.text[range.clone()], Some(*highlight)));
+ highlight_ranges.next();
+ offset = range.end;
+ }
+ } else if offset < self.text.len() {
+ result = Some((&self.text[offset..], None));
+ offset = self.text.len();
+ } else {
+ result = None;
+ }
+ result
+ });
- let mut wrapper = cx.font_cache.line_wrapper(font_id, self.style.font_size);
- let mut lines = Vec::new();
+ // Perform shaping on these highlighted chunks
+ let shaped_lines = layout_highlighted_chunks(
+ chunks,
+ &self.style,
+ cx.text_layout_cache,
+ &cx.font_cache,
+ usize::MAX,
+ self.text.matches('\n').count() + 1,
+ );
+
+ // If line wrapping is enabled, wrap each of the shaped lines.
+ let font_id = self.style.font_id;
let mut line_count = 0;
let mut max_line_width = 0_f32;
- for line in self.text.lines() {
- let shaped_line = cx.text_layout_cache.layout_str(
- line,
- self.style.font_size,
- &[(line.len(), self.style.to_run())],
- );
- let wrap_boundaries = if self.soft_wrap {
- wrapper
- .wrap_shaped_line(line, &shaped_line, constraint.max.x())
- .collect::<Vec<_>>()
+ let mut wrap_boundaries = Vec::new();
+ let mut wrapper = cx.font_cache.line_wrapper(font_id, self.style.font_size);
+ for (line, shaped_line) in self.text.lines().zip(&shaped_lines) {
+ if self.soft_wrap {
+ let boundaries = wrapper
+ .wrap_shaped_line(line, shaped_line, constraint.max.x())
+ .collect::<Vec<_>>();
+ line_count += boundaries.len() + 1;
+ wrap_boundaries.push(boundaries);
} else {
- Vec::new()
- };
-
+ line_count += 1;
+ }
max_line_width = max_line_width.max(shaped_line.width());
- line_count += wrap_boundaries.len() + 1;
- lines.push((shaped_line, wrap_boundaries));
}
+ let line_height = cx.font_cache.line_height(font_id, self.style.font_size);
let size = vec2f(
max_line_width
.ceil()
@@ -84,7 +122,14 @@ impl Element for Text {
.min(constraint.max.x()),
(line_height * line_count as f32).ceil(),
);
- (size, LayoutState { lines, line_height })
+ (
+ size,
+ LayoutState {
+ shaped_lines,
+ wrap_boundaries,
+ line_height,
+ },
+ )
}
fn paint(
@@ -95,8 +140,10 @@ impl Element for Text {
cx: &mut PaintContext,
) -> Self::PaintState {
let mut origin = bounds.origin();
- for (line, wrap_boundaries) in &layout.lines {
- let wrapped_line_boundaries = RectF::new(
+ let empty = Vec::new();
+ for (ix, line) in layout.shaped_lines.iter().enumerate() {
+ let wrap_boundaries = layout.wrap_boundaries.get(ix).unwrap_or(&empty);
+ let boundaries = RectF::new(
origin,
vec2f(
bounds.width(),
@@ -104,16 +151,20 @@ impl Element for Text {
),
);
- if wrapped_line_boundaries.intersects(visible_bounds) {
- line.paint_wrapped(
- origin,
- visible_bounds,
- layout.line_height,
- wrap_boundaries.iter().copied(),
- cx,
- );
+ if boundaries.intersects(visible_bounds) {
+ if self.soft_wrap {
+ line.paint_wrapped(
+ origin,
+ visible_bounds,
+ layout.line_height,
+ wrap_boundaries.iter().copied(),
+ cx,
+ );
+ } else {
+ line.paint(origin, visible_bounds, layout.line_height, cx);
+ }
}
- origin.set_y(wrapped_line_boundaries.max_y());
+ origin.set_y(boundaries.max_y());
}
}
@@ -143,3 +194,71 @@ impl Element for Text {
})
}
}
+
+/// Perform text layout on a series of highlighted chunks of text.
+pub fn layout_highlighted_chunks<'a>(
+ chunks: impl Iterator<Item = (&'a str, Option<HighlightStyle>)>,
+ style: &'a TextStyle,
+ text_layout_cache: &'a TextLayoutCache,
+ font_cache: &'a Arc<FontCache>,
+ max_line_len: usize,
+ max_line_count: usize,
+) -> Vec<Line> {
+ let mut layouts = Vec::with_capacity(max_line_count);
+ let mut prev_font_properties = style.font_properties.clone();
+ let mut prev_font_id = style.font_id;
+ let mut 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 (ix, mut line_chunk) in chunk.split('\n').enumerate() {
+ if ix > 0 {
+ layouts.push(text_layout_cache.layout_str(&line, style.font_size, &styles));
+ line.clear();
+ styles.clear();
+ row += 1;
+ line_exceeded_max_len = false;
+ if row == max_line_count {
+ return layouts;
+ }
+ }
+
+ if !line_chunk.is_empty() && !line_exceeded_max_len {
+ let highlight_style = highlight_style.unwrap_or(style.clone().into());
+
+ // Avoid a lookup if the font properties match the previous ones.
+ let font_id = if highlight_style.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)
+ };
+
+ if line.len() + line_chunk.len() > max_line_len {
+ let mut chunk_len = max_line_len - line.len();
+ while !line_chunk.is_char_boundary(chunk_len) {
+ chunk_len -= 1;
+ }
+ line_chunk = &line_chunk[..chunk_len];
+ line_exceeded_max_len = true;
+ }
+
+ line.push_str(line_chunk);
+ styles.push((
+ line_chunk.len(),
+ RunStyle {
+ font_id,
+ color: highlight_style.color,
+ underline: highlight_style.underline,
+ },
+ ));
+ prev_font_id = font_id;
+ prev_font_properties = highlight_style.font_properties;
+ }
+ }
+ }
+
+ layouts
+}
@@ -30,7 +30,7 @@ pub struct TextStyle {
pub underline: Option<Color>,
}
-#[derive(Copy, Clone, Debug, Default)]
+#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct HighlightStyle {
pub color: Color,
pub font_properties: Properties,
@@ -1865,7 +1865,7 @@ impl BufferSnapshot {
let range = item_node.start_byte()..item_node.end_byte();
let mut text = String::new();
let mut name_ranges = Vec::new();
- let mut text_runs = Vec::new();
+ let mut highlight_ranges = Vec::new();
for capture in mat.captures {
let node_is_name;
@@ -1903,7 +1903,11 @@ impl BufferSnapshot {
} else {
offset += chunk.text.len();
}
- text_runs.push((chunk.text.len(), chunk.highlight_style));
+ if let Some(style) = chunk.highlight_style {
+ let start = text.len();
+ let end = start + chunk.text.len();
+ highlight_ranges.push((start..end, style));
+ }
text.push_str(chunk.text);
if offset >= range.end {
break;
@@ -1922,8 +1926,8 @@ impl BufferSnapshot {
depth: stack.len() - 1,
range: self.anchor_after(range.start)..self.anchor_before(range.end),
text,
- name_ranges: name_ranges.into_boxed_slice(),
- text_runs,
+ name_ranges,
+ highlight_ranges,
})
})
.collect::<Vec<_>>();
@@ -13,8 +13,8 @@ pub struct OutlineItem<T> {
pub depth: usize,
pub range: Range<T>,
pub text: String,
- pub name_ranges: Box<[Range<u32>]>,
- pub text_runs: Vec<(usize, Option<HighlightStyle>)>,
+ pub name_ranges: Vec<Range<u32>>,
+ pub highlight_ranges: Vec<(Range<usize>, HighlightStyle)>,
}
impl<T> Outline<T> {
@@ -5,7 +5,9 @@ use editor::{
use fuzzy::StringMatch;
use gpui::{
action,
+ color::Color,
elements::*,
+ fonts::HighlightStyle,
geometry::vector::Vector2F,
keymap::{
self,
@@ -20,6 +22,7 @@ use ordered_float::OrderedFloat;
use postage::watch;
use std::{
cmp::{self, Reverse},
+ ops::Range,
sync::Arc,
};
use workspace::{Settings, Workspace};
@@ -364,14 +367,180 @@ impl OutlineView {
} else {
&settings.theme.selector.item
};
- let outline_match = &self.outline.items[string_match.candidate_index];
+ let outline_item = &self.outline.items[string_match.candidate_index];
- Label::new(outline_match.text.clone(), style.label.clone())
- .with_highlights(string_match.positions.clone())
+ Text::new(outline_item.text.clone(), style.label.text.clone())
+ .with_soft_wrap(false)
+ .with_highlights(combine_syntax_and_fuzzy_match_highlights(
+ &outline_item.text,
+ style.label.text.clone().into(),
+ &outline_item.highlight_ranges,
+ &string_match.positions,
+ Color::red(),
+ ))
.contained()
- .with_padding_left(20. * outline_match.depth as f32)
+ .with_padding_left(20. * outline_item.depth as f32)
.contained()
.with_style(style.container)
.boxed()
}
}
+
+fn combine_syntax_and_fuzzy_match_highlights(
+ text: &str,
+ default_style: HighlightStyle,
+ syntax_ranges: &[(Range<usize>, HighlightStyle)],
+ match_indices: &[usize],
+ match_underline: Color,
+) -> Vec<(Range<usize>, HighlightStyle)> {
+ let mut result = Vec::new();
+ let mut match_indices = match_indices.iter().copied().peekable();
+
+ for (range, syntax_highlight) in syntax_ranges
+ .iter()
+ .cloned()
+ .chain([(usize::MAX..0, Default::default())])
+ {
+ // Add highlights for any fuzzy match characters before the next
+ // syntax highlight range.
+ while let Some(&match_index) = match_indices.peek() {
+ if match_index >= range.start {
+ break;
+ }
+ match_indices.next();
+ let end_index = char_ix_after(match_index, text);
+ result.push((
+ match_index..end_index,
+ HighlightStyle {
+ underline: Some(match_underline),
+ ..default_style
+ },
+ ));
+ }
+
+ if range.start == usize::MAX {
+ break;
+ }
+
+ // Add highlights for any fuzzy match characters within the
+ // syntax highlight range.
+ let mut offset = range.start;
+ while let Some(&match_index) = match_indices.peek() {
+ if match_index >= range.end {
+ break;
+ }
+
+ match_indices.next();
+ if match_index > offset {
+ result.push((offset..match_index, syntax_highlight));
+ }
+
+ let mut end_index = char_ix_after(match_index, text);
+ while let Some(&next_match_index) = match_indices.peek() {
+ if next_match_index == end_index {
+ end_index = char_ix_after(next_match_index, text);
+ match_indices.next();
+ } else {
+ break;
+ }
+ }
+
+ result.push((
+ match_index..end_index,
+ HighlightStyle {
+ underline: Some(match_underline),
+ ..syntax_highlight
+ },
+ ));
+ offset = end_index;
+ }
+
+ if offset < range.end {
+ result.push((offset..range.end, syntax_highlight));
+ }
+ }
+
+ result
+}
+
+fn char_ix_after(ix: usize, text: &str) -> usize {
+ ix + text[ix..].chars().next().unwrap().len_utf8()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use gpui::fonts::HighlightStyle;
+
+ #[test]
+ fn test_combine_syntax_and_fuzzy_match_highlights() {
+ let string = "abcdefghijklmnop";
+ let default = HighlightStyle::default();
+ let syntax_ranges = [
+ (
+ 0..3,
+ HighlightStyle {
+ color: Color::red(),
+ ..default
+ },
+ ),
+ (
+ 4..10,
+ HighlightStyle {
+ color: Color::green(),
+ ..default
+ },
+ ),
+ ];
+ let match_indices = [4, 6, 7];
+ let match_underline = Color::white();
+ assert_eq!(
+ combine_syntax_and_fuzzy_match_highlights(
+ &string,
+ default,
+ &syntax_ranges,
+ &match_indices,
+ match_underline
+ ),
+ &[
+ (
+ 0..3,
+ HighlightStyle {
+ color: Color::red(),
+ ..default
+ },
+ ),
+ (
+ 4..5,
+ HighlightStyle {
+ color: Color::green(),
+ underline: Some(match_underline),
+ ..default
+ },
+ ),
+ (
+ 5..6,
+ HighlightStyle {
+ color: Color::green(),
+ ..default
+ },
+ ),
+ (
+ 6..8,
+ HighlightStyle {
+ color: Color::green(),
+ underline: Some(match_underline),
+ ..default
+ },
+ ),
+ (
+ 8..10,
+ HighlightStyle {
+ color: Color::green(),
+ ..default
+ },
+ ),
+ ]
+ );
+ }
+}