diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index a7b082dbdf0e483cf058339a79a2886a05e51c7b..ff4b792338a9a9fd82a9fb6348d68e5d512d85ed 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -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( diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 70a3ad82f4a4c263990a604250902d8c13f7edf4..30020a0d55603154c28408ff3a2a6f9ac6dbc8a0 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -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(), diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index 2f20b77d566a8b14ab303d787bfffa89a1e2d007..7c983f1e6fb7b99659e9053feffb4f21f4535ffc 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -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, HighlightStyle)>, } pub struct LayoutState { - lines: Vec<(Line, Vec)>, + shaped_lines: Vec, + wrap_boundaries: Vec>, 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, 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::>() + 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::>(); + 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)>, + 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 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 +} diff --git a/crates/gpui/src/fonts.rs b/crates/gpui/src/fonts.rs index 6509360a626a9e8342afa1adbb90c9cedee327ce..25e16b717065d0e701e588fd5c3ec2c8c8a5a9fe 100644 --- a/crates/gpui/src/fonts.rs +++ b/crates/gpui/src/fonts.rs @@ -30,7 +30,7 @@ pub struct TextStyle { pub underline: Option, } -#[derive(Copy, Clone, Debug, Default)] +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] pub struct HighlightStyle { pub color: Color, pub font_properties: Properties, diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 6a4f3bebd8f4b89b740a11574c0e7ea98ac84106..2b24a055244804cdbb91a6f8922488dc92aaccc5 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -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::>(); diff --git a/crates/language/src/outline.rs b/crates/language/src/outline.rs index c0b12b12100461df87fdb7f8cf1b35929e124f51..e6a5258f9c7bff9bcb7261fa3d52a3d6091f7e59 100644 --- a/crates/language/src/outline.rs +++ b/crates/language/src/outline.rs @@ -13,8 +13,8 @@ pub struct OutlineItem { pub depth: usize, pub range: Range, pub text: String, - pub name_ranges: Box<[Range]>, - pub text_runs: Vec<(usize, Option)>, + pub name_ranges: Vec>, + pub highlight_ranges: Vec<(Range, HighlightStyle)>, } impl Outline { diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 36fe071b4553f008b254426272f4243d7565f5c3..96daebf1fad902bd9a7e1689ddbb4ca4c3b0ed1a 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -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, HighlightStyle)], + match_indices: &[usize], + match_underline: Color, +) -> Vec<(Range, 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 + }, + ), + ] + ); + } +}