From adeb7e686403ecca464b1d5184b71732f83f9a43 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 13 Jan 2022 18:09:54 -0800 Subject: [PATCH] Incorporate syntax highlighting into symbol outline view Still need to figure out how to style the fuzzy match characters now that there's syntax highlighting. Right now, they are underlined in red. --- crates/editor/src/element.rs | 107 +++++------------ crates/editor/src/multi_buffer.rs | 2 +- crates/gpui/src/elements/text.rs | 189 ++++++++++++++++++++++++------ crates/gpui/src/fonts.rs | 2 +- crates/language/src/buffer.rs | 12 +- crates/language/src/outline.rs | 4 +- crates/outline/src/outline.rs | 177 +++++++++++++++++++++++++++- 7 files changed, 369 insertions(+), 124 deletions(-) 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 + }, + ), + ] + ); + } +}