Start on a new FragmentList

Nathan Sobo created

Here I'm exploring a new approach to the project-wide diagnostics view that can exactly mirror the contents of cargo check. The `FragmentList` composes an arbitrary list of fragments from other buffers and presents them as if they were a single buffer.

Change summary

Cargo.lock                                |   1 
crates/editor/Cargo.toml                  |   2 
crates/editor/src/display_map.rs          |   3 
crates/editor/src/display_map/fold_map.rs |  13 
crates/editor/src/editor.rs               |  10 
crates/editor/src/element.rs              |   8 
crates/editor/src/test.rs                 |  13 -
crates/language/Cargo.toml                |   3 
crates/language/src/fragment_list.rs      | 291 +++++++++++++++++++++++++
crates/language/src/language.rs           |   1 
crates/text/src/text.rs                   |   2 
crates/util/src/test.rs                   |  13 +
12 files changed, 329 insertions(+), 31 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -2587,6 +2587,7 @@ dependencies = [
  "serde",
  "similar",
  "smol",
+ "sum_tree",
  "text",
  "theme",
  "tree-sitter",

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"

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();

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();

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(

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,

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<T>(PhantomData<T>);
 
 impl<T: 'static> Entity for Observer<T> {

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"

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<Entry>,
+}
+
+pub struct FragmentProperties<'a, T> {
+    buffer: &'a ModelHandle<Buffer>,
+    range: Range<T>,
+    header_height: u8,
+}
+
+#[derive(Clone)]
+struct Entry {
+    buffer: buffer::Snapshot,
+    buffer_id: usize,
+    buffer_range: Range<usize>,
+    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<usize>,
+    cursor: Cursor<'a, Entry, usize>,
+    header_height: u8,
+    entry_chunks: Option<buffer::Chunks<'a>>,
+    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<Self>,
+    ) -> 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::<TextSummary, _>(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<T>,
+        theme: Option<&'a SyntaxTheme>,
+    ) -> Chunks<'a> {
+        let range = range.start.to_offset(self)..range.end.to_offset(self);
+        let mut cursor = self.entries.cursor::<usize>();
+        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<Self::Item> {
+        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"       //
+            )
+        )
+    }
+}

crates/text/src/text.rs 🔗

@@ -49,7 +49,7 @@ pub struct Buffer {
     subscriptions: Vec<Weak<Mutex<Vec<Patch<usize>>>>>,
 }
 
-#[derive(Clone)]
+#[derive(Clone, Debug)]
 pub struct Snapshot {
     visible_text: Rope,
     deleted_text: Rope,

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
+}