Merge pull request #1142 from zed-industries/multibuffer-jump-to

Antonio Scandurra created

Show "Jump to Buffer" icon on every excerpt header

Change summary

crates/diagnostics/src/diagnostics.rs      |  99 ------
crates/editor/src/display_map/block_map.rs |   8 
crates/editor/src/editor.rs                |  91 +++++
crates/editor/src/element.rs               |  71 ++++
crates/editor/src/movement.rs              |  12 
crates/editor/src/multi_buffer.rs          | 326 +++++++++++++++++------
crates/language/src/buffer.rs              |  24 
crates/project/src/project.rs              |   8 
crates/project/src/worktree.rs             |   4 
crates/search/src/buffer_search.rs         |   2 
crates/theme/src/theme.rs                  |   2 
styles/src/styleTree/editor.ts             |  16 
12 files changed, 442 insertions(+), 221 deletions(-)

Detailed changes

crates/diagnostics/src/diagnostics.rs 🔗

@@ -5,16 +5,17 @@ use collections::{BTreeMap, HashSet};
 use editor::{
     diagnostic_block_renderer,
     display_map::{BlockDisposition, BlockId, BlockProperties, RenderBlock},
-    highlight_diagnostic_message, Autoscroll, Editor, ExcerptId, MultiBuffer, ToOffset,
+    highlight_diagnostic_message, Autoscroll, Editor, ExcerptId, ExcerptRange, MultiBuffer,
+    ToOffset,
 };
 use gpui::{
-    actions, elements::*, fonts::TextStyle, impl_internal_actions, platform::CursorStyle,
-    serde_json, AnyViewHandle, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext,
-    Task, View, ViewContext, ViewHandle, WeakViewHandle,
+    actions, elements::*, fonts::TextStyle, impl_internal_actions, serde_json, AnyViewHandle,
+    AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext,
+    ViewHandle, WeakViewHandle,
 };
 use language::{
     Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection,
-    SelectionGoal, ToPoint,
+    SelectionGoal,
 };
 use project::{DiagnosticSummary, Project, ProjectPath};
 use serde_json::json;
@@ -27,7 +28,7 @@ use std::{
     path::PathBuf,
     sync::Arc,
 };
-use util::{ResultExt, TryFutureExt};
+use util::TryFutureExt;
 use workspace::{ItemHandle as _, ItemNavHistory, Workspace};
 
 actions!(diagnostics, [Deploy]);
@@ -38,7 +39,6 @@ const CONTEXT_LINE_COUNT: u32 = 1;
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(ProjectDiagnosticsEditor::deploy);
-    cx.add_action(ProjectDiagnosticsEditor::jump);
     items::init(cx);
 }
 
@@ -194,30 +194,6 @@ impl ProjectDiagnosticsEditor {
         }
     }
 
-    fn jump(workspace: &mut Workspace, action: &Jump, cx: &mut ViewContext<Workspace>) {
-        let editor = workspace.open_path(action.path.clone(), true, cx);
-        let position = action.position;
-        let anchor = action.anchor;
-        cx.spawn_weak(|_, mut cx| async move {
-            let editor = editor.await.log_err()?.downcast::<Editor>()?;
-            editor.update(&mut cx, |editor, cx| {
-                let buffer = editor.buffer().read(cx).as_singleton()?;
-                let buffer = buffer.read(cx);
-                let cursor = if buffer.can_resolve(&anchor) {
-                    anchor.to_point(buffer)
-                } else {
-                    buffer.clip_point(position, Bias::Left)
-                };
-                editor.change_selections(Some(Autoscroll::Newest), cx, |s| {
-                    s.select_ranges([cursor..cursor]);
-                });
-                Some(())
-            })?;
-            Some(())
-        })
-        .detach()
-    }
-
     fn update_excerpts(&mut self, language_server_id: Option<usize>, cx: &mut ViewContext<Self>) {
         let mut paths = Vec::new();
         self.paths_to_update.retain(|path, server_id| {
@@ -348,7 +324,10 @@ impl ProjectDiagnosticsEditor {
                                 .insert_excerpts_after(
                                     &prev_excerpt_id,
                                     buffer.clone(),
-                                    [excerpt_start..excerpt_end],
+                                    [ExcerptRange {
+                                        context: excerpt_start..excerpt_end,
+                                        primary: Some(range.clone()),
+                                    }],
                                     excerpts_cx,
                                 )
                                 .pop()
@@ -363,20 +342,13 @@ impl ProjectDiagnosticsEditor {
                                 is_first_excerpt_for_group = false;
                                 let mut primary =
                                     group.entries[group.primary_ix].diagnostic.clone();
-                                let anchor = group.entries[group.primary_ix].range.start;
-                                let position = anchor.to_point(&snapshot);
                                 primary.message =
                                     primary.message.split('\n').next().unwrap().to_string();
                                 group_state.block_count += 1;
                                 blocks_to_add.push(BlockProperties {
                                     position: header_position,
                                     height: 2,
-                                    render: diagnostic_header_renderer(
-                                        primary,
-                                        path.clone(),
-                                        position,
-                                        anchor,
-                                    ),
+                                    render: diagnostic_header_renderer(primary),
                                     disposition: BlockDisposition::Above,
                                 });
                             }
@@ -633,18 +605,10 @@ impl workspace::Item for ProjectDiagnosticsEditor {
     }
 }
 
-fn diagnostic_header_renderer(
-    diagnostic: Diagnostic,
-    path: ProjectPath,
-    position: Point,
-    anchor: Anchor,
-) -> RenderBlock {
-    enum JumpIcon {}
-
+fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
     let (message, highlights) = highlight_diagnostic_message(&diagnostic.message);
     Arc::new(move |cx| {
         let settings = cx.global::<Settings>();
-        let tooltip_style = settings.theme.tooltip.clone();
         let theme = &settings.theme.editor;
         let style = theme.diagnostic_header.clone();
         let font_size = (style.text_scale_factor * settings.buffer_font_size).round();
@@ -685,43 +649,6 @@ fn diagnostic_header_renderer(
                     .aligned()
                     .boxed()
             }))
-            .with_child(
-                MouseEventHandler::new::<JumpIcon, _, _>(diagnostic.group_id, cx, |state, _| {
-                    let style = style.jump_icon.style_for(state, false);
-                    Svg::new("icons/jump.svg")
-                        .with_color(style.color)
-                        .constrained()
-                        .with_width(style.icon_width)
-                        .aligned()
-                        .contained()
-                        .with_style(style.container)
-                        .constrained()
-                        .with_width(style.button_width)
-                        .with_height(style.button_width)
-                        .boxed()
-                })
-                .with_cursor_style(CursorStyle::PointingHand)
-                .on_click({
-                    let path = path.clone();
-                    move |_, _, cx| {
-                        cx.dispatch_action(Jump {
-                            path: path.clone(),
-                            position,
-                            anchor,
-                        });
-                    }
-                })
-                .with_tooltip(
-                    diagnostic.group_id,
-                    "Jump to diagnostic".to_string(),
-                    Some(Box::new(editor::OpenExcerpts)),
-                    tooltip_style,
-                    cx,
-                )
-                .aligned()
-                .flex_float()
-                .boxed(),
-            )
             .contained()
             .with_style(style.container)
             .with_padding_left(x_padding)

crates/editor/src/display_map/block_map.rs 🔗

@@ -2,7 +2,7 @@ use super::{
     wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot},
     TextHighlights,
 };
-use crate::{Anchor, ToPoint as _};
+use crate::{Anchor, ExcerptRange, ToPoint as _};
 use collections::{Bound, HashMap, HashSet};
 use gpui::{ElementBox, RenderContext};
 use language::{BufferSnapshot, Chunk, Patch};
@@ -97,8 +97,9 @@ struct Transform {
 pub enum TransformBlock {
     Custom(Arc<Block>),
     ExcerptHeader {
+        key: usize,
         buffer: BufferSnapshot,
-        range: Range<text::Anchor>,
+        range: ExcerptRange<text::Anchor>,
         height: u8,
         starts_new_buffer: bool,
     },
@@ -126,7 +127,7 @@ impl Debug for TransformBlock {
             Self::Custom(block) => f.debug_struct("Custom").field("block", block).finish(),
             Self::ExcerptHeader { buffer, .. } => f
                 .debug_struct("ExcerptHeader")
-                .field("path", &buffer.path())
+                .field("path", &buffer.file().map(|f| f.path()))
                 .finish(),
         }
     }
@@ -359,6 +360,7 @@ impl BlockMap {
                                 .from_point(Point::new(excerpt_boundary.row, 0), Bias::Left)
                                 .row(),
                             TransformBlock::ExcerptHeader {
+                                key: excerpt_boundary.key,
                                 buffer: excerpt_boundary.buffer,
                                 range: excerpt_boundary.range,
                                 height: if excerpt_boundary.starts_new_buffer {

crates/editor/src/editor.rs 🔗

@@ -36,10 +36,11 @@ use language::{
 };
 use multi_buffer::MultiBufferChunks;
 pub use multi_buffer::{
-    Anchor, AnchorRangeExt, ExcerptId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
+    Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset,
+    ToPoint,
 };
 use ordered_float::OrderedFloat;
-use project::{HoverBlock, Project, ProjectTransaction};
+use project::{HoverBlock, Project, ProjectPath, ProjectTransaction};
 use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
 use serde::{Deserialize, Serialize};
 use settings::Settings;
@@ -85,6 +86,13 @@ pub struct HoverAt {
     point: Option<DisplayPoint>,
 }
 
+#[derive(Clone, Debug, PartialEq)]
+pub struct Jump {
+    path: ProjectPath,
+    position: Point,
+    anchor: language::Anchor,
+}
+
 #[derive(Clone, Deserialize, PartialEq)]
 pub struct Input(pub String);
 
@@ -215,7 +223,7 @@ impl_actions!(
     ]
 );
 
-impl_internal_actions!(editor, [Scroll, Select, HoverAt]);
+impl_internal_actions!(editor, [Scroll, Select, HoverAt, Jump]);
 
 enum DocumentHighlightRead {}
 enum DocumentHighlightWrite {}
@@ -305,6 +313,7 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(Editor::hover);
     cx.add_action(Editor::hover_at);
     cx.add_action(Editor::open_excerpts);
+    cx.add_action(Editor::jump);
     cx.add_action(Editor::restart_language_server);
     cx.add_async_action(Editor::confirm_completion);
     cx.add_async_action(Editor::confirm_code_action);
@@ -2833,11 +2842,11 @@ impl Editor {
                             let start = highlight
                                 .range
                                 .start
-                                .max(&excerpt_range.start, cursor_buffer_snapshot);
+                                .max(&excerpt_range.context.start, cursor_buffer_snapshot);
                             let end = highlight
                                 .range
                                 .end
-                                .min(&excerpt_range.end, cursor_buffer_snapshot);
+                                .min(&excerpt_range.context.end, cursor_buffer_snapshot);
                             if start.cmp(&end, cursor_buffer_snapshot).is_ge() {
                                 continue;
                             }
@@ -5825,6 +5834,34 @@ impl Editor {
             nav_history.borrow_mut().enable();
         });
     }
+
+    fn jump(workspace: &mut Workspace, action: &Jump, cx: &mut ViewContext<Workspace>) {
+        let editor = workspace.open_path(action.path.clone(), true, cx);
+        let position = action.position;
+        let anchor = action.anchor;
+        cx.spawn_weak(|_, mut cx| async move {
+            let editor = editor.await.log_err()?.downcast::<Editor>()?;
+            editor.update(&mut cx, |editor, cx| {
+                let buffer = editor.buffer().read(cx).as_singleton()?;
+                let buffer = buffer.read(cx);
+                let cursor = if buffer.can_resolve(&anchor) {
+                    language::ToPoint::to_point(&anchor, buffer)
+                } else {
+                    buffer.clip_point(position, Bias::Left)
+                };
+
+                let nav_history = editor.nav_history.take();
+                editor.change_selections(Some(Autoscroll::Newest), cx, |s| {
+                    s.select_ranges([cursor..cursor]);
+                });
+                editor.nav_history = nav_history;
+
+                Some(())
+            })?;
+            Some(())
+        })
+        .detach()
+    }
 }
 
 impl EditorSnapshot {
@@ -7718,12 +7755,18 @@ mod tests {
             let mut multibuffer = MultiBuffer::new(0);
             multibuffer.push_excerpts(
                 toml_buffer.clone(),
-                [Point::new(0, 0)..Point::new(2, 0)],
+                [ExcerptRange {
+                    context: Point::new(0, 0)..Point::new(2, 0),
+                    primary: None,
+                }],
                 cx,
             );
             multibuffer.push_excerpts(
                 rust_buffer.clone(),
-                [Point::new(0, 0)..Point::new(1, 0)],
+                [ExcerptRange {
+                    context: Point::new(0, 0)..Point::new(1, 0),
+                    primary: None,
+                }],
                 cx,
             );
             multibuffer
@@ -9595,8 +9638,14 @@ mod tests {
             multibuffer.push_excerpts(
                 buffer.clone(),
                 [
-                    Point::new(0, 0)..Point::new(0, 4),
-                    Point::new(1, 0)..Point::new(1, 4),
+                    ExcerptRange {
+                        context: Point::new(0, 0)..Point::new(0, 4),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(1, 0)..Point::new(1, 4),
+                        primary: None,
+                    },
                 ],
                 cx,
             );
@@ -9634,6 +9683,10 @@ mod tests {
                 [aaaa
                 (bbbb]
                 cccc)"});
+        let excerpt_ranges = excerpt_ranges.into_iter().map(|context| ExcerptRange {
+            context,
+            primary: None,
+        });
         let buffer = cx.add_model(|cx| Buffer::new(0, initial_text, cx));
         let multibuffer = cx.add_model(|cx| {
             let mut multibuffer = MultiBuffer::new(0);
@@ -9687,8 +9740,14 @@ mod tests {
                 .push_excerpts(
                     buffer.clone(),
                     [
-                        Point::new(0, 0)..Point::new(1, 4),
-                        Point::new(1, 0)..Point::new(2, 4),
+                        ExcerptRange {
+                            context: Point::new(0, 0)..Point::new(1, 4),
+                            primary: None,
+                        },
+                        ExcerptRange {
+                            context: Point::new(1, 0)..Point::new(2, 4),
+                            primary: None,
+                        },
                     ],
                     cx,
                 )
@@ -9771,8 +9830,14 @@ mod tests {
                 .push_excerpts(
                     buffer.clone(),
                     [
-                        Point::new(0, 0)..Point::new(1, 4),
-                        Point::new(1, 0)..Point::new(2, 4),
+                        ExcerptRange {
+                            context: Point::new(0, 0)..Point::new(1, 4),
+                            primary: None,
+                        },
+                        ExcerptRange {
+                            context: Point::new(1, 0)..Point::new(2, 4),
+                            primary: None,
+                        },
                     ],
                     cx,
                 )

crates/editor/src/element.rs 🔗

@@ -27,6 +27,7 @@ use gpui::{
 };
 use json::json;
 use language::{Bias, DiagnosticSeverity, Selection};
+use project::ProjectPath;
 use settings::Settings;
 use smallvec::SmallVec;
 use std::{
@@ -795,6 +796,7 @@ impl EditorElement {
             return Default::default();
         };
 
+        let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
         let scroll_x = snapshot.scroll_position.x();
         snapshot
             .blocks_in_range(rows.clone())
@@ -827,10 +829,60 @@ impl EditorElement {
                         })
                     }
                     TransformBlock::ExcerptHeader {
+                        key,
                         buffer,
+                        range,
                         starts_new_buffer,
                         ..
                     } => {
+                        let jump_icon = project::File::from_dyn(buffer.file()).map(|file| {
+                            let jump_position = range
+                                .primary
+                                .as_ref()
+                                .map_or(range.context.start, |primary| primary.start);
+                            let jump_action = crate::Jump {
+                                path: ProjectPath {
+                                    worktree_id: file.worktree_id(cx),
+                                    path: file.path.clone(),
+                                },
+                                position: language::ToPoint::to_point(&jump_position, buffer),
+                                anchor: jump_position,
+                            };
+
+                            enum JumpIcon {}
+                            cx.render(&editor, |_, cx| {
+                                MouseEventHandler::new::<JumpIcon, _, _>(*key, cx, |state, _| {
+                                    let style = style.jump_icon.style_for(state, false);
+                                    Svg::new("icons/jump.svg")
+                                        .with_color(style.color)
+                                        .constrained()
+                                        .with_width(style.icon_width)
+                                        .aligned()
+                                        .contained()
+                                        .with_style(style.container)
+                                        .constrained()
+                                        .with_width(style.button_width)
+                                        .with_height(style.button_width)
+                                        .boxed()
+                                })
+                                .with_cursor_style(CursorStyle::PointingHand)
+                                .on_click({
+                                    move |_, _, cx| cx.dispatch_action(jump_action.clone())
+                                })
+                                .with_tooltip(
+                                    *key,
+                                    "Jump to Buffer".to_string(),
+                                    Some(Box::new(crate::OpenExcerpts)),
+                                    tooltip_style.clone(),
+                                    cx,
+                                )
+                                .aligned()
+                                .flex_float()
+                                .boxed()
+                            })
+                        });
+
+                        let padding = gutter_padding + scroll_x * em_width;
                         if *starts_new_buffer {
                             let style = &self.style.diagnostic_path_header;
                             let font_size =
@@ -838,7 +890,8 @@ impl EditorElement {
 
                             let mut filename = None;
                             let mut parent_path = None;
-                            if let Some(path) = buffer.path() {
+                            if let Some(file) = buffer.file() {
+                                let path = file.path();
                                 filename =
                                     path.file_name().map(|f| f.to_string_lossy().to_string());
                                 parent_path =
@@ -853,6 +906,7 @@ impl EditorElement {
                                     )
                                     .contained()
                                     .with_style(style.filename.container)
+                                    .aligned()
                                     .boxed(),
                                 )
                                 .with_children(parent_path.map(|path| {
@@ -862,20 +916,25 @@ impl EditorElement {
                                     )
                                     .contained()
                                     .with_style(style.path.container)
+                                    .aligned()
                                     .boxed()
                                 }))
-                                .aligned()
-                                .left()
+                                .with_children(jump_icon)
                                 .contained()
                                 .with_style(style.container)
-                                .with_padding_left(gutter_padding + scroll_x * em_width)
+                                .with_padding_left(padding)
+                                .with_padding_right(padding)
                                 .expanded()
                                 .named("path header block")
                         } else {
                             let text_style = self.style.text.clone();
-                            Label::new("…".to_string(), text_style)
+                            Flex::row()
+                                .with_child(Label::new("…".to_string(), text_style).boxed())
+                                .with_children(jump_icon)
                                 .contained()
-                                .with_padding_left(gutter_padding + scroll_x * em_width)
+                                .with_padding_left(padding)
+                                .with_padding_right(padding)
+                                .expanded()
                                 .named("collapsed context")
                         }
                     }

crates/editor/src/movement.rs 🔗

@@ -272,7 +272,7 @@ pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range<
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{test::marked_display_snapshot, Buffer, DisplayMap, MultiBuffer};
+    use crate::{test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange, MultiBuffer};
     use language::Point;
     use settings::Settings;
 
@@ -494,8 +494,14 @@ mod tests {
             multibuffer.push_excerpts(
                 buffer.clone(),
                 [
-                    Point::new(0, 0)..Point::new(1, 4),
-                    Point::new(2, 0)..Point::new(3, 2),
+                    ExcerptRange {
+                        context: Point::new(0, 0)..Point::new(1, 4),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(2, 0)..Point::new(3, 2),
+                        primary: None,
+                    },
                 ],
                 cx,
             );

crates/editor/src/multi_buffer.rs 🔗

@@ -31,6 +31,7 @@ use text::{
     Edit, Point, PointUtf16, TextSummary,
 };
 use theme::SyntaxTheme;
+use util::post_inc;
 
 const NEWLINES: &'static [u8] = &[b'\n'; u8::MAX as usize];
 
@@ -40,6 +41,7 @@ pub struct MultiBuffer {
     snapshot: RefCell<MultiBufferSnapshot>,
     buffers: RefCell<HashMap<usize, BufferState>>,
     used_excerpt_ids: SumTree<ExcerptId>,
+    next_excerpt_key: usize,
     subscriptions: Topic,
     singleton: bool,
     replica_id: ReplicaId,
@@ -102,23 +104,31 @@ pub struct MultiBufferSnapshot {
 
 pub struct ExcerptBoundary {
     pub id: ExcerptId,
+    pub key: usize,
     pub row: u32,
     pub buffer: BufferSnapshot,
-    pub range: Range<text::Anchor>,
+    pub range: ExcerptRange<text::Anchor>,
     pub starts_new_buffer: bool,
 }
 
 #[derive(Clone)]
 struct Excerpt {
     id: ExcerptId,
+    key: usize,
     buffer_id: usize,
     buffer: BufferSnapshot,
-    range: Range<text::Anchor>,
+    range: ExcerptRange<text::Anchor>,
     max_buffer_row: u32,
     text_summary: TextSummary,
     has_trailing_newline: bool,
 }
 
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct ExcerptRange<T> {
+    pub context: Range<T>,
+    pub primary: Option<Range<T>>,
+}
+
 #[derive(Clone, Debug, Default)]
 struct ExcerptSummary {
     excerpt_id: ExcerptId,
@@ -161,6 +171,7 @@ impl MultiBuffer {
             snapshot: Default::default(),
             buffers: Default::default(),
             used_excerpt_ids: Default::default(),
+            next_excerpt_key: Default::default(),
             subscriptions: Default::default(),
             singleton: false,
             replica_id,
@@ -198,7 +209,8 @@ impl MultiBuffer {
         Self {
             snapshot: RefCell::new(self.snapshot.borrow().clone()),
             buffers: RefCell::new(buffers),
-            used_excerpt_ids: Default::default(),
+            used_excerpt_ids: self.used_excerpt_ids.clone(),
+            next_excerpt_key: self.next_excerpt_key,
             subscriptions: Default::default(),
             singleton: self.singleton,
             replica_id: self.replica_id,
@@ -215,7 +227,14 @@ impl MultiBuffer {
     pub fn singleton(buffer: ModelHandle<Buffer>, cx: &mut ModelContext<Self>) -> Self {
         let mut this = Self::new(buffer.read(cx).replica_id());
         this.singleton = true;
-        this.push_excerpts(buffer, [text::Anchor::MIN..text::Anchor::MAX], cx);
+        this.push_excerpts(
+            buffer,
+            [ExcerptRange {
+                context: text::Anchor::MIN..text::Anchor::MAX,
+                primary: None,
+            }],
+            cx,
+        );
         this.snapshot.borrow_mut().singleton = true;
         this
     }
@@ -343,8 +362,12 @@ impl MultiBuffer {
             }
             let start_excerpt = cursor.item().expect("start offset out of bounds");
             let start_overshoot = range.start - cursor.start();
-            let buffer_start =
-                start_excerpt.range.start.to_offset(&start_excerpt.buffer) + start_overshoot;
+            let buffer_start = start_excerpt
+                .range
+                .context
+                .start
+                .to_offset(&start_excerpt.buffer)
+                + start_overshoot;
 
             cursor.seek(&range.end, Bias::Right, &());
             if cursor.item().is_none() && range.end == *cursor.start() {
@@ -352,7 +375,12 @@ impl MultiBuffer {
             }
             let end_excerpt = cursor.item().expect("end offset out of bounds");
             let end_overshoot = range.end - cursor.start();
-            let buffer_end = end_excerpt.range.start.to_offset(&end_excerpt.buffer) + end_overshoot;
+            let buffer_end = end_excerpt
+                .range
+                .context
+                .start
+                .to_offset(&end_excerpt.buffer)
+                + end_overshoot;
 
             if start_excerpt.id == end_excerpt.id {
                 buffer_edits
@@ -360,10 +388,18 @@ impl MultiBuffer {
                     .or_insert(Vec::new())
                     .push((buffer_start..buffer_end, new_text, true));
             } else {
-                let start_excerpt_range =
-                    buffer_start..start_excerpt.range.end.to_offset(&start_excerpt.buffer);
-                let end_excerpt_range =
-                    end_excerpt.range.start.to_offset(&end_excerpt.buffer)..buffer_end;
+                let start_excerpt_range = buffer_start
+                    ..start_excerpt
+                        .range
+                        .context
+                        .end
+                        .to_offset(&start_excerpt.buffer);
+                let end_excerpt_range = end_excerpt
+                    .range
+                    .context
+                    .start
+                    .to_offset(&end_excerpt.buffer)
+                    ..buffer_end;
                 buffer_edits
                     .entry(start_excerpt.buffer_id)
                     .or_insert(Vec::new())
@@ -383,7 +419,7 @@ impl MultiBuffer {
                         .entry(excerpt.buffer_id)
                         .or_insert(Vec::new())
                         .push((
-                            excerpt.range.to_offset(&excerpt.buffer),
+                            excerpt.range.context.to_offset(&excerpt.buffer),
                             new_text.clone(),
                             false,
                         ));
@@ -523,8 +559,8 @@ impl MultiBuffer {
                     break;
                 }
 
-                let mut start = excerpt.range.start.clone();
-                let mut end = excerpt.range.end.clone();
+                let mut start = excerpt.range.context.start.clone();
+                let mut end = excerpt.range.context.end.clone();
                 if excerpt.id == selection.start.excerpt_id {
                     start = selection.start.text_anchor.clone();
                 }
@@ -644,7 +680,7 @@ impl MultiBuffer {
     pub fn push_excerpts<O>(
         &mut self,
         buffer: ModelHandle<Buffer>,
-        ranges: impl IntoIterator<Item = Range<O>>,
+        ranges: impl IntoIterator<Item = ExcerptRange<O>>,
         cx: &mut ModelContext<Self>,
     ) -> Vec<ExcerptId>
     where
@@ -692,7 +728,10 @@ impl MultiBuffer {
                 }
             }
 
-            excerpt_ranges.push(excerpt_start..excerpt_end);
+            excerpt_ranges.push(ExcerptRange {
+                context: excerpt_start..excerpt_end,
+                primary: Some(range),
+            });
             range_counts.push(ranges_in_excerpt);
         }
 
@@ -722,7 +761,7 @@ impl MultiBuffer {
         &mut self,
         prev_excerpt_id: &ExcerptId,
         buffer: ModelHandle<Buffer>,
-        ranges: impl IntoIterator<Item = Range<O>>,
+        ranges: impl IntoIterator<Item = ExcerptRange<O>>,
         cx: &mut ModelContext<Self>,
     ) -> Vec<ExcerptId>
     where
@@ -786,10 +825,17 @@ impl MultiBuffer {
             if let Err(ix) = buffer_state.excerpts.binary_search(&id) {
                 buffer_state.excerpts.insert(ix, id.clone());
             }
-            let range = buffer_snapshot.anchor_before(&range.start)
-                ..buffer_snapshot.anchor_after(&range.end);
+            let range = ExcerptRange {
+                context: buffer_snapshot.anchor_before(&range.context.start)
+                    ..buffer_snapshot.anchor_after(&range.context.end),
+                primary: range.primary.map(|primary| {
+                    buffer_snapshot.anchor_before(&primary.start)
+                        ..buffer_snapshot.anchor_after(&primary.end)
+                }),
+            };
             let excerpt = Excerpt::new(
                 id.clone(),
+                post_inc(&mut self.next_excerpt_key),
                 buffer_id,
                 buffer_snapshot.clone(),
                 range,
@@ -846,7 +892,7 @@ impl MultiBuffer {
         &self,
         buffer: &ModelHandle<Buffer>,
         cx: &AppContext,
-    ) -> Vec<(ExcerptId, Range<text::Anchor>)> {
+    ) -> Vec<(ExcerptId, ExcerptRange<text::Anchor>)> {
         let mut excerpts = Vec::new();
         let snapshot = self.read(cx);
         let buffers = self.buffers.borrow();
@@ -894,7 +940,7 @@ impl MultiBuffer {
                     .unwrap()
                     .buffer
                     .clone(),
-                excerpt.range.clone(),
+                excerpt.range.context.clone(),
             )
         })
     }
@@ -914,7 +960,7 @@ impl MultiBuffer {
         }
 
         cursor.item().map(|excerpt| {
-            let excerpt_start = excerpt.range.start.to_offset(&excerpt.buffer);
+            let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer);
             let buffer_point = excerpt_start + offset - *cursor.start();
             let buffer = self.buffers.borrow()[&excerpt.buffer_id].buffer.clone();
 
@@ -943,7 +989,7 @@ impl MultiBuffer {
             if excerpt.has_trailing_newline {
                 end_before_newline -= 1;
             }
-            let excerpt_start = excerpt.range.start.to_offset(&excerpt.buffer);
+            let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer);
             let start = excerpt_start + (cmp::max(start, *cursor.start()) - *cursor.start());
             let end = excerpt_start + (cmp::min(end, end_before_newline) - *cursor.start());
             let buffer = self.buffers.borrow()[&excerpt.buffer_id].buffer.clone();
@@ -1234,7 +1280,7 @@ impl MultiBuffer {
                     buffer
                         .edits_since_in_range::<usize>(
                             old_excerpt.buffer.version(),
-                            old_excerpt.range.clone(),
+                            old_excerpt.range.context.clone(),
                         )
                         .map(|mut edit| {
                             let excerpt_old_start = cursor.start().1;
@@ -1249,6 +1295,7 @@ impl MultiBuffer {
 
                 new_excerpt = Excerpt::new(
                     id.clone(),
+                    old_excerpt.key,
                     buffer_id,
                     buffer.snapshot(),
                     old_excerpt.range.clone(),
@@ -1384,7 +1431,10 @@ impl MultiBuffer {
                         let end_ix =
                             buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Right);
                         let start_ix = buffer.clip_offset(rng.gen_range(0..=end_ix), Bias::Left);
-                        start_ix..end_ix
+                        ExcerptRange {
+                            context: start_ix..end_ix,
+                            primary: None,
+                        }
                     })
                     .collect::<Vec<_>>();
                 log::info!(
@@ -1393,7 +1443,7 @@ impl MultiBuffer {
                     ranges,
                     ranges
                         .iter()
-                        .map(|range| &buffer_text[range.clone()])
+                        .map(|range| &buffer_text[range.context.clone()])
                         .collect::<Vec<_>>()
                 );
 
@@ -1465,7 +1515,7 @@ impl MultiBufferSnapshot {
         cursor.seek(&offset, Bias::Left, &());
         let mut excerpt_chunks = cursor.item().map(|excerpt| {
             let end_before_footer = cursor.start() + excerpt.text_summary.bytes;
-            let start = excerpt.range.start.to_offset(&excerpt.buffer);
+            let start = excerpt.range.context.start.to_offset(&excerpt.buffer);
             let end = start + (cmp::min(offset, end_before_footer) - cursor.start());
             excerpt.buffer.reversed_chunks_in_range(start..end)
         });
@@ -1476,7 +1526,7 @@ impl MultiBufferSnapshot {
                 excerpt_chunks = Some(
                     excerpt
                         .buffer
-                        .reversed_chunks_in_range(excerpt.range.clone()),
+                        .reversed_chunks_in_range(excerpt.range.context.clone()),
                 );
             }
 
@@ -1581,7 +1631,7 @@ impl MultiBufferSnapshot {
         let mut cursor = self.excerpts.cursor::<usize>();
         cursor.seek(&offset, Bias::Right, &());
         let overshoot = if let Some(excerpt) = cursor.item() {
-            let excerpt_start = excerpt.range.start.to_offset(&excerpt.buffer);
+            let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer);
             let buffer_offset = excerpt
                 .buffer
                 .clip_offset(excerpt_start + (offset - cursor.start()), bias);
@@ -1600,7 +1650,7 @@ impl MultiBufferSnapshot {
         let mut cursor = self.excerpts.cursor::<Point>();
         cursor.seek(&point, Bias::Right, &());
         let overshoot = if let Some(excerpt) = cursor.item() {
-            let excerpt_start = excerpt.range.start.to_point(&excerpt.buffer);
+            let excerpt_start = excerpt.range.context.start.to_point(&excerpt.buffer);
             let buffer_point = excerpt
                 .buffer
                 .clip_point(excerpt_start + (point - cursor.start()), bias);
@@ -1621,7 +1671,7 @@ impl MultiBufferSnapshot {
         let overshoot = if let Some(excerpt) = cursor.item() {
             let excerpt_start = excerpt
                 .buffer
-                .offset_to_point_utf16(excerpt.range.start.to_offset(&excerpt.buffer));
+                .offset_to_point_utf16(excerpt.range.context.start.to_offset(&excerpt.buffer));
             let buffer_point = excerpt
                 .buffer
                 .clip_point_utf16(excerpt_start + (point - cursor.start()), bias);
@@ -1690,8 +1740,8 @@ impl MultiBufferSnapshot {
         if let Some(excerpt) = cursor.item() {
             let (start_offset, start_point) = cursor.start();
             let overshoot = offset - start_offset;
-            let excerpt_start_offset = excerpt.range.start.to_offset(&excerpt.buffer);
-            let excerpt_start_point = excerpt.range.start.to_point(&excerpt.buffer);
+            let excerpt_start_offset = excerpt.range.context.start.to_offset(&excerpt.buffer);
+            let excerpt_start_point = excerpt.range.context.start.to_point(&excerpt.buffer);
             let buffer_point = excerpt
                 .buffer
                 .offset_to_point(excerpt_start_offset + overshoot);
@@ -1711,8 +1761,8 @@ impl MultiBufferSnapshot {
         if let Some(excerpt) = cursor.item() {
             let (start_offset, start_point) = cursor.start();
             let overshoot = offset - start_offset;
-            let excerpt_start_offset = excerpt.range.start.to_offset(&excerpt.buffer);
-            let excerpt_start_point = excerpt.range.start.to_point_utf16(&excerpt.buffer);
+            let excerpt_start_offset = excerpt.range.context.start.to_offset(&excerpt.buffer);
+            let excerpt_start_point = excerpt.range.context.start.to_point_utf16(&excerpt.buffer);
             let buffer_point = excerpt
                 .buffer
                 .offset_to_point_utf16(excerpt_start_offset + overshoot);
@@ -1732,8 +1782,9 @@ impl MultiBufferSnapshot {
         if let Some(excerpt) = cursor.item() {
             let (start_offset, start_point) = cursor.start();
             let overshoot = point - start_offset;
-            let excerpt_start_point = excerpt.range.start.to_point(&excerpt.buffer);
-            let excerpt_start_point_utf16 = excerpt.range.start.to_point_utf16(&excerpt.buffer);
+            let excerpt_start_point = excerpt.range.context.start.to_point(&excerpt.buffer);
+            let excerpt_start_point_utf16 =
+                excerpt.range.context.start.to_point_utf16(&excerpt.buffer);
             let buffer_point = excerpt
                 .buffer
                 .point_to_point_utf16(excerpt_start_point + overshoot);
@@ -1753,8 +1804,8 @@ impl MultiBufferSnapshot {
         if let Some(excerpt) = cursor.item() {
             let (start_point, start_offset) = cursor.start();
             let overshoot = point - start_point;
-            let excerpt_start_offset = excerpt.range.start.to_offset(&excerpt.buffer);
-            let excerpt_start_point = excerpt.range.start.to_point(&excerpt.buffer);
+            let excerpt_start_offset = excerpt.range.context.start.to_offset(&excerpt.buffer);
+            let excerpt_start_point = excerpt.range.context.start.to_point(&excerpt.buffer);
             let buffer_offset = excerpt
                 .buffer
                 .point_to_offset(excerpt_start_point + overshoot);
@@ -1774,10 +1825,10 @@ impl MultiBufferSnapshot {
         if let Some(excerpt) = cursor.item() {
             let (start_point, start_offset) = cursor.start();
             let overshoot = point - start_point;
-            let excerpt_start_offset = excerpt.range.start.to_offset(&excerpt.buffer);
+            let excerpt_start_offset = excerpt.range.context.start.to_offset(&excerpt.buffer);
             let excerpt_start_point = excerpt
                 .buffer
-                .offset_to_point_utf16(excerpt.range.start.to_offset(&excerpt.buffer));
+                .offset_to_point_utf16(excerpt.range.context.start.to_offset(&excerpt.buffer));
             let buffer_offset = excerpt
                 .buffer
                 .point_utf16_to_offset(excerpt_start_point + overshoot);
@@ -1811,8 +1862,8 @@ impl MultiBufferSnapshot {
         cursor.seek(&Point::new(row, 0), Bias::Right, &());
         if let Some(excerpt) = cursor.item() {
             let overshoot = row - cursor.start().row;
-            let excerpt_start = excerpt.range.start.to_point(&excerpt.buffer);
-            let excerpt_end = excerpt.range.end.to_point(&excerpt.buffer);
+            let excerpt_start = excerpt.range.context.start.to_point(&excerpt.buffer);
+            let excerpt_end = excerpt.range.context.end.to_point(&excerpt.buffer);
             let buffer_row = excerpt_start.row + overshoot;
             let line_start = Point::new(buffer_row, 0);
             let line_end = Point::new(buffer_row, excerpt.buffer.line_len(buffer_row));
@@ -1847,7 +1898,7 @@ impl MultiBufferSnapshot {
                 end_before_newline -= 1;
             }
 
-            let excerpt_start = excerpt.range.start.to_offset(&excerpt.buffer);
+            let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer);
             let start_in_excerpt = excerpt_start + (range.start - cursor.start());
             let end_in_excerpt =
                 excerpt_start + (cmp::min(end_before_newline, range.end) - cursor.start());
@@ -1881,7 +1932,7 @@ impl MultiBufferSnapshot {
             if let Some(excerpt) = cursor.item() {
                 range.end = cmp::max(*cursor.start(), range.end);
 
-                let excerpt_start = excerpt.range.start.to_offset(&excerpt.buffer);
+                let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer);
                 let end_in_excerpt = excerpt_start + (range.end - cursor.start());
                 summary.add_assign(
                     &excerpt
@@ -1907,8 +1958,9 @@ impl MultiBufferSnapshot {
         let mut position = D::from_text_summary(&cursor.start().text);
         if let Some(excerpt) = cursor.item() {
             if excerpt.id == anchor.excerpt_id {
-                let excerpt_buffer_start = excerpt.range.start.summary::<D>(&excerpt.buffer);
-                let excerpt_buffer_end = excerpt.range.end.summary::<D>(&excerpt.buffer);
+                let excerpt_buffer_start =
+                    excerpt.range.context.start.summary::<D>(&excerpt.buffer);
+                let excerpt_buffer_end = excerpt.range.context.end.summary::<D>(&excerpt.buffer);
                 let buffer_position = cmp::min(
                     excerpt_buffer_end,
                     anchor.text_anchor.summary::<D>(&excerpt.buffer),
@@ -1954,8 +2006,10 @@ impl MultiBufferSnapshot {
             let position = D::from_text_summary(&cursor.start().text);
             if let Some(excerpt) = cursor.item() {
                 if excerpt.id == *excerpt_id {
-                    let excerpt_buffer_start = excerpt.range.start.summary::<D>(&excerpt.buffer);
-                    let excerpt_buffer_end = excerpt.range.end.summary::<D>(&excerpt.buffer);
+                    let excerpt_buffer_start =
+                        excerpt.range.context.start.summary::<D>(&excerpt.buffer);
+                    let excerpt_buffer_end =
+                        excerpt.range.context.end.summary::<D>(&excerpt.buffer);
                     summaries.extend(
                         excerpt
                             .buffer
@@ -2036,10 +2090,14 @@ impl MultiBufferSnapshot {
                     anchor = if let Some(excerpt) = next_excerpt {
                         let mut text_anchor = excerpt
                             .range
+                            .context
                             .start
                             .bias(anchor.text_anchor.bias, &excerpt.buffer);
-                        if text_anchor.cmp(&excerpt.range.end, &excerpt.buffer).is_gt() {
-                            text_anchor = excerpt.range.end.clone();
+                        if text_anchor
+                            .cmp(&excerpt.range.context.end, &excerpt.buffer)
+                            .is_gt()
+                        {
+                            text_anchor = excerpt.range.context.end.clone();
                         }
                         Anchor {
                             buffer_id: Some(excerpt.buffer_id),
@@ -2049,13 +2107,14 @@ impl MultiBufferSnapshot {
                     } else if let Some(excerpt) = prev_excerpt {
                         let mut text_anchor = excerpt
                             .range
+                            .context
                             .end
                             .bias(anchor.text_anchor.bias, &excerpt.buffer);
                         if text_anchor
-                            .cmp(&excerpt.range.start, &excerpt.buffer)
+                            .cmp(&excerpt.range.context.start, &excerpt.buffer)
                             .is_lt()
                         {
-                            text_anchor = excerpt.range.start.clone();
+                            text_anchor = excerpt.range.context.start.clone();
                         }
                         Anchor {
                             buffer_id: Some(excerpt.buffer_id),
@@ -2106,7 +2165,7 @@ impl MultiBufferSnapshot {
                 bias = Bias::Right;
             }
 
-            let buffer_start = excerpt.range.start.to_offset(&excerpt.buffer);
+            let buffer_start = excerpt.range.context.start.to_offset(&excerpt.buffer);
             let text_anchor =
                 excerpt.clip_anchor(excerpt.buffer.anchor_at(buffer_start + overshoot, bias));
             Anchor {
@@ -2196,6 +2255,7 @@ impl MultiBufferSnapshot {
                 let starts_new_buffer = Some(excerpt.buffer_id) != prev_buffer_id;
                 let boundary = ExcerptBoundary {
                     id: excerpt.id.clone(),
+                    key: excerpt.key,
                     row: cursor.start().1.row,
                     buffer: excerpt.buffer.clone(),
                     range: excerpt.range.clone(),
@@ -2239,8 +2299,11 @@ impl MultiBufferSnapshot {
                     return None;
                 }
 
-                let excerpt_buffer_start =
-                    start_excerpt.range.start.to_offset(&start_excerpt.buffer);
+                let excerpt_buffer_start = start_excerpt
+                    .range
+                    .context
+                    .start
+                    .to_offset(&start_excerpt.buffer);
                 let excerpt_buffer_end = excerpt_buffer_start + start_excerpt.text_summary.bytes;
 
                 let start_in_buffer =
@@ -2340,8 +2403,11 @@ impl MultiBufferSnapshot {
                     return None;
                 }
 
-                let excerpt_buffer_start =
-                    start_excerpt.range.start.to_offset(&start_excerpt.buffer);
+                let excerpt_buffer_start = start_excerpt
+                    .range
+                    .context
+                    .start
+                    .to_offset(&start_excerpt.buffer);
                 let excerpt_buffer_end = excerpt_buffer_start + start_excerpt.text_summary.bytes;
 
                 let start_in_buffer =
@@ -2427,7 +2493,8 @@ impl MultiBufferSnapshot {
         cursor
             .take_while(move |excerpt| excerpt.id <= range.end.excerpt_id)
             .flat_map(move |excerpt| {
-                let mut query_range = excerpt.range.start.clone()..excerpt.range.end.clone();
+                let mut query_range =
+                    excerpt.range.context.start.clone()..excerpt.range.context.end.clone();
                 if excerpt.id == range.start.excerpt_id {
                     query_range.start = range.start.text_anchor.clone();
                 }
@@ -2617,15 +2684,18 @@ impl History {
 impl Excerpt {
     fn new(
         id: ExcerptId,
+        key: usize,
         buffer_id: usize,
         buffer: BufferSnapshot,
-        range: Range<text::Anchor>,
+        range: ExcerptRange<text::Anchor>,
         has_trailing_newline: bool,
     ) -> Self {
         Excerpt {
             id,
-            max_buffer_row: range.end.to_point(&buffer).row,
-            text_summary: buffer.text_summary_for_range::<TextSummary, _>(range.to_offset(&buffer)),
+            key,
+            max_buffer_row: range.context.end.to_point(&buffer).row,
+            text_summary: buffer
+                .text_summary_for_range::<TextSummary, _>(range.context.to_offset(&buffer)),
             buffer_id,
             buffer,
             range,
@@ -2638,7 +2708,7 @@ impl Excerpt {
         range: Range<usize>,
         language_aware: bool,
     ) -> ExcerptChunks<'a> {
-        let content_start = self.range.start.to_offset(&self.buffer);
+        let content_start = self.range.context.start.to_offset(&self.buffer);
         let chunks_start = content_start + range.start;
         let chunks_end = content_start + cmp::min(range.end, self.text_summary.bytes);
 
@@ -2660,7 +2730,7 @@ impl Excerpt {
     }
 
     fn bytes_in_range(&self, range: Range<usize>) -> ExcerptBytes {
-        let content_start = self.range.start.to_offset(&self.buffer);
+        let content_start = self.range.context.start.to_offset(&self.buffer);
         let bytes_start = content_start + range.start;
         let bytes_end = content_start + cmp::min(range.end, self.text_summary.bytes);
         let footer_height = if self.has_trailing_newline
@@ -2680,10 +2750,16 @@ impl Excerpt {
     }
 
     fn clip_anchor(&self, text_anchor: text::Anchor) -> text::Anchor {
-        if text_anchor.cmp(&self.range.start, &self.buffer).is_lt() {
-            self.range.start.clone()
-        } else if text_anchor.cmp(&self.range.end, &self.buffer).is_gt() {
-            self.range.end.clone()
+        if text_anchor
+            .cmp(&self.range.context.start, &self.buffer)
+            .is_lt()
+        {
+            self.range.context.start.clone()
+        } else if text_anchor
+            .cmp(&self.range.context.end, &self.buffer)
+            .is_gt()
+        {
+            self.range.context.end.clone()
         } else {
             text_anchor
         }
@@ -2693,11 +2769,13 @@ impl Excerpt {
         Some(self.buffer_id) == anchor.buffer_id
             && self
                 .range
+                .context
                 .start
                 .cmp(&anchor.text_anchor, &self.buffer)
                 .is_le()
             && self
                 .range
+                .context
                 .end
                 .cmp(&anchor.text_anchor, &self.buffer)
                 .is_ge()
@@ -2802,7 +2880,7 @@ impl<'a> MultiBufferRows<'a> {
 
         if let Some(excerpt) = self.excerpts.item() {
             let overshoot = row - self.excerpts.start().row;
-            let excerpt_start = excerpt.range.start.to_point(&excerpt.buffer).row;
+            let excerpt_start = excerpt.range.context.start.to_point(&excerpt.buffer).row;
             self.buffer_row_range.start = excerpt_start + overshoot;
             self.buffer_row_range.end = excerpt_start + excerpt.text_summary.lines.row + 1;
         }
@@ -2822,7 +2900,7 @@ impl<'a> Iterator for MultiBufferRows<'a> {
             self.excerpts.item()?;
             self.excerpts.next(&());
             let excerpt = self.excerpts.item()?;
-            self.buffer_row_range.start = excerpt.range.start.to_point(&excerpt.buffer).row;
+            self.buffer_row_range.start = excerpt.range.context.start.to_point(&excerpt.buffer).row;
             self.buffer_row_range.end =
                 self.buffer_row_range.start + excerpt.text_summary.lines.row + 1;
         }
@@ -3079,7 +3157,14 @@ mod tests {
 
         let subscription = multibuffer.update(cx, |multibuffer, cx| {
             let subscription = multibuffer.subscribe();
-            multibuffer.push_excerpts(buffer_1.clone(), [Point::new(1, 2)..Point::new(2, 5)], cx);
+            multibuffer.push_excerpts(
+                buffer_1.clone(),
+                [ExcerptRange {
+                    context: Point::new(1, 2)..Point::new(2, 5),
+                    primary: None,
+                }],
+                cx,
+            );
             assert_eq!(
                 subscription.consume().into_inner(),
                 [Edit {
@@ -3088,8 +3173,22 @@ mod tests {
                 }]
             );
 
-            multibuffer.push_excerpts(buffer_1.clone(), [Point::new(3, 3)..Point::new(4, 4)], cx);
-            multibuffer.push_excerpts(buffer_2.clone(), [Point::new(3, 1)..Point::new(3, 3)], cx);
+            multibuffer.push_excerpts(
+                buffer_1.clone(),
+                [ExcerptRange {
+                    context: Point::new(3, 3)..Point::new(4, 4),
+                    primary: None,
+                }],
+                cx,
+            );
+            multibuffer.push_excerpts(
+                buffer_2.clone(),
+                [ExcerptRange {
+                    context: Point::new(3, 1)..Point::new(3, 3),
+                    primary: None,
+                }],
+                cx,
+            );
             assert_eq!(
                 subscription.consume().into_inner(),
                 [Edit {
@@ -3253,7 +3352,7 @@ mod tests {
                         boundary.row,
                         boundary
                             .buffer
-                            .text_for_range(boundary.range)
+                            .text_for_range(boundary.range.context)
                             .collect::<String>(),
                         boundary.starts_new_buffer,
                     )
@@ -3334,8 +3433,22 @@ mod tests {
         let buffer_2 = cx.add_model(|cx| Buffer::new(0, "efghi", cx));
         let multibuffer = cx.add_model(|cx| {
             let mut multibuffer = MultiBuffer::new(0);
-            multibuffer.push_excerpts(buffer_1.clone(), [0..4], cx);
-            multibuffer.push_excerpts(buffer_2.clone(), [0..5], cx);
+            multibuffer.push_excerpts(
+                buffer_1.clone(),
+                [ExcerptRange {
+                    context: 0..4,
+                    primary: None,
+                }],
+                cx,
+            );
+            multibuffer.push_excerpts(
+                buffer_2.clone(),
+                [ExcerptRange {
+                    context: 0..5,
+                    primary: None,
+                }],
+                cx,
+            );
             multibuffer
         });
         let old_snapshot = multibuffer.read(cx).snapshot(cx);
@@ -3385,7 +3498,14 @@ mod tests {
         buffer_1.update(cx, |buffer, cx| buffer.edit([(4..4, "123")], cx));
         let excerpt_id_1 = multibuffer.update(cx, |multibuffer, cx| {
             multibuffer
-                .push_excerpts(buffer_1.clone(), [0..7], cx)
+                .push_excerpts(
+                    buffer_1.clone(),
+                    [ExcerptRange {
+                        context: 0..7,
+                        primary: None,
+                    }],
+                    cx,
+                )
                 .pop()
                 .unwrap()
         });
@@ -3397,7 +3517,24 @@ mod tests {
         let (excerpt_id_2, excerpt_id_3) = multibuffer.update(cx, |multibuffer, cx| {
             multibuffer.remove_excerpts([&excerpt_id_1], cx);
             let mut ids = multibuffer
-                .push_excerpts(buffer_2.clone(), [0..4, 6..10, 12..16], cx)
+                .push_excerpts(
+                    buffer_2.clone(),
+                    [
+                        ExcerptRange {
+                            context: 0..4,
+                            primary: None,
+                        },
+                        ExcerptRange {
+                            context: 6..10,
+                            primary: None,
+                        },
+                        ExcerptRange {
+                            context: 12..16,
+                            primary: None,
+                        },
+                    ],
+                    cx,
+                )
                 .into_iter();
             (ids.next().unwrap(), ids.next().unwrap())
         });
@@ -3437,7 +3574,15 @@ mod tests {
         let excerpt_id_5 = multibuffer.update(cx, |multibuffer, cx| {
             multibuffer.remove_excerpts([&excerpt_id_3], cx);
             multibuffer
-                .insert_excerpts_after(&excerpt_id_3, buffer_2.clone(), [5..8], cx)
+                .insert_excerpts_after(
+                    &excerpt_id_3,
+                    buffer_2.clone(),
+                    [ExcerptRange {
+                        context: 5..8,
+                        primary: None,
+                    }],
+                    cx,
+                )
                 .pop()
                 .unwrap()
         });
@@ -3584,7 +3729,10 @@ mod tests {
                             .insert_excerpts_after(
                                 &prev_excerpt_id,
                                 buffer_handle.clone(),
-                                [start_ix..end_ix],
+                                [ExcerptRange {
+                                    context: start_ix..end_ix,
+                                    primary: None,
+                                }],
                                 cx,
                             )
                             .pop()
@@ -3893,8 +4041,22 @@ mod tests {
         let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
         let group_interval = multibuffer.read(cx).history.group_interval;
         multibuffer.update(cx, |multibuffer, cx| {
-            multibuffer.push_excerpts(buffer_1.clone(), [0..buffer_1.read(cx).len()], cx);
-            multibuffer.push_excerpts(buffer_2.clone(), [0..buffer_2.read(cx).len()], cx);
+            multibuffer.push_excerpts(
+                buffer_1.clone(),
+                [ExcerptRange {
+                    context: 0..buffer_1.read(cx).len(),
+                    primary: None,
+                }],
+                cx,
+            );
+            multibuffer.push_excerpts(
+                buffer_2.clone(),
+                [ExcerptRange {
+                    context: 0..buffer_2.read(cx).len(),
+                    primary: None,
+                }],
+                cx,
+            );
         });
 
         let mut now = Instant::now();

crates/language/src/buffer.rs 🔗

@@ -49,7 +49,7 @@ lazy_static! {
 
 pub struct Buffer {
     text: TextBuffer,
-    file: Option<Box<dyn File>>,
+    file: Option<Arc<dyn File>>,
     saved_version: clock::Global,
     saved_mtime: SystemTime,
     language: Option<Arc<Language>>,
@@ -72,7 +72,7 @@ pub struct Buffer {
 pub struct BufferSnapshot {
     text: text::BufferSnapshot,
     tree: Option<Tree>,
-    path: Option<Arc<Path>>,
+    file: Option<Arc<dyn File>>,
     diagnostics: DiagnosticSet,
     diagnostics_update_count: usize,
     file_update_count: usize,
@@ -152,7 +152,7 @@ pub enum Event {
     Closed,
 }
 
-pub trait File {
+pub trait File: Send + Sync {
     fn as_local(&self) -> Option<&dyn LocalFile>;
 
     fn is_local(&self) -> bool {
@@ -306,7 +306,7 @@ impl Buffer {
     pub fn from_file<T: Into<Arc<str>>>(
         replica_id: ReplicaId,
         base_text: T,
-        file: Box<dyn File>,
+        file: Arc<dyn File>,
         cx: &mut ModelContext<Self>,
     ) -> Self {
         Self::build(
@@ -322,7 +322,7 @@ impl Buffer {
     pub fn from_proto(
         replica_id: ReplicaId,
         message: proto::BufferState,
-        file: Option<Box<dyn File>>,
+        file: Option<Arc<dyn File>>,
         cx: &mut ModelContext<Self>,
     ) -> Result<Self> {
         let buffer = TextBuffer::new(
@@ -403,7 +403,7 @@ impl Buffer {
         self
     }
 
-    fn build(buffer: TextBuffer, file: Option<Box<dyn File>>) -> Self {
+    fn build(buffer: TextBuffer, file: Option<Arc<dyn File>>) -> Self {
         let saved_mtime;
         if let Some(file) = file.as_ref() {
             saved_mtime = file.mtime();
@@ -438,7 +438,7 @@ impl Buffer {
         BufferSnapshot {
             text: self.text.snapshot(),
             tree: self.syntax_tree(),
-            path: self.file.as_ref().map(|f| f.path().clone()),
+            file: self.file.clone(),
             remote_selections: self.remote_selections.clone(),
             diagnostics: self.diagnostics.clone(),
             diagnostics_update_count: self.diagnostics_update_count,
@@ -496,7 +496,7 @@ impl Buffer {
         &mut self,
         version: clock::Global,
         mtime: SystemTime,
-        new_file: Option<Box<dyn File>>,
+        new_file: Option<Arc<dyn File>>,
         cx: &mut ModelContext<Self>,
     ) {
         self.saved_mtime = mtime;
@@ -550,7 +550,7 @@ impl Buffer {
 
     pub fn file_updated(
         &mut self,
-        new_file: Box<dyn File>,
+        new_file: Arc<dyn File>,
         cx: &mut ModelContext<Self>,
     ) -> Task<()> {
         let old_file = if let Some(file) = self.file.as_ref() {
@@ -1980,8 +1980,8 @@ impl BufferSnapshot {
         self.selections_update_count
     }
 
-    pub fn path(&self) -> Option<&Arc<Path>> {
-        self.path.as_ref()
+    pub fn file(&self) -> Option<&dyn File> {
+        self.file.as_deref()
     }
 
     pub fn file_update_count(&self) -> usize {
@@ -1994,7 +1994,7 @@ impl Clone for BufferSnapshot {
         Self {
             text: self.text.clone(),
             tree: self.tree.clone(),
-            path: self.path.clone(),
+            file: self.file.clone(),
             remote_selections: self.remote_selections.clone(),
             diagnostics: self.diagnostics.clone(),
             selections_update_count: self.selections_update_count,

crates/project/src/project.rs 🔗

@@ -4013,7 +4013,7 @@ impl Project {
                                 })
                                 .log_err();
                         }
-                        buffer.file_updated(Box::new(new_file), cx).detach();
+                        buffer.file_updated(Arc::new(new_file), cx).detach();
                     }
                 });
             } else {
@@ -4565,7 +4565,7 @@ impl Project {
                 .and_then(|b| b.upgrade(cx))
                 .ok_or_else(|| anyhow!("no such buffer"))?;
             buffer.update(cx, |buffer, cx| {
-                buffer.file_updated(Box::new(file), cx).detach();
+                buffer.file_updated(Arc::new(file), cx).detach();
             });
             Ok(())
         })
@@ -5089,8 +5089,8 @@ impl Project {
                                     anyhow!("no worktree found for id {}", file.worktree_id)
                                 })?;
                             buffer_file =
-                                Some(Box::new(File::from_proto(file, worktree.clone(), cx)?)
-                                    as Box<dyn language::File>);
+                                Some(Arc::new(File::from_proto(file, worktree.clone(), cx)?)
+                                    as Arc<dyn language::File>);
                             buffer_worktree = Some(worktree);
                             Ok::<_, anyhow::Error>(())
                         })?;

crates/project/src/worktree.rs 🔗

@@ -519,7 +519,7 @@ impl LocalWorktree {
             let (file, contents) = this
                 .update(&mut cx, |t, cx| t.as_local().unwrap().load(&path, cx))
                 .await?;
-            Ok(cx.add_model(|cx| Buffer::from_file(0, contents, Box::new(file), cx)))
+            Ok(cx.add_model(|cx| Buffer::from_file(0, contents, Arc::new(file), cx)))
         })
     }
 
@@ -648,7 +648,7 @@ impl LocalWorktree {
             };
 
             buffer_handle.update(&mut cx, |buffer, cx| {
-                buffer.did_save(version, file.mtime, Some(Box::new(file)), cx);
+                buffer.did_save(version, file.mtime, Some(Arc::new(file)), cx);
             });
 
             Ok(())

crates/search/src/buffer_search.rs 🔗

@@ -485,7 +485,7 @@ impl BufferSearchBar {
                         );
                     } else {
                         for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) {
-                            let excerpt_range = excerpt.range.to_offset(&excerpt.buffer);
+                            let excerpt_range = excerpt.range.context.to_offset(&excerpt.buffer);
                             let rope = excerpt.buffer.as_rope().slice(excerpt_range.clone());
                             ranges.extend(query.search(&rope).await.into_iter().map(|range| {
                                 let start = excerpt

crates/theme/src/theme.rs 🔗

@@ -454,6 +454,7 @@ pub struct Editor {
     pub code_actions_indicator: Color,
     pub unnecessary_code_fade: f32,
     pub hover_popover: HoverPopover,
+    pub jump_icon: Interactive<IconButton>,
 }
 
 #[derive(Clone, Deserialize, Default)]
@@ -473,7 +474,6 @@ pub struct DiagnosticHeader {
     pub code: ContainedText,
     pub text_scale_factor: f32,
     pub icon_width_factor: f32,
-    pub jump_icon: Interactive<IconButton>,
 }
 
 #[derive(Clone, Deserialize, Default)]

styles/src/styleTree/editor.ts 🔗

@@ -101,14 +101,6 @@ export default function editor(theme: Theme) {
       background: backgroundColor(theme, 300),
       iconWidthFactor: 1.5,
       textScaleFactor: 0.857, // NateQ: Will we need dynamic sizing for text? If so let's create tokens for these.
-      jumpIcon: {
-        color: iconColor(theme, "primary"),
-        iconWidth: 10,
-        buttonWidth: 10,
-        hover: {
-          color: iconColor(theme, "active")
-        }
-      },
       border: border(theme, "secondary", {
         bottom: true,
         top: true,
@@ -147,6 +139,14 @@ export default function editor(theme: Theme) {
     invalidInformationDiagnostic: diagnostic(theme, "muted"),
     invalidWarningDiagnostic: diagnostic(theme, "muted"),
     hover_popover: hoverPopover(theme),
+    jumpIcon: {
+      color: iconColor(theme, "primary"),
+      iconWidth: 10,
+      buttonWidth: 10,
+      hover: {
+        color: iconColor(theme, "active")
+      }
+    },
     syntax,
   };
 }