diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index 7b349edcd4..7474beb7e5 100644 --- a/crates/assistant/src/assistant.rs +++ b/crates/assistant/src/assistant.rs @@ -189,7 +189,7 @@ pub struct LanguageModelChoiceDelta { struct MessageMetadata { role: Role, status: MessageStatus, - // todo!("delete this") + // TODO: Delete this #[serde(skip)] ambient_context: AmbientContextSnapshot, } diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index a5f185c03a..78c9b08721 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -19,6 +19,7 @@ use crate::{ use anyhow::{anyhow, Result}; use client::telemetry::Telemetry; use collections::{hash_map, HashMap, HashSet, VecDeque}; +use editor::FoldPlaceholder; use editor::{ actions::{FoldAt, MoveDown, MoveUp}, display_map::{ @@ -34,7 +35,7 @@ use fs::Fs; use futures::StreamExt; use gpui::{ canvas, div, point, relative, rems, uniform_list, Action, AnyElement, AnyView, AppContext, - AsyncAppContext, AsyncWindowContext, AvailableSpace, ClipboardItem, Context, Entity, + AsyncAppContext, AsyncWindowContext, AvailableSpace, ClipboardItem, Context, Empty, Entity, EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, HighlightStyle, InteractiveElement, IntoElement, Model, ModelContext, ParentElement, Pixels, Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, TextStyle, @@ -2943,6 +2944,10 @@ impl ConversationEditor { .insert_flaps( [Flap::new( start..end, + FoldPlaceholder { + render: Arc::new(|_, _, _| Empty.into_any()), + constrain_width: false, + }, render_slash_command_output_toggle, render_slash_command_output_trailer, )], diff --git a/crates/assistant2/src/assistant2.rs b/crates/assistant2/src/assistant2.rs index 72292c4d22..bb60a6487c 100644 --- a/crates/assistant2/src/assistant2.rs +++ b/crates/assistant2/src/assistant2.rs @@ -869,7 +869,6 @@ impl AssistantChat { crate::ui::ChatMessage::new( *id, UserOrAssistant::User(self.user_store.read(cx).current_user()), - // todo!(): clean up the vec usage vec![ body.clone().into_any_element(), h_flex() diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index ce74880e47..9c8d9d6f9c 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -24,17 +24,27 @@ mod inlay_map; mod tab_map; mod wrap_map; -use crate::{hover_links::InlayHighlight, movement::TextLayoutDetails, InlayId}; -use crate::{EditorStyle, RowExt}; -pub use block_map::{BlockMap, BlockPoint}; -use collections::{HashMap, HashSet}; -use fold_map::FoldMap; -use gpui::{ - AnyElement, Font, HighlightStyle, Hsla, LineLayout, Model, ModelContext, Pixels, UnderlineStyle, +use crate::{ + hover_links::InlayHighlight, movement::TextLayoutDetails, EditorStyle, InlayId, RowExt, }; -use inlay_map::InlayMap; +pub use block_map::{ + BlockBufferRows, BlockChunks as DisplayChunks, BlockContext, BlockDisposition, BlockId, + BlockMap, BlockPoint, BlockProperties, BlockStyle, RenderBlock, TransformBlock, +}; +use block_map::{BlockRow, BlockSnapshot}; +use collections::{HashMap, HashSet}; +pub use flap_map::*; +pub use fold_map::{Fold, FoldId, FoldPlaceholder, FoldPoint}; +use fold_map::{FoldMap, FoldSnapshot}; +use gpui::{ + AnyElement, Font, HighlightStyle, LineLayout, Model, ModelContext, Pixels, UnderlineStyle, +}; +pub(crate) use inlay_map::Inlay; +use inlay_map::{InlayMap, InlaySnapshot}; +pub use inlay_map::{InlayOffset, InlayPoint}; use language::{ - language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription, + language_settings::language_settings, ChunkRenderer, OffsetUtf16, Point, + Subscription as BufferSubscription, }; use lsp::DiagnosticSeverity; use multi_buffer::{ @@ -42,27 +52,12 @@ use multi_buffer::{ ToOffset, ToPoint, }; use serde::Deserialize; +use std::ops::Add; use std::{any::TypeId, borrow::Cow, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc}; use sum_tree::{Bias, TreeMap}; -use tab_map::TabMap; +use tab_map::{TabMap, TabSnapshot}; use ui::WindowContext; - -use wrap_map::WrapMap; - -pub use block_map::{ - BlockBufferRows, BlockChunks as DisplayChunks, BlockContext, BlockDisposition, BlockId, - BlockProperties, BlockStyle, RenderBlock, TransformBlock, -}; -pub use flap_map::*; - -use self::block_map::{BlockRow, BlockSnapshot}; -use self::fold_map::FoldSnapshot; -pub use self::fold_map::{Fold, FoldId, FoldPoint}; -use self::inlay_map::InlaySnapshot; -pub use self::inlay_map::{InlayOffset, InlayPoint}; -use self::tab_map::TabSnapshot; -use self::wrap_map::WrapSnapshot; -pub(crate) use inlay_map::Inlay; +use wrap_map::{WrapMap, WrapSnapshot}; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum FoldStatus { @@ -105,10 +100,12 @@ pub struct DisplayMap { inlay_highlights: InlayHighlights, /// A container for explicitly foldable ranges, which supersede indentation based fold range suggestions. flap_map: FlapMap, + fold_placeholder: FoldPlaceholder, pub clip_at_line_ends: bool, } impl DisplayMap { + #[allow(clippy::too_many_arguments)] pub fn new( buffer: Model, font: Font, @@ -116,6 +113,7 @@ impl DisplayMap { wrap_width: Option, buffer_header_height: u8, excerpt_header_height: u8, + fold_placeholder: FoldPlaceholder, cx: &mut ModelContext, ) -> Self { let buffer_subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); @@ -138,6 +136,7 @@ impl DisplayMap { wrap_map, block_map, flap_map, + fold_placeholder, text_highlights: Default::default(), inlay_highlights: Default::default(), clip_at_line_ends: false, @@ -167,6 +166,7 @@ impl DisplayMap { text_highlights: self.text_highlights.clone(), inlay_highlights: self.inlay_highlights.clone(), clip_at_line_ends: self.clip_at_line_ends, + fold_placeholder: self.fold_placeholder.clone(), } } @@ -174,14 +174,19 @@ impl DisplayMap { self.fold( other .folds_in_range(0..other.buffer_snapshot.len()) - .map(|fold| (fold.range.to_offset(&other.buffer_snapshot), fold.text)), + .map(|fold| { + ( + fold.range.to_offset(&other.buffer_snapshot), + fold.placeholder.clone(), + ) + }), cx, ); } pub fn fold( &mut self, - ranges: impl IntoIterator, &'static str)>, + ranges: impl IntoIterator, FoldPlaceholder)>, cx: &mut ModelContext, ) { let snapshot = self.buffer.read(cx).snapshot(cx); @@ -324,10 +329,6 @@ impl DisplayMap { .update(cx, |map, cx| map.set_font_with_size(font, font_size, cx)) } - pub fn set_fold_ellipses_color(&mut self, color: Hsla) -> bool { - self.fold_map.set_ellipses_color(color) - } - pub fn set_wrap_width(&self, width: Option, cx: &mut ModelContext) -> bool { self.wrap_map .update(cx, |map, cx| map.set_wrap_width(width, cx)) @@ -394,9 +395,10 @@ pub struct HighlightStyles { } pub struct HighlightedChunk<'a> { - pub chunk: &'a str, + pub text: &'a str, pub style: Option, pub is_tab: bool, + pub renderer: Option, } #[derive(Clone)] @@ -411,6 +413,7 @@ pub struct DisplaySnapshot { text_highlights: TextHighlights, inlay_highlights: InlayHighlights, clip_at_line_ends: bool, + pub(crate) fold_placeholder: FoldPlaceholder, } impl DisplaySnapshot { @@ -648,9 +651,10 @@ impl DisplaySnapshot { } HighlightedChunk { - chunk: chunk.text, + text: chunk.text, style: highlight_style, is_tab: chunk.is_tab, + renderer: chunk.renderer, } }) } @@ -672,7 +676,7 @@ impl DisplaySnapshot { let range = display_row..display_row.next_row(); for chunk in self.highlighted_chunks(range, false, &editor_style) { - line.push_str(chunk.chunk); + line.push_str(chunk.text); let text_style = if let Some(style) = chunk.style { Cow::Owned(editor_style.text.clone().highlight(style)) @@ -680,7 +684,7 @@ impl DisplaySnapshot { Cow::Borrowed(&editor_style.text) }; - runs.push(text_style.to_run(chunk.chunk.len())) + runs.push(text_style.to_run(chunk.text.len())) } if line.ends_with('\n') { @@ -883,13 +887,16 @@ impl DisplaySnapshot { pub fn foldable_range( &self, buffer_row: MultiBufferRow, - ) -> Option<(Range, &'static str)> { + ) -> Option<(Range, FoldPlaceholder)> { let start = MultiBufferPoint::new(buffer_row.0, self.buffer_snapshot.line_len(buffer_row)); if let Some(flap) = self .flap_snapshot .query_row(buffer_row, &self.buffer_snapshot) { - Some((flap.range.to_point(&self.buffer_snapshot), "")) + Some(( + flap.range.to_point(&self.buffer_snapshot), + flap.placeholder.clone(), + )) } else if self.starts_indent(MultiBufferRow(start.row)) && !self.is_line_folded(MultiBufferRow(start.row)) { @@ -909,7 +916,7 @@ impl DisplaySnapshot { } } let end = end.unwrap_or(max_point); - Some((start..end, "⋯")) + Some((start..end, self.fold_placeholder.clone())) } else { None } @@ -950,6 +957,14 @@ impl Debug for DisplayPoint { #[serde(transparent)] pub struct DisplayRow(pub u32); +impl Add for DisplayRow { + type Output = Self; + + fn add(self, other: Self) -> Self::Output { + DisplayRow(self.0 + other.0) + } +} + impl DisplayPoint { pub fn new(row: DisplayRow, column: u32) -> Self { Self(BlockPoint(Point::new(row.0, column))) @@ -1083,6 +1098,7 @@ pub mod tests { wrap_width, buffer_start_excerpt_header_height, excerpt_header_height, + FoldPlaceholder::test(), cx, ) }); @@ -1186,7 +1202,12 @@ pub mod tests { } else { log::info!("folding ranges: {:?}", ranges); map.update(cx, |map, cx| { - map.fold(ranges.into_iter().map(|range| (range, "⋯")), cx); + map.fold( + ranges + .into_iter() + .map(|range| (range, FoldPlaceholder::test())), + cx, + ); }); } } @@ -1323,6 +1344,7 @@ pub mod tests { wrap_width, 1, 1, + FoldPlaceholder::test(), cx, ) }); @@ -1424,7 +1446,16 @@ pub mod tests { let font_size = px(14.0); let map = cx.new_model(|cx| { - DisplayMap::new(buffer.clone(), font("Helvetica"), font_size, None, 1, 1, cx) + DisplayMap::new( + buffer.clone(), + font("Helvetica"), + font_size, + None, + 1, + 1, + FoldPlaceholder::test(), + cx, + ) }); buffer.update(cx, |buffer, cx| { @@ -1510,8 +1541,18 @@ pub mod tests { let font_size = px(14.0); - let map = cx - .new_model(|cx| DisplayMap::new(buffer, font("Helvetica"), font_size, None, 1, 1, cx)); + let map = cx.new_model(|cx| { + DisplayMap::new( + buffer, + font("Helvetica"), + font_size, + None, + 1, + 1, + FoldPlaceholder::test(), + cx, + ) + }); assert_eq!( cx.update(|cx| syntax_chunks(DisplayRow(0)..DisplayRow(5), &map, &theme, cx)), vec![ @@ -1536,7 +1577,7 @@ pub mod tests { map.fold( vec![( MultiBufferPoint::new(0, 6)..MultiBufferPoint::new(3, 2), - "⋯", + FoldPlaceholder::test(), )], cx, ) @@ -1602,7 +1643,16 @@ pub mod tests { let font_size = px(16.0); let map = cx.new_model(|cx| { - DisplayMap::new(buffer, font("Courier"), font_size, Some(px(40.0)), 1, 1, cx) + DisplayMap::new( + buffer, + font("Courier"), + font_size, + Some(px(40.0)), + 1, + 1, + FoldPlaceholder::test(), + cx, + ) }); assert_eq!( cx.update(|cx| syntax_chunks(DisplayRow(0)..DisplayRow(5), &map, &theme, cx)), @@ -1621,7 +1671,7 @@ pub mod tests { map.fold( vec![( MultiBufferPoint::new(0, 6)..MultiBufferPoint::new(3, 2), - "⋯", + FoldPlaceholder::test(), )], cx, ) @@ -1674,8 +1724,18 @@ pub mod tests { let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx)); let font_size = px(16.0); - let map = - cx.new_model(|cx| DisplayMap::new(buffer, font("Courier"), font_size, None, 1, 1, cx)); + let map = cx.new_model(|cx| { + DisplayMap::new( + buffer, + font("Courier"), + font_size, + None, + 1, + 1, + FoldPlaceholder::test(), + cx, + ) + }); enum MyType {} @@ -1789,8 +1849,16 @@ pub mod tests { let buffer = MultiBuffer::build_simple(text, cx); let font_size = px(14.0); cx.new_model(|cx| { - let mut map = - DisplayMap::new(buffer.clone(), font("Helvetica"), font_size, None, 1, 1, cx); + let mut map = DisplayMap::new( + buffer.clone(), + font("Helvetica"), + font_size, + None, + 1, + 1, + FoldPlaceholder::test(), + cx, + ); let snapshot = map.buffer.read(cx).snapshot(cx); let range = snapshot.anchor_before(Point::new(2, 0))..snapshot.anchor_after(Point::new(3, 3)); @@ -1798,6 +1866,7 @@ pub mod tests { map.flap_map.insert( [Flap::new( range, + FoldPlaceholder::test(), |_row, _status, _toggle, _cx| div(), |_row, _status, _cx| div(), )], @@ -1817,7 +1886,16 @@ pub mod tests { let font_size = px(14.0); let map = cx.new_model(|cx| { - DisplayMap::new(buffer.clone(), font("Helvetica"), font_size, None, 1, 1, cx) + DisplayMap::new( + buffer.clone(), + font("Helvetica"), + font_size, + None, + 1, + 1, + FoldPlaceholder::test(), + cx, + ) }); let map = map.update(cx, |map, cx| map.snapshot(cx)); assert_eq!(map.text(), "✅ α\nβ \n🏀β γ"); @@ -1883,7 +1961,16 @@ pub mod tests { let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx); let font_size = px(14.0); let map = cx.new_model(|cx| { - DisplayMap::new(buffer.clone(), font("Helvetica"), font_size, None, 1, 1, cx) + DisplayMap::new( + buffer.clone(), + font("Helvetica"), + font_size, + None, + 1, + 1, + FoldPlaceholder::test(), + cx, + ) }); assert_eq!( map.update(cx, |map, cx| map.snapshot(cx)).max_point(), diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 2ec868fc6a..5d534e6017 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -875,7 +875,7 @@ impl<'a> Iterator for BlockChunks<'a> { Some(Chunk { text: prefix, - ..self.input_chunk + ..self.input_chunk.clone() }) } } diff --git a/crates/editor/src/display_map/flap_map.rs b/crates/editor/src/display_map/flap_map.rs index c2a6731672..9c33f766c9 100644 --- a/crates/editor/src/display_map/flap_map.rs +++ b/crates/editor/src/display_map/flap_map.rs @@ -6,6 +6,8 @@ use sum_tree::{Bias, SeekTarget, SumTree}; use text::Point; use ui::WindowContext; +use crate::FoldPlaceholder; + #[derive(Copy, Clone, Default, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] pub struct FlapId(usize); @@ -76,6 +78,7 @@ type RenderTrailerFn = #[derive(Clone)] pub struct Flap { pub range: Range, + pub placeholder: FoldPlaceholder, pub render_toggle: RenderToggleFn, pub render_trailer: RenderTrailerFn, } @@ -83,6 +86,7 @@ pub struct Flap { impl Flap { pub fn new( range: Range, + placeholder: FoldPlaceholder, render_toggle: RenderToggle, render_trailer: RenderTrailer, ) -> Self @@ -107,6 +111,7 @@ impl Flap { { Flap { range, + placeholder, render_toggle: Arc::new(move |row, folded, toggle, cx| { render_toggle(row, folded, toggle, cx).into_any_element() }), @@ -256,11 +261,13 @@ mod test { let flaps = [ Flap::new( snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(1, 5)), + FoldPlaceholder::test(), |_row, _folded, _toggle, _cx| div(), |_row, _folded, _cx| div(), ), Flap::new( snapshot.anchor_before(Point::new(3, 0))..snapshot.anchor_after(Point::new(3, 5)), + FoldPlaceholder::test(), |_row, _folded, _toggle, _cx| div(), |_row, _folded, _cx| div(), ), diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index d85be31cec..a117f2058b 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -2,17 +2,54 @@ use super::{ inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot}, Highlights, }; -use gpui::{ElementId, HighlightStyle, Hsla}; -use language::{Chunk, Edit, Point, TextSummary}; +use gpui::{AnyElement, ElementId, WindowContext}; +use language::{Chunk, ChunkRenderer, Edit, Point, TextSummary}; use multi_buffer::{Anchor, AnchorRangeExt, MultiBufferRow, MultiBufferSnapshot, ToOffset}; use std::{ cmp::{self, Ordering}, - iter, + fmt, iter, ops::{Add, AddAssign, Deref, DerefMut, Range, Sub}, + sync::Arc, }; use sum_tree::{Bias, Cursor, FilterCursor, SumTree}; use util::post_inc; +#[derive(Clone)] +pub struct FoldPlaceholder { + /// Creates an element to represent this fold's placeholder. + pub render: Arc, &mut WindowContext) -> AnyElement>, + /// If true, the element is constrained to the shaped width of an ellipsis. + pub constrain_width: bool, +} + +impl FoldPlaceholder { + #[cfg(any(test, feature = "test-support"))] + pub fn test() -> Self { + use gpui::IntoElement; + + Self { + render: Arc::new(|_id, _range, _cx| gpui::Empty.into_any_element()), + constrain_width: true, + } + } +} + +impl fmt::Debug for FoldPlaceholder { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("FoldPlaceholder") + .field("constrain_width", &self.constrain_width) + .finish() + } +} + +impl Eq for FoldPlaceholder {} + +impl PartialEq for FoldPlaceholder { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.render, &other.render) && self.constrain_width == other.constrain_width + } +} + #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] pub struct FoldPoint(pub Point); @@ -54,7 +91,7 @@ impl FoldPoint { let mut offset = cursor.start().1.output.len; if !overshoot.is_zero() { let transform = cursor.item().expect("display point out of range"); - assert!(transform.output_text.is_none()); + assert!(transform.placeholder.is_none()); let end_inlay_offset = snapshot .inlay_snapshot .to_offset(InlayPoint(cursor.start().1.input.lines + overshoot)); @@ -75,7 +112,7 @@ pub(crate) struct FoldMapWriter<'a>(&'a mut FoldMap); impl<'a> FoldMapWriter<'a> { pub(crate) fn fold( &mut self, - ranges: impl IntoIterator, &'static str)>, + ranges: impl IntoIterator, FoldPlaceholder)>, ) -> (FoldSnapshot, Vec) { let mut edits = Vec::new(); let mut folds = Vec::new(); @@ -99,7 +136,7 @@ impl<'a> FoldMapWriter<'a> { folds.push(Fold { id: FoldId(post_inc(&mut self.0.next_fold_id.0)), range: fold_range, - text: fold_text, + placeholder: fold_text, }); let inlay_range = @@ -183,7 +220,6 @@ impl<'a> FoldMapWriter<'a> { /// See the [`display_map` module documentation](crate::display_map) for more information. pub(crate) struct FoldMap { snapshot: FoldSnapshot, - ellipses_color: Option, next_fold_id: FoldId, } @@ -198,15 +234,13 @@ impl FoldMap { input: inlay_snapshot.text_summary(), output: inlay_snapshot.text_summary(), }, - output_text: None, + placeholder: None, }, &(), ), inlay_snapshot: inlay_snapshot.clone(), version: 0, - ellipses_color: None, }, - ellipses_color: None, next_fold_id: FoldId::default(), }; let snapshot = this.snapshot.clone(); @@ -232,15 +266,6 @@ impl FoldMap { (FoldMapWriter(self), snapshot, edits) } - pub fn set_ellipses_color(&mut self, color: Hsla) -> bool { - if self.ellipses_color == Some(color) { - false - } else { - self.ellipses_color = Some(color); - true - } - } - fn check_invariants(&self) { if cfg!(test) { assert_eq!( @@ -329,9 +354,9 @@ impl FoldMap { let buffer_start = fold.range.start.to_offset(&inlay_snapshot.buffer); let buffer_end = fold.range.end.to_offset(&inlay_snapshot.buffer); ( + fold.clone(), inlay_snapshot.to_inlay_offset(buffer_start) ..inlay_snapshot.to_inlay_offset(buffer_end), - fold.text, ) }); folds_cursor.next(&inlay_snapshot.buffer); @@ -342,17 +367,17 @@ impl FoldMap { while folds .peek() - .map_or(false, |(fold_range, _)| fold_range.start < edit.new.end) + .map_or(false, |(_, fold_range)| fold_range.start < edit.new.end) { - let (mut fold_range, fold_text) = folds.next().unwrap(); + let (fold, mut fold_range) = folds.next().unwrap(); let sum = new_transforms.summary(); assert!(fold_range.start.0 >= sum.input.len); - while folds.peek().map_or(false, |(next_fold_range, _)| { + while folds.peek().map_or(false, |(_, next_fold_range)| { next_fold_range.start <= fold_range.end }) { - let (next_fold_range, _) = folds.next().unwrap(); + let (_, next_fold_range) = folds.next().unwrap(); if next_fold_range.end > fold_range.end { fold_range.end = next_fold_range.end; } @@ -367,21 +392,36 @@ impl FoldMap { output: text_summary.clone(), input: text_summary, }, - output_text: None, + placeholder: None, }, &(), ); } if fold_range.end > fold_range.start { + const ELLIPSIS: &'static str = "⋯"; + + let fold_id = fold.id; new_transforms.push( Transform { summary: TransformSummary { - output: TextSummary::from(fold_text), + output: TextSummary::from(ELLIPSIS), input: inlay_snapshot .text_summary_for_range(fold_range.start..fold_range.end), }, - output_text: Some(fold_text), + placeholder: Some(TransformPlaceholder { + text: ELLIPSIS, + renderer: ChunkRenderer { + render: Arc::new(move |cx| { + (fold.placeholder.render)( + fold_id, + fold.range.0.clone(), + cx, + ) + }), + constrain_width: fold.placeholder.constrain_width, + }, + }), }, &(), ); @@ -398,7 +438,7 @@ impl FoldMap { output: text_summary.clone(), input: text_summary, }, - output_text: None, + placeholder: None, }, &(), ); @@ -414,7 +454,7 @@ impl FoldMap { output: text_summary.clone(), input: text_summary, }, - output_text: None, + placeholder: None, }, &(), ); @@ -484,7 +524,6 @@ pub struct FoldSnapshot { folds: SumTree, pub inlay_snapshot: InlaySnapshot, pub version: usize, - pub ellipses_color: Option, } impl FoldSnapshot { @@ -508,9 +547,9 @@ impl FoldSnapshot { if let Some(transform) = cursor.item() { let start_in_transform = range.start.0 - cursor.start().0 .0; let end_in_transform = cmp::min(range.end, cursor.end(&()).0).0 - cursor.start().0 .0; - if let Some(output_text) = transform.output_text { + if let Some(placeholder) = transform.placeholder.as_ref() { summary = TextSummary::from( - &output_text + &placeholder.text [start_in_transform.column as usize..end_in_transform.column as usize], ); } else { @@ -533,8 +572,9 @@ impl FoldSnapshot { .output; if let Some(transform) = cursor.item() { let end_in_transform = range.end.0 - cursor.start().0 .0; - if let Some(output_text) = transform.output_text { - summary += TextSummary::from(&output_text[..end_in_transform.column as usize]); + if let Some(placeholder) = transform.placeholder.as_ref() { + summary += + TextSummary::from(&placeholder.text[..end_in_transform.column as usize]); } else { let inlay_start = self.inlay_snapshot.to_offset(cursor.start().1); let inlay_end = self @@ -631,7 +671,7 @@ impl FoldSnapshot { let inlay_offset = self.inlay_snapshot.to_inlay_offset(buffer_offset); let mut cursor = self.transforms.cursor::(); cursor.seek(&inlay_offset, Bias::Right, &()); - cursor.item().map_or(false, |t| t.output_text.is_some()) + cursor.item().map_or(false, |t| t.placeholder.is_some()) } pub fn is_line_folded(&self, buffer_row: MultiBufferRow) -> bool { @@ -646,7 +686,7 @@ impl FoldSnapshot { let buffer_point = self.inlay_snapshot.to_buffer_point(inlay_point); if buffer_point.row != buffer_row.0 { return false; - } else if transform.output_text.is_some() { + } else if transform.placeholder.is_some() { return true; } } @@ -693,7 +733,6 @@ impl FoldSnapshot { inlay_offset: inlay_start, output_offset: range.start.0, max_output_offset: range.end.0, - ellipses_color: self.ellipses_color, } } @@ -720,7 +759,7 @@ impl FoldSnapshot { cursor.seek(&point, Bias::Right, &()); if let Some(transform) = cursor.item() { let transform_start = cursor.start().0 .0; - if transform.output_text.is_some() { + if transform.placeholder.is_some() { if point.0 == transform_start || matches!(bias, Bias::Left) { FoldPoint(transform_start) } else { @@ -810,15 +849,21 @@ fn consolidate_fold_edits(edits: &mut Vec) { } } -#[derive(Clone, Debug, Default, Eq, PartialEq)] +#[derive(Clone, Debug, Default)] struct Transform { summary: TransformSummary, - output_text: Option<&'static str>, + placeholder: Option, +} + +#[derive(Clone, Debug)] +struct TransformPlaceholder { + text: &'static str, + renderer: ChunkRenderer, } impl Transform { fn is_fold(&self) -> bool { - self.output_text.is_some() + self.placeholder.is_some() } } @@ -858,7 +903,7 @@ impl Into for FoldId { pub struct Fold { pub id: FoldId, pub range: FoldRange, - pub text: &'static str, + pub placeholder: FoldPlaceholder, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -1004,7 +1049,6 @@ pub struct FoldChunks<'a> { inlay_offset: InlayOffset, output_offset: usize, max_output_offset: usize, - ellipses_color: Option, } impl<'a> Iterator for FoldChunks<'a> { @@ -1019,7 +1063,7 @@ impl<'a> Iterator for FoldChunks<'a> { // If we're in a fold, then return the fold's display text and // advance the transform and buffer cursors to the end of the fold. - if let Some(output_text) = transform.output_text { + if let Some(placeholder) = transform.placeholder.as_ref() { self.inlay_chunk.take(); self.inlay_offset += InlayOffset(transform.summary.input.len); self.inlay_chunks.seek(self.inlay_offset); @@ -1030,13 +1074,10 @@ impl<'a> Iterator for FoldChunks<'a> { self.transform_cursor.next(&()); } - self.output_offset += output_text.len(); + self.output_offset += placeholder.text.len(); return Some(Chunk { - text: output_text, - highlight_style: self.ellipses_color.map(|color| HighlightStyle { - color: Some(color), - ..Default::default() - }), + text: placeholder.text, + renderer: Some(placeholder.renderer.clone()), ..Default::default() }); } @@ -1048,7 +1089,7 @@ impl<'a> Iterator for FoldChunks<'a> { } // Otherwise, take a chunk from the buffer's text. - if let Some((buffer_chunk_start, mut chunk)) = self.inlay_chunk { + if let Some((buffer_chunk_start, mut chunk)) = self.inlay_chunk.clone() { let buffer_chunk_end = buffer_chunk_start + InlayOffset(chunk.text.len()); let transform_end = self.transform_cursor.end(&()).1; let chunk_end = buffer_chunk_end.min(transform_end); @@ -1165,8 +1206,8 @@ mod tests { let (mut writer, _, _) = map.write(inlay_snapshot, vec![]); let (snapshot2, edits) = writer.fold(vec![ - (Point::new(0, 2)..Point::new(2, 2), "⋯"), - (Point::new(2, 4)..Point::new(4, 1), "⋯"), + (Point::new(0, 2)..Point::new(2, 2), FoldPlaceholder::test()), + (Point::new(2, 4)..Point::new(4, 1), FoldPlaceholder::test()), ]); assert_eq!(snapshot2.text(), "aa⋯cc⋯eeeee"); assert_eq!( @@ -1245,19 +1286,25 @@ mod tests { let mut map = FoldMap::new(inlay_snapshot.clone()).0; let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); - writer.fold(vec![(5..8, "⋯")]); + writer.fold(vec![(5..8, FoldPlaceholder::test())]); let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); assert_eq!(snapshot.text(), "abcde⋯ijkl"); // Create an fold adjacent to the start of the first fold. let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); - writer.fold(vec![(0..1, "⋯"), (2..5, "⋯")]); + writer.fold(vec![ + (0..1, FoldPlaceholder::test()), + (2..5, FoldPlaceholder::test()), + ]); let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); assert_eq!(snapshot.text(), "⋯b⋯ijkl"); // Create an fold adjacent to the end of the first fold. let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); - writer.fold(vec![(11..11, "⋯"), (8..10, "⋯")]); + writer.fold(vec![ + (11..11, FoldPlaceholder::test()), + (8..10, FoldPlaceholder::test()), + ]); let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); assert_eq!(snapshot.text(), "⋯b⋯kl"); } @@ -1267,7 +1314,10 @@ mod tests { // Create two adjacent folds. let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); - writer.fold(vec![(0..2, "⋯"), (2..5, "⋯")]); + writer.fold(vec![ + (0..2, FoldPlaceholder::test()), + (2..5, FoldPlaceholder::test()), + ]); let (snapshot, _) = map.read(inlay_snapshot, vec![]); assert_eq!(snapshot.text(), "⋯fghijkl"); @@ -1291,10 +1341,10 @@ mod tests { let mut map = FoldMap::new(inlay_snapshot.clone()).0; let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); writer.fold(vec![ - (Point::new(0, 2)..Point::new(2, 2), "⋯"), - (Point::new(0, 4)..Point::new(1, 0), "⋯"), - (Point::new(1, 2)..Point::new(3, 2), "⋯"), - (Point::new(3, 1)..Point::new(4, 1), "⋯"), + (Point::new(0, 2)..Point::new(2, 2), FoldPlaceholder::test()), + (Point::new(0, 4)..Point::new(1, 0), FoldPlaceholder::test()), + (Point::new(1, 2)..Point::new(3, 2), FoldPlaceholder::test()), + (Point::new(3, 1)..Point::new(4, 1), FoldPlaceholder::test()), ]); let (snapshot, _) = map.read(inlay_snapshot, vec![]); assert_eq!(snapshot.text(), "aa⋯eeeee"); @@ -1311,8 +1361,8 @@ mod tests { let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); writer.fold(vec![ - (Point::new(0, 2)..Point::new(2, 2), "⋯"), - (Point::new(3, 1)..Point::new(4, 1), "⋯"), + (Point::new(0, 2)..Point::new(2, 2), FoldPlaceholder::test()), + (Point::new(3, 1)..Point::new(4, 1), FoldPlaceholder::test()), ]); let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); assert_eq!(snapshot.text(), "aa⋯cccc\nd⋯eeeee"); @@ -1336,10 +1386,10 @@ mod tests { let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); writer.fold(vec![ - (Point::new(0, 2)..Point::new(2, 2), "⋯"), - (Point::new(0, 4)..Point::new(1, 0), "⋯"), - (Point::new(1, 2)..Point::new(3, 2), "⋯"), - (Point::new(3, 1)..Point::new(4, 1), "⋯"), + (Point::new(0, 2)..Point::new(2, 2), FoldPlaceholder::test()), + (Point::new(0, 4)..Point::new(1, 0), FoldPlaceholder::test()), + (Point::new(1, 2)..Point::new(3, 2), FoldPlaceholder::test()), + (Point::new(3, 1)..Point::new(4, 1), FoldPlaceholder::test()), ]); let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); let fold_ranges = snapshot @@ -1414,10 +1464,10 @@ mod tests { snapshot_edits.push((snapshot.clone(), edits)); let mut expected_text: String = inlay_snapshot.text().to_string(); - for (fold_range, fold_text) in map.merged_folds().into_iter().rev() { + for fold_range in map.merged_folds().into_iter().rev() { let fold_inlay_start = inlay_snapshot.to_inlay_offset(fold_range.start); let fold_inlay_end = inlay_snapshot.to_inlay_offset(fold_range.end); - expected_text.replace_range(fold_inlay_start.0..fold_inlay_end.0, fold_text); + expected_text.replace_range(fold_inlay_start.0..fold_inlay_end.0, "⋯"); } assert_eq!(snapshot.text(), expected_text); @@ -1429,7 +1479,7 @@ mod tests { let mut prev_row = 0; let mut expected_buffer_rows = Vec::new(); - for (fold_range, _fold_text) in map.merged_folds().into_iter() { + for fold_range in map.merged_folds() { let fold_start = inlay_snapshot .to_point(inlay_snapshot.to_inlay_offset(fold_range.start)) .row(); @@ -1543,7 +1593,7 @@ mod tests { let folded_buffer_rows = map .merged_folds() .iter() - .flat_map(|(fold_range, _)| { + .flat_map(|fold_range| { let start_row = fold_range.start.to_point(&buffer_snapshot).row; let end = fold_range.end.to_point(&buffer_snapshot); if end.column == 0 { @@ -1640,8 +1690,8 @@ mod tests { let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); writer.fold(vec![ - (Point::new(0, 2)..Point::new(2, 2), "⋯"), - (Point::new(3, 1)..Point::new(4, 1), "⋯"), + (Point::new(0, 2)..Point::new(2, 2), FoldPlaceholder::test()), + (Point::new(3, 1)..Point::new(4, 1), FoldPlaceholder::test()), ]); let (snapshot, _) = map.read(inlay_snapshot, vec![]); @@ -1659,7 +1709,7 @@ mod tests { } impl FoldMap { - fn merged_folds(&self) -> Vec<(Range, &'static str)> { + fn merged_folds(&self) -> Vec> { let inlay_snapshot = self.snapshot.inlay_snapshot.clone(); let buffer = &inlay_snapshot.buffer; let mut folds = self.snapshot.folds.items(buffer); @@ -1667,17 +1717,12 @@ mod tests { folds.sort_by(|a, b| a.range.cmp(&b.range, buffer)); let mut folds = folds .iter() - .map(|fold| { - ( - fold.range.start.to_offset(buffer)..fold.range.end.to_offset(buffer), - fold.text, - ) - }) + .map(|fold| fold.range.start.to_offset(buffer)..fold.range.end.to_offset(buffer)) .peekable(); let mut merged_folds = Vec::new(); - while let Some((mut fold_range, fold_text)) = folds.next() { - while let Some((next_range, _)) = folds.peek() { + while let Some(mut fold_range) = folds.next() { + while let Some(next_range) = folds.peek() { if fold_range.end >= next_range.start { if next_range.end > fold_range.end { fold_range.end = next_range.end; @@ -1688,7 +1733,7 @@ mod tests { } } if fold_range.end > fold_range.start { - merged_folds.push((fold_range, fold_text)); + merged_folds.push(fold_range); } } merged_folds @@ -1723,8 +1768,7 @@ mod tests { for _ in 0..rng.gen_range(1..=2) { let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right); let start = buffer.clip_offset(rng.gen_range(0..=end), Left); - let text = if rng.gen() { "⋯" } else { "" }; - to_fold.push((start..end, text)); + to_fold.push((start..end, FoldPlaceholder::test())); } log::info!("folding {:?}", to_fold); let (mut writer, snapshot, edits) = self.write(inlay_snapshot, vec![]); diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 214b7aa4e7..ad5cc84ef7 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -284,7 +284,7 @@ impl<'a> Iterator for InlayChunks<'a> { self.output_offset.0 += prefix.len(); let mut prefix = Chunk { text: prefix, - ..*chunk + ..chunk.clone() }; if !self.active_highlights.is_empty() { let mut highlight_style = HighlightStyle::default(); diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index 018797386d..87eee6d177 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -508,7 +508,7 @@ impl<'a> Iterator for TabChunks<'a> { self.chunk.text = suffix; return Some(Chunk { text: prefix, - ..self.chunk + ..self.chunk.clone() }); } else { self.chunk.text = &self.chunk.text[1..]; @@ -529,7 +529,7 @@ impl<'a> Iterator for TabChunks<'a> { return Some(Chunk { text: &SPACES[..len as usize], is_tab: true, - ..self.chunk + ..self.chunk.clone() }); } } diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index a292f9f3c5..645ff78c20 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -812,7 +812,7 @@ impl<'a> Iterator for WrapChunks<'a> { self.transforms.next(&()); return Some(Chunk { text: &display_text[start_ix..end_ix], - ..self.input_chunk + ..self.input_chunk.clone() }); } @@ -842,7 +842,7 @@ impl<'a> Iterator for WrapChunks<'a> { self.input_chunk.text = suffix; Some(Chunk { text: prefix, - ..self.input_chunk + ..self.input_chunk.clone() }) } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1d75d41075..9009795a92 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -52,8 +52,8 @@ use clock::ReplicaId; use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; use convert_case::{Case, Casing}; use debounced_delay::DebouncedDelay; -pub use display_map::DisplayPoint; use display_map::*; +pub use display_map::{DisplayPoint, FoldPlaceholder}; use editor_settings::CurrentLineHighlight; pub use editor_settings::EditorSettings; use element::LineWithInvisibles; @@ -1577,8 +1577,48 @@ impl Editor { ) -> Self { let style = cx.text_style(); let font_size = style.font_size.to_pixels(cx.rem_size()); + let editor = cx.view().downgrade(); + let fold_placeholder = FoldPlaceholder { + constrain_width: true, + render: Arc::new(move |fold_id, fold_range, cx| { + let editor = editor.clone(); + div() + .id(fold_id) + .bg(cx.theme().colors().ghost_element_background) + .hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) + .active(|style| style.bg(cx.theme().colors().ghost_element_active)) + .rounded_sm() + .size_full() + .cursor_pointer() + .child("⋯") + .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()) + .on_click(move |_, cx| { + editor + .update(cx, |editor, cx| { + editor.unfold_ranges( + [fold_range.start..fold_range.end], + true, + false, + cx, + ); + cx.stop_propagation(); + }) + .ok(); + }) + .into_any() + }), + }; let display_map = cx.new_model(|cx| { - DisplayMap::new(buffer.clone(), style.font(), font_size, None, 2, 1, cx) + DisplayMap::new( + buffer.clone(), + style.font(), + font_size, + None, + 2, + 1, + fold_placeholder, + cx, + ) }); let selections = SelectionsCollection::new(display_map.clone(), buffer.clone()); @@ -5826,7 +5866,7 @@ impl Editor { let mut end = fold.range.end.to_point(&buffer); start.row -= row_delta; end.row -= row_delta; - refold_ranges.push((start..end, fold.text)); + refold_ranges.push((start..end, fold.placeholder.clone())); } } } @@ -5920,7 +5960,7 @@ impl Editor { let mut end = fold.range.end.to_point(&buffer); start.row += row_delta; end.row += row_delta; - refold_ranges.push((start..end, fold.text)); + refold_ranges.push((start..end, fold.placeholder.clone())); } } } @@ -9298,14 +9338,14 @@ impl Editor { let buffer_row = fold_at.buffer_row; let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - if let Some((fold_range, fold_text)) = display_map.foldable_range(buffer_row) { + if let Some((fold_range, placeholder)) = display_map.foldable_range(buffer_row) { let autoscroll = self .selections .all::(cx) .iter() .any(|selection| fold_range.overlaps(&selection.range())); - self.fold_ranges([(fold_range, fold_text)], autoscroll, cx); + self.fold_ranges([(fold_range, placeholder)], autoscroll, cx); } } @@ -9359,9 +9399,9 @@ impl Editor { .buffer_snapshot .line_len(MultiBufferRow(s.end.row)), ); - (start..end, "⋯") + (start..end, display_map.fold_placeholder.clone()) } else { - (s.start..s.end, "⋯") + (s.start..s.end, display_map.fold_placeholder.clone()) } }); self.fold_ranges(ranges, true, cx); @@ -9369,7 +9409,7 @@ impl Editor { pub fn fold_ranges( &mut self, - ranges: impl IntoIterator, &'static str)>, + ranges: impl IntoIterator, FoldPlaceholder)>, auto_scroll: bool, cx: &mut ViewContext, ) { diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index c433cfaabe..c055112768 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -496,8 +496,8 @@ fn test_clone(cx: &mut TestAppContext) { editor.change_selections(None, cx, |s| s.select_ranges(selection_ranges.clone())); editor.fold_ranges( [ - (Point::new(1, 0)..Point::new(2, 0), "⋯"), - (Point::new(3, 0)..Point::new(4, 0), "⋯"), + (Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()), + (Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()), ], true, cx, @@ -905,9 +905,9 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) { _ = view.update(cx, |view, cx| { view.fold_ranges( vec![ - (Point::new(0, 6)..Point::new(0, 12), "⋯"), - (Point::new(1, 2)..Point::new(1, 4), "⋯"), - (Point::new(2, 4)..Point::new(2, 8), "⋯"), + (Point::new(0, 6)..Point::new(0, 12), FoldPlaceholder::test()), + (Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()), + (Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()), ], true, cx, @@ -3409,9 +3409,9 @@ fn test_move_line_up_down(cx: &mut TestAppContext) { _ = view.update(cx, |view, cx| { view.fold_ranges( vec![ - (Point::new(0, 2)..Point::new(1, 2), "⋯"), - (Point::new(2, 3)..Point::new(4, 1), "⋯"), - (Point::new(7, 0)..Point::new(8, 4), "⋯"), + (Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()), + (Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()), + (Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()), ], true, cx, @@ -3893,9 +3893,9 @@ fn test_split_selection_into_lines(cx: &mut TestAppContext) { _ = view.update(cx, |view, cx| { view.fold_ranges( vec![ - (Point::new(0, 2)..Point::new(1, 2), "⋯"), - (Point::new(2, 3)..Point::new(4, 1), "⋯"), - (Point::new(7, 0)..Point::new(8, 4), "⋯"), + (Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()), + (Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()), + (Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()), ], true, cx, @@ -4550,8 +4550,14 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { _ = view.update(cx, |view, cx| { view.fold_ranges( vec![ - (Point::new(0, 21)..Point::new(0, 24), "⋯"), - (Point::new(3, 20)..Point::new(3, 22), "⋯"), + ( + Point::new(0, 21)..Point::new(0, 24), + FoldPlaceholder::test(), + ), + ( + Point::new(3, 20)..Point::new(3, 22), + FoldPlaceholder::test(), + ), ], true, cx, @@ -11973,6 +11979,7 @@ fn test_flap_insertion_and_rendering(cx: &mut TestAppContext) { let flap = Flap::new( range, + FoldPlaceholder::test(), { let toggle_callback = render_args.clone(); move |row, folded, callback, _cx| { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index d4c34ebc2e..2c3e8603f8 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -23,7 +23,6 @@ use crate::{ LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, Selection, SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, MAX_LINE_LEN, }; -use anyhow::Result; use client::ParticipantIndex; use collections::{BTreeMap, HashMap}; use git::{blame::BlameEntry, diff::DiffHunkStatus, Oid}; @@ -31,11 +30,11 @@ use gpui::{ anchored, deferred, div, fill, outline, point, px, quad, relative, size, svg, transparent_black, Action, AnchorCorner, AnyElement, AvailableSpace, Bounds, ClipboardItem, ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity, - GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, ModifiersChangedEvent, + FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, - ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size, Stateful, - StatefulInteractiveElement, Style, Styled, TextRun, TextStyle, TextStyleRefinement, View, - ViewContext, WeakView, WindowContext, + ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, + Style, Styled, TextRun, TextStyle, TextStyleRefinement, View, ViewContext, WeakView, + WindowContext, }; use itertools::Itertools; use language::language_settings::{ @@ -48,12 +47,12 @@ use project::{ ProjectPath, }; use settings::Settings; -use smallvec::SmallVec; +use smallvec::{smallvec, SmallVec}; use std::{ any::TypeId, borrow::Cow, cmp::{self, Ordering}, - fmt::Write, + fmt::{self, Write}, iter, mem, ops::{Deref, Range}, sync::Arc, @@ -857,76 +856,6 @@ impl EditorElement { (selections, active_rows, newest_selection_head) } - #[allow(clippy::too_many_arguments)] - fn layout_folds( - &self, - snapshot: &EditorSnapshot, - content_origin: gpui::Point, - visible_anchor_range: Range, - visible_display_row_range: Range, - scroll_pixel_position: gpui::Point, - line_height: Pixels, - line_layouts: &[LineWithInvisibles], - cx: &mut WindowContext, - ) -> Vec { - snapshot - .folds_in_range(visible_anchor_range.clone()) - .filter_map(|fold| { - // Skip folds that have no text. - if fold.text.is_empty() { - return None; - } - - let fold_range = fold.range.clone(); - let display_range = fold.range.start.to_display_point(&snapshot) - ..fold.range.end.to_display_point(&snapshot); - debug_assert_eq!(display_range.start.row(), display_range.end.row()); - let row = display_range.start.row(); - debug_assert!(row < visible_display_row_range.end); - let line_layout = line_layouts - .get(row.minus(visible_display_row_range.start) as usize) - .map(|l| &l.line)?; - - let start_x = content_origin.x - + line_layout.x_for_index(display_range.start.column() as usize) - - scroll_pixel_position.x; - let start_y = - content_origin.y + row.as_f32() * line_height - scroll_pixel_position.y; - let end_x = content_origin.x - + line_layout.x_for_index(display_range.end.column() as usize) - - scroll_pixel_position.x; - - let fold_bounds = Bounds { - origin: point(start_x, start_y), - size: size(end_x - start_x, line_height), - }; - - let mut hover_element = div() - .id(fold.id) - .size_full() - .cursor_pointer() - .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()) - .on_click( - cx.listener_for(&self.editor, move |editor: &mut Editor, _, cx| { - editor.unfold_ranges( - [fold_range.start..fold_range.end], - true, - false, - cx, - ); - cx.stop_propagation(); - }), - ) - .into_any(); - hover_element.prepaint_as_root(fold_bounds.origin, fold_bounds.size.into(), cx); - Some(FoldLayout { - display_range, - hover_element, - }) - }) - .collect() - } - fn collect_cursors( &self, snapshot: &EditorSnapshot, @@ -994,8 +923,7 @@ impl EditorElement { } let cursor_row_layout = &line_layouts - [cursor_position.row().minus(visible_display_row_range.start) as usize] - .line; + [cursor_position.row().minus(visible_display_row_range.start) as usize]; let cursor_column = cursor_position.column() as usize; let cursor_character_x = cursor_row_layout.x_for_index(cursor_column); @@ -1228,7 +1156,7 @@ impl EditorElement { ); let size = element.layout_as_root(available_space, cx); - let line = &lines[ix].line; + let line = &lines[ix]; let padding = if line.width == Pixels::ZERO { Pixels::ZERO } else { @@ -1373,7 +1301,7 @@ impl EditorElement { let line_end = if let Some(flap_trailer) = flap_trailer { flap_trailer.bounds.right() } else { - content_origin.x - scroll_pixel_position.x + line_layout.line.width + content_origin.x - scroll_pixel_position.x + line_layout.width }; let padded_line_end = line_end + em_width * INLINE_BLAME_PADDING_EM_WIDTHS; @@ -1860,7 +1788,7 @@ impl EditorElement { rows: Range, line_number_layouts: &[Option], snapshot: &EditorSnapshot, - cx: &WindowContext, + cx: &mut WindowContext, ) -> Vec { if rows.start >= rows.end { return Vec::new(); @@ -1894,8 +1822,11 @@ impl EditorElement { .log_err() }) .map(|line| LineWithInvisibles { - line, + width: line.width, + len: line.len, + fragments: smallvec![LineFragment::Text(line)], invisibles: Vec::new(), + font_size, }) .collect() } else { @@ -1912,6 +1843,30 @@ impl EditorElement { } } + fn prepaint_lines( + &self, + start_row: DisplayRow, + line_layouts: &mut [LineWithInvisibles], + line_height: Pixels, + scroll_pixel_position: gpui::Point, + content_origin: gpui::Point, + cx: &mut WindowContext, + ) -> SmallVec<[AnyElement; 1]> { + let mut line_elements = SmallVec::new(); + for (ix, line) in line_layouts.iter_mut().enumerate() { + let row = start_row + DisplayRow(ix as u32); + line.prepaint( + line_height, + scroll_pixel_position, + row, + content_origin, + &mut line_elements, + cx, + ); + } + line_elements + } + #[allow(clippy::too_many_arguments)] fn build_blocks( &self, @@ -1949,11 +1904,9 @@ impl EditorElement { let anchor_x = text_x + if rows.contains(&align_to.row()) { line_layouts[align_to.row().minus(rows.start) as usize] - .line .x_for_index(align_to.column() as usize) } else { layout_line(align_to.row(), snapshot, &self.style, cx) - .unwrap() .x_for_index(align_to.column() as usize) }; @@ -2310,7 +2263,7 @@ impl EditorElement { let (x, y) = match position { crate::ContextMenuOrigin::EditorPoint(point) => { - let cursor_row_layout = &line_layouts[point.row().minus(start_row) as usize].line; + let cursor_row_layout = &line_layouts[point.row().minus(start_row) as usize]; let x = cursor_row_layout.x_for_index(point.column() as usize) - scroll_pixel_position.x; let y = point.row().next_row().as_f32() * line_height - scroll_pixel_position.y; @@ -2406,7 +2359,7 @@ impl EditorElement { // This is safe because we check on layout whether the required row is available let hovered_row_layout = - &line_layouts[position.row().minus(visible_display_row_range.start) as usize].line; + &line_layouts[position.row().minus(visible_display_row_range.start) as usize]; // Compute Hovered Point let x = @@ -2920,7 +2873,6 @@ impl EditorElement { }; cx.set_cursor_style(cursor_style, &layout.text_hitbox); - cx.with_element_namespace("folds", |cx| self.paint_folds(layout, cx)); let invisible_display_ranges = self.paint_highlights(layout, cx); self.paint_lines(&invisible_display_ranges, layout, cx); self.paint_redactions(layout, cx); @@ -2979,7 +2931,7 @@ impl EditorElement { fn paint_lines( &mut self, invisible_display_ranges: &[Range], - layout: &EditorLayout, + layout: &mut EditorLayout, cx: &mut WindowContext, ) { let whitespace_setting = self @@ -3001,6 +2953,10 @@ impl EditorElement { cx, ) } + + for line_element in &mut layout.line_elements { + line_element.paint(cx); + } } fn paint_redactions(&mut self, layout: &EditorLayout, cx: &mut WindowContext) { @@ -3378,7 +3334,7 @@ impl EditorElement { .iter_rows() .map(|row| { let line_layout = - &layout.position_map.line_layouts[row.minus(start_row) as usize].line; + &layout.position_map.line_layouts[row.minus(start_row) as usize]; HighlightedRangeLine { start_x: if row == range.start.row() { layout.content_origin.x @@ -3405,37 +3361,6 @@ impl EditorElement { } } - fn paint_folds(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) { - if layout.folds.is_empty() { - return; - } - - cx.paint_layer(layout.text_hitbox.bounds, |cx| { - let fold_corner_radius = 0.15 * layout.position_map.line_height; - for mut fold in mem::take(&mut layout.folds) { - fold.hover_element.paint(cx); - - let hover_element = fold.hover_element.downcast_mut::>().unwrap(); - let fold_background = if hover_element.interactivity().active.unwrap() { - cx.theme().colors().ghost_element_active - } else if hover_element.interactivity().hovered.unwrap() { - cx.theme().colors().ghost_element_hover - } else { - cx.theme().colors().ghost_element_background - }; - - self.paint_highlighted_range( - fold.display_range.clone(), - fold_background, - fold_corner_radius, - fold_corner_radius * 2., - layout, - cx, - ); - } - }) - } - fn paint_inline_blame(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) { if let Some(mut inline_blame) = layout.inline_blame.take() { cx.paint_layer(layout.text_hitbox.bounds, |cx| { @@ -3813,8 +3738,34 @@ fn deploy_blame_entry_context_menu( #[derive(Debug)] pub(crate) struct LineWithInvisibles { - pub line: ShapedLine, + fragments: SmallVec<[LineFragment; 1]>, invisibles: Vec, + len: usize, + width: Pixels, + font_size: Pixels, +} + +#[allow(clippy::large_enum_variant)] +enum LineFragment { + Text(ShapedLine), + Element { + element: Option, + width: Pixels, + len: usize, + }, +} + +impl fmt::Debug for LineFragment { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + LineFragment::Text(shaped_line) => f.debug_tuple("Text").field(shaped_line).finish(), + LineFragment::Element { width, len, .. } => f + .debug_struct("Element") + .field("width", width) + .field("len", len) + .finish(), + } + } } impl LineWithInvisibles { @@ -3825,100 +3776,161 @@ impl LineWithInvisibles { max_line_count: usize, line_number_layouts: &[Option], editor_mode: EditorMode, - cx: &WindowContext, + cx: &mut WindowContext, ) -> Vec { let mut layouts = Vec::with_capacity(max_line_count); + let mut fragments: SmallVec<[LineFragment; 1]> = SmallVec::new(); let mut line = String::new(); let mut invisibles = Vec::new(); + let mut width = Pixels::ZERO; + let mut len = 0; let mut styles = Vec::new(); let mut non_whitespace_added = false; let mut row = 0; let mut line_exceeded_max_len = false; let font_size = text_style.font_size.to_pixels(cx.rem_size()); + let ellipsis = SharedString::from("⋯"); + for highlighted_chunk in chunks.chain([HighlightedChunk { - chunk: "\n", + text: "\n", style: None, is_tab: false, + renderer: None, }]) { - for (ix, mut line_chunk) in highlighted_chunk.chunk.split('\n').enumerate() { - if ix > 0 { + if let Some(renderer) = highlighted_chunk.renderer { + if !line.is_empty() { let shaped_line = cx .text_system() .shape_line(line.clone().into(), font_size, &styles) .unwrap(); - layouts.push(Self { - line: shaped_line, - invisibles: std::mem::take(&mut invisibles), - }); - + width += shaped_line.width; + len += shaped_line.len; + fragments.push(LineFragment::Text(shaped_line)); line.clear(); styles.clear(); - row += 1; - line_exceeded_max_len = false; - non_whitespace_added = false; - if row == max_line_count { - return layouts; - } } - if !line_chunk.is_empty() && !line_exceeded_max_len { - let text_style = if let Some(style) = highlighted_chunk.style { - Cow::Owned(text_style.clone().highlight(style)) + let available_width = if renderer.constrain_width { + let chunk = if highlighted_chunk.text == ellipsis.as_ref() { + ellipsis.clone() } else { - Cow::Borrowed(text_style) + SharedString::from(Arc::from(highlighted_chunk.text)) }; + let shaped_line = cx + .text_system() + .shape_line( + chunk, + font_size, + &[text_style.to_run(highlighted_chunk.text.len())], + ) + .unwrap(); + AvailableSpace::Definite(shaped_line.width) + } else { + AvailableSpace::MinContent + }; - 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; + let mut element = (renderer.render)(cx); + let line_height = text_style.line_height_in_pixels(cx.rem_size()); + let size = element.layout_as_root( + size(available_width, AvailableSpace::Definite(line_height)), + cx, + ); + + width += size.width; + len += highlighted_chunk.text.len(); + fragments.push(LineFragment::Element { + element: Some(element), + width: size.width, + len: highlighted_chunk.text.len(), + }); + } else { + for (ix, mut line_chunk) in highlighted_chunk.text.split('\n').enumerate() { + if ix > 0 { + let shaped_line = cx + .text_system() + .shape_line(line.clone().into(), font_size, &styles) + .unwrap(); + width += shaped_line.width; + len += shaped_line.len; + fragments.push(LineFragment::Text(shaped_line)); + layouts.push(Self { + width: mem::take(&mut width), + len: mem::take(&mut len), + fragments: mem::take(&mut fragments), + invisibles: std::mem::take(&mut invisibles), + font_size, + }); + + line.clear(); + styles.clear(); + row += 1; + line_exceeded_max_len = false; + non_whitespace_added = false; + if row == max_line_count { + return layouts; } - line_chunk = &line_chunk[..chunk_len]; - line_exceeded_max_len = true; } - styles.push(TextRun { - len: line_chunk.len(), - font: text_style.font(), - color: text_style.color, - background_color: text_style.background_color, - underline: text_style.underline, - strikethrough: text_style.strikethrough, - }); - - if editor_mode == EditorMode::Full { - // Line wrap pads its contents with fake whitespaces, - // avoid printing them - let inside_wrapped_string = line_number_layouts - .get(row) - .and_then(|layout| layout.as_ref()) - .is_none(); - if highlighted_chunk.is_tab { - if non_whitespace_added || !inside_wrapped_string { - invisibles.push(Invisible::Tab { - line_start_offset: line.len(), - }); - } + if !line_chunk.is_empty() && !line_exceeded_max_len { + let text_style = if let Some(style) = highlighted_chunk.style { + Cow::Owned(text_style.clone().highlight(style)) } else { - invisibles.extend( - line_chunk - .bytes() - .enumerate() - .filter(|(_, line_char)| { - let is_whitespace = (*line_char as char).is_whitespace(); - non_whitespace_added |= !is_whitespace; - is_whitespace - && (non_whitespace_added || !inside_wrapped_string) - }) - .map(|(whitespace_index, _)| Invisible::Whitespace { - line_offset: line.len() + whitespace_index, - }), - ) - } - } + Cow::Borrowed(text_style) + }; - line.push_str(line_chunk); + 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; + } + + styles.push(TextRun { + len: line_chunk.len(), + font: text_style.font(), + color: text_style.color, + background_color: text_style.background_color, + underline: text_style.underline, + strikethrough: text_style.strikethrough, + }); + + if editor_mode == EditorMode::Full { + // Line wrap pads its contents with fake whitespaces, + // avoid printing them + let inside_wrapped_string = line_number_layouts + .get(row) + .and_then(|layout| layout.as_ref()) + .is_none(); + if highlighted_chunk.is_tab { + if non_whitespace_added || !inside_wrapped_string { + invisibles.push(Invisible::Tab { + line_start_offset: line.len(), + }); + } + } else { + invisibles.extend( + line_chunk + .bytes() + .enumerate() + .filter(|(_, line_byte)| { + let is_whitespace = + (*line_byte as char).is_whitespace(); + non_whitespace_added |= !is_whitespace; + is_whitespace + && (non_whitespace_added || !inside_wrapped_string) + }) + .map(|(whitespace_index, _)| Invisible::Whitespace { + line_offset: line.len() + whitespace_index, + }), + ) + } + } + + line.push_str(line_chunk); + } } } } @@ -3926,6 +3938,34 @@ impl LineWithInvisibles { layouts } + fn prepaint( + &mut self, + line_height: Pixels, + scroll_pixel_position: gpui::Point, + row: DisplayRow, + content_origin: gpui::Point, + line_elements: &mut SmallVec<[AnyElement; 1]>, + cx: &mut WindowContext, + ) { + let line_y = line_height * (row.as_f32() - scroll_pixel_position.y / line_height); + let mut fragment_origin = content_origin + gpui::point(-scroll_pixel_position.x, line_y); + for fragment in &mut self.fragments { + match fragment { + LineFragment::Text(line) => { + fragment_origin.x += line.width; + } + LineFragment::Element { element, width, .. } => { + let mut element = element + .take() + .expect("you can't prepaint LineWithInvisibles twice"); + element.prepaint_at(fragment_origin, cx); + line_elements.push(element); + fragment_origin.x += *width; + } + } + } + } + fn draw( &self, layout: &EditorLayout, @@ -3939,9 +3979,20 @@ impl LineWithInvisibles { let line_y = line_height * (row.as_f32() - layout.position_map.scroll_pixel_position.y / line_height); - let line_origin = + let mut fragment_origin = content_origin + gpui::point(-layout.position_map.scroll_pixel_position.x, line_y); - self.line.paint(line_origin, line_height, cx).log_err(); + + for fragment in &self.fragments { + match fragment { + LineFragment::Text(line) => { + line.paint(fragment_origin, line_height, cx).log_err(); + fragment_origin.x += line.width; + } + LineFragment::Element { width, .. } => { + fragment_origin.x += *width; + } + } + } self.draw_invisibles( &selection_ranges, @@ -3979,7 +4030,7 @@ impl LineWithInvisibles { Invisible::Whitespace { line_offset } => (line_offset, &layout.space_invisible), }; - let x_offset = self.line.x_for_index(token_offset); + let x_offset = self.x_for_index(token_offset); let invisible_offset = (layout.position_map.em_width - invisible_symbol.width).max(Pixels::ZERO) / 2.0; let origin = content_origin @@ -4000,6 +4051,90 @@ impl LineWithInvisibles { invisible_symbol.paint(origin, line_height, cx).log_err(); } } + + pub fn x_for_index(&self, index: usize) -> Pixels { + let mut fragment_start_x = Pixels::ZERO; + let mut fragment_start_index = 0; + + for fragment in &self.fragments { + match fragment { + LineFragment::Text(shaped_line) => { + let fragment_end_index = fragment_start_index + shaped_line.len; + if index < fragment_end_index { + return fragment_start_x + + shaped_line.x_for_index(index - fragment_start_index); + } + fragment_start_x += shaped_line.width; + fragment_start_index = fragment_end_index; + } + LineFragment::Element { len, width, .. } => { + let fragment_end_index = fragment_start_index + len; + if index < fragment_end_index { + return fragment_start_x; + } + fragment_start_x += *width; + fragment_start_index = fragment_end_index; + } + } + } + + fragment_start_x + } + + pub fn index_for_x(&self, x: Pixels) -> Option { + let mut fragment_start_x = Pixels::ZERO; + let mut fragment_start_index = 0; + + for fragment in &self.fragments { + match fragment { + LineFragment::Text(shaped_line) => { + let fragment_end_x = fragment_start_x + shaped_line.width; + if x < fragment_end_x { + return Some( + fragment_start_index + shaped_line.index_for_x(x - fragment_start_x)?, + ); + } + fragment_start_x = fragment_end_x; + fragment_start_index += shaped_line.len; + } + LineFragment::Element { len, width, .. } => { + let fragment_end_x = fragment_start_x + *width; + if x < fragment_end_x { + return Some(fragment_start_index); + } + fragment_start_index += len; + fragment_start_x = fragment_end_x; + } + } + } + + None + } + + pub fn font_id_for_index(&self, index: usize) -> Option { + let mut fragment_start_index = 0; + + for fragment in &self.fragments { + match fragment { + LineFragment::Text(shaped_line) => { + let fragment_end_index = fragment_start_index + shaped_line.len; + if index < fragment_end_index { + return shaped_line.font_id_for_index(index - fragment_start_index); + } + fragment_start_index = fragment_end_index; + } + LineFragment::Element { len, .. } => { + let fragment_end_index = fragment_start_index + len; + if index < fragment_end_index { + return None; + } + fragment_start_index = fragment_end_index; + } + } + } + + None + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -4309,18 +4444,16 @@ impl Element for EditorElement { ); let mut max_visible_line_width = Pixels::ZERO; - let line_layouts = + let mut line_layouts = self.layout_lines(start_row..end_row, &line_numbers, &snapshot, cx); for line_with_invisibles in &line_layouts { - if line_with_invisibles.line.width > max_visible_line_width { - max_visible_line_width = line_with_invisibles.line.width; + if line_with_invisibles.width > max_visible_line_width { + max_visible_line_width = line_with_invisibles.width; } } let longest_line_width = - layout_line(snapshot.longest_row(), &snapshot, &style, cx) - .unwrap() - .width; + layout_line(snapshot.longest_row(), &snapshot, &style, cx).width; let mut scroll_width = longest_line_width.max(max_visible_line_width) + overscroll.width; @@ -4430,6 +4563,15 @@ impl Element for EditorElement { } }); + let line_elements = self.prepaint_lines( + start_row, + &mut line_layouts, + line_height, + scroll_pixel_position, + content_origin, + cx, + ); + cx.with_element_namespace("blocks", |cx| { self.layout_blocks( &mut blocks, @@ -4470,19 +4612,6 @@ impl Element for EditorElement { cx, ); - let folds = cx.with_element_namespace("folds", |cx| { - self.layout_folds( - &snapshot, - content_origin, - start_anchor..end_anchor, - start_row..end_row, - scroll_pixel_position, - line_height, - &line_layouts, - cx, - ) - }); - let gutter_settings = EditorSettings::get_global(cx).gutter; let mut context_menu_visible = false; @@ -4628,11 +4757,11 @@ impl Element for EditorElement { highlighted_rows, highlighted_ranges, redacted_ranges, + line_elements, line_numbers, display_hunks, blamed_display_rows, inline_blame, - folds, blocks, cursors, visible_cursors, @@ -4750,11 +4879,11 @@ pub struct EditorLayout { visible_display_row_range: Range, active_rows: BTreeMap, highlighted_rows: BTreeMap, + line_elements: SmallVec<[AnyElement; 1]>, line_numbers: Vec>, display_hunks: Vec<(DisplayDiffHunk, Option)>, blamed_display_rows: Option>, inline_blame: Option, - folds: Vec, blocks: Vec, highlighted_ranges: Vec<(Range, Hsla)>, redacted_ranges: Vec>, @@ -4893,11 +5022,6 @@ struct FlapTrailerLayout { bounds: Bounds, } -struct FoldLayout { - display_range: Range, - hover_element: AnyElement, -} - struct PositionMap { size: Size, line_height: Pixels, @@ -4942,7 +5066,6 @@ impl PositionMap { let (column, x_overshoot_after_line_end) = if let Some(line) = self .line_layouts .get(row as usize - scroll_position.y as usize) - .map(|LineWithInvisibles { line, .. }| line) { if let Some(ix) = line.index_for_x(x) { (ix as u32, px(0.)) @@ -4979,37 +5102,12 @@ fn layout_line( row: DisplayRow, snapshot: &EditorSnapshot, style: &EditorStyle, - cx: &WindowContext, -) -> Result { - let mut line = snapshot.line(row); - - let len = { - let line_len = line.len(); - if line_len > MAX_LINE_LEN { - let mut len = MAX_LINE_LEN; - while !line.is_char_boundary(len) { - len -= 1; - } - - line.truncate(len); - len - } else { - line_len - } - }; - - cx.text_system().shape_line( - line.into(), - style.text.font_size.to_pixels(cx.rem_size()), - &[TextRun { - len, - font: style.text.font(), - color: Hsla::default(), - background_color: None, - underline: None, - strikethrough: None, - }], - ) + cx: &mut WindowContext, +) -> LineWithInvisibles { + let chunks = snapshot.highlighted_chunks(row..row + DisplayRow(1), true, style); + LineWithInvisibles::from_chunks(chunks, &style.text, MAX_LINE_LEN, 1, &[], snapshot.mode, cx) + .pop() + .unwrap() } #[derive(Debug)] diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index d750b70267..510d9569aa 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -577,7 +577,7 @@ mod tests { use crate::{ display_map::Inlay, test::{editor_test_context::EditorTestContext, marked_display_snapshot}, - Buffer, DisplayMap, DisplayRow, ExcerptRange, InlayId, MultiBuffer, + Buffer, DisplayMap, DisplayRow, ExcerptRange, FoldPlaceholder, InlayId, MultiBuffer, }; use gpui::{font, Context as _}; use language::Capability; @@ -695,8 +695,18 @@ mod tests { let font_size = px(14.0); let buffer = MultiBuffer::build_simple(input_text, cx); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let display_map = - cx.new_model(|cx| DisplayMap::new(buffer, font, font_size, None, 1, 1, cx)); + let display_map = cx.new_model(|cx| { + DisplayMap::new( + buffer, + font, + font_size, + None, + 1, + 1, + FoldPlaceholder::test(), + cx, + ) + }); // add all kinds of inlays between two word boundaries: we should be able to cross them all, when looking for another boundary let mut id = 0; @@ -901,8 +911,18 @@ mod tests { ); multibuffer }); - let display_map = - cx.new_model(|cx| DisplayMap::new(multibuffer, font, px(14.0), None, 2, 2, cx)); + let display_map = cx.new_model(|cx| { + DisplayMap::new( + multibuffer, + font, + px(14.0), + None, + 2, + 2, + FoldPlaceholder::test(), + cx, + ) + }); let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn"); diff --git a/crates/editor/src/scroll/autoscroll.rs b/crates/editor/src/scroll/autoscroll.rs index 5c33532f0c..cde450f11b 100644 --- a/crates/editor/src/scroll/autoscroll.rs +++ b/crates/editor/src/scroll/autoscroll.rs @@ -251,12 +251,10 @@ impl Editor { let end_column = cmp::min(display_map.line_len(head.row()), head.column() + 3); target_left = target_left.min( layouts[head.row().minus(start_row) as usize] - .line .x_for_index(start_column as usize), ); target_right = target_right.max( layouts[head.row().minus(start_row) as usize] - .line .x_for_index(end_column as usize) + max_glyph_width, ); diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index 3a829923c6..f1bdb26f34 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -3,11 +3,9 @@ pub mod editor_test_context; use crate::{ display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint}, - DisplayPoint, Editor, EditorMode, MultiBuffer, + DisplayPoint, Editor, EditorMode, FoldPlaceholder, MultiBuffer, }; - use gpui::{Context, Font, FontFeatures, FontStyle, FontWeight, Model, Pixels, ViewContext}; - use project::Project; use util::test::{marked_text_offsets, marked_text_ranges}; @@ -35,7 +33,18 @@ pub fn marked_display_snapshot( let font_size: Pixels = 14usize.into(); let buffer = MultiBuffer::build_simple(&unmarked_text, cx); - let display_map = cx.new_model(|cx| DisplayMap::new(buffer, font, font_size, None, 1, 1, cx)); + let display_map = cx.new_model(|cx| { + DisplayMap::new( + buffer, + font, + font_size, + None, + 1, + 1, + FoldPlaceholder::test(), + cx, + ) + }); let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); let markers = markers .into_iter() diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index b892571e0f..e34033dfe5 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -143,7 +143,7 @@ impl StyledText { } } - /// todo!() + /// Get the layout for this element. This can be used to map indices to pixels and vice versa. pub fn layout(&self) -> &TextLayout { &self.layout } @@ -232,7 +232,7 @@ impl IntoElement for StyledText { } } -/// todo!() +/// The Layout for TextElement. This can be used to map indices to pixels and vice versa. #[derive(Default, Clone)] pub struct TextLayout(Arc>>); @@ -358,7 +358,7 @@ impl TextLayout { } } - /// todo!() + /// Get the byte index into the input of the pixel position. pub fn index_for_position(&self, mut position: Point) -> Result { let element_state = self.lock(); let element_state = element_state @@ -392,7 +392,7 @@ impl TextLayout { Err(line_start_ix.saturating_sub(1)) } - /// todo!() + /// Get the pixel position for the given byte index. pub fn position_for_index(&self, index: usize) -> Option> { let element_state = self.lock(); let element_state = element_state @@ -423,17 +423,17 @@ impl TextLayout { None } - /// todo!() + /// The bounds of this layout. pub fn bounds(&self) -> Bounds { self.0.lock().as_ref().unwrap().bounds.unwrap() } - /// todo!() + /// The line height for this layout. pub fn line_height(&self) -> Pixels { self.0.lock().as_ref().unwrap().line_height } - /// todo!() + /// The text for this layout. pub fn text(&self) -> String { self.0 .lock() diff --git a/crates/gpui/src/text_system/line_layout.rs b/crates/gpui/src/text_system/line_layout.rs index aa1b96b1cc..af01eb70ed 100644 --- a/crates/gpui/src/text_system/line_layout.rs +++ b/crates/gpui/src/text_system/line_layout.rs @@ -302,7 +302,7 @@ impl WrappedLineLayout { } } - /// todo!() + /// Returns the pixel position for the given byte index. pub fn position_for_index(&self, index: usize, line_height: Pixels) -> Option> { let mut line_start_ix = 0; let mut line_end_indices = self diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index ab7775960a..bde5a8442a 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -19,7 +19,10 @@ use crate::{ use anyhow::{anyhow, Context, Result}; pub use clock::ReplicaId; use futures::channel::oneshot; -use gpui::{AppContext, EventEmitter, HighlightStyle, ModelContext, Task, TaskLabel}; +use gpui::{ + AnyElement, AppContext, EventEmitter, HighlightStyle, ModelContext, Task, TaskLabel, + WindowContext, +}; use lazy_static::lazy_static; use lsp::LanguageServerId; use parking_lot::Mutex; @@ -31,6 +34,7 @@ use std::{ cmp::{self, Ordering}, collections::BTreeMap, ffi::OsStr, + fmt, future::Future, iter::{self, Iterator, Peekable}, mem, @@ -461,7 +465,7 @@ pub struct BufferChunks<'a> { /// A chunk of a buffer's text, along with its syntax highlight and /// diagnostic status. -#[derive(Clone, Copy, Debug, Default)] +#[derive(Clone, Debug, Default)] pub struct Chunk<'a> { /// The text of the chunk. pub text: &'a str, @@ -476,6 +480,25 @@ pub struct Chunk<'a> { pub is_unnecessary: bool, /// Whether this chunk of text was originally a tab character. pub is_tab: bool, + /// An optional recipe for how the chunk should be presented. + pub renderer: Option, +} + +/// A recipe for how the chunk should be presented. +#[derive(Clone)] +pub struct ChunkRenderer { + /// creates a custom element to represent this chunk. + pub render: Arc AnyElement>, + /// If true, the element is constrained to the shaped width of the text. + pub constrain_width: bool, +} + +impl fmt::Debug for ChunkRenderer { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("ChunkRenderer") + .field("constrain_width", &self.constrain_width) + .finish() + } } /// A set of edits to a given version of a buffer, computed asynchronously.