diff --git a/Cargo.lock b/Cargo.lock index f622754379..686157fb85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2587,6 +2587,7 @@ dependencies = [ "serde", "similar", "smol", + "sum_tree", "text", "theme", "tree-sitter", diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index ed0d1b7413..46250a55df 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -11,6 +11,7 @@ test-support = [ "text/test-support", "language/test-support", "gpui/test-support", + "util/test-support", ] [dependencies] @@ -37,6 +38,7 @@ smol = "1.2" text = { path = "../text", features = ["test-support"] } language = { path = "../language", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } +util = { path = "../util", features = ["test-support"] } ctor = "0.1" env_logger = "0.8" rand = "0.8" diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index fa0f173318..9167356795 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -459,6 +459,7 @@ mod tests { use rand::{prelude::StdRng, Rng}; use std::{env, sync::Arc}; use theme::SyntaxTheme; + use util::test::sample_text; use Bias::*; #[gpui::test(iterations = 100)] @@ -720,7 +721,7 @@ mod tests { #[gpui::test] fn test_text_chunks(cx: &mut gpui::MutableAppContext) { - let text = sample_text(6, 6); + let text = sample_text(6, 6, 'a'); let buffer = cx.add_model(|cx| Buffer::new(0, text, cx)); let tab_size = 4; let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap(); diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 0e920c5e6a..31edebe99c 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -1064,16 +1064,17 @@ impl FoldEdit { #[cfg(test)] mod tests { use super::*; - use crate::{test::sample_text, ToPoint}; + use crate::ToPoint; use language::Buffer; use rand::prelude::*; use std::{env, mem}; use text::RandomCharIter; + use util::test::sample_text; use Bias::{Left, Right}; #[gpui::test] fn test_basic_folds(cx: &mut gpui::MutableAppContext) { - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(5, 6), cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(5, 6, 'a'), cx)); let buffer_snapshot = buffer.read(cx).snapshot(); let mut map = FoldMap::new(buffer_snapshot.clone()).0; @@ -1187,7 +1188,7 @@ mod tests { #[gpui::test] fn test_overlapping_folds(cx: &mut gpui::MutableAppContext) { - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(5, 6), cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(5, 6, 'a'), cx)); let buffer_snapshot = buffer.read(cx).snapshot(); let mut map = FoldMap::new(buffer_snapshot.clone()).0; let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]); @@ -1203,7 +1204,7 @@ mod tests { #[gpui::test] fn test_merging_folds_via_edit(cx: &mut gpui::MutableAppContext) { - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(5, 6), cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(5, 6, 'a'), cx)); let buffer_snapshot = buffer.read(cx).snapshot(); let mut map = FoldMap::new(buffer_snapshot.clone()).0; @@ -1226,7 +1227,7 @@ mod tests { #[gpui::test] fn test_folds_in_range(cx: &mut gpui::MutableAppContext) { - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(5, 6), cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(5, 6, 'a'), cx)); let buffer_snapshot = buffer.read(cx).snapshot(); let mut map = FoldMap::new(buffer_snapshot.clone()).0; let buffer = buffer.read(cx); @@ -1471,7 +1472,7 @@ mod tests { #[gpui::test] fn test_buffer_rows(cx: &mut gpui::MutableAppContext) { - let text = sample_text(6, 6) + "\n"; + let text = sample_text(6, 6, 'a') + "\n"; let buffer = cx.add_model(|cx| Buffer::new(0, text, cx)); let buffer_snapshot = buffer.read(cx).snapshot(); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 539736aca2..4299807f97 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3676,9 +3676,9 @@ pub fn diagnostic_style( #[cfg(test)] mod tests { use super::*; - use crate::test::sample_text; use text::Point; use unindent::Unindent; + use util::test::sample_text; #[gpui::test] fn test_selection_with_mouse(cx: &mut gpui::MutableAppContext) { @@ -3912,7 +3912,7 @@ mod tests { #[gpui::test] fn test_move_cursor(cx: &mut gpui::MutableAppContext) { - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6), cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'a'), cx)); let settings = EditorSettings::test(&cx); let (_, view) = cx.add_window(Default::default(), |cx| { build_editor(buffer.clone(), settings, cx) @@ -4708,7 +4708,7 @@ mod tests { #[gpui::test] fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) { let settings = EditorSettings::test(&cx); - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(10, 5), cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(10, 5, 'a'), cx)); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); view.update(cx, |view, cx| { view.fold_ranges( @@ -4954,7 +4954,7 @@ mod tests { #[gpui::test] fn test_select_line(cx: &mut gpui::MutableAppContext) { let settings = EditorSettings::test(&cx); - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 5), cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 5, 'a'), cx)); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); view.update(cx, |view, cx| { view.select_display_ranges( @@ -5000,7 +5000,7 @@ mod tests { #[gpui::test] fn test_split_selection_into_lines(cx: &mut gpui::MutableAppContext) { let settings = EditorSettings::test(&cx); - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(9, 5), cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(9, 5, 'a'), cx)); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); view.update(cx, |view, cx| { view.fold_ranges( diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index db16b3f01d..45764e65fb 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1164,17 +1164,15 @@ fn scale_horizontal_mouse_autoscroll_delta(delta: f32) -> f32 { #[cfg(test)] mod tests { use super::*; - use crate::{ - test::sample_text, - {Editor, EditorSettings}, - }; + use crate::{Editor, EditorSettings}; use language::Buffer; + use util::test::sample_text; #[gpui::test] fn test_layout_line_numbers(cx: &mut gpui::MutableAppContext) { let settings = EditorSettings::test(cx); - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6), cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'a'), cx)); let (window_id, editor) = cx.add_window(Default::default(), |cx| { Editor::for_buffer( buffer, diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index 26f29364fd..3fb538dfbd 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -9,19 +9,6 @@ fn init_logger() { env_logger::init(); } -pub fn sample_text(rows: usize, cols: usize) -> String { - let mut text = String::new(); - for row in 0..rows { - let c: char = ('a' as u32 + row as u32) as u8 as char; - let mut line = c.to_string().repeat(cols); - if row < rows - 1 { - line.push('\n'); - } - text += &line; - } - text -} - pub struct Observer(PhantomData); impl Entity for Observer { diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index ad0f84b4dc..16c1f6edee 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -12,6 +12,7 @@ test-support = [ "text/test-support", "lsp/test-support", "tree-sitter-rust", + "util/test-support", ] [dependencies] @@ -20,6 +21,7 @@ clock = { path = "../clock" } gpui = { path = "../gpui" } lsp = { path = "../lsp" } rpc = { path = "../rpc" } +sum_tree = { path = "../sum_tree" } theme = { path = "../theme" } util = { path = "../util" } anyhow = "1.0.38" @@ -39,6 +41,7 @@ tree-sitter-rust = { version = "0.19.0", optional = true } text = { path = "../text", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } lsp = { path = "../lsp", features = ["test-support"] } +util = { path = "../util", features = ["test-support"] } rand = "0.8.3" tree-sitter-rust = "0.19.0" unindent = "0.1.7" diff --git a/crates/language/src/fragment_list.rs b/crates/language/src/fragment_list.rs new file mode 100644 index 0000000000..deef5570a9 --- /dev/null +++ b/crates/language/src/fragment_list.rs @@ -0,0 +1,291 @@ +use std::{ + cmp, + ops::{Deref, Range}, +}; +use sum_tree::{Bias, Cursor, SumTree}; +use text::TextSummary; +use theme::SyntaxTheme; +use util::post_inc; + +use crate::{buffer, Buffer, Chunk}; +use gpui::{Entity, ModelContext, ModelHandle}; + +const NEWLINES: &'static [u8] = &[b'\n'; u8::MAX as usize]; + +pub trait ToOffset { + fn to_offset<'a>(&self, content: &Snapshot) -> usize; +} + +pub type FragmentId = usize; + +#[derive(Default)] +pub struct FragmentList { + snapshot: Snapshot, + next_fragment_id: FragmentId, +} + +#[derive(Clone, Default)] +pub struct Snapshot { + entries: SumTree, +} + +pub struct FragmentProperties<'a, T> { + buffer: &'a ModelHandle, + range: Range, + header_height: u8, +} + +#[derive(Clone)] +struct Entry { + buffer: buffer::Snapshot, + buffer_id: usize, + buffer_range: Range, + text_summary: TextSummary, + header_height: u8, +} + +#[derive(Clone, Debug, Default)] +struct EntrySummary { + min_buffer_id: usize, + max_buffer_id: usize, + text: TextSummary, +} + +pub struct Chunks<'a> { + range: Range, + cursor: Cursor<'a, Entry, usize>, + header_height: u8, + entry_chunks: Option>, + theme: Option<&'a SyntaxTheme>, +} + +impl FragmentList { + pub fn new() -> Self { + Self::default() + } + + pub fn push<'a, O: text::ToOffset>( + &mut self, + props: FragmentProperties<'a, O>, + cx: &mut ModelContext, + ) -> FragmentId { + let id = post_inc(&mut self.next_fragment_id); + + let buffer = props.buffer.read(cx); + let buffer_range = props.range.start.to_offset(buffer)..props.range.end.to_offset(buffer); + let mut text_summary = + buffer.text_summary_for_range::(buffer_range.clone()); + if props.header_height > 0 { + text_summary.first_line_chars = 0; + text_summary.lines.row += props.header_height as u32; + text_summary.lines_utf16.row += props.header_height as u32; + text_summary.bytes += props.header_height as usize; + } + + self.snapshot.entries.push( + Entry { + buffer: props.buffer.read(cx).snapshot(), + buffer_id: props.buffer.id(), + buffer_range, + text_summary, + header_height: props.header_height, + }, + &(), + ); + + id + } +} + +impl Deref for FragmentList { + type Target = Snapshot; + + fn deref(&self) -> &Self::Target { + &self.snapshot + } +} + +impl Entity for FragmentList { + type Event = (); +} + +impl Snapshot { + pub fn text(&self) -> String { + self.chunks(0..self.len(), None) + .map(|chunk| chunk.text) + .collect() + } + + pub fn len(&self) -> usize { + self.entries.summary().text.bytes + } + + pub fn chunks<'a, T: ToOffset>( + &'a self, + range: Range, + theme: Option<&'a SyntaxTheme>, + ) -> Chunks<'a> { + let range = range.start.to_offset(self)..range.end.to_offset(self); + let mut cursor = self.entries.cursor::(); + cursor.seek(&range.start, Bias::Right, &()); + + let entry_chunks = cursor.item().map(|entry| { + let buffer_start = entry.buffer_range.start + (range.start - cursor.start()); + let buffer_end = cmp::min( + entry.buffer_range.end, + entry.buffer_range.start + (range.end - cursor.start()), + ); + entry.buffer.chunks(buffer_start..buffer_end, theme) + }); + let header_height = cursor.item().map_or(0, |entry| entry.header_height); + + Chunks { + range, + cursor, + header_height, + entry_chunks, + theme, + } + } +} + +impl sum_tree::Item for Entry { + type Summary = EntrySummary; + + fn summary(&self) -> Self::Summary { + EntrySummary { + min_buffer_id: self.buffer_id, + max_buffer_id: self.buffer_id, + text: self.text_summary.clone(), + } + } +} + +impl sum_tree::Summary for EntrySummary { + type Context = (); + + fn add_summary(&mut self, summary: &Self, _: &()) { + self.min_buffer_id = cmp::min(self.min_buffer_id, summary.min_buffer_id); + self.max_buffer_id = cmp::max(self.max_buffer_id, summary.max_buffer_id); + self.text.add_summary(&summary.text, &()); + } +} + +impl<'a> sum_tree::Dimension<'a, EntrySummary> for usize { + fn add_summary(&mut self, summary: &'a EntrySummary, _: &()) { + *self += summary.text.bytes + } +} + +impl<'a> Iterator for Chunks<'a> { + type Item = Chunk<'a>; + + fn next(&mut self) -> Option { + if self.header_height > 0 { + let chunk = Chunk { + text: unsafe { + std::str::from_utf8_unchecked(&NEWLINES[..self.header_height as usize]) + }, + ..Default::default() + }; + self.header_height = 0; + return Some(chunk); + } + + if let Some(entry_chunks) = self.entry_chunks.as_mut() { + if let Some(chunk) = entry_chunks.next() { + return Some(chunk); + } else { + self.entry_chunks.take(); + } + } + + self.cursor.next(&()); + let entry = self.cursor.item()?; + + let buffer_end = cmp::min( + entry.buffer_range.end, + entry.buffer_range.start + (self.range.end - self.cursor.start()), + ); + + self.header_height = entry.header_height; + self.entry_chunks = Some( + entry + .buffer + .chunks(entry.buffer_range.start..buffer_end, self.theme), + ); + + Some(Chunk { + text: "\n", + ..Default::default() + }) + } +} + +impl ToOffset for usize { + fn to_offset<'a>(&self, _: &Snapshot) -> usize { + *self + } +} + +#[cfg(test)] +mod tests { + use super::{FragmentList, FragmentProperties}; + use crate::Buffer; + use gpui::MutableAppContext; + use text::Point; + use util::test::sample_text; + + #[gpui::test] + fn test_fragment_buffer(cx: &mut MutableAppContext) { + let buffer_1 = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'a'), cx)); + let buffer_2 = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'g'), cx)); + + let list = cx.add_model(|cx| { + let mut list = FragmentList::new(); + + list.push( + FragmentProperties { + buffer: &buffer_1, + range: Point::new(1, 2)..Point::new(2, 5), + header_height: 2, + }, + cx, + ); + list.push( + FragmentProperties { + buffer: &buffer_1, + range: Point::new(3, 3)..Point::new(4, 4), + header_height: 1, + }, + cx, + ); + list.push( + FragmentProperties { + buffer: &buffer_2, + range: Point::new(3, 1)..Point::new(3, 3), + header_height: 3, + }, + cx, + ); + list + }); + + assert_eq!( + list.read(cx).text(), + concat!( + "\n", // Preserve newlines + "\n", // + "bbbb\n", // + "ccccc\n", // + "\n", // + "ddd\n", // + "eeee\n", // + "\n", // + "\n", // + "\n", // + "jj" // + ) + ) + } +} diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 77d01c7ecf..704e9b967c 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -1,4 +1,5 @@ mod buffer; +mod fragment_list; mod highlight_map; pub mod proto; #[cfg(test)] diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index a434e97e2e..0fef4aac82 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -49,7 +49,7 @@ pub struct Buffer { subscriptions: Vec>>>>, } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Snapshot { visible_text: Rope, deleted_text: Rope, diff --git a/crates/util/src/test.rs b/crates/util/src/test.rs index 57a4b21105..71b847df69 100644 --- a/crates/util/src/test.rs +++ b/crates/util/src/test.rs @@ -35,3 +35,16 @@ fn write_tree(path: &Path, tree: serde_json::Value) { panic!("You must pass a JSON object to this helper") } } + +pub fn sample_text(rows: usize, cols: usize, start_char: char) -> String { + let mut text = String::new(); + for row in 0..rows { + let c: char = (start_char as u32 + row as u32) as u8 as char; + let mut line = c.to_string().repeat(cols); + if row < rows - 1 { + line.push('\n'); + } + text += &line; + } + text +}